Розробка vesting-schedule для команди та інвесторів
Vesting без cliff — це поганий vesting. Засновник, який отримує токени лінійно з першого дня, не має фінансового стимулу лишатися в проекті після перших місяців. Стандартна Web3 схема: 1 рік cliff (немає токенів взагалі) + 3 роки лінійного vesting. Інвестори зазвичай отримують коротший цикл: 6 місяців cliff + 18–24 місяці linear.
Смарт-контракт vesting
Промисловий стандарт — OpenZeppelin VestingWallet. Для команди та інвесторів з різними параметрами деплоїмо окремі інстансі:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/finance/VestingWallet.sol";
// Деплой для конкретного бенефіціара
// VestingWallet(beneficiary, startTimestamp, durationSeconds)
// Команда: cliff 1 рік, vesting 4 роки суммарно
// start = TGE + 1 рік (cliff), duration = 3 роки
address teamMemberVesting = address(new VestingWallet(
teamMemberAddress,
block.timestamp + 365 days, // start після cliff
3 * 365 days // 3 роки лінійного vesting
));
// Поповнити токенами
IERC20(tokenAddress).transfer(teamMemberVesting, allocatedAmount);
VestingWallet — простий і аудований контракт. Бенефіціар вызов release(tokenAddress) для отримання розблокованих токенів. Не можна відозвати — якщо потрібна revocability (звільнений співробітник), потрібна кастомна реалізація.
Revocable vesting для команди
Стандартна VestingWallet не підтримує revoke. Для співробітників потрібна можливість зупинити vesting при звільненні:
contract RevocableVesting is Ownable2Step {
IERC20 public immutable token;
address public beneficiary;
uint256 public immutable start;
uint256 public immutable cliff;
uint256 public immutable duration;
uint256 public immutable totalAllocation;
uint256 public released;
bool public revoked;
uint256 public revokedAt;
constructor(
address _token,
address _beneficiary,
address _owner,
uint256 _start,
uint256 _cliff,
uint256 _duration,
uint256 _totalAllocation
) Ownable2Step() {
token = IERC20(_token);
beneficiary = _beneficiary;
start = _start;
cliff = _cliff;
duration = _duration;
totalAllocation = _totalAllocation;
_transferOwnership(_owner);
}
function vestedAmount() public view returns (uint256) {
uint256 endTime = revoked ? revokedAt : block.timestamp;
if (endTime < start + cliff) return 0;
if (endTime >= start + cliff + duration) return totalAllocation;
uint256 elapsed = endTime - (start + cliff);
return totalAllocation * elapsed / duration;
}
function releasable() public view returns (uint256) {
return vestedAmount() - released;
}
function release() external {
require(msg.sender == beneficiary, "Not beneficiary");
uint256 amount = releasable();
require(amount > 0, "Nothing to release");
released += amount;
token.safeTransfer(beneficiary, amount);
emit TokensReleased(amount);
}
// Owner (компанія) може відозвати невестед токени
function revoke() external onlyOwner {
require(!revoked, "Already revoked");
revoked = true;
revokedAt = block.timestamp;
// Вже вестед — відаємо бенефіціару
uint256 vestedNow = vestedAmount() - released;
if (vestedNow > 0) {
released += vestedNow;
token.safeTransfer(beneficiary, vestedNow);
}
// Невестед — повертаємо в казну
uint256 remaining = token.balanceOf(address(this));
if (remaining > 0) {
token.safeTransfer(owner(), remaining);
}
emit VestingRevoked(revokedAt, vestedNow, remaining);
}
}
Cliff реалізований як перевірка endTime < start + cliff — до цієї дати vestedAmount() повертає 0. Після cliff — лінійне зростання пропорційно часу що пройшов.
Фабрика для деплою кількох контрактів
Деплоїти по одному контракту на кожного отримувача вручну — неудобно. Фабрика:
contract VestingFactory is Ownable2Step {
address public token;
address[] public allVestings;
mapping(address => address) public vestingOf; // beneficiary => vesting contract
event VestingCreated(address indexed beneficiary, address vestingContract, uint256 amount);
struct VestingParams {
address beneficiary;
uint256 amount;
uint256 cliffDays;
uint256 vestingDays;
}
function batchCreate(
VestingParams[] calldata params,
uint256 tgeTimestamp
) external onlyOwner {
for (uint256 i = 0; i < params.length; i++) {
VestingParams memory p = params[i];
require(vestingOf[p.beneficiary] == address(0), "Already has vesting");
RevocableVesting vesting = new RevocableVesting(
token,
p.beneficiary,
owner(),
tgeTimestamp,
p.cliffDays * 1 days,
p.vestingDays * 1 days,
p.amount
);
IERC20(token).safeTransferFrom(msg.sender, address(vesting), p.amount);
allVestings.push(address(vesting));
vestingOf[p.beneficiary] = address(vesting);
emit VestingCreated(p.beneficiary, address(vesting), p.amount);
}
}
}
Один вызов batchCreate — деплой всіх контрактів для команди та інвесторів. Апрув токенів на фабрику перед вызовом.
Параметри за типами отримувачів
| Тип | Cliff | Vesting | Revocable |
|---|---|---|---|
| Засновники | 12 мес | 36 мес після cliff | Так |
| Ранні співробітники | 12 мес | 24–36 мес після cliff | Так |
| Seed інвесторів | 6–12 мес | 12–24 мес після cliff | Ні |
| Стратегічні партнери | 6 мес | 12–18 мес | Частково |
| Радники | 3–6 мес | 12–18 мес | Ні |
Для інвесторів — контракт часто неотзиваемый (умова інвестиційної угоди). Для співробітників — відзиваемый при звільненні.
Податкові розрахунки
Vesting on-chain — це on-chain подій, які залишають слід. У різних юрисдикціях момент оподаткування різний: при vesting (unlock) або при продажі. Це не технічна, а юридична проблема, але смарт-контракт повинен емітувати правильні подій з timestamps для аудиту.
Термін розробки: 3–5 днів для стандартних vesting контрактів з фабрикою та тестами. Аудит опційний для невеликих аллокацій, обов'язковий при сумах >$500k.







