Lab · the bug class behind RF-01

Small outputs, zero input

An exact-output swap works backwards: you name what you want out, and the pool computes what you must pay. Round that division down and, when the input token has few decimals, small outputs cost nothing.

A minimal pool written for this page, not Revert Finance’s code. The accepted finding is RF-01, medium, on Cantina.

Constant product, no feepool

reserveIn
100000000 units1,000,000.00 GUSD
reserveOut
1000000000000000000000000 wei1,000,000 DAI

Run the swap

Press Swap ×100 and read what you paid. Then apply the fix and swap again.

= 0.005 DAI

amountIn = 0 GUSD units — free outputQuote for 5000000000000000 wei (0.005 DAI) on the current pool.
Taken
0 DAI0 wei
Paid
Swaps
0 executed · 0 free

The source

Bug · rounds down in use

function getAmountIn(
  uint256 amountOut,
  uint256 reserveIn,
  uint256 reserveOut
) internal pure returns (uint256) {
  return amountOut * reserveIn
    / (reserveOut - amountOut); // rounds down
}

Fix · rounds up in use

  return Math.mulDiv(
    amountOut,
    reserveIn,
    reserveOut - amountOut,
    Math.Rounding.Ceil
  );

Round in the pool’s favor. Rounding up costs the trader at most one unit. Rounding down costs the pool.

Impact

Rounding down undercharges by less than one unit of the input token per call. With GUSD’s two decimals, that means each call on this pool can take just under 0.01 DAI for nothing, and taking 1 DAI takes 101 free swaps. On Ethereum mainnet the gas for a swap usually costs more than that. It still breaks the pool’s promise that output is never free, which matters on cheap chains, at scale, and anywhere other code relies on it.

Stage 4 of 4 · one question

What is the largest amountOut (in wei) that costs zero input on the initial pool?

The simulator will get you there, and so will the inequality. Reset first: every swap moves the edge.

Digits only. The check runs in your browser.