Summary
In the accounting logic of the claimAndSwap function across all strategy implementations (StrategyOp.sol, StrategyMainnet.sol, and 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));
}
-
The function uses asset.balanceOf()
for profit verification instead of balanceDeployed()
-
balanceDeployed()
includes:
-
By only checking asset balance, the function fails to account for the underlying tokens claimed from the transmuter before the swap
We expects total value (balanceDeployed
) to increase by at least minOut
, but the contract's implementation only verifies the increase in asset balance, ignoring the decrease in underlying balance from the claim operation.
Vulnerability Details
In StrategyOp.sol's claimAndSwap, _swapUnderlyingToAsset
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));
}
function _swapUnderlyingToAsset(uint256 _amount, uint256 minOut, IVeloRouter.route[] calldata _path) internal {
require(minOut > _amount, "minOut too low");
uint256 underlyingBalance = underlying.balanceOf(address(this));
require(underlyingBalance >= _amount, "not enough underlying balance");
IVeloRouter(router).swapExactTokensForTokens(_amount, minOut, _path, address(this), block.timestamp);
}
-
The strategy assumes that requiring minOut > _amountClaim
is sufficient to ensure profitability
-
This assumption is incorrect because:
The market rate between underlying and asset could be significantly higher than 1:1
The check doesn't account for fees or slippage in the swap
There's no validation against current market prices or expected rates
Impact
-
Keepers could execute unprofitable trades where:
-
This leads to value loss when
_amountClaim = 100 ETH
minOut = 101 alETH
Market Rate = 1 ETH : 1.5 alETH
Expected Return = 150 alETH
Actual Return = 101 alETH
Loss = 49 alETH
The same issue exists in StrategyMainnet.sol and StrategyArb.sol as they share the same fundamental design.
The strategy could accept trades that result in significant value loss while still passing the minimal checks in place.
Tools Used
Recommendations
Check issue across all strategy contracts
// StrategyOp.sol, StrategyMainnet.sol, StrategyArb.sol
+ uint256 public constant MINIMUM_PROFIT_BPS = 100; // 1% minimum profit
+ uint256 public constant BPS_DENOMINATOR = 10000;
function claimAndSwap(
uint256 _amountClaim,
uint256 _minOut,
IVeloRouter.route[] calldata _path
) external onlyKeepers {
+ // @audit-fix Track total value before any operations
+ uint256 totalBefore = balanceDeployed();
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");
+ // @audit-fix Ensure minimum profitability threshold is met
+ uint256 totalAfter = balanceDeployed();
+ uint256 minProfit = (_amountClaim * MINIMUM_PROFIT_BPS) / BPS_DENOMINATOR;
+ require(
+ totalAfter >= totalBefore + minProfit,
+ "Insufficient profit margin"
+ );
transmuter.deposit(asset.balanceOf(address(this)), address(this));
}
function _swapUnderlyingToAsset(
uint256 _amount,
uint256 _minOut,
IVeloRouter.route[] calldata _path
) internal {
- require(minOut > _amount, "minOut too low");
+ // @audit-fix Enforce minimum profitable output based on amount and minimum profit margin
+ uint256 minRequired = _amount + (_amount * MINIMUM_PROFIT_BPS) / BPS_DENOMINATOR;
+ require(_minOut >= minRequired, "Minimum profit not met");
uint256 underlyingBalance = underlying.balanceOf(address(this));
require(underlyingBalance >= _amount, "not enough underlying balance");
IVeloRouter(router).swapExactTokensForTokens(
_amount,
_minOut,
_path,
address(this),
block.timestamp
);
}