-
Notifications
You must be signed in to change notification settings - Fork 0
/
InactivityCover.sol
1122 lines (1033 loc) · 56.6 KB
/
InactivityCover.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.12;
pragma abicoder v2;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "../interfaces/StakingInterface.sol";
import "../interfaces/IOracleMaster.sol";
import "../interfaces/Types.sol";
import "../interfaces/IAuthManager.sol";
import "../interfaces/IPushable.sol";
import "../interfaces/IProxy.sol";
import "./DepositStaking.sol";
contract InactivityCover is IPushable, ReentrancyGuard {
struct ScheduledDecrease {
uint128 era; // the era when the scheduled decrease was created
uint256 amount;
}
struct Member {
uint256 deposit; // deposit
uint256 maxCoveredDelegation; // any amount of this limit is not covered (used to incentivize splitting large delegations among multiple collators)
uint256 delegatorsReportedInEra; //
uint256 lastDelegationsTotal; // total backing of this collator the last time a report was pushed
uint128 noZeroPtsCoverAfterEra; // if positive (non-zero), then the member does not offer 0-point cover after this era
uint128 noActiveSetCoverAfterEra; // if positive (non-zero), the member does not offer out-of-active-set cover after this era
uint128 defaultCount; // how many time this member has defaulted
uint128 lastPushedEra; // the last era that was pushed and processed for this member; oracles may agree to not report an era for a member if there is no effect (no cover claims)
uint128 wentInactiveEra; // last era that the member's bool active value was set to false
uint128 wentActiveEra; // last era that the member's bool active value was set to true
bool isMember; // once a member, always a member
bool active; // starts active, can go inactive by reducing deposit to less than minimum deposit
}
event DepositEvent(address member, uint256 amount);
event DecreaseCoverScheduledEvent(address member, uint256 amount, uint128 eraId);
event DecreaseCoverEvent(address member, uint256 amount);
event CancelDecreaseCoverEvent(address member);
event MemberNotInActiveSetEvent(address member, uint128 eraId);
event MemberHasZeroPointsEvent(address member, uint128 eraId);
event PayoutEvent(address delegator, uint256 amount);
event DelegatorNotPaidEvent(address delegator, uint256 amount);
event DelegatorPayoutLessThanMinEvent(address delegator);
event MemberNotPaidEvent(address member, uint256 amount);
event MemberSetMaxCoveredDelegationEvent(address member, uint256 amount);
event MemberSetCoverTypesEvent(
address member,
bool noZeroPtsCoverAfterEra,
bool noActiveSetCoverAfterEra
);
event MemberInvoicedEvent(address member, uint256 amount, uint128 eraId);
event MemberDefaultEvent(address member);
event MemberActiveStatusChanged(address member, bool active);
event OraclePaidEvent(address member, uint256 amount, uint128 eraId);
event UpgradedToV2Event(address member);
event SetStakeUnitCoverEvent(uint256 amount);
event SetMinPayoutEvent(uint256 amount);
event SetRefundOracleGasPriceEvent(uint256 amount);
event SetErasBetweenForcedUndelegationsEvent(uint256 amount);
event SetMaxEraMemberPayoutEvent(uint256 amount);
event SetMaxErasCoveredEvent(uint256 amount);
event SetNoManualWhitelistingRequiredEvent(bool notRequired);
event SetMemberFeeEvent(uint256 amount);
event WhitelistEvent(address member, address proxy);
/// The ParachainStaking wrapper at the known pre-compile address. This will be used to make all calls
/// to the underlying staking solution
ParachainStaking public staking;
IProxy public proxy;
// auth manager contract address
address public AUTH_MANAGER;
// oracle master contract
address public ORACLE_MASTER;
// deposit staking contract
address public DEPOSIT_STAKING;
// CONSTANTS
// Minimum amount a user can deposit
uint256 public MIN_DEPOSIT;
// Maximum total amount a user can deposit
uint256 public MAX_DEPOSIT_TOTAL;
// Refund given per 1 MOVR staked for every missed round
uint256 public STAKE_UNIT_COVER;
// Minimum one-time payment for a delegator
uint256 public MIN_PAYOUT;
// Minimum number of eras between forced undelegations
uint128 public ERAS_BETWEEN_FORCED_UNDELEGATION;
// Maximum era payout
uint256 public MAX_ERA_MEMBER_PAYOUT;
// Maximum eras covered
uint128 public MAX_ERAS_COVERED = 360;
//Variables for Cover Claims
// Current era id (round)
uint128 public eraId;
// Whitelisted members to their proxy/management accounts
mapping(address => address) public whitelisted;
// Total members deposit
uint256 public membersDepositTotal;
// Addresss of any account that has ever made a deposit
address[] public memberAddresses;
// Adresses to deposit amounts
mapping(address => Member) public members;
// Scheduled cover decreases by members
mapping(address => ScheduledDecrease) public scheduledDecreasesMap;
// Toal amount owed to delegators to pay all pending cover claims
// Τhe contract's balance can grow through staking so we need to cover the deposited amount separately
uint256 public payoutsOwedTotal;
// The number of eras each member covers (forecast)
// this is also the number of eras a member must wait to execute a decrease request
mapping(address => uint128) public erasCovered;
// map of delegators to amounts owed by collators
mapping(address => uint256) public payoutAmounts;
// map of total payouts to delegators
mapping(address => uint256) public totalPayouts;
// If not 0, the oracle is credited with the tx cost for caclulating the cover payments
uint256 public refundOracleGasPrice;
// If set to true, collators don't need whitelisting and can join/deposit funds from a Governance proxy
bool public noManualWhitelistingRequired;
/* If a collator cannot withdraw their funds due to the funds being locked in staking, their address is
recorded in memberNotPaid .This will prohibit the manager from bonding more until that collator is paid
by forcing undelegate
*/
address public memberNotPaid;
// Same as above for delegators who cannot claims their cover due to funds being locked
address public delegatorNotPaid;
// How much does a member (who does not run an oracle) get charged every time they are invoiced
uint256 public memberFee; // default is zero
// The least era when members were successfulyl invoiced
uint128 public membersInvoicedLastEra;
// Set to true after runtime 2100 to enable getting delegaiton amounts from chain
bool public runtime2100;
// For emergency pausing
bool public paused;
// Manager role
bytes32 internal immutable ROLE_MANAGER;
// Allows function calls only from Oracle
modifier onlyOracle() {
address oracle = IOracleMaster(ORACLE_MASTER).getOracle();
require(msg.sender == oracle, "NOT_OR");
_;
}
// Allows function calls only from Oracle
modifier onlyDepositStaking() {
require(msg.sender == DEPOSIT_STAKING, "NOT_DS");
_;
}
// Allows function calls only from member with specific role
modifier auth(bytes32 role) {
require(IAuthManager(AUTH_MANAGER).has(role, msg.sender), "UNAUTH");
_;
}
constructor () {
ROLE_MANAGER = keccak256("ROLE_MANAGER");
}
/**
@notice Initialize contract.
@dev Can only be called once and should be called right after contract deployment
*/
function initialize(
address _auth_manager,
address _oracle_master,
address _deposit_staking,
uint256 _min_deposit,
uint256 _max_deposit_total,
uint256 _stake_unit_cover,
uint256 _min_payout,
uint256 _max_era_member_payout,
uint128 _eras_between_forced_undelegation
) external {
require(
AUTH_MANAGER == address(0) && _auth_manager != address(0),
"ALREADY_INITIALIZED"
);
staking = ParachainStaking(0x0000000000000000000000000000000000000800);
proxy = IProxy(0x000000000000000000000000000000000000080b);
AUTH_MANAGER = _auth_manager;
ORACLE_MASTER = _oracle_master;
DEPOSIT_STAKING = _deposit_staking;
MIN_DEPOSIT = _min_deposit;
MAX_DEPOSIT_TOTAL = _max_deposit_total;
MIN_PAYOUT = _min_payout;
STAKE_UNIT_COVER = _stake_unit_cover;
MAX_ERA_MEMBER_PAYOUT = _max_era_member_payout;
ERAS_BETWEEN_FORCED_UNDELEGATION = _eras_between_forced_undelegation;
}
/// ***************** MEMBER (COLLATOR) FUNCS *****************
/**
@notice Deposit cover funds for a member collator
@dev Collators can start offering cover to their delegators by making a cover deposit larger than the MIN_DEPOSIT.
The covererage period offered (ad advertised at stakemovr.com and stakeglmr.com) is calculated based on the size of the deposit.
Collators can deposit more funds or schedule a withdrawal at any time.
If noManualWhitelistingRequired is true, then the caller must be a Gov proxy of the collator member address.
If noManualWhitelistingRequired is false, i.e. whitelisting IS required, then the caller must be already whitelisted by the contract manager.
If the collator has defaulted in making a cover payment, then they must deposit at least the defualetd amount.
A succesful deposit will enroll the collator (member.isMember=true) but it will only activate cover (member.active=true) if the total deposit is larger than MIN_DEPOSIT.
@param _member The collator address the deposit is for. The caller of the depositCover method is not the member/collator address
(the collator address should be kept in cold storage and not sued for contract interactions). The caller is wither manually whitelisted address,
or a Gov proxy of the collator address. This is why the method has to provider the collator address in the _member field.
*/
function depositCover(address _member) external payable {
require(_isMemberAuth(msg.sender, _member), "N_COLLATOR_PROXY");
require(msg.value >= MIN_DEPOSIT, "BEL_MIN_DEP"); // avoid spam deposits
require(_member != address(0), "ZERO_ADDR");
require(
members[_member].deposit + msg.value <= MAX_DEPOSIT_TOTAL,
"EXC_MAX_DEP"
);
require(members[_member].defaultCount <= 3, "EXC_MAX_DEF");
if (!members[_member].isMember) {
memberAddresses.push(_member);
members[_member].isMember = true;
members[_member].maxCoveredDelegation = type(uint256).max; // default no-max value (editable)
erasCovered[_member] = 8; // initial cover period - to be updated in the next oracle push
} else {
_updateErasCovered(_member, members[_member].lastDelegationsTotal);
}
members[_member].deposit += msg.value;
if (members[_member].deposit >= MIN_DEPOSIT && !members[_member].active) {
members[_member].wentActiveEra = eraId;
members[_member].active = true;
emit MemberActiveStatusChanged(_member, true);
}
membersDepositTotal += msg.value;
emit DepositEvent(_member, msg.value);
}
/**
@notice Schedule a deposit decrease that can be executed in the future
@dev A member can request to withdraw its cover funds. The member has to wait for a number of rounds
until they can withdraw. During this waiting time, their funds continue to cover their delegators.
@param _member the collator member the calling address represents. Do not use the collator account to interact with this contract.
@param _amount how much to decrease the cover by.
*/
function scheduleDecreaseCover(address _member, uint256 _amount) external {
require(_isMemberAuth(msg.sender, _member), "N_COLLATOR_PROXY");
_scheduleDecreaseCover(_amount, _member);
}
/**
@notice Cancel a scheduled cover decrease (withdrawal)
@dev Members can cancel a scheduled deposit decrease while it is still pending.
The cancellation will take effect immediately, which will result in changes in the advertised cover offering at stakemovr.com
*/
function cancelDecreaseCover(address _member) external {
require(_isMemberAuth(msg.sender, _member), "N_COLLATOR_PROXY");
require(members[_member].deposit != 0, "NO_DEP");
require(scheduledDecreasesMap[_member].amount != 0, "DECR_N_EXIST");
// Reset memberNotPaid to 0 if it was set to this collator, otherwise leave as is.
// Anybody can execute a scheduled member withdrawal on their behalf, but only the member can cancel its request.
// Therefore, it is necessary to reset memberNotPaid on cancellation, to avoid a stuck non-zero membernotPaid value.
if (memberNotPaid == _member) {
memberNotPaid = address(0);
}
delete scheduledDecreasesMap[_member];
emit CancelDecreaseCoverEvent(_member);
}
/**
@notice Set the maximum delegation that this collator covers (per single delegation). Any amount above this will only be covered up to the max.
@dev Members can choose to protect delegations up to a specific amount (this might incentivize delegators to spread their stake among collators)
A delegator can circumvent this by using multiple delegator addresses if they really want to delegate all their funds with one collator and sitll be covered.
Member must be active to edit this parameter. Memebr are not allowed to set a max below 500 to avoid abuse. To disable this limit, the member
can set maxCoveredDelegation to a very high number.
@param _max_covered the max delegation that is covered (any amount above that will not receive cover only up to the max amount)
*/
function memberSetMaxCoveredDelegation(
address _member,
uint256 _max_covered
) external {
require(_isMemberAuth(msg.sender, _member), "N_COLLATOR_PROXY");
require(members[_member].active, "NOT_ACTIVE");
// To disable max_covered, we can use a very high value.
require(_max_covered >= 20000 ether, "INVALID"); // TODO change value for Moonbeam
members[_member].maxCoveredDelegation = _max_covered;
emit MemberSetMaxCoveredDelegationEvent(_member, _max_covered);
}
/**
@notice Memebrs can set the type of cover offered to their delegators
@dev Members can protect their delegators against them going down (zero points) or out (not in active set) or both.
At least one cover type is required. The change becomes effective after a number of eras have passed to protect delegators.
The delay period is different for each member and it depends on the cover duration, i.e. erasCovered (which depends on deposit).
The method stores the era in which a type of cover will be disabled based on the current erasCovered value.
@param _noZeroPtsCoverAfterEra false if you want to cover zero-point rounds (down), true if you want to disbale that cover type
@param _noActiveSetCoverAfterEra false if you want to cover being kicked out of the active set (out), true if you want to disable that cover type
*/
function memberSetCoverTypes(
address _member,
bool _noZeroPtsCoverAfterEra,
bool _noActiveSetCoverAfterEra
) external {
require(_isMemberAuth(msg.sender, _member), "N_COLLATOR_PROXY");
require(members[_member].active, "NOT_ACTIVE");
// at least one of the cover types must be active (true)
require(
_noZeroPtsCoverAfterEra || _noActiveSetCoverAfterEra,
"INV_COVER"
);
// The eraIds signify the eras on which the cover will stop providing that specific type
members[_member].noZeroPtsCoverAfterEra = _noZeroPtsCoverAfterEra
? _getEra() + erasCovered[_member]
: 0;
members[_member].noActiveSetCoverAfterEra = _noActiveSetCoverAfterEra
? _getEra() + erasCovered[_member]
: 0;
emit MemberSetCoverTypesEvent(
_member,
_noZeroPtsCoverAfterEra,
_noActiveSetCoverAfterEra
);
}
/**
@notice A member can authorize the transfer of its collator cover managerment rights to another address
@dev A member might want to use a different address to manage its collator cover. This method allows
to tarnsfer the management rights which also immediately removes those rights from the previous address (caller).
A Gov proxy account can also use this method to authorize another addres (not a Gov proxy) to manage its collator cover.
Only one such authorized address can exist for a collator at a time. However, obviously multiple Gov proxy addresses can exist,
with the same access rights to managing cover. Authorizing the 0x0 address, disables non-proxy authorization
for that member, and can only be undone by the manager by whitelisting again.
*/
function transferMemberAuth(address _member, address proxyAccount) external {
require(_isMemberAuth(msg.sender, _member), "N_COLLATOR_PROXY");
whitelisted[_member] = proxyAccount;
emit WhitelistEvent(_member, proxyAccount);
}
/// ***************** MEMBER FUNCS THAT CAN BE CALLED BY ANYBODY *****************
/**
@notice Execute a scheduled cover decrease (withdrawal) by a member
@dev Anybody can execute a matured deposit decrease request of a member.
For the request to be mature/executable, a number of eras must have passed since the request was made.
This erasCovered "delay period" is different for every member and depends on that memebrs total deposit (the larger the deposit, the longer the period).
The delay period is set when the decrease is scheduled, and is based on the data at that era.
The method also records defaults in paying members by updating the memberNotPaid value. Should a default be recorded,
no other default will be recorded until that default is resolved. A default limits the staking manager's ability
to delegate or bondMore of the contract's liquid funds.
@param _member The collator member whose scheduled withdrawal we are executing (anybody can execute it)
*/
function executeScheduled(address payable _member) external {
require(scheduledDecreasesMap[_member].amount != 0, "DECR_N_EXIST");
require(
// The current era must be after the era the decrease is scheduled for
scheduledDecreasesMap[_member].era <= _getEra(),
"NOT_EXEC"
);
uint256 amount = scheduledDecreasesMap[_member].amount;
// Check if contract has enough reducible balance (may be locked in staking)
if (address(this).balance < amount) {
// Only update if not already set
// This means that memberNotPaid will always store the first member that was not paid and only that member,
// until they are paid (anybody can execute payment if funds are liquid) or until they cancel their decrease
if (memberNotPaid == address(0)) {
memberNotPaid = _member;
emit MemberNotPaidEvent(_member, amount);
}
return;
}
// Reset memberNotPaid to 0 if it was set to this collator, otherwise leave as is
if (memberNotPaid == _member) {
memberNotPaid = address(0);
}
members[_member].deposit -= amount;
if (members[_member].deposit < MIN_DEPOSIT) {
members[_member].active = false;
members[_member].wentInactiveEra = eraId;
emit MemberActiveStatusChanged(_member, false);
}
membersDepositTotal -= amount;
delete scheduledDecreasesMap[_member];
emit DecreaseCoverEvent(_member, amount);
(bool sent, ) = _member.call{value: amount}("");
require(sent, "TRANSF_FAIL");
}
/**
@notice Pays out the accumulated delegator rewards claims to the given delegators.
@dev Calling this method will result to the supplied delegators being paid the cover claims balance they are owed.
A positive balance may have resulted from multiple roun covers, or even from multiple collators.
If a payment default is recorded, the delegatorNotPaid state variable is set. Subsequent defaults will not update that value.
Therefore, the delegator that was not paid must be paid for the variable to clear and to unblock the staking manager from
delegating or bonding more. A default does not block other delegators from getting payouts.
The method can be called by anybody (similar to Moonbeam's execute delegation decrease).
This is required, so that (for example), the manager can initiate a previously defaulted payment to reset delegatorNotPaid and disable forceScheduleRevoke.
The function can be called with multiple delegators for saving gas costs.
@param delegators The delegators to pay cover claims to. These are accumulated claims and could even be from multiple collators.
*/
function payOutCover(address payable[] calldata delegators) external nonReentrant {
require(!paused);
uint256 delegatorsLength = delegators.length;
for (uint256 i = 0; i < delegatorsLength;) {
address delegator = delegators[i];
require(delegator != address(0), "ZERO_ADDR");
uint256 toPay = payoutAmounts[delegator];
if (toPay == 0 || toPay < MIN_PAYOUT) {
emit DelegatorPayoutLessThanMinEvent(delegator);
unchecked {
++i;
}
continue;
}
// Check if contract has enough reducible balance (may be locked in staking)
if (address(this).balance < toPay) {
// only update if not already set
if (delegatorNotPaid == address(0)) {
delegatorNotPaid = delegator;
}
// will continue paying as many delegators as possible (smaller amounts owed) until drained
emit DelegatorNotPaidEvent(delegator, toPay);
unchecked {
++i;
}
continue;
}
// Reset delegatorNotPaid to 0 (if it is this delegator) as they can now get paid
if (delegatorNotPaid == delegator) {
delegatorNotPaid = address(0);
}
// delete payout entry from delegator
delete payoutAmounts[delegator];
// debit the cover owed
payoutsOwedTotal -= toPay;
totalPayouts[delegator] += toPay;
emit PayoutEvent(delegator, toPay);
(bool sent, ) = delegator.call{value: toPay}("");
require(sent, "TRANSF_FAIL");
unchecked {
++i;
}
}
}
/**
@dev Anybody can execute anybody's delegation request in Moonbeam/Moonriver, so this method is not required.
However, we include this method in case Moonbeam changes the execute permissions in the future.
@param candidate the collator that the contract is revoking from or decreasing its delegation to
*/
function executeDelegationRequest(
address candidate
) external {
staking.executeDelegationRequest(address(this), candidate);
}
/**
@notice Invoices active members and credits oracles
@dev Cover members are charged a fee every 84 rounds (1 week). Members can wave this fee by running an oracle.
The collected fees, from all non-oracle-running members, are equally split and credited to oracle-running members.
We directly credit the deposits of the oracle-running members, which means that the deposits of oracle-running members
will grow over time (assuming zero cover claims) while deposits of non-oracle-running members will decrease over time.
The purpose of the fee is to incentivize collators to run oracles, thereby increasing the security of the contract.
The manager will experiment with setting the fee to a value that incentivizes enough collators to run oracles,
without discouraging the collators that cannot run oracles from using the contract.
*/
function invoiceMembers() external {
uint128 eraNow = _getEra();
require(eraNow > membersInvoicedLastEra, "ALREADY_INVOICED");
// can only charge fee every 32 eras. We use 32 eras to match the uint32 oracle points bitmap.
// The bitmap bits get shifted on era nonces (not eras), and sometimes, twice on the same era nonce (and sometimes zero)
// However, on average, we expect most eras to last one era nonce (no claims). The idea is that, invoicing should
// decide if a member qualifies for a fee waiver by checking its oracle activity since roughly the last time invoicing ran
require(eraNow % 32 == 0, "ERA_INV");
require(memberFee != 0, "ZERO_FEE");
membersInvoicedLastEra = eraNow;
uint256 length = memberAddresses.length;
uint256 totalFee;
uint256 membersWithOracles; // a bitmap with 1's in the members indices of members that have oracle points getOraclePointBitmap(memberAddress) > 0
uint256 membersWithOraclesCount;
// debit non-oracle-running members
for(uint256 i; i < length;) {
address memberAddress = memberAddresses[i];
// If the collator is active AND it is not participating as an oracle, then charge it
if (members[memberAddress].active) {
if (IOracleMaster(ORACLE_MASTER).getOraclePointBitmap(memberAddress) == 0) { // this is a uint32
if ( members[memberAddress].deposit < memberFee) {
// by setting active = false, we force the collator to have to meet MIN_DEPOSIT again to reactivate (should be enough to cover memberFee)
// defaulted amounts are written off and not paid even if the member becomes active again
members[memberAddress].active = false;
members[memberAddress].wentInactiveEra = eraId;
members[memberAddress].defaultCount++;
emit MemberDefaultEvent(memberAddress);
emit MemberActiveStatusChanged(memberAddress, false);
unchecked {
++i;
}
continue;
}
members[memberAddress].deposit -= memberFee;
totalFee += memberFee;
emit MemberInvoicedEvent(memberAddress, memberFee, eraNow);
} else {
// Set the bitmap's bit to 1 so we don't have to call getOraclePointBitmap again in the next iteration for crediting oracles
membersWithOracles = membersWithOracles | (1 << i);
membersWithOraclesCount++;
}
}
unchecked {
++i;
}
}
// if there are zero non-racle-running members, return
if (totalFee == 0) {
return;
}
// if there are no oracle-running members, credit the manager
if (membersWithOraclesCount == 0) {
membersDepositTotal -= totalFee; // decrease the total members deposit
// the totalFee amount can now be withdrawn with withdrawRewards (C + D, in withdrawRewards, are decreased by totalFee)
} else {
// else, credit the oracle-running members
uint256 oraclePayment = totalFee / membersWithOraclesCount;
uint256 remainder = totalFee;
for(uint256 i; i < length;) {
if (membersWithOracles & (1 << i) != 0) {
address memberAddress = memberAddresses[i];
members[memberAddress].deposit += oraclePayment;
remainder -= oraclePayment;
emit OraclePaidEvent(memberAddress, oraclePayment, eraNow);
}
unchecked {
++i;
}
}
// if there is anything left (due to uneven division), credit the manager with the remainder
membersDepositTotal -= remainder;
}
}
/// ***************** MANAGEMENT FUNCTIONS *****************
/**
@dev Allows the manager to withdraw any contract balance that is above the total deposits balance.
This includes contract staking rewards or other funds that were sent to the contract outside the deposit function.
The method checks that the funds are over and above total deposits by keeping track of 4 values:
A) contract balance, that is the reducible balance visible to the EVM (this is maintained automatically)
B) totalStaked, that is the amount currently staked or pending decrease or revoke (maintained by the staking precompile))
C) membersDepositTotal, this is the total deposits of all members that have NOT been claimed; any pending deposit decreases (not executed) do not reduce total deposits
D) payoutsOwedTotal this are the member deposits that have been moved to the delegator payables account, i.e. funds owed to the delegators due to cover claims
The manager can then withdraw any amount < (A + B) - (C + D), i.e. any extra funds that now owed to delegators or members.
@param amount How much to withdraw
@param receiver Who to send the withdrawal to
*/
function withdrawRewards(
uint256 amount,
address payable receiver
) external auth(ROLE_MANAGER) {
// The contract must have enough non-locked funds
require(address(this).balance > amount, "NO_FUNDS");
require(
_getFreeBalance() - amount > membersDepositTotal + payoutsOwedTotal,
"NO_REWARDS"
);
(bool sent, ) = receiver.call{value: amount}("");
require(sent, "TRANSF_FAIL");
}
/**
@notice Manager can whitelist collators to allow them to deposit funds and activate cover
@dev The method saves a proxy account (representative account) and a collator account that the proxy represents.
Collators cannot authorize actions with their collator account for security reasons, i.e. they should not use their
collator account to interact with any smart contracts.
If noManualWhitelistingRequired is true, then collators will be able to authorize actions using a Gov proxy of their collator.
We say "WILL be able" because, currently, smart contracts cannot access the proxy precompile, so proxy authorization does not work.
If noManualWhitelistingRequired is false, then the address MUST be whitelisted to authorize actions on behalf of the collator.
A collator must be whitelisted to make a deposit and to start offering cover.
@param _member the collator address that this account will represent
@param proxyAccount the proxy or representative account that represents the collator; this is not to be confused with the Gov proxy
*/
function whitelist(
address _member,
address proxyAccount
) external auth(ROLE_MANAGER) {
require(whitelisted[_member] == address(0), "ALREADY_WHITELISTED");
whitelisted[_member] = proxyAccount;
emit WhitelistEvent(_member, proxyAccount);
}
function pause(bool _paused) external auth(ROLE_MANAGER) {
paused = _paused;
}
/**
@notice Set the minimum member deposit required
@dev if a collator has not deposited the minimum ammount, then its active flag is false. Inactive collators cannot execute several member methods.
@param _min_deposit the min total deposit that is needed to activate cover offering
*/
function setMinDeposit(uint256 _min_deposit) external auth(ROLE_MANAGER) {
MIN_DEPOSIT = _min_deposit;
}
function setRuntime2100() external auth(ROLE_MANAGER) {
runtime2100 = true;
}
/**
@notice Set the maximum total deposit. Member collators cannot deposit more than this total amount.
@dev Setting a max total deposit puts a limit to how many days worth of cover collators can provide to their delegators.
This is to avoid a never-ending competition of increasing cover offerings that don't offer real world value.
@param _max_deposit_total The max deposit allowed
*/
function setMaxDepositTotal(
uint256 _max_deposit_total
) external auth(ROLE_MANAGER) {
MAX_DEPOSIT_TOTAL = _max_deposit_total;
}
/**
@dev Manager can override the erasCovered value of a member temporarilly. This is temporary because the
value will be overwritten again in the next pushData or member deposit.
@param _erasCovered The decrease execution delay
*/
function setErasCovered(
uint128 _erasCovered,
address member
) external auth(ROLE_MANAGER) {
// Cannot set delay to longer than 3 months (12 rounds per day * 30 * 3)
require(_erasCovered <= 1080, "HIGH");
erasCovered[member] = _erasCovered;
}
/**
@dev Manager can "forgive" a member that has been blocked from participating due to many defaults
@param _defaultCount the new default count
@param _member the member to update its default count
*/
function setDefaultCount(uint128 _defaultCount, address _member) external auth(ROLE_MANAGER) {
// Canot increase the default count of a member
require(_defaultCount < members[_member].defaultCount, "INV_DEF_COUNT");
members[_member].defaultCount = _defaultCount;
}
/**
@notice Sets the cover refund (in Wei) given to delegators for every 1 ETH (MOVR) staked per round
@dev The cover contract offers the same cover for every token delegated, for all collators. This means that collators pay the same
amount per delegated token to their delegators. The more delegations a collator has, the more they will have to pay out.
The manager is responsible for setting the stake unit cover to a value based on the average APR and updaitng it from time to time.
If a collator is offering above average APR, then its delegators will be underpaid; and vice versa.
This should not be a problem as most collators tend to offer similar APRs with the exception of some whale-backed collators.
@param _stake_unit_cover the unit cover
*/
function setStakeUnitCover(
uint256 _stake_unit_cover
) external auth(ROLE_MANAGER) {
uint256 maxAPR = 30; // 30%
uint256 minRoundsPerDay = 1; // current is 12, but this might change
// Protect against nonsensical values of unit cover that could drain the account
// Currently the worst case scenario (maxed unit cover) is that delegators get 30% * 12 = 360% APR
require(
_stake_unit_cover <
(maxAPR * 1 ether) / (100 * 365 * minRoundsPerDay),
"HIGH"
);
require(_stake_unit_cover > 0, "ZERO");
STAKE_UNIT_COVER = _stake_unit_cover;
emit SetStakeUnitCoverEvent(_stake_unit_cover);
}
/**
@notice Sets the minimum amount that a delegator can claim from accumulated covers
@dev Delegation cover rewards can be very low if the delegaiton amount is low. This method allows to set a minimum
claimable cover so as to help delegators execute reasonable claim transactions.
@param _min_payout the min payout amount
*/
function setMinPayout(uint256 _min_payout) external auth(ROLE_MANAGER) {
// Protect delegators from having to wait forever to get paid due to an unresonable min payment
require(_min_payout <= 1 ether, "HIGH");
MIN_PAYOUT = _min_payout;
emit SetMinPayoutEvent(_min_payout);
}
/**
@notice When covers must be calculated and transfered to delegators, the respective collator can refund the oracle that pushedData the tx fees for that calculation
@dev Contarct transaciton fees are relatively low when there are no claims to compute. This means that, most of the time, oracles
can agree that there is nothing to do, for little gas. However, when a collators misses a round, the oracle that happens to complete the quorum,
has to pay for calculating all the delegator claims. This is a loop that can run up to 300 times, resulting to higher gas costs.
In those cases, the gas costs for the loop are refunded to the oracle by the collator that missed that round.
The refund is not transfered but rather, credited to the oracle's delegator cover account. The oracle can then request a payout.
@param _refundOracleGasPrice The gas price used to calculate the refund
*/
function setRefundOracleGasPrice(
uint256 _refundOracleGasPrice
) external auth(ROLE_MANAGER) {
require(_refundOracleGasPrice <= 10_000_000, "INV_PRICE"); // TODO change for Moonbeam
// for market values, check https://moonbeam-gasinfo.netlify.app/
refundOracleGasPrice = _refundOracleGasPrice;
emit SetRefundOracleGasPriceEvent(_refundOracleGasPrice);
}
/**
@notice Set how often forced revokes can take place
@dev forceScheduleRevoke can only be called on a limited frequency to avoid spamming the contract and liquidating all funds unecessarilly.
Anybody can call forceScheduleRevoke if the contract has defaults in making a payment to a member or delegator. Defaults are possible
because the contract may stake its funds, thereby reducing its reducible balance that is available to make payments. Forcing a revoke
will allow funds to be returned to the reducible balance after N rounds. Therefore, the manager should set ERAS_BETWEEN_FORCED_UNDELEGATION
to a value that is > N to allow for the unstaked funds to clear the defaults.
@param _eras_between_forced_undelegation the number of rounds that must pass to be able to call forceUndelegate again
*/
function setErasBetweenForcedUndelegations(
uint128 _eras_between_forced_undelegation
) external auth(ROLE_MANAGER) {
// Protect contract from multiple undelegations without allowing time to unbond
require(_eras_between_forced_undelegation <= 300, "HIGH");
ERAS_BETWEEN_FORCED_UNDELEGATION = _eras_between_forced_undelegation;
emit SetErasBetweenForcedUndelegationsEvent(_eras_between_forced_undelegation);
}
/**
@notice Set a limit to how much cover can be credited to delegators for a collator for one missed round
@dev Cover amounts are calculated based on the values provided by the oracle quorum. Therefore, a malicious quorum could direct funds
to addresses that are not entitled to any cover. This is unlikely as it requires a large number of collator orcales colluding, and that StakeBaby
fails to exercise its veto power on quorum reports. In any case, MAX_ERA_MEMBER_PAYOUT is a last-defense security measure, should everything else fail.
By placing a sensical limit to the max round payout, we can limit the damage done by limiting the rate of funds outflow and giving the
manager the opportunity to detect the situaiton and pause oracle reporting.
*/
function setMaxEraMemberPayout(uint256 _max_era_member_payout) external auth(ROLE_MANAGER) {
MAX_ERA_MEMBER_PAYOUT = _max_era_member_payout;
emit SetMaxEraMemberPayoutEvent(_max_era_member_payout);
}
function setMaxErasCovered(uint128 _max_eras_covered) external auth(ROLE_MANAGER) {
require(MAX_ERAS_COVERED > 1, "LOW");
MAX_ERAS_COVERED = _max_eras_covered;
emit SetMaxErasCoveredEvent(_max_eras_covered);
}
/**
@notice See deposit cover and whitelist for an explanation of noManualWhitelistingRequired
@param _noManualWhitelistingRequired true when we want to allow members to manage their accounts using a Gov proxy, false when we require manual whitelisting
*/
function setNoManualWhitelistingRequired(bool _noManualWhitelistingRequired) external auth(ROLE_MANAGER) {
noManualWhitelistingRequired = _noManualWhitelistingRequired;
emit SetNoManualWhitelistingRequiredEvent(_noManualWhitelistingRequired);
}
function setMemberFee(uint256 _memberFee) external auth(ROLE_MANAGER) {
// _memberFee should be well below 1 MOVR (per 32 rounds) but we set the max to 1 to allow for increases in round duration
require(_memberFee <= 1 ether, "FEE_INV");
memberFee = _memberFee;
emit SetMemberFeeEvent(_memberFee);
}
/// ***************** GETTERS *****************
function getMember(
address member
)
external
view
returns (
bool,
bool,
uint256,
uint256,
uint256,
uint128,
uint128,
uint128,
uint128,
uint128
)
{
Member memory m = members[member];
return (
m.isMember,
m.active,
m.deposit,
m.maxCoveredDelegation,
m.delegatorsReportedInEra,
m.lastPushedEra,
m.noZeroPtsCoverAfterEra,
m.noActiveSetCoverAfterEra,
m.wentInactiveEra,
m.wentActiveEra
);
}
function getScheduledDecrease(
address member
) external view returns (uint128, uint256) {
return (
scheduledDecreasesMap[member].era,
scheduledDecreasesMap[member].amount
);
}
function getMembersCount() external view returns(uint256) {
return memberAddresses.length;
}
/// ***************** FUNCTIONS CALLABLE ONLY BY OTHERS CONTRACTS *****************
/**
@notice The method is used by the oracle to push data into the contract and calculate potential cover claims.
@dev Cover claims are not transfered to delegators; instead, they are credited as sums to a mapping and can be transfered later in a separate tx.
Cover claims are credited only if the collator offers the appropriate type of cover for the occation. For example, a collator that
offers only active-set cover, will not credit its delegators if it has missed a rounf because its server was down.
The method also:
A) updates the coveredEras value for a collator, given the latest total backing.
B) sets the delegatoNotPaid flag if a default happens
C) credits a gas fee refund to the oracle if claim covers are clculated
@param _eraId The round number
@param _report The collator data, including authored block counts, delegators, etc.
*/
function pushData(
uint128 _eraId,
Types.OracleData calldata _report,
address _oracleCollator
) external onlyOracle {
// we allow reporting the same era more than once, because oracles may split the report to pieces if many collators miss rounds
// this is required because each pushData cannot handle more than 500 delegator payouts
require(_isLastCompletedEra(_eraId), "INV_ERA");
eraId = _eraId;
uint256 collatorsLength = _report.collators.length;
for (uint256 i = 0; i < collatorsLength;) {
uint256 startGas = gasleft();
Types.CollatorData calldata collatorData = _report.collators[i];
bool isNotCandidate = !staking.isCandidate(collatorData.collatorAccount);
Member memory member = members[collatorData.collatorAccount];
if (_eraId > member.lastPushedEra && !_report.finalize) {
// if this is the first report for this collator for this era, and it is not the last report
// then set delegatorsReportedInEra to the number of delegators submitted with this report
members[collatorData.collatorAccount].delegatorsReportedInEra = collatorData.topActiveDelegations.length;
} else if(_report.finalize) {
// else, if this is the last report for the collator/s, then reset
// this will allow the oracles to start afresh in the next era
members[collatorData.collatorAccount].delegatorsReportedInEra = 0;
} else {
// finally, if this is an eraNonce in-between the first and the last ones for this collator,
// then increment delegatorsReportedInEra by the number of delegators submitted
// Currently, oracles are set to submit up to 150 delegators on each report, and the max count of topActiveDelegators is 300, so this line does not run
members[collatorData.collatorAccount].delegatorsReportedInEra += collatorData.topActiveDelegations.length;
}
members[collatorData.collatorAccount].lastPushedEra = _eraId;
members[collatorData.collatorAccount].lastDelegationsTotal
= _getCandidateTotalCounted(collatorData.collatorAccount, collatorData.delegationsTotal, isNotCandidate);
if (!member.isMember || !member.active) {
unchecked {
++i;
}
continue; // not a member or not active
}
_updateErasCovered(
collatorData.collatorAccount,
collatorData.delegationsTotal
);
bool mustPay;
uint128 noActiveSetCoverAfterEra = member.noActiveSetCoverAfterEra;
if (
// check that member is offering active-set cover;
// if noActiveSetCoverAfterEra is a positive number, then the member will stop offering cover after noActiveSetCoverAfterEra + erasCovered
(noActiveSetCoverAfterEra == 0 || noActiveSetCoverAfterEra > _eraId) &&
// collator must not be in the active set
!collatorData.active
) {
// if collator is out of the active set
emit MemberNotInActiveSetEvent(collatorData.collatorAccount, _eraId);
mustPay = true;
}
uint128 noZeroPtsCoverAfterEra = member.noZeroPtsCoverAfterEra;
if (
// check that member is offering zero-points cover;
// if noZeroPtsCoverAfterEra is a positive number, then the member will stop offering cover after noZeroPtsCoverAfterEra + erasCovered
(noZeroPtsCoverAfterEra == 0 || noZeroPtsCoverAfterEra > _eraId) &&
// collator must be in the active set and have reported 0 points for this era
collatorData.active &&
collatorData.points == 0
) {
// if collator is in the active set but produced 0 blocks
emit MemberHasZeroPointsEvent(
collatorData.collatorAccount,
_eraId
);
mustPay = true;
}
if (!mustPay) {
unchecked {
++i;
}
continue;
}
// this loop may run for 300 times so it must be optimized
uint256 toPayTotal;
uint256 topActiveDelegationsLength = collatorData.topActiveDelegations.length;
for (
uint128 j = 0;
j < topActiveDelegationsLength;
) {
Types.DelegationsData calldata delegationData = collatorData
.topActiveDelegations[j];
uint256 delegationAmount = _getDelegationAmount(delegationData.ownerAccount, collatorData.collatorAccount, delegationData.amount, isNotCandidate);
uint256 toPay = delegationAmount > member.maxCoveredDelegation
? (STAKE_UNIT_COVER *
member.maxCoveredDelegation) / 1 ether
: (STAKE_UNIT_COVER * delegationAmount) / 1 ether; // also works for delegationAmount == 0
if (members[collatorData.collatorAccount].deposit < toPay) { // cannot use cached member, causedeposit is updated
// because delegations are sorted lowest-> highest, we know that we have paid as many delegators as possible before defaulting
members[collatorData.collatorAccount].active = false;
members[collatorData.collatorAccount].wentInactiveEra = _eraId;
members[collatorData.collatorAccount].defaultCount++;
// defaulted amounts are written off and not paid if the member becomes active again
emit MemberDefaultEvent(collatorData.collatorAccount);
emit MemberActiveStatusChanged(collatorData.collatorAccount, false);
break;
}
payoutAmounts[delegationData.ownerAccount] += toPay; // credit the delegator
members[collatorData.collatorAccount].deposit -= toPay; // debit the collator deposit
toPayTotal += toPay;
unchecked {
++j;
}
}
require(toPayTotal <= MAX_ERA_MEMBER_PAYOUT, "EXCEEDS_MAX_ERA_MEMBER_PAYOUT");
membersDepositTotal -= toPayTotal; // decrease the total members deposit
payoutsOwedTotal += toPayTotal; // current total (not paid out)
// Refund oracle for gas costs. Calculating cover claims for every delegator can get expensive for 300 delegators.
// Oracles pay some minor tx fees when they submit a report, but they get reimusrsed for the calculation of the claims when that happens.
// This is not only fair but also necessary because only 1 oracle will have to run pushData per eraNonce (the oracle that happens to be the Nth one in an N-quorum)
// If the oracle is not reimbursed, then there is an incentive to not be the Nth oracle to avoid the fee.
if (refundOracleGasPrice != 0 && _oracleCollator != address(0)) {
uint256 gasUsed = startGas - gasleft();
uint256 refund = gasUsed * refundOracleGasPrice;
if (members[collatorData.collatorAccount].deposit < refund) { // don't use cached member, because deposit may be modified
// by setting active= false, the member has to reach the MIN_DEPOSIT again to reactive which is more than enough to cover the refund
members[collatorData.collatorAccount].active = false;
members[collatorData.collatorAccount].wentInactiveEra = _eraId;
members[collatorData.collatorAccount].defaultCount++;
// defaulted amounts are written off and not paid if the member becomes active again
emit MemberDefaultEvent(collatorData.collatorAccount);
emit MemberActiveStatusChanged(collatorData.collatorAccount, false);
unchecked {
++i;
}
continue;
}
members[collatorData.collatorAccount].deposit -= refund;
members[_oracleCollator].deposit += refund;
// because we are only moving funds from one deposit to another, we don't need to update membersDepositTotal or payoutsOwedTotal
}
unchecked {
++i;
}
}
}
function delegate(
address candidate,
uint256 amount,
uint256 candidateDelegationCount,
uint256 delegatorDelegationCount
) external virtual onlyDepositStaking {
staking.delegate(
candidate,
amount,
candidateDelegationCount,
delegatorDelegationCount
);
}
function delegator_bond_more(
address candidate,
uint256 more
) external virtual onlyDepositStaking {
staking.delegatorBondMore(candidate, more);
}
function schedule_delegator_bond_less(