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
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:
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.
0x21fb0317b1cb8af900b47ec04f932891e3d91dda70b5d0ddcd0f33aca7486b85This 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:
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.
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.