Back to Contract Registry
DAO Treasury Vesting
DAO treasury vesting contract (4,356 days).
0x13a35CE0F81cd1722157d7949742c2782cb15E4F
LinearVesting Base Mainnet Verified on Basescan
Source Viewer
1 // SPDX-License-Identifier: MIT
2 pragma solidity ^0.8.20;
3
4 import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5 import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6 import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
7
8 /**
9 * @title LinearVesting
10 * @author LockItIn DAO
11 * @notice Simple linear token vesting over a fixed duration
12 * @dev Solidity 0.8.20, optimizer ON, runs = 777
13 *
14 * Used for:
15 * - Founder vesting: 40M LOCKIT over 2,777 days (beneficiary = founder wallet)
16 * - DAO vesting: 350M LOCKIT over 4,356 days (beneficiary = DAO Timelock)
17 *
18 * Features:
19 * - Linear release: tokens unlock proportionally over time
20 * - Immutable: token, duration, and minVestingAmount set at deploy
21 * - Pull pattern: beneficiary claims when ready
22 * - Beneficiary transferable (for DAO governance flexibility)
23 * - Excess token rescue (tokens sent after start)
24 * - Dust attack prevention via minVestingAmount
25 *
26 * Deployment:
27 * 1. Deploy with beneficiary, token, duration, and minVestingAmount
28 * 2. Transfer exact vesting amount to this contract
29 * 3. Anyone calls startVesting() to begin the clock (requires minVestingAmount present)
30 * 4. Beneficiary calls release() periodically to claim
31 *
32 * WARNING: Tokens sent AFTER startVesting() are NOT included in vesting.
33 * Use rescueExcessTokens() to recover them.
34 *
35 * VERSION: 1.2 (Final)
36 * CHANGES FROM v1.1:
37 * - Added minVestingAmount constructor param to prevent "dust initialization" bypass
38 * - Made startVesting() permissionless again (safe due to minVestingAmount check)
39 * - This allows DAO vesting to start at TGE without requiring governance proposal
40 * CHANGES FROM v1.0:
41 * - Fixed getVestingInfo() to correctly report _unvested vs _remaining
42 * - Added _vested field to getVestingInfo()
43 */
44 contract LinearVesting is ReentrancyGuard {
45 using SafeERC20 for IERC20;
46
47 // ═══════════════════════════════════════════════════════════════════════════
48 // ERRORS
49 // ═══════════════════════════════════════════════════════════════════════════
50
51 error InvalidAddress();
52 error InvalidDuration();
53 error InvalidMinAmount();
54 error NothingToRelease();
55 error VestingNotStarted();
56 error VestingAlreadyStarted();
57 error NotBeneficiary();
58 error NoExcessTokens();
59 error BelowMinVestingAmount();
60
61 // ═══════════════════════════════════════════════════════════════════════════
62 // EVENTS
63 // ═══════════════════════════════════════════════════════════════════════════
64
65 event VestingContractDeployed(
66 address indexed beneficiary,
67 address indexed token,
68 uint256 duration,
69 uint256 minVestingAmount
70 );
71 event VestingStarted(
72 uint256 startTime,
73 uint256 endTime,
74 uint256 totalAmount
75 );
76 event TokensReleased(
77 address indexed beneficiary,
78 uint256 amount,
79 uint256 totalReleased
80 );
81 event BeneficiaryUpdated(
82 address indexed oldBeneficiary,
83 address indexed newBeneficiary
84 );
85 event ExcessTokensRescued(
86 address indexed token,
87 uint256 amount,
88 address indexed recipient
89 );
90
91 // ═══════════════════════════════════════════════════════════════════════════
92 // IMMUTABLE STATE
93 // ═══════════════════════════════════════════════════════════════════════════
94
95 IERC20 public immutable token;
96 uint256 public immutable duration; // Vesting duration in seconds
97 uint256 public immutable minVestingAmount; // Minimum required to start vesting
98
99 // ═══════════════════════════════════════════════════════════════════════════
100 // MUTABLE STATE
101 // ═══════════════════════════════════════════════════════════════════════════
102
103 address public beneficiary;
104 uint256 public startTime; // Set when startVesting() called
105 uint256 public totalVestingAmount; // Snapshot of balance at start
106 uint256 public released; // Tokens already released
107
108 // ═══════════════════════════════════════════════════════════════════════════
109 // CONSTRUCTOR
110 // ═══════════════════════════════════════════════════════════════════════════
111
112 /**
113 * @notice Deploy vesting contract
114 * @param _beneficiary Address that receives vested tokens
115 * @param _token ERC20 token to vest
116 * @param _durationSeconds Vesting duration in SECONDS (not days!)
117 * - Founder: 2,777 days = 239,932,800 seconds
118 * - DAO: 4,356 days = 376,358,400 seconds
119 * @param _minVestingAmount Minimum tokens required to start vesting
120 * This prevents "dust initialization" attacks where someone starts
121 * vesting with 1 wei, then the real allocation arrives and becomes
122 * "excess" that can be immediately withdrawn.
123 * Set to exact expected allocation (e.g., 40_000_000e18 for founder)
124 */
125 constructor(
126 address _beneficiary,
127 address _token,
128 uint256 _durationSeconds,
129 uint256 _minVestingAmount
130 ) {
131 if (_beneficiary == address(0)) revert InvalidAddress();
132 if (_token == address(0)) revert InvalidAddress();
133 if (_durationSeconds == 0) revert InvalidDuration();
134 if (_minVestingAmount == 0) revert InvalidMinAmount();
135
136 beneficiary = _beneficiary;
137 token = IERC20(_token);
138 duration = _durationSeconds;
139 minVestingAmount = _minVestingAmount;
140
141 emit VestingContractDeployed(_beneficiary, _token, _durationSeconds, _minVestingAmount);
142 }
143
144 // ═══════════════════════════════════════════════════════════════════════════
145 // VESTING INITIALIZATION
146 // ═══════════════════════════════════════════════════════════════════════════
147
148 /**
149 * @notice Start vesting with current token balance
150 * @dev Can only be called once. Tokens must be transferred before calling.
151 * ANYONE can call - this allows DAO vesting to start at TGE without
152 * requiring a governance proposal.
153 *
154 * Security: The minVestingAmount check prevents "dust initialization"
155 * attacks. Even if an attacker sends 1 wei and tries to start, it reverts.
156 * Vesting can only start when the real allocation is present.
157 *
158 * The contract snapshots whatever balance is present (must be >= min).
159 * Any tokens sent AFTER this call are "excess" and can be rescued.
160 */
161 function startVesting() external {
162 if (startTime != 0) revert VestingAlreadyStarted();
163
164 uint256 balance = token.balanceOf(address(this));
165 if (balance < minVestingAmount) revert BelowMinVestingAmount();
166
167 startTime = block.timestamp;
168 totalVestingAmount = balance;
169
170 emit VestingStarted(startTime, startTime + duration, balance);
171 }
172
173 // ═══════════════════════════════════════════════════════════════════════════
174 // RELEASE
175 // ═══════════════════════════════════════════════════════════════════════════
176
177 /**
178 * @notice Release vested tokens to beneficiary
179 * @dev Anyone can call, but tokens always go to beneficiary
180 */
181 function release() external nonReentrant {
182 if (startTime == 0) revert VestingNotStarted();
183
184 uint256 releasable = _releasableAmount();
185 if (releasable == 0) revert NothingToRelease();
186
187 released += releasable;
188 token.safeTransfer(beneficiary, releasable);
189
190 emit TokensReleased(beneficiary, releasable, released);
191 }
192
193 // ═══════════════════════════════════════════════════════════════════════════
194 // BENEFICIARY MANAGEMENT
195 // ═══════════════════════════════════════════════════════════════════════════
196
197 /**
198 * @notice Transfer beneficiary rights to new address
199 * @dev Only current beneficiary can call. Single-step transfer.
200 * For DAO vesting (beneficiary = Timelock), changing this requires
201 * a full governance proposal → vote → timelock delay, which provides
202 * sufficient protection against mistakes.
203 * @param _newBeneficiary New address to receive vested tokens
204 */
205 function transferBeneficiary(address _newBeneficiary) external {
206 if (msg.sender != beneficiary) revert NotBeneficiary();
207 if (_newBeneficiary == address(0)) revert InvalidAddress();
208
209 emit BeneficiaryUpdated(beneficiary, _newBeneficiary);
210 beneficiary = _newBeneficiary;
211 }
212
213 // ═══════════════════════════════════════════════════════════════════════════
214 // RESCUE FUNCTIONS
215 // ═══════════════════════════════════════════════════════════════════════════
216
217 /**
218 * @notice Rescue excess vesting tokens sent after vesting started
219 * @dev Only beneficiary can call. Only rescues tokens above vesting obligation.
220 * This handles the case where someone accidentally sends more tokens
221 * after startVesting() was called.
222 */
223 function rescueExcessTokens() external nonReentrant {
224 if (msg.sender != beneficiary) revert NotBeneficiary();
225 if (startTime == 0) revert VestingNotStarted();
226
227 uint256 currentBalance = token.balanceOf(address(this));
228 uint256 remaining = totalVestingAmount - released;
229
230 if (currentBalance <= remaining) revert NoExcessTokens();
231
232 uint256 excess = currentBalance - remaining;
233 token.safeTransfer(beneficiary, excess);
234
235 emit ExcessTokensRescued(address(token), excess, beneficiary);
236 }
237
238 /**
239 * @notice Rescue other tokens accidentally sent to this contract
240 * @dev Only beneficiary can call. Cannot rescue the vesting token.
241 * @param _token Token to rescue
242 */
243 function rescueOtherTokens(address _token) external nonReentrant {
244 if (msg.sender != beneficiary) revert NotBeneficiary();
245 if (_token == address(token)) revert InvalidAddress(); // Use rescueExcessTokens
246
247 IERC20 otherToken = IERC20(_token);
248 uint256 balance = otherToken.balanceOf(address(this));
249 if (balance == 0) revert NoExcessTokens();
250
251 otherToken.safeTransfer(beneficiary, balance);
252
253 emit ExcessTokensRescued(_token, balance, beneficiary);
254 }
255
256 // ═══════════════════════════════════════════════════════════════════════════
257 // VIEW FUNCTIONS
258 // ═══════════════════════════════════════════════════════════════════════════
259
260 /**
261 * @notice Calculate currently releasable amount
262 */
263 function releasableAmount() external view returns (uint256) {
264 if (startTime == 0) return 0;
265 return _releasableAmount();
266 }
267
268 /**
269 * @notice Calculate total vested amount (released + releasable)
270 */
271 function vestedAmount() external view returns (uint256) {
272 if (startTime == 0) return 0;
273 return _vestedAmount();
274 }
275
276 /**
277 * @notice Get complete vesting schedule info
278 * @return _beneficiary Address receiving vested tokens
279 * @return _token Token being vested
280 * @return _startTime When vesting started (0 if not started)
281 * @return _endTime When vesting completes (0 if not started)
282 * @return _duration Vesting duration in seconds
283 * @return _totalAmount Total tokens being vested
284 * @return _released Tokens already claimed
285 * @return _releasable Tokens available to claim now
286 * @return _vested Total tokens that have vested (time-based)
287 * @return _unvested Tokens that have NOT yet vested
288 * @return _remaining Total unclaimed tokens (vested + unvested)
289 */
290 function getVestingInfo() external view returns (
291 address _beneficiary,
292 address _token,
293 uint256 _startTime,
294 uint256 _endTime,
295 uint256 _duration,
296 uint256 _totalAmount,
297 uint256 _released,
298 uint256 _releasable,
299 uint256 _vested,
300 uint256 _unvested,
301 uint256 _remaining
302 ) {
303 _beneficiary = beneficiary;
304 _token = address(token);
305 _startTime = startTime;
306 _endTime = startTime == 0 ? 0 : startTime + duration;
307 _duration = duration;
308 _totalAmount = totalVestingAmount;
309 _released = released;
310
311 if (startTime == 0) {
312 _releasable = 0;
313 _vested = 0;
314 _unvested = 0;
315 _remaining = 0;
316 } else {
317 uint256 vested = _vestedAmount();
318 _releasable = vested - released;
319 _vested = vested;
320 _unvested = totalVestingAmount - vested;
321 _remaining = totalVestingAmount - released;
322 }
323 }
324
325 /**
326 * @notice Get the minimum vesting amount required to start
327 * @dev Useful for UIs to show funding requirements
328 */
329 function getMinVestingAmount() external view returns (uint256) {
330 return minVestingAmount;
331 }
332
333 /**
334 * @notice Check if vesting has started
335 */
336 function isStarted() external view returns (bool) {
337 return startTime != 0;
338 }
339
340 /**
341 * @notice Check if contract is ready to start (has minimum funding)
342 */
343 function isReadyToStart() external view returns (bool) {
344 if (startTime != 0) return false; // Already started
345 return token.balanceOf(address(this)) >= minVestingAmount;
346 }
347
348 /**
349 * @notice Check if vesting is fully complete
350 */
351 function isComplete() external view returns (bool) {
352 if (startTime == 0) return false;
353 return block.timestamp >= startTime + duration;
354 }
355
356 /**
357 * @notice Get time remaining until fully vested
358 * @return Seconds remaining (0 if complete or not started)
359 */
360 function timeRemaining() external view returns (uint256) {
361 if (startTime == 0) return 0;
362 uint256 endTime = startTime + duration;
363 if (block.timestamp >= endTime) return 0;
364 return endTime - block.timestamp;
365 }
366
367 /**
368 * @notice Get vesting progress as basis points (0-10000)
369 * @return Progress in bps (10000 = 100%)
370 */
371 function progressBps() external view returns (uint256) {
372 if (startTime == 0) return 0;
373 if (block.timestamp >= startTime + duration) return 10000;
374 uint256 elapsed = block.timestamp - startTime;
375 return (elapsed * 10000) / duration;
376 }
377
378 // ═══════════════════════════════════════════════════════════════════════════
379 // INTERNAL
380 // ═══════════════════════════════════════════════════════════════════════════
381
382 function _vestedAmount() internal view returns (uint256) {
383 if (block.timestamp >= startTime + duration) {
384 return totalVestingAmount;
385 }
386 return (totalVestingAmount * (block.timestamp - startTime)) / duration;
387 }
388
389 function _releasableAmount() internal view returns (uint256) {
390 return _vestedAmount() - released;
391 }
392 }