Розроблення контрактів ліквідиті-майнінгу
Ліквідиті-майнінг — це механізм, при якому користувачі надають ліквідність і отримують винагороди в токенах протоколу. SushiSwap у 2020 році провів «vampire attack» на Uniswap саме через це: розгорнув аналогічний AMM з агресивними SUSHI-винагородами і протягом 24 годин привабив $1B TVL. Механіка працює.
Але за зовнішною простотою «стейкуєш LP-токен → отримуєш винагороду» приховується нетривіальна математика та кілька класів вразливостей, які регулярно зустрічаються в production.
Математика розподілу винагород
Алгоритм Masterchef (Synthetix StakingRewards)
Базовий алгоритм, використовуваний більшістю протоколів, заснований на накопленій вартості винагороди на одиницю стейка (rewardPerTokenStored).
// Накопленої винагороди на токен
uint256 public rewardPerTokenStored;
function rewardPerToken() public view returns (uint256) {
if (totalSupply == 0) return rewardPerTokenStored;
return rewardPerTokenStored + (
(block.timestamp - lastUpdateTime)
* rewardRate
* 1e18
/ totalSupply
);
}
// Винагорода конкретного користувача
function earned(address account) public view returns (uint256) {
return (balanceOf[account] *
(rewardPerToken() - userRewardPerTokenPaid[account])
/ 1e18)
+ rewards[account];
}
Ключова властивість: оновлення rewardPerTokenStored відбувається при кожному stake/withdraw/getReward, а не при кожному блоці. Це O(1) операція незалежно від кількості учасників.
При кожній взаємодії користувача зберігаємо userRewardPerTokenPaid[user] = rewardPerToken(). Різниця між поточним rewardPerToken() та збереженим значенням — накопленої винагороди з моменту останньої взаємодії.
Ця математика коректна при безперервному часі. На практиці дискретні блоки створюють похибку округлення, яку необхідно враховувати.
Boosted Rewards (ve-токени)
Модель Convex/Curve: винагорода залежить не лише від суми стейка, а й від кількості заблокованого governance-токена (veToken). Формула:
effective_balance = min(
0.4 * balance + 0.6 * (totalSupply * veBal / veTotalSupply),
balance
)
Це створює стимул для утримання governance-токена, зменшує тиск на продаж reward-токена. Математично складніше, потребує ретельного тестування edge cases при нульовому veBalance.
Multi-reward розподіл
Коли потрібно розподіляти кілька токенів одночасно (наприклад, протокольний токен + USDC fee sharing), архітектура ускладнюється. Кожен reward-токен потребує власного rewardPerTokenStored. OpenZeppelin не надає готову реалізацію для multi-reward; використовуємо Synthetix StakingMultiRewards як референс.
Вразливості, які ми бачили в аудитах
Inflation attack при першому депозиті
При порожньому стейкинг-контракті (totalSupply = 0) перший користувач може маніпулювати rewardPerToken(). Атака: розгорнув контракт, атакуючий робить мікро-депозит (1 wei), контракт накопичує всі винагороди для 1 wei, атакуючий отримує непропорційно велику частку.
Рішення: віртуальний початковий баланс (VIRTUAL_TOTAL_SUPPLY = 1e18), який ніколи не виводиться, або перевірка мінімального депозиту.
Flash loan атака на винагороди
Атакуючий бере flash loan на LP-токени, стейкує величезну суму, отримує винагороду за один блок, виводить. При високому rewardRate це може бути прибутковим.
Захист: мінімальний період стейкингу (lockup period, 1-7 днів). Альтернатива — vesting винагород: не видавати одразу, а лінійно протягом N днів. Навіть 24-годинний vesting робить flash loan атаку нерентабельною.
Griefing через updateReward
Функції stake, withdraw, getReward повинні викликати _updateReward(msg.sender). Якщо це modifier — кожен вклик оновлює стан для msg.sender. Але якщо _updateReward дорогостояний (наприклад, ітерує по масиву), зовнішній користувач може викликати функції в циклі і зробити контракт недоступним через gas griefing.
Рішення: _updateReward повинен бути O(1). Не ітеруйте по масивам учасників.
Втрата точності при малих сумах
При діленні rewardRate * dt / totalSupply, якщо totalSupply дуже великий, а dt малий, результат може бути 0 через цілочисленне ділення. Винагороди «випаровуються».
Рішення: масштабування через 1e18 (або 1e27 для Aave-like precision), накопичення remainder'ів.
Економіка та параметри
rewardRate — кількість reward-токенів за секунду. Обчислюється як rewardAmount / duration. При notifyRewardAmount() перераховується з урахуванням залишку поточного періоду.
function notifyRewardAmount(uint256 reward) external onlyOwner {
if (block.timestamp >= periodFinish) {
rewardRate = reward / rewardsDuration;
} else {
uint256 remaining = periodFinish - block.timestamp;
uint256 leftover = remaining * rewardRate;
rewardRate = (reward + leftover) / rewardsDuration;
}
// ...
}
Emergency withdrawal — користувач повинен мати можливість вивести стейк навіть якщо контракт на паузі. Винагороди при цьому можна не видавати, але основний стейк — обов'язково. Цього вимагає не тільки здоровий глузд, але й деякі юрисдикції.
Процес роботи
Проектування (0.5-1 день). Визначаємо: один reward-токен або кілька, потрібен ли boosting, мінімальний lockup, механізм поповнення винагород (manual vs автоматизований).
Розроблення (2-3 дні). Основний контракт, события для indexing (The Graph субграф будується за events), тести з fuzz-тестуванням арифметики.
Тести (1-2 дні). Покриваємо: нормальний стейкинг/вивід, flash loan сценарій з мінімальним lockup, precision tests з граничними значеннями, multi-user сценарії з різними пропорціями.
Деплой. Окремий deployer-скрипт з конфігурацією початкового rewardRate, transferOwnership на multisig.
Для більшості проектів повний цикл — 3-5 робочих днів. Складні схеми з veToken boosting та multiple reward tokens — 8-12 днів.







