Розробка платформи у стилі pump.fun
pump.fun вирішив конкретну інфраструктурну проблему: запуск токена на Solana займав години та вимагав технічних знань. Платформа зробила це доступним за 30 секунд. Механіка простий — bonding curve до досягнення певної ринкової капіталізації, потім автоматична міграція ліквідності в Raydium. Щодня через платформу проходять десятки мільйонів доларів. Технічно це цікава система з кількома нетривіальними компонентами.
Bonding Curve: ядро механіки
Bonding curve — математична функція, що визначає ціну токена залежно від поточного supply. Немає orderbook, немає LP, немає зовнішної ціни — контракт сам визначає обмінний курс.
Лінійна крива:
Price = initial_price + slope * supply
Проста, передбачувана, але зростання ціни пропорційне обсягу покупок — whale може швидко розтовпити ціну.
Експоненціальна крива:
Price = initial_price * e^(k * supply)
Гостріший ріст при високому supply. Ранні покупці отримають значно більшу перевагу.
pump.fun використовує polynomial bonding curve — реалізація з virtual reserves, що імітує поведінку Uniswap AMM без реальної ліквідності:
virtual_sol_reserves = 30 SOL
virtual_token_reserves = 1_073_000_000 токенів
real_sol_reserves = 0 (накопичується від продажів)
real_token_reserves = 793_100_000 токенів (продаються через криву)
Ціна визначається через формулу constant product: k = virtual_sol * virtual_token_supply. При покупці dx SOL:
new_virtual_sol = virtual_sol + dx
new_virtual_token = k / new_virtual_sol
tokens_received = virtual_token - new_virtual_token
Це точна копія механіки Uniswap V2, але з virtual reserves замість реальних LP токенів.
Реалізація на EVM
contract BondingCurve {
uint256 public constant VIRTUAL_SOL_RESERVES = 30 ether; // в 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 в ETH
uint256 public realEthReserves; // накопичений ETH
uint256 public tokensSold; // продано через криву
// Скільки токенів отримаєш за 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;
}
// Скільки ETH отримаєш за X токенів
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);
// Перевіряємо поріг міграції
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);
}
}
Автоматична міграція на DEX
При досягненні поріга (pump.fun — $69k ринкова капіталізація) контракт автоматично:
- Зупиняє торгівлю через bonding curve
- Створює пул на Uniswap V2 (або V3)
- Додає накопичений ETH + решта токенів як ліквідність
- Спалює або локує LP токени назавжди
function _migrateToDEX() internal {
migrated = true;
uint256 ethForLiquidity = realEthReserves;
uint256 tokensForLiquidity = TOTAL_SUPPLY - tokensSold; // непроданий supply
// Створюємо пару та додаємо ліквідність
address pair = IUniswapV2Factory(UNISWAP_FACTORY).createPair(
token,
WETH
);
// Апрув та додавання ліквідності
IERC20(token).approve(UNISWAP_ROUTER, tokensForLiquidity);
(, , uint256 lpTokens) = IUniswapV2Router(UNISWAP_ROUTER).addLiquidityETH{
value: ethForLiquidity
}(
token,
tokensForLiquidity,
tokensForLiquidity, // minTokens = 100% (немає слиппажу при створенні пулу)
ethForLiquidity, // minETH = 100%
address(this),
block.timestamp + 300
);
// Спалюємо LP токени — ліквідність постійна
IERC20(pair).transfer(address(0xdead), lpTokens);
emit Migrated(pair, ethForLiquidity, tokensForLiquidity);
}
Заблокована vs спалена LP: pump.fun спалює LP токени (відправляє на dead address). Альтернатива — локування через Unicrypt/Team.Finance. Спалювання більш радикальне, але необоротне — якщо є баг у контракті, не можна його виправити.
Token Factory: запуск за один виклик
Кожний користувач запускає новий токен. Потрібна фабрика, що розгортає токен + bonding curve контракт за одну транзакцію:
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) {
// Розгортаємо мінімальний ERC-20
token = address(new MinimalERC20(name, symbol, TOTAL_SUPPLY));
curve = address(new BondingCurve(token, msg.sender));
// Переводимо всі токени у криву
MinimalERC20(token).transfer(curve, TOTAL_SUPPLY);
// Перша покупка якщо був ETH
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 для передбачуваних адрес — корисно для frontend: можна розрахувати адресу токена до розгортання та показати користувачу заздалегідь.
Anti-rug механізми
Основні ризики:創atор dump'ає (купив 80% supply через криву при низькій ціні, продає після hype). pump.fun частково вирішує це архітектурно — після міграції LP заблокована та creator не може вивести ліквідність.
Максимальна алокація на адресу — під час використання кривої одна адреса не може купити більше X% supply одразу:
uint256 public constant MAX_BUY_PERCENT = 10; // максимум 10% за транзакцію
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");
// ...
}
Затримка між покупками — захист від швидкого накопичення ботами.
Індексування та виявлення
З тисячами нових токенів щодня потрібен real-time індекс:
The Graph subgraph для індексування событій TokenCreated, Trade, Migrated. GraphQL API для frontend.
Алгоритм тренду (спрощено):
score = (volume_1h * 3) + (volume_24h * 1) + (buyers_1h * 50) - (sellers_1h * 30)
Вищий вага на недавній обсяг та кількість унікальних покупців (не обсяг від одного whale).
WebSocket для live trades — frontend підписується на события конкретного токена та показує trades у реальному часі.
Економіка платформи
pump.fun заробляє від:
- 1% комісії від кожної trade через bonding curve
- 0.5% обсягу після міграції в Raydium
- Платної верифікації творців (опціонально)
При обсягу $1M/день це $10,000/день тільки від trading fee. Для EVM реалізації на Base/Arbitrum — модель аналогічна, але gas cost вище ніж на Solana, важливо для малих trades.
Технічний стек
Контракти: Solidity + Foundry (тестування з fuzz для інваріантів: totalEth = sum(all buys) - sum(all sells))
Індексування: The Graph або кастомний indexer (Node.js + ethers.js + PostgreSQL)
Frontend: React + wagmi + viem, real-time через WebSocket до кастомного indexer
Чарти: TradingView Lightweight Charts над indexed trade data
Зберігання метаданих: IPFS (зображення, опис токена)
Етапи розробки
| Фаза | Вміст | Час |
|---|---|---|
| Bonding curve математика | Параметри кривої, тести інваріантів | 1–2 тижні |
| Core контракти | Factory, BondingCurve, міграція | 3–4 тижні |
| Security аудит | Фокус на маніпуляцію кривої, reentrancy | 2–3 тижні |
| Indexer | Subgraph або кастомний indexer | 2–3 тижні |
| Frontend | Trading interface, discovery, charts | 4–6 тижнів |
| Testnet | Повний цикл create → trade → migrate | 2–3 тижні |
| Mainnet | Розгортання на target chain | 1 тиждень |
Критичні тести: fuzz invariants (totalETHin - totalETHout = realEthReserves), математика міграції (ціна DEX пулу одразу після міграції повинна совпадати з ціною bonding curve у момент міграції), extreme slippage сценарії.







