Insufficient liquidity checks for `PUT` may cause exercise failures
The protocol checks total stablecoin value when opening PUT positions and processing stablecoin withdrawals.
Severity
Impact: High
Likelihood: Medium
Description
The protocol checks total stablecoin value when opening PUT positions and processing stablecoin withdrawals. However, payouts during position exercise require a specific stablecoin (pos.buyToken) to be available. This discrepancy can cause PUT positions to become unexercisable even when the overall pool appears solvent.
Consider the following scenarios: Assume:
OperationalTreasury.baseSetUp.token:USDC(buyToken)- Pool's stablecoin:
USDC(listed & reward),USDXL(listed) - Stable coin price is 1:1
Scenario 1: PUT position opens with 0 reserved for its payout token
Assume:
poolAmount[USDC]= 0 USDC -> 0 USDpoolAmount[USDXL]= 3500 USDXL -> 3500 USD_getPoolValue(true, false)= 0 + 3500 = 3500 USD
- User opens
PUTposition, resulting inlockedUSD= 3500 USD- The check passes:
availableStable = 3500 ≥ lockedUSD:0 + pos.sizeUSD: 3500 - This allows to create the
PUTposition that will payUSDCif position is profit but there are noUSDCin the pool.
- The check passes:
function _checkEnoughLiquidity(//...) internal pure {if (pos.opType == OptionType.CALL) {--- SNIPPED ---} else if (pos.opType == OptionType.PUT) {@> if (lockedUSD + pos.sizeUSD > availableStable) {revert NotEnoughLiquidityPut(pos.sizeUSD, availableStable, lockedUSD);}}}
- User attempts to exercise their
PUTposition, but if at that time thepoolAmount[USDC]still holds 0USDC, the_payout()reverts.
function _payout(Position memory pos, PositionClose memory close) internal returns (uint256 pnl) {--- SNIPPED ---@> strg.ledger.state.poolAmount[pos.buyToken] -= pnl;_doTransferOut(pos.buyToken, msg.sender, pnl);}
Scenario 2: LP withdrawal drains payout token after the position opened
Assume:
poolAmount[USDC]= 2000 USDC -> 2000 USDpoolAmount[USDXL]= 3500 USDXL -> 3500 USD_getPoolValue(true, false)= 2000 + 3500 = 5500 USD
- User opens
PUTposition, resulting inlockedUSD= 3500 USD- The check passes:
availableStable = 5500 ≥ lockedUSD:0 + pos.sizeUSD: 3500
- The check passes:
- Liquidity provider removes their liquidity and requests token output as
USDC- LP amount worth 2000
USDCequivalent _checkWithdrawlImpact()checks:newUSDValuation = 5500 - 2000 = 3500 USD, which isnewUSDValuation: 3500 >= lockedUSD: 3500- The process allows to withdraw 2000
USDCamount poolAmount[USDC]= 2000 - 2000 = 0USDC
- LP amount worth 2000
function _checkWithdrawlImpact(//...) internal view {--- SNIPPED ---else if (strg.setUp.assets.stablecoins.contains(_token)) {uint8 decimals = IERC20Metadata(_token).decimals();uint256 withdrawUSD = (_amount * _assetPrice).toDecimals(decimals + 8, 18);@> uint256 newUSDValuation = _getPoolValue(true, false) - withdrawUSD;@> if (newUSDValuation < strg.ledger.state.lockedUSD) revert PoolErrors.InsufficientPoolAmount(_token);}}
- User attempts to exercise their
PUTposition, butpoolAmount[USDC]= 0USDC, so the_payout()reverts.
Recommendation
Track per-buyToken (stablecoin) locked amounts and validate against individual balances in both PUT position opening and withdrawal impact checks.
Report provenance
Published by Pashov Audit Group, with Carlos (Bloqarl) participating as a contractor. The source report states that its fixes review was not fully completed; no final remediation disposition is recorded for this finding. Read the full report.