-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathveOLAS.sol
805 lines (730 loc) · 35.3 KB
/
veOLAS.sol
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
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import "@openzeppelin/contracts/governance/utils/IVotes.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import "./interfaces/IErrors.sol";
/**
Votes have a weight depending on time, so that users are committed to the future of (whatever they are voting for).
Vote weight decays linearly over time. Lock time cannot be more than `MAXTIME` (4 years).
Voting escrow has time-weighted votes derived from the amount of tokens locked. The maximum voting power can be
achieved with the longest lock possible. This way the users are incentivized to lock tokens for more time.
# w ^ = amount * time_locked / MAXTIME
# 1 + /
# | /
# | /
# | /
# |/
# 0 +--------+------> time
# maxtime (4 years?)
We cannot really do block numbers per se because slope is per time, not per block, and per block could be fairly bad
because Ethereum changes its block times. What we can do is to extrapolate ***At functions.
*/
/// @title Voting Escrow OLAS - the workflow is ported from Curve Finance Vyper implementation
/// @author Aleksandr Kuperman - <[email protected]>
/// Code ported from: https://github.com/curvefi/curve-dao-contracts/blob/master/contracts/VotingEscrow.vy
/// and: https://github.com/solidlyexchange/solidly/blob/master/contracts/ve.sol
/* This VotingEscrow is based on the OLAS token that has the following specifications:
* - For the first 10 years there will be the cap of 1 billion (1e27) tokens;
* - After 10 years, the inflation rate is 2% per year.
* The maximum number of tokens for each year then can be calculated from the formula: 2^n = 1e27 * (1.02)^x,
* where n is the specified number of bits that is sufficient to store and not overflow the total supply,
* and x is the number of years. We limit n by 128, thus it would take 1340+ years to reach that total supply.
* The amount for each locker is eventually cannot overcome this number as well, and thus uint128 is sufficient.
*
* We then limit the time in seconds to last until the value of 2^64 - 1, or for the next 583+ billion years.
* The number of blocks is essentially cannot be bigger than the number of seconds, and thus it is safe to assume
* that uint64 for the number of blocks is also sufficient.
*
* We also limit the individual deposit amount to be no bigger than 2^96 - 1, or the value of total supply in 220+ years.
* This limitation is dictated by the fact that there will be at least several accounts with locked tokens, and the
* sum of all of them cannot be bigger than the total supply. Checking the limit of deposited / increased amount
* allows us to perform the unchecked operation on adding the amounts.
*
* The rest of calculations throughout the contract do not go beyond specified limitations. The contract was checked
* by echidna and the results can be found in the audit section of the repository.
*
* These specified limits allowed us to have storage-added structs to be bound by 2*256 and 1*256 bit sizes
* respectively, thus limiting the gas amount compared to using bigger variable sizes.
*
* Note that after 220 years it is no longer possible to deposit / increase the locked amount to be bigger than 2^96 - 1.
* It is going to be not safe to use this contract for governance after 1340 years.
*/
// Struct for storing balance and unlock time
// The struct size is one storage slot of uint256 (128 + 64 + padding)
struct LockedBalance {
// Token amount. It will never practically be bigger. Initial OLAS cap is 1 bn tokens, or 1e27.
// After 10 years, the inflation rate is 2% per year. It would take 1340+ years to reach 2^128 - 1
uint128 amount;
// Unlock time. It will never practically be bigger
uint64 endTime;
}
// Structure for voting escrow points
// The struct size is two storage slots of 2 * uint256 (128 + 128 + 64 + 64 + 128)
struct PointVoting {
// w(i) = at + b (bias)
int128 bias;
// dw / dt = a (slope)
int128 slope;
// Timestamp. It will never practically be bigger than 2^64 - 1
uint64 ts;
// Block number. It will not be bigger than the timestamp
uint64 blockNumber;
// Token amount. It will never practically be bigger. Initial OLAS cap is 1 bn tokens, or 1e27.
// After 10 years, the inflation rate is 2% per year. It would take 1340+ years to reach 2^128 - 1
uint128 balance;
}
/// @notice This token supports the ERC20 interface specifications except for transfers and approvals.
contract veOLAS is IErrors, IVotes, IERC20, IERC165 {
enum DepositType {
DEPOSIT_FOR_TYPE,
CREATE_LOCK_TYPE,
INCREASE_LOCK_AMOUNT,
INCREASE_UNLOCK_TIME
}
event Deposit(address indexed account, uint256 amount, uint256 locktime, DepositType depositType, uint256 ts);
event Withdraw(address indexed account, uint256 amount, uint256 ts);
event Supply(uint256 previousSupply, uint256 currentSupply);
// 1 week time
uint64 internal constant WEEK = 1 weeks;
// Maximum lock time (4 years)
uint256 internal constant MAXTIME = 4 * 365 * 86400;
// Maximum lock time (4 years) in int128
int128 internal constant IMAXTIME = 4 * 365 * 86400;
// Number of decimals
uint8 public constant decimals = 18;
// Token address
address public immutable token;
// Total token supply
uint256 public supply;
// Mapping of account address => LockedBalance
mapping(address => LockedBalance) public mapLockedBalances;
// Total number of economical checkpoints (starting from zero)
uint256 public totalNumPoints;
// Mapping of point Id => point
mapping(uint256 => PointVoting) public mapSupplyPoints;
// Mapping of account address => PointVoting[point Id]
mapping(address => PointVoting[]) public mapUserPoints;
// Mapping of time => signed slope change
mapping(uint64 => int128) public mapSlopeChanges;
// Voting token name
string public name;
// Voting token symbol
string public symbol;
/// @dev Contract constructor
/// @param _token Token address.
/// @param _name Token name.
/// @param _symbol Token symbol.
constructor(address _token, string memory _name, string memory _symbol)
{
token = _token;
name = _name;
symbol = _symbol;
// Create initial point such that default timestamp and block number are not zero
// See cast specification in the PointVoting structure
mapSupplyPoints[0] = PointVoting(0, 0, uint64(block.timestamp), uint64(block.number), 0);
}
/// @dev Gets the most recently recorded user point for `account`.
/// @param account Account address.
/// @return pv Last checkpoint.
function getLastUserPoint(address account) external view returns (PointVoting memory pv) {
uint256 lastPointNumber = mapUserPoints[account].length;
if (lastPointNumber > 0) {
pv = mapUserPoints[account][lastPointNumber - 1];
}
}
/// @dev Gets the number of user points.
/// @param account Account address.
/// @return accountNumPoints Number of user points.
function getNumUserPoints(address account) external view returns (uint256 accountNumPoints) {
accountNumPoints = mapUserPoints[account].length;
}
/// @dev Gets the checkpoint structure at number `idx` for `account`.
/// @notice The out of bound condition is treated by the default code generation check.
/// @param account User wallet address.
/// @param idx User point number.
/// @return The requested checkpoint.
function getUserPoint(address account, uint256 idx) external view returns (PointVoting memory) {
return mapUserPoints[account][idx];
}
/// @dev Record global and per-user data to checkpoint.
/// @param account Account address. User checkpoint is skipped if the address is zero.
/// @param oldLocked Previous locked amount / end lock time for the user.
/// @param newLocked New locked amount / end lock time for the user.
/// @param curSupply Current total supply (to avoid using a storage total supply variable)
function _checkpoint(
address account,
LockedBalance memory oldLocked,
LockedBalance memory newLocked,
uint128 curSupply
) internal {
PointVoting memory uOld;
PointVoting memory uNew;
int128 oldDSlope;
int128 newDSlope;
uint256 curNumPoint = totalNumPoints;
if (account != address(0)) {
// Calculate slopes and biases
// Kept at zero when they have to
if (oldLocked.endTime > block.timestamp && oldLocked.amount > 0) {
uOld.slope = int128(oldLocked.amount) / IMAXTIME;
uOld.bias = uOld.slope * int128(uint128(oldLocked.endTime - uint64(block.timestamp)));
}
if (newLocked.endTime > block.timestamp && newLocked.amount > 0) {
uNew.slope = int128(newLocked.amount) / IMAXTIME;
uNew.bias = uNew.slope * int128(uint128(newLocked.endTime - uint64(block.timestamp)));
}
// Reads values of scheduled changes in the slope
// oldLocked.endTime can be in the past and in the future
// newLocked.endTime can ONLY be in the FUTURE unless everything is expired: then zeros
oldDSlope = mapSlopeChanges[oldLocked.endTime];
if (newLocked.endTime > 0) {
if (newLocked.endTime == oldLocked.endTime) {
newDSlope = oldDSlope;
} else {
newDSlope = mapSlopeChanges[newLocked.endTime];
}
}
}
PointVoting memory lastPoint;
if (curNumPoint > 0) {
lastPoint = mapSupplyPoints[curNumPoint];
} else {
// If no point is created yet, we take the actual time and block parameters
lastPoint = PointVoting(0, 0, uint64(block.timestamp), uint64(block.number), 0);
}
uint64 lastCheckpoint = lastPoint.ts;
// initialPoint is used for extrapolation to calculate the block number and save them
// as we cannot figure that out in exact values from inside of the contract
PointVoting memory initialPoint = lastPoint;
uint256 block_slope; // dblock/dt
if (block.timestamp > lastPoint.ts) {
// This 1e18 multiplier is needed for the numerator to be bigger than the denominator
// We need to calculate this in > uint64 size (1e18 is > 2^59 multiplied by 2^64).
block_slope = (1e18 * uint256(block.number - lastPoint.blockNumber)) / uint256(block.timestamp - lastPoint.ts);
}
// If last point is already recorded in this block, slope == 0, but we know the block already in this case
// Go over weeks to fill in the history and (or) calculate what the current point is
{
// The timestamp is rounded by a week and < 2^64-1
uint64 tStep = (lastCheckpoint / WEEK) * WEEK;
for (uint256 i = 0; i < 255; ++i) {
// Hopefully it won't happen that this won't get used in 5 years!
// If it does, users will be able to withdraw but vote weight will be broken
// This is always practically < 2^64-1
unchecked {
tStep += WEEK;
}
int128 dSlope;
if (tStep > block.timestamp) {
tStep = uint64(block.timestamp);
} else {
dSlope = mapSlopeChanges[tStep];
}
lastPoint.bias -= lastPoint.slope * int128(int64(tStep - lastCheckpoint));
lastPoint.slope += dSlope;
if (lastPoint.bias < 0) {
// This could potentially happen, but fuzzer didn't find available "real" combinations
lastPoint.bias = 0;
}
if (lastPoint.slope < 0) {
// This cannot happen - just in case. Again, fuzzer didn't reach this
lastPoint.slope = 0;
}
lastCheckpoint = tStep;
lastPoint.ts = tStep;
// After division by 1e18 the uint64 size can be reclaimed
lastPoint.blockNumber = initialPoint.blockNumber + uint64((block_slope * uint256(tStep - initialPoint.ts)) / 1e18);
lastPoint.balance = initialPoint.balance;
// In order for the overflow of total number of economical checkpoints (starting from zero)
// The _checkpoint() call must happen n >(2^256 -1)/255 or n > ~1e77/255 > ~1e74 times
unchecked {
curNumPoint += 1;
}
if (tStep == block.timestamp) {
lastPoint.blockNumber = uint64(block.number);
lastPoint.balance = curSupply;
break;
} else {
mapSupplyPoints[curNumPoint] = lastPoint;
}
}
}
totalNumPoints = curNumPoint;
// Now mapSupplyPoints is filled until current time
if (account != address(0)) {
// If last point was in this block, the slope change has been already applied. In such case we have 0 slope(s)
lastPoint.slope += (uNew.slope - uOld.slope);
lastPoint.bias += (uNew.bias - uOld.bias);
if (lastPoint.slope < 0) {
lastPoint.slope = 0;
}
if (lastPoint.bias < 0) {
lastPoint.bias = 0;
}
}
// Record the last updated point
mapSupplyPoints[curNumPoint] = lastPoint;
if (account != address(0)) {
// Schedule the slope changes (slope is going down)
// We subtract new_user_slope from [newLocked.endTime]
// and add old_user_slope to [oldLocked.endTime]
if (oldLocked.endTime > block.timestamp) {
// oldDSlope was <something> - uOld.slope, so we cancel that
oldDSlope += uOld.slope;
if (newLocked.endTime == oldLocked.endTime) {
oldDSlope -= uNew.slope; // It was a new deposit, not extension
}
mapSlopeChanges[oldLocked.endTime] = oldDSlope;
}
if (newLocked.endTime > block.timestamp && newLocked.endTime > oldLocked.endTime) {
newDSlope -= uNew.slope; // old slope disappeared at this point
mapSlopeChanges[newLocked.endTime] = newDSlope;
// else: we recorded it already in oldDSlope
}
// Now handle user history
uNew.ts = uint64(block.timestamp);
uNew.blockNumber = uint64(block.number);
uNew.balance = newLocked.amount;
mapUserPoints[account].push(uNew);
}
}
/// @dev Record global data to checkpoint.
function checkpoint() external {
_checkpoint(address(0), LockedBalance(0, 0), LockedBalance(0, 0), uint128(supply));
}
/// @dev Deposits and locks tokens for a specified account.
/// @param account Target address for the locked amount.
/// @param amount Amount to deposit.
/// @param unlockTime New time when to unlock the tokens, or 0 if unchanged.
/// @param lockedBalance Previous locked amount / end time.
/// @param depositType Deposit type.
function _depositFor(
address account,
uint256 amount,
uint256 unlockTime,
LockedBalance memory lockedBalance,
DepositType depositType
) internal {
uint256 supplyBefore = supply;
uint256 supplyAfter;
// Cannot overflow because the total supply << 2^128-1
unchecked {
supplyAfter = supplyBefore + amount;
supply = supplyAfter;
}
// Get the old locked data
LockedBalance memory oldLocked;
(oldLocked.amount, oldLocked.endTime) = (lockedBalance.amount, lockedBalance.endTime);
// Adding to the existing lock, or if a lock is expired - creating a new one
// This cannot be larger than the total supply
unchecked {
lockedBalance.amount += uint128(amount);
}
if (unlockTime > 0) {
lockedBalance.endTime = uint64(unlockTime);
}
mapLockedBalances[account] = lockedBalance;
// Possibilities:
// Both oldLocked.endTime could be current or expired (>/< block.timestamp)
// amount == 0 (extend lock) or amount > 0 (add to lock or extend lock)
// lockedBalance.endTime > block.timestamp (always)
_checkpoint(account, oldLocked, lockedBalance, uint128(supplyAfter));
if (amount > 0) {
// OLAS is a solmate-based ERC20 token with optimized transferFrom() that either returns true or reverts
IERC20(token).transferFrom(msg.sender, address(this), amount);
}
emit Deposit(account, amount, lockedBalance.endTime, depositType, block.timestamp);
emit Supply(supplyBefore, supplyAfter);
}
/// @dev Deposits `amount` tokens for `account` and adds to the lock.
/// @dev Anyone (even a smart contract) can deposit for someone else, but
/// cannot extend their locktime and deposit for a brand new user.
/// @param account Account address.
/// @param amount Amount to add.
function depositFor(address account, uint256 amount) external {
LockedBalance memory lockedBalance = mapLockedBalances[account];
// Check if the amount is zero
if (amount == 0) {
revert ZeroValue();
}
// The locked balance must already exist
if (lockedBalance.amount == 0) {
revert NoValueLocked(account);
}
// Check the lock expiry
if (lockedBalance.endTime < (block.timestamp + 1)) {
revert LockExpired(msg.sender, lockedBalance.endTime, block.timestamp);
}
// Since in the _depositFor() we have the unchecked sum of amounts, this is needed to prevent unsafe behavior.
// After 10 years, the inflation rate is 2% per year. It would take 220+ years to reach 2^96 - 1 total supply
if (amount > type(uint96).max) {
revert Overflow(amount, type(uint96).max);
}
_depositFor(account, amount, 0, lockedBalance, DepositType.DEPOSIT_FOR_TYPE);
}
/// @dev Deposits `amount` tokens for `msg.sender` and locks for `unlockTime`.
/// @param amount Amount to deposit.
/// @param unlockTime Time when tokens unlock, rounded down to a whole week.
function createLock(uint256 amount, uint256 unlockTime) external {
_createLockFor(msg.sender, amount, unlockTime);
}
/// @dev Deposits `amount` tokens for `account` and locks for `unlockTime`.
/// @notice Tokens are taken from `msg.sender`'s balance.
/// @param account Account address.
/// @param amount Amount to deposit.
/// @param unlockTime Time when tokens unlock, rounded down to a whole week.
function createLockFor(address account, uint256 amount, uint256 unlockTime) external {
// Check if the account address is zero
if (account == address(0)) {
revert ZeroAddress();
}
_createLockFor(account, amount, unlockTime);
}
/// @dev Deposits `amount` tokens for `account` and locks for `unlockTime`.
/// @notice Tokens are taken from `msg.sender`'s balance.
/// @param account Account address.
/// @param amount Amount to deposit.
/// @param unlockTime Time when tokens unlock, rounded down to a whole week.
function _createLockFor(address account, uint256 amount, uint256 unlockTime) private {
// Check if the amount is zero
if (amount == 0) {
revert ZeroValue();
}
// Lock time is rounded down to weeks
// Cannot practically overflow because block.timestamp + unlockTime (max 4 years) << 2^64-1
unchecked {
unlockTime = ((block.timestamp + unlockTime) / WEEK) * WEEK;
}
LockedBalance memory lockedBalance = mapLockedBalances[account];
// The locked balance must be zero in order to start the lock
if (lockedBalance.amount > 0) {
revert LockedValueNotZero(account, uint256(lockedBalance.amount));
}
// Check for the lock time correctness
if (unlockTime < (block.timestamp + 1)) {
revert UnlockTimeIncorrect(account, block.timestamp, unlockTime);
}
// Check for the lock time not to exceed the MAXTIME
if (unlockTime > block.timestamp + MAXTIME) {
revert MaxUnlockTimeReached(account, block.timestamp + MAXTIME, unlockTime);
}
// After 10 years, the inflation rate is 2% per year. It would take 220+ years to reach 2^96 - 1 total supply
if (amount > type(uint96).max) {
revert Overflow(amount, type(uint96).max);
}
_depositFor(account, amount, unlockTime, lockedBalance, DepositType.CREATE_LOCK_TYPE);
}
/// @dev Deposits `amount` additional tokens for `msg.sender` without modifying the unlock time.
/// @param amount Amount of tokens to deposit and add to the lock.
function increaseAmount(uint256 amount) external {
LockedBalance memory lockedBalance = mapLockedBalances[msg.sender];
// Check if the amount is zero
if (amount == 0) {
revert ZeroValue();
}
// The locked balance must already exist
if (lockedBalance.amount == 0) {
revert NoValueLocked(msg.sender);
}
// Check the lock expiry
if (lockedBalance.endTime < (block.timestamp + 1)) {
revert LockExpired(msg.sender, lockedBalance.endTime, block.timestamp);
}
// Check the max possible amount to add, that must be less than the total supply
// After 10 years, the inflation rate is 2% per year. It would take 220+ years to reach 2^96 - 1 total supply
if (amount > type(uint96).max) {
revert Overflow(amount, type(uint96).max);
}
_depositFor(msg.sender, amount, 0, lockedBalance, DepositType.INCREASE_LOCK_AMOUNT);
}
/// @dev Extends the unlock time.
/// @param unlockTime New tokens unlock time.
function increaseUnlockTime(uint256 unlockTime) external {
LockedBalance memory lockedBalance = mapLockedBalances[msg.sender];
// Cannot practically overflow because block.timestamp + unlockTime (max 4 years) << 2^64-1
unchecked {
unlockTime = ((block.timestamp + unlockTime) / WEEK) * WEEK;
}
// The locked balance must already exist
if (lockedBalance.amount == 0) {
revert NoValueLocked(msg.sender);
}
// Check the lock expiry
if (lockedBalance.endTime < (block.timestamp + 1)) {
revert LockExpired(msg.sender, lockedBalance.endTime, block.timestamp);
}
// Check for the lock time correctness
if (unlockTime < (lockedBalance.endTime + 1)) {
revert UnlockTimeIncorrect(msg.sender, lockedBalance.endTime, unlockTime);
}
// Check for the lock time not to exceed the MAXTIME
if (unlockTime > block.timestamp + MAXTIME) {
revert MaxUnlockTimeReached(msg.sender, block.timestamp + MAXTIME, unlockTime);
}
_depositFor(msg.sender, 0, unlockTime, lockedBalance, DepositType.INCREASE_UNLOCK_TIME);
}
/// @dev Withdraws all tokens for `msg.sender`. Only possible if the lock has expired.
function withdraw() external {
LockedBalance memory lockedBalance = mapLockedBalances[msg.sender];
if (lockedBalance.endTime > block.timestamp) {
revert LockNotExpired(msg.sender, lockedBalance.endTime, block.timestamp);
}
uint256 amount = uint256(lockedBalance.amount);
mapLockedBalances[msg.sender] = LockedBalance(0, 0);
uint256 supplyBefore = supply;
uint256 supplyAfter;
// The amount cannot be less than the total supply
unchecked {
supplyAfter = supplyBefore - amount;
supply = supplyAfter;
}
// oldLocked can have either expired <= timestamp or zero end
// lockedBalance has only 0 end
// Both can have >= 0 amount
_checkpoint(msg.sender, lockedBalance, LockedBalance(0, 0), uint128(supplyAfter));
emit Withdraw(msg.sender, amount, block.timestamp);
emit Supply(supplyBefore, supplyAfter);
// OLAS is a solmate-based ERC20 token with optimized transfer() that either returns true or reverts
IERC20(token).transfer(msg.sender, amount);
}
/// @dev Finds a closest point that has a specified block number.
/// @param blockNumber Block to find.
/// @param account Account address for user points.
/// @return point Point with the approximate index number for the specified block.
/// @return minPointNumber Point number.
function _findPointByBlock(uint256 blockNumber, address account) internal view
returns (PointVoting memory point, uint256 minPointNumber)
{
// Get the last available point number
uint256 maxPointNumber;
if (account == address(0)) {
maxPointNumber = totalNumPoints;
} else {
maxPointNumber = mapUserPoints[account].length;
if (maxPointNumber == 0) {
return (point, minPointNumber);
}
// Already checked for > 0 in this case
unchecked {
maxPointNumber -= 1;
}
}
// Binary search that will be always enough for 128-bit numbers
for (uint256 i = 0; i < 128; ++i) {
if ((minPointNumber + 1) > maxPointNumber) {
break;
}
uint256 mid = (minPointNumber + maxPointNumber + 1) / 2;
// Choose the source of points
if (account == address(0)) {
point = mapSupplyPoints[mid];
} else {
point = mapUserPoints[account][mid];
}
if (point.blockNumber < (blockNumber + 1)) {
minPointNumber = mid;
} else {
maxPointNumber = mid - 1;
}
}
// Get the found point
if (account == address(0)) {
point = mapSupplyPoints[minPointNumber];
} else {
point = mapUserPoints[account][minPointNumber];
}
}
/// @dev Gets the voting power for an `account` at time `ts`.
/// @param account Account address.
/// @param ts Time to get voting power at.
/// @return vBalance Account voting power.
function _balanceOfLocked(address account, uint64 ts) internal view returns (uint256 vBalance) {
uint256 pointNumber = mapUserPoints[account].length;
if (pointNumber > 0) {
PointVoting memory uPoint = mapUserPoints[account][pointNumber - 1];
uPoint.bias -= uPoint.slope * int128(int64(ts) - int64(uPoint.ts));
if (uPoint.bias > 0) {
vBalance = uint256(int256(uPoint.bias));
}
}
}
/// @dev Gets the account balance in native token.
/// @param account Account address.
/// @return balance Account balance.
function balanceOf(address account) public view override returns (uint256 balance) {
balance = uint256(mapLockedBalances[account].amount);
}
/// @dev Gets the `account`'s lock end time.
/// @param account Account address.
/// @return unlockTime Lock end time.
function lockedEnd(address account) external view returns (uint256 unlockTime) {
unlockTime = uint256(mapLockedBalances[account].endTime);
}
/// @dev Gets the account balance at a specific block number.
/// @param account Account address.
/// @param blockNumber Block number.
/// @return balance Account balance.
function balanceOfAt(address account, uint256 blockNumber) external view returns (uint256 balance) {
// Find point with the closest block number to the provided one
(PointVoting memory uPoint, ) = _findPointByBlock(blockNumber, account);
// If the block number at the point index is bigger than the specified block number, the balance was zero
if (uPoint.blockNumber < (blockNumber + 1)) {
balance = uint256(uPoint.balance);
}
}
/// @dev Gets the voting power.
/// @param account Account address.
function getVotes(address account) public view override returns (uint256) {
return _balanceOfLocked(account, uint64(block.timestamp));
}
/// @dev Gets the block time adjustment for two neighboring points.
/// @notice `blockNumber` must not be lower than the contract deployment block number,
/// as the behavior and the return value is undefined.
/// @param blockNumber Block number.
/// @return point Point with the specified block number (or closest to it).
/// @return blockTime Adjusted block time of the neighboring point.
function _getBlockTime(uint256 blockNumber) internal view returns (PointVoting memory point, uint256 blockTime) {
// Check the block number to be in the past or equal to the current block
if (blockNumber > block.number) {
revert WrongBlockNumber(blockNumber, block.number);
}
// Get the minimum historical point with the provided block number
uint256 minPointNumber;
(point, minPointNumber) = _findPointByBlock(blockNumber, address(0));
uint256 dBlock;
uint256 dt;
if (minPointNumber < totalNumPoints) {
PointVoting memory pointNext = mapSupplyPoints[minPointNumber + 1];
dBlock = pointNext.blockNumber - point.blockNumber;
dt = pointNext.ts - point.ts;
} else {
dBlock = block.number - point.blockNumber;
dt = block.timestamp - point.ts;
}
blockTime = point.ts;
if (dBlock > 0) {
blockTime += (dt * (blockNumber - point.blockNumber)) / dBlock;
}
}
/// @dev Gets voting power at a specific block number.
/// @param account Account address.
/// @param blockNumber Block number.
/// @return balance Voting balance / power.
function getPastVotes(address account, uint256 blockNumber) public view override returns (uint256 balance) {
// Find the user point for the provided block number
(PointVoting memory uPoint, ) = _findPointByBlock(blockNumber, account);
// Get block time adjustment.
(, uint256 blockTime) = _getBlockTime(blockNumber);
// Calculate bias based on a block time
uPoint.bias -= uPoint.slope * int128(int64(uint64(blockTime)) - int64(uPoint.ts));
if (uPoint.bias > 0) {
balance = uint256(uint128(uPoint.bias));
}
}
/// @dev Calculate total voting power at some point in the past.
/// @param lastPoint The point (bias/slope) to start the search from.
/// @param ts Time to calculate the total voting power at.
/// @return vSupply Total voting power at that time.
function _supplyLockedAt(PointVoting memory lastPoint, uint64 ts) internal view returns (uint256 vSupply) {
// The timestamp is rounded and < 2^64-1
uint64 tStep = (lastPoint.ts / WEEK) * WEEK;
for (uint256 i = 0; i < 255; ++i) {
// This is always practically < 2^64-1
unchecked {
tStep += WEEK;
}
int128 dSlope;
if (tStep > ts) {
tStep = ts;
} else {
dSlope = mapSlopeChanges[tStep];
}
lastPoint.bias -= lastPoint.slope * int128(int64(tStep) - int64(lastPoint.ts));
if (tStep == ts) {
break;
}
lastPoint.slope += dSlope;
lastPoint.ts = tStep;
}
if (lastPoint.bias > 0) {
vSupply = uint256(uint128(lastPoint.bias));
}
}
/// @dev Gets total token supply.
/// @return Total token supply.
function totalSupply() public view override returns (uint256) {
return supply;
}
/// @dev Gets total token supply at a specific block number.
/// @param blockNumber Block number.
/// @return supplyAt Supply at the specified block number.
function totalSupplyAt(uint256 blockNumber) external view returns (uint256 supplyAt) {
// Find point with the closest block number to the provided one
(PointVoting memory sPoint, ) = _findPointByBlock(blockNumber, address(0));
// If the block number at the point index is bigger than the specified block number, the balance was zero
if (sPoint.blockNumber < (blockNumber + 1)) {
supplyAt = uint256(sPoint.balance);
}
}
/// @dev Calculates total voting power at time `ts`.
/// @param ts Time to get total voting power at.
/// @return Total voting power.
function totalSupplyLockedAtT(uint256 ts) public view returns (uint256) {
PointVoting memory lastPoint = mapSupplyPoints[totalNumPoints];
return _supplyLockedAt(lastPoint, uint64(ts));
}
/// @dev Calculates current total voting power.
/// @return Total voting power.
function totalSupplyLocked() public view returns (uint256) {
return totalSupplyLockedAtT(block.timestamp);
}
/// @dev Calculate total voting power at some point in the past.
/// @param blockNumber Block number to calculate the total voting power at.
/// @return Total voting power.
function getPastTotalSupply(uint256 blockNumber) public view override returns (uint256) {
(PointVoting memory sPoint, uint256 blockTime) = _getBlockTime(blockNumber);
// Now dt contains info on how far are we beyond the point
return _supplyLockedAt(sPoint, uint64(blockTime));
}
/// @dev Gets information about the interface support.
/// @param interfaceId A specified interface Id.
/// @return True if this contract implements the interface defined by interfaceId.
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IERC20).interfaceId || interfaceId == type(IVotes).interfaceId ||
interfaceId == type(IERC165).interfaceId;
}
/// @dev Reverts the transfer of this token.
function transfer(address to, uint256 amount) external virtual override returns (bool) {
revert NonTransferable(address(this));
}
/// @dev Reverts the approval of this token.
function approve(address spender, uint256 amount) external virtual override returns (bool) {
revert NonTransferable(address(this));
}
/// @dev Reverts the transferFrom of this token.
function transferFrom(address from, address to, uint256 amount) external virtual override returns (bool) {
revert NonTransferable(address(this));
}
/// @dev Reverts the allowance of this token.
function allowance(address owner, address spender) external view virtual override returns (uint256)
{
revert NonTransferable(address(this));
}
/// @dev Reverts delegates of this token.
function delegates(address account) external view virtual override returns (address)
{
revert NonDelegatable(address(this));
}
/// @dev Reverts delegate for this token.
function delegate(address delegatee) external virtual override
{
revert NonDelegatable(address(this));
}
/// @dev Reverts delegateBySig for this token.
function delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s)
external virtual override
{
revert NonDelegatable(address(this));
}
}