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

trades executing with higher slippage than intended

Summary

In StrategyMainnet.claimAndSwap

function claimAndSwap(
uint256 _amountClaim,
uint256 _minOut,
uint256 _routeNumber
) external onlyKeepers {
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
// @FOUND The minOut check happens before the swap, allowing the actual output to be lower than minOut
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 code checks that minOut > amountClaim, but this doesn't guarantee the actual swap output meets the minOut requirement. The router.exchange() call could return less than minOut while still satisfying the initial check.

  1. The current implementation validates minOut against amountClaim instead of the actual swap output

  2. This allows a scenario where:

    • minOut > amountClaim (passes first check)

    • Actual swap output < minOut (should fail but doesn't)

    • Final balance check can be bypassed due to this incorrect validation order

Vulnerability Details

In StrategyMainnet.claimAndSwap function implementation across all strategy contracts.

function claimAndSwap(uint256 _amountClaim, uint256 _minOut, uint256 _routeNumber) external onlyKeepers {
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
// @FOUND Critical: Balance check after swap can be manipulated since balanceBefore is captured after claim
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");
}

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 Critical: Same balance check vulnerability as StrategyMainnet
_swapUnderlyingToAsset(_amountClaim, _minOut, _path);
uint256 balAfter = asset.balanceOf(address(this));
require((balAfter - balBefore) >= _minOut, "Slippage too high");
}

In StrategyArb.claimAndSwap

function claimAndSwap(uint256 _amountClaim, uint256 _minOut, uint256 routeNumber) external onlyKeepers {
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
// @FOUND Critical: Same balance check vulnerability as other strategies
claimAndSwap@withrevert(e, amount, minOut, routeNumber);
uint256 balanceAfter = balanceDeployed@withrevert();
require((balAfter - balBefore) >= _minOut, "Slippage too high");
}

from an incorrect sequence of operations in the balance checking logic. The balanceBefore snapshot is taken after the claim operation but before the swap. This creates a window where the actual output validation can be manipulated.

Impact

  1. The transmuter.claim() operation increases the underlying token balance

  2. balanceBefore is captured after this increase

  3. The swap occurs with potentially unfavorable rates

  4. The final check (balAfter - balBefore) >= _minOut becomes unreliable because balanceBefore was inflated

This can lead to:

  • Execution of swaps with higher slippage than intended

  • Potential loss of funds through manipulated exchange rates

  • Bypass of intended minimum output protection

The bug affects all three strategy implementations identically, as they share the same balance checking pattern.

Recommendations

For StrategyMainnet.sol

function claimAndSwap(uint256 _amountClaim, uint256 _minOut, uint256 _routeNumber) external onlyKeepers {
+ // @Mitigation - Capture initial asset balance before any operations
+ uint256 balBefore = asset.balanceOf(address(this));
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));
}

For StrategyOp.sol

function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IVeloRouter.route[] calldata _path) external onlyKeepers {
+ // @Mitigation - Capture initial balance and add strict output validation
+ uint256 balBefore = asset.balanceOf(address(this));
transmuter.claim(_amountClaim, address(this));
- uint256 balBefore = asset.balanceOf(address(this));
+ // @Mitigation - Add validation for claimed amount
+ require(underlying.balanceOf(address(this)) >= _amountClaim, "Insufficient claim");
_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));
}

For StrategyArb.sol

function claimAndSwap(uint256 _amountClaim, uint256 _minOut, uint256 routeNumber) external onlyKeepers {
+ // @Mitigation - Capture initial balance and implement strict checks
+ uint256 balBefore = asset.balanceOf(address(this));
+ uint256 underlyingBefore = underlying.balanceOf(address(this));
transmuter.claim(_amountClaim, address(this));
- uint256 balBefore = asset.balanceOf(address(this));
+ // @Mitigation - Verify claim success
+ require(underlying.balanceOf(address(this)) - underlyingBefore >= _amountClaim, "Claim failed");
claimAndSwap@withrevert(e, amount, minOut, routeNumber);
uint256 balAfter = asset.balanceOf(address(this));
require((balAfter - balBefore) >= _minOut, "Slippage too high");
}
Updates

Lead Judging Commences

inallhonesty Lead Judge
8 months ago

Appeal created

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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