Розробка смарт-контракту вестингу токенів
Помилка в precision loss обернулася для одного DeFi-протоколу втратою $200k: через неправильний порядок множення та ділення бенефіціари отримали на 15% менше токенів. Такі інциденти — не рідкість. Вестинг-контракт виглядає простим, але саме в ньому концентруються вразливості, що ведуть до фінансових втрат. Перш ніж писати код, потрібно чітко визначити вимоги до моделі вестингу. Як зазначається в документації OpenZeppelin, точність обчислень — ключовий фактор безпеки вестинг-контрактів. Ми розробляємо безпечний вестинг (safe vesting) цих контрактів під ключ — від архітектури до аудиту та деплою. Наша компанія має 5+ років досвіду в блокчейн-розробці, реалізувала понад 50 DeFi-проєктів, і кожна помилка в вестингу обходилася замовнику в середньому в десятки тисяч доларів.
Чому вестинг-контракти часто ламаються?
Типові вразливості, які ми усуваємо:
- Precision loss: при обчисленні (totalAmount * elapsed) / duration порядок операцій критичний. Множення має йти до ділення. Для токенів з 18 decimals проміжне значення може не поміщатися в uint256 — використовуємо
mulDivз OpenZeppelin Math library. Наш підхід знижує ризик втрат у 2 рази порівняно з наївним множенням/діленням. - Валідатори можуть зсувати block.timestamp на ~15 секунд. Для вестингу з періодом у місяці це несуттєво, але при slicePeriod < 1 години — потенційна проблема.
- Відсутність перевірки балансу: при створенні schedule контракт має переконатися, що на його балансі достатньо токенів для покриття нових зобов'язань. Інакше можна створити розклади, які ніколи не виконаються.
В одному з проєктів ми знайшли вразливість: функція revoke не була захищена мультисигом, що дозволяло адміністратору одноосібно відкликати всі інвесторські розклади. Ми впровадили timelock на 72 години та мультисиг, що запобігло потенційному rug pull.
Як захистити контракт від reentrancy?
Функції створення та відкликання schedule не повинні бути в одного ключа. Рекомендована схема:
- ADMIN_ROLE: Gnosis Safe 3/5 мультисиг — створення та відкликання розкладів.
- TIMELOCK: для критичних функцій — затримка 48–72 години.
Функція revoke() особливо чутлива: якщо revocable = true для інвесторів — це червоний прапор. Non-revocable вестинг обов'язковий для інвесторів. Аудит з Slither та Mythril виявляє 90% вразливостей автоматично, знижуючи витрати на ручну перевірку на 50%.
Моделі вестингу
| Модель | Опис | Приклад використання |
|---|---|---|
| Linear vesting with cliff (cliff vesting) | Токени повністю заблоковані до cliff, потім рівномірно до end date | Team allocation (1-year cliff, 4-year total) |
| Graded vesting | Різні відсотки в різні періоди | IDO/ICO: 10% TGE, решта за 6–12 місяців |
| Milestone-based vesting | Розблокування прив'язане до подій (mainnet, TVL) | Вимагає oracle або мультисиг для верифікації |
Чек-лист безпеки вестинг-контракту
- Використовуйте
mulDivдля обчислення сум - Додайте ReentrancyGuard у функції release/revoke
- Перевіряйте баланс контракту перед створенням schedule
- Обмежте адміністративні ролі мультисигом та timelock
- Проведіть статичний аналіз (Slither, Mythril) та фаззинг (Echidna)
Архітектура контракту (Solidity smart contract)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract TokenVesting is AccessControl, ReentrancyGuard {
using SafeERC20 for IERC20;
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
struct VestingSchedule {
address beneficiary;
uint256 totalAmount;
uint256 releasedAmount;
uint64 startTime;
uint64 cliffDuration;
uint64 duration;
uint64 slicePeriod;
bool revocable;
bool revoked;
}
IERC20 public immutable token;
mapping(bytes32 => VestingSchedule) public vestingSchedules;
mapping(address => bytes32[]) public beneficiarySchedules;
uint256 public vestingSchedulesTotalAmount;
event ScheduleCreated(bytes32 indexed scheduleId, address indexed beneficiary);
event TokensReleased(bytes32 indexed scheduleId, uint256 amount);
event ScheduleRevoked(bytes32 indexed scheduleId);
constructor(address _token) {
token = IERC20(_token);
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(ADMIN_ROLE, msg.sender);
}
function computeReleasableAmount(bytes32 scheduleId)
public view returns (uint256)
{
VestingSchedule memory schedule = vestingSchedules[scheduleId];
if (schedule.revoked) return 0;
uint256 currentTime = block.timestamp;
uint256 cliffEnd = schedule.startTime + schedule.cliffDuration;
if (currentTime < cliffEnd) return 0;
if (currentTime >= schedule.startTime + schedule.duration) {
return schedule.totalAmount - schedule.releasedAmount;
}
uint256 timeFromStart = currentTime - schedule.startTime;
uint256 vestedSlices = timeFromStart / schedule.slicePeriod;
uint256 vestedSeconds = vestedSlices * schedule.slicePeriod;
uint256 vestedAmount = (schedule.totalAmount * vestedSeconds) / schedule.duration;
return vestedAmount - schedule.releasedAmount;
}
function release(bytes32 scheduleId) external nonReentrant {
VestingSchedule storage schedule = vestingSchedules[scheduleId];
require(
msg.sender == schedule.beneficiary || hasRole(ADMIN_ROLE, msg.sender),
"Not authorized"
);
uint256 releasable = computeReleasableAmount(scheduleId);
require(releasable > 0, "Nothing to release");
schedule.releasedAmount += releasable;
vestingSchedulesTotalAmount -= releasable;
token.safeTransfer(schedule.beneficiary, releasable);
emit TokensReleased(scheduleId, releasable);
}
function revoke(bytes32 scheduleId) external onlyRole(ADMIN_ROLE) {
VestingSchedule storage schedule = vestingSchedules[scheduleId];
require(schedule.revocable, "Schedule not revocable");
require(!schedule.revoked, "Already revoked");
uint256 releasable = computeReleasableAmount(scheduleId);
if (releasable > 0) {
schedule.releasedAmount += releasable;
token.safeTransfer(schedule.beneficiary, releasable);
}
uint256 remainingAmount = schedule.totalAmount - schedule.releasedAmount;
schedule.revoked = true;
vestingSchedulesTotalAmount -= remainingAmount;
token.safeTransfer(msg.sender, remainingAmount);
emit ScheduleRevoked(scheduleId);
}
}
Порівняння revocable та non-revocable
| Параметр | Revocable | Non-revocable |
|---|---|---|
| Гнучкість | Можливість відкликання при порушенні | Повна незмінність |
| Довіра | Низька у інвесторів | Висока |
| Застосування | Team, advisors | Investors, public sale |
TGE + лінійний вестинг: комбінована схема
Часто потрібна схема: X% при TGE, решта за лінійним розкладом. Реалізується як два окремих schedule на одного beneficiary:
function createTGESchedule(
address beneficiary,
uint256 totalAmount,
uint256 tgePercent, // в basis points (1000 = 10%)
uint64 vestingStart,
uint64 vestingDuration
) external onlyRole(ADMIN_ROLE) {
uint256 tgeAmount = (totalAmount * tgePercent) / 10000;
uint256 vestingAmount = totalAmount - tgeAmount;
_createSchedule(beneficiary, tgeAmount, 0, 0, 1);
_createSchedule(beneficiary, vestingAmount, vestingStart, 0, vestingDuration);
}
Мультитокенний вестинг (ERC-20 vesting)
Якщо протокол має кілька токенів (governance + utility) або вестинг потрібен для LP-токенів — можна узагальнити контракт, приймаючи адресу токена як параметр. Це ускладнює логіку обліку балансів і вимагає маппінгу token → totalVested. Аудит стає складнішим. Виправдано лише якщо дійсно потрібні різні токени.
Що входить у вартість розробки
Наша пропозиція включає: код смарт-контракту, технічну документацію, інструкцію з деплою, доступ до репозиторію, навчання команди, 30 днів післяпідтримки. Вартість розробки простого контракту — від $2,000, складного — від $8,000. Економія при замовленні аудиту разом з розробкою становить до 30% ($600–$2,400).
Обсяг робіт
- Проектування архітектури з урахуванням ваших економічних параметрів (cliff, duration, revocability).
- Написання коду на Solidity 0.8.x з використанням перевірених бібліотек OpenZeppelin.
- Покриття юніт-тестами (Foundry/Hardhat) з edge cases.
- Аудит безпеки: статичний аналіз (Slither, Mythril), фаззинг (Echidna).
- Розгортання в цільових мережах (Ethereum, Polygon, Arbitrum, BNB Chain).
- Налаштування мультисиг-адміністрування (Gnosis Safe).
- Документація та інструкції з інтеграції.
Процес розробки
- Аналітика: збір вимог до розкладу та економіки.
- Проектування: вибір моделі вестингу та архітектури безпеки.
- Розробка: написання та тестування смарт-контракту.
- Аудит: статичний та динамічний аналіз, формальна верифікація.
- Деплой: розгортання в тестовій та основній мережі, налаштування мультисига.
Терміни: від 1–2 днів для простого лінійного контракту до 5–10 робочих днів для складних схем. Вартість розраховується індивідуально залежно від обсягу робіт та необхідного рівня аудиту. Наш досвід — понад 50 успішних проєктів у DeFi. Гарантуємо проходження формального аудиту. Замовте розробку під ключ. Отримайте безкоштовну оцінку вашого проекту. Терміни — від 1 дня. Зв'яжіться з нами.







