|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | +pragma solidity ^0.8.20; |
| 3 | + |
| 4 | +import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; |
| 5 | + |
| 6 | +/// @title Vesting Manager with Vesting Schedules |
| 7 | +contract VestingManager { |
| 8 | + IERC20 public immutable token; |
| 9 | + uint64 public immutable tgeTimestamp; |
| 10 | + |
| 11 | + uint32 public constant PPM = 1_000_000; // 100% |
| 12 | + uint64 public constant MONTH = 30 days; |
| 13 | + |
| 14 | + struct Term { |
| 15 | + uint32 percentage; |
| 16 | + uint64 cliff; |
| 17 | + uint64 period; |
| 18 | + uint16 num; |
| 19 | + uint16 next; |
| 20 | + } |
| 21 | + |
| 22 | + struct VestingSchedule { |
| 23 | + address beneficiary; |
| 24 | + uint256 totalAmount; |
| 25 | + uint256 vestedAmount; |
| 26 | + uint256 termIndex; |
| 27 | + Term[] terms; |
| 28 | + } |
| 29 | + |
| 30 | + VestingSchedule[] private schedules; |
| 31 | + |
| 32 | + event ScheduleCreated(uint256 indexed id, address indexed beneficiary, uint256 totalAmount); |
| 33 | + event Vested(uint256 indexed id, address indexed beneficiary, uint256 indexed termIndex, uint256 periodIdx, uint256 amount); |
| 34 | + |
| 35 | + constructor(address _token, uint64 _tgeTimestamp, address[] memory beneficiaries) { |
| 36 | + require(_token != address(0), "token zero"); |
| 37 | + require(beneficiaries.length == 12, "need 12 addresses"); |
| 38 | + token = IERC20(_token); |
| 39 | + tgeTimestamp = _tgeTimestamp; |
| 40 | + |
| 41 | + uint256 A_device = 210_000_000 ether; |
| 42 | + uint256 A_node = 200_000_000 ether; |
| 43 | + uint256 A_preseed = 62_500_000 ether; |
| 44 | + uint256 A_seed = 80_000_000 ether; |
| 45 | + uint256 A_public = 5_000_000 ether; |
| 46 | + uint256 A_pos = 65_000_000 ether; |
| 47 | + uint256 A_found = 78_000_000 ether; |
| 48 | + uint256 A_team = 120_000_000 ether; |
| 49 | + uint256 A_adv = 20_000_000 ether; |
| 50 | + uint256 A_liq = 22_500_000 ether; |
| 51 | + uint256 A_comm = 75_000_000 ether; |
| 52 | + uint256 A_cex = 62_000_000 ether; |
| 53 | + |
| 54 | + Term[] memory terms; |
| 55 | + { |
| 56 | + terms = new Term[](2); |
| 57 | + terms[0] = Term({ percentage: 50_000, cliff: 0, period: 0, num: 1, next: 0 }); |
| 58 | + terms[1] = Term({ percentage: 950_000, cliff: MONTH, period: MONTH, num: 60, next: 0 }); |
| 59 | + _createScheduleInternal(beneficiaries[0], A_device, terms); |
| 60 | + } |
| 61 | + { |
| 62 | + terms = new Term[](2); |
| 63 | + terms[0] = Term({ percentage: 50_000, cliff: 0, period: 0, num: 1, next: 0 }); |
| 64 | + terms[1] = Term({ percentage: 950_000, cliff: MONTH, period: MONTH, num: 60, next: 0 }); |
| 65 | + _createScheduleInternal(beneficiaries[1], A_node, terms); |
| 66 | + } |
| 67 | + { |
| 68 | + terms = new Term[](1); |
| 69 | + terms[0] = Term({ percentage: PPM, cliff: 12 * MONTH, period: MONTH, num: 36, next: 0 }); |
| 70 | + _createScheduleInternal(beneficiaries[2], A_preseed, terms); |
| 71 | + } |
| 72 | + { |
| 73 | + terms = new Term[](1); |
| 74 | + terms[0] = Term({ percentage: PPM, cliff: 12 * MONTH, period: MONTH, num: 24, next: 0 }); |
| 75 | + _createScheduleInternal(beneficiaries[3], A_seed, terms); |
| 76 | + } |
| 77 | + { |
| 78 | + terms = new Term[](1); |
| 79 | + terms[0] = Term({ percentage: PPM, cliff: 0, period: 0, num: 1, next: 0 }); |
| 80 | + _createScheduleInternal(beneficiaries[4], A_public, terms); |
| 81 | + } |
| 82 | + { |
| 83 | + terms = new Term[](2); |
| 84 | + terms[0] = Term({ percentage: 250_000, cliff: 0, period: 0, num: 1, next: 0 }); |
| 85 | + terms[1] = Term({ percentage: 750_000, cliff: MONTH, period: MONTH, num: 60, next: 0 }); |
| 86 | + _createScheduleInternal(beneficiaries[5], A_pos, terms); |
| 87 | + } |
| 88 | + { |
| 89 | + terms = new Term[](2); |
| 90 | + terms[0] = Term({ percentage: 400_000, cliff: 0, period: 0, num: 1, next: 0 }); |
| 91 | + terms[1] = Term({ percentage: 600_000, cliff: MONTH, period: MONTH, num: 12, next: 0 }); |
| 92 | + _createScheduleInternal(beneficiaries[6], A_found, terms); |
| 93 | + } |
| 94 | + { |
| 95 | + terms = new Term[](1); |
| 96 | + terms[0] = Term({ percentage: PPM, cliff: 12 * MONTH, period: MONTH, num: 36, next: 0 }); |
| 97 | + _createScheduleInternal(beneficiaries[7], A_team, terms); |
| 98 | + } |
| 99 | + { |
| 100 | + terms = new Term[](1); |
| 101 | + terms[0] = Term({ percentage: PPM, cliff: 12 * MONTH, period: MONTH, num: 36, next: 0 }); |
| 102 | + _createScheduleInternal(beneficiaries[8], A_adv, terms); |
| 103 | + } |
| 104 | + { |
| 105 | + terms = new Term[](1); |
| 106 | + terms[0] = Term({ percentage: PPM, cliff: 0, period: 0, num: 1, next: 0 }); |
| 107 | + _createScheduleInternal(beneficiaries[9], A_liq, terms); |
| 108 | + } |
| 109 | + { |
| 110 | + terms = new Term[](3); |
| 111 | + terms[0] = Term({ percentage: 250_000, cliff: 0, period: 0, num: 1, next: 0 }); |
| 112 | + terms[1] = Term({ percentage: 250_000, cliff: 3 * MONTH, period: 0, num: 1, next: 0 }); |
| 113 | + terms[2] = Term({ percentage: 500_000, cliff: 6 * MONTH, period: 0, num: 1, next: 0 }); |
| 114 | + _createScheduleInternal(beneficiaries[10], A_comm, terms); |
| 115 | + } |
| 116 | + { |
| 117 | + terms = new Term[](1); |
| 118 | + terms[0] = Term({ percentage: PPM, cliff: 0, period: 0, num: 1, next: 0 }); |
| 119 | + _createScheduleInternal(beneficiaries[11], A_cex, terms); |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + function _createScheduleInternal( |
| 124 | + address beneficiary, |
| 125 | + uint256 totalAmount, |
| 126 | + Term[] memory terms |
| 127 | + ) internal { |
| 128 | + uint256 sumWeight = 0; |
| 129 | + for (uint i = 0; i < terms.length; ++i) { |
| 130 | + if (terms[i].period == 0) { |
| 131 | + require(terms[i].num == 1, "num must 1 when period==0"); |
| 132 | + } else { |
| 133 | + require(terms[i].num > 0, "num zero"); |
| 134 | + } |
| 135 | + sumWeight += terms[i].percentage; |
| 136 | + } |
| 137 | + require(sumWeight == PPM, "weights must sum to PPM"); |
| 138 | + |
| 139 | + VestingSchedule storage s = schedules.push(); |
| 140 | + s.beneficiary = beneficiary; |
| 141 | + s.totalAmount = totalAmount; |
| 142 | + s.vestedAmount = 0; |
| 143 | + s.termIndex = 0; |
| 144 | + for (uint i = 0; i < terms.length; ++i) { |
| 145 | + s.terms.push(terms[i]); |
| 146 | + } |
| 147 | + |
| 148 | + emit ScheduleCreated(schedules.length - 1, beneficiary, totalAmount); |
| 149 | + } |
| 150 | + |
| 151 | + function claim(uint256 scheduleId) external { |
| 152 | + require(scheduleId < schedules.length, "invalid index"); |
| 153 | + VestingSchedule storage s = schedules[scheduleId]; |
| 154 | + require(s.termIndex < s.terms.length, "invalid term index"); |
| 155 | + Term storage t = s.terms[s.termIndex]; |
| 156 | + uint256 termIdx = s.termIndex; |
| 157 | + uint256 periodIdx = t.next; |
| 158 | + require(periodIdx < t.num, "invalid period id"); |
| 159 | + require(block.timestamp >= tgeTimestamp + t.cliff + t.period * periodIdx, "cliff not reached"); |
| 160 | + uint256 amount = s.totalAmount * t.percentage / 1_000_000 / t.num; |
| 161 | + if (periodIdx + 1 == t.num && termIdx + 1 == s.terms.length) { |
| 162 | + amount = s.totalAmount - s.vestedAmount; |
| 163 | + } |
| 164 | + s.vestedAmount += amount; |
| 165 | + t.next += 1; |
| 166 | + if (periodIdx + 1 == t.num) { |
| 167 | + s.termIndex += 1; |
| 168 | + } |
| 169 | + require(token.transfer(s.beneficiary, amount), "transfer failed"); |
| 170 | + |
| 171 | + emit Vested(scheduleId, s.beneficiary, termIdx, periodIdx, amount); |
| 172 | + } |
| 173 | + |
| 174 | + function numOfSchedules() external view returns (uint256) { |
| 175 | + return schedules.length; |
| 176 | + } |
| 177 | + |
| 178 | + function getSchedule(uint256 scheduleId) external view returns ( |
| 179 | + address beneficiary, |
| 180 | + uint256 totalAmount, |
| 181 | + uint256 vestedAmount, |
| 182 | + uint256 termIndex, |
| 183 | + Term[] memory terms |
| 184 | + ) { |
| 185 | + require(scheduleId < schedules.length, "invalid index"); |
| 186 | + VestingSchedule storage s = schedules[scheduleId]; |
| 187 | + return (s.beneficiary, s.totalAmount, s.vestedAmount, s.termIndex, s.terms); |
| 188 | + } |
| 189 | + |
| 190 | + function claimable(uint256 scheduleId) external view returns (uint256) { |
| 191 | + require(scheduleId < schedules.length, "invalid index"); |
| 192 | + VestingSchedule storage s = schedules[scheduleId]; |
| 193 | + if (s.termIndex >= s.terms.length) { |
| 194 | + return 0; |
| 195 | + } |
| 196 | + Term storage t = s.terms[s.termIndex]; |
| 197 | + if (t.next >= t.num) { |
| 198 | + return 0; |
| 199 | + } |
| 200 | + if (block.timestamp < tgeTimestamp + t.cliff + t.period * t.next) { |
| 201 | + return 0; |
| 202 | + } |
| 203 | + if (t.next + 1 == t.num && s.termIndex + 1 == s.terms.length) { |
| 204 | + return s.totalAmount - s.vestedAmount; |
| 205 | + } |
| 206 | + return s.totalAmount * t.percentage / 1_000_000 / t.num; |
| 207 | + } |
| 208 | +} |
0 commit comments