Griefing (no profit motive for attacker, but damage to users or protocol)
Contract fails to deliver promised returns, but doesn't lose value
Description
Brief / Intro
The refactored selfCloseExitTo function in CollateralPool.sol is vulnerable to a griefing attack where an attacker frontruns a victim’s self-close exit by calling exit to reduce pool collateral. This manipulation raises the required fAsset amount for the victim’s selfCloseExitTo call, causing the victim’s transaction to revert due to insufficient allowance.
Vulnerability details
selfCloseExitTo computes a required f-asset amount using the current totalCollateral and then checks:
Because requiredFAssets depends on totalCollateral, which is mutable and can be decreased by other users’ exit calls, the computed requiredFAssets can increase between the time the victim reads it off-chain and approves that amount, and the time their selfCloseExitTo transaction executes. An attacker can frontrun by calling exit first (reducing totalCollateral), increasing requiredFAssets, causing the victim’s transaction to revert at the allowance check.
Two code branches in _getFAssetRequiredToNotSpoilCR
1
Pool is above exitCR
Code excerpt:
Behavior: decreasing totalCollateral (N) decreases the subtracted term → increases resultWithoutRounding → increases requiredFAssets. Thus an attacker withdrawing collateral increases the f-asset requirement for a pending self-close exit.
2
Pool at or below exitCR
Code excerpt:
Behavior: decreasing totalCollateral (N) makes the fraction natShare / totalCollateral larger → increases required f-assets as well. Again, an attacker reducing total collateral raises the requirement for the same natShare.
Attack flow
Victim deposits into the pool and obtains pool tokens (tokenBalance).
Off-chain, the victim computes initialRequired = collateralPool.fAssetRequiredForSelfCloseExit(tokenBalance) and approves initialRequired to collateralPool.
Attacker observes the mempool and frontruns the victim by calling collateralPool.exit(attackerTokens), reducing totalCollateral.
The victim’s approved allowance is now too small relative to the new requiredFAssets computed at execution time.
The victim’s selfCloseExitTo transaction reverts at the allowance check:
revert reason: FAssetAllowanceTooSmall
Impact details
Denial of exit — Victim’s transaction reverts due to insufficient allowance, preventing them from closing their position and retrieving collateral.
Temporary fund lock — Victim’s funds are effectively locked in the pool until they recompute and approve a higher fAsset amount.
Repeated exploitation — An attacker could repeatedly frontrun multiple victims, causing widespread griefing and user frustration.
Operational disruption — Users may be forced to over-approve fAssets to avoid reverts, increasing exposure.
Proof of Concept
This PoC demonstrates how an attacker can block a victim’s selfCloseExit by manipulating the pool’s collateral ratio (CR). Add the following test to CollateralPool.ts:
// f-asset required for CR to stay above exitCR (might not be needed)
// solve (N - n) / (p / q (F - f)) >= cr get f = max(0, F - q (N - n) / (p cr))
resultWithoutRounding = MathUtils.subOrZero(backedFAssets,
assetPrice.div * (totalCollateral - _natShare) * SafePct.MAX_BIPS / (assetPrice.mul * exitCR));
// f-asset that preserves pool CR (assume poolNatBalance >= natShare > 0)
// solve (N - n) / (F - f) = N / F get f = n F / N
resultWithoutRounding = backedFAssets.mulDivRoundUp(_natShare, totalCollateral);
$ yarn hardhat test ./test/unit/collateralPool/CollateralPool.ts
yarn run v1.22.22
Contract: CollateralPool.sol; test/unit/collateralPool/CollateralPool.ts; Collateral pool basic tests
Testing the original stuck funds bug and its fix
Initial required FAssets for victim exit: 8333333334000000000
New required FAssets after attacker manipulation: 10000000000000000000
✔ should demonstrate griefing: attacker raises CR via exit and blocks victim's selfCloseExit (48ms)
1 passing (934ms)