Розробка контрактів вестингу токенів

Проєктуємо та розробляємо блокчейн-рішення повного циклу: від архітектури смарт-контрактів до запуску DeFi-протоколів, NFT-маркетплейсів та криптобірж. Аудит безпеки, токеноміка, інтеграція з наявною інфраструктурою.
Показано 1 з 1Усі 1306 послуг
Розробка контрактів вестингу токенів
Середній
~2-3 дні
Часті запитання

Напрямки блокчейн-розробки

Етапи блокчейн-розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1306
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1217
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    919
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1146
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    609
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    884

Розробка контракту вестинга токенів

Вестинг-контракт виглядає простим — лінійне розблокування токенів по часу. На практиці саме в цих контрактах концентрується значний обсяг фінансових втрат: неправильна обробка cliff, вразливість до rug pull через backdoor в admin-функціях, відсутність ревокації при звільненні та баги з точністю обчислень при роботі з великими числами. Перш ніж писати код, потрібно чітко визначити вимоги до моделі вестинга.

Моделі вестинга

Типові схеми, які зустрічаються на практиці:

Linear vesting with cliff: найпоширеніша для команди та інвесторів. Токени повністю заблоковані до cliff date, потім розблокуються рівномірно до end date. Приклад: 1-year cliff, 4-year total vesting — стандарт для team allocation в більшості протоколів.

Graded vesting: різні відсотки в різні періоди. Використовується для IDO/ICO: 10% TGE, потім рівномірно 6–12 місяців.

Milestone-based vesting: розблокування привʼязано до подій (mainnet launch, TVL цілі), а не тільки часу. Вимагає oracle або мультисиг для верифікації milestones.

Архітектура контракту

// 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;      // мінімальний період розблокування (напр. 30 днів)
        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;
        }
        
        // лінійне розблокування з урахуванням slicePeriod
        uint256 timeFromStart = currentTime - schedule.startTime;
        uint256 vestedSlices = timeFromStart / schedule.slicePeriod;
        uint256 vestedSeconds = vestedSlices * schedule.slicePeriod;
        
        // важливо: деление перед множенням для запобігання overflow
        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);
    }
}

Типичні помилки

Precision loss: при розрахунку (totalAmount * elapsed) / duration порядок операцій критичний. Множення повинно передувати діленню. Для токенів з 18 decimals проміжне значення може не поміститися в uint256 при дуже великих сумах — використовуйте mulDiv з OpenZeppelin Math library.

block.timestamp маніпуляція: валідатори можуть зсунути timestamp в невеликих межах (~15 секунд на Ethereum). Для вестинга з періодом у місяці це несущественно, але для slicePeriod < 1 години — потенційна проблема.

Відсутність перевірки баланса: при створенні schedule контракт повинен убедиться що на його балансі достатньо токенів для покриття нових зобов'язань: token.balanceOf(address(this)) >= vestingSchedulesTotalAmount + newAmount.

Безпека та access control

Функції створення та відзиву schedule не повинні бути у одного ключа. Рекомендована схема:

  • ADMIN_ROLE: Gnosis Safe 3/5 мультисиг — створення та відзив вестинг-розписань
  • TIMELOCK: для критичних функцій (зміна admin, upgrade) — 48–72 години затримки

Функція revoke() — особливо чутлива. Вона дозволяє забрати всі незаробленні токени. Якщо revocable = true для investor allocation — це червоний флаг. Revocable vesting доцільно тільки для команди.

TGE + лінійний вестинг: комбінована схема

Часто потрібна схема: X% при TGE (Token Generation Event), решта по лінійному розписанню. Реалізується як два окремі 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;
    
    // немедленное разблокирование TGE части
    _createSchedule(beneficiary, tgeAmount, 0, 0, 1); // duration=1 сразу доступно
    
    // лінійний вестинг решти
    _createSchedule(beneficiary, vestingAmount, vestingStart, 0, vestingDuration);
}

Мультитокенний вестинг

Якщо протокол має кілька токенів (governance + utility) або вестинг потрібен для LP-токенів — можна узагальнити контракт, приймаючи адресу токена як параметр. Це ускладнює логіку обліку балансів та вимагає маппинга token → totalVested. Аудит стає складніше. Виправдано тільки якщо дійсно потрібні різні токени — не ускладнюйте заради "гнучкості".