`payOff()` underflow in `OperationalTreasury` locks assets permanently
The payOff() function in the OperationalTreasury contract calculates feesUSD based on the full position size and current price, without considering the actual profit (pnl).
Severity
Impact: High
Likelihood: Medium
Description
The payOff() function in the OperationalTreasury contract calculates feesUSD based on the full position size and current price, without considering the actual profit (pnl). Specifically:
feesUSD = positionAmount × FeePercentage × CurrentPricepnl = positionAmount × (CurrentPrice - StrikePrice)
This means that for positions where CurrentPrice is only slightly above StrikePrice, the calculated feesUSD may exceed the pnl. In such cases,the netAmount = pnl - feesUSD line will underflow:
function payOff(uint256 positionID, address account) external override nonReentrant whenNotPaused {--snip--@2> IPositionInteraction(baseSetUp.pool).exerciseOrLiquidatePosition(close);uint256 feesUSD = _calculateFeesUSD(uAsset, uint256(amount)).toDecimals(18, baseSetUp.token.decimals());@1> uint256 netAmount = pnl - feesUSD;// fees deduced from PnLstrg.ledger.collectedFees[address(baseSetUp.token)] += feesUSD;--snip--}
When payOff() reverts due to the underflow at @1>, the call to exerciseOrLiquidatePosition() at @2> is not executed. As a result, the underlying assets in the pool remain locked forever.
Recommendations
Cap the fees to be no greater than the actual pnl to prevent underflow and allow position closure:
uint256 feesUSD = _calculateFeesUSD(...);if (feesUSD > pnl) {feesUSD = pnl;}uint256 netAmount = pnl - feesUSD;
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.