DeFiFoundrySolidity
16,653 OP
View results
Submission Details
Severity: medium
Invalid

Balance check happens after claim but before swap, allowing manipulation

Summary

There's accounting mismatch between the balanceDeployed() function of StrategyOp.sol and the actual deposit process.

function balanceDeployed() public view returns (uint256) {
// @check Accounting includes underlying token balance which should not be counted as deployed
return transmuter.getUnexchangedBalance(address(this)) + underlying.balanceOf(address(this)) + asset.balanceOf(address(this));
}

The balanceDeployed() function incorrectly includes the underlying token balance in its calculation. When _deployFunds() is called, it deposits the asset (alETH) into the transmuter, but balanceDeployed() also counts any WETH balance. This creates a situation where:

  1. The actual deployed amount is only tracked in transmuter.getUnexchangedBalance()

  2. But balanceDeployed() adds the underlying token balance which is not actually deployed

  3. This breaks the specification's requirement that deployment must increase balance by exactly the input amount

The vulnerability exists because of incorrect accounting assumptions about what constitutes "deployed" funds. The underlying token (WETH) sitting in the contract should not be considered deployed since it hasn't been put to work in the transmuter.

This can lead to:

  • Incorrect total value calculations

  • Misleading deployment status reporting

  • Potential manipulation of strategy metrics

Vulnerability Details

The issue is located within the implementation of the claimAndSwap function across all three strategy variants (StrategyOp.sol, StrategyMainnet.sol, and StrategyArb.sol).

In StrategyOp.sol#claimAndSwap

function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IVeloRouter.route[] calldata _path) external onlyKeepers {
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
// @check: Balance check happens after claim but before swap, allowing manipulation
_swapUnderlyingToAsset(_amountClaim, _minOut, _path);
uint256 balAfter = asset.balanceOf(address(this));
require((balAfter - balBefore) >= _minOut, "Slippage too high");
transmuter.deposit(asset.balanceOf(address(this)), address(this));
}

The balance check is performed incorrectly in the sequence of operations. The function:

  1. Claims WETH from transmuter

  2. Records alETH balance

  3. Performs swap

  4. Verifies balance change

This creates a sudden vulnerability because:

  • The initial balance check happens after claiming but before swapping

  • An attacker could manipulate the contract's alETH balance between these steps

  • The slippage check becomes ineffective as it's based on potentially manipulated values

Impact across implementations:

function claimAndSwap(uint256 _amountClaim, uint256 _minOut, uint256 _routeNumber) external onlyKeepers {
// @check Same vulnerability with Curve Router implementation
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
router.exchange(...);
}
function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IRamsesRouter.route[] calldata _path) external onlyKeepers {
// @check Same vulnerability with Ramses Router implementation
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
_swapUnderlyingToAsset(_amountClaim, _minOut, _path);
}

The vulnerability affects all three chains (Optimism, Ethereum, Arbitrum) and could lead to:

  • Loss of funds through manipulated swaps

  • Incorrect slippage protection

  • Potential sandwich attacks during the swap process

Impact

The bug is caused by incorrect sequencing of operations in the claimAndSwap function across all strategy variants. The root issue originates from taking balance measurements at the wrong time during the swap process.

The problematic sequence:

  1. Claims WETH from transmuter

  2. Takes initial alETH balance measurement

  3. Performs swap

  4. Takes final balance measurement

  5. Checks slippage

This sequence creates a window where the balance check can be manipulated because:

  • The initial balance is measured after claiming but before swapping

  • The final balance check only verifies the difference between these two points

  • The actual swap outcome could be manipulated while still passing the slippage check

The core issue is that the balance checks don't properly encapsulate the entire operation from start to finish. Instead, they only measure a subset of the transaction window, making the slippage protection ineffective.

This vulnerability exists because the code assumes that measuring balances around just the swap operation is sufficient for slippage protection. However, this assumption is incorrect as it doesn't account for the entire state change from the initial pre-claim state to the final post-swap state.

The issue affects all three strategy variants (StrategyOp.sol, StrategyMainnet.sol, StrategyArb.sol) since they all implement the same flawed balance checking pattern, just with different DEX routers (Velodrome, Curve, Ramses).

Recommendations

// StrategyOp.sol, StrategyMainnet.sol, StrategyArb.sol
function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IVeloRouter.route[] calldata _path) external onlyKeepers {
- transmuter.claim(_amountClaim, address(this));
- uint256 balBefore = asset.balanceOf(address(this));
- _swapUnderlyingToAsset(_amountClaim, _minOut, _path);
- uint256 balAfter = asset.balanceOf(address(this));
- require((balAfter - balBefore) >= _minOut, "Slippage too high");
- transmuter.deposit(asset.balanceOf(address(this)), address(this));
+ // @Recommendation - Take initial balance before any operations
+ uint256 initialBalance = asset.balanceOf(address(this));
+
+ // @Recommendation - Record initial WETH balance to ensure claim amount
+ uint256 initialWeth = underlying.balanceOf(address(this));
+
+ // @Recommendation - Perform claim
+ transmuter.claim(_amountClaim, address(this));
+
+ // @Recommendation - Verify claimed amount
+ require(
+ underlying.balanceOf(address(this)) == initialWeth + _amountClaim,
+ "Claim amount mismatch"
+ );
+
+ // @Recommendation - Perform swap with strict slippage check
+ _swapUnderlyingToAsset(_amountClaim, _minOut, _path);
+
+ // @Recommendation - Verify final balance reflects minimum expected gain
+ uint256 finalBalance = asset.balanceOf(address(this));
+ require(
+ finalBalance >= initialBalance + _minOut,
+ "Insufficient output amount"
+ );
+
+ // @Recommendation - Deposit only the newly acquired assets
+ uint256 newAssets = finalBalance - initialBalance;
+ transmuter.deposit(newAssets, address(this));
}
Updates

Appeal created

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.