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

A keeper could claim more tokens than available in the transmuter

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));
// @FOUND The minOut check only verifies minOut > _amountClaim but doesn't account for exchange rate
// This allows unprofitable swaps since 1:1 trades would pass this check
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:

  1. Don't generate profit

  2. Don't cover gas costs

  3. 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));
// @FOUND Critical profitability check vulnerability:
// The swap can execute even when minOut equals amountClaim, allowing 1:1 trades
// that generate no profit and waste gas
_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 {
// @FOUND The minOut validation is insufficient:
// require(minOut > _amount) allows trades with minimal profit that don't cover gas costs
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:

  1. A trade swapping 1 ETH for 1.000000000000000001 ETH would be considered valid

  2. Such trades incur gas costs without meaningful profit

  3. This defeats the arbitrage purpose of the strategy

Impact

  1. Keepers can execute technically valid but economically unprofitable trades

  2. Strategy performance will be degraded by gas costs exceeding minimal profits

  3. 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));
}
Updates

Appeal created

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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