Разработка контрактов ликвидити-майнинга
Liquidity mining — механизм, при котором пользователи предоставляют ликвидность и получают вознаграждение в токенах протокола. 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
)
Это создаёт incentive холдить governance-токен, снижает sell pressure на 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 attack на rewards
Атакующий берёт 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). Не итерировать по массивам участников.
Precision loss при малых суммах
При делении 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 — пользователь должен иметь возможность вывести stake даже если contarct на паузе. Награды при этом можно не выдавать, но основной стейк — обязательно. Этого требует не только здравый смысл, но и некоторые юрисдикции.
Процесс работы
Проектирование (0.5-1 день). Определяем: один reward-токен или несколько, нужен ли boosting, минимальный lockup, механизм пополнения наград (manual vs автоматизированный).
Разработка (2-3 дня). Основной контракт, события для indexing (The Graph субграф строится по events), тесты с fuzz-тестированием арифметики.
Тесты (1-2 дня). Покрываем: нормальный стейкинг/вывод, flash loan сценарий с минимальным lockup, precision tests с граничными значениями, multi-user scenarios с разными пропорциями.
Деплой. Отдельный deployer-скрипт с конфигурацией первоначального rewardRate, transferOwnership на multisig.
Для большинства проектов полный цикл — 3-5 рабочих дней. Сложные схемы с veToken boosting и multiple reward tokens — 8-12 дней.







