LPsLux Proposals
DEX & Trading
LP-9025

MEV Protection & Fair Ordering

Review

Comprehensive MEV protection mechanisms, fair ordering protocols, and transaction privacy for DeFi

Category
Core
Created
2025-01-15

LP-9025: MEV Protection & Fair Ordering

Abstract

This LP defines MEV (Maximal Extractable Value) protection mechanisms, fair ordering protocols, and transaction privacy standards for Lux DeFi. Ensures users are protected from front-running, sandwich attacks, and other MEV extraction techniques.

Motivation

MEV protection is critical for:

  • Protecting users from front-running
  • Preventing sandwich attacks
  • Ensuring fair transaction ordering
  • Maintaining market integrity
  • Building user trust

Specification

1. MEV Types & Mitigation

MEV TypeDescriptionMitigation
Front-runningInserting tx before victimCommit-reveal, encryption
Back-runningInserting tx after victimTime-based batching
SandwichWrapping victim txSlippage protection, private mempool
JIT LiquidityJust-in-time LP provisionMEV-aware routing
LiquidationRacing to liquidate positionsFair liquidation auction
ArbitrageCross-DEX price differencesAtomic execution

2. Fair Ordering Protocol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface IFairOrdering {
    enum OrderingMethod {
        FIFO,           // First-in-first-out
        BATCH_AUCTION,  // Batched with uniform clearing
        COMMIT_REVEAL,  // Two-phase submission
        ENCRYPTED,      // Threshold encrypted
        RANDOM          // Randomized ordering
    }

    struct OrderingConfig {
        OrderingMethod method;
        uint256 batchDuration;
        uint256 revealWindow;
        uint256 minParticipants;
        bool mevProtectionEnabled;
    }

    struct CommitRevealOrder {
        bytes32 commitment;
        uint256 commitBlock;
        bool revealed;
        bytes orderData;
        uint256 nonce;
    }

    // Commit phase
    function commitOrder(bytes32 commitment) external returns (bytes32 orderId);

    // Reveal phase
    function revealOrder(
        bytes32 orderId,
        bytes calldata orderData,
        bytes32 salt
    ) external;

    // Batch execution
    function executeBatch(uint256 batchId) external;

    // Events
    event OrderCommitted(bytes32 indexed orderId, address indexed user, uint256 commitBlock);
    event OrderRevealed(bytes32 indexed orderId, bytes orderData);
    event BatchExecuted(uint256 indexed batchId, uint256 ordersExecuted);
}

3. Commit-Reveal Implementation

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract CommitRevealDEX is IFairOrdering {
    uint256 public constant COMMIT_WINDOW = 3; // blocks
    uint256 public constant REVEAL_WINDOW = 3; // blocks
    uint256 public constant EXECUTION_DELAY = 1; // blocks

    mapping(bytes32 => CommitRevealOrder) public orders;
    mapping(uint256 => bytes32[]) public batchOrders;
    uint256 public currentBatch;

    function commitOrder(bytes32 commitment) external override returns (bytes32 orderId) {
        orderId = keccak256(abi.encode(msg.sender, commitment, block.number));

        orders[orderId] = CommitRevealOrder({
            commitment: commitment,
            commitBlock: block.number,
            revealed: false,
            orderData: "",
            nonce: 0
        });

        batchOrders[currentBatch].push(orderId);

        emit OrderCommitted(orderId, msg.sender, block.number);
    }

    function revealOrder(
        bytes32 orderId,
        bytes calldata orderData,
        bytes32 salt
    ) external override {
        CommitRevealOrder storage order = orders[orderId];

        require(!order.revealed, "Already revealed");
        require(block.number >= order.commitBlock + COMMIT_WINDOW, "Commit window active");
        require(block.number < order.commitBlock + COMMIT_WINDOW + REVEAL_WINDOW, "Reveal expired");

        // Verify commitment
        bytes32 expectedCommitment = keccak256(abi.encode(orderData, salt));
        require(order.commitment == expectedCommitment, "Invalid reveal");

        order.revealed = true;
        order.orderData = orderData;

        emit OrderRevealed(orderId, orderData);
    }

    function executeBatch(uint256 batchId) external override {
        require(batchId < currentBatch, "Batch not ready");

        bytes32[] storage orderIds = batchOrders[batchId];

        // Shuffle orders to prevent ordering manipulation
        _shuffleOrders(orderIds);

        uint256 executed = 0;
        for (uint i = 0; i < orderIds.length; i++) {
            CommitRevealOrder storage order = orders[orderIds[i]];
            if (order.revealed) {
                _executeOrder(order.orderData);
                executed++;
            }
        }

        emit BatchExecuted(batchId, executed);
    }

    function _shuffleOrders(bytes32[] storage orderIds) internal {
        // Fisher-Yates shuffle using block hash as randomness
        for (uint i = orderIds.length - 1; i > 0; i--) {
            uint j = uint(keccak256(abi.encode(blockhash(block.number - 1), i))) % (i + 1);
            (orderIds[i], orderIds[j]) = (orderIds[j], orderIds[i]);
        }
    }

    function _executeOrder(bytes memory orderData) internal {
        // Decode and execute order
        (address tokenIn, address tokenOut, uint256 amountIn, uint256 minOut) =
            abi.decode(orderData, (address, address, uint256, uint256));

        // Execute swap
    }

    function startNewBatch() external {
        currentBatch++;
    }
}

4. Batch Auction System

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract BatchAuctionDEX {
    struct Batch {
        uint256 startBlock;
        uint256 endBlock;
        uint256 clearingPrice;
        bool settled;
        Order[] buyOrders;
        Order[] sellOrders;
    }

    struct Order {
        address user;
        uint256 amount;
        uint256 limitPrice;
        bool filled;
        uint256 fillAmount;
    }

    uint256 public constant BATCH_DURATION = 12; // blocks (~2.5 min)

    mapping(uint256 => Batch) public batches;
    uint256 public currentBatchId;

    event OrderSubmitted(uint256 indexed batchId, address indexed user, bool isBuy, uint256 amount, uint256 limitPrice);
    event BatchSettled(uint256 indexed batchId, uint256 clearingPrice, uint256 volume);

    function submitBuyOrder(uint256 amount, uint256 maxPrice) external {
        _ensureCurrentBatch();

        batches[currentBatchId].buyOrders.push(Order({
            user: msg.sender,
            amount: amount,
            limitPrice: maxPrice,
            filled: false,
            fillAmount: 0
        }));

        emit OrderSubmitted(currentBatchId, msg.sender, true, amount, maxPrice);
    }

    function submitSellOrder(uint256 amount, uint256 minPrice) external {
        _ensureCurrentBatch();

        batches[currentBatchId].sellOrders.push(Order({
            user: msg.sender,
            amount: amount,
            limitPrice: minPrice,
            filled: false,
            fillAmount: 0
        }));

        emit OrderSubmitted(currentBatchId, msg.sender, false, amount, minPrice);
    }

    function settleBatch(uint256 batchId) external {
        Batch storage batch = batches[batchId];
        require(block.number > batch.endBlock, "Batch not ended");
        require(!batch.settled, "Already settled");

        // Find clearing price using uniform price auction
        uint256 clearingPrice = _findClearingPrice(batch);
        batch.clearingPrice = clearingPrice;

        // Fill orders at clearing price
        uint256 totalVolume = _fillOrders(batch, clearingPrice);

        batch.settled = true;

        emit BatchSettled(batchId, clearingPrice, totalVolume);
    }

    function _findClearingPrice(Batch storage batch) internal view returns (uint256) {
        // Sort buy orders descending, sell orders ascending
        // Find intersection point
        // This is a simplified version - production would use more efficient algo

        uint256 bestPrice = 0;
        uint256 maxVolume = 0;

        // Try each price point
        for (uint i = 0; i < batch.buyOrders.length; i++) {
            uint256 price = batch.buyOrders[i].limitPrice;
            uint256 buyVolume = _calculateBuyVolume(batch, price);
            uint256 sellVolume = _calculateSellVolume(batch, price);
            uint256 matchedVolume = buyVolume < sellVolume ? buyVolume : sellVolume;

            if (matchedVolume > maxVolume) {
                maxVolume = matchedVolume;
                bestPrice = price;
            }
        }

        return bestPrice;
    }

    function _calculateBuyVolume(Batch storage batch, uint256 price) internal view returns (uint256 volume) {
        for (uint i = 0; i < batch.buyOrders.length; i++) {
            if (batch.buyOrders[i].limitPrice >= price) {
                volume += batch.buyOrders[i].amount;
            }
        }
    }

    function _calculateSellVolume(Batch storage batch, uint256 price) internal view returns (uint256 volume) {
        for (uint i = 0; i < batch.sellOrders.length; i++) {
            if (batch.sellOrders[i].limitPrice <= price) {
                volume += batch.sellOrders[i].amount;
            }
        }
    }

    function _fillOrders(Batch storage batch, uint256 price) internal returns (uint256 totalVolume) {
        // Fill all orders at clearing price
        // Pro-rata if demand/supply imbalanced
        return 0;
    }

    function _ensureCurrentBatch() internal {
        if (block.number > batches[currentBatchId].endBlock) {
            currentBatchId++;
            batches[currentBatchId].startBlock = block.number;
            batches[currentBatchId].endBlock = block.number + BATCH_DURATION;
        }
    }
}

5. Private Transaction Pool

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface IPrivateMempool {
    struct PrivateTx {
        bytes32 txHash;
        bytes encryptedData;
        uint256 submittedAt;
        uint256 targetBlock;
        bool executed;
    }

    // Submit encrypted transaction
    function submitPrivateTx(
        bytes calldata encryptedTx,
        uint256 targetBlock
    ) external returns (bytes32 txId);

    // Validator decrypts and includes
    function decryptAndExecute(
        bytes32 txId,
        bytes calldata decryptionKey
    ) external;

    // Flashbots-style bundle
    function submitBundle(
        bytes[] calldata txs,
        uint256 targetBlock
    ) external returns (bytes32 bundleId);
}

contract MEVProtectedRouter {
    IPrivateMempool public mempool;
    mapping(address => bool) public trustedRelayers;

    // Submit swap through private mempool
    function protectedSwap(
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        uint256 minAmountOut,
        uint256 deadline
    ) external {
        // Encode swap data
        bytes memory swapData = abi.encode(
            msg.sender,
            tokenIn,
            tokenOut,
            amountIn,
            minAmountOut,
            deadline
        );

        // Encrypt with threshold encryption
        bytes memory encryptedData = _encryptForValidators(swapData);

        // Submit to private mempool
        mempool.submitPrivateTx(encryptedData, block.number + 1);
    }

    function _encryptForValidators(bytes memory data) internal view returns (bytes memory) {
        // Threshold encryption implementation
        // Data can only be decrypted when included in block
        return data; // Simplified
    }
}

6. Slippage Protection

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

library SlippageGuard {
    uint256 constant MAX_BPS = 10000;

    struct SlippageConfig {
        uint256 maxSlippageBps;      // User-defined max
        uint256 dynamicSlippageBps;  // Auto-calculated
        bool useDynamicSlippage;
        uint256 priceImpactLimit;
    }

    function calculateSafeSlippage(
        uint256 amount,
        uint256 poolLiquidity,
        uint256 volatility
    ) internal pure returns (uint256 slippageBps) {
        // Base slippage on trade size vs liquidity
        uint256 sizeImpact = (amount * MAX_BPS) / poolLiquidity;

        // Add volatility premium
        uint256 volatilityPremium = volatility / 10; // 10% of volatility

        slippageBps = sizeImpact + volatilityPremium + 50; // 0.5% base

        // Cap at reasonable maximum
        if (slippageBps > 500) slippageBps = 500; // 5% max
    }

    function validateExecution(
        uint256 expectedOut,
        uint256 actualOut,
        uint256 maxSlippageBps
    ) internal pure returns (bool valid) {
        uint256 minAcceptable = (expectedOut * (MAX_BPS - maxSlippageBps)) / MAX_BPS;
        return actualOut >= minAcceptable;
    }
}

contract SlippageProtectedSwap {
    using SlippageGuard for *;

    function swap(
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        uint256 minAmountOut,
        uint256 deadline
    ) external returns (uint256 amountOut) {
        require(block.timestamp <= deadline, "Expired");

        // Get expected output
        uint256 expectedOut = getQuote(tokenIn, tokenOut, amountIn);

        // Validate user's slippage tolerance
        uint256 userSlippage = ((expectedOut - minAmountOut) * 10000) / expectedOut;

        // Calculate safe slippage
        uint256 liquidity = getPoolLiquidity(tokenIn, tokenOut);
        uint256 volatility = getVolatility(tokenIn, tokenOut);
        uint256 safeSlippage = SlippageGuard.calculateSafeSlippage(amountIn, liquidity, volatility);

        // Warn if user slippage is too high
        require(userSlippage <= safeSlippage * 2, "Slippage too high - MEV risk");

        // Execute swap
        amountOut = _executeSwap(tokenIn, tokenOut, amountIn);

        // Validate execution
        require(amountOut >= minAmountOut, "Slippage exceeded");

        return amountOut;
    }

    function getQuote(address, address, uint256) internal view returns (uint256) { return 0; }
    function getPoolLiquidity(address, address) internal view returns (uint256) { return 0; }
    function getVolatility(address, address) internal view returns (uint256) { return 0; }
    function _executeSwap(address, address, uint256) internal returns (uint256) { return 0; }
}

7. MEV-Aware Routing

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract MEVAwareRouter {
    struct Route {
        address[] path;
        address[] pools;
        uint256 expectedOutput;
        uint256 mevRisk;          // 0-100 score
        uint256 priceImpact;
        bool usesPrivatePool;
    }

    // Find optimal route considering MEV risk
    function findOptimalRoute(
        address tokenIn,
        address tokenOut,
        uint256 amountIn
    ) external view returns (Route memory bestRoute) {
        Route[] memory routes = _findAllRoutes(tokenIn, tokenOut, amountIn);

        uint256 bestScore = 0;
        for (uint i = 0; i < routes.length; i++) {
            // Score = output * (1 - mevRisk/100) * (1 - priceImpact/100)
            uint256 score = routes[i].expectedOutput *
                (100 - routes[i].mevRisk) *
                (100 - routes[i].priceImpact) / 10000;

            if (score > bestScore) {
                bestScore = score;
                bestRoute = routes[i];
            }
        }
    }

    function _calculateMEVRisk(Route memory route) internal view returns (uint256 risk) {
        // Factors that increase MEV risk:
        // - Large trade size relative to pool
        // - Low liquidity pools
        // - Popular token pairs
        // - Predictable execution timing

        uint256 sizeRisk = _calculateSizeRisk(route);
        uint256 liquidityRisk = _calculateLiquidityRisk(route);
        uint256 popularityRisk = _calculatePopularityRisk(route);

        risk = (sizeRisk + liquidityRisk + popularityRisk) / 3;
        if (route.usesPrivatePool) {
            risk = risk / 2; // 50% reduction for private pools
        }
    }

    function _findAllRoutes(address, address, uint256) internal view returns (Route[] memory) {
        return new Route[](0);
    }
    function _calculateSizeRisk(Route memory) internal pure returns (uint256) { return 0; }
    function _calculateLiquidityRisk(Route memory) internal pure returns (uint256) { return 0; }
    function _calculatePopularityRisk(Route memory) internal pure returns (uint256) { return 0; }
}

8. MEV Metrics & Monitoring

interface MEVMetrics {
  // Real-time metrics
  metrics: {
    // Sandwich detection
    'mev.sandwich.detected': Counter;
    'mev.sandwich.volume': Gauge;
    'mev.sandwich.profit': Gauge;

    // Front-running
    'mev.frontrun.detected': Counter;
    'mev.frontrun.victims': Counter;

    // User protection
    'mev.protection.activated': Counter;
    'mev.protection.savings': Gauge;

    // Network health
    'mev.extraction.total': Gauge;
    'mev.extraction.percentage': Gauge;
  };

  // Alerts
  alerts: [
    {
      name: 'high_mev_extraction',
      condition: 'mev.extraction.percentage > 5',
      severity: 'warning',
    },
    {
      name: 'sandwich_attack_spike',
      condition: 'rate(mev.sandwich.detected[5m]) > 10',
      severity: 'critical',
    },
  ];
}

MEV Protection Levels

LevelFeaturesLatencyCost
BasicSlippage protection, deadlineNoneFree
Standard+ Batch auction, fair ordering+1 blockLow
Advanced+ Private mempool, encryption+2 blocksMedium
Maximum+ Threshold encryption, TEE+3 blocksHigh

Rationale

The MEV protection mechanisms address the largest source of value extraction from DeFi users:

  1. Commit-reveal prevents front-running by hiding order details until execution is committed
  2. Batch auctions eliminate ordering games by executing all orders at a single clearing price
  3. Private mempools prevent validators and searchers from viewing pending transactions
  4. MEV-aware routing incorporates extraction risk into path optimization
  5. Tiered protection levels allow users to trade off latency for protection

Studies show MEV extraction exceeds $600M annually on Ethereum alone. These protections aim to return that value to users.

Backwards Compatibility

MEV protection integrates with existing infrastructure:

  • Standard DEX interfaces: All protected swaps use ERC-20 approve/transfer patterns
  • Router compatibility: MEVAwareRouter implements ISwapRouter interface
  • Wallet integration: Works with existing wallet signing flows
  • Block builders: Compatible with Flashbots, MEV Boost, and similar systems

Migration path for existing protocols:

  1. Deploy MEV protection contracts alongside existing router
  2. Update frontend to offer protection toggle
  3. Gradually migrate volume to protected routes
  4. Maintain legacy router for backward compatibility

Security Considerations

  1. Encryption key management - Distributed threshold
  2. Validator collusion - Detection and slashing
  3. Timing attacks - Randomized execution
  4. Front-running within batch - Shuffled ordering

Copyright and related rights waived via CC0.