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