Development of Pump.fun-Style Platform
pump.fun solved a specific infrastructure problem: launching a token on Solana took hours and required technical knowledge. The platform made it accessible in 30 seconds. The mechanics are straightforward — bonding curve until reaching a certain market cap, then automatic liquidity migration to Raydium. Tens of millions of dollars flow through the platform daily. Technically, this is an interesting system with several non-trivial components.
Bonding Curve: Core Mechanics
Bonding curve is a mathematical function that determines token price based on current supply. No orderbook, no LP, no external price — the contract itself determines the exchange rate.
Linear curve:
Price = initial_price + slope * supply
Simple, predictable, but price growth proportional to buy volume — a whale can quickly push the price.
Exponential curve:
Price = initial_price * e^(k * supply)
Sharper growth at high supply. Early buyers get significantly more advantage.
pump.fun uses polynomial bonding curve — implementation with virtual reserves, mimicking Uniswap AMM behavior without real liquidity:
virtual_sol_reserves = 30 SOL
virtual_token_reserves = 1_073_000_000 tokens
real_sol_reserves = 0 (accumulates from sales)
real_token_reserves = 793_100_000 tokens (sold via curve)
Price is determined via constant product formula: k = virtual_sol * virtual_token_supply. When buying dx SOL:
new_virtual_sol = virtual_sol + dx
new_virtual_token = k / new_virtual_sol
tokens_received = virtual_token - new_virtual_token
This is exactly Uniswap V2 mechanics, but with virtual reserves instead of real LP tokens.
Implementation on EVM
contract BondingCurve {
uint256 public constant VIRTUAL_SOL_RESERVES = 30 ether; // in ETH/SOL
uint256 public constant VIRTUAL_TOKEN_RESERVES = 1_073_000_000e18;
uint256 public constant TOTAL_SUPPLY = 1_000_000_000e18;
uint256 public constant MIGRATION_THRESHOLD = 69_000 * 1e18; // $69k in ETH
uint256 public realEthReserves; // accumulated ETH
uint256 public tokensSold; // sold via curve
// How many tokens you get for X ETH
function getTokensOut(uint256 ethIn) public view returns (uint256) {
uint256 virtualEth = VIRTUAL_SOL_RESERVES + realEthReserves;
uint256 virtualTokens = VIRTUAL_TOKEN_RESERVES - tokensSold;
uint256 k = virtualEth * virtualTokens;
uint256 newVirtualEth = virtualEth + ethIn;
uint256 newVirtualTokens = k / newVirtualEth;
return virtualTokens - newVirtualTokens;
}
// How many ETH you get for X tokens
function getEthOut(uint256 tokensIn) public view returns (uint256) {
uint256 virtualEth = VIRTUAL_SOL_RESERVES + realEthReserves;
uint256 virtualTokens = VIRTUAL_TOKEN_RESERVES - tokensSold;
uint256 k = virtualEth * virtualTokens;
uint256 newVirtualTokens = virtualTokens + tokensIn;
uint256 newVirtualEth = k / newVirtualTokens;
return virtualEth - newVirtualEth;
}
function buy(uint256 minTokensOut) external payable nonReentrant {
require(msg.value > 0, "No ETH sent");
require(!migrated, "Token migrated to DEX");
uint256 fee = (msg.value * FEE_BPS) / 10000; // 1%
uint256 ethIn = msg.value - fee;
uint256 tokensOut = getTokensOut(ethIn);
require(tokensOut >= minTokensOut, "Slippage exceeded");
realEthReserves += ethIn;
tokensSold += tokensOut;
IERC20(token).safeTransfer(msg.sender, tokensOut);
payable(feeRecipient).transfer(fee);
emit Trade(msg.sender, ethIn, tokensOut, true);
// Check migration threshold
if (realEthReserves >= MIGRATION_THRESHOLD) {
_migrateToDEX();
}
}
function sell(uint256 tokensIn, uint256 minEthOut) external nonReentrant {
require(!migrated, "Token migrated to DEX");
require(tokensIn > 0, "Zero tokens");
uint256 ethOut = getEthOut(tokensIn);
uint256 fee = (ethOut * FEE_BPS) / 10000;
uint256 ethToUser = ethOut - fee;
require(ethToUser >= minEthOut, "Slippage exceeded");
IERC20(token).safeTransferFrom(msg.sender, address(this), tokensIn);
tokensSold -= tokensIn;
realEthReserves -= ethOut;
payable(msg.sender).transfer(ethToUser);
payable(feeRecipient).transfer(fee);
emit Trade(msg.sender, tokensIn, ethToUser, false);
}
}
Automatic DEX Migration
When threshold is reached (pump.fun — $69k market cap), the contract automatically:
- Stops trading via bonding curve
- Creates a pool on Uniswap V2 (or V3)
- Adds accumulated ETH + remaining tokens as liquidity
- Burns or locks LP tokens forever
function _migrateToDEX() internal {
migrated = true;
uint256 ethForLiquidity = realEthReserves;
uint256 tokensForLiquidity = TOTAL_SUPPLY - tokensSold; // unsold supply
// Create pair and add liquidity
address pair = IUniswapV2Factory(UNISWAP_FACTORY).createPair(
token,
WETH
);
// Approve and add liquidity
IERC20(token).approve(UNISWAP_ROUTER, tokensForLiquidity);
(, , uint256 lpTokens) = IUniswapV2Router(UNISWAP_ROUTER).addLiquidityETH{
value: ethForLiquidity
}(
token,
tokensForLiquidity,
tokensForLiquidity, // minTokens = 100% (no slippage at pool creation)
ethForLiquidity, // minETH = 100%
address(this),
block.timestamp + 300
);
// Burn LP tokens — liquidity is permanent
IERC20(pair).transfer(address(0xdead), lpTokens);
emit Migrated(pair, ethForLiquidity, tokensForLiquidity);
}
Locked vs burned LP: pump.fun burns LP tokens (sends to dead address). Alternative — lock via Unicrypt/Team.Finance. Burning is more radical, but irreversible — if there's a contract bug, can't fix it.
Token Factory: Launch in One Call
Each user launches a new token. Need a factory that deploys token + bonding curve contract in one transaction:
contract TokenFactory {
event TokenCreated(
address indexed token,
address indexed curve,
address indexed creator,
string name,
string symbol,
string uri,
uint256 timestamp
);
address[] public allTokens;
mapping(address => TokenInfo) public tokenInfo;
function createToken(
string calldata name,
string calldata symbol,
string calldata uri,
uint256 initialBuyEth
) external payable returns (address token, address curve) {
// Deploy minimal ERC-20
token = address(new MinimalERC20(name, symbol, TOTAL_SUPPLY));
curve = address(new BondingCurve(token, msg.sender));
// Transfer all tokens to curve
MinimalERC20(token).transfer(curve, TOTAL_SUPPLY);
// Initial buy if ETH provided
if (initialBuyEth > 0) {
uint256 creationFee = CREATION_FEE;
require(msg.value >= creationFee + initialBuyEth, "Insufficient ETH");
BondingCurve(payable(curve)).buy{value: initialBuyEth}(0);
}
allTokens.push(token);
tokenInfo[token] = TokenInfo({
curve: curve,
creator: msg.sender,
name: name,
symbol: symbol,
uri: uri,
createdAt: block.timestamp
});
emit TokenCreated(token, curve, msg.sender, name, symbol, uri, block.timestamp);
}
}
CREATE2 for predictable addresses — useful for frontend: can calculate token address before deployment and show user in advance.
Anti-rug Mechanisms
Main risks: creator dumps (bought 80% supply via curve at low price, sells after hype). pump.fun partially solves this architecturally — after migration, LP is locked and creator cannot withdraw liquidity.
Maximum allocation per address — while using the curve, one address cannot buy more than X% of supply at once:
uint256 public constant MAX_BUY_PERCENT = 10; // max 10% per transaction
function buy(uint256 minTokensOut) external payable {
uint256 tokensOut = getTokensOut(msg.value);
uint256 maxTokens = (TOTAL_SUPPLY * MAX_BUY_PERCENT) / 100;
require(tokensOut <= maxTokens, "Buy too large");
// ...
}
Cooldown between purchases — protection against rapid accumulation by bots.
Indexing and Discovery
With thousands of new tokens daily, need real-time index:
The Graph subgraph for indexing TokenCreated, Trade, Migrated events. GraphQL API for frontend.
Trending algorithm (simplified):
score = (volume_1h * 3) + (volume_24h * 1) + (buyers_1h * 50) - (sellers_1h * 30)
Higher weight on recent volume and unique buyer count (not volume from single whale).
WebSocket for live trades — frontend subscribes to events of specific token and displays trades in real time.
Platform Economics
pump.fun earns from:
- 1% fee from each trade via bonding curve
- 0.5% of volume after migration to Raydium
- Optional creator verification payment
At $1M/day volume this is $10,000/day from trading fee alone. For EVM implementation on Base/Arbitrum — model is similar, but gas cost higher than Solana, important for small trades.
Technical Stack
Contracts: Solidity + Foundry (testing with fuzz for invariants: totalEth = sum(all buys) - sum(all sells))
Indexing: The Graph or custom indexer (Node.js + ethers.js + PostgreSQL)
Frontend: React + wagmi + viem, real-time via WebSocket to custom indexer
Charts: TradingView Lightweight Charts over indexed trade data
Metadata storage: IPFS (token image, description)
Development Phases
| Phase | Content | Time |
|---|---|---|
| Bonding curve math | Curve parameters, invariant tests | 1–2 weeks |
| Core contracts | Factory, BondingCurve, migration | 3–4 weeks |
| Security audit | Focus on curve manipulation, reentrancy | 2–3 weeks |
| Indexer | Subgraph or custom indexer | 2–3 weeks |
| Frontend | Trading interface, discovery, charts | 4–6 weeks |
| Testnet | Full cycle create → trade → migrate | 2–3 weeks |
| Mainnet | Deploy to target chain | 1 week |
Critical tests: fuzz invariants (totalETHin - totalETHout = realEthReserves), migration math (DEX pool price immediately after migration should match bonding curve price at migration moment), extreme slippage scenarios.







