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

Strategy could accept trades that result in significant value loss while still passing the minimal checks in place

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 {
// @audit-issue Incorrect accounting: balanceBefore only checks asset balance, ignoring underlying tokens
// This leads to incorrect profit calculation since underlying tokens claimed from transmuter aren't counted
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));
}
  1. The function uses asset.balanceOf() for profit verification instead of balanceDeployed()

  2. balanceDeployed() includes:

    • transmuter.getUnexchangedBalance()

    • underlying.balanceOf()

    • asset.balanceOf()

  3. 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 {
// @audit-issue The minOut check is insufficient - it only verifies minOut > _amountClaim
// but doesn't account for potential losses during the swap operation
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 {
// @audit-issue The minOut validation here is incorrect
// It only checks minOut > _amount without considering the actual market rate
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);
}
  1. The strategy assumes that requiring minOut > _amountClaim is sufficient to ensure profitability

  2. 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

  1. Keepers could execute unprofitable trades where:

    • minOut is slightly higher than _amountClaim

    • But significantly below market rate

  2. This leads to value loss when

// Example scenario
_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
);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge
6 months ago

Appeal created

inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
inallhonesty Lead Judge 6 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.