purchase of at-the-expiry option is possible creating free option opportunity for people to bond for 1 week.
purchase is a function for the rpdxV2Core to purchase a perpetual atlantic put which can be settled into the collateral whenever it's price becomes ITM. Premium is calculated based on the timeToExpiry (with ref to blackScholes model).
Here is the workflow:
purchase uses timeToExpiry to calculate the premium needed for buying an option, which is the difference between nextFundingPaymentTimestamp() to the current block timestamp.
nextFundingPaymentTimestamp = genesis + (latestFundingPaymentPointer * fundingDuration), where fundingDuration is a fixed 7 days, genesis is immutably set upon construction.
latestFundingPaymentPointer is updated every time the block.timestamp >= nextFundingPaymentTimestamp(). There is a possibility that block.timestamp == nextFundingPaymentTimestamp()
When such things happen, according to black scholes model, the premium required to purchase a put option with zero time to expiry is 0. Essentially if an attacker can always choose to bond/purchase option at this time, such that the Vault collect no premium for that initial epoch from the V2Core even it keeps writing options.
Although the vault would collect funding from the V2Core according to pre-defined funding rate, however, the payFunding is only callable by V2Core itself, and only triggerable by provideFunding at V2Core which is also an admin function. This control flow means if the admin forget or miss the funding call then the premium would never be collected.
function purchase(
uint256 amount,
address to
)
external
nonReentrant
onlyRole(RDPXV2CORE_ROLE)
returns (uint256 premium, uint256 tokenId)
{
_whenNotPaused();
_validate(amount > 0, 2);
updateFunding();
uint256 currentPrice = getUnderlyingPrice(); // price of underlying wrt collateralToken
uint256 strike = roundUp(currentPrice - (currentPrice / 4)); // 25% below the current price
IPerpetualAtlanticVaultLP perpetualAtlanticVaultLp = IPerpetualAtlanticVaultLP(
addresses.perpetualAtlanticVaultLP
);
// Check if vault has enough collateral to write the options
uint256 requiredCollateral = (amount * strike) / 1e8;
_validate(
requiredCollateral <= perpetualAtlanticVaultLp.totalAvailableCollateral(),
3
);
uint256 timeToExpiry = nextFundingPaymentTimestamp() - block.timestamp;
// Get total premium for all options being purchased
premium = calculatePremium(strike, amount, timeToExpiry, 0);
...
purchase can be called by bond -> _purchaseOption at the V2Core
function _purchaseOptions(
uint256 _amount
) internal returns (uint256 premium) {
/**
* Purchase options and store ERC721 option id
* Note that the amount of options purchased is the amount of rDPX received
* from the user to sufficiently collateralize the underlying DpxEth stored in the bond
**/
uint256 optionId;
(premium, optionId) = IPerpetualAtlanticVault(
addresses.perpetualAtlanticVault
).purchase(_amount, address(this));
optionsOwned[optionId] = true;
reserveAsset[reservesIndex["WETH"]].tokenBalance -= premium;
}
Impact: free option purchase + exercise for ITM strikes at the time of expiry possible. Since settle at RdpxV2Core can only be called by admin, thus the attack can only completed if any user called purchase of option through bondWithDelegate and the the admin settle the option within the same block (time == block.timestamp).
privileged call + severe impact = medium risk issue
Lines of code
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L283-L286
Vulnerability details
Impact
purchase of at-the-expiry option is possible creating free option opportunity for people to bond for 1 week.
purchase is a function for the rpdxV2Core to purchase a perpetual atlantic put which can be settled into the collateral whenever it's price becomes ITM. Premium is calculated based on the timeToExpiry (with ref to blackScholes model).
Here is the workflow:
purchase uses timeToExpiry to calculate the premium needed for buying an option, which is the difference between
nextFundingPaymentTimestamp()
to the current block timestamp.nextFundingPaymentTimestamp =
genesis + (latestFundingPaymentPointer * fundingDuration)
, where fundingDuration is a fixed 7 days, genesis is immutably set upon construction.latestFundingPaymentPointer is updated every time the
block.timestamp >= nextFundingPaymentTimestamp()
. There is a possibility thatblock.timestamp == nextFundingPaymentTimestamp()
When such things happen, according to black scholes model, the premium required to purchase a put option with zero time to expiry is 0. Essentially if an attacker can always choose to bond/purchase option at this time, such that the Vault collect no premium for that initial epoch from the V2Core even it keeps writing options.
Although the vault would collect funding from the V2Core according to pre-defined funding rate, however, the payFunding is only callable by V2Core itself, and only triggerable by provideFunding at V2Core which is also an admin function. This control flow means if the admin forget or miss the funding call then the premium would never be collected.
purchase can be called by bond -> _purchaseOption at the V2Core
_purchaseOption
Impact: free option purchase + exercise for ITM strikes at the time of expiry possible. Since
settle
at RdpxV2Core can only be called by admin, thus the attack can only completed if any user called purchase of option throughbondWithDelegate
and the the admin settle the option within the same block (time == block.timestamp).privileged call + severe impact = medium risk issue
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L283
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L463
Proof of Concept
Tools Used
Recommended Mitigation Steps
Consider revoking the ability to purchase at the expiry time.
Assessed type
Invalid Validation