Разработка платформы в стиле 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 без реального liquidity:
virtual_sol_reserves = 30 SOL
virtual_token_reserves = 1_073_000_000 tokens
real_sol_reserves = 0 (накапливается от продаж)
real_token_reserves = 793_100_000 tokens (продаются через кривую)
Цена определяется через формулу 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, но с виртуальными резервами вместо реальных 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
При достижении threshold (у pump.fun — $69k market cap) контракт автоматически:
- Останавливает торговлю через 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
);
// Approve и добавление ликвидности
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);
}
Locked vs burned LP: pump.fun сжигает LP токены (отправляет на dead address). Альтернатива — локать через Unicrypt/Team.Finance. Сжигание радикальнее, но необратимо — если в контракте баг, исправить нельзя.
Фабрика токенов: запуск за один вызов
Каждый пользователь запускает новый токен. Нужна фабрика, которая деплоит токен + 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 механизмы
Основные риски: создатель dump'ает (купил 80% supply через кривую при низкой цене, продаёт после hype). pump.fun частично решает это архитектурой — после миграции LP залочен и создатель не может вытащить ликвидность.
Максимальная аллокация на адрес — при работе через кривую один адрес не может купить более 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");
// ...
}
Cooldown между покупками — защита от rapid accumulation ботами.
Индексирование и discovery
С тысячами новых токенов в день нужен real-time индекс:
The Graph subgraph для индексирования событий TokenCreated, Trade, Migrated. GraphQL API для frontend.
Trending алгоритм (упрощённо):
score = (volume_1h * 3) + (volume_24h * 1) + (buyers_1h * 50) - (sellers_1h * 30)
Больший вес у недавнего объёма и количества уникальных покупателей (а не объёма от одного whale).
WebSocket для live trades — frontend подписывается на события конкретного токена и отображает trades в реальном времени.
Экономика платформы
pump.fun зарабатывает:
- 1% fee от каждой trade через bonding curve
- 0.5% от объёма после миграции в Raydium
- Платная верификация создателей (опционально)
При объёме $1M/день это $10,000/день только от trading fee. Для EVM реализации на Base/Arbitrum — модель аналогична, но gas cost выше чем на Solana, что важно для маленьких сделок.
Технический стек
Контракты: Solidity + Foundry (тестирование с fuzz для invariants: 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 math | Расчёт параметров кривой, тесты инвариантов | 1–2 нед |
| Core contracts | Factory, BondingCurve, миграция | 3–4 нед |
| Security audit | Особое внимание на манипуляцию кривой, reentrancy | 2–3 нед |
| Indexer | Subgraph или кастомный indexer | 2–3 нед |
| Frontend | Trading interface, discovery, charts | 4–6 нед |
| Testnet | Полный цикл создание → торговля → миграция | 2–3 нед |
| Mainnet | Деплой на target chain | 1 нед |
Критические тесты: fuzz invariants (totalETHin - totalETHout = realEthReserves), migration math (цена в DEX пуле сразу после миграции должна совпадать с ценой на bonding curve в момент миграции), extreme slippage scenarios.







