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
Permit-Related State Variables
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 verificationPERMIT_TYPEHASH
: Hash value of Permit message typenonces
: 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:
Increase total supply
Increase target address balance
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:
Decrease source address balance
Decrease total supply
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:
Check deadline
Build EIP-712 message digest
Recover signer address
Verify signer is the owner
Execute approval operation
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