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));
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.
-
The current implementation validates minOut against amountClaim instead of the actual swap output
-
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));
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));
_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));
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
The transmuter.claim()
operation increases the underlying token balance
balanceBefore
is captured after this increase
The swap occurs with potentially unfavorable rates
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");
}