Разработка стейблкоина
Когда приходит запрос "нам нужен стейблкоин", первый вопрос не "какую цену поддерживать?", а "за счёт чего держится привязка?". Это определяет всё: архитектуру контрактов, требования к инфраструктуре, регуляторные риски и сложность операционного управления. Три принципиально разные архитектуры — и у каждой свои условия работоспособности.
Три модели стейблкоина
Фиатно-обеспеченный (fiat-backed)
Модель USDC, USDT: 1 токен = 1 доллар в банке. Технически самая простая: mintер минтит при получении фиата, бёрнер сжигает при выводе. Центральный эмитент — кастодиан.
Технические компоненты: ERC-20 с role-based minting, blacklist (да, USDC и USDT могут заморозить ваш адрес — это contractual obligation перед регуляторами), upgradeable proxy (Circle обновляла USDC контракт несколько раз).
Регуляторная реальность: в EU требует лицензия EMI (Electronic Money Institution) под MiCA с 2024 года. В США — state money transmitter licenses в каждом штате. Барьер входа — десятки миллионов долларов в резервах и compliance.
Крипто-обеспеченный (crypto-backed)
Модель DAI (CDP модель MakerDAO): пользователь блокирует ETH/WBTC как collateral, получает DAI. Over-collateralization 150%+ обеспечивает буфер при волатильности. При падении цены collateral ниже liquidation threshold — позиция ликвидируется.
Это наиболее сложная и интересная архитектура с инженерной точки зрения.
Алгоритмический
Модель не привязана к внешнему активу — seigniorage механика или rebase. История алгоритмических стейблкоинов удручающая: Terra/Luna ($40B капитализации → $0 за три дня в мае 2022), Basis Cash, Empty Set Dollar — все коллапсировали при потере доверия. Чистый алгоритмический стейблкоин без какого-либо collateral backing — это история для академических работ, не production.
Детальная реализация CDP стейблкоина
Сосредоточимся на crypto-backed архитектуре — она реалистична для независимой разработки и технически содержательна.
Core contracts структура
VaultManager — открытие/закрытие позиций, управление collateral
PriceFeed — Chainlink оракулы для цен collateral
LiquidationEngine — автоматическая ликвидация недообеспеченных позиций
StablecoinToken — ERC-20 стейблкоин с controlled minting
StabilityPool — пул ликвидаторов, получают collateral со скидкой
FeeCollector — сбор stability fee, распределение treasury
VaultManager: создание позиции
contract VaultManager {
struct Vault {
uint256 collateralAmount; // ETH/WBTC locked
uint256 debtAmount; // mint'нутых стейблкоинов
address collateralToken;
uint256 lastFeeTimestamp;
}
mapping(address => mapping(address => Vault)) public vaults;
// Параметры по типу collateral
mapping(address => CollateralParams) public collateralParams;
struct CollateralParams {
uint256 liquidationRatio; // e.g., 150% = 15000 (bps)
uint256 stabilityFeeRate; // годовая ставка, e.g., 0.5%
uint256 liquidationPenalty; // штраф при ликвидации, e.g., 13%
uint256 debtCeiling; // максимум долга по этому collateral
bool isEnabled;
}
function openVault(
address collateralToken,
uint256 collateralAmount,
uint256 stablecoinAmount // сколько стейблкоинов хочет получить
) external nonReentrant {
CollateralParams memory params = collateralParams[collateralToken];
require(params.isEnabled, "Collateral not supported");
// Проверяем что collateral ratio достаточен
uint256 collateralValueUSD = _getCollateralValue(
collateralToken,
collateralAmount
);
uint256 requiredCollateral = (stablecoinAmount * params.liquidationRatio) / 10000;
require(collateralValueUSD >= requiredCollateral, "Insufficient collateral");
// Проверяем debt ceiling
require(
totalDebt[collateralToken] + stablecoinAmount <= params.debtCeiling,
"Debt ceiling reached"
);
// Принимаем collateral
IERC20(collateralToken).transferFrom(msg.sender, address(this), collateralAmount);
// Обновляем vault
Vault storage vault = vaults[msg.sender][collateralToken];
vault.collateralAmount += collateralAmount;
vault.debtAmount += stablecoinAmount;
vault.collateralToken = collateralToken;
vault.lastFeeTimestamp = block.timestamp;
totalDebt[collateralToken] += stablecoinAmount;
// Минтим стейблкоин пользователю
stablecoin.mint(msg.sender, stablecoinAmount);
emit VaultOpened(msg.sender, collateralToken, collateralAmount, stablecoinAmount);
}
}
Stability Fee: непрерывное начисление
Stability fee — процентная ставка, которая непрерывно начисляется на долг. Это и регуляторный рычаг (повышение fee → меньше минтят → меньше предложение → цена идёт к $1 при давлении вниз), и источник дохода протокола.
function _accrueFee(address user, address collateralToken) internal {
Vault storage vault = vaults[user][collateralToken];
if (vault.debtAmount == 0) return;
CollateralParams memory params = collateralParams[collateralToken];
uint256 elapsed = block.timestamp - vault.lastFeeTimestamp;
// Непрерывное начисление: debt * (1 + rate)^t ≈ debt * (1 + rate * t) для малых t
// Точная формула через натуральный логарифм:
uint256 feeMultiplier = _continuousCompound(params.stabilityFeeRate, elapsed);
uint256 newDebt = (vault.debtAmount * feeMultiplier) / RAY; // RAY = 1e27
uint256 fee = newDebt - vault.debtAmount;
vault.debtAmount = newDebt;
vault.lastFeeTimestamp = block.timestamp;
// Fee идёт в Surplus Buffer протокола
surplusBuffer += fee;
stablecoin.mint(address(this), fee); // стейблкоин "создаётся" как fee
}
Математика: rpow(base, n, RAY) — точное целочисленное возведение в степень, используется из MakerDAO DSMath.
Ликвидационный движок
contract LiquidationEngine {
// Collateral Ratio = (collateralValue / debtValue) * 100
function getCollateralRatio(
address user,
address collateralToken
) public view returns (uint256) {
Vault memory vault = vaultManager.getVault(user, collateralToken);
if (vault.debtAmount == 0) return type(uint256).max;
uint256 collateralValue = priceFeed.getPrice(collateralToken)
* vault.collateralAmount / 1e18;
return (collateralValue * 10000) / vault.debtAmount;
}
function liquidate(
address user,
address collateralToken,
uint256 debtToRepay
) external nonReentrant {
CollateralParams memory params = collateralParams[collateralToken];
uint256 cr = getCollateralRatio(user, collateralToken);
require(cr < params.liquidationRatio, "Vault is healthy");
// Ликвидатор погашает часть долга, получает collateral со скидкой
// Например: погашает $100 долга, получает $113 в ETH (13% бонус)
uint256 collateralToSeize = (debtToRepay
* (10000 + params.liquidationPenalty) // добавляем penalty
* 1e18) / (priceFeed.getPrice(collateralToken) * 10000);
// Проверяем что не сеизим больше чем есть
Vault storage vault = vaults[user][collateralToken];
collateralToSeize = Math.min(collateralToSeize, vault.collateralAmount);
// Ликвидатор сжигает стейблкоин для погашения
stablecoin.burnFrom(msg.sender, debtToRepay);
vault.debtAmount -= debtToRepay;
vault.collateralAmount -= collateralToSeize;
// Ликвидатор получает collateral
IERC20(collateralToken).transfer(msg.sender, collateralToSeize);
emit Liquidation(user, collateralToken, debtToRepay, collateralToSeize);
}
}
Проблема быстрых рыночных падений — если цена ETH падает на 30% за несколько минут (как бывает при flash crashes), ликвидаторы не успевают среагировать, система накапливает bad debt. Решение — Dutch Auction liquidations (как в MakerDAO v2 Liquidations 2.0): цена аукциона стартует высокой и снижается каждые несколько секунд, побуждая ликвидаторов действовать быстрее.
Price oracle: критический компонент
Стейблкоин полностью зависит от надёжности price oracle. Chainlink Data Feeds — стандарт:
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract PriceFeed {
mapping(address => AggregatorV3Interface) public priceFeeds;
uint256 public constant PRICE_STALENESS_THRESHOLD = 3600; // 1 час
function getPrice(address token) external view returns (uint256) {
AggregatorV3Interface feed = priceFeeds[token];
(
uint80 roundId,
int256 price,
,
uint256 updatedAt,
uint80 answeredInRound
) = feed.latestRoundData();
require(price > 0, "Invalid price");
require(updatedAt >= block.timestamp - PRICE_STALENESS_THRESHOLD, "Stale price");
require(answeredInRound >= roundId, "Stale round");
return uint256(price); // 8 decimals для большинства Chainlink feeds
}
}
Никогда не используем spot price из Uniswap/Curve напрямую — flash loan атаки манипулируют ценой в одном блоке. TWAP как резервный оракул, Chainlink как primary.
Механизмы поддержания привязки
Arbitrage incentives — рыночный механизм:
- Цена стейблкоина < $1: арбитражёры покупают дёшево, погашают долг (сжигают стейблкоин), получают collateral → предложение падает → цена растёт
- Цена > $1: пользователи минтят новый стейблкоин (продают его) → предложение растёт → цена падает
PSM (Peg Stability Module) — как у MakerDAO: прямой своп 1:1 между стейблкоином и USDC за небольшую комиссию. Жёсткий якорь, но вводит централизованный актив (USDC) как anchor.
Динамическая Stability Fee — governance меняет fee rate в ответ на отклонение цены. Медленный механизм (требует governance vote или automated policy).
Безопасность и известные атаки
Cream Finance Hack (2021, $130M) — атака через flash loan на CREAM, которые использовали собственный токен как collateral для себя же. Circular dependency в оракуле + flash loan = drain. Для CDP: collateral не должен зависеть от стоимости самого стейблкоина.
Euler Finance Hack (2023, $197M) — уязвимость в logique донейшена collateral без соответствующего увеличения debt. Тщательная проверка accounting invariants обязательна: totalCollateral * price >= totalDebt * liquidationRatio должно быть верно в любой момент после любой транзакции.
Invariant testing в Foundry — обязательный паттерн:
// Инвариант: протокол всегда solvent
function invariant_solvency() public view {
uint256 totalCollateralValue = calculateTotalCollateralValue();
uint256 totalDebt = stablecoin.totalSupply();
assertGe(totalCollateralValue, totalDebt);
}
Сроки и этапы
| Фаза | Содержание | Срок |
|---|---|---|
| Protocol design | Механика, параметры, tokenomics | 2–3 нед |
| Core contracts | VaultManager, LiquidationEngine, PriceFeed | 4–6 нед |
| Governance & parameters | TimeGovernor, parameter adjustment system | 2–3 нед |
| Testing suite | Unit + fuzz + invariant tests | 3–4 нед |
| Frontend interface | Vault management UI | 3–5 нед |
| Audit | 1–2 аудиторских фирмы | 4–8 нед |
| Testnet + bug bounty | 4–6 нед | |
| Mainnet (staged rollout) | Постепенное увеличение debt ceiling | 2–4 нед |
Минимальный реалистичный срок до mainnet: 6–9 месяцев. Два аудита — не роскошь, а необходимость: за 2021–2023 годы CDP протоколы потеряли >$500M из-за ошибок в liquidation logic и оракулах.







