Разработка NFT-стейкинга
NFT-стейкинг — это механика удержания: пользователь лочит NFT в контракте на период, получает ERC-20 токены в качестве награды. Работает как retention tool для gaming проектов, как governance power accumulator, как emission mechanism для новых токенов. Контракт выглядит просто, но имеет три неочевидных места, где что-то идёт не так.
Где ломается стейкинг-контракт
Reward calculation: накопленные ошибки округления
Самый частый баг — drift в расчёте наград из-за integer division. Стандартный паттерн:
rewardPerTokenStored += rewardRate * deltaTime / totalStaked
При totalStaked = 1000 NFT и rewardRate = 1e18 wei/sec каждую секунду rewardPerTokenStored увеличивается на 1e15. Всё корректно. Но при totalStaked = 3 NFT: 1e18 / 3 = 333333333333333333 — 1 wei потеряно на каждой секунде. За год это ~31M секунд = ~31M wei потеряно. Незначительно, но при большом rewardRate или малом totalStaked drift растёт.
Решение: reward per token с точностью 1e36 (не 1e18). Хранить rewardPerTokenStored в ray-подобных единицах с масштабированием, делить при выплате. Это стандарт в Synthetix-derived стейкинг-контрактах (StakingRewards.sol).
ERC-721 transfer после стейкинга: ownership confusion
Наивный контракт: при стейке сохраняем stakers[tokenId] = msg.sender, НЕ переводим NFT в контракт. Надеемся, что пользователь сам не переведёт. Это сломается: пользователь стейкает, потом продаёт NFT на маркетплейсе, новый владелец ничего не знает о стейкинге, а старый продолжает получать награды. Или старый пытается unstake чужой NFT.
Правильно: при стейке NFT физически переводится в контракт через transferFrom(msg.sender, address(this), tokenId). При unstake — возвращается. Контракт — custodial. Пользователь не может продать застейканный NFT без unstake.
Альтернатива — non-custodial стейкинг через одобрение + off-chain snapshot. Менее распространено, требует другой архитектуры с Merkle-based claim.
Reentrancy через ERC-721 onERC721Received
При вызове safeTransferFrom контракт вызывает onERC721Received на получателе. Если в unstake функции сначала происходит transfer NFT назад, а потом обновляется stakedTokens[msg.sender] — открывается reentrancy через ERC-721 callback. Сценарий: злоумышленник деплоит получателя с onERC721Received, который повторно вызывает unstake. Итог — двойной вывод наград.
Паттерн Checks-Effects-Interactions: сначала обновляем все storage переменные (delete stakedTokens[msg.sender], totalStaked--, обновляем rewards[msg.sender]), потом делаем внешние вызовы (transfer NFT, transfer rewards). nonReentrant из OpenZeppelin — дополнительный слой защиты.
Архитектура стейкинг-контракта
Мультиколлекция и бусты
Для gaming проектов часто нужно стейкать NFT из разных коллекций с разными весами: герой-легенда даёт 10x rewards/день, обычный герой — 1x. Реализация через mapping(address collection => uint256 multiplier) — owner регистрирует коллекции с весами.
Trait-based буст — NFT с редкими атрибутами дают больше наград. Trait данные хранятся on-chain (дорого) или доступны через Chainlink Functions / собственный oracle. Прагматичное решение: backend подписывает (tokenId, multiplier, nonce), пользователь передаёт подпись при стейке — контракт верифицирует ECDSA.
Lock periods и bonuses
Стейкинг с lock: пользователь выбирает период (30/90/180 дней) при стейке, получает multiplier за долгосрочный лок. Структура:
struct StakeInfo {
address owner;
uint256 stakedAt;
uint256 lockUntil;
uint256 rewardMultiplier; // в basis points: 10000 = 1x, 15000 = 1.5x
uint256 rewardDebt;
}
Early unstake возможен, но с penalty — часть накопленных наград сжигается или остаётся в контракте.
ERC-4626-подобный подход для fungible staking
Если стейкать одинаковые NFT (например, edition коллекция), можно реализовать vault с ERC-4626-подобной механикой: shares пропорционально вкладу, autocompounding наград. Менее применимо для PFP с уникальными ID.
Интеграция с reward token
Стейкинг контракт должен минтить или переводить reward token. Два подхода:
Pre-funded vault — контракт держит фиксированный запас ERC-20 для наград. Прозрачно, предсказуемо, но требует upfront funding.
Minter role — контракт имеет MINTER_ROLE в reward token и минтит награды по запросу. Гибко, но инфляция не ограничена контрактом — нужна внешняя токеномика с emission cap.
Процесс работы
Разработка (3-5 дней). StakingRewards.sol адаптация + тесты в Foundry. Фuzz на расчёт наград с граничными значениями totalStaked.
Аудит. Reentrancy проверка, overflow в reward math, privilege escalation через owner functions.
Деплой. Hardhat-deploy для воспроизводимого деплоя с конфигурацией параметров.
Ориентиры по срокам
Базовый стейкинг одной коллекции — 3-4 дня. Мультиколлекция с trait-бустами и lock periods — 1-1.5 недели.
Стоимость рассчитывается индивидуально.







