JAMMPair Contract
Overview
JAMMPair is the core trading pair contract of JAMM DEX, implementing specific AMM liquidity pool functionality. Each trading pair is an independent JAMMPair contract instance, responsible for managing reserves of two tokens, executing swap logic, handling liquidity operations, and maintaining price oracle data.
Contract Inheritance Structure
contract JAMMPair is JAMMERC20 {
using UQ112x112 for uint224;
// ...
}
JAMMPair inherits from JAMMERC20, meaning each trading pair is itself an ERC-20 token (LP token).
Core Constants
uint public constant MINIMUM_LIQUIDITY = 10 ** 3; // 1000
bytes4 private constant SELECTOR = bytes4(keccak256(bytes("transfer(address,uint256)")));
MINIMUM_LIQUIDITY
: Permanently locked minimum liquidity amountSELECTOR
: ERC-20 transfer function selector for safe transfers
State Variables
Basic Information
address public factory; // Factory contract address
address public token0; // First token address (smaller address)
address public token1; // Second token address (larger address)
uint24 public fee; // Trading fee rate
Reserve Management
uint112 private reserve0; // token0 reserves
uint112 private reserve1; // token1 reserves
uint32 private blockTimestampLast; // Last update block timestamp
Storage Optimization: Three variables packed in one storage slot:
uint112 + uint112 + uint32 = 256 bits
Price Oracle
uint public price0CumulativeLast; // token0 cumulative price
uint public price1CumulativeLast; // token1 cumulative price
uint public kLast; // Last k value (reserve0 * reserve1)
Reentrancy Protection
uint private unlocked = 1;
modifier lock() {
require(unlocked == 1, "JAMM: LOCKED");
unlocked = 0;
_;
unlocked = 1;
}
Initialization
Constructor
constructor() {
factory = msg.sender;
}
Constructor only sets factory address, actual initialization is done through initialize
function.
Initialize Function
function initialize(
address _token0,
address _token1,
uint24 _fee
) external {
require(msg.sender == factory, "JAMM: FORBIDDEN");
token0 = _token0;
token1 = _token1;
fee = _fee;
}
Security: Only Factory contract can call the initialize function.
Core Functions
Reserve Query
function getReserves()
public
view
returns (
uint112 _reserve0,
uint112 _reserve1,
uint32 _blockTimestampLast
)
{
_reserve0 = reserve0;
_reserve1 = reserve1;
_blockTimestampLast = blockTimestampLast;
}
This is the most commonly used query function, returning current reserves and last update time.
Reserve Update
function _update(
uint balance0,
uint balance1,
uint112 _reserve0,
uint112 _reserve1
) private {
require(
balance0 <= type(uint112).max && balance1 <= type(uint112).max,
"JAMM: OVERFLOW"
);
uint32 blockTimestamp = uint32(block.timestamp % 2 ** 32);
uint32 timeElapsed = blockTimestamp - blockTimestampLast;
if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
// Update cumulative prices
price0CumulativeLast +=
uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) *
timeElapsed;
price1CumulativeLast +=
uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) *
timeElapsed;
}
reserve0 = uint112(balance0);
reserve1 = uint112(balance1);
blockTimestampLast = blockTimestamp;
emit Sync(reserve0, reserve1);
}
Update Logic:
Check balances won't overflow uint112
Calculate time elapsed
Update cumulative prices (if time elapsed and reserves are non-zero)
Update reserves and timestamp
Emit Sync event
Liquidity Management
Add Liquidity (Mint)
function mint(address to) external lock returns (uint liquidity) {
(uint112 _reserve0, uint112 _reserve1, ) = getReserves();
uint balance0 = JAMMERC20(token0).balanceOf(address(this));
uint balance1 = JAMMERC20(token1).balanceOf(address(this));
uint amount0 = balance0 - _reserve0;
uint amount1 = balance1 - _reserve1;
bool feeOn = _mintFee(_reserve0, _reserve1);
uint _totalSupply = totalSupply;
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY;
_mint(address(0), MINIMUM_LIQUIDITY); // Permanently lock
} else {
liquidity = Math.min(
(amount0 * _totalSupply) / _reserve0,
(amount1 * _totalSupply) / _reserve1
);
}
require(liquidity > 0, "JAMM: INSUFFICIENT_LIQUIDITY_MINTED");
_mint(to, liquidity);
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0) * uint(reserve1);
emit Mint(msg.sender, amount0, amount1);
}
Minting Process:
Get current reserves and balances
Calculate newly added token amounts
Handle protocol fees
Calculate LP tokens to mint
Mint LP tokens to specified address
Update reserves
Update k value (if protocol fees enabled)
Emit Mint event
Remove Liquidity (Burn)
function burn(
address to
) external lock returns (uint amount0, uint amount1) {
(uint112 _reserve0, uint112 _reserve1, ) = getReserves();
address _token0 = token0;
address _token1 = token1;
uint balance0 = JAMMERC20(_token0).balanceOf(address(this));
uint balance1 = JAMMERC20(_token1).balanceOf(address(this));
uint liquidity = balanceOf[address(this)];
bool feeOn = _mintFee(_reserve0, _reserve1);
uint _totalSupply = totalSupply;
amount0 = (liquidity * balance0) / _totalSupply;
amount1 = (liquidity * balance1) / _totalSupply;
require(
amount0 > 0 && amount1 > 0,
"JAMM: INSUFFICIENT_LIQUIDITY_BURNED"
);
_burn(address(this), liquidity);
_safeTransfer(_token0, to, amount0);
_safeTransfer(_token1, to, amount1);
balance0 = JAMMERC20(_token0).balanceOf(address(this));
balance1 = JAMMERC20(_token1).balanceOf(address(this));
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0) * uint(reserve1);
emit Burn(msg.sender, amount0, amount1, to);
}
Burning Process:
Get current state
Handle protocol fees
Calculate proportional token amounts to return
Burn LP tokens
Transfer tokens to specified address
Update reserves
Emit Burn event
Token Swapping
Main Swap Function
function swap(
uint amount0Out,
uint amount1Out,
address to,
bytes calldata data,
address _referrer
) external lock {
require(
amount0Out > 0 || amount1Out > 0,
"JAMM: INSUFFICIENT_OUTPUT_AMOUNT"
);
(uint112 _reserve0, uint112 _reserve1, ) = getReserves();
require(
amount0Out < _reserve0 && amount1Out < _reserve1,
"JAMM: INSUFFICIENT_LIQUIDITY"
);
uint balance0;
uint balance1;
{
require(to != token0 && to != token1, "JAMM: INVALID_TO");
if (amount0Out > 0) _safeTransfer(token0, to, amount0Out);
if (amount1Out > 0) _safeTransfer(token1, to, amount1Out);
if (data.length > 0)
IJAMMCallee(to).jammCall(
msg.sender,
amount0Out,
amount1Out,
data,
_referrer
);
balance0 = JAMMERC20(token0).balanceOf(address(this));
balance1 = JAMMERC20(token1).balanceOf(address(this));
}
uint amount0In = balance0 > _reserve0 - amount0Out
? balance0 - (_reserve0 - amount0Out)
: 0;
uint amount1In = balance1 > _reserve1 - amount1Out
? balance1 - (_reserve1 - amount1Out)
: 0;
require(
amount0In > 0 || amount1In > 0,
"JAMM: INSUFFICIENT_INPUT_AMOUNT"
);
_collectFee(amount0In, amount1In, _referrer);
{
balance0 = JAMMERC20(token0).balanceOf(address(this));
balance1 = JAMMERC20(token1).balanceOf(address(this));
uint balance0Adjusted = (balance0 *
10000 -
(amount0In * fee * 4) /
5);
uint balance1Adjusted = (balance1 *
10000 -
(amount1In * fee * 4) /
5);
require(
balance0Adjusted * balance1Adjusted >=
uint(_reserve0) * uint(_reserve1) * (10000 ** 2),
"JAMM: K"
);
}
_update(balance0, balance1, _reserve0, _reserve1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}
Swap Process:
Validate output amounts and liquidity sufficiency
Optimistic transfer (transfer tokens first)
Support flash loan callback
Calculate actual input amounts
Collect trading fees
Verify constant product formula
Update reserves
Emit Swap event
Fee Collection
function _collectFee(
uint amount0In,
uint amount1In,
address _referrer
) private {
address referrer = IJAMMFactory(factory).referrer(tx.origin);
address feeTo = IJAMMFactory(factory).feeTo();
if (_referrer == address(0) && referrer == address(0)) {
// No referrer
_safeTransferFee(token0, feeTo, (amount0In * fee) / 50000);
_safeTransferFee(token1, feeTo, (amount1In * fee) / 50000);
} else {
// Has referrer
_safeTransferFee(token0, feeTo, (amount0In * fee) / 100000);
_safeTransferFee(token1, feeTo, (amount1In * fee) / 100000);
if (referrer != address(0)) {
_safeTransferFee(token0, referrer, (amount0In * fee) / 100000);
_safeTransferFee(token1, referrer, (amount1In * fee) / 100000);
} else {
IJAMMFactory(factory).setReferrer(_referrer);
_safeTransferFee(token0, _referrer, (amount0In * fee) / 100000);
_safeTransferFee(token1, _referrer, (amount1In * fee) / 100000);
}
}
}
Fee Distribution Logic:
No referrer: Protocol collects
fee/50000
of feesHas referrer: Protocol and referrer each collect
fee/100000
of fees
Protocol Fee Minting
function _mintFee(
uint112 _reserve0,
uint112 _reserve1
) private returns (bool feeOn) {
address mintTo = IJAMMFactory(factory).mintTo();
feeOn = mintTo != address(0);
uint _kLast = kLast;
if (feeOn) {
if (_kLast != 0) {
uint rootK = Math.sqrt(uint(_reserve0) * uint(_reserve1));
uint rootKLast = Math.sqrt(_kLast);
if (rootK > rootKLast) {
uint numerator = totalSupply * (rootK - rootKLast) * 8;
uint denominator = rootK * 17 + rootKLast * 8;
uint liquidity = numerator / denominator;
if (liquidity > 0) _mint(mintTo, liquidity);
}
}
} else if (_kLast != 0) {
kLast = 0;
}
}
Protocol Fee Calculation:
Based on k value growth to calculate protocol fees
Formula:
liquidity = totalSupply * (√k - √kLast) * 8 / (√k * 17 + √kLast * 8)
Safe Transfer
Safe Transfer Function
function _safeTransfer(address token, address to, uint value) private {
if (value == 0) {
return;
}
(bool success, bytes memory data) = token.call(
abi.encodeWithSelector(SELECTOR, to, value)
);
require(
success && (data.length == 0 || abi.decode(data, (bool))),
"JAMM: TRANSFER_FAILED"
);
}
Fee Transfer Function
function _safeTransferFee(address token, address to, uint value) private {
_safeTransfer(token, to, value);
if (value > 0) {
emit Fee(tx.origin, to, token, value);
}
}
Features:
Handles non-standard ERC-20 tokens
Zero value transfers return directly
Fee transfers emit Fee events
Utility Functions
Skim Function
function skim(address to) external lock {
address _token0 = token0;
address _token1 = token1;
_safeTransfer(
_token0,
to,
JAMMERC20(_token0).balanceOf(address(this)) - reserve0
);
_safeTransfer(
_token1,
to,
JAMMERC20(_token1).balanceOf(address(this)) - reserve1
);
}
Purpose: Remove excess token balances beyond reserves.
Sync Function
function sync() external lock {
_update(
JAMMERC20(token0).balanceOf(address(this)),
JAMMERC20(token1).balanceOf(address(this)),
reserve0,
reserve1
);
}
Purpose: Force synchronization of reserves with actual balances.
Event System
Core 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);
event Fee(address indexed sender, address indexed referrer, address token, uint amount);
Usage Examples
Query Trading Pair Information
const pair = new ethers.Contract(pairAddress, pairABI, provider);
// Get basic information
const token0 = await pair.token0();
const token1 = await pair.token1();
const fee = await pair.fee();
console.log("Token0:", token0);
console.log("Token1:", token1);
console.log("Fee rate:", fee);
// Get reserves
const reserves = await pair.getReserves();
console.log("Reserve0:", ethers.utils.formatEther(reserves.reserve0));
console.log("Reserve1:", ethers.utils.formatEther(reserves.reserve1));
console.log("Last updated:", new Date(reserves.blockTimestampLast * 1000));
Listen to Swap Events
pair.on("Swap", (sender, amount0In, amount1In, amount0Out, amount1Out, to) => {
console.log("Swap event:");
console.log("- Sender:", sender);
console.log("- Input0:", ethers.utils.formatEther(amount0In));
console.log("- Input1:", ethers.utils.formatEther(amount1In));
console.log("- Output0:", ethers.utils.formatEther(amount0Out));
console.log("- Output1:", ethers.utils.formatEther(amount1Out));
console.log("- Recipient:", to);
});
Listen to Fee Events
pair.on("Fee", (sender, referrer, token, amount) => {
console.log("Fee event:");
console.log("- Trader:", sender);
console.log("- Referrer:", referrer);
console.log("- Token:", token);
console.log("- Amount:", ethers.utils.formatEther(amount));
});
Last updated