forked from xmas7/nft-art-marketplace
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathMETH.sol
773 lines (700 loc) · 30.3 KB
/
METH.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
/*
MUSEE Protocol
*/
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.0;
import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
import "./libraries/LockedBalance.sol";
error METH_Cannot_Deposit_For_Lockup_With_Address_Zero();
error METH_Cannot_Deposit_To_Address_Zero();
error METH_Cannot_Deposit_To_METH();
error METH_Cannot_Withdraw_To_Address_Zero();
error METH_Cannot_Withdraw_To_METH();
error METH_Cannot_Withdraw_To_Market();
error METH_Escrow_Expired();
error METH_Escrow_Not_Found();
error METH_Expiration_Too_Far_In_Future();
/// @param amount The current allowed amount the spender is authorized to transact for this account.
error METH_Insufficient_Allowance(uint256 amount);
/// @param amount The current available (unlocked) token count of this account.
error METH_Insufficient_Available_Funds(uint256 amount);
/// @param amount The current number of tokens this account has for the given lockup expiry bucket.
error METH_Insufficient_Escrow(uint256 amount);
error METH_Invalid_Lockup_Duration();
error METH_Market_Must_Be_A_Contract();
error METH_Must_Deposit_Non_Zero_Amount();
error METH_Must_Lockup_Non_Zero_Amount();
error METH_No_Funds_To_Withdraw();
error METH_Only_MUSEE_Market_Allowed();
error METH_Too_Much_ETH_Provided();
error METH_Transfer_To_Address_Zero_Not_Allowed();
error METH_Transfer_To_METH_Not_Allowed();
/**
* @title An ERC-20 token which wraps ETH, potentially with a 1 day lockup period.
* @notice METH is an [ERC-20 token](https://eips.ethereum.org/EIPS/eip-20) modeled after
* [WETH9](https://etherscan.io/address/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2#code).
* It has the added ability to lockup tokens for 24-25 hours - during this time they may not be
* transferred or withdrawn, except by our market contract which requested the lockup in the first place.
* @dev Locked balances are rounded up to the next hour.
* They are grouped by the expiration time of the lockup into what we refer to as a lockup "bucket".
* At any time there may be up to 25 buckets but never more than that which prevents loops from exhausting gas limits.
* METH is an upgradeable contract. Overtime we will progressively decentralize, potentially giving upgrade permissions
* to a DOA ownership or removing the permissions entirely.
*/
contract METH {
using AddressUpgradeable for address payable;
using LockedBalance for LockedBalance.Lockups;
using Math for uint256;
/// @notice Tracks an account's info.
struct AccountInfo {
/// @notice The number of tokens which have been unlocked already.
uint96 freedBalance;
/// @notice The first applicable lockup bucket for this account.
uint32 lockupStartIndex;
/// @notice Stores up to 25 buckets of locked balance for a user, one per hour.
LockedBalance.Lockups lockups;
/// @notice Returns the amount which a spender is still allowed to withdraw from this account.
mapping(address => uint256) allowance;
}
/// @notice Stores per-account details.
mapping(address => AccountInfo) private accountToInfo;
// Lockup configuration
/// @notice The minimum lockup period in seconds.
uint256 private immutable lockupDuration;
/// @notice The interval to which lockup expiries are rounded, limiting the max number of outstanding lockup buckets.
uint256 private immutable lockupInterval;
/// @notice The Musee market contract with permissions to manage lockups.
address payable private immutable museeMarket;
// ERC-20 metadata fields
/**
* @notice The number of decimals the token uses.
* @dev This method can be used to improve usability when displaying token amounts, but all interactions
* with this contract use whole amounts not considering decimals.
* @return 18
*/
uint8 public constant decimals = 18;
/**
* @notice The name of the token.
* @return Musee ETH
*/
string public constant name = "Musee ETH";
/**
* @notice The symbol of the token.
* @return METH
*/
string public constant symbol = "METH";
// ERC-20 events
/**
* @notice Emitted when the allowance for a spender account is updated.
* @param from The account the spender is authorized to transact for.
* @param spender The account with permissions to manage METH tokens for the `from` account.
* @param amount The max amount of tokens which can be spent by the `spender` account.
*/
event Approval(address indexed from, address indexed spender, uint256 amount);
/**
* @notice Emitted when a transfer of METH tokens is made from one account to another.
* @param from The account which is sending METH tokens.
* @param to The account which is receiving METH tokens.
* @param amount The number of METH tokens which were sent.
*/
event Transfer(address indexed from, address indexed to, uint256 amount);
// Custom events
/**
* @notice Emitted when METH tokens are locked up by the Musee market for 24-25 hours
* and may include newly deposited ETH which is added to the account's total METH balance.
* @param account The account which has access to the METH after the `expiration`.
* @param expiration The time at which the `from` account will have access to the locked METH.
* @param amount The number of METH tokens which where locked up.
* @param valueDeposited The amount of ETH added to their account's total METH balance,
* this may be lower than `amount` if available METH was leveraged.
*/
event BalanceLocked(address indexed account, uint256 indexed expiration, uint256 amount, uint256 valueDeposited);
/**
* @notice Emitted when METH tokens are unlocked by the Musee market.
* @dev This event will not be emitted when lockups expire,
* it's only for tokens which are unlocked before their expiry.
* @param account The account which had locked METH freed before expiration.
* @param expiration The time this balance was originally scheduled to be unlocked.
* @param amount The number of METH tokens which were unlocked.
*/
event BalanceUnlocked(address indexed account, uint256 indexed expiration, uint256 amount);
/**
* @notice Emitted when ETH is withdrawn from a user's account.
* @dev This may be triggered by the user, an approved operator, or the Musee market.
* @param from The account from which METH was deducted in order to send the ETH.
* @param to The address the ETH was sent to.
* @param amount The number of tokens which were deducted from the user's METH balance and transferred as ETH.
*/
event ETHWithdrawn(address indexed from, address indexed to, uint256 amount);
/// @dev Allows the Musee market permission to manage lockups for a user.
modifier onlyMuseeMarket() {
if (msg.sender != museeMarket) {
revert METH_Only_MUSEE_Market_Allowed();
}
_;
}
/**
* @notice Set immutable variables for the implementation contract.
* @dev Using immutable instead of constants allows us to use different values on testnet.
* @param _museeMarket The address of the Musee NFT marketplace.
* @param _lockupDuration The minimum length of time to lockup tokens for when `BalanceLocked`, in seconds.
*/
constructor(address payable _museeMarket, uint256 _lockupDuration) {
// if (!_museeMarket.isContract()) {
// revert METH_Market_Must_Be_A_Contract();
// }
museeMarket = _museeMarket;
lockupDuration = _lockupDuration;
lockupInterval = _lockupDuration / 24;
if (lockupInterval * 24 != _lockupDuration || _lockupDuration == 0) {
revert METH_Invalid_Lockup_Duration();
}
}
/**
* @notice Transferring ETH (via `msg.value`) to the contract performs a `deposit` into the user's account.
*/
receive() external payable {
depositFor(msg.sender);
}
/**
* @notice Approves a `spender` as an operator with permissions to transfer from your account.
* @dev To prevent attack vectors, clients SHOULD make sure to create user interfaces in such a way
* that they set the allowance first to 0 before setting it to another value for the same spender.
* We will add support for `increaseAllowance` in the future.
* @param spender The address of the operator account that has approval to spend funds
* from the `msg.sender`'s account.
* @param amount The max number of METH tokens from `msg.sender`'s account that this spender is
* allowed to transact with.
* @return success Always true.
*/
function approve(address spender, uint256 amount) external returns (bool success) {
accountToInfo[msg.sender].allowance[spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
/**
* @notice Deposit ETH (via `msg.value`) and receive the equivalent amount in METH tokens.
* These tokens are not subject to any lockup period.
*/
function deposit() external payable {
depositFor(msg.sender);
}
/**
* @notice Deposit ETH (via `msg.value`) and credit the `account` provided with the equivalent amount in METH tokens.
* These tokens are not subject to any lockup period.
* @dev This may be used by the Musee market to credit a user's account with METH tokens.
* @param account The account to credit with METH tokens.
*/
function depositFor(address account) public payable {
if (msg.value == 0) {
revert METH_Must_Deposit_Non_Zero_Amount();
} else if (account == address(0)) {
revert METH_Cannot_Deposit_To_Address_Zero();
} else if (account == address(this)) {
revert METH_Cannot_Deposit_To_METH();
}
AccountInfo storage accountInfo = accountToInfo[account];
// ETH value cannot realistically overflow 96 bits.
unchecked {
accountInfo.freedBalance += uint96(msg.value);
}
emit Transfer(address(0), account, msg.value);
}
/**
* @notice Used by the market contract only:
* Remove an account's lockup and then create a new lockup, potentially for a different account.
* @dev Used by the market when an offer for an NFT is increased.
* This may be for a single account (increasing their offer)
* or two different accounts (outbidding someone elses offer).
* @param unlockFrom The account whose lockup is to be removed.
* @param unlockExpiration The original lockup expiration for the tokens to be unlocked.
* This will revert if the lockup has already expired.
* @param unlockAmount The number of tokens to be unlocked from `unlockFrom`'s account.
* This will revert if the tokens were previously unlocked.
* @param lockupFor The account to which the funds are to be deposited for (via the `msg.value`) and tokens locked up.
* @param lockupAmount The number of tokens to be locked up for the `lockupFor`'s account.
* `msg.value` must be <= `lockupAmount` and any delta will be taken from the account's available METH balance.
* @return expiration The expiration timestamp for the METH tokens that were locked.
*/
function marketChangeLockup(
address unlockFrom,
uint256 unlockExpiration,
uint256 unlockAmount,
address lockupFor,
uint256 lockupAmount
) external payable onlyMuseeMarket returns (uint256 expiration) {
_marketUnlockFor(unlockFrom, unlockExpiration, unlockAmount);
return _marketLockupFor(lockupFor, lockupAmount);
}
/**
* @notice Used by the market contract only:
* Lockup an account's METH tokens for 24-25 hours.
* @dev Used by the market when a new offer for an NFT is made.
* @param account The account to which the funds are to be deposited for (via the `msg.value`) and tokens locked up.
* @param amount The number of tokens to be locked up for the `lockupFor`'s account.
* `msg.value` must be <= `amount` and any delta will be taken from the account's available METH balance.
* @return expiration The expiration timestamp for the METH tokens that were locked.
*/
function marketLockupFor(address account, uint256 amount)
external
payable
onlyMuseeMarket
returns (uint256 expiration)
{
return _marketLockupFor(account, amount);
}
/**
* @notice Used by the market contract only:
* Remove an account's lockup, making the METH tokens available for transfer or withdrawal.
* @dev Used by the market when an offer is invalidated, which occurs when an auction for the same NFT
* receives its first bid or the buyer purchased the NFT another way, such as with `buy`.
* @param account The account whose lockup is to be unlocked.
* @param expiration The original lockup expiration for the tokens to be unlocked unlocked.
* This will revert if the lockup has already expired.
* @param amount The number of tokens to be unlocked from `account`.
* This will revert if the tokens were previously unlocked.
*/
function marketUnlockFor(
address account,
uint256 expiration,
uint256 amount
) external onlyMuseeMarket {
_marketUnlockFor(account, expiration, amount);
}
/**
* @notice Used by the market contract only:
* Removes tokens from the user's available balance and returns ETH to the caller.
* @dev Used by the market when a user's available METH balance is used to make a purchase
* including accepting a buy price or a private sale, or placing a bid in an auction.
* @param from The account whose available balance is to be withdrawn from.
* @param amount The number of tokens to be deducted from `unlockFrom`'s available balance and transferred as ETH.
* This will revert if the tokens were previously unlocked.
*/
function marketWithdrawFrom(address from, uint256 amount) external onlyMuseeMarket {
AccountInfo storage accountInfo = _freeFromEscrow(from);
_deductBalanceFrom(accountInfo, amount);
// With the external call after state changes, we do not need a nonReentrant guard
payable(msg.sender).sendValue(amount);
emit ETHWithdrawn(from, msg.sender, amount);
}
/**
* @notice Used by the market contract only:
* Removes a lockup from the user's account and then returns ETH to the caller.
* @dev Used by the market to extract unexpired funds as ETH to distribute for
* a sale when the user's offer is accepted.
* @param account The account whose lockup is to be removed.
* @param expiration The original lockup expiration for the tokens to be unlocked.
* This will revert if the lockup has already expired.
* @param amount The number of tokens to be unlocked and withdrawn as ETH.
*/
function marketWithdrawLocked(
address account,
uint256 expiration,
uint256 amount
) external onlyMuseeMarket {
_removeFromLockedBalance(account, expiration, amount);
// With the external call after state changes, we do not need a nonReentrant guard
payable(msg.sender).sendValue(amount);
emit ETHWithdrawn(account, msg.sender, amount);
}
/**
* @notice Transfers an amount from your account.
* @param to The address of the account which the tokens are transferred from.
* @param amount The number of METH tokens to be transferred.
* @return success Always true (reverts if insufficient funds).
*/
function transfer(address to, uint256 amount) external returns (bool success) {
return transferFrom(msg.sender, to, amount);
}
/**
* @notice Transfers an amount from the account specified if the `msg.sender` has approval.
* @param from The address from which the available tokens are transferred from.
* @param to The address to which the tokens are to be transferred.
* @param amount The number of METH tokens to be transferred.
* @return success Always true (reverts if insufficient funds or not approved).
*/
function transferFrom(
address from,
address to,
uint256 amount
) public returns (bool success) {
if (to == address(0)) {
revert METH_Transfer_To_Address_Zero_Not_Allowed();
} else if (to == address(this)) {
revert METH_Transfer_To_METH_Not_Allowed();
}
AccountInfo storage fromAccountInfo = _freeFromEscrow(from);
if (from != msg.sender) {
_deductAllowanceFrom(fromAccountInfo, amount, from);
}
_deductBalanceFrom(fromAccountInfo, amount);
AccountInfo storage toAccountInfo = accountToInfo[to];
// Total ETH cannot realistically overflow 96 bits.
unchecked {
toAccountInfo.freedBalance += uint96(amount);
}
emit Transfer(from, to, amount);
return true;
}
/**
* @notice Withdraw all tokens available in your account and receive ETH.
*/
function withdrawAvailableBalance() external {
AccountInfo storage accountInfo = _freeFromEscrow(msg.sender);
uint256 amount = accountInfo.freedBalance;
if (amount == 0) {
revert METH_No_Funds_To_Withdraw();
}
delete accountInfo.freedBalance;
// With the external call after state changes, we do not need a nonReentrant guard
payable(msg.sender).sendValue(amount);
emit ETHWithdrawn(msg.sender, msg.sender, amount);
}
/**
* @notice Withdraw the specified number of tokens from the `from` accounts available balance
* and send ETH to the destination address, if the `msg.sender` has approval.
* @param from The address from which the available funds are to be withdrawn.
* @param to The destination address for the ETH to be transferred to.
* @param amount The number of tokens to be withdrawn and transferred as ETH.
*/
function withdrawFrom(
address from,
address payable to,
uint256 amount
) external {
if (amount == 0) {
revert METH_No_Funds_To_Withdraw();
} else if (to == address(0)) {
revert METH_Cannot_Withdraw_To_Address_Zero();
} else if (to == address(this)) {
revert METH_Cannot_Withdraw_To_METH();
} else if (to == address(museeMarket)) {
revert METH_Cannot_Withdraw_To_Market();
}
AccountInfo storage accountInfo = _freeFromEscrow(from);
if (from != msg.sender) {
_deductAllowanceFrom(accountInfo, amount, from);
}
_deductBalanceFrom(accountInfo, amount);
// With the external call after state changes, we do not need a nonReentrant guard
to.sendValue(amount);
emit ETHWithdrawn(from, to, amount);
}
/**
* @dev Require msg.sender has been approved and deducts the amount from the available allowance.
*/
function _deductAllowanceFrom(
AccountInfo storage accountInfo,
uint256 amount,
address from
) private {
uint256 spenderAllowance = accountInfo.allowance[msg.sender];
if (spenderAllowance != type(uint256).max) {
if (spenderAllowance < amount) {
revert METH_Insufficient_Allowance(spenderAllowance);
}
// The check above ensures allowance cannot underflow.
unchecked {
spenderAllowance -= amount;
}
accountInfo.allowance[msg.sender] = spenderAllowance;
emit Approval(from, msg.sender, spenderAllowance);
}
}
/**
* @dev Removes an amount from the account's available METH balance.
*/
function _deductBalanceFrom(AccountInfo storage accountInfo, uint256 amount) private {
uint96 freedBalance = accountInfo.freedBalance;
// Free from escrow in order to consider any expired escrow balance
if (freedBalance < amount) {
revert METH_Insufficient_Available_Funds(freedBalance);
}
// The check above ensures balance cannot underflow.
unchecked {
accountInfo.freedBalance = freedBalance - uint96(amount);
}
}
/**
* @dev Moves expired escrow to the available balance.
* Sets the next bucket that hasn't expired as the new start index.
*/
function _freeFromEscrow(address account) private returns (AccountInfo storage) {
AccountInfo storage accountInfo = accountToInfo[account];
uint256 escrowIndex = accountInfo.lockupStartIndex;
LockedBalance.Lockup memory escrow = accountInfo.lockups.get(escrowIndex);
// If the first bucket (the oldest) is empty or not yet expired, no change to escrowStartIndex is required
if (escrow.expiration == 0 || escrow.expiration >= block.timestamp) {
return accountInfo;
}
while (true) {
// Total ETH cannot realistically overflow 96 bits.
unchecked {
accountInfo.freedBalance += escrow.totalAmount;
accountInfo.lockups.del(escrowIndex);
// Escrow index cannot overflow 32 bits.
escrow = accountInfo.lockups.get(escrowIndex + 1);
}
// If the next bucket is empty, the start index is set to the previous bucket
if (escrow.expiration == 0) {
break;
}
// Escrow index cannot overflow 32 bits.
unchecked {
// Increment the escrow start index if the next bucket is not empty
++escrowIndex;
}
// If the next bucket is expired, that's the new start index
if (escrow.expiration >= block.timestamp) {
break;
}
}
// Escrow index cannot overflow 32 bits.
unchecked {
accountInfo.lockupStartIndex = uint32(escrowIndex);
}
return accountInfo;
}
/**
* @notice Lockup an account's METH tokens for 24-25 hours.
*/
/* solhint-disable-next-line code-complexity */
function _marketLockupFor(address account, uint256 amount) private returns (uint256 expiration) {
if (account == address(0)) {
revert METH_Cannot_Deposit_For_Lockup_With_Address_Zero();
}
if (amount == 0) {
revert METH_Must_Lockup_Non_Zero_Amount();
}
// Block timestamp in seconds is small enough to never overflow
unchecked {
// Lockup expires after 24 hours, rounded up to the next hour for a total of [24-25) hours
expiration = lockupDuration + block.timestamp.ceilDiv(lockupInterval) * lockupInterval;
}
// Update available escrow
// Always free from escrow to ensure the max bucket count is <= 25
AccountInfo storage accountInfo = _freeFromEscrow(account);
if (msg.value < amount) {
unchecked {
// The if check above prevents an underflow here
_deductBalanceFrom(accountInfo, amount - msg.value);
}
} else if (msg.value != amount) {
// There's no reason to send msg.value more than the amount being locked up
revert METH_Too_Much_ETH_Provided();
}
// Add to locked escrow
unchecked {
// The number of buckets is always < 256 bits.
for (uint256 escrowIndex = accountInfo.lockupStartIndex; ; ++escrowIndex) {
LockedBalance.Lockup memory escrow = accountInfo.lockups.get(escrowIndex);
if (escrow.expiration == 0) {
if (expiration > type(uint32).max) {
revert METH_Expiration_Too_Far_In_Future();
}
// Amount (ETH) will always be < 96 bits.
accountInfo.lockups.set(escrowIndex, expiration, amount);
break;
}
if (escrow.expiration == expiration) {
// Total ETH will always be < 96 bits.
accountInfo.lockups.setTotalAmount(escrowIndex, escrow.totalAmount + amount);
break;
}
}
}
emit BalanceLocked(account, expiration, amount, msg.value);
}
/**
* @notice Remove an account's lockup, making the METH tokens available for transfer or withdrawal.
*/
function _marketUnlockFor(
address account,
uint256 expiration,
uint256 amount
) private {
AccountInfo storage accountInfo = _removeFromLockedBalance(account, expiration, amount);
// Total ETH cannot realistically overflow 96 bits.
unchecked {
accountInfo.freedBalance += uint96(amount);
}
}
/**
* @dev Removes the specified amount from locked escrow, potentially before its expiration.
*/
/* solhint-disable-next-line code-complexity */
function _removeFromLockedBalance(
address account,
uint256 expiration,
uint256 amount
) private returns (AccountInfo storage) {
if (expiration < block.timestamp) {
revert METH_Escrow_Expired();
}
AccountInfo storage accountInfo = accountToInfo[account];
uint256 escrowIndex = accountInfo.lockupStartIndex;
LockedBalance.Lockup memory escrow = accountInfo.lockups.get(escrowIndex);
if (escrow.expiration == expiration) {
// If removing from the first bucket, we may be able to delete it
if (escrow.totalAmount == amount) {
accountInfo.lockups.del(escrowIndex);
// Bump the escrow start index unless it's the last one
unchecked {
if (accountInfo.lockups.get(escrowIndex + 1).expiration != 0) {
// The number of escrow buckets will never overflow 32 bits.
++accountInfo.lockupStartIndex;
}
}
} else {
if (escrow.totalAmount < amount) {
revert METH_Insufficient_Escrow(escrow.totalAmount);
}
// The require above ensures balance will not underflow.
unchecked {
accountInfo.lockups.setTotalAmount(escrowIndex, escrow.totalAmount - amount);
}
}
} else {
// Removing from the 2nd+ bucket
while (true) {
// The number of escrow buckets will never overflow 32 bits.
unchecked {
++escrowIndex;
}
escrow = accountInfo.lockups.get(escrowIndex);
if (escrow.expiration == expiration) {
if (amount > escrow.totalAmount) {
revert METH_Insufficient_Escrow(escrow.totalAmount);
}
// The require above ensures balance will not underflow.
unchecked {
accountInfo.lockups.setTotalAmount(escrowIndex, escrow.totalAmount - amount);
}
// We may have an entry with 0 totalAmount but expiration will be set
break;
}
if (escrow.expiration == 0) {
revert METH_Escrow_Not_Found();
}
}
}
emit BalanceUnlocked(account, expiration, amount);
return accountInfo;
}
/**
* @notice Returns the amount which a spender is still allowed to transact from the `account`'s balance.
* @param account The owner of the funds.
* @param operator The address with approval to spend from the `account`'s balance.
* @return amount The number of tokens the `operator` is still allowed to transact with.
*/
function allowance(address account, address operator) external view returns (uint256 amount) {
AccountInfo storage accountInfo = accountToInfo[account];
amount = accountInfo.allowance[operator];
}
/**
* @notice Returns the balance of an account which is available to transfer or withdraw.
* @dev This will automatically increase as soon as locked tokens reach their expiry date.
* @param account The account to query the available balance of.
* @return balance The available balance of the account.
*/
function balanceOf(address account) external view returns (uint256 balance) {
AccountInfo storage accountInfo = accountToInfo[account];
balance = accountInfo.freedBalance;
// Total ETH cannot realistically overflow 96 bits and escrowIndex will always be < 256 bits.
unchecked {
// Add expired lockups
for (uint256 escrowIndex = accountInfo.lockupStartIndex; ; ++escrowIndex) {
LockedBalance.Lockup memory escrow = accountInfo.lockups.get(escrowIndex);
if (escrow.expiration == 0 || escrow.expiration >= block.timestamp) {
break;
}
balance += escrow.totalAmount;
}
}
}
/**
* @notice Gets the Musee market address which has permissions to manage lockups.
* @return market The Musee market contract address.
*/
function getMuseeMarket() external view returns (address market) {
market = museeMarket;
}
/**
* @notice Returns the balance and each outstanding (unexpired) lockup bucket for an account, grouped by expiry.
* @dev `expires.length` == `amounts.length`
* and `amounts[i]` is the number of tokens which will expire at `expires[i]`.
* The results returned are sorted by expiry, with the earliest expiry date first.
* @param account The account to query the locked balance of.
* @return expiries The time at which each outstanding lockup bucket expires.
* @return amounts The number of METH tokens which will expire for each outstanding lockup bucket.
*/
function getLockups(address account) external view returns (uint256[] memory expiries, uint256[] memory amounts) {
AccountInfo storage accountInfo = accountToInfo[account];
// Count lockups
uint256 lockedCount;
// The number of buckets is always < 256 bits.
unchecked {
for (uint256 escrowIndex = accountInfo.lockupStartIndex; ; ++escrowIndex) {
LockedBalance.Lockup memory escrow = accountInfo.lockups.get(escrowIndex);
if (escrow.expiration == 0) {
break;
}
if (escrow.expiration >= block.timestamp && escrow.totalAmount != 0) {
// Lockup count will never overflow 256 bits.
++lockedCount;
}
}
}
// Allocate arrays
expiries = new uint256[](lockedCount);
amounts = new uint256[](lockedCount);
// Populate results
uint256 i;
// The number of buckets is always < 256 bits.
unchecked {
for (uint256 escrowIndex = accountInfo.lockupStartIndex; ; ++escrowIndex) {
LockedBalance.Lockup memory escrow = accountInfo.lockups.get(escrowIndex);
if (escrow.expiration == 0) {
break;
}
if (escrow.expiration >= block.timestamp && escrow.totalAmount != 0) {
expiries[i] = escrow.expiration;
amounts[i] = escrow.totalAmount;
++i;
}
}
}
}
/**
* @notice Returns the total balance of an account, including locked METH tokens.
* @dev Use `balanceOf` to get the number of tokens available for transfer or withdrawal.
* @param account The account to query the total balance of.
* @return balance The total METH balance tracked for this account.
*/
function totalBalanceOf(address account) external view returns (uint256 balance) {
AccountInfo storage accountInfo = accountToInfo[account];
balance = accountInfo.freedBalance;
// Total ETH cannot realistically overflow 96 bits and escrowIndex will always be < 256 bits.
unchecked {
// Add all lockups
for (uint256 escrowIndex = accountInfo.lockupStartIndex; ; ++escrowIndex) {
LockedBalance.Lockup memory escrow = accountInfo.lockups.get(escrowIndex);
if (escrow.expiration == 0) {
break;
}
balance += escrow.totalAmount;
}
}
}
/**
* @notice Returns the total amount of ETH locked in this contract.
* @return supply The total amount of ETH locked in this contract.
* @dev It is possible for this to diverge from the total token count by transferring ETH on self destruct
* but this is on-par with the WETH implementation and done for gas savings.
*/
function totalSupply() external view returns (uint256 supply) {
return address(this).balance;
}
}
// I don't think we need this for now