Summary
In StrategyOp.claimAndSwap
function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IVeloRouter.route[] calldata _path) external onlyKeepers {
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
require(minOut > _amount, "minOut too low");
_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));
}
While the code checks that minOut > amount, this only ensures a 1:1 exchange rate minimum. However, the strategy's purpose is arbitrage, which requires a profit margin above the 1:1 rate.
because the current checks allow trades at exactly 1:1 ratio, which:
Don't generate profit
Don't cover gas costs
Defeat the arbitrage purpose of the strategy
A malicious or careless keeper could execute unprofitable trades that technically pass the minOut check but don't generate actual returns for the protocol.
Vulnerability Details
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 same issue exists in StrategyOp.sol and StrategyMainnet.sol in their respective claimAndSwap functions
function _swapUnderlyingToAsset(uint256 _amount, uint256 minOut, IVeloRouter.route[] calldata _path) internal {
require(minOut > _amount, "minOut too low");
IVeloRouter(router).swapExactTokensForTokens(_amount, minOut, _path, address(this), block.timestamp);
}
From incorrect profit margin validation in the cross-chain arbitrage logic. The code only verifies that minOut > amount, which means:
A trade swapping 1 ETH for 1.000000000000000001 ETH would be considered valid
Such trades incur gas costs without meaningful profit
This defeats the arbitrage purpose of the strategy
Impact
Keepers can execute technically valid but economically unprofitable trades
Strategy performance will be degraded by gas costs exceeding minimal profits
The protocol's arbitrage mechanism becomes ineffective at capturing meaningful price differences
Recommendations
// StrategyArb.sol, StrategyOp.sol, StrategyMainnet.sol
+ uint256 public constant MINIMUM_PROFIT_BPS = 50; // 0.5% minimum profit margin
function _swapUnderlyingToAsset(uint256 _amount, uint256 minOut, IVeloRouter.route[] calldata _path) internal {
- require(minOut > _amount, "minOut too low");
+ // @Mitigation - Enforce minimum profit margin to ensure trades are meaningfully profitable
+ uint256 minimumOut = _amount + (_amount * MINIMUM_PROFIT_BPS / 10000);
+ require(minOut >= minimumOut, "Insufficient profit margin");
uint256 underlyingBalance = underlying.balanceOf(address(this));
require(underlyingBalance >= _amount, "not enough underlying balance");
IVeloRouter(router).swapExactTokensForTokens(_amount, minOut, _path, address(this), block.timestamp);
}
function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IVeloRouter.route[] calldata _path) external onlyKeepers {
+ // @Mitigation - Add pre-claim validation of available balance and expected profit
+ uint256 claimable = transmuter.getClaimableBalance(address(this));
+ require(_amountClaim <= claimable, "Exceeds claimable balance");
+
+ uint256 minimumProfit = (_amountClaim * MINIMUM_PROFIT_BPS) / 10000;
+ require(_minOut >= _amountClaim + minimumProfit, "Insufficient expected profit");
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");
+ // @Mitigation - Verify actual profit meets minimum threshold
+ uint256 profit = balAfter - balBefore - _amountClaim;
+ require(profit >= minimumProfit, "Insufficient actual profit");
transmuter.deposit(asset.balanceOf(address(this)), address(this));
}