diff --git a/ql/pricingengines/basket/fd2dblackscholesvanillaengine.cpp b/ql/pricingengines/basket/fd2dblackscholesvanillaengine.cpp index e09d8239d36..007b0746a2b 100644 --- a/ql/pricingengines/basket/fd2dblackscholesvanillaengine.cpp +++ b/ql/pricingengines/basket/fd2dblackscholesvanillaengine.cpp @@ -53,6 +53,7 @@ namespace QuantLib { // 1. Payoff const ext::shared_ptr payoff = ext::dynamic_pointer_cast(arguments_.payoff); + QL_REQUIRE(payoff, "basket payoff expected"); // 2. Mesher const Time maturity = p1_->time(arguments_.exercise->lastDate()); diff --git a/test-suite/basketoption.cpp b/test-suite/basketoption.cpp index 1c71da9adf4..c4f107226ae 100644 --- a/test-suite/basketoption.cpp +++ b/test-suite/basketoption.cpp @@ -3033,6 +3033,69 @@ BOOST_AUTO_TEST_CASE(testGaussianCopulaSpreadEngineParityAcrossVols) { } } +BOOST_AUTO_TEST_CASE(testFd2dBlackScholesVanillaEngineRejectsNonBasketPayoff) { + BOOST_TEST_MESSAGE( + "Testing Fd2dBlackScholesVanillaEngine's guard against a " + "non-basket payoff..."); + + // Fd2dBlackScholesVanillaEngine::calculate() dynamic_pointer_casts + // arguments_.payoff to BasketPayoff but, unlike its sibling + // FdNdimBlackScholesVanillaEngine (which QL_REQUIREs the cast to + // succeed), used to have no check at all: a null cast would silently + // flow into FdmLogBasketInnerValue::innerValue()'s unconditional + // (*payoff_)(x) dereference. That is a null-pointer dereference + // (undefined behavior), not a catchable exception, so it cannot safely + // be exercised pre-fix in an automated test -- this test instead + // demonstrates the fixed engine's behavior: driving it directly through + // its own PricingEngine::getArguments()/calculate() interface (the same + // entry point BasketOption itself uses, but without BasketOption's + // constructor-level type safety) with a non-basket payoff throws a + // clean QuantLib::Error instead. + + DayCounter dc = Actual360(); + Date today = Settings::instance().evaluationDate(); + + auto spot1 = ext::make_shared(100.0); + auto spot2 = ext::make_shared(100.0); + ext::shared_ptr qTS1 = flatRate(today, 0.0, dc); + ext::shared_ptr qTS2 = flatRate(today, 0.0, dc); + ext::shared_ptr rTS = flatRate(today, 0.05, dc); + ext::shared_ptr volTS1 = flatVol(today, 0.2, dc); + ext::shared_ptr volTS2 = flatVol(today, 0.2, dc); + + auto p1 = ext::make_shared( + Handle(spot1), Handle(qTS1), + Handle(rTS), Handle(volTS1)); + auto p2 = ext::make_shared( + Handle(spot2), Handle(qTS2), + Handle(rTS), Handle(volTS2)); + + auto exercise = ext::make_shared(today + Period(1, Years)); + + ext::shared_ptr engine = + ext::make_shared(p1, p2, 0.0, 20, 20, 10); + + // Sanity check: a genuine basket payoff, through the normal BasketOption + // path, still prices fine with the fix in place. + BasketOption basketOption( + ext::make_shared( + ext::make_shared(Option::Call, 100.0)), + exercise); + basketOption.setPricingEngine(engine); + BOOST_CHECK_NO_THROW(basketOption.NPV()); + + // Now drive the same engine instance directly, bypassing BasketOption's + // constructor-level BasketPayoff requirement, with a plain (non-basket) + // payoff -- exactly the misuse the sibling N-dim engine's own guard + // exists to catch. + auto* arguments = dynamic_cast(engine->getArguments()); + BOOST_REQUIRE(arguments != nullptr); + arguments->payoff = ext::make_shared(Option::Call, 100.0); + arguments->exercise = exercise; + + BOOST_CHECK_THROW(engine->calculate(), Error); +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END()