Section 3 of 16

Build
+15 Lynx

Pair: State and Initialization

What You Are Building

UniswapV2Pair is the core AMM contract. It inherits UniswapV2ERC20 (the LP token you just built) and adds everything that makes the constant product market maker work: reserves, the oracle, minting, burning, and swapping.

In this section you are setting up the foundation. The state variables, the initialization function, a safe ERC-20 transfer helper, the reentrancy guard, and the reserve getter. Nothing here moves tokens or enforces invariants yet. That comes in the next sections. But every function in the pair depends on what you write here.

State Variables and Storage Packing

The pair stores two token addresses and three reserve values. Uniswap V2 packs the reserves into a single storage slot using smaller types:

uint112 reserve0;
uint112 reserve1;
uint32 blockTimestampLast;

112 + 112 + 32 = 256 bits. One slot. This means getReserves() reads one SLOAD instead of three. On every swap, every mint, every burn, the contract reads reserves. Packing them saves thousands of gas over the lifetime of the pair.

Why uint112? Because 2^112 is roughly 5.19 * 10^33. With 18 decimal tokens, that supports balances up to about 5.19 * 10^15 tokens. More than enough for any real token. And the 32 bits left over hold a Unix timestamp, which wraps in the year 2106.

The initialize() Function

Pairs are deployed by the Factory. The Factory calls initialize(token0, token1) exactly once, right after deploying the pair. This sets which two tokens the pair trades.

Why not use constructor arguments? Because the Factory uses CREATE2 to deploy pairs with deterministic addresses. The CREATE2 address depends on the init code hash. If token addresses were constructor arguments, they would be part of the init code, and each pair would have a different hash. By using initialize() instead, all pairs share the same init code, and the Factory can compute any pair's address from just the two token addresses and a fixed hash.

The initialize() function must be callable only by the factory. Store msg.sender in the constructor as the factory address, then check it in initialize().

The _safeTransfer() Helper

ERC-20 tokens are not standardized in their return values. Some return true on success, some return nothing (USDT is the famous example). Uniswap V2 handles this with a low-level call pattern:

  1. Encode the transfer(address,uint256) call using abi.encodeWithSelector.
  2. Execute it with a low-level .call().
  3. Require that the call succeeded (no revert).
  4. If the call returned data, decode it as a bool and require it to be true.

This pattern handles both compliant tokens (return true) and non-compliant tokens (return nothing). It reverts on tokens that return false or revert.

The lock Modifier

The pair uses a reentrancy guard on mint(), burn(), and swap(). The pattern is simple: a uint256 unlocked variable starts at 1. The modifier sets it to 0 before execution, runs the function body, then sets it back to 1. Any reentrant call sees unlocked == 0 and reverts.

This is the classic reentrancy guard. Uniswap V2 was written before OpenZeppelin standardized ReentrancyGuard, so it rolls its own.

Your Task

The starter code gives you the full contract declaration with all state variables, events, and constants already defined. You need to implement four things:

  1. initialize(): Set token0 and token1. Only callable by factory.
  2. getReserves(): Return reserve0, reserve1, and blockTimestampLast from storage.
  3. _safeTransfer(): Execute a low-level ERC-20 transfer that handles non-compliant tokens.
  4. lock modifier: The reentrancy guard using the unlocked state variable.

Your Code

Solution.sol
Solidity
Loading editor...

Requirements

lock modifier requires unlocked == 1
lock modifier sets unlocked to 0
lock modifier resets unlocked to 1
getReserves returns reserve0
getReserves returns reserve1
getReserves returns blockTimestampLast
_safeTransfer uses low-level call
_safeTransfer uses abi.encodeWithSelector
_safeTransfer checks success and return data
initialize checks factory
initialize sets token0
initialize sets token1