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

The balance check and slippage protection happen after claiming tokens from the transmuter, creating a window where funds are vulnerable.

Summary

In the claimAndSwap function across all strategy implementations

function claimAndSwap(uint256 _amountClaim, uint256 _minOut, uint256 routeNumber) external onlyKeepers {
// @audit-issue Incorrect accounting: balanceDeployed() includes underlying tokens in total, but minOut check only considers asset balance
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
router.exchange(
routes[_routeNumber],
swapParams[_routeNumber],
_amountClaim,
_minOut,
pools[_routeNumber],
address(this)
);
uint256 balAfter = asset.balanceOf(address(this));
require((balAfter - balBefore) >= _minOut, "Slippage too high");
transmuter.deposit(asset.balanceOf(address(this)), address(this));
}

The vulnerability stems from inconsistent accounting between the balanceDeployed() function and the slippage checks in claimAndSwap. While balanceDeployed() includes both underlying tokens (WETH) and asset tokens (alETH) in its total:

function balanceDeployed() public view returns (uint256) {
return transmuter.getUnexchangedBalance(address(this)) + underlying.balanceOf(address(this)) + asset.balanceOf(address(this));
}

The slippage protection in claimAndSwap only checks the asset balance difference

require((balAfter - balBefore) >= _minOut, "Slippage too high");

This shows that while the asset balance check passes, the total strategy value (balanceDeployed) can decrease, violating the intended that each swap should increase the total value by at least minOut.

Vulnerability Details

In StrategyMainnet.sol's claimAndSwap

function claimAndSwap(uint256 _amountClaim, uint256 _minOut, uint256 _routeNumber) external onlyKeepers {
// @audit-issue The minOut check is flawed - it compares minOut to _amountClaim but this doesn't guarantee profitability
// because balanceDeployed() includes both underlying and asset tokens
require(_minOut > _amountClaim, "minOut too low");
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
router.exchange(...);
uint256 balAfter = asset.balanceOf(address(this));
require((balAfter - balBefore) >= _minOut, "Slippage too high");
}

The root cause is in the profitability check logic

  1. The strategy tracks total value through balanceDeployed():

function balanceDeployed() public view returns (uint256) {
// @audit-issue This includes both underlying and asset tokens
return transmuter.getUnexchangedBalance(address(this)) +
underlying.balanceOf(address(this)) +
asset.balanceOf(address(this));
}

The bug occurs because

  1. When claiming WETH from transmuter, it's counted in balanceDeployed()

  2. When swapping to alETH, the WETH is removed but counted again as alETH

  3. The minOut check require(_minOut > _amountClaim) doesn't account for this double-counting

Impact

A keeper could execute a swap that appears profitable (passes minOut check) but actually reduces the strategy's total value. For example:

  1. Claim 100 WETH (_amountClaim = 100)

  2. Set minOut to 101 alETH (passes require(_minOut > _amountClaim))

  3. Swap executes, but total strategy value decreases because the 100 WETH was already counted in balanceDeployed()

Recommendations

// StrategyMainnet.sol, StrategyOp.sol, StrategyArb.sol
function claimAndSwap(uint256 _amountClaim, uint256 _minOut, uint256 _routeNumber) external onlyKeepers {
- transmuter.claim(_amountClaim, address(this));
- uint256 balBefore = asset.balanceOf(address(this));
- require(_minOut > _amountClaim, "minOut too low");
+ // @audit-fix Track total value before claim to ensure true profitability
+ uint256 totalBefore = balanceDeployed();
+
+ transmuter.claim(_amountClaim, address(this));
+
+ // @audit-fix Require minimum profit threshold over total value
+ require(_minOut > _amountClaim + PROFIT_THRESHOLD, "Insufficient profit margin");
router.exchange(
routes[_routeNumber],
swapParams[_routeNumber],
_amountClaim,
_minOut,
pools[_routeNumber],
address(this)
);
- uint256 balAfter = asset.balanceOf(address(this));
- require((balAfter - balBefore) >= _minOut, "Slippage too high");
+ // @audit-fix Verify total strategy value increased by required amount
+ require(balanceDeployed() >= totalBefore + _minOut, "Trade not profitable");
transmuter.deposit(asset.balanceOf(address(this)), address(this));
}
+ // @audit-fix Add configurable profit threshold
+ uint256 public constant PROFIT_THRESHOLD = 1e16; // 1% minimum profit
Updates

Lead Judging Commences

inallhonesty Lead Judge
6 months ago

Appeal created

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

Support

FAQs

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