Разработка vesting-schedule для команды и инвесторов
Vesting без cliff — это плохой vesting. Основатель, который получает токены линейно с первого дня, не имеет финансового стимула оставаться в проекте после первых месяцев. Стандартная схема в веб3: 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);
}
// Невестед — возвращаем в treasury
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 события, которые оставляют след. В разных юрисдикциях момент налогообложения разный: при вестинге (unlock) или при продаже. Это не техническая, а юридическая проблема, но смарт-контракт должен эмитить правильные события с timestamps для аудита.
Срок разработки: 3–5 дней для стандартных vesting контрактов с фабрикой и тестами. Аудит опционален для небольших аллокаций, обязателен при суммах >$500k.







