|
| 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