cpamm-mev

A Uniswap V2 style liquidity pool written in Solidity, and a calculator that uses live reserves from Uniswap V2 on Ethereum mainnet to show price impact, sandwich attacks, arbitrage against Binance, and impermanent loss.

1. Price impact

Swaps move along the curve x × y = k, so larger trades get a worse average price. The 0.3% fee stays in the pool.

Received-
Average price-
Price impact-
Fee paid to liquidity providers-

2. Sandwich attack

A bot buys just before a victim's swap and sells just after it. The victim's slippage tolerance limits how far the bot can move the price. The calculator finds the front-run size that maximises the bot's profit.

Best front-run size-
Bot profit after gas-
Victim receives less by-

Chart: bot profit against front-run size. The dashed line is the largest front-run the victim's slippage limit allows.

3. Arbitrage against Binance

When the pool price differs from Binance by more than the 0.3% fee, there is a trade that moves the pool back to the market price. Optimal input: (√(γ·x·y·P) − y) / γ, where γ = 0.997.

Pool vs Binance-
Trade-
Profit before gas-
Pool price after-

4. Impermanent loss

Compared with holding the two tokens, a liquidity provider's position is worth 2√r / (1 + r) − 1 less, where r is the price ratio. Trading fees offset this.

Value if held-
Value as liquidity provider, with fees-
Impermanent loss-
Fee APR needed to break even-

5. The contract

contracts/MiniPair.sol is a constant-product pool for two ERC-20 tokens with LP shares, a 0.3% fee, slippage and deadline checks, and reentrancy protection. The Hardhat tests use the same math library as this page: they plan a sandwich attack off-chain, run it against the contract, and check that the profit matches exactly.

function swapExactIn(address tokenIn, uint256 amountIn, uint256 minOut, address to, uint256 deadline)
    external nonReentrant beforeDeadline(deadline) returns (uint256 amountOut)
{
    bool zeroForOne;
    if (tokenIn == address(token0)) zeroForOne = true;
    else if (tokenIn != address(token1)) revert InvalidToken();

    (uint256 rIn, uint256 rOut) = zeroForOne ? (reserve0, reserve1) : (reserve1, reserve0);
    amountOut = getAmountOut(amountIn, rIn, rOut);
    if (amountOut < minOut) revert InsufficientOutput(amountOut, minOut);

    uint256 kBefore = uint256(reserve0) * reserve1;
    IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
    (zeroForOne ? token1 : token0).safeTransfer(to, amountOut);
    _sync();
    assert(uint256(reserve0) * reserve1 >= kBefore);
    emit Swap(msg.sender, tokenIn, amountIn, amountOut, to);
}

Tests: 8 contract tests (Hardhat) and 4 tests of the JavaScript math.

Reserves from publicnode.com, prices from Binance. Not audited. Source: github.com/hpark236/cpamm-mev