Summary
When the keeper claims and swaps WETH to alETH, the _minOut parameter should be at least greater than or equal to _amountClaim.
Vulnerability Details
It is possible that when the keeper swaps an amount of WETH equivalent to alETH (i.e., the conversion rate between WETH and alETH is 1:1), the function will revert if _minOut is set to _amountClaim. This happens because the function requires the output amount to strictly exceed _minOut. As a result, users will not be able to claim and swap until the conversion rate changes from 1:1.
Impact
If the conversion rate between WETH and alETH is exactly 1:1, the keeper will not be able to successfully claim and swap. This could lead to operational delays or interruptions until the conversion rate changes.
Tools Used
Manuel Review and foundry
Change the test function function "test_claim_and_swap(uint256 _amount)" of Operation.t.sol
function test_claim_and_swap(uint256 _amount) public {
......
if (block.chainid == 1) {
// Mainnet
IStrategyInterface(address(strategy)).claimAndSwap(
claimable,
- claimable * 103 / 100,
+ claimable * 100 / 100,
0
);
} else if (block.chainid == 10) {
// NOTE on OP we swap directly to WETH
IVeloRouter.route[] memory veloRoute = new IVeloRouter.route[]();
veloRoute[0] = IVeloRouter.route(address(underlying), address(asset), true, 0xF1046053aa5682b4F9a81b5481394DA16BE5FF5a);
// Velo Iterface
- IStrategyInterfaceVelo(address(strategy)).claimAndSwap(claimable, claimable * 103 / 100, veloRoute);
+ IStrategyInterfaceVelo(address(strategy)).claimAndSwap(claimable, claimable * 100 / 100, veloRoute);
} else if (block.chainid == 42161) {
// ARB
// NOTE we swap first to eFrax and then to WETH
IRamsesRouter.route[] memory ramsesRoute = new IRamsesRouter.route[]();
address eFrax = 0x178412e79c25968a32e89b11f63B33F733770c2A;
ramsesRoute[0] = IRamsesRouter.route(address(underlying), eFrax, true);
ramsesRoute[1] = IRamsesRouter.route(eFrax, address(asset), true);
- IStrategyInterfaceRamses(address(strategy)).claimAndSwap(claimable, claimable * 103 / 100, ramsesRoute);
+ IStrategyInterfaceRamses(address(strategy)).claimAndSwap(claimable, claimable * 100 / 100, ramsesRoute);
} else {
revert("Chain ID not supported");
}
....
}
The test revert with eeror ""minOut too low""
Recommendations
Update the function claimAndSwap in all file StrategyArb, StrategyMainnet, StrategyOP
MAINET
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");
+ 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));
}
ARB
function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IRamsesRouter.route[] calldata _path) external onlyKeepers {
transmuter.claim(_amountClaim, address(this)); //@audit check that this part don't fail
uint256 balBefore = asset.balanceOf(address(this));
_swapUnderlyingToAsset(_amountClaim, _minOut, _path); //@audit check that this part don't fail
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, IRamsesRouter.route[] calldata _path) internal {
// TODO : we swap WETH to ALETH -> need to check that price is better than 1:1
// uint256 oraclePrice = 1e18 * 101 / 100;
+ require(minOut >= _amount, "minOut too low");
- require(minOut > _amount, "minOut too low");
uint256 underlyingBalance = underlying.balanceOf(address(this));
require(underlyingBalance >= _amount, "not enough underlying balance");
IRamsesRouter(router).swapExactTokensForTokens(_amount, minOut, _path, address(this), block.timestamp);
}
OPS
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));
}
/**
@dev internal function for swapping WETH to alETH via Velo Router
*/
function _swapUnderlyingToAsset(uint256 _amount, uint256 minOut, IVeloRouter.route[] calldata _path) internal {
// TODO : we swap WETH to ALETH -> need to check that price is better than 1:1
// uint256 oraclePrice = 1e18 * 101 / 100;
- require(minOut > _amount, "minOut too low");
+ 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);
}