Summary
In StrategyMainnet.sol#claimAndSwap
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");
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));
}
from incorrect balance tracking in the profitability check. The function tracks only the asset balance change (asset.balanceOf(address(this))) but fails to account for the total deployed balance which includes:
This creates a discrepancy between what verifies (balanceDeployed()) and what the contract actually checks, allowing trades that appear profitable in terms of asset balance but decrease the total strategy value.
Vulnerability Details
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");
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 bug exists because of incorrect accounting in the profitability check. When transmuter.claim() is called, it reduces the strategy's position in the transmuter (unexchangedBalance) and provides WETH (underlying). The subsequent swap profitability check only verifies:
_minOut > _amountClaim
(balAfter - balBefore) >= _minOut
However, this fails to account for the total value change including the transmuter position reduction.
Impact Across Chains:
Ethereum: Affects Curve swaps through CurveRouterNG
Optimism: Affects Velodrome swaps
Arbitrum: Affects Ramses swaps
The vulnerability allows unprofitable trades because:
-
Initial state: Strategy has X alETH in transmuter
-
Keeper calls claimAndSwap:
Claims Y WETH from transmuter (reduces transmuter position by Y alETH)
Swaps Y WETH for Z alETH where Z > Y but Z < Y + fees
Trade appears profitable (Z > Y) but total strategy value decreases
Impact
Looking at the balanceDeployed() function across all strategies
function balanceDeployed() public view returns (uint256) {
return transmuter.getUnexchangedBalance(address(this)) + underlying.balanceOf(address(this)) + asset.balanceOf(address(this));
}
The issue stems from the claimAndSwap() function only checking asset balance changes
function claimAndSwap(uint256 _amountClaim, uint256 _minOut, uint256 _routeNumber) external onlyKeepers {
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");
}
Claiming from the transmuter reduces the strategy's position value (unexchangedBalance), but the profitability check only validates the increase in asset balance from the swap. This creates a scenario where:
Total Value Before = unexchangedBalance + 0 + 0
After Claim = (unexchangedBalance - X) + X + 0
After Swap = (unexchangedBalance - X) + 0 + Y
Even if Y > X (swap appears profitable), the total value can decrease due to fees and slippage, breaking the core invariant that all operations must increase total strategy value.
Recommendations
function claimAndSwap(uint256 _amountClaim, uint256 _minOut, uint256 _routeNumber) external onlyKeepers {
+
+ uint256 totalBefore = balanceDeployed();
transmuter.claim(_amountClaim, address(this));
- uint256 balBefore = asset.balanceOf(address(this));
require(_minOut > _amountClaim, "minOut too low");
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");
+
+ uint256 totalAfter = balanceDeployed();
+ require(totalAfter > totalBefore, "Trade not profitable");
+ require(totalAfter >= totalBefore + minimumProfitMargin, "Insufficient profit");
transmuter.deposit(asset.balanceOf(address(this)), address(this));
}
+
+uint256 public minimumProfitMargin;
+
+function setMinimumProfitMargin(uint256 _margin) external onlyManagement {
+ minimumProfitMargin = _margin;
+}