-
Notifications
You must be signed in to change notification settings - Fork 50
/
Copy pathFlowIDTableStaking.cdc
2005 lines (1586 loc) · 98.9 KB
/
FlowIDTableStaking.cdc
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
/*
FlowIDTableStaking
The Flow ID Table and Staking contract manages
node operators' and delegators' information
and Flow tokens that are staked as part of the Flow Protocol.
Nodes submit their stake to the public addNodeInfo function
during the staking auction phase.
This records their info and committed tokens. They also will get a Node
Object that they can use to stake, unstake, and withdraw rewards.
Each node has multiple token buckets that hold their tokens
based on their status: committed, staked, unstaking, unstaked, and rewarded.
Delegators can also register to delegate FLOW to a node operator
during the staking auction phase by using the registerNewDelegator() function.
They have the same token buckets that node operators do.
The Admin has the authority to remove node records,
refund insufficiently staked nodes, pay rewards,
and move tokens between buckets. These will happen once every epoch.
See additional staking documentation here: https://docs.onflow.org/staking/
*/
import FungibleToken from "FungibleToken"
import FlowToken from "FlowToken"
import Burner from "Burner"
import FlowFees from "FlowFees"
import Crypto
access(all) contract FlowIDTableStaking {
/// Epoch
access(all) event NewEpoch(totalStaked: UFix64, totalRewardPayout: UFix64, newEpochCounter: UInt64)
access(all) event EpochTotalRewardsPaid(total: UFix64, fromFees: UFix64, minted: UFix64, feesBurned: UFix64, epochCounterForRewards: UInt64)
/// Node
access(all) event NewNodeCreated(nodeID: String, role: UInt8, amountCommitted: UFix64)
access(all) event TokensCommitted(nodeID: String, amount: UFix64)
access(all) event TokensStaked(nodeID: String, amount: UFix64)
access(all) event NodeTokensRequestedToUnstake(nodeID: String, amount: UFix64)
access(all) event TokensUnstaking(nodeID: String, amount: UFix64)
access(all) event TokensUnstaked(nodeID: String, amount: UFix64)
access(all) event NodeRemovedAndRefunded(nodeID: String, amount: UFix64)
access(all) event RewardsPaid(nodeID: String, amount: UFix64, epochCounter: UInt64)
access(all) event UnstakedTokensWithdrawn(nodeID: String, amount: UFix64)
access(all) event RewardTokensWithdrawn(nodeID: String, amount: UFix64)
access(all) event NetworkingAddressUpdated(nodeID: String, newAddress: String)
access(all) event NodeWeightChanged(nodeID: String, newWeight: UInt64)
/// Delegator
access(all) event NewDelegatorCreated(nodeID: String, delegatorID: UInt32)
access(all) event DelegatorTokensCommitted(nodeID: String, delegatorID: UInt32, amount: UFix64)
access(all) event DelegatorTokensStaked(nodeID: String, delegatorID: UInt32, amount: UFix64)
access(all) event DelegatorTokensRequestedToUnstake(nodeID: String, delegatorID: UInt32, amount: UFix64)
access(all) event DelegatorTokensUnstaking(nodeID: String, delegatorID: UInt32, amount: UFix64)
access(all) event DelegatorTokensUnstaked(nodeID: String, delegatorID: UInt32, amount: UFix64)
access(all) event DelegatorRewardsPaid(nodeID: String, delegatorID: UInt32, amount: UFix64, epochCounter: UInt64)
access(all) event DelegatorUnstakedTokensWithdrawn(nodeID: String, delegatorID: UInt32, amount: UFix64)
access(all) event DelegatorRewardTokensWithdrawn(nodeID: String, delegatorID: UInt32, amount: UFix64)
/// Contract Fields
access(all) event NewDelegatorCutPercentage(newCutPercentage: UFix64)
access(all) event NewWeeklyPayout(newPayout: UFix64)
access(all) event NewStakingMinimums(newMinimums: {UInt8: UFix64})
access(all) event NewDelegatorStakingMinimum(newMinimum: UFix64)
/// Holds the identity table for all the nodes in the network.
/// Includes nodes that aren't actively participating
/// key = node ID
access(contract) var nodes: @{String: NodeRecord}
/// The minimum amount of tokens that each staker type has to stake
/// in order to be considered valid
/// Keys:
/// 1 - Collector Nodes
/// 2 - Consensus Nodes
/// 3 - Execution Nodes
/// 4 - Verification Nodes
/// 5 - Access Nodes
access(account) var minimumStakeRequired: {UInt8: UFix64}
/// The total amount of tokens that are staked for all the nodes
/// of each node type during the current epoch
access(account) var totalTokensStakedByNodeType: {UInt8: UFix64}
/// The total amount of tokens that will be paid as rewards duringt the current epoch
access(account) var epochTokenPayout: UFix64
/// The ratio of the weekly awards that each node type gets
/// NOTE: Currently is not used
access(contract) var rewardRatios: {UInt8: UFix64}
/// The percentage of rewards that every node operator takes from
/// the users that are delegating to it
access(account) var nodeDelegatingRewardCut: UFix64
/// Paths for storing staking resources
access(all) let NodeStakerStoragePath: StoragePath
access(all) let NodeStakerPublicPath: PublicPath
access(all) let StakingAdminStoragePath: StoragePath
access(all) let DelegatorStoragePath: StoragePath
/*********** ID Table and Staking Composite Type Definitions *************/
/// Contains information that is specific to a node in Flow
access(all) resource NodeRecord {
/// The unique ID of the node
/// Set when the node is created
access(all) let id: String
/// The type of node
access(all) var role: UInt8
access(all) var networkingAddress: String
access(all) var networkingKey: String
access(all) var stakingKey: String
/// TODO: Proof of Possession (PoP) of the staking private key
/// The total tokens that only this node currently has staked, not including delegators
/// This value must always be above the minimum requirement to stay staked or accept delegators
access(mapping Identity) var tokensStaked: @FlowToken.Vault
/// The tokens that this node has committed to stake for the next epoch.
/// Moves to the tokensStaked bucket at the end of an epoch
access(mapping Identity) var tokensCommitted: @FlowToken.Vault
/// The tokens that this node has unstaked from the previous epoch
/// Moves to the tokensUnstaked bucket at the end of an epoch.
access(mapping Identity) var tokensUnstaking: @FlowToken.Vault
/// Tokens that this node has unstaked and are able to withdraw whenever they want
access(mapping Identity) var tokensUnstaked: @FlowToken.Vault
/// Staking rewards are paid to this bucket
access(mapping Identity) var tokensRewarded: @FlowToken.Vault
/// List of delegators for this node operator
access(all) let delegators: @{UInt32: DelegatorRecord}
/// The incrementing ID used to register new delegators
access(all) var delegatorIDCounter: UInt32
/// The amount of tokens that this node has requested to unstake for the next epoch
access(all) var tokensRequestedToUnstake: UFix64
/// Weight as determined by the amount staked after the staking auction (currently always 100)
access(all) var initialWeight: UInt64
init(
id: String,
role: UInt8,
networkingAddress: String,
networkingKey: String,
stakingKey: String,
tokensCommitted: @{FungibleToken.Vault}
) {
pre {
id.length == 64: "Node ID length must be 32 bytes (64 hex characters)"
FlowIDTableStaking.isValidNodeID(id): "The node ID must have only numbers and lowercase hex characters"
FlowIDTableStaking.nodes[id] == nil: "The ID cannot already exist in the record"
role >= UInt8(1) && role <= UInt8(5): "The role must be 1, 2, 3, 4, or 5"
networkingAddress.length > 0 && networkingAddress.length <= 510: "The networkingAddress must be less than 510 characters"
networkingKey.length == 128: "The networkingKey length must be exactly 64 bytes (128 hex characters)"
stakingKey.length == 192: "The stakingKey length must be exactly 96 bytes (192 hex characters)"
!FlowIDTableStaking.getNetworkingAddressClaimed(address: networkingAddress): "The networkingAddress cannot have already been claimed"
!FlowIDTableStaking.getNetworkingKeyClaimed(key: networkingKey): "The networkingKey cannot have already been claimed"
!FlowIDTableStaking.getStakingKeyClaimed(key: stakingKey): "The stakingKey cannot have already been claimed"
}
let stakeKey = PublicKey(
publicKey: stakingKey.decodeHex(),
signatureAlgorithm: SignatureAlgorithm.BLS_BLS12_381
)
let netKey = PublicKey(
publicKey: networkingKey.decodeHex(),
signatureAlgorithm: SignatureAlgorithm.ECDSA_P256
)
// TODO: Verify the provided Proof of Possession of the staking private key
self.id = id
self.role = role
self.networkingAddress = networkingAddress
self.networkingKey = networkingKey
self.stakingKey = stakingKey
self.initialWeight = 0
self.delegators <- {}
self.delegatorIDCounter = 0
FlowIDTableStaking.updateClaimed(path: /storage/networkingAddressesClaimed, networkingAddress, claimed: true)
FlowIDTableStaking.updateClaimed(path: /storage/networkingKeysClaimed, networkingKey, claimed: true)
FlowIDTableStaking.updateClaimed(path: /storage/stakingKeysClaimed, stakingKey, claimed: true)
self.tokensCommitted <- tokensCommitted as! @FlowToken.Vault
self.tokensStaked <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()) as! @FlowToken.Vault
self.tokensUnstaking <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()) as! @FlowToken.Vault
self.tokensUnstaked <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()) as! @FlowToken.Vault
self.tokensRewarded <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()) as! @FlowToken.Vault
self.tokensRequestedToUnstake = 0.0
emit NewNodeCreated(nodeID: self.id, role: self.role, amountCommitted: self.tokensCommitted.balance)
}
/// Utility Function that checks a node's overall committed balance from its borrowed record
access(account) view fun nodeFullCommittedBalance(): UFix64 {
if (self.tokensCommitted.balance + self.tokensStaked.balance) < self.tokensRequestedToUnstake {
return 0.0
} else {
return self.tokensCommitted.balance + self.tokensStaked.balance - self.tokensRequestedToUnstake
}
}
/// borrow a reference to to one of the delegators for a node in the record
access(account) view fun borrowDelegatorRecord(_ delegatorID: UInt32): auth(FungibleToken.Withdraw) &DelegatorRecord {
pre {
self.delegators[delegatorID] != nil:
"Specified delegator ID does not exist in the record"
}
return (&self.delegators[delegatorID] as auth(FungibleToken.Withdraw) &DelegatorRecord?)!
}
/// Add a delegator to the node record
access(account) fun setDelegator(delegatorID: UInt32, delegator: @DelegatorRecord) {
self.delegators[delegatorID] <-! delegator
}
access(account) fun setDelegatorIDCounter(_ newCounter: UInt32) {
self.delegatorIDCounter = newCounter
}
access(account) fun setNetworkingAddress(_ newAddress: String) {
self.networkingAddress = newAddress
}
access(contract) fun setTokensRequestedToUnstake(_ newUnstakeRequest: UFix64) {
self.tokensRequestedToUnstake = newUnstakeRequest
}
access(contract) fun setWeight(_ newWeight: UInt64) {
self.initialWeight = newWeight
}
}
/// Struct to create to get read-only info about a node
access(all) struct NodeInfo {
access(all) let id: String
access(all) let role: UInt8
access(all) let networkingAddress: String
access(all) let networkingKey: String
access(all) let stakingKey: String
access(all) let tokensStaked: UFix64
access(all) let tokensCommitted: UFix64
access(all) let tokensUnstaking: UFix64
access(all) let tokensUnstaked: UFix64
access(all) let tokensRewarded: UFix64
/// list of delegator IDs for this node operator
access(all) let delegators: &[UInt32]
access(all) let delegatorIDCounter: UInt32
access(all) let tokensRequestedToUnstake: UFix64
access(all) let initialWeight: UInt64
view init(nodeID: String) {
let nodeRecord = FlowIDTableStaking.borrowNodeRecord(nodeID)
self.id = nodeRecord.id
self.role = nodeRecord.role
self.networkingAddress = nodeRecord.networkingAddress
self.networkingKey = nodeRecord.networkingKey
self.stakingKey = nodeRecord.stakingKey
self.tokensStaked = nodeRecord.tokensStaked.balance
self.tokensCommitted = nodeRecord.tokensCommitted.balance
self.tokensUnstaking = nodeRecord.tokensUnstaking.balance
self.tokensUnstaked = nodeRecord.tokensUnstaked.balance
self.tokensRewarded = nodeRecord.tokensRewarded.balance
self.delegators = nodeRecord.delegators.keys
self.delegatorIDCounter = nodeRecord.delegatorIDCounter
self.tokensRequestedToUnstake = nodeRecord.tokensRequestedToUnstake
self.initialWeight = nodeRecord.initialWeight
}
/// Derived Fields
access(all) view fun totalCommittedWithDelegators(): UFix64 {
let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.id)
var committedSum = self.totalCommittedWithoutDelegators()
var index = 0
while index < self.delegators.length {
let delegator = self.delegators[index]
index = index + 1
let delRecord = nodeRecord.borrowDelegatorRecord(delegator)
committedSum = committedSum + delRecord.delegatorFullCommittedBalance()
}
return committedSum
}
access(all) view fun totalCommittedWithoutDelegators(): UFix64 {
let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.id)
return nodeRecord.nodeFullCommittedBalance()
}
access(all) view fun totalStakedWithDelegators(): UFix64 {
let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.id)
var stakedSum = self.tokensStaked
var index = 0
while index < self.delegators.length {
let delegator = self.delegators[index]
index = index + 1
let delRecord = nodeRecord.borrowDelegatorRecord(delegator)
stakedSum = stakedSum + delRecord.tokensStaked.balance
}
return stakedSum
}
access(all) view fun totalTokensInRecord(): UFix64 {
return self.tokensStaked + self.tokensCommitted + self.tokensUnstaking + self.tokensUnstaked + self.tokensRewarded
}
}
/// Records the staking info associated with a delegator
/// This resource is stored in the NodeRecord object that is being delegated to
access(all) resource DelegatorRecord {
/// Tokens this delegator has committed for the next epoch
access(mapping Identity) var tokensCommitted: @FlowToken.Vault
/// Tokens this delegator has staked for the current epoch
access(mapping Identity) var tokensStaked: @FlowToken.Vault
/// Tokens this delegator has requested to unstake and is locked for the current epoch
access(mapping Identity) var tokensUnstaking: @FlowToken.Vault
/// Tokens this delegator has been rewarded and can withdraw
access(mapping Identity) let tokensRewarded: @FlowToken.Vault
/// Tokens that this delegator unstaked and can withdraw
access(mapping Identity) let tokensUnstaked: @FlowToken.Vault
/// Amount of tokens that the delegator has requested to unstake
access(all) var tokensRequestedToUnstake: UFix64
init() {
self.tokensCommitted <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()) as! @FlowToken.Vault
self.tokensStaked <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()) as! @FlowToken.Vault
self.tokensUnstaking <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()) as! @FlowToken.Vault
self.tokensRewarded <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()) as! @FlowToken.Vault
self.tokensUnstaked <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()) as! @FlowToken.Vault
self.tokensRequestedToUnstake = 0.0
}
/// Utility Function that checks a delegator's overall committed balance from its borrowed record
access(contract) view fun delegatorFullCommittedBalance(): UFix64 {
if (self.tokensCommitted.balance + self.tokensStaked.balance) < self.tokensRequestedToUnstake {
return 0.0
} else {
return self.tokensCommitted.balance + self.tokensStaked.balance - self.tokensRequestedToUnstake
}
}
access(contract) fun setTokensRequestedToUnstake(_ newUnstakeRequest: UFix64) {
self.tokensRequestedToUnstake = newUnstakeRequest
}
}
/// Struct that can be returned to show all the info about a delegator
access(all) struct DelegatorInfo {
access(all) let id: UInt32
access(all) let nodeID: String
access(all) let tokensCommitted: UFix64
access(all) let tokensStaked: UFix64
access(all) let tokensUnstaking: UFix64
access(all) let tokensRewarded: UFix64
access(all) let tokensUnstaked: UFix64
access(all) let tokensRequestedToUnstake: UFix64
view init(nodeID: String, delegatorID: UInt32) {
let nodeRecord = FlowIDTableStaking.borrowNodeRecord(nodeID)
let delegatorRecord = nodeRecord.borrowDelegatorRecord(delegatorID)
self.id = delegatorID
self.nodeID = nodeID
self.tokensCommitted = delegatorRecord.tokensCommitted.balance
self.tokensStaked = delegatorRecord.tokensStaked.balance
self.tokensUnstaking = delegatorRecord.tokensUnstaking.balance
self.tokensUnstaked = delegatorRecord.tokensUnstaked.balance
self.tokensRewarded = delegatorRecord.tokensRewarded.balance
self.tokensRequestedToUnstake = delegatorRecord.tokensRequestedToUnstake
}
access(all) view fun totalTokensInRecord(): UFix64 {
return self.tokensStaked + self.tokensCommitted + self.tokensUnstaking + self.tokensUnstaked + self.tokensRewarded
}
}
access(all) resource interface NodeStakerPublic {
access(all) let id: String
}
access(all) entitlement NodeOperator
/// Resource that the node operator controls for staking
access(all) resource NodeStaker: NodeStakerPublic {
/// Unique ID for the node operator
access(all) let id: String
init(id: String) {
self.id = id
}
/// Tells whether the node is a new node who currently is not participating with tokens staked
/// and has enough committed for the next epoch for its role
access(self) view fun isEligibleForCandidateNodeStatus(_ nodeRecord: &FlowIDTableStaking.NodeRecord): Bool {
let participantList = FlowIDTableStaking.account.storage.borrow<&{String: Bool}>(from: /storage/idTableCurrentList)!
if participantList[nodeRecord.id] == true {
return false
}
return nodeRecord.tokensStaked.balance == 0.0 &&
FlowIDTableStaking.isGreaterThanMinimumForRole(numTokens: nodeRecord.tokensCommitted.balance, role: nodeRecord.role)
}
/// Change the node's networking address to a new one
access(NodeOperator) fun updateNetworkingAddress(_ newAddress: String) {
pre {
FlowIDTableStaking.stakingEnabled(): "Cannot update networking address if the staking auction isn't in progress"
newAddress.length > 0 && newAddress.length <= 510: "The networkingAddress must be less than 510 characters"
!FlowIDTableStaking.getNetworkingAddressClaimed(address: newAddress): "The networkingAddress cannot have already been claimed"
}
// Borrow the node's record from the staking contract
let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.id)
FlowIDTableStaking.updateClaimed(path: /storage/networkingAddressesClaimed, nodeRecord.networkingAddress, claimed: false)
nodeRecord.setNetworkingAddress(newAddress)
FlowIDTableStaking.updateClaimed(path: /storage/networkingAddressesClaimed, newAddress, claimed: true)
emit NetworkingAddressUpdated(nodeID: self.id, newAddress: newAddress)
}
/// Add new tokens to the system to stake during the next epoch
access(NodeOperator) fun stakeNewTokens(_ tokens: @{FungibleToken.Vault}) {
pre {
FlowIDTableStaking.stakingEnabled(): "Cannot stake if the staking auction isn't in progress"
}
// Borrow the node's record from the staking contract
let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.id)
emit TokensCommitted(nodeID: nodeRecord.id, amount: tokens.balance)
// Add the new tokens to tokens committed
nodeRecord.tokensCommitted.deposit(from: <-tokens)
// Only add them as a candidate node if they don't already
// have tokens staked and are above the minimum
if self.isEligibleForCandidateNodeStatus(nodeRecord) {
FlowIDTableStaking.addToCandidateNodeList(nodeID: nodeRecord.id, roleToAdd: nodeRecord.role)
}
FlowIDTableStaking.modifyNewMovesPending(nodeID: self.id, delegatorID: nil, existingList: nil)
}
/// Stake tokens that are in the tokensUnstaked bucket
access(NodeOperator) fun stakeUnstakedTokens(amount: UFix64) {
pre {
FlowIDTableStaking.stakingEnabled(): "Cannot stake if the staking auction isn't in progress"
}
let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.id)
var remainingAmount = amount
// If there are any tokens that have been requested to unstake for the current epoch,
// cancel those first before staking new unstaked tokens
if remainingAmount <= nodeRecord.tokensRequestedToUnstake {
nodeRecord.setTokensRequestedToUnstake(nodeRecord.tokensRequestedToUnstake - remainingAmount)
remainingAmount = 0.0
} else if remainingAmount > nodeRecord.tokensRequestedToUnstake {
remainingAmount = remainingAmount - nodeRecord.tokensRequestedToUnstake
nodeRecord.setTokensRequestedToUnstake(0.0)
}
// Commit the remaining amount from the tokens unstaked bucket
nodeRecord.tokensCommitted.deposit(from: <-nodeRecord.tokensUnstaked.withdraw(amount: remainingAmount))
emit TokensCommitted(nodeID: nodeRecord.id, amount: remainingAmount)
// Only add them as a candidate node if they don't already
// have tokens staked and are above the minimum
if self.isEligibleForCandidateNodeStatus(nodeRecord) {
FlowIDTableStaking.addToCandidateNodeList(nodeID: nodeRecord.id, roleToAdd: nodeRecord.role)
}
FlowIDTableStaking.modifyNewMovesPending(nodeID: self.id, delegatorID: nil, existingList: nil)
}
/// Stake tokens that are in the tokensRewarded bucket
access(NodeOperator) fun stakeRewardedTokens(amount: UFix64) {
pre {
FlowIDTableStaking.stakingEnabled(): "Cannot stake if the staking auction isn't in progress"
}
let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.id)
nodeRecord.tokensCommitted.deposit(from: <-nodeRecord.tokensRewarded.withdraw(amount: amount))
emit TokensCommitted(nodeID: nodeRecord.id, amount: amount)
// Only add them as a candidate node if they don't already
// have tokens staked and are above the minimum
if self.isEligibleForCandidateNodeStatus(nodeRecord) {
FlowIDTableStaking.addToCandidateNodeList(nodeID: nodeRecord.id, roleToAdd: nodeRecord.role)
}
FlowIDTableStaking.modifyNewMovesPending(nodeID: self.id, delegatorID: nil, existingList: nil)
}
/// Request amount tokens to be removed from staking at the end of the next epoch
access(NodeOperator) fun requestUnstaking(amount: UFix64) {
pre {
FlowIDTableStaking.stakingEnabled(): "Cannot unstake if the staking auction isn't in progress"
}
let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.id)
// If the request is greater than the total number of tokens
// that can be unstaked, revert
assert (
nodeRecord.tokensStaked.balance +
nodeRecord.tokensCommitted.balance
>= amount + nodeRecord.tokensRequestedToUnstake,
message: "Not enough tokens to unstake!"
)
// Node operators who have delegators have to have enough of their own tokens staked
// to meet the minimum, without any contributions from delegators
assert (
nodeRecord.delegators.length == 0 ||
FlowIDTableStaking.isGreaterThanMinimumForRole(numTokens: FlowIDTableStaking.NodeInfo(nodeID: nodeRecord.id).totalCommittedWithoutDelegators() - amount, role: nodeRecord.role),
message: "Cannot unstake below the minimum if there are delegators"
)
let amountCommitted = nodeRecord.tokensCommitted.balance
// If the request can come from committed, withdraw from committed to unstaked
if amountCommitted >= amount {
// withdraw the requested tokens from committed since they have not been staked yet
nodeRecord.tokensUnstaked.deposit(from: <-nodeRecord.tokensCommitted.withdraw(amount: amount))
emit TokensUnstaked(nodeID: self.id, amount: amount)
} else {
let amountCommitted = nodeRecord.tokensCommitted.balance
// withdraw the requested tokens from committed since they have not been staked yet
nodeRecord.tokensUnstaked.deposit(from: <-nodeRecord.tokensCommitted.withdraw(amount: amountCommitted))
// update request to show that leftover amount is requested to be unstaked
nodeRecord.setTokensRequestedToUnstake(nodeRecord.tokensRequestedToUnstake + (amount - amountCommitted))
FlowIDTableStaking.modifyNewMovesPending(nodeID: self.id, delegatorID: nil, existingList: nil)
emit TokensUnstaked(nodeID: self.id, amount: amountCommitted)
emit NodeTokensRequestedToUnstake(nodeID: self.id, amount: nodeRecord.tokensRequestedToUnstake)
}
// Remove the node as a candidate node if they were one before but aren't now
if !self.isEligibleForCandidateNodeStatus(nodeRecord) {
FlowIDTableStaking.removeFromCandidateNodeList(nodeID: self.id, role: nodeRecord.role)
}
}
/// Requests to unstake all of the node operators staked and committed tokens
/// as well as all the staked and committed tokens of all of their delegators
access(NodeOperator) fun unstakeAll() {
pre {
FlowIDTableStaking.stakingEnabled(): "Cannot unstake if the staking auction isn't in progress"
}
let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.id)
if nodeRecord.tokensCommitted.balance > 0.0 {
emit TokensUnstaked(nodeID: self.id, amount: nodeRecord.tokensCommitted.balance)
/// if the request can come from committed, withdraw from committed to unstaked
/// withdraw the requested tokens from committed since they have not been staked yet
nodeRecord.tokensUnstaked.deposit(from: <-nodeRecord.tokensCommitted.withdraw(amount: nodeRecord.tokensCommitted.balance))
}
if nodeRecord.tokensStaked.balance > 0.0 {
/// update request to show that leftover amount is requested to be unstaked
nodeRecord.setTokensRequestedToUnstake(nodeRecord.tokensStaked.balance)
FlowIDTableStaking.modifyNewMovesPending(nodeID: self.id, delegatorID: nil, existingList: nil)
emit NodeTokensRequestedToUnstake(nodeID: self.id, amount: nodeRecord.tokensRequestedToUnstake)
}
FlowIDTableStaking.removeFromCandidateNodeList(nodeID: self.id, role: nodeRecord.role)
}
/// Withdraw tokens from the unstaked bucket
access(NodeOperator) fun withdrawUnstakedTokens(amount: UFix64): @{FungibleToken.Vault} {
let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.id)
emit UnstakedTokensWithdrawn(nodeID: nodeRecord.id, amount: amount)
return <- nodeRecord.tokensUnstaked.withdraw(amount: amount)
}
/// Withdraw tokens from the rewarded bucket
access(NodeOperator) fun withdrawRewardedTokens(amount: UFix64): @{FungibleToken.Vault} {
let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.id)
emit RewardTokensWithdrawn(nodeID: nodeRecord.id, amount: amount)
return <- nodeRecord.tokensRewarded.withdraw(amount: amount)
}
}
/// Public interface to query information about a delegator
/// from the account it is stored in
access(all) resource interface NodeDelegatorPublic {
access(all) let id: UInt32
access(all) let nodeID: String
}
access(all) entitlement DelegatorOwner
/// Resource object that the delegator stores in their account to perform staking actions
access(all) resource NodeDelegator: NodeDelegatorPublic {
access(all) let id: UInt32
access(all) let nodeID: String
init(id: UInt32, nodeID: String) {
self.id = id
self.nodeID = nodeID
}
/// Delegate new tokens to the node operator
access(DelegatorOwner) fun delegateNewTokens(from: @{FungibleToken.Vault}) {
pre {
FlowIDTableStaking.stakingEnabled(): "Cannot delegate if the staking auction isn't in progress"
}
// borrow the node record of the node in order to get the delegator record
let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.nodeID)
let delRecord = nodeRecord.borrowDelegatorRecord(self.id)
emit DelegatorTokensCommitted(nodeID: self.nodeID, delegatorID: self.id, amount: from.balance)
// Commit the new tokens to the delegator record
delRecord.tokensCommitted.deposit(from: <-from)
FlowIDTableStaking.modifyNewMovesPending(nodeID: self.nodeID, delegatorID: self.id, existingList: nil)
}
/// Delegate tokens from the unstaked bucket to the node operator
access(DelegatorOwner) fun delegateUnstakedTokens(amount: UFix64) {
pre {
FlowIDTableStaking.stakingEnabled(): "Cannot delegate if the staking auction isn't in progress"
}
let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.nodeID)
let delRecord = nodeRecord.borrowDelegatorRecord(self.id)
var remainingAmount = amount
// If there are any tokens that have been requested to unstake for the current epoch,
// cancel those first before staking new unstaked tokens
if remainingAmount <= delRecord.tokensRequestedToUnstake {
delRecord.setTokensRequestedToUnstake(delRecord.tokensRequestedToUnstake - remainingAmount)
remainingAmount = 0.0
} else if remainingAmount > delRecord.tokensRequestedToUnstake {
remainingAmount = remainingAmount - delRecord.tokensRequestedToUnstake
delRecord.setTokensRequestedToUnstake(0.0)
}
// Commit the remaining unstaked tokens
delRecord.tokensCommitted.deposit(from: <-delRecord.tokensUnstaked.withdraw(amount: remainingAmount))
emit DelegatorTokensCommitted(nodeID: self.nodeID, delegatorID: self.id, amount: amount)
FlowIDTableStaking.modifyNewMovesPending(nodeID: self.nodeID, delegatorID: self.id, existingList: nil)
}
/// Delegate tokens from the rewards bucket to the node operator
access(DelegatorOwner) fun delegateRewardedTokens(amount: UFix64) {
pre {
FlowIDTableStaking.stakingEnabled(): "Cannot delegate if the staking auction isn't in progress"
}
let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.nodeID)
let delRecord = nodeRecord.borrowDelegatorRecord(self.id)
delRecord.tokensCommitted.deposit(from: <-delRecord.tokensRewarded.withdraw(amount: amount))
emit DelegatorTokensCommitted(nodeID: self.nodeID, delegatorID: self.id, amount: amount)
FlowIDTableStaking.modifyNewMovesPending(nodeID: self.nodeID, delegatorID: self.id, existingList: nil)
}
/// Request to unstake delegated tokens during the next epoch
access(DelegatorOwner) fun requestUnstaking(amount: UFix64) {
pre {
FlowIDTableStaking.stakingEnabled(): "Cannot request unstaking if the staking auction isn't in progress"
}
let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.nodeID)
let delRecord = nodeRecord.borrowDelegatorRecord(self.id)
// The delegator must have enough tokens to unstake
assert (
delRecord.tokensStaked.balance +
delRecord.tokensCommitted.balance
>= amount + delRecord.tokensRequestedToUnstake,
message: "Not enough tokens to unstake!"
)
// if the request can come from committed, withdraw from committed to unstaked
if delRecord.tokensCommitted.balance >= amount {
// withdraw the requested tokens from committed since they have not been staked yet
delRecord.tokensUnstaked.deposit(from: <-delRecord.tokensCommitted.withdraw(amount: amount))
emit DelegatorTokensUnstaked(nodeID: self.nodeID, delegatorID: self.id, amount: amount)
} else {
/// Get the balance of the tokens that are currently committed
let amountCommitted = delRecord.tokensCommitted.balance
if amountCommitted > 0.0 {
delRecord.tokensUnstaked.deposit(from: <-delRecord.tokensCommitted.withdraw(amount: amountCommitted))
}
/// update request to show that leftover amount is requested to be unstaked
delRecord.setTokensRequestedToUnstake(delRecord.tokensRequestedToUnstake + (amount - amountCommitted))
FlowIDTableStaking.modifyNewMovesPending(nodeID: self.nodeID, delegatorID: self.id, existingList: nil)
emit DelegatorTokensUnstaked(nodeID: self.nodeID, delegatorID: self.id, amount: amountCommitted)
emit DelegatorTokensRequestedToUnstake(nodeID: self.nodeID, delegatorID: self.id, amount: delRecord.tokensRequestedToUnstake)
}
}
/// Withdraw tokens from the unstaked bucket
access(DelegatorOwner) fun withdrawUnstakedTokens(amount: UFix64): @{FungibleToken.Vault} {
let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.nodeID)
let delRecord = nodeRecord.borrowDelegatorRecord(self.id)
emit DelegatorUnstakedTokensWithdrawn(nodeID: nodeRecord.id, delegatorID: self.id, amount: amount)
return <- delRecord.tokensUnstaked.withdraw(amount: amount)
}
/// Withdraw tokens from the rewarded bucket
access(DelegatorOwner) fun withdrawRewardedTokens(amount: UFix64): @{FungibleToken.Vault} {
let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.nodeID)
let delRecord = nodeRecord.borrowDelegatorRecord(self.id)
emit DelegatorRewardTokensWithdrawn(nodeID: nodeRecord.id, delegatorID: self.id, amount: amount)
return <- delRecord.tokensRewarded.withdraw(amount: amount)
}
}
/// Includes all the rewards breakdowns for all the nodes and delegators for a specific epoch
/// as well as the total amount of tokens to be minted for rewards
access(all) struct EpochRewardsSummary {
access(all) let totalRewards: UFix64
access(all) let breakdown: [RewardsBreakdown]
view init(totalRewards: UFix64, breakdown: [RewardsBreakdown]) {
self.totalRewards = totalRewards
self.breakdown = breakdown
}
}
/// Details the rewards breakdown for an individual node and its delegators
access(all) struct RewardsBreakdown {
access(all) let nodeID: String
access(all) var nodeRewards: UFix64
access(all) let delegatorRewards: {UInt32: UFix64}
view init(nodeID: String) {
self.nodeID = nodeID
self.nodeRewards = 0.0
self.delegatorRewards = {}
}
access(all) fun setNodeRewards(_ rewards: UFix64) {
self.nodeRewards = rewards
}
/// Scale the rewards of a single delegator by a scaling factor
access(all) fun scaleDelegatorRewards(delegatorID: UInt32, scalingFactor: UFix64) {
if let reward = self.delegatorRewards[delegatorID] {
self.delegatorRewards[delegatorID] = reward * scalingFactor
}
}
access(all) fun scaleOperatorRewards(scalingFactor: UFix64) {
self.nodeRewards = self.nodeRewards * scalingFactor
}
/// Scale the rewards of all the stakers in the record
access(all) fun scaleAllRewards(scalingFactor: UFix64) {
self.scaleOperatorRewards(scalingFactor: scalingFactor)
for id in self.delegatorRewards.keys {
self.scaleDelegatorRewards(delegatorID: id, scalingFactor: scalingFactor)
}
}
/// Sets the reward amount for a specific delegator of this node
access(all) fun setDelegatorReward(delegatorID: UInt32, rewards: UFix64) {
self.delegatorRewards[delegatorID] = rewards
}
}
/// Interface that only contains operations that are part
/// of the regular automated functioning of the epoch process
/// These are accessed by the `FlowEpoch` contract through a capability
access(all) resource interface EpochOperations {
access(all) fun setEpochTokenPayout(_ newPayout: UFix64)
access(all) fun setSlotLimits(slotLimits: {UInt8: UInt16})
access(all) fun setNodeWeight(nodeID: String, weight: UInt64)
access(all) fun startStakingAuction()
access(all) fun endStakingAuction(): [String]
access(all) fun payRewards(forEpochCounter: UInt64, rewardsSummary: EpochRewardsSummary)
access(all) fun calculateRewards(): EpochRewardsSummary
access(all) fun moveTokens(newEpochCounter: UInt64)
}
access(all) resource Admin: EpochOperations {
/// Sets a new set of minimum staking requirements for all the nodes
/// Nodes' indexes are their role numbers
access(all) fun setMinimumStakeRequirements(_ newRequirements: {UInt8: UFix64}) {
pre {
newRequirements.keys.length == 5:
"There must be six entries for node minimum stake requirements"
}
FlowIDTableStaking.minimumStakeRequired = newRequirements
emit NewStakingMinimums(newMinimums: newRequirements)
}
/// Sets a new set of minimum staking requirements for all the delegators
access(all) fun setDelegatorMinimumStakeRequirement(_ newRequirement: UFix64) {
FlowIDTableStaking.account.storage.load<UFix64>(from: /storage/delegatorStakingMinimum)
FlowIDTableStaking.account.storage.save(newRequirement, to: /storage/delegatorStakingMinimum)
emit NewDelegatorStakingMinimum(newMinimum: newRequirement)
}
/// Changes the total weekly payout to a new value
access(all) fun setEpochTokenPayout(_ newPayout: UFix64) {
if newPayout != FlowIDTableStaking.epochTokenPayout {
emit NewWeeklyPayout(newPayout: newPayout)
}
FlowIDTableStaking.epochTokenPayout = newPayout
}
/// Sets a new delegator cut percentage that nodes take from delegator rewards
access(all) fun setCutPercentage(_ newCutPercentage: UFix64) {
pre {
newCutPercentage > 0.0 && newCutPercentage < 1.0:
"Cut percentage must be between 0 and 1!"
}
if newCutPercentage != FlowIDTableStaking.nodeDelegatingRewardCut {
emit NewDelegatorCutPercentage(newCutPercentage: newCutPercentage)
}
FlowIDTableStaking.nodeDelegatingRewardCut = newCutPercentage
}
/// Sets new limits to the number of candidate nodes for an epoch
access(all) fun setCandidateNodeLimit(role: UInt8, newLimit: UInt64) {
pre {
role >= UInt8(1) && role <= UInt8(5): "The role must be 1, 2, 3, 4, or 5"
}
let candidateNodeLimits = FlowIDTableStaking.account.storage.load<{UInt8: UInt64}>(from: /storage/idTableCandidateNodeLimits)!
candidateNodeLimits[role] = newLimit
FlowIDTableStaking.account.storage.save<{UInt8: UInt64}>(candidateNodeLimits, to: /storage/idTableCandidateNodeLimits)
}
/// Set slot (count) limits for each node role
/// The slot limit limits the number of participant nodes with the given role which may be added to the network.
/// It only prevents candidate nodes from joining. It does not cause existing participant nodes to unstake,
/// even if the number of participant nodes exceeds the slot limit.
access(all) fun setSlotLimits(slotLimits: {UInt8: UInt16}) {
pre {
slotLimits.keys.length == 5: "Slot Limits Dictionary can only have 5 entries"
slotLimits[1] != nil: "Need to have a limit set for collector nodes"
slotLimits[2] != nil: "Need to have a limit set for consensus nodes"
slotLimits[3] != nil: "Need to have a limit set for execution nodes"
slotLimits[4] != nil: "Need to have a limit set for verification nodes"
slotLimits[5] != nil: "Need to have a limit set for access nodes"
}
FlowIDTableStaking.account.storage.load<{UInt8: UInt16}>(from: /storage/flowStakingSlotLimits)
FlowIDTableStaking.account.storage.save(slotLimits, to: /storage/flowStakingSlotLimits)
}
/// Sets the number of open node slots to allow per epoch
/// Only access nodes are used for this currently,
/// but other node types will be added in the future
access(all) fun setOpenNodeSlots(openSlots: {UInt8: UInt16}) {
pre {
openSlots[5] != nil: "Need to have a value set for access nodes"
}
FlowIDTableStaking.account.storage.load<{UInt8: UInt16}>(from: /storage/flowStakingOpenNodeSlots)
FlowIDTableStaking.account.storage.save(openSlots, to: /storage/flowStakingOpenNodeSlots)
}
/// Sets a list of node IDs who will not receive rewards for the current epoch
/// This is used during epochs to punish nodes who have poor uptime
/// or who do not update to latest node software quickly enough
/// The parameter is a dictionary mapping node IDs
/// to a percentage, which is the percentage of their expected rewards that
/// they will receive instead of the full amount
access(all) fun setNonOperationalNodesList(_ nodeIDs: {String: UFix64}) {
for percentage in nodeIDs.values {
assert(
percentage >= 0.0 && percentage < 1.0,
message: "Percentage value to decrease rewards payout should be between 0 and 1"
)
}
FlowIDTableStaking.account.storage.load<{String: UFix64}>(from: /storage/idTableNonOperationalNodesList)
FlowIDTableStaking.account.storage.save<{String: UFix64}>(nodeIDs, to: /storage/idTableNonOperationalNodesList)
}
/// Allows the protocol to set a specific weight for a node
/// if their staked amount changes or if they are removed
access(all) fun setNodeWeight(nodeID: String, weight: UInt64) {
if weight > 100 {
panic("Specified node weight out of range.")
}
let nodeRecord = FlowIDTableStaking.borrowNodeRecord(nodeID)
nodeRecord.setWeight(weight)
emit NodeWeightChanged(nodeID: nodeID, newWeight: weight)
}
/// Sets a list of approved node IDs for the next epoch
/// Nodes not on this list will be unstaked at the end of the staking auction
/// and not considered to be a proposed/staked node
access(all) fun setApprovedList(_ newApproveList: {String: Bool}) {
let currentApproveList = FlowIDTableStaking.getApprovedList()
?? panic("Could not load approve list from storage")
for id in newApproveList.keys {
if FlowIDTableStaking.nodes[id] == nil {
panic("Approved node ".concat(id).concat(" does not already exist in the identity table"))
}
}
// If one of the nodes has been removed from the approve list
// it need to be set as movesPending so it will be caught in the `removeInvalidNodes` method
// If this happens not during the staking auction, the node should be removed and marked to unstake immediately
for id in currentApproveList.keys {
if newApproveList[id] == nil {
if FlowIDTableStaking.stakingEnabled() {
FlowIDTableStaking.modifyNewMovesPending(nodeID: id, delegatorID: nil, existingList: nil)
} else {
self.unsafeRemoveAndRefundNodeRecord(id)
}
}
}
self.unsafeSetApprovedList(newApproveList)
}
/// Sets the approved list without validating it (requires caller to validate)
access(self) fun unsafeSetApprovedList(_ newApproveList: {String: Bool}) {
let currentApproveList = FlowIDTableStaking.account.storage.load<{String: Bool}>(from: /storage/idTableApproveList)
?? panic("Could not load the current approve list from storage")
FlowIDTableStaking.account.storage.save<{String: Bool}>(newApproveList, to: /storage/idTableApproveList)
}
/// Removes and refunds the node record without also removing them from the approved-list
access(self) fun unsafeRemoveAndRefundNodeRecord(_ nodeID: String) {
let nodeRecord = FlowIDTableStaking.borrowNodeRecord(nodeID)
emit NodeRemovedAndRefunded(nodeID: nodeRecord.id, amount: nodeRecord.tokensCommitted.balance + nodeRecord.tokensStaked.balance)
// move their committed tokens back to their unstaked tokens
nodeRecord.tokensUnstaked.deposit(from: <-nodeRecord.tokensCommitted.withdraw(amount: nodeRecord.tokensCommitted.balance))