Розробка NFT-стейкингу
NFT-стейкинг — це механіка утримання: користувач лочить NFT у контракті на період, отримує ERC-20 токени як награду. Працює як retention tool для ігрових проектів, як accumulator governance power, як 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 після стейкингу: confusion ownership
Наївний контракт: при стейку зберігаємо stakers[tokenId] = msg.sender, НЕ передаємо NFT у контракт. Надіємося, що користувач сам не передасть. Це розбивається: користувач робить стейк, потім продає NFT на маркетплейсі, новий власник нічого не знає про стейкинг, старий продовжує отримувати награди. Або старий намагається unstake чужий NFT.
Правильно: при стейку NFT фізично передається у контракт через transferFrom(msg.sender, address(this), tokenId). При unstake — повертається. Контракт — custodial. Користувач не може продати стейкований NFT без unstake.
Альтернатива — non-custodial стейкинг через approval + 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 — додатковий шар захисту.
Архітектура стейкинг-контракту
Мультиколекція та бусти
Для ігрових проектів часто потрібно робити стейк NFT з різних колекцій з різними вагами: hero-легенда дає 10x rewards/день, звичайний hero — 1x. Реалізація через mapping(address collection => uint256 multiplier) — owner реєструє колекції з вагами.
Trait-based буст — NFT з рідкісними атрибутами дають більше награды. Trait дані зберігаються on-chain (дорого) або доступні через Chainlink Functions / кастомний oracle. Прагматичне рішення: бекенд підписує (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 стейкингу
Якщо робити стейк однакових 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. Fuzz на расчет награды з граничними значеннями totalStaked.
Аудит. Перевірка reentrancy, overflow в reward math, privilege escalation через owner functions.
Деплой. Hardhat-deploy для відтворюваного деплоя з конфігурацією параметрів.
Оцінка часу
Базовий стейкинг однієї колекції — 3-4 дні. Мультиколекція з trait-бустами та lock periods — 1-1.5 тижня.
Вартість розраховується індивідуально.







