-
Notifications
You must be signed in to change notification settings - Fork 13
/
LToken.sol
1024 lines (865 loc) · 40.9 KB
/
LToken.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
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
// Contracts
import {ERC20WrapperUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20WrapperUpgradeable.sol";
import "./abstracts/base/ERC20BaseUpgradeable.sol";
import {InvestUpgradeable} from "./abstracts/InvestUpgradeable.sol";
import {LDYStaking} from "./LDYStaking.sol";
// Libraries
import {SafeERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
import {SUD} from "./libs/SUD.sol";
// Interfaces
import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import {IERC20MetadataUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol";
import {ITransfersListener} from "./interfaces/ITransfersListener.sol";
/**
* @title LToken
* @author Lila Rest (https://lila.rest)
* @custom:security-contact security@ledgity.com
*
* @notice Main contract of the Ledgity Yield protocol. It powers every L-Token (i.e.,
* investment pools backed by RWA). An L-Token is an ERC20 wrapper around a stablecoin.
* As soon as a wallet holds some L-Tokens, it starts receiving rewards in
* the form of additional L-Tokens, which are auto-compounded over time.
*
* @dev Definitions:
* - Deposit: Swap of underlying tokens for L-Tokens (1:1 ratio).
* - Withdrawal: Swap of L-Tokens for underlying tokens (1:1 ratio, minus applicable fees).
* - Instant: Processed immediately.
* - Request: Queued for later processing.
* - Big Request: A requested withdrawal exceeding half of the retention rate.
* - (Withdrawal) queue: A list of all requested withdrawals sorted by priority.
* - Request ID: The index of a withdrawal request in the queue array.
* - Retention rate: Maximum fraction of underlying tokens TVL the contract can retain.
* - Fees Rate: Percentage of fees applied to successful withdrawals.
* - Usable underlyings: Amount of underlying tokens that have been deposited through
* expected ways and are so considered safe to use by the contract.
* - Transfers listeners: External contracts listening on L-Tokens transfers.
* - Fund wallet: Wallet managed by the Ledgity's financial team.
* - Withdrawer wallet: Managed by an off-chain server to automate withdrawal request
* processing.
*
* Note that words between parenthesis are sometimes omitted for brevity.
*
* @dev Deployment notice:
* This contract can safely receive funds immediately after initialization. (i.e., there
* is no way for funds to be sent to non-owned addresses). It is, however, recommended to
* replace ASAP owner and fund wallets with multi-sig wallets.
*
* @dev For further details, see "LToken" section of whitepaper.
* @custom:oz-upgrades-unsafe-allow external-library-linking
* @custom:security-contact security@ledgity.com
*/
contract LToken is ERC20BaseUpgradeable, InvestUpgradeable, ERC20WrapperUpgradeable {
using SafeERC20Upgradeable for IERC20Upgradeable;
/// @dev Represents type of actions triggering ActivityEvent events.
enum Action {
Deposit,
Withdraw
}
/// @dev Represents different status of actions triggering ActivityEvent events.
enum Status {
Queued,
Cancelled,
Success,
Moved
}
/**
* @notice Represents a withdrawal request in the queue.
* @dev A request fits in a single storage slot (32 bytes).
* @param account The account that initiated the request.
* @param amount The amount of underlying tokens requested.
*/
struct WithdrawalRequest {
address account; // 20 bytes
uint96 amount; // 12 bytes
}
/// @notice Upper limit of retention rate.
uint32 private constant MAX_RETENTION_RATE_UD7x3 = 10 * 10 ** 3; // 10%
/// @notice Upper limit of fees rate.
uint32 private constant MAX_FEES_RATE_UD7x3 = 20 * 10 ** 3; // 20%
/// @notice Used in activity events to represent the absence of request ID.
int256 private constant NO_ID = -1;
/// @notice Holds a reference to the LDYStaking contract.
LDYStaking public ldyStaking;
/// @notice Holds address of withdrawer wallet (managed by withdrawal server).
address payable public withdrawer;
/// @notice Holds address of fund wallet (managed by Ledgity financial team).
address public fund;
/// @notice Holds the withdrawal fees rate in UD7x3 format (e.g., 350 = 0.350%).
uint32 public feesRateUD7x3;
/// @notice Holds the retention rate in UD7x3 format.
uint32 public retentionRateUD7x3;
/// @notice Holds the amount of withdrawal fees not yet claimed by contract's owner.
uint256 public unclaimedFees;
/// @notice Holds the amount of L-Tokens currently in the withdrawal queue.
uint256 public totalQueued;
/**
* @notice Holds the amount of underlying tokens considered as usable by the contract.
* @dev Are usable, only underlying tokens deposit through deposit() or fund() functions.
*/
uint256 public usableUnderlyings;
/// @notice Holds an ordered list of active withdrawal requests.
WithdrawalRequest[] public withdrawalQueue;
/// @notice Holds the index of the next withdrawal request to process in the queue.
uint256 public withdrawalQueueCursor;
/**
* @notice Holds a list of all currently frozen withdrawal requests.
* @dev If a request emitter as been blacklisted, its request is moved here to prevent
* it from blocking the queue.
*/
WithdrawalRequest[] public frozenRequests;
/**
* @notice Holds a list of contracts' references that are listening to L-Tokens transfers.
* @dev onLTokenTransfer() functions of those contracts will be called on each transfer.
*/
ITransfersListener[] public transfersListeners;
/**
* @notice Holds the withdrwalFee amount in ETH that will be sent to withdrawer wallet.
*/
uint256 public withdrwalFeeInEth;
/**
* @notice Emitted to inform listeners about a change in the contract's TVL.
* @dev TVL = realTotalSupply()
* @param newTVL The new TVL of the contract.
*/
event TVLChangeEvent(uint256 newTVL);
/**
* @notice Emitted to inform listerners about an activity related to deposits and withdrawals.
* @param id ID of the involved withdrawal request or NO_ID (-1) if not applicable.
* @param account The account involved in the activity.
* @param action The type of activity.
* @param amount The amount of underlying tokens involved in the activity.
* @param newStatus The new status of the activity.
* @param newId The new ID of the request if it has been moved in the queue.
*/
event ActivityEvent(
int256 indexed id,
address indexed account,
Action indexed action,
uint256 amount,
uint256 amountAfterFees,
Status newStatus,
int256 newId
);
/**
* @notice Emitted to inform listeners that some rewards have been minted.
* @param account The account that received the rewards.
* @param balanceBefore The balance of the account before the minting.
* @param rewards The amount of minted rewards.
*/
event MintedRewardsEvent(address indexed account, uint256 balanceBefore, uint256 rewards);
/// @notice Reverts if the function caller is not the withdrawer wallet.
modifier onlyWithdrawer() {
require(_msgSender() == withdrawer, "L39");
_;
}
/// @notice Reverts if the function caller is not the fund wallet.
modifier onlyFund() {
require(_msgSender() == fund, "L40");
_;
}
/**
* @notice Initializer function of the contract. It replaces the constructor()
* function in the context of upgradeable contracts.
* @dev See: https://docs.openzeppelin.com/contracts/4.x/upgradeable
* @param globalOwner_ The address of the GlobalOwner contract.
* @param globalPause_ The address of the GlobalPause contract.
* @param globalBlacklist_ The address of the GlobalBlacklist contract.
* @param underlyingToken The address of the underlying stablecoin ERC20 token.
*/
function initialize(
address globalOwner_,
address globalPause_,
address globalBlacklist_,
address ldyStaking_,
address underlyingToken
) public initializer {
// Initialize ERC20 base.
string memory underlyingSymbol = IERC20MetadataUpgradeable(underlyingToken).symbol();
__ERC20Base_init(
globalOwner_,
globalPause_,
globalBlacklist_,
string(abi.encodePacked("Ledgity ", underlyingSymbol)),
string(abi.encodePacked("L", underlyingSymbol))
);
// IMPORTANT: Below calls must not be restricted to owner at any point.
// This is because the GlobalOwner contract may not be a fresh one, and so
// the contract deployer may not be the owner anymore after ERC20Base init.
// Initialize other parents contracts.
__ERC20Wrapper_init(IERC20Upgradeable(underlyingToken));
__Invest_init_unchained(address(this));
// Set LDYStaking contract
ldyStaking = LDYStaking(ldyStaking_);
// Set initial withdrawal fees rate to 0.3%
feesRateUD7x3 = 300;
// Set initial retention rate to 10%
retentionRateUD7x3 = 10_000;
// Default withdrawer and fund wallet to contract owner address. This prevents
// any loss of funds if a deposit/withdrawal is made before those are manually set.
withdrawer = payable(owner());
fund = payable(owner());
// Set initial withdrwalFeeInEth
withdrwalFeeInEth = 0.00075 * 1e18;
}
/**
* @notice Required override of decimals() which is implemented by both
* ERC20Upgradeable and ERC20WrapperUpgradeable parent contracts.
* @dev The ERC20WrapperUpgradeable version is preferred because it mirrors the
* decimals amount of the underlying stablecoin token.
* @inheritdoc ERC20WrapperUpgradeable
*/
function decimals()
public
view
override(ERC20Upgradeable, ERC20WrapperUpgradeable)
returns (uint8)
{
return ERC20WrapperUpgradeable.decimals();
}
/**
* @notice Required override of paused() which is implemented by both
* GlobalPausableUpgradeable and ERC20BaseUpgradeable parent contracts.
* @dev Both version are the same as ERC20BaseUpgradeable.paused() mirrors
* GlobalPausableUpgradeable.paused(), so a random one is chosen.
* @inheritdoc GlobalPausableUpgradeable
*/
function paused()
public
view
virtual
override(GlobalPausableUpgradeable, ERC20BaseUpgradeable)
returns (bool)
{
return GlobalPausableUpgradeable.paused();
}
/**
* @notice Updates the current withdrawal fee rate.
* @param feesRateUD7x3_ The new withdrawal fee rate in UD7x3 format.
*/
function setFeesRate(uint32 feesRateUD7x3_) public onlyOwner {
require(feesRateUD7x3_ <= MAX_FEES_RATE_UD7x3, "L88");
feesRateUD7x3 = feesRateUD7x3_;
}
/**
* @notice Updates the current withdrawalFeeInETH.
* @param withdrwalFeeInEth_ The new withdrawalFee in ETH.
*/
function setWithdrwalFeeInEth(uint256 withdrwalFeeInEth_) public onlyOwner {
require(withdrwalFeeInEth <= MAX_FEES_RATE_UD7x3, "L88");
withdrwalFeeInEth = withdrwalFeeInEth_;
}
/**
* @notice Updates the current underlying token retention rate.
* @dev The retention rate is capped at 10%, which ensures that no more than 10% of
* deposited assets will ever be exposed in this contract (reduces attack surface).
* @param retentionRateUD7x3_ The new retention rate in UD7x3 format.
*/
function setRetentionRate(uint32 retentionRateUD7x3_) public onlyOwner {
require(retentionRateUD7x3_ <= MAX_RETENTION_RATE_UD7x3, "L41");
retentionRateUD7x3 = retentionRateUD7x3_;
}
/**
* @notice Updates the address of LDYStaking contract.
* @param ldyStakingAddress The address of the new LDYStaking contract.
*/
function setLDYStaking(address ldyStakingAddress) public onlyOwner {
ldyStaking = LDYStaking(ldyStakingAddress);
}
/**
* @notice Updates the address of the withdrawer wallet.
* @param withdrawer_ The address of the new withdrawer wallet.
*/
function setWithdrawer(address payable withdrawer_) public onlyOwner {
// Ensure address is not the zero address (pre-processing fees would be lost else)
require(withdrawer_ != address(0), "L63");
// Set new withdrawer wallet's address
withdrawer = withdrawer_;
}
/**
* @notice Updates the address of the fund wallet.
* @param fund_ The address of the new fund wallet.
*/
function setFund(address payable fund_) public onlyOwner {
// Ensure address is not the zero address (deposited tokens would be lost else)
require(fund_ != address(0), "L64");
// Set new fund wallet's address
fund = fund_;
}
/**
* @notice Adds a new contract to the L-Token transfers list.
* @dev Each time a transfer occurs, the onLTokenTransfer() function of the
* specified contract will be called.
* @dev IMPORTANT SECURITY NOTE: This method is not intended to be used with
* contracts that are not owned by the Ledgity team.
* @param listenerContract The address of the new transfers listener contract.
*/
function listenToTransfers(address listenerContract) public onlyOwner {
transfersListeners.push(ITransfersListener(listenerContract));
}
/**
* @notice Removes a contract from the L-Token transfers list.
* @dev The onLTokenTransfer() function of the specified contract will not be called
* anymore each time a L-Token transfer occurs.
* @param listenerContract The address of the listener contract.
*/
function unlistenToTransfers(address listenerContract) public onlyOwner {
// Find index of listener contract in transferListeners array
int256 index = -1;
uint256 transfersListenersLength = transfersListeners.length;
for (uint256 i = 0; i < transfersListenersLength; i++) {
if (address(transfersListeners[i]) == listenerContract) {
index = int256(i);
break;
}
}
// Revert if given contract wasn't listening to transfers
require(index > -1, "L42");
// Else, remove transfers listener contract from listeners array
transfersListeners[uint256(index)] = transfersListeners[transfersListenersLength - 1];
transfersListeners.pop();
}
/**
* @notice Retrieves the amount of given account's not yet minted rewards.
* @dev This is a public implementation of InvestUpgradeable_rewardsOf(). In the
* context of LToken, this function returns the amount of rewards that have not been
* distributed/minted yet to the specified account.
* @dev This is particularly useful for off-chain services to display charts and
* statistics, as seen in the Ledgity Yield's frontend.
* @param account The account to check the unminted rewards of.
* @return The amount of account's unminted rewards.
*/
function unmintedRewardsOf(address account) public view returns (uint256) {
return _rewardsOf(account, true);
}
/**
* @notice Retrieves the "real" balance of an account, i.e., excluding its not yet
* minted/distributed rewards.
* @param account The account to check the real balance of.
* @return The real balance of the account.
*/
function realBalanceOf(address account) public view returns (uint256) {
return super.balanceOf(account);
}
/**
* @notice Retrieves the total balance of L-Tokens that belong to the account.
* @dev This is an oOverride of ERC20Upgradeable.balanceOf() that rewards that have
* not been yet minted to the specified account.
* @param account The account to check the total balance of.
* @return The total balance of the account.
*/
function balanceOf(address account) public view override returns (uint256) {
return realBalanceOf(account) + unmintedRewardsOf(account);
}
/**
* @notice Returns the "real" amount of existing L-Tokens, i.e., excluding not yet
* minted withdrawal fees and L-Tokens currently in the withdrawal queue.
* @return The real total supply of L-Tokens.
*/
function realTotalSupply() public view returns (uint256) {
return super.totalSupply();
}
/**
* @notice Retrives the total supply of L-Tokens, including not yet minted withdrawal
* fees and L-Tokens currently in the withdrawal queue.
* @return The total supply of L-Tokens.
*/
function totalSupply() public view override returns (uint256) {
return realTotalSupply() + totalQueued + unclaimedFees;
}
/**
* @notice Recovers a specified amount of a given token address.
* @dev This override of RecoverableUpgradeable.recoverERC20() prevents the recovered
* token from being the underlying token.
* @inheritdoc RecoverableUpgradeable
*/
function recoverERC20(address tokenAddress, uint256 amount) public override onlyOwner {
// Ensure the token is not the underlying token
require(tokenAddress != address(underlying()), "L43");
// Proceed to recovery
super.recoverERC20(tokenAddress, amount);
}
/**
* @notice Recovers underlying tokens accidentally sent to the contract.
* @dev To prevent owner from being able to drain the contract, this function only
* allows recovering "unusable" underlying tokens, i.e., tokens that have not been
* sent through fund() or deposit() functions.
*/
function recoverUnderlying() external onlyOwner {
// Compute the recoverable amount by taking the difference between the contract's
// balance and the amount of usable underlying tokens
uint256 recoverableAmount = underlying().balanceOf(address(this)) - usableUnderlyings;
// Revert if there is nothing to recover
require(recoverableAmount > 0, "L44");
// Else, proceed to underlying tokens recovery
super.recoverERC20(address(underlying()), recoverableAmount);
}
/**
* @notice Retrieves the amount of underlying tokens invested by the given account.
* @dev Implementing this function is required by the InvestUpgradeable contract. In
* LToken contract, the investment of an account is equal to its real balance.
* @inheritdoc InvestUpgradeable
*/
function _investmentOf(address account) internal view override returns (uint256) {
return realBalanceOf(account);
}
/**
* @notice Distributes a specified amount of rewards (in L-Tokens) to a given account.
* @dev Implementing this function is required by the InvestUpgradeable contract so
* it can distribute rewards to accounts before each period reset.
* @dev InvestUpgradeable contract already ensure that amount > 0.
* @inheritdoc InvestUpgradeable
*/
function _distributeRewards(address account, uint256 amount) internal override returns (bool) {
// Inform listeners of the rewards minting
emit MintedRewardsEvent(account, realBalanceOf(account), amount);
// Mint L-Tokens rewards to account
_mint(account, amount);
// Return true indicating to InvestUpgradeable that the rewards have been distributed
return true;
}
/**
* @notice Override of ERC20._beforeTokenTransfer() to integrate with InvestUpgradeable.
* @dev This overriden version ensure that _beforeInvestmentChange() hook is properly
* called each time an account's balance is going to change.
* @dev Note: whenNotPaused and notBlacklisted modifiers are not set as they are
* already included in ERC20BaseUpgradeable._beforeTokenTransfer().
* @inheritdoc ERC20BaseUpgradeable
*/
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal override(ERC20Upgradeable, ERC20BaseUpgradeable) {
ERC20BaseUpgradeable._beforeTokenTransfer(from, to, amount);
// Invoke _beforeInvestmentChange() hook for non-zero accounts
if (from != address(0)) _beforeInvestmentChange(from, true);
if (to != address(0)) _beforeInvestmentChange(to, true);
}
/**
* @notice Override of ERC20._afterTokenTransfer() to notify all transfers listeners.
* @dev This overriden version will trigger onLTokenTransfer() functions of all
* transfers listeners.
* @dev Note: whenNotPaused and notBlacklisted modifiers are not set as they are
* already checked in _beforeTokenTransfer().
* @inheritdoc ERC20Upgradeable
*/
function _afterTokenTransfer(address from, address to, uint256 amount) internal override {
super._afterTokenTransfer(from, to, amount);
// If some L-Token have been burned/minted, inform listeners of a TVL change
if (from == address(0) || to == address(0)) emit TVLChangeEvent(totalSupply());
// Trigger onLTokenTransfer() functions of all the transfers listeners
for (uint256 i = 0; i < transfersListeners.length; i++) {
transfersListeners[i].onLTokenTransfer(from, to, amount);
}
}
/**
* @notice Computes the maximum amount of underlying tokens that should be retained
* by the contract (based on retention rate).
* @return amount The expected amount of retained underlying tokens.
*/
function getExpectedRetained() public view returns (uint256 amount) {
// Cache invested token's decimals number
uint256 d = SUD.decimalsOf(address(invested()));
// Convert totalSupply and retentionRate to SUD
uint256 totalSupplySUD = SUD.fromAmount(totalSupply(), d);
uint256 retentionRateSUD = SUD.fromRate(retentionRateUD7x3, d);
// Compute and return expected retained amount
uint256 expectedRetainedSUD = (totalSupplySUD * retentionRateSUD) / SUD.fromInt(100, d);
return SUD.toAmount(expectedRetainedSUD, d);
}
/// @notice Transfers underlying tokens exceeding the retention rate to the fund wallet.
function _transferExceedingToFund() internal {
// Retrieve the expected amount retained
uint256 expectedRetained = getExpectedRetained();
// If usable underlyings are less than or equal to expected retained, return
if (usableUnderlyings <= expectedRetained) return;
// Else, exceeding amount is equal to difference between those values
uint256 exceedingAmount = usableUnderlyings - expectedRetained;
// Decrease usable underlyings amount accordingly
usableUnderlyings -= exceedingAmount;
// Transfer the exceeding amount to the fund wallet
underlying().safeTransfer(fund, exceedingAmount);
}
/**
* @notice Override of ERC20WrapperUpgradeable.withdrawTo() that reverts.
* Use instantWithdrawal() or requestWithdrawal() functions instead.
* @inheritdoc ERC20WrapperUpgradeable
*/
function withdrawTo(address account, uint256 amount) public pure override returns (bool) {
account; // Silence unused variable compiler warning
amount;
revert("L45");
}
/**
* @notice Override of ERC20WrapperUpgradeable.depositFor() that reverts.
* Use deposit() function instead.
* @inheritdoc ERC20WrapperUpgradeable
*/
function depositFor(address account, uint256 amount) public pure override returns (bool) {
account; // Silence unused variable compiler warning
amount;
revert("L46");
}
/**
* @notice Allows exchanging some underlying tokens for the same amount of L-Tokens.
* @param amount The amount of underlying tokens to deposit.
*/
function deposit(uint256 amount) public whenNotPaused notBlacklisted(_msgSender()) {
// Ensure the account has enough underlying tokens to deposit
require(underlying().balanceOf(_msgSender()) >= amount, "L47");
// Update usable underlyings balance accordingly
usableUnderlyings += amount;
// Inform listeners of the deposit activity event
emit ActivityEvent(
NO_ID,
_msgSender(),
Action.Deposit,
amount,
amount,
Status.Success,
NO_ID
);
// Receive underlying tokens and mint L-Tokens to the account in a 1:1 ratio
super.depositFor(_msgSender(), amount);
// Transfer exceeding underlying tokens to the fund wallet
_transferExceedingToFund();
}
/**
* @notice Computes fees and net withdrawn amount for a given account withdrawing a
* given amount.
* @param account The account initiating the withdrawal.
* @param amount The amount of the withdrawal.
*/
function getWithdrawnAmountAndFees(
address account,
uint256 amount
) public view returns (uint256 withdrawnAmount, uint256 fees) {
// If the account is eligible to staking tier 2, no fees are applied
if (ldyStaking.tierOf(account) >= 2) return (amount, 0);
// Cache invested token's decimals number
uint256 d = SUD.decimalsOf(address(invested()));
// Convert amount and fees rate to SUD
uint256 amountSUD = SUD.fromAmount(amount, d);
uint256 feesRateSUD = SUD.fromRate(feesRateUD7x3, d);
// Compute fees and withdrawn amount (initial amount minus fees)
uint256 feesSUD = (amountSUD * feesRateSUD) / SUD.fromInt(100, d);
fees = SUD.toAmount(feesSUD, d);
withdrawnAmount = amount - fees;
}
/**
* @notice Allows instaneously exchanging a given amount of L-Tokens for the same
* amount of underlying tokens. It will fail if the contract currently doesn't hold
* enough underlying tokens to cover the withdrawal.
* @dev In order to save some gas and time to users, frontends should propose this
* function to users only when it has been verified that it will not revert. They
* should propose the requestWithdrawal() function otherwise.
* @param amount The amount L-Tokens to withdraw.
*/
function instantWithdrawal(uint256 amount) external whenNotPaused notBlacklisted(_msgSender()) {
// Ensure the account has enough L-Tokens to withdraw
require(amount <= balanceOf(_msgSender()), "L48");
// Can the contract cover this withdrawal plus all already queued requests?
bool cond1 = totalQueued + amount <= usableUnderlyings;
// Is caller eligible to staking tier 2 and the contract can cover this withdrawal?
bool cond2 = ldyStaking.tierOf(_msgSender()) >= 2 && amount <= usableUnderlyings;
// Revert if conditions are not met for the withdrawal to be processed instantaneously
if (!(cond1 || cond2)) revert("L49");
// Else, retrieve withdrawal fees and net withdrawn amount
(uint256 withdrawnAmount, uint256 fees) = getWithdrawnAmountAndFees(_msgSender(), amount);
// Increase unclaimed fees amount accordingly
unclaimedFees += fees;
// Decrease usable underlyings balance accordingly
usableUnderlyings -= withdrawnAmount;
// Inform listeners of this instant withdrawal activity event
emit ActivityEvent(
NO_ID,
_msgSender(),
Action.Withdraw,
amount,
withdrawnAmount,
Status.Success,
NO_ID
);
// Burn withdrawal fees from the account
_burn(_msgSender(), fees);
// Burn account's withdrawn L-Tokens and transfer to it underlying tokens in a 1:1 ratio
super.withdrawTo(_msgSender(), withdrawnAmount);
}
/**
* @notice Allows requesting the exchange of a given amount of L-Tokens for the same
* amount of underlying tokens. The request will be automatically processed later.
* @dev The sender must attach withdrwalFeeInETH to pre-pay the future processing gas fees
* paid by the withdrawer wallet.
* @param amount The amount L-Tokens to withdraw.
*/
function requestWithdrawal(
uint256 amount
) public payable whenNotPaused notBlacklisted(_msgSender()) {
// Ensure the account has enough L-Tokens to withdraw
require(amount <= balanceOf(_msgSender()), "L53");
// Ensure the requested amount doesn't overflow uint96
require(amount <= type(uint96).max, "L54");
// Ensure the sender attached the pre-paid processing gas fees
require(msg.value == withdrwalFeeInEth, "L55");
// Create withdrawal request data
WithdrawalRequest memory request = WithdrawalRequest({
account: _msgSender(),
amount: uint96(amount)
});
// Will hold the request ID
uint256 requestId;
// Append request to the withdrawal queue:
// - At the beginning, if account is eligible to staking tier 2 and cursor is not 0
if (ldyStaking.tierOf(_msgSender()) >= 2 && withdrawalQueueCursor > 0) {
withdrawalQueueCursor--;
requestId = withdrawalQueueCursor;
withdrawalQueue[requestId] = request;
}
// - At the end else
else {
withdrawalQueue.push(request);
requestId = withdrawalQueue.length - 1;
}
// Increase total amount queued accordingly
totalQueued += amount;
// Inform listeners of this new queued withdrawal activity event
emit ActivityEvent(
int256(requestId),
_msgSender(),
Action.Withdraw,
amount,
amount,
Status.Queued,
NO_ID
);
// Burn withdrawal L-Tokens amount from account's balance
_burn(_msgSender(), amount);
// Forward pre-paid processing gas fees to the withdrawer wallet
(bool sent, ) = withdrawer.call{value: msg.value}("");
require(sent, "L56");
}
/**
* @notice Processes queued withdrawal requests until there is else no more requests,
* else not enough underlying tokens to continue.
* @dev For further details, see "LToken > Withdrawals" section of whitepaper.
*/
function processQueuedRequests() external onlyWithdrawer whenNotPaused {
// Accumulators variables, will be written on-chain after the loop
uint256 cumulatedFees = 0;
uint256 cumulatedWithdrawnAmount = 0;
uint256 nextRequestId = withdrawalQueueCursor;
// Cache queue length to avoid multiple SLOADs and avoid infinite loop as big
// requests are increasing the queue length when moved at the end of the queue.
uint256 queueLength = withdrawalQueue.length;
// Iterate over requests to be processed
while (nextRequestId < queueLength) {
// Stop processing requests if there is not enough gas left to continue the
// loop and properly end the function call. This prevents an attacker from
// blocking the withdrawal processing by creating a ton of tiny requests so
// this function call cannot fit anymore in block gas limit.
if (gasleft() < 45000) break;
// Retrieve request data
WithdrawalRequest memory request = withdrawalQueue[nextRequestId];
// Skip empty request (processed big requests or cancelled requests)
if (request.account == address(0)) {}
//
// If account has been blacklisted since request emission
else if (isBlacklisted(request.account)) {
// Remove request from queue
delete withdrawalQueue[nextRequestId];
// Append request in the frozen requests list
frozenRequests.push(request);
}
//
// Or if request is a big request, move it at the end of the queue for now.
// This request will be processed manually later using processBigQueuedRequest()
else if (request.amount > getExpectedRetained() / 2) {
// Inform listeners of this queued request being moved at the end of the queue
emit ActivityEvent(
int256(nextRequestId),
_msgSender(),
Action.Withdraw,
request.amount,
request.amount,
Status.Moved,
int256(withdrawalQueue.length)
);
// Remove request from queue
delete withdrawalQueue[nextRequestId];
// Append request at the end of the queue
withdrawalQueue.push(request);
}
//
// Else, continue request processing
else {
// Retrieve withdrawal fees and net withdrawn amount
(uint256 withdrawnAmount, uint256 fees) = getWithdrawnAmountAndFees(
request.account,
request.amount
);
// Break if the contract doesn't hold enough funds to cover the request
if (withdrawnAmount > usableUnderlyings - cumulatedWithdrawnAmount) break;
// Accumulate fees and withdrawn amount
cumulatedFees += fees;
cumulatedWithdrawnAmount += withdrawnAmount;
// Inform listeners of this queued withdrawal processing activity event
emit ActivityEvent(
int256(nextRequestId),
request.account,
Action.Withdraw,
request.amount,
withdrawnAmount,
Status.Success,
NO_ID
);
// Remove request from queue
delete withdrawalQueue[nextRequestId];
// Transfer underlying tokens to account. Burning L-Tokens is not required
// as equestWithdrawal() already did it.
// Security note: Re-entrancy warning are disabled as the request has
// just been deleted from the queue, it will so be skipped if trying to
// process it again.
// slither-disable-next-line reentrancy-no-eth
underlying().safeTransfer(request.account, withdrawnAmount);
}
// Increment next request ID
nextRequestId++;
}
// Increase unclaimed fees by the amount of cumulated fees
unclaimedFees += cumulatedFees;
// Decrease usable underlyings by the cumulated amount of withdrawn underlyings
usableUnderlyings -= cumulatedWithdrawnAmount;
// Decrease total amount queued by the cumulated amount requested
totalQueued -= cumulatedWithdrawnAmount + cumulatedFees;
// Update new queue cursor
withdrawalQueueCursor = nextRequestId;
// Retention rate cannot exceeds as the withdrawal decreases both usable
// underlyings and expected retained amounts by the same number and as the
// expected retained amount is a subset of usable underlyings amount.
}
/**
* @notice Processes a given queued big withdrawal request (one that exceeds half of
* the retention rate).
* @dev In contrast to non-big requests processing, this function will uses to fund
* wallet's balance to fill the request. This allows processing requests that are
* greater than retention rate without having to exceed this rate on the contract.
* @param requestId The ID of the big request to process.
*/
function processBigQueuedRequest(uint256 requestId) external onlyFund whenNotPaused {
// Retrieve request data
WithdrawalRequest memory request = withdrawalQueue[requestId];
// Ensure the request is active
require(request.account != address(0), "L66");
// Ensure the request emitter has not been blacklisted since request emission
require(!isBlacklisted(request.account), "L50");
// Ensure this is indeed a big request
require(request.amount > getExpectedRetained() / 2, "L51");
// Retrieve withdrawal fees and net withdrawn amount
(uint256 withdrawnAmount, uint256 fees) = getWithdrawnAmountAndFees(
request.account,
request.amount
);
// Ensure withdrawn amount can be covered by contract + fund wallet balances
uint256 fundBalance = underlying().balanceOf(fund);
require(withdrawnAmount <= usableUnderlyings + fundBalance, "L52");
// Increase amount of unclaimed fees accordingly
unclaimedFees += fees;
// Decrease total queued amount by request amount
totalQueued -= request.amount;
// Increment queue cursor if request was the next request to be processed
if (requestId == withdrawalQueueCursor) withdrawalQueueCursor++;
// Inform listeners of this queued withdrawal processing activity event
emit ActivityEvent(
int256(requestId),
request.account,
Action.Withdraw,
request.amount,
withdrawnAmount,
Status.Success,
NO_ID
);
// Remove request from queue
delete withdrawalQueue[requestId];
// If fund wallet's balance can cover request, rely on it only
if (withdrawnAmount <= fundBalance) {
underlying().safeTransferFrom(_msgSender(), request.account, withdrawnAmount);
}
// Else, cover request from both fund wallet and contract balances
else {
// Compute amount missing from fund wallet to cover request
uint256 missingAmount = withdrawnAmount - fundBalance;
// Decrease usable amount of underlying tokens accordingly
usableUnderlyings -= missingAmount;
// Transfer entire fund balance to request's emitter
underlying().safeTransferFrom(_msgSender(), request.account, fundBalance);
// Transfer missing amount from contract balance to request emitter
underlying().safeTransfer(request.account, missingAmount);
}
// Transfer exceeding underlying tokens to the fund wallet
_transferExceedingToFund();
}
/**
* @notice Cancels a given withdrawal request. The request emitter receive back its
* L-Tokens and no fees will be charged.
* @param requestId The ID of the withdrawal request to cancel.
*/
function cancelWithdrawalRequest(
uint256 requestId
) public whenNotPaused notBlacklisted(_msgSender()) {
// Retrieve request data
WithdrawalRequest memory request = withdrawalQueue[requestId];
// Ensure request belongs to caller
require(_msgSender() == request.account, "L57");
// Decrease total amount queued accordingly
totalQueued -= request.amount;
// Delete the withdrawal request from queue
delete withdrawalQueue[requestId];
// Inform listeners of this cancelled withdrawal request activity event
emit ActivityEvent(
int256(requestId),
request.account,
Action.Withdraw,
request.amount,
request.amount,
Status.Cancelled,
NO_ID
);
// Mint back L-Tokens to account
_mint(request.account, uint256(request.amount));
}
/**
* @notice Used by the fund wallet to repatriate underlying tokens on the contract
* whenever those are required to fulfill some withdrawal requests.
* @dev The function will revert if repatriated amount makes the contract exceeding
* the retention rate.
* @param amount The amount of underlying tokens to repatriate.
*/
function repatriate(uint256 amount) external onlyFund whenNotPaused {
// Ensure the fund wallet has enough funds to repatriate
require(amount <= underlying().balanceOf(fund), "L58");
// Calculate new contract usable balance
uint256 newBalance = usableUnderlyings + amount;
// Ensure the new balance doesn't exceed the retention rate
require(newBalance <= getExpectedRetained(), "L59");
// Increase usable underlyings amount by repatriated amount
usableUnderlyings += amount;