Skip to content

Commit 1584eed

Browse files
committed
draft: vesting tests
1 parent b2fceb7 commit 1584eed

4 files changed

Lines changed: 213 additions & 2 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// TODO: Уточнить, что не подходит для реализации вестинга ребейз токенов
2+
13
## Foundry
24

35
**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**

src/tokenDistribution/MetaTokenDistributor.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,12 @@ contract MetaTokenDistributor is IMetaTokenDistributor {
4343
}
4444

4545
function startVesting(VestingType vestingType) external returns (address vesting) {
46+
// TODO: механизм, который позволит создавать вестинг для каждого типа единожды
4647
if (block.timestamp < _vestingStartTime) {
4748
revert VestingStartTimeHasNotArrived();
4849
}
4950

51+
// TODO: Может быть клонировано с Clones.cloneWithImmutableArgs
5052
vesting = Clones.clone(_vestingImpl);
5153

5254
// TODO: Можно две функции объединить
@@ -58,6 +60,7 @@ contract MetaTokenDistributor is IMetaTokenDistributor {
5860
(uint256 vestingAmount,,,,) = _vestingParams.getVestingParams(vestingType);
5961

6062
_META.safeTransfer(address(vesting), vestingAmount);
63+
6164
IVesting(vesting).initialize(_META, schedule, beneficiaries);
6265

6366
emit VestingStarted(vesting);

src/tokenDistribution/vesting/Vesting.sol

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.s
77
import {IVesting} from "./interfaces/IVesting.sol";
88
import {Schedule, Period, Beneficiary} from "../utils/Common.sol";
99

10-
// TODO: Проверить отчет на предмет замечаний
11-
1210
/**
1311
* @title Vesting contract of the base token
1412
* @dev Each new vesting instance is created through a factory using the Minimal Clones pattern.
@@ -66,6 +64,7 @@ contract Vesting is IVesting, Initializable {
6664
function claim() external {
6765
Schedule memory schedule = _schedule;
6866

67+
// TODO: кажется это может быть удалено?
6968
if (block.timestamp < schedule.startTime) {
7069
revert VestingHasNotStarted();
7170
}
@@ -98,6 +97,9 @@ contract Vesting is IVesting, Initializable {
9897

9998
/// @notice Returns the total amount of locked tokens
10099
function totalLocked() external view returns (uint256) {
100+
// TODO: заменить на
101+
// uint256 initialTotalLocked = _initialTotalLocked;
102+
// return initialTotalLocked - _computeUnlocked(initialTotalLocked);
101103
return _initialTotalLocked - totalUnlocked();
102104
}
103105

@@ -108,6 +110,9 @@ contract Vesting is IVesting, Initializable {
108110

109111
/// @notice Returns the amount of locked tokens for a specific account
110112
function lockedOf(address account) external view returns (uint256) {
113+
// TODO: заменить на
114+
// uint256 initialLocked = _initialLocked[account];
115+
// return initialLocked - _computeUnlocked(initialLocked);
111116
return _initialLocked[account] - unlockedOf(account);
112117
}
113118

@@ -191,6 +196,7 @@ contract Vesting is IVesting, Initializable {
191196
}
192197

193198
_initialLocked[beneficiary.account] = beneficiary.amount;
199+
// TODO: переделать на чтение из memory и последующая запись в storage
194200
_initialTotalLocked += beneficiary.amount;
195201
}
196202

@@ -207,6 +213,7 @@ contract Vesting is IVesting, Initializable {
207213
* @return unlockedAmount The total amount of unlocked tokens
208214
*/
209215
function _computeUnlocked(uint256 initialLockedAmount) private view returns (uint256 unlockedAmount) {
216+
// TODO: добавить _schedule в memory
210217
if (block.timestamp < _schedule.startTime) {
211218
return 0;
212219
}
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity 0.8.28;
3+
4+
import {Test, console} from "forge-std/Test.sol";
5+
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
6+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
7+
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
8+
9+
import {Vesting, IVesting} from "src/tokenDistribution/vesting/Vesting.sol";
10+
import {MetaToken} from "src/MetaToken.sol";
11+
import {VestingType, Beneficiary, Schedule, Period} from "src/tokenDistribution/utils/Common.sol";
12+
13+
contract VestingTest is Test {
14+
uint256 public constant MONTH = 30;
15+
uint256 public constant BASIS_POINTS = 10_000; // the same vesting contract
16+
17+
uint256 public constant SCHEDULE_TGE_PORTION = 2000;
18+
uint256 public constant SCHEDULE_PERIOD_PORTION = 4000;
19+
uint256 public constant SCHEDULE_TOTAL_PERIODS = 4;
20+
21+
uint256 public constant VESTING_TOTAL_AMOUNT = 1_000_000e18;
22+
uint256 public constant BENEFICIARY1_AMOUNT = VESTING_TOTAL_AMOUNT * 0.6e18 / 1e18; // 60%
23+
uint256 public constant BENEFICIARY2_AMOUNT = VESTING_TOTAL_AMOUNT * 0.4e18 / 1e18; // 40%
24+
25+
IERC20 public META;
26+
Vesting public vesting;
27+
28+
address distributor;
29+
address beneficiary1;
30+
address beneficiary2;
31+
32+
function setUp() public {
33+
distributor = makeAddr("distributor");
34+
beneficiary1 = makeAddr("beneficiary1");
35+
beneficiary2 = makeAddr("beneficiary2");
36+
37+
META = IERC20(address(new MetaToken(distributor)));
38+
39+
(Beneficiary[] memory beneficiaries, Schedule memory schedule) = _getBeneficiariesAndSchedule();
40+
vesting = _deployVesting(META, beneficiaries, schedule);
41+
}
42+
43+
// region - Deploy Vesting -
44+
45+
function test_deploy() public view {
46+
assertEq(vesting.getBaseToken(), address(META));
47+
assertEq(vesting.totalLocked() + vesting.totalUnlocked(), VESTING_TOTAL_AMOUNT);
48+
49+
assertEq(vesting.getSchedule().startTime, block.timestamp);
50+
assertEq(vesting.getSchedule().periods.length, SCHEDULE_TOTAL_PERIODS);
51+
assertEq(vesting.getSchedule().periods[0].endTime, block.timestamp);
52+
assertEq(vesting.getSchedule().periods[0].portion, SCHEDULE_TGE_PORTION);
53+
assertEq(vesting.getSchedule().periods[1].endTime, block.timestamp + MONTH);
54+
assertEq(vesting.getSchedule().periods[1].portion, 0);
55+
assertEq(vesting.getSchedule().periods[2].endTime, block.timestamp + 2 * MONTH);
56+
assertEq(vesting.getSchedule().periods[2].portion, SCHEDULE_PERIOD_PORTION);
57+
assertEq(vesting.getSchedule().periods[3].endTime, block.timestamp + 3 * MONTH);
58+
assertEq(vesting.getSchedule().periods[3].portion, SCHEDULE_PERIOD_PORTION);
59+
60+
assertEq(vesting.unlockedOf(beneficiary1), BENEFICIARY1_AMOUNT * SCHEDULE_TGE_PORTION / BASIS_POINTS);
61+
assertEq(vesting.unlockedOf(beneficiary2), BENEFICIARY2_AMOUNT * SCHEDULE_TGE_PORTION / BASIS_POINTS);
62+
63+
assertEq(vesting.lockedOf(beneficiary1), BENEFICIARY1_AMOUNT * (BASIS_POINTS - SCHEDULE_TGE_PORTION) / BASIS_POINTS);
64+
assertEq(vesting.lockedOf(beneficiary2), BENEFICIARY2_AMOUNT * (BASIS_POINTS - SCHEDULE_TGE_PORTION) / BASIS_POINTS);
65+
}
66+
67+
function test_deploy_emitVestingAndBeneficiariesInitialized() external {
68+
Vesting vestingImpl = new Vesting();
69+
bytes32 salt = keccak256(abi.encodePacked(VestingType.TEAM, block.timestamp));
70+
(Beneficiary[] memory beneficiaries, Schedule memory schedule) = _getBeneficiariesAndSchedule();
71+
72+
address vestingAddress = Clones.predictDeterministicAddress(address(vestingImpl), salt);
73+
74+
vm.expectEmit(true, true, true, true);
75+
emit IERC20.Transfer(distributor, vestingAddress, VESTING_TOTAL_AMOUNT);
76+
77+
vm.prank(distributor);
78+
META.transfer(vestingAddress, VESTING_TOTAL_AMOUNT);
79+
80+
vm.expectEmit(true, true, true, true);
81+
emit IVesting.ScheduleInitialized(schedule);
82+
83+
vm.expectEmit(true, true, true, true);
84+
emit IVesting.BeneficiariesInitialized(beneficiaries);
85+
86+
vestingAddress = Clones.cloneDeterministic(address(vestingImpl), salt);
87+
Vesting(vestingAddress).initialize(META, schedule, beneficiaries);
88+
}
89+
90+
function test_deploy_revertInitializeImplementation() external {
91+
(Beneficiary[] memory beneficiaries, Schedule memory schedule) = _getBeneficiariesAndSchedule();
92+
Vesting vestingImpl = new Vesting();
93+
94+
vm.expectRevert(Initializable.InvalidInitialization.selector);
95+
96+
vestingImpl.initialize(META, schedule, beneficiaries);
97+
}
98+
99+
function test_deploy_revertRepeatInitialize() external {
100+
(Beneficiary[] memory beneficiaries, Schedule memory schedule) = _getBeneficiariesAndSchedule();
101+
102+
vm.expectRevert(Initializable.InvalidInitialization.selector);
103+
104+
vesting.initialize(META, schedule, beneficiaries);
105+
}
106+
107+
function test_deploy_revertIfInvalidStartTime(uint64 blockTimestamp, uint64 invalidBlockTimestamp) external {
108+
(Beneficiary[] memory beneficiaries, Schedule memory schedule) = _getBeneficiariesAndSchedule();
109+
110+
blockTimestamp = uint64(bound(blockTimestamp, 1, type(uint64).max));
111+
invalidBlockTimestamp = uint64(bound(invalidBlockTimestamp, 0, blockTimestamp - 1));
112+
schedule.startTime = invalidBlockTimestamp;
113+
114+
Vesting vestingImpl = new Vesting();
115+
bytes32 salt = keccak256(abi.encodePacked(VestingType.TEAM, block.timestamp));
116+
address vestingAddress = Clones.predictDeterministicAddress(address(vestingImpl), salt);
117+
118+
vm.prank(distributor);
119+
META.transfer(vestingAddress, VESTING_TOTAL_AMOUNT);
120+
121+
vestingAddress = Clones.cloneDeterministic(address(vestingImpl), salt);
122+
123+
vm.warp(blockTimestamp);
124+
vm.expectRevert(IVesting.InvalidStartTime.selector);
125+
126+
Vesting(vestingAddress).initialize(META, schedule, beneficiaries);
127+
}
128+
129+
// TODO: продолжить тесты
130+
131+
// endregion
132+
133+
// region - Service function -
134+
135+
function _deployVesting(IERC20 baseToken, Beneficiary[] memory beneficiaries, Schedule memory schedule) private returns (Vesting) {
136+
Vesting vestingImpl = new Vesting();
137+
bytes32 salt = keccak256(abi.encodePacked(VestingType.TEAM, block.timestamp));
138+
139+
address vestingAddress = Clones.predictDeterministicAddress(address(vestingImpl), salt);
140+
141+
vm.prank(distributor);
142+
baseToken.transfer(vestingAddress, VESTING_TOTAL_AMOUNT);
143+
144+
vestingAddress = Clones.cloneDeterministic(address(vestingImpl), salt);
145+
Vesting(vestingAddress).initialize(baseToken, schedule, beneficiaries);
146+
147+
return Vesting(vestingAddress);
148+
149+
}
150+
151+
function _getBeneficiariesAndSchedule() private view returns (Beneficiary[] memory beneficiaries, Schedule memory schedule) {
152+
beneficiaries = _getBeneficiaries();
153+
schedule = _getSchedule();
154+
}
155+
156+
function _getBeneficiaries() private view returns (Beneficiary[] memory beneficiaries) {
157+
beneficiaries = new Beneficiary[](2);
158+
beneficiaries[0] = Beneficiary({
159+
account: beneficiary1,
160+
amount: BENEFICIARY1_AMOUNT
161+
});
162+
beneficiaries[1] = Beneficiary({
163+
account: beneficiary2,
164+
amount: BENEFICIARY2_AMOUNT
165+
});
166+
}
167+
168+
function _getSchedule() private view returns (Schedule memory schedule) {
169+
Period[] memory periods = new Period[](SCHEDULE_TOTAL_PERIODS);
170+
// tge
171+
periods[0] = Period({
172+
endTime: block.timestamp,
173+
portion: SCHEDULE_TGE_PORTION
174+
});
175+
176+
// cliff
177+
periods[1] = Period({
178+
endTime: block.timestamp + MONTH,
179+
portion: 0
180+
});
181+
182+
periods[2] = Period({
183+
endTime: block.timestamp + 2 * MONTH,
184+
portion: SCHEDULE_PERIOD_PORTION
185+
});
186+
187+
periods[3] = Period({
188+
endTime: block.timestamp + 3 * MONTH,
189+
portion: SCHEDULE_PERIOD_PORTION
190+
});
191+
192+
schedule = Schedule({
193+
startTime: block.timestamp,
194+
periods: periods
195+
});
196+
}
197+
198+
// endregion
199+
}

0 commit comments

Comments
 (0)