Skip to content

fix: prevent near-zero division DoS in SpreadCloseSwapService#766

Open
ffulb wants to merge 1 commit intoIPOR-Labs:mainfrom
ffulb:fix/spread-close-swap-division-dos
Open

fix: prevent near-zero division DoS in SpreadCloseSwapService#766
ffulb wants to merge 1 commit intoIPOR-Labs:mainfrom
ffulb:fix/spread-close-swap-division-dos

Conversation

@ffulb
Copy link
Copy Markdown

@ffulb ffulb commented Apr 3, 2026

Security Fix — Critical DoS Vulnerability

Vulnerability

In SpreadCloseSwapService.updateTimeWeightedNotionalOnClose(), when closing the last swap (nextSwapId == 0), the time-weighted notional is recalculated with:

(actualTimeWeightedNotionalToSave * tenorInSeconds) / (tenorInSeconds - swapTimePast)

If swapTimePast approaches tenorInSeconds (e.g., a swap opened near tenor expiry), the denominator approaches 1. This causes the result to overflow uint96 when packed by SafeCast.toUint96() in SpreadStorageLibs.saveTimeWeightedNotionalForAssetAndTenor().

Impact

  • Permanent DoS of the spread storage for the affected asset/tenor pair
  • All subsequent swap close operations revert
  • Users with open swaps cannot close them — collateral permanently locked
  • Attacker can brick all 9 asset/tenor combinations (DAI/USDC/USDT x 28/60/90 days)

Attack Scenario

  1. Attacker opens a swap on 28-day tenor at time T
  2. Wait until block.timestamp - T ≈ tenorInSeconds - 1
  3. Close another swap where nextSwapId == 0
  4. swapTimePast ≈ tenorInSeconds - 1, denominator = 1
  5. 7.9e46 * 2419200 / 1 = 1.9e53 → after /1e18 = 1.9e35 → exceeds uint96 max (7.9e28)
  6. SafeCast.toUint96() reverts permanently

Fix

Enforce minimum denominator of 1% of tenor length, preventing the amplification factor from exceeding 100x.

Additional Findings

I have 5 additional findings (2 High, 2 Medium, 1 Low) in the spread calculation subsystem:

  • High: Unenforced invariant timeOfLastUpdate > closedSwap.openSwapTimestamp causes underflow
  • High: uint96 truncation precision loss at high TVL
  • Medium: Empty pool sets lastUpdateTime=0, enabling spread manipulation
  • Medium: Rounding asymmetry in IporMath biases demand spread
  • Low: uint32 timestamp overflow in 2106

Happy to share details. Contact via GitHub.

Wallet for bounty: 0xd67c6444cD3617Bd6D0A52aCE0E4aA29127cEA68

When closing the last swap in a linked list (nextSwapId == 0),
the time-weighted notional is recalculated with division by
(tenorInSeconds - swapTimePast). If swapTimePast approaches
tenorInSeconds, the denominator approaches 1, causing the result
to overflow uint96 in SpreadStorageLibs.saveTimeWeightedNotionalForAssetAndTenor().

This permanently bricks the spread storage for the affected
asset/tenor pair, blocking all subsequent swap operations.

Fix: enforce a minimum denominator of 1% of the tenor length.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant