The normal behavior should provide users with predictable fees when they submit swap transactions. Users should be able to rely on current fee settings when making swap decisions.
pragma solidity ^0.8.26;
import "forge-std/Test.sol";
import "forge-std/console.sol";
contract FeeFrontrunningTest is Test {
address owner = address(0x1);
address user = address(0x2);
address maliciousOwner = address(0x3);
uint24 public buyFee = 0;
uint24 public sellFee = 3000;
function swap(uint256 amount, bool isBuy) public view returns (uint256 fee, uint256 received) {
uint24 appliedFee = isBuy ? buyFee : sellFee;
fee = (amount * appliedFee) / 1000000;
received = amount - fee;
}
function changeFee(uint24 _buyFee, uint24 _sellFee) public {
buyFee = _buyFee;
sellFee = _sellFee;
}
function test_FrontrunningAttack_LargeSwap() public {
console.log("\n=== Front-Running Attack Scenario ===");
console.log("User wants to sell 100,000 ReFi tokens");
uint256 swapAmount = 100000 ether;
console.log("\nStep 1: User checks fee on frontend");
console.log("Current sell fee:", sellFee, "basis points (0.3%)");
(uint256 normalFee, uint256 normalReceived) = swap(swapAmount, false);
console.log("Expected fee:", normalFee / 1 ether, "tokens");
console.log("Expected to receive:", normalReceived / 1 ether, "tokens");
console.log("\nStep 2: User transaction enters mempool");
console.log("Transaction awaiting confirmation...");
console.log("\nStep 3: Owner monitors mempool and sees large swap");
console.log("Owner decides to front-run");
console.log("\nStep 4: Owner front-runs with fee increase");
uint24 attackFee = 50000;
changeFee(0, attackFee);
console.log("Owner sets sell fee to:", attackFee, "basis points (5%)");
console.log("\nStep 5: User's swap executes");
(uint256 actualFee, uint256 actualReceived) = swap(swapAmount, false);
console.log("Actual fee charged:", actualFee / 1 ether, "tokens");
console.log("Actually received:", actualReceived / 1 ether, "tokens");
uint256 extraFee = actualFee - normalFee;
console.log("\n=== Attack Impact ===");
console.log("Expected fee:", normalFee / 1 ether, "tokens (0.3%)");
console.log("Actual fee:", actualFee / 1 ether, "tokens (5%)");
console.log("Extra loss:", extraFee / 1 ether, "tokens");
console.log("Loss percentage:", (extraFee * 100) / swapAmount, "%");
console.log("\nStep 6: Owner resets fee to normal");
changeFee(0, 3000);
console.log("Sell fee reset to:", sellFee, "basis points");
console.log("Attack complete, evidence hidden");
assertGt(actualFee, normalFee, "User paid more than expected");
assertEq(extraFee, 4700 ether, "User lost 4700 extra tokens");
}
function test_MultipleVictimsAttack() public {
console.log("\n=== Multiple Victims Attack ===");
uint256 victim1Amount = 50000 ether;
uint256 victim2Amount = 75000 ether;
uint256 victim3Amount = 100000 ether;
changeFee(0, 3000);
(uint256 v1NormalFee, ) = swap(victim1Amount, false);
(uint256 v2NormalFee, ) = swap(victim2Amount, false);
(uint256 v3NormalFee, ) = swap(victim3Amount, false);
uint256 totalNormalFees = v1NormalFee + v2NormalFee + v3NormalFee;
console.log("Total normal fees (0.3%):", totalNormalFees / 1 ether, "tokens");
changeFee(0, 50000);
(uint256 v1AttackFee, ) = swap(victim1Amount, false);
(uint256 v2AttackFee, ) = swap(victim2Amount, false);
(uint256 v3AttackFee, ) = swap(victim3Amount, false);
uint256 totalAttackFees = v1AttackFee + v2AttackFee + v3AttackFee;
console.log("Total attack fees (5%):", totalAttackFees / 1 ether, "tokens");
uint256 extraProfit = totalAttackFees - totalNormalFees;
console.log("Owner's extra profit:", extraProfit / 1 ether, "tokens");
console.log("Victims' combined loss:", extraProfit / 1 ether, "tokens");
}
function test_TimingWindow() public {
console.log("\n=== Attack Timing Window ===");
uint256 userSubmitTime = block.timestamp;
uint256 ownerFrontrunTime = block.timestamp;
uint256 swapExecutionTime = block.timestamp + 12;
console.log("Block n: User submits swap tx");
console.log("Block n: Owner sees tx in mempool");
console.log("Block n: Owner submits ChangeFee with higher gas");
console.log("Block n+1: Owner's ChangeFee executes first");
console.log("Block n+1: User's swap executes with new fee");
console.log("\nAttack window: Entire duration in mempool");
console.log("Protection: NONE - instant fee changes allowed");
}
function test_ComparisonWithTimelock() public {
console.log("\n=== With vs Without Timelock ===");
console.log("\nCurrent Implementation (NO TIMELOCK):");
console.log("✗ Fee changes instant");
console.log("✗ Users cannot react");
console.log("✗ No warning period");
console.log("✗ Owner can front-run");
console.log("✗ No protection mechanism");
console.log("\nWith Timelock:");
console.log("✓ Fee changes delayed (e.g., 24 hours)");
console.log("✓ Users can withdraw before change");
console.log("✓ Transparent upcoming changes");
console.log("✓ Cannot front-run (change not immediate)");
console.log("✓ Trust through time");
}
function test_RealWorldImpact() public {
console.log("\n=== Real-World Impact Analysis ===");
uint256 dailyVolume = 1000000 ether;
uint256 sellVolume = dailyVolume / 2;
uint256 normalFees = (sellVolume * 3000) / 1000000;
console.log("\nNormal daily fees (0.3% on sells):", normalFees / 1 ether, "tokens");
uint256 largeSwap1 = 100000 ether;
uint256 largeSwap2 = 150000 ether;
uint256 attackProfit = ((largeSwap1 + largeSwap2) * (50000 - 3000)) / 1000000;
console.log("\nAttack day extra profit:", attackProfit / 1 ether, "tokens");
console.log("Percentage of daily volume: ~", (attackProfit * 100) / dailyVolume, "%");
console.log("\nUser Trust Impact:");
console.log("- Users realize they can't trust displayed fees");
console.log("- Large traders avoid the protocol");
console.log("- TVL decreases as users exit");
console.log("- Protocol reputation destroyed");
console.log("- Competing protocols gain market share");
}
function test_SlippageBypassPotential() public {
console.log("\n=== Slippage Protection Bypass ===");
uint256 swapAmount = 100000 ether;
console.log("User sets 1% slippage protection");
console.log("Swap amount:", swapAmount / 1 ether, "tokens");
changeFee(0, 3000);
(, uint256 expectedOutput) = swap(swapAmount, false);
console.log("Expected output:", expectedOutput / 1 ether, "tokens");
uint256 minOutput = expectedOutput * 99 / 100;
console.log("Minimum accepted:", minOutput / 1 ether, "tokens");
changeFee(0, 20000);
(, uint256 attackOutput) = swap(swapAmount, false);
console.log("\nWith 2% fee:");
console.log("Actual output:", attackOutput / 1 ether, "tokens");
console.log("Within slippage?", attackOutput >= minOutput ? "YES" : "NO");
if (attackOutput >= minOutput) {
console.log("\n❌ Attack bypasses slippage protection!");
console.log("User accepts the swap thinking it's normal slippage");
console.log("But actually paid extra fees due to manipulation");
}
}
}