Разработка IDO-платформы
IDO (Initial DEX Offering) — механизм первичного размещения токенов через децентрализованный обмен. Технически это звучит проще, чем есть на самом деле. Основная проблема любого token launch — front-running и MEV: боты мониторят mempool и скупают токены раньше реальных покупателей, сразу же сдампив цену. Хорошая IDO-платформа — это в первую очередь система защиты от этого, а не просто "контракт с кнопкой купить".
Модели IDO-платформ
Перед проектированием нужно выбрать фундаментальную модель:
Fixed price sale — самая простая: цена фиксирована, whitelist участников. Проблема: при недооценке проекта токены выкупаются ботами в первый блок. Требует строгого whitelist + commit-reveal или временных слотов.
Dutch auction — цена начинается высокой и снижается до момента полной продажи. Даёт fair price discovery. Проблема: сложно объяснить пользователям, высокий риск манипуляций на последних минутах.
Overflow/refund model (IDO по образцу Binance Launchpad) — пользователи "вносят" любую сумму, итоговое распределение пропорционально вкладу. Переплата возвращается. Честно, но требует сложной логики расчёта аллокаций.
Liquidity Bootstrapping Pool (LBP) — Balancer-based механизм. Начальное соотношение весов в пуле (например, 95/5 token/USDC) меняется по времени до конечного (50/50). Цена начинается высокой и снижается. Хорошая защита от ботов за счёт high initial price.
Архитектура смарт-контрактов
Core: IDO Pool Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
contract IDOPool is ReentrancyGuard, AccessControl {
using SafeERC20 for IERC20;
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
struct PoolConfig {
IERC20 saleToken;
IERC20 paymentToken; // USDC, USDT или нативная монета
uint256 tokenPrice; // в paymentToken, 18 decimals
uint256 hardCap; // максимальный raise в paymentToken
uint256 softCap; // минимальный raise для успеха
uint256 minAllocation; // минимальная покупка на кошелёк
uint256 maxAllocation; // максимальная покупка на кошелёк
uint64 startTime;
uint64 endTime;
uint64 claimTime; // когда открывается claim
bytes32 whitelistMerkleRoot;
bool isPublic; // false = только whitelist
}
struct UserInfo {
uint256 contributed; // сколько paymentToken внесено
uint256 tokenAllocation; // сколько saleToken получит
bool claimed;
bool refunded;
}
PoolConfig public config;
mapping(address => UserInfo) public userInfo;
uint256 public totalRaised;
PoolStatus public status;
enum PoolStatus { PENDING, ACTIVE, FILLED, FAILED, FINALIZED }
event Contributed(address indexed user, uint256 amount, uint256 tokenAllocation);
event Claimed(address indexed user, uint256 amount);
event Refunded(address indexed user, uint256 amount);
function contribute(
uint256 paymentAmount,
bytes32[] calldata merkleProof
) external nonReentrant {
require(status == PoolStatus.ACTIVE, "Pool not active");
require(block.timestamp >= config.startTime, "Not started");
require(block.timestamp <= config.endTime, "Ended");
// whitelist проверка через Merkle proof
if (!config.isPublic) {
bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
require(
MerkleProof.verify(merkleProof, config.whitelistMerkleRoot, leaf),
"Not whitelisted"
);
}
UserInfo storage user = userInfo[msg.sender];
uint256 newContribution = user.contributed + paymentAmount;
require(newContribution >= config.minAllocation, "Below min allocation");
require(newContribution <= config.maxAllocation, "Exceeds max allocation");
require(totalRaised + paymentAmount <= config.hardCap, "Exceeds hard cap");
config.paymentToken.safeTransferFrom(msg.sender, address(this), paymentAmount);
uint256 tokenAmount = (paymentAmount * 1e18) / config.tokenPrice;
user.contributed += paymentAmount;
user.tokenAllocation += tokenAmount;
totalRaised += paymentAmount;
if (totalRaised >= config.hardCap) {
status = PoolStatus.FILLED;
}
emit Contributed(msg.sender, paymentAmount, tokenAmount);
}
function claim() external nonReentrant {
require(status == PoolStatus.FINALIZED, "Not finalized");
require(block.timestamp >= config.claimTime, "Claim not open");
UserInfo storage user = userInfo[msg.sender];
require(user.tokenAllocation > 0, "Nothing to claim");
require(!user.claimed, "Already claimed");
user.claimed = true;
config.saleToken.safeTransfer(msg.sender, user.tokenAllocation);
emit Claimed(msg.sender, user.tokenAllocation);
}
function refund() external nonReentrant {
require(status == PoolStatus.FAILED, "Pool not failed");
UserInfo storage user = userInfo[msg.sender];
require(user.contributed > 0, "Nothing to refund");
require(!user.refunded, "Already refunded");
user.refunded = true;
uint256 refundAmount = user.contributed;
config.paymentToken.safeTransfer(msg.sender, refundAmount);
emit Refunded(msg.sender, refundAmount);
}
function finalize() external onlyRole(ADMIN_ROLE) {
require(
status == PoolStatus.ACTIVE || status == PoolStatus.FILLED,
"Cannot finalize"
);
require(block.timestamp > config.endTime, "Not ended");
if (totalRaised >= config.softCap) {
status = PoolStatus.FINALIZED;
// перевод собранных средств проекту
config.paymentToken.safeTransfer(projectWallet, totalRaised);
} else {
status = PoolStatus.FAILED;
// возврат saleToken проекту
uint256 unsoldTokens = config.saleToken.balanceOf(address(this));
config.saleToken.safeTransfer(projectWallet, unsoldTokens);
}
}
}
Merkle Tree Whitelist
Хранить whitelist on-chain дорого — 1000 адресов = ~$30-50 в gas при деплое на Ethereum. Merkle tree решает это: хранится только 32-байтный root, пользователь предоставляет proof при транзакции:
import { MerkleTree } from "merkletreejs";
import keccak256 from "keccak256";
function buildWhitelist(addresses: string[]): { root: string; proofs: Map<string, string[]> } {
const leaves = addresses.map(addr => keccak256(addr));
const tree = new MerkleTree(leaves, keccak256, { sortPairs: true });
const root = tree.getHexRoot();
const proofs = new Map<string, string[]>();
for (const addr of addresses) {
proofs.set(addr, tree.getHexProof(keccak256(addr)));
}
return { root, proofs };
}
IDO Factory
Для платформы с несколькими одновременными IDO необходим Factory pattern:
contract IDOFactory {
address[] public pools;
mapping(address => bool) public isPool;
event PoolCreated(address indexed pool, address indexed projectToken);
function createPool(IDOPool.PoolConfig calldata config) external returns (address pool) {
pool = address(new IDOPool(config, msg.sender, address(this)));
pools.push(pool);
isPool[pool] = true;
emit PoolCreated(pool, address(config.saleToken));
}
}
Tier-система и staking
Профессиональные IDO-платформы (DAO Maker, Polkastarter, TrustPad) используют tier-систему: пользователи стейкают платформенный токен и получают гарантированную аллокацию пропорционально уровню:
contract TierSystem {
IERC20 public platformToken;
struct Tier {
string name;
uint256 minStake; // минимальный stake для tier
uint256 allocationMultiplier; // в basis points (10000 = 100%)
uint256 guaranteedAllocation; // гарантированная сумма в USD
}
Tier[] public tiers;
mapping(address => uint256) public stakedAmount;
mapping(address => uint256) public stakeTimestamp;
uint256 public lockPeriod = 7 days; // lock перед IDO
function getUserTier(address user) public view returns (uint256 tierIndex) {
uint256 staked = stakedAmount[user];
for (uint256 i = tiers.length; i > 0; i--) {
if (staked >= tiers[i-1].minStake) return i-1;
}
return type(uint256).max; // нет tier
}
}
Защита от ботов и MEV
Commit-reveal scheme: пользователи в фазе 1 отправляют hash(amount, nonce, address) без раскрытия суммы. В фазе 2 раскрывают реальные данные. Боты не знают итоговую сумму до moment reveal.
FCFS с time slots: каждому tier назначено свое временное окно. Tier 1 покупает с 12:00 до 12:05, Tier 2 — с 12:05 до 12:15. Боты первого уровня не могут опередить стейкеров высшего tier.
Anti-snipe: первые N блоков после открытия sales — 100% tax на sell для сдерживания снайперов. Это спорная мера, но часто применяется.
Private mempool / Flashbots Protect: для EVM-сетей submission через Flashbots RPC исключает транзакции из публичного mempool и защищает от front-running.
Vesting при claim
Мгновенный cliff release всех токенов при claim создаёт немедленный sell pressure. Правильная схема: TGE unlock 20%, остальное по вестингу. Реализуется через интеграцию с vesting-контрактом при finalize:
function finalize() external {
// ...
// создаём vesting schedules для каждого участника
for (address participant in participants) {
uint256 tgeAmount = userInfo[participant].tokenAllocation * TGE_PERCENT / 100;
uint256 vestingAmount = userInfo[participant].tokenAllocation - tgeAmount;
vestingContract.createSchedule(participant, tgeAmount, 0, 0, 1);
vestingContract.createSchedule(participant, vestingAmount, claimTime, 0, vestingDuration);
}
}
Инфраструктура платформы
Кроме смарт-контрактов, IDO-платформа требует:
| Компонент | Технологии |
|---|---|
| Frontend dApp | React + wagmi/viem, Web3Modal |
| KYC/AML | Sumsub, Synaps или custom |
| Whitelist management | API + Merkle tree generation |
| Real-time updates | WebSocket + event listening |
| Admin panel | Pool management, allocation calculator |
| Analytics | The Graph subgraph для on-chain данных |
| Notifications | Email + Telegram при открытии pool |
KYC интеграция — обязательная тема для юрисдикций с регулированием (EU MiCA, US). Суть: KYC-провайдер верифицирует пользователя, передаёт подпись/статус, который проверяется before whitelist registration или on-chain.







