@juiceswapxyz/launchpad
v2.1.0
Published
Production-ready token launchpad with bonding curve mechanism, secure metadata support, and automatic DEX graduation for Citrea
Readme
@juiceswapxyz/launchpad
Token launchpad with bonding curve mechanism and automatic DEX graduation for JuiceSwap on Citrea.
Installation
npm install @juiceswapxyz/launchpadOverview
Tokens launch on a constant-product bonding curve. When fully sold, they automatically graduate to JuiceSwap V2 with permanently locked liquidity.
- All tokens trade against JUSD (Juice Dollar)
- Graduation creates TOKEN/JUSD pairs on JuiceSwap V2
- LP tokens burned to
0xdead(permanent lock)
Frontend Integration
Exports
| Export | Description |
|--------|-------------|
| TokenFactoryABI | ABI for creating tokens and querying factory |
| BondingCurveTokenABI | ABI for buy/sell/graduate operations |
| ADDRESS | Contract addresses by chain ID |
| LAUNCHPAD_CONSTANTS | Protocol constants (supply, reserves, fees) |
| getAddresses(chainId) | Helper to get addresses for a chain |
| isChainSupported(chainId) | Check if chain is supported |
Usage
import { TokenFactoryABI, ADDRESS } from '@juiceswapxyz/launchpad';
import { getContract } from 'viem';
const factory = getContract({
address: ADDRESS[5115].factory, // Citrea Testnet
abi: TokenFactoryABI,
client: publicClient,
});
const hash = await factory.write.createToken([
'My Token',
'MTK',
'ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdsgaTQ'
]);Token Metadata
All tokens MUST include metadata when created. Metadata provides essential token information (logo, description, social links) and is immutable after creation.
Storage Options
IPFS (Recommended)
- Decentralized, content-addressed storage
- Industry standard for token metadata
- Format:
ipfs://Qm... - Services: Pinata, Web3.Storage, Infura
Arweave
- Permanent pay-once storage
- Format:
ar://...
HTTPS (Not Recommended)
- Centralized, mutable, can disappear
- Only use for testing
Metadata JSON Structure
The metadata follows the OpenSea Metadata Standard:
{
"name": "My Awesome Token",
"description": "A revolutionary token launching on Citrea with fair bonding curve distribution",
"image": "ipfs://QmImageHash.../logo.png",
"external_url": "https://mytoken.com",
"attributes": [
{ "trait_type": "Category", "value": "Meme" },
{ "trait_type": "Network", "value": "Citrea" }
],
"properties": {
"website": "https://mytoken.com",
"twitter": "https://twitter.com/mytoken",
"telegram": "https://t.me/mytoken",
"discord": "https://discord.gg/mytoken"
}
}Required Fields:
| Field | Type | Description |
|-------|------|-------------|
| name | string | Token name (should match on-chain name) |
| description | string | Token purpose and value proposition (100-500 chars recommended) |
| image | URI | Logo/avatar (512x512px PNG/SVG, IPFS recommended) |
Optional Fields:
external_url- Project websiteattributes- Categorical traits for filtering (e.g., Category, Network, Launch Type)properties- Social links and additional metadata (twitter, telegram, discord, whitepaper, etc.)
Security Constraints
The contract enforces these validation rules:
| Field | Maximum | Validation | |-------|---------|------------| | Name | 100 characters | No control characters (0x00-0x1F, 0x7F), allows Unicode | | Symbol | 20 characters | Uppercase A-Z and 0-9 only | | Metadata URI | 1,024 bytes | No control characters |
Forbidden Characters:
- Null bytes (
\x00) - Control characters (
\x01-\x1F) - newline, tab, escape codes, etc. - DEL character (
\x7F)
Allowed:
- ASCII printable characters (0x20-0x7E) ✅
- Unicode characters (emoji, international text) ✅
- URL encoding (%, &, =) ✅
Contract Behavior
What the contract validates:
- ✅ Metadata URI is non-empty
- ✅ URI length ≤ 1KB
- ✅ No control characters in URI
- ✅ Name/symbol length limits
- ✅ Symbol contains only A-Z, 0-9
What the contract does NOT validate:
- ❌ URI format or reachability
- ❌ JSON structure or content
- ❌ Image existence or validity
Note: Metadata is immutable after token creation (prevents rug pulls). Content validation is the responsibility of frontends and indexers.
How to Upload Metadata
Option 1: Pinata (Easiest)
- Sign up at pinata.cloud
- Upload logo image → copy IPFS hash (e.g.,
QmImageHash...) - Create
metadata.jsonwith image URI:ipfs://QmImageHash... - Upload
metadata.json→ copy metadata hash (e.g.,QmMetadataHash...) - Use metadata hash when creating token:
ipfs://QmMetadataHash...
Option 2: IPFS CLI
# Upload image
ipfs add logo.png
# Returns: QmImageHash...
# Create metadata.json
cat > metadata.json <<EOF
{
"name": "My Token",
"description": "Token description here",
"image": "ipfs://QmImageHash..."
}
EOF
# Upload metadata
ipfs add metadata.json
# Returns: QmMetadataHash...Creating a Token
import { TokenFactoryABI, ADDRESS } from '@juiceswapxyz/launchpad';
// After uploading metadata to IPFS
const metadataURI = "ipfs://QmMetadataHash...";
const hash = await factory.write.createToken([
'My Awesome Token',
'MAT',
metadataURI
]);Fetching Metadata in Frontend
// Fetch metadata from IPFS using public gateway
async function fetchMetadata(uri: string) {
if (uri.startsWith('ipfs://')) {
const hash = uri.replace('ipfs://', '');
const url = `https://ipfs.io/ipfs/${hash}`;
const response = await fetch(url);
return await response.json();
}
if (uri.startsWith('ar://')) {
const id = uri.replace('ar://', '');
const url = `https://arweave.net/${id}`;
const response = await fetch(url);
return await response.json();
}
// HTTPS - direct fetch
return await fetch(uri).then(r => r.json());
}Tip: Use multiple IPFS gateways for reliability (ipfs.io, cloudflare-ipfs.com, gateway.pinata.cloud).
Best Practices
✅ Use IPFS for permanent, decentralized storage ✅ Include all required fields (name, description, image) ✅ Add social links in properties for community discovery ✅ Use high-quality images (512x512px minimum, PNG/SVG) ✅ Pin IPFS content to ensure availability (use Pinata, Infura, Web3.Storage) ✅ Test metadata before deploying token ❌ Don't use HTTPS for production (centralized, can disappear) ❌ Don't include sensitive info in metadata (all public)
FAQ
Can I update metadata after token creation? No. Metadata is immutable to prevent rug pulls and maintain trust.
What if my IPFS content becomes unavailable? Pin your content on multiple IPFS nodes or use pinning services (Pinata, Infura, Web3.Storage).
Does the contract validate my JSON structure? No. Only the URI format is validated. Content validation (JSON parsing, required fields) is the responsibility of frontends and indexers.
What image formats and sizes are supported? PNG and SVG are recommended. Keep file size under 500KB. Minimum 512x512px for logos. Animated PNGs (APNG) and animated SVGs are supported.
Contracts
| Contract | Description |
|----------|-------------|
| TokenFactory.sol | Factory using EIP-1167 minimal proxies |
| BondingCurveToken.sol | ERC20 with bonding curve and graduation |
Bonding Curve
Constants
| Parameter | Value | Description | |-----------|-------|-------------| | Total Supply | 1,000,000,000 | Fixed supply per token | | Virtual Token Reserves | 1,073,000,000 | For pricing curve | | Virtual Base Reserves | Configurable (default 4,500 JUSD) | Initial virtual liquidity | | Real Token Reserves | 793,100,000 | 79.31% sold on curve | | Reserved for DEX | 206,900,000 | 20.69% for V2 LP | | Fee | 1% (100 bps) | On input (buy) / output (sell) |
Formulas
Buy (fee from input):
baseInAfterFee = baseIn - (baseIn * 1%)
tokensOut = virtualTokenReserves - (k / (virtualBaseReserves + baseInAfterFee))Sell (fee from output):
baseOutBeforeFee = virtualBaseReserves - (k / (virtualTokenReserves + tokensIn))
baseOut = baseOutBeforeFee - (baseOutBeforeFee * 1%)Where k = virtualBaseReserves * virtualTokenReserves
Graduation
Triggered when realTokenReserves == 0 (all tokens sold from curve).
- Add liquidity: 206.9M tokens + collected JUSD to V2
- Burn LP tokens to
0xdead - Transfer accumulated fees to fee recipient
Fees
- 1% fee on all buy/sell transactions
- Fees accumulate during bonding curve phase
- Sent to
feeRecipientat graduation
Repository Structure
contracts/
├── BondingCurveToken.sol
├── TokenFactory.sol
└── mocks/ # Test mocks only
exports/
├── index.ts # Barrel export
├── address.config.ts # Chain addresses
├── constants.ts # Protocol constants
└── abis/
├── TokenFactory.ts
└── BondingCurveToken.ts
test/
├── BondingCurveToken.test.ts
├── TokenFactory.test.ts
├── Graduation.test.ts
└── integration/ # Fork tests
scripts/
├── deploy.ts
└── exportAbis.tsUsage
Environment
Copy .env.example to .env and set:
PRIVATE_KEY=
CITREA_TESTNET_RPC=https://rpc.testnet.citrea.xyz
CITREA_MAINNET_RPC=https://rpc.citrea.xyz
UNISWAP_V2_ROUTER= # JuiceSwap router
BASE_ASSET_ADDRESS= # JUSD address
INIT_CODE_HASH= # V2 factory init code hash
FEE_RECIPIENT= # Optional, defaults to deployer
INITIAL_VIRTUAL_BASE=4500 # Optional, in JUSD unitsCommands
npm install
npm run compile # Compile contracts
npm test # Run unit tests
npm run test:coverage # Run with coverage
npm run test:integration:fork # Run integration tests on fork
npm run deploy:testnet # Deploy to Citrea Testnet
npm run deploy:mainnet # Deploy to Citrea Mainnet
npm run build # Build npm package (exports ABIs)Publishing
ABIs are automatically exported and the package is built before publishing via prepublishOnly.
npm run compile # Compile contracts first (if changed)
npm publish --access publicTo manually export ABIs without publishing:
npm run ts:export:abis # Export ABIs only
npm run build # Export ABIs + bundle packageSecurity
Audited Dependencies
- OpenZeppelin Contracts v5.4.0 (ERC20, ReentrancyGuard, Ownable, Pausable, Clones)
- Uniswap V2 Core/Periphery
Requires Audit
BondingCurveToken.sol- bonding curve logic, graduation mechanismTokenFactory.sol- proxy deployment
Protections
- Reentrancy guards on all state-changing functions
- Slippage protection via
minTokensOut/minBaseOut - Single initialization (proxy pattern)
- Front-running protection (blocks transfers to V2 pair before graduation)
References
License
MIT
