Розробка стейблкоїна
Коли приходить запит "нам потрібен стейблкоїн", перше запитання не "яку ціну підтримувати?", а "за рахунок чого тримається прив'язка?". Це визначає все: архітектуру контрактів, вимоги до інфраструктури, регуляторні ризики та складність операційного управління. Існують три принципово різні архітектури — кожна зі своїми умовами робочого стану.
Три моделі стейблкоїна
Забезпечений фіатом
Модель USDC, USDT: 1 токен = 1 доларів на банківському рахунку. Технічно найпростіша: мінтери мінтять при отриманні фіату, бернери спалюють при виводі. Центральний емітент виступає кастодіаном.
Технічні компоненти: ERC-20 з role-based minting, чорний список (так, USDC і USDT можуть заморозити вашу адресу — договірне зобов'язання перед регуляторами), upgradeable proxy (Circle оновлював контракт USDC кілька разів).
Регуляторна реальність: в ЄС потребує ліцензію EMI (Electronic Money Institution) під MiCA з 2024 року. В США — state money transmitter licenses у кожному штаті. Бар'єр входу — десятки мільйонів доларів у резервах та compliance.
Забезпечений крипто
Модель DAI (MakerDAO CDP модель): користувач блокує ETH/WBTC як забезпечення, отримує DAI. Over-collateralization на 150%+ забезпечує буфер при волатильності. Коли ціна забезпечення падає нижче порога ліквідації — позиція ліквідується.
Це найбільш складна та цікава архітектура з інженерної точки зору.
Алгоритмічний
Не прив'язаний до зовнішнього активу — seigniorage механіка або rebase. Історія алгоритмічних стейблкоїнів похмура: Terra/Luna ($40B капіталізація → $0 за три дні в травні 2022), Basis Cash, Empty Set Dollar — всі розпались при втраті довіри. Чистий алгоритмічний стейблкоїн без якого-небудь collateral backing — це матеріал для академічних робіт, не для production.
Детальна реалізація CDP стейблкоїна
Сосредоточимось на крипто-забезпеченій архітектурі — вона реалістична для незалежної розробки та технічно змістовна.
Структура основних контрактів
VaultManager — відкриття/закриття позицій, управління забезпеченням
PriceFeed — Chainlink оракули для цін забезпечення
LiquidationEngine — автоматична ліквідація недостатньо забезпечених позицій
StablecoinToken — ERC-20 стейблкоїн з контрольованим мінтингом
StabilityPool — пул ліквідаторів, отримують забезпечення зі знижкою
FeeCollector — збір stability fee, розподіл kazny
VaultManager: Відкриття позиції
contract VaultManager {
struct Vault {
uint256 collateralAmount; // ETH/WBTC заблоковано
uint256 debtAmount; // мінтені стейблкоїни
address collateralToken;
uint256 lastFeeTimestamp;
}
mapping(address => mapping(address => Vault)) public vaults;
// Параметри за типом забезпечення
mapping(address => CollateralParams) public collateralParams;
struct CollateralParams {
uint256 liquidationRatio; // напр., 150% = 15000 (bps)
uint256 stabilityFeeRate; // річна ставка, напр., 0.5%
uint256 liquidationPenalty; // штраф при ліквідації, напр., 13%
uint256 debtCeiling; // максимум боргу за цим забезпеченням
bool isEnabled;
}
function openVault(
address collateralToken,
uint256 collateralAmount,
uint256 stablecoinAmount // скільки стейблкоїнів хочемо отримати
) external nonReentrant {
CollateralParams memory params = collateralParams[collateralToken];
require(params.isEnabled, "Collateral not supported");
// Перевірити що ratio забезпечення достатній
uint256 collateralValueUSD = _getCollateralValue(
collateralToken,
collateralAmount
);
uint256 requiredCollateral = (stablecoinAmount * params.liquidationRatio) / 10000;
require(collateralValueUSD >= requiredCollateral, "Insufficient collateral");
// Перевірити потолок боргу
require(
totalDebt[collateralToken] + stablecoinAmount <= params.debtCeiling,
"Debt ceiling reached"
);
// Прийняти забезпечення
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");
// Ліквідатор погашає частину боргу, отримує забезпечення зі знижкою
// Приклад: погашає $100 боргу, отримує $113 в ETH (бонус 13%)
uint256 collateralToSeize = (debtToRepay
* (10000 + params.liquidationPenalty) // додаємо штраф
* 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;
// Ліквідатор отримує забезпечення
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: Критичний компонент
Стейблкоїн повністю залежить від надійності оракула. 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 як первинний.
Механізми утримання прив'язки
Arbitrage incentives — ринковий механізм:
- Ціна стейблкоїна < $1: арбітражери купують дешево, погашають борг (спалюють стейблкоїн), отримують забезпечення → пропозиція падає → ціна зростає
- Ціна > $1: користувачі мінтять новий стейблкоїн (продають його) → пропозиція зростає → ціна падає
PSM (Peg Stability Module) — як у MakerDAO: прямий 1:1 своп між стейблкоїном та USDC за невелику комісію. Жорсткий якір, але вводить USDC як централізований актив якоря.
Динамічна Stability Fee — governance змінює fee rate у відповідь на відхилення ціни. Повільний механізм (потребує governance vote або автоматизованої політики).
Безпека та відомі атаки
Cream Finance Hack (2021, $130M) — flash loan атака на CREAM з використанням власного токена як забезпечення для себе. Циркулярна залежність в оракулі + flash loan = drain. Для CDP: забезпечення не повинно залежати від вартості стейблкоїна.
Euler Finance Hack (2023, $197M) — уразливість в депонуванні забезпечення без відповідного збільшення боргу. Ретельна перевірка облікових інваріантів обов'язкова: totalCollateral * price >= totalDebt * liquidationRatio повинна триматися в будь-який момент після будь-якої транзакції.
Invariant testing у Foundry — обов'язковий паттерн:
// Інваріант: протокол завжди платоспроможний
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, система регулювання параметрів | 2–3 тижні |
| Testing suite | Unit + fuzz + invariant тести | 3–4 тижні |
| Frontend interface | UI управління Vault | 3–5 тижнів |
| Audit | 1–2 аудиторських фірми | 4–8 тижнів |
| Testnet + bug bounty | 4–6 тижнів | |
| Mainnet (staged rollout) | Поступовий розширення потолку боргу | 2–4 тижні |
Мінімальний реалістичний термін до mainnet: 6–9 місяців. Два аудити — не розкіш, а необхідність: протягом 2021–2023 років CDP протоколи втратили >$500M через помилки в логіці ліквідації та оракулах.







