Documentation

V2 core

ArmoryV2Factory and ArmoryV2Pair, covering CREATE2 addressing, the pair init code hash, the K invariant, TWAP cumulatives, and every deviation from canonical UniswapV2.

On this page
ContractAddressApeScan
ArmoryV2Factory0x7AA7…50D5

ArmoryV2Factory

Canonical UniswapV2Factory, renamed. Solidity 0.5.16.

address public feeTo;
address public feeToSetter;
mapping(address => mapping(address => address)) public getPair;
address[] public allPairs;
 
event PairCreated(address indexed token0, address indexed token1, address pair, uint);
 
constructor(address _feeToSetter) public;
 
function allPairsLength() external view returns (uint);
function createPair(address tokenA, address tokenB) external returns (address pair);
function setFeeTo(address _feeTo) external;
function setFeeToSetter(address _feeToSetter) external;

createPair sorts the two tokens ascending by address, deploys ArmoryV2Pair with CREATE2 under salt = keccak256(abi.encodePacked(token0, token1)), calls initialize(token0, token1) on the new pair, and writes getPair in both directions. Reverts:

ConditionRevert string
tokenA == tokenBUniswapV2: IDENTICAL_ADDRESSES
sorted token0 is the zero addressUniswapV2: ZERO_ADDRESS
the pair already existsUniswapV2: PAIR_EXISTS

setFeeTo and setFeeToSetter both require msg.sender == feeToSetter and revert UniswapV2: FORBIDDEN otherwise. feeToSetter is held by ArmoryDexAdmin, not by an EOA. See access control.

The pair init code hash

You cannot derive a pair address off-chain without it, and Armory's differs from Uniswap mainnet's because the LP token's name and symbol string constants changed, which changed the pair's creation code.

0x21fb0317b1cb8af900b47ec04f932891e3d91dda70b5d0ddcd0f33aca7486b85

This hash is bound to the pair bytecode

Any change to ArmoryV2Pair (a source edit, a compiler bump, an optimizer settings change, or a brand rename that touches the LP token strings) produces a new hash. Everything that derives pair addresses off-chain has to be updated in lockstep. Use the value published for the deployment you are targeting.

The derivation:

pair = address(uint(keccak256(abi.encodePacked(
    hex'ff',
    factory,
    keccak256(abi.encodePacked(token0, token1)),
    hex'21fb0317b1cb8af900b47ec04f932891e3d91dda70b5d0ddcd0f33aca7486b85'
))));

ArmoryV2Pair

Canonical UniswapV2Pair, renamed, inheriting ArmoryV2ERC20.

uint public constant MINIMUM_LIQUIDITY = 10**3;
 
address public factory;
address public token0;
address public token1;
 
uint public price0CumulativeLast;
uint public price1CumulativeLast;
uint public kLast;
 
function getReserves() public view
    returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast);
 
function initialize(address _token0, address _token1) external;
function mint(address to) external returns (uint liquidity);
function burn(address to) external returns (uint amount0, uint amount1);
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
function skim(address to) external;
function sync() external;

Reserves are uint112 packed with a uint32 blockTimestampLast into one slot. They are private and only readable through getReserves().

Events:

event Mint(address indexed sender, uint amount0, uint amount1);
event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
event Swap(
    address indexed sender,
    uint amount0In, uint amount1In,
    uint amount0Out, uint amount1Out,
    address indexed to
);
event Sync(uint112 reserve0, uint112 reserve1);

The LP fee and the K invariant

The swap fee is fixed at 0.3%, taken on the input side, and it is not configurable. It is enforced by the invariant check inside swap, not by a separate transfer:

uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
require(
    balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2),
    'UniswapV2: K'
);

The matching closed form, which the periphery library uses for quotes, is amountOut = (amountIn * 997 * reserveOut) / (reserveIn * 1000 + amountIn * 997).

swap is the low-level entry point. It transfers the requested outputs optimistically, then, if data.length > 0, calls IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data) before measuring balances. That is the flash-swap hook. Other reverts in swap:

ConditionRevert string
both outputs zeroUniswapV2: INSUFFICIENT_OUTPUT_AMOUNT
an output is not strictly less than its reserveUniswapV2: INSUFFICIENT_LIQUIDITY
to is token0 or token1UniswapV2: INVALID_TO
nothing was paid inUniswapV2: INSUFFICIENT_INPUT_AMOUNT
reentrancyUniswapV2: LOCKED

MINIMUM_LIQUIDITY

On the first mint into an empty pair, sqrt(amount0 * amount1) - 1000 LP tokens go to the depositor and 1000 are minted to the zero address permanently. Budget for it when you compute an initial deposit's share, and never assume the first LP can withdraw its full deposit. Subsequent mints take min(amount0 * totalSupply / reserve0, amount1 * totalSupply / reserve1) and revert UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED on zero.

burn reverts UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED if either side would round to zero. It computes payouts from balances, not reserves, so a pair holding donated tokens pays them out pro rata.

The protocol fee is lazy

If factory.feeTo() is non-zero, _mintFee runs at the top of every mint and every burn, never on a swap. It mints LP tokens representing 1/6 of the growth in sqrt(k) since the last liquidity event, using kLast as the watermark:

uint rootK     = Math.sqrt(uint(_reserve0).mul(_reserve1));
uint rootKLast = Math.sqrt(_kLast);
if (rootK > rootKLast) {
    uint numerator   = totalSupply.mul(rootK.sub(rootKLast));
    uint denominator = rootK.mul(5).add(rootKLast);
    uint liquidity   = numerator / denominator;
    if (liquidity > 0) _mint(feeTo, liquidity);
}

Two consequences an integrator has to model. First, totalSupply can increase inside your own mint or burn transaction before your share is computed. The code re-reads totalSupply after _mintFee precisely because of this. Second, if the fee is switched off, kLast is zeroed and the accrued-but-unminted growth is forgiven.

TWAP cumulatives

_update runs once per block per pair and accumulates UQ112x112 prices:

price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;

Both the accumulators and blockTimestampLast wrap deliberately. blockTimestamp is uint32(block.timestamp % 2**32) and the subtraction is expected to overflow, so compute deltas with wrapping arithmetic. Accumulators advance only when timeElapsed > 0 and both reserves are non-zero, so a pair that has not been touched this block gives you a stale endpoint. Combine getReserves() with the current timestamp if you need a spot correction, in the standard V2 oracle pattern.

skim and sync

skim(to) sends balance - reserve on both sides to to. sync() forces reserves up to balances. Both exist because the pair trusts its own reserve accounting over its token balances, and both are permissionless. If you send tokens to a pair without calling swap or mint in the same transaction, anyone can skim them.

ArmoryV2ERC20, the LP token

Standard V2 LP ERC-20 with EIP-2612 permit. The only deviation from upstream:

string public constant name     = 'Armory V2';
string public constant symbol   = 'ARMORY-V2';
uint8  public constant decimals = 18;

DOMAIN_SEPARATOR is computed in the constructor from that name, version "1", the chain id read at construction time, and the pair address. PERMIT_TYPEHASH is the standard keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)") = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9.

Sign permits against the pair's own name

The EIP-712 domain name is Armory V2, not Uniswap V2. A signature built with the wrong domain name recovers a different address and the pair reverts UniswapV2: INVALID_SIGNATURE.

transferFrom treats type(uint256).max allowance as infinite and does not decrement it. permit reverts UniswapV2: EXPIRED past the deadline.

Deviations from canonical UniswapV2

The complete list.

#DeviationEffect on you
1Contract names are ArmoryV2Factory / ArmoryV2Pair / ArmoryV2ERC20Artifact names only. ABIs and interface names (IUniswapV2Pair, and so on) are unchanged.
2LP token name = Armory V2, symbol = ARMORY-V2Different DOMAIN_SEPARATOR; different pair creation bytecode.
3Init code hash constant in UniswapV2Library.pairForUse 0x21fb0317…7486b85, not Uniswap's.
4Pragma cap <0.8.0 added to UniswapV2LibraryCompile-time only.

There is no fifth. The factory, the pair, the swap fee, the K invariant, MINIMUM_LIQUIDITY, the TWAP accumulators and the feeTo mechanism are all stock.

The router that wraps all of this is on the V2 periphery page.