-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathVesting.sol
More file actions
244 lines (191 loc) · 8.55 KB
/
Vesting.sol
File metadata and controls
244 lines (191 loc) · 8.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import {IVesting} from "./interfaces/IVesting.sol";
import {Schedule, Period, Beneficiary} from "../utils/Common.sol";
/**
* @title Vesting contract of the base token
* @author Pavel Naydanov
* @dev Each new vesting instance is created using the Minimal Clones pattern.
* Vesting settings are set at the time of deploying a new instance in the initialize() function.
* Beneficiaries can claim the token according to the schedule by calling the claim() function
*/
contract Vesting is IVesting, Initializable {
using SafeERC20 for IERC20;
/// @notice The factor used to calculate percentages for the vesting schedule period.
/// It is set to 10_000 to handle basis points (0.01% increments)
uint256 private constant _BASIS_POINTS = 10_000; // ex: 10% = 1_000
/// @notice Structure of the vesting schedule params
Schedule private _schedule;
/// @notice Base token that will be vested
IERC20 private _baseToken;
/// @notice The initial amount of tokens in the account that was locked
mapping(address account => uint256 amount) private _initialLocked;
/// @notice The total initial amount of tokens that was locked
uint256 private _initialTotalLocked;
/// @notice The amount of released tokens by the account
mapping(address account => uint256 amount) private _released;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
/**
* @notice Initialization function for the vesting contract
* @param baseToken Address of the base token that will be vested
* @param schedule The structure that defines the vesting schedule
* @param beneficiaries List of accounts and amounts to be distributed
*/
function initialize(IERC20 baseToken, Schedule calldata schedule, Beneficiary[] calldata beneficiaries)
external
initializer
{
if (address(baseToken) == address(0)) {
revert ZeroAddress();
}
_baseToken = baseToken;
_initializeSchedule(schedule);
_initializeBeneficiaries(beneficiaries, baseToken);
}
/**
* @notice Claims the base token according to the vesting schedule
* @dev The claimed amount will be store in _released mapping
*/
function claim() external {
uint256 unlockedAmount = availableToClaim(msg.sender);
if (unlockedAmount > 0) {
_released[msg.sender] += unlockedAmount;
_baseToken.safeTransfer(msg.sender, unlockedAmount);
emit Claimed(msg.sender, unlockedAmount);
}
}
// region - Public view function -
/// @notice Retrieves the vesting schedule structure
function getSchedule() external view returns (Schedule memory) {
return _schedule;
}
/// @notice Returns the base token address
function getBaseToken() external view returns (address) {
return address(_baseToken);
}
/// @notice Returns the total amount of unlocked tokens
function totalUnlocked() external view returns (uint256) {
return _computeUnlocked(_initialTotalLocked);
}
/// @notice Returns the total amount of locked tokens
function totalLocked() external view returns (uint256) {
uint256 initialTotalLocked = _initialTotalLocked;
return initialTotalLocked - _computeUnlocked(initialTotalLocked);
}
/// @notice Returns the amount of unlocked tokens for a specific account
function unlockedOf(address account) external view returns (uint256) {
return _computeUnlocked(_initialLocked[account]);
}
/// @notice Returns the amount of locked tokens for a specific account
function lockedOf(address account) external view returns (uint256) {
uint256 initialLocked = _initialLocked[account];
return initialLocked - _computeUnlocked(initialLocked);
}
/// @notice Returns the available amount of unlocked tokens that can be claimed by a specific account
function availableToClaim(address account) public view returns (uint256) {
return _computeUnlocked(_initialLocked[account]) - _released[account];
}
// endregion
// region - Private functions
/**
* @notice Validates and sets the vesting schedule parameters
* @param schedule The structure that defines the vesting schedule
*/
function _initializeSchedule(Schedule calldata schedule) private {
if (schedule.startTime == 0) {
revert InvalidStartTime();
}
// Check that every period portion param is valid.
// Variable to keep track of the total percentage of the vesting schedule periods
uint256 totalPercentageOfPortions;
uint256 numberOfPeriods = schedule.periods.length;
for (uint256 i = 0; i < numberOfPeriods; i++) {
Period memory period = schedule.periods[i];
if (period.portion > _BASIS_POINTS) {
revert InvalidPortion();
}
totalPercentageOfPortions += period.portion;
if (i > 0) {
Period memory prevPeriod = schedule.periods[i - 1];
if (prevPeriod.endTime >= period.endTime) {
revert IncorrectPeriodTime();
}
}
}
if (totalPercentageOfPortions != _BASIS_POINTS) {
revert IncorrectTotalPeriodPortions();
}
_schedule.startTime = schedule.startTime;
for (uint256 i = 0; i < schedule.periods.length; i++) {
Period memory period = schedule.periods[i];
_schedule.periods.push(period);
}
emit ScheduleInitialized(schedule);
}
/**
* @notice Validates and sets the list of beneficiaries
* @param beneficiaries List of accounts and amounts to be distributed
* @param baseToken Address of base token for vesting
*/
function _initializeBeneficiaries(Beneficiary[] calldata beneficiaries, IERC20 baseToken) private {
if (beneficiaries.length == 0) {
revert ZeroBeneficiaries();
}
uint256 initialTotalLocked = 0;
for (uint256 i = 0; i < beneficiaries.length; i++) {
Beneficiary memory beneficiary = beneficiaries[i];
if (beneficiary.account == address(0)) {
revert ZeroAddress();
}
if (beneficiary.amount == 0) {
revert ZeroAmount();
}
if (_initialLocked[beneficiary.account] > 0) {
revert DuplicateBeneficiary(beneficiary.account);
}
initialTotalLocked += beneficiary.amount;
_initialLocked[beneficiary.account] = beneficiary.amount;
}
if (initialTotalLocked != baseToken.balanceOf(address(this))) {
revert IncorrectAmountOfBeneficiaries();
}
_initialTotalLocked = initialTotalLocked;
emit BeneficiariesInitialized(beneficiaries);
}
/**
* @notice Computes the amount of unlocked tokens based on the initial locked amount
* @param initialLockedAmount The total amount of initially locked tokens
* @return unlockedAmount The total amount of unlocked tokens
*/
function _computeUnlocked(uint256 initialLockedAmount) private view returns (uint256 unlockedAmount) {
Schedule memory schedule = _schedule;
if (block.timestamp < schedule.startTime) {
return 0;
}
uint256 startTime = schedule.startTime;
for (uint256 i = 0; i < schedule.periods.length; i++) {
Period memory period = schedule.periods[i];
uint256 endTime = period.endTime;
uint256 portion = period.portion;
if (block.timestamp < endTime) {
uint256 partPeriod = block.timestamp - startTime;
uint256 fullPeriod = endTime - startTime;
// Calculate the unlocked tokens based on the elapsed time within the period
unlockedAmount += (initialLockedAmount * partPeriod * portion) / (fullPeriod * _BASIS_POINTS);
// Exit the loop since the unlocked tokens for the current period
// have been calculated
break;
} else {
// All tokens for the current period are unlocked
unlockedAmount += (initialLockedAmount * portion) / _BASIS_POINTS;
startTime = endTime;
}
}
}
// endregion
}