Skip to content

Commit 7c70bee

Browse files
committed
implement vesting manager
1 parent 25b2793 commit 7c70bee

File tree

4 files changed

+360
-28
lines changed

4 files changed

+360
-28
lines changed

src/NubilaNetwork.sol

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity ^0.8.20;
33

4-
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
4+
import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
55

66
contract NubilaNetwork is ERC20 {
7-
constructor(address vestingManager) ERC20("Nubila Network", "NUBI") {
8-
require(vestingManager != address(0), "vesting manager address is zero");
9-
_mint(vestingManager, 1_000_000_000 ether);
7+
constructor(address owner) ERC20("Nubila Network", "NUBI") {
8+
require(owner != address(0), "owner address is zero");
9+
_mint(owner, 1_000_000_000 ether);
1010
}
1111
}

src/VestingManager.sol

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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+
}

test/Counter.t.sol

Lines changed: 0 additions & 24 deletions
This file was deleted.

test/VestingManager.t.sol

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.13;
3+
4+
import {Test, console} from "forge-std/Test.sol";
5+
import {NubilaNetwork} from "../src/NubilaNetwork.sol";
6+
import {VestingManager} from "../src/VestingManager.sol";
7+
8+
contract VestingManagerTest is Test {
9+
VestingManager public manager;
10+
NubilaNetwork public token;
11+
address[] public beneficiaries;
12+
13+
function setUp() public {
14+
beneficiaries.push(address(0x1));
15+
beneficiaries.push(address(0x2));
16+
beneficiaries.push(address(0x3));
17+
beneficiaries.push(address(0x4));
18+
beneficiaries.push(address(0x5));
19+
beneficiaries.push(address(0x6));
20+
beneficiaries.push(address(0x7));
21+
beneficiaries.push(address(0x8));
22+
beneficiaries.push(address(0x9));
23+
beneficiaries.push(address(0xA));
24+
beneficiaries.push(address(0xB));
25+
beneficiaries.push(address(0xC));
26+
token = new NubilaNetwork(address(this));
27+
manager = new VestingManager(address(token), uint64(block.timestamp + 1 minutes), beneficiaries);
28+
assertTrue(token.transfer(address(manager), token.totalSupply()));
29+
}
30+
31+
function testNumOfSchedules() public view {
32+
assertEq(manager.numOfSchedules(), 12);
33+
}
34+
35+
function testGetSchedule() public view {
36+
{
37+
(address beneficiary, uint256 totalAmount, uint256 vestedAmount, uint256 termIndex, VestingManager.Term[] memory terms) = manager.getSchedule(0);
38+
assertEq(beneficiary, address(0x1));
39+
assertEq(totalAmount, 210_000_000 ether);
40+
assertEq(vestedAmount, 0);
41+
assertEq(termIndex, 0);
42+
assertEq(terms.length, 2);
43+
assertEq(terms[0].percentage, 50_000);
44+
assertEq(terms[0].cliff, 0);
45+
assertEq(terms[0].period, 0);
46+
assertEq(terms[0].num, 1);
47+
assertEq(terms[1].percentage, 950_000);
48+
assertEq(terms[1].cliff, 30 days);
49+
assertEq(terms[1].period, 30 days);
50+
assertEq(terms[1].num, 60);
51+
}
52+
{
53+
(address beneficiary, uint256 totalAmount, uint256 vestedAmount, uint256 termIndex, VestingManager.Term[] memory terms) = manager.getSchedule(1);
54+
assertEq(beneficiary, address(0x2));
55+
assertEq(totalAmount, 200_000_000 ether);
56+
assertEq(vestedAmount, 0);
57+
assertEq(termIndex, 0);
58+
assertEq(terms.length, 2);
59+
assertEq(terms[0].percentage, 50_000);
60+
assertEq(terms[0].cliff, 0);
61+
assertEq(terms[0].period, 0);
62+
assertEq(terms[0].num, 1);
63+
assertEq(terms[1].percentage, 950_000);
64+
assertEq(terms[1].cliff, 30 days);
65+
assertEq(terms[1].period, 30 days);
66+
assertEq(terms[1].num, 60);
67+
}
68+
{
69+
(address beneficiary, uint256 totalAmount, uint256 vestedAmount, uint256 termIndex, VestingManager.Term[] memory terms) = manager.getSchedule(2);
70+
assertEq(beneficiary, address(0x3));
71+
assertEq(totalAmount, 62_500_000 ether);
72+
assertEq(vestedAmount, 0);
73+
assertEq(termIndex, 0);
74+
assertEq(terms.length, 1);
75+
assertEq(terms[0].percentage, 1_000_000);
76+
assertEq(terms[0].cliff, 12 * 30 days);
77+
assertEq(terms[0].period, 30 days);
78+
assertEq(terms[0].num, 36);
79+
}
80+
}
81+
82+
function testBeforeTGE() public {
83+
vm.warp(block.timestamp + 30 seconds);
84+
for (uint i = 0; i < 12; ++i) {
85+
assertEq(manager.claimable(i), 0);
86+
}
87+
}
88+
89+
function testSchedule0AfterTGE() public {
90+
vm.warp(block.timestamp + 2 minutes);
91+
// schedule 0
92+
assertEq(manager.claimable(0), 10_500_000 ether);
93+
manager.claim(0);
94+
assertEq(token.balanceOf(beneficiaries[0]), 10_500_000 ether);
95+
assertEq(manager.claimable(0), 0);
96+
vm.warp(block.timestamp + 31 days);
97+
assertEq(manager.claimable(0), 3_325_000 ether);
98+
vm.prank(beneficiaries[0]);
99+
manager.claim(0);
100+
assertEq(token.balanceOf(beneficiaries[0]), 13_825_000 ether);
101+
assertEq(manager.claimable(0), 0);
102+
vm.warp(block.timestamp + 30 days);
103+
assertEq(manager.claimable(0), 3_325_000 ether);
104+
vm.prank(beneficiaries[0]);
105+
manager.claim(0);
106+
assertEq(token.balanceOf(beneficiaries[0]), 17_150_000 ether);
107+
assertEq(manager.claimable(0), 0);
108+
}
109+
110+
function testSchedule1AfterTGE() public {
111+
vm.warp(block.timestamp + 2 minutes);
112+
// schedule 1
113+
assertEq(manager.claimable(1), 10_000_000 ether);
114+
manager.claim(1);
115+
assertEq(token.balanceOf(beneficiaries[1]), 10_000_000 ether);
116+
assertEq(manager.claimable(1), 0);
117+
vm.warp(block.timestamp + 31 days);
118+
assertEq(manager.claimable(1), 3_166_666 ether + 666666666666666666);
119+
manager.claim(1);
120+
assertEq(token.balanceOf(beneficiaries[1]), 13_166_666 ether + 666666666666666666);
121+
assertEq(manager.claimable(1), 0);
122+
vm.warp(block.timestamp + 30 days);
123+
assertEq(manager.claimable(1), 3_166_666 ether + 666666666666666666);
124+
manager.claim(1);
125+
assertEq(token.balanceOf(beneficiaries[1]), 16_333_333 ether + 333333333333333332);
126+
assertEq(manager.claimable(1), 0);
127+
}
128+
129+
function testSchedule2AfterTGE() public {
130+
vm.warp(block.timestamp + 2 minutes);
131+
assertEq(manager.claimable(2), 0);
132+
vm.warp(block.timestamp + 12 * 30 days);
133+
assertEq(manager.claimable(2), 1_736_111 ether + 111111111111111111);
134+
manager.claim(2);
135+
assertEq(token.balanceOf(beneficiaries[2]), 1_736_111 ether + 111111111111111111);
136+
assertEq(manager.claimable(2), 0);
137+
vm.warp(block.timestamp + 30 days);
138+
assertEq(manager.claimable(2), 1_736_111 ether + 111111111111111111);
139+
manager.claim(2);
140+
assertEq(token.balanceOf(beneficiaries[2]), 3_472_222 ether + 222222222222222222);
141+
assertEq(manager.claimable(2), 0);
142+
vm.warp(block.timestamp + 30 days);
143+
assertEq(manager.claimable(2), 1_736_111 ether + 111111111111111111);
144+
manager.claim(2);
145+
assertEq(token.balanceOf(beneficiaries[2]), 5_208_333 ether + 333333333333333333);
146+
assertEq(manager.claimable(2), 0);
147+
}
148+
}

0 commit comments

Comments
 (0)