JAMMERC20 Contract

Overview

JAMMERC20 is the standard implementation of LP tokens in JAMM DEX. It not only fully complies with the ERC-20 standard but also supports EIP-2612 Permit functionality. Each JAMMPair contract inherits from JAMMERC20, giving liquidity tokens complete token functionality and modern authorization mechanisms.

Contract Basic Information

contract JAMMERC20 is IJAMMERC20 {
    string public constant name = "JAMM LPs";
    string public constant symbol = "JAMM-LP";
    uint8 public constant decimals = 18;
    uint public totalSupply;
    mapping(address => uint) public balanceOf;
    mapping(address => mapping(address => uint)) public allowance;
}

Token Information:

  • Name: "JAMM LPs" (unified name for all trading pairs)

  • Symbol: "JAMM-LP" (unified symbol for all trading pairs)

  • Decimals: 18 decimal places

  • Supply: Dynamic, changes based on liquidity addition/removal

EIP-2612 Permit Support

bytes32 public DOMAIN_SEPARATOR;
bytes32 public constant PERMIT_TYPEHASH =
    0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
mapping(address => uint) public nonces;

Variable Description:

  • DOMAIN_SEPARATOR: EIP-712 domain separator for signature verification

  • PERMIT_TYPEHASH: Hash value of Permit message type

  • nonces: Nonce for each address to prevent replay attacks

Domain Separator Initialization

constructor() {
    uint chainId;
    assembly {
        chainId := chainid()
    }
    DOMAIN_SEPARATOR = keccak256(
        abi.encode(
            keccak256(
                "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
            ),
            keccak256(bytes(name)),
            keccak256(bytes("1")),
            chainId,
            address(this)
        )
    );
}

Domain Separator Components:

  • Domain type hash

  • Token name hash

  • Version number hash ("1")

  • Chain ID

  • Contract address

Core ERC-20 Functions

Internal Functions

Mint Tokens

function _mint(address to, uint value) internal {
    totalSupply = totalSupply + value;
    balanceOf[to] = balanceOf[to] + value;
    emit Transfer(address(0), to, value);
}

Minting Logic:

  1. Increase total supply

  2. Increase target address balance

  3. Emit Transfer event (from zero address)

Burn Tokens

function _burn(address from, uint value) internal {
    balanceOf[from] = balanceOf[from] - value;
    totalSupply = totalSupply - value;
    emit Transfer(from, address(0), value);
}

Burning Logic:

  1. Decrease source address balance

  2. Decrease total supply

  3. Emit Transfer event (to zero address)

Internal Approval

function _approve(address owner, address spender, uint value) private {
    allowance[owner][spender] = value;
    emit Approval(owner, spender, value);
}

Internal Transfer

function _transfer(address from, address to, uint value) private {
    balanceOf[from] = balanceOf[from] - value;
    balanceOf[to] = balanceOf[to] + value;
    emit Transfer(from, to, value);
}

External Functions

Approval

function approve(address spender, uint value) external returns (bool) {
    _approve(msg.sender, spender, value);
    return true;
}

Transfer

function transfer(address to, uint value) external returns (bool) {
    _transfer(msg.sender, to, value);
    return true;
}

Authorized Transfer

function transferFrom(
    address from,
    address to,
    uint value
) external returns (bool) {
    uint currentAllowance = allowance[from][msg.sender];
    require(currentAllowance >= value, "JAMM: INSUFFICIENT_ALLOWANCE");

    if (currentAllowance != type(uint).max) {
        allowance[from][msg.sender] = currentAllowance - value;
    }
    _transfer(from, to, value);
    return true;
}

Authorized Transfer Features:

  • Check if allowance is sufficient

  • If allowance is not maximum value, decrease allowance

  • Maximum value allowance won't decrease (saves gas)

Permit Function Details

Permit Function

function permit(
    address owner,
    address spender,
    uint value,
    uint deadline,
    uint8 v,
    bytes32 r,
    bytes32 s
) external {
    require(deadline >= block.timestamp, "JAMM: EXPIRED");
    bytes32 digest = keccak256(
        abi.encodePacked(
            "\x19\x01",
            DOMAIN_SEPARATOR,
            keccak256(
                abi.encode(
                    PERMIT_TYPEHASH,
                    owner,
                    spender,
                    value,
                    nonces[owner]++,
                    deadline
                )
            )
        )
    );
    address recoveredAddress = ecrecover(digest, v, r, s);
    require(
        recoveredAddress != address(0) && recoveredAddress == owner,
        "JAMM: INVALID_SIGNATURE"
    );
    _approve(owner, spender, value);
}

Permit Process:

  1. Check deadline

  2. Build EIP-712 message digest

  3. Recover signer address

  4. Verify signer is the owner

  5. Execute approval operation

  6. Increment nonce to prevent replay

Message Structure

Structured data for Permit message:

const types = {
    Permit: [
        { name: 'owner', type: 'address' },
        { name: 'spender', type: 'address' },
        { name: 'value', type: 'uint256' },
        { name: 'nonce', type: 'uint256' },
        { name: 'deadline', type: 'uint256' }
    ]
};

Security Features

Overflow Protection

Uses Solidity 0.8.21's built-in overflow protection:

  • Addition overflow automatically reverts

  • Subtraction underflow automatically reverts

  • No need for additional SafeMath library

Replay Attack Protection

mapping(address => uint) public nonces;
// In permit function
nonces[owner]++

After each permit use, nonce automatically increments to prevent signature reuse.

Signature Verification

address recoveredAddress = ecrecover(digest, v, r, s);
require(
    recoveredAddress != address(0) && recoveredAddress == owner,
    "JAMM: INVALID_SIGNATURE"
);

Strictly verifies signature validity and signer identity.

Usage Examples

Basic ERC-20 Operations

const lpToken = new ethers.Contract(pairAddress, erc20ABI, signer);

// Query balance
const balance = await lpToken.balanceOf(userAddress);
console.log("LP Balance:", ethers.utils.formatEther(balance));

// Query total supply
const totalSupply = await lpToken.totalSupply();
console.log("Total Supply:", ethers.utils.formatEther(totalSupply));

// Transfer
const transferTx = await lpToken.transfer(recipientAddress, amount);
await transferTx.wait();

// Approve
const approveTx = await lpToken.approve(spenderAddress, amount);
await approveTx.wait();

// Transfer from
const transferFromTx = await lpToken.transferFrom(ownerAddress, recipientAddress, amount);
await transferFromTx.wait();

Permit Signature Generation

// Generate Permit signature
async function generatePermitSignature(
    lpTokenAddress,
    owner,
    spender,
    value,
    deadline,
    signer
) {
    const lpToken = new ethers.Contract(lpTokenAddress, erc20ABI, provider);
    
    // Get current nonce
    const nonce = await lpToken.nonces(owner);
    
    // Get chain ID
    const chainId = await signer.getChainId();
    
    // Build domain separator
    const domain = {
        name: 'JAMM LPs',
        version: '1',
        chainId: chainId,
        verifyingContract: lpTokenAddress
    };
    
    // Build message types
    const types = {
        Permit: [
            { name: 'owner', type: 'address' },
            { name: 'spender', type: 'address' },
            { name: 'value', type: 'uint256' },
            { name: 'nonce', type: 'uint256' },
            { name: 'deadline', type: 'uint256' }
        ]
    };
    
    // Build message values
    const message = {
        owner: owner,
        spender: spender,
        value: value,
        nonce: nonce,
        deadline: deadline
    };
    
    // Generate signature
    const signature = await signer._signTypedData(domain, types, message);
    const { v, r, s } = ethers.utils.splitSignature(signature);
    
    return { v, r, s, nonce, deadline };
}

Using Permit for Authorization

// Use Permit for authorization
async function permitApprove(lpTokenAddress, spender, value, signer) {
    const lpToken = new ethers.Contract(lpTokenAddress, erc20ABI, signer);
    const owner = await signer.getAddress();
    const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // Expires in 20 minutes
    
    // Generate signature
    const { v, r, s } = await generatePermitSignature(
        lpTokenAddress,
        owner,
        spender,
        value,
        deadline,
        signer
    );
    
    // Call permit function
    const tx = await lpToken.permit(owner, spender, value, deadline, v, r, s);
    await tx.wait();
    
    console.log("Permit authorization successful");
}

Using Permit in Router

// Remove liquidity with Permit
async function removeLiquidityWithPermit(
    tokenA,
    tokenB,
    fee,
    liquidity,
    amountAMin,
    amountBMin,
    to,
    signer
) {
    const router = new ethers.Contract(routerAddress, routerABI, signer);
    const deadline = Math.floor(Date.now() / 1000) + 60 * 20;
    
    // Calculate pair address
    const pairAddress = await factory.getPair(tokenA, tokenB, fee);
    
    // Generate Permit signature
    const { v, r, s } = await generatePermitSignature(
        pairAddress,
        await signer.getAddress(),
        routerAddress,
        liquidity,
        deadline,
        signer
    );
    
    // Remove liquidity with Permit
    const tx = await router.removeLiquidityWithPermit(
        tokenA,
        tokenB,
        fee,
        liquidity,
        amountAMin,
        amountBMin,
        to,
        deadline,
        false, // approveMax
        v,
        r,
        s
    );
    
    await tx.wait();
    console.log("Liquidity removal successful");
}

Event Listening

Listen to Transfer Events

const lpToken = new ethers.Contract(pairAddress, erc20ABI, provider);

// Listen to all transfers
lpToken.on("Transfer", (from, to, value) => {
    if (from === ethers.constants.AddressZero) {
        console.log("LP token minted:", ethers.utils.formatEther(value));
    } else if (to === ethers.constants.AddressZero) {
        console.log("LP token burned:", ethers.utils.formatEther(value));
    } else {
        console.log("LP token transfer:", {
            from: from,
            to: to,
            value: ethers.utils.formatEther(value)
        });
    }
});

Listen to Approval Events

lpToken.on("Approval", (owner, spender, value) => {
    console.log("Approval event:", {
        owner: owner,
        spender: spender,
        value: ethers.utils.formatEther(value)
    });
});

Last updated