Smart Contract
DelegatorManager
A.d6f80565193ad727.DelegatorManager
1/**
2
3# Staking delegators management for liquid staking
4
5# Author: Increment Labs
6
7*/
8
9import FlowToken from 0x1654653399040a61
10import FungibleToken from 0xf233dcee88fe0abe
11
12import FlowIDTableStaking from 0x8624b52f9ddcd04a
13import FlowEpoch from 0x8624b52f9ddcd04a
14
15import stFlowToken from 0xd6f80565193ad727
16import LiquidStakingConfig from 0xd6f80565193ad727
17import LiquidStakingError from 0xd6f80565193ad727
18
19access(all) contract DelegatorManager {
20 /// All delegators managed by liquid staking protocol
21 /// {delegator uuid -> NodeDelegator}
22 access(self) let allDelegators: @{UInt64: FlowIDTableStaking.NodeDelegator}
23
24 /// Weight list of nodes eligible to receive delegates, used by the delegation strategy
25 /// {approvedNodeID -> weight}
26 access(self) let approvedNodeIDList: {String: UFix64}
27
28 /// Resource ids of delegator objects on approved nodes
29 /// {approvedNodeID -> delegator uuid}
30 access(self) let approvedDelegatorIDs: {String: UInt64}
31
32 /// Resource ids of migrated delegators of nodes
33 /// nodeIDs might be outside of approvedNodeIDList, as these are migrated delegators.
34 /// {nodeID -> {delegatorID -> uuid}}
35 access(self) let migratedDelegatorIDs: {String: {UInt32: UInt64}}
36
37 /// Epoch number of the latest liquid staking round
38 /// When a new flowchain epoch starts, a new liquid staking round won't start until all rewards
39 /// are collected and stFlow price is updated accordingly.
40 access(all) var quoteEpochCounter: UInt64
41
42 /// The default node ID for staking
43 /// All newly committed tokens will be temporarily delegated to this node by default
44 /// Delegation strategy will distribute these committed tokens to other nodes' before end of staking period
45 access(all) var defaultNodeIDToStake: String
46
47 /// All unstaking requests are temporarily cached here.
48 /// Strategy bots will handle cached unstaking requests before staking auction end
49 access(all) var requestedToUnstake: UFix64
50
51 /// Collect and aggregate all rewards & unstaked tokens from all delegators at the beginning of each epoch
52 access(self) let totalRewardedVault: @FlowToken.Vault
53 access(self) let totalUnstakedVault: @FlowToken.Vault
54
55 /// All epoch snapshot history
56 /// {epoch index -> snapshot}
57 access(self) let epochSnapshotHistory: {UInt64: EpochSnapshot}
58
59 /// Vault of protocol fees
60 access(self) let protocolFeeVault: @FlowToken.Vault
61
62 /// Paths
63 access(all) let adminPath: StoragePath
64 access(all) let delegationStrategyPath: StoragePath
65
66 /// Events
67 access(all) event NewQuoteEpoch(epoch: UInt64)
68 access(all) event RegisterNewDelegator(nodeID: String, delegatorID: UInt32)
69 access(all) event RewardsInfoCheckpointed(currEpoch: UInt64, received: UFix64, estimated: UFix64, estimateNextReward: UFix64)
70 access(all) event DepositProtocolFees(amount: UFix64, purpose: String)
71 access(all) event ExternalRewardsContributed(amount: UFix64)
72 access(all) event RestakeCanceledTokens(amount: UFix64, type: String, fromProtocolEpoch: UInt64)
73 access(all) event RedelegateTokens(amount: UFix64, currEpoch: UInt64)
74 access(all) event CompoundReward(rewardAmount: UFix64, protocolEpoch: UInt64)
75 access(all) event DelegatorsCollected(startIndex: Int, endIndex: Int, uncollectedCount: Int)
76 access(all) event StrategyTransferCommittedTokens(from: String, to: String, amount: UFix64)
77 access(all) event StrategyProcessUnstakeRequest(amount: UFix64, nodeIDToUnstake: String, delegatorIDToUnstake: UInt32, leftoverAmount: UFix64)
78 access(all) event DelegatorRemoved(nodeID: String, delegatorID: UInt32, uuid: UInt64)
79 access(all) event SetApprovedNodeList(nodeIDs: {String: UFix64}, defaultNodeIDToStake: String)
80 access(all) event UpsertApprovedNode(nodeID: String, oldWeight: UFix64?, newWeight: UFix64)
81 access(all) event SetDefaultStakeNode(from: String, to: String)
82 access(all) event ApprovedNodeCanceled(nodeID: String)
83 access(all) event ApprovedNodeRemoved(nodeID: String)
84 access(all) event RedelegateRequested(nodeID: String, delegatorID: UInt32, redelegateCommittedAmount: UFix64, redelegateRequestToUnstake: UFix64)
85
86 /// Reserved parameter fields: {ParamName: Value}
87 access(self) let _reservedFields: {String: AnyStruct}
88
89 /// Snapshot data of a historical Flow network Epoch
90 ///
91 /// Strategy bots will collect rewards from all managed delegators (up to 50,000) when a new flow chain epoch starts,
92 /// and then update the new stFlow token price for the next round of liquid staking protocol
93 access(all) struct EpochSnapshot {
94 // Snapshotted protocol epoch
95 access(all) let epochCounter: UInt64
96
97 /// Price: stFlow to Flow (>= 1.0)
98 access(all) var scaledQuoteStFlowFlow: UInt256
99 /// Price: Flow to stFlow (<= 1.0)
100 access(all) var scaledQuoteFlowStFlow: UInt256
101
102 /// Total staked amount of all delegators
103 access(all) var allDelegatorStaked: UFix64
104 /// Total committed amount of all delegators
105 access(all) var allDelegatorCommitted: UFix64
106 /// Total requests to unstake of all delegators
107 access(all) var allDelegatorRequestedToUnstake: UFix64
108
109 /// After-fee total rewards received of current protocol epoch
110 access(all) var receivedReward: UFix64
111 /// Estimated rewards to be received
112 access(all) var estimatedReward: UFix64
113
114 /// Snapshotted delegator infos
115 /// {nodeID -> {delegatorID -> DelegatorInfo}}
116 access(self) let delegatorInfoDict: {String: {UInt32: FlowIDTableStaking.DelegatorInfo}}
117
118 /// { delegator uuid -> collected? }
119 access(self) let delegatorCollected: {UInt64: Bool}
120
121 /// Canceled committed tokens of protocol epoch N is checkpointed in DelegatorManager.epochSnapshotHistory[N+1]
122 /// Restake protocol epoch N's canceledCommittedTokens before advancing into protocol epoch N+1
123 access(all) var canceledCommittedTokens: UFix64
124 /// Canceled staked tokens of protocol epoch N is checkpointed in DelegatorManager.epochSnapshotHistory[N+1]
125 /// Restake protocol epoch N-1's canceledStakedTokens before advancing into next protocol
126 access(all) var canceledStakedTokens: UFix64
127
128 /// Tokens that are requested to unstake, will be in unstaking mode in the next protocol epoch
129 access(all) var redelegatedTokensRequestToUnstake: UFix64
130 /// Tokens in unstaking mode, will be turned to unstaked (and available to be restaked) in the next protocol epoch
131 access(all) var redelegatedTokensUnderUnstaking: UFix64
132
133 /// Start time of new quote epoch
134 access(all) var quoteEpochStartTimestamp: UFix64
135 access(all) var quoteEpochStartBlockHeight: UInt64
136 access(all) var quoteEpochStartBlockView: UInt64
137
138 /// Update or insert the snapshotted delegator info
139 access(contract) fun upsertDelegatorInfo(nodeID: String, delegatorID: UInt32) {
140 let delegatorInfo = FlowIDTableStaking.DelegatorInfo(nodeID: nodeID, delegatorID: delegatorID)
141
142 if self.delegatorInfoDict.containsKey(nodeID) == false {
143 self.delegatorInfoDict[nodeID] = {}
144 } else {
145 if self.delegatorInfoDict[nodeID]!.containsKey(delegatorID) == true {
146 self.allDelegatorStaked = self.allDelegatorStaked - self.delegatorInfoDict[nodeID]![delegatorID]!.tokensStaked
147 self.allDelegatorCommitted = self.allDelegatorCommitted - self.delegatorInfoDict[nodeID]![delegatorID]!.tokensCommitted
148 self.allDelegatorRequestedToUnstake = self.allDelegatorRequestedToUnstake - self.delegatorInfoDict[nodeID]![delegatorID]!.tokensRequestedToUnstake
149 }
150 }
151
152 self.allDelegatorStaked = self.allDelegatorStaked + delegatorInfo.tokensStaked
153 self.allDelegatorCommitted = self.allDelegatorCommitted + delegatorInfo.tokensCommitted
154 self.allDelegatorRequestedToUnstake = self.allDelegatorRequestedToUnstake + delegatorInfo.tokensRequestedToUnstake
155
156 self.delegatorInfoDict[nodeID]!.insert(key: delegatorID, delegatorInfo)
157 }
158
159 /// Delegator count that has been collected
160 access(all) view fun getCollectedDelegatorCount(): Int {
161 return self.delegatorCollected.length
162 }
163
164 access(all) view fun isDelegatorCollected(uuid: UInt64): Bool {
165 return self.delegatorCollected.containsKey(uuid)
166 }
167
168 access(all) view fun borrowDelegatorInfo(nodeID: String, delegatorID: UInt32): &FlowIDTableStaking.DelegatorInfo? {
169 if self.delegatorInfoDict.containsKey(nodeID) == false {
170 return nil
171 }
172 return &self.delegatorInfoDict[nodeID]![delegatorID] as &FlowIDTableStaking.DelegatorInfo?
173 }
174
175 access(all) view fun getNodeIDList(): [String] {
176 return self.delegatorInfoDict.keys
177 }
178
179 access(all) view fun getDelegatorIDListOnNode(nodeID: String): [UInt32] {
180 return self.delegatorInfoDict[nodeID] == nil ? [] : self.delegatorInfoDict[nodeID]!.keys
181 }
182
183 access(contract) fun markDelegatorCollected(uuid: UInt64) {
184 self.delegatorCollected.insert(key: uuid, true)
185 }
186
187 access(contract) fun removeDelegatorInfo(nodeID: String, delegatorID: UInt32) {
188 if self.delegatorInfoDict.containsKey(nodeID) && self.delegatorInfoDict[nodeID]!.containsKey(delegatorID) {
189 let delegatorInfo = FlowIDTableStaking.DelegatorInfo(nodeID: nodeID, delegatorID: delegatorID)
190 assert(
191 delegatorInfo.totalTokensInRecord() + delegatorInfo.tokensRequestedToUnstake == 0.0,
192 message: "cannot remove delegator info still in-use"
193 )
194 self.delegatorInfoDict[nodeID]!.remove(key: delegatorID)
195 }
196 }
197
198 access(contract) fun setReceivedReward(received: UFix64) {
199 self.receivedReward = received
200 }
201
202 access(contract) fun setEstimatedReward(estimated: UFix64) {
203 self.estimatedReward = estimated
204 }
205
206 access(contract) fun addCanceledCommittedTokens(amount: UFix64) {
207 self.canceledCommittedTokens = self.canceledCommittedTokens + amount
208 }
209
210 access(contract) fun addCanceledStakedTokens(amount: UFix64) {
211 self.canceledStakedTokens = self.canceledStakedTokens + amount
212 }
213
214 access(contract) fun addRedelegatedTokensRequestToUnstake(amount: UFix64) {
215 self.redelegatedTokensRequestToUnstake = self.redelegatedTokensRequestToUnstake + amount
216 }
217
218 access(contract) fun addRedelegatedTokensUnderUnstaking(amount: UFix64) {
219 self.redelegatedTokensUnderUnstaking = self.redelegatedTokensUnderUnstaking + amount
220 }
221
222 access(contract) fun setStflowPrice(stFlowToFlow: UInt256, flowToStFlow: UInt256) {
223 self.scaledQuoteStFlowFlow = stFlowToFlow
224 self.scaledQuoteFlowStFlow = flowToStFlow
225 }
226
227 init(epochCounter: UInt64) {
228 self.epochCounter = epochCounter
229
230 self.allDelegatorStaked = 0.0
231 self.allDelegatorCommitted = 0.0
232 self.allDelegatorRequestedToUnstake = 0.0
233
234 self.receivedReward = 0.0
235 self.estimatedReward = 0.0
236
237 self.scaledQuoteStFlowFlow = LiquidStakingConfig.UFix64ToScaledUInt256(1.0)
238 self.scaledQuoteFlowStFlow = LiquidStakingConfig.UFix64ToScaledUInt256(1.0)
239
240 self.delegatorInfoDict = {}
241 self.delegatorCollected = {}
242
243 self.canceledCommittedTokens = 0.0
244 self.canceledStakedTokens = 0.0
245
246 self.redelegatedTokensRequestToUnstake = 0.0
247 self.redelegatedTokensUnderUnstaking = 0.0
248
249 let currentBlock = getCurrentBlock()
250 self.quoteEpochStartTimestamp = currentBlock.timestamp
251 self.quoteEpochStartBlockHeight = currentBlock.height
252 self.quoteEpochStartBlockView = currentBlock.view
253 }
254 }
255
256 /// Start a new liquid staking epoch after all protocol managed delegators have been collected
257 access(self) fun advanceEpoch() {
258 pre {
259 FlowEpoch.currentEpochCounter > self.quoteEpochCounter: "can only advance protocol epoch after a new chain epoch starts"
260 }
261
262 // Checkpoint rewards info
263 self.snapshotRewardsInfo()
264
265 // Check if approved nodes is stakable
266 self.filterApprovedNodeListOnEpochStart()
267
268 // Re-commit canceled tokens
269 self.restakeCanceledTokens()
270
271 // Re-commit redelegated tokens
272 self.redelegateTokens()
273
274 // Checkpoint stFlow price for the next epoch
275 self.stFlowQuote()
276
277 // Finally, start the new protocol epoch
278 self.quoteEpochCounter = FlowEpoch.currentEpochCounter
279
280 emit NewQuoteEpoch(epoch: self.quoteEpochCounter)
281 }
282
283 /// Calculate and checkpoint stFlow price for the next epoch
284 ///
285 /// [currentReward] + [totalCommitted] + [totalStaked]
286 /// stFlow_Flow price = ---------------------------------------------------
287 /// [stFlow totalSupply]
288 ///
289 access(self) fun stFlowQuote() {
290 pre {
291 FlowEpoch.currentEpochCounter > self.quoteEpochCounter: "stFlow price can only be checkpointed after new chain epoch and before new protocol epoch start"
292 }
293
294 let nextEpochSnapshot = self.borrowCurrentChainEpochSnapshot()
295
296 let currentReward = self.totalRewardedVault.balance
297
298 let totalCommitted = nextEpochSnapshot.allDelegatorCommitted
299
300 let totalStaked = nextEpochSnapshot.allDelegatorStaked
301 + nextEpochSnapshot.canceledStakedTokens
302 + nextEpochSnapshot.redelegatedTokensRequestToUnstake
303 + nextEpochSnapshot.redelegatedTokensUnderUnstaking
304 - self.requestedToUnstake
305 - nextEpochSnapshot.allDelegatorRequestedToUnstake
306
307 let flowSupply = currentReward + totalCommitted + totalStaked
308 let stFlowSupply = stFlowToken.totalSupply
309
310 var stFlow_flow: UInt256 = 0
311 var flow_stFlow: UInt256 = 0
312 if flowSupply == 0.0 || stFlowSupply == 0.0 {
313 stFlow_flow = LiquidStakingConfig.UFix64ToScaledUInt256(1.0)
314 flow_stFlow = LiquidStakingConfig.UFix64ToScaledUInt256(1.0)
315 } else {
316 let scaledFlowSupply = LiquidStakingConfig.UFix64ToScaledUInt256(flowSupply)
317 let scaledStFlowSupply = LiquidStakingConfig.UFix64ToScaledUInt256(stFlowSupply)
318
319 stFlow_flow = scaledFlowSupply * LiquidStakingConfig.scaleFactor / scaledStFlowSupply
320 flow_stFlow = scaledStFlowSupply * LiquidStakingConfig.scaleFactor / scaledFlowSupply
321 }
322
323 nextEpochSnapshot.setStflowPrice(stFlowToFlow: stFlow_flow, flowToStFlow: flow_stFlow)
324 }
325
326 /// Deposit flowToken to the default NodeDelegator. DelegationStrategy will redistribute to other approved nodes.
327 ///
328 /// Called by LiquidStaking::stake()
329 access(account) fun depositToCommitted(flowVault: @FlowToken.Vault) {
330 let defaultDelegator = self.borrowApprovedDelegatorFromNode(self.defaultNodeIDToStake)
331 ?? panic("delegator object not found from the defaultNodeToStake")
332 // Stake to the committed vault
333 defaultDelegator.delegateNewTokens(from: <-flowVault)
334
335 // Update snapshot
336 if FlowEpoch.currentEpochCounter > self.quoteEpochCounter {
337 self.borrowCurrentChainEpochSnapshot().upsertDelegatorInfo(nodeID: defaultDelegator.nodeID, delegatorID: defaultDelegator.id)
338 } else {
339 self.borrowCurrentQuoteEpochSnapshot().upsertDelegatorInfo(nodeID: defaultDelegator.nodeID, delegatorID: defaultDelegator.id)
340 }
341 }
342
343 /// Withdraw flow tokens from committed vault
344 ///
345 /// Called by LiquidStaking::unstakeQuickly()
346 access(account) fun withdrawFromCommitted(amount: UFix64): @FlowToken.Vault {
347 // All committed tokens will be accumulated in the default node's delegator
348 // until they are distributed to other delegators by delegation strategy before the end of the staking stage
349 let defaultDelegator = self.borrowApprovedDelegatorFromNode(self.defaultNodeIDToStake)
350 ?? panic("delegator object not found from the defaultNodeToStake")
351 let defaultDelegatorInfo = FlowIDTableStaking.DelegatorInfo(nodeID: defaultDelegator.nodeID, delegatorID: defaultDelegator.id)
352
353 assert(defaultDelegatorInfo.tokensCommitted >= amount, message: "Not enough committed tokens to withdraw")
354
355 // Cancel the committed tokens from delegator
356 defaultDelegator.requestUnstaking(amount: amount)
357
358 let flowVault <- defaultDelegator.withdrawUnstakedTokens(amount: amount)
359
360 // Update snapshot
361 self.borrowCurrentQuoteEpochSnapshot().upsertDelegatorInfo(nodeID: defaultDelegator.nodeID, delegatorID: defaultDelegator.id)
362
363 return <-(flowVault as! @FlowToken.Vault)
364 }
365
366 /// All unstaking requests are marked and will be processed before staking auction end
367 ///
368 /// Called by LiquidStaking::unstake()
369 access(account) fun requestWithdrawFromStaked(amount: UFix64) {
370 let currentEpochSnapshot = self.borrowCurrentQuoteEpochSnapshot()
371 assert(
372 currentEpochSnapshot.allDelegatorStaked + currentEpochSnapshot.allDelegatorCommitted
373 >=
374 amount + currentEpochSnapshot.allDelegatorRequestedToUnstake + self.requestedToUnstake
375 , message: LiquidStakingError.ErrorEncode(
376 msg: "Not enough tokens to request unstake",
377 err: LiquidStakingError.ErrorCode.INVALID_PARAMETERS
378 )
379 )
380
381 // mark unstake requests
382 self.requestedToUnstake = self.requestedToUnstake + amount
383 }
384
385 /// Withdraw tokens from unstaked vault
386 ///
387 /// Called by LiquidStaking::cashoutWithdrawVoucher()
388 access(account) fun withdrawFromUnstaked(amount: UFix64): @FlowToken.Vault {
389 return <-(self.totalUnstakedVault.withdraw(amount: amount) as! @FlowToken.Vault)
390 }
391
392 /// Migrate Delegator
393 ///
394 /// Called by LiquidStaking::migrate()
395 access(account) fun migrateDelegator(delegator: @FlowIDTableStaking.NodeDelegator) {
396 let nodeID = delegator.nodeID
397 let delegatorID = delegator.id
398 let uuid = delegator.uuid
399
400 self.insertMigratedDelegatorUUID(nodeID: nodeID, delegatorID: delegatorID, uuid: uuid)
401
402 // Update snapshot
403 self.borrowCurrentQuoteEpochSnapshot().upsertDelegatorInfo(nodeID: delegator.nodeID, delegatorID: delegator.id)
404
405 self.allDelegators[uuid] <-! delegator
406 }
407
408 /// Restake rewarded tokens to compound rewards.
409 access(self) fun compoundRewards() {
410 if (self.totalRewardedVault.balance > 0.0) {
411 let rewardVault <- self.totalRewardedVault.withdraw(amount: self.totalRewardedVault.balance)
412 emit CompoundReward(rewardAmount: rewardVault.balance, protocolEpoch: self.quoteEpochCounter)
413 self.depositToCommitted(flowVault: <-(rewardVault as! @FlowToken.Vault))
414 }
415 }
416
417 /// Restake any tokens staked to nodes canceled in the idtable
418 access(self) fun restakeCanceledTokens() {
419 pre {
420 FlowEpoch.currentEpochCounter > self.quoteEpochCounter: "restake canceled tokens can only happen after new chain epoch and before new protocol epoch start"
421 }
422 // Protocol epoch N
423 let currEpochSnapshot = self.borrowCurrentQuoteEpochSnapshot()
424 // Chain epoch N+1
425 let nextEpochSnapshot = self.borrowCurrentChainEpochSnapshot()
426
427 // restake canceled committed tokens from protocol epoch N, they are in unstaked mode in protocol epoch N+1
428 if nextEpochSnapshot.canceledCommittedTokens > 0.0 {
429 let canceledCommittedTokens <- self.totalUnstakedVault.withdraw(amount: nextEpochSnapshot.canceledCommittedTokens)
430 self.depositToCommitted(flowVault: <-(canceledCommittedTokens as! @FlowToken.Vault))
431 emit RestakeCanceledTokens(amount: nextEpochSnapshot.canceledCommittedTokens, type: "canceled-committed", fromProtocolEpoch: self.quoteEpochCounter)
432 }
433
434 // restake canceled staked tokens from protocol epoch N-1, they were in unstaking mode in protocol epoch N, and become unstaked in protocol epoch N+1
435 if currEpochSnapshot.canceledStakedTokens > 0.0 {
436 let canceledStakedTokens <- self.totalUnstakedVault.withdraw(amount: currEpochSnapshot.canceledStakedTokens)
437 self.depositToCommitted(flowVault: <-(canceledStakedTokens as! @FlowToken.Vault))
438 emit RestakeCanceledTokens(amount: currEpochSnapshot.canceledStakedTokens, type: "canceled-staked", fromProtocolEpoch: self.quoteEpochCounter - 1)
439 }
440 }
441
442 /// Process redelegate requests and restake unstaked redelegated tokens
443 access(self) fun redelegateTokens() {
444 pre {
445 FlowEpoch.currentEpochCounter > self.quoteEpochCounter: "redelegate requests can only be processed after new chain epoch and before new protocol epoch start"
446 }
447
448 let currEpochSnapshot = self.borrowCurrentQuoteEpochSnapshot()
449 let nextEpochSnapshot = self.borrowCurrentChainEpochSnapshot()
450
451 // requestToUnstake tokens -> unstaking mode in the next protocol epoch
452 if currEpochSnapshot.redelegatedTokensRequestToUnstake > 0.0 {
453 nextEpochSnapshot.addRedelegatedTokensUnderUnstaking(amount: currEpochSnapshot.redelegatedTokensRequestToUnstake)
454 }
455
456 // unstaking mode -> unstaked in the next protocol epoch, and available to restake now
457 if currEpochSnapshot.redelegatedTokensUnderUnstaking > 0.0 {
458 let redelegatedVault <- self.totalUnstakedVault.withdraw(amount: currEpochSnapshot.redelegatedTokensUnderUnstaking)
459 self.depositToCommitted(flowVault: <-(redelegatedVault as! @FlowToken.Vault))
460 emit RedelegateTokens(amount: currEpochSnapshot.redelegatedTokensUnderUnstaking, currEpoch: self.quoteEpochCounter)
461 }
462 }
463
464 /// Collect from all managed delegators on a new chain epoch
465 ///
466 /// Move unstaked & rewarded vaults from delegators -> totalUnstaked & totalRewarded vaults
467 /// Collection will start immediately after the new chain epoch starts
468 /// Due to the large amount of possible delegators (including migrated ones), collection will be processed in batches
469 ///
470 /// During this short time, no new stake request will be accepted until all managed delegators are collected,
471 /// which will recalculate the price of stFlow this epoch
472 ///
473 /// This function is made public so anyone can call to keep the protocol moving
474 /// This function should be implemented as idempotent
475 access(all) fun collectDelegatorsOnEpochStart(startIndex: Int, endIndex: Int, ifAdvanceEpoch: Bool) {
476 pre {
477 FlowEpoch.currentEpochCounter > self.quoteEpochCounter: "No need to collect, chain epoch not advanced yet"
478 startIndex <= endIndex: "Invalid index"
479 }
480
481 // When underlying system's auto reward payment is turned on
482 if FlowEpoch.automaticRewardsEnabled() == true {
483 if self.quoteEpochCounter > 0 {
484 assert(
485 FlowEpoch.getEpochMetadata(self.quoteEpochCounter)!.rewardsPaid == true, message:
486 LiquidStakingError.ErrorEncode(
487 msg: "Rewards has not been paid yet for protocol epoch ".concat(self.quoteEpochCounter.toString()),
488 err: LiquidStakingError.ErrorCode.STAKING_REWARD_NOT_PAID
489 )
490 )
491 }
492 }
493
494 // Create a new epoch when necessary
495 if self.epochSnapshotHistory.containsKey(FlowEpoch.currentEpochCounter) == false {
496 self.epochSnapshotHistory[FlowEpoch.currentEpochCounter] = EpochSnapshot(epochCounter: FlowEpoch.currentEpochCounter)
497 }
498
499 let currEpochSnapshot = self.borrowCurrentQuoteEpochSnapshot()
500 let nextEpochSnapshot = self.borrowCurrentChainEpochSnapshot()
501
502 let delegatorUUIDList = self.allDelegators.keys
503 let delegatorLength = delegatorUUIDList.length
504 var index = startIndex
505 while index <= endIndex && index < delegatorLength {
506 let uuid = delegatorUUIDList[index]
507 if nextEpochSnapshot.isDelegatorCollected(uuid: uuid) {
508 index = index + 1
509 continue
510 }
511
512 let delegator = self.borrowManagedDelegator(uuid: uuid)!
513 let nodeID = delegator.nodeID
514 let delegatorID = delegator.id
515
516 // Latest DelegatorInfo from flowchain idtable
517 let latestDelegatorInfo = FlowIDTableStaking.DelegatorInfo(nodeID: nodeID, delegatorID: delegatorID)
518 // Cached DelegatorInfo for the current protocol epoch
519 let currDelegatorInfo = currEpochSnapshot.borrowDelegatorInfo(nodeID: nodeID, delegatorID: delegatorID)
520 // After FlowIDTableStaking::moveTokens(), delegators should be no committed tokens left
521 assert(latestDelegatorInfo.tokensCommitted == 0.0, message: "Panic: committed tokens has not been moved to staked vault")
522
523 if (currDelegatorInfo != nil) {
524 if FlowEpoch.automaticRewardsEnabled() == false {
525 // Hmm... the manual reward payment is somehow delayed
526 if latestDelegatorInfo.tokensRewarded == 0.0 && currDelegatorInfo!.tokensStaked >= LiquidStakingConfig.minStakingAmount {
527 // Clear the previous collection, waiting for reward payment
528 self.epochSnapshotHistory.remove(key: FlowEpoch.currentEpochCounter)
529 return
530 }
531 }
532
533 // !!
534 // The node was canceled in FlowIDTableStaking::removeUnapprovedNodes()
535 // staked tokens -> unstaking vault
536 // committed tokens -> unstaked vault
537 if latestDelegatorInfo.tokensStaked == 0.0
538 &&
539 (currDelegatorInfo!.tokensCommitted > 0.0 || (currDelegatorInfo!.tokensStaked - currDelegatorInfo!.tokensRequestedToUnstake > 0.0)) {
540
541 // Tokens that are forcely canceled, need to be re-committed
542 let tokensToRecommitNow = currDelegatorInfo!.tokensCommitted
543 let tokensToRecommitNextEpoch = currDelegatorInfo!.tokensStaked - currDelegatorInfo!.tokensRequestedToUnstake
544
545 nextEpochSnapshot.addCanceledCommittedTokens(amount: tokensToRecommitNow)
546 nextEpochSnapshot.addCanceledStakedTokens(amount: tokensToRecommitNextEpoch)
547 }
548 }
549
550 // Collect rewarded tokens
551 if latestDelegatorInfo.tokensRewarded > 0.0 {
552 self.totalRewardedVault.deposit(from: <-delegator.withdrawRewardedTokens(amount: latestDelegatorInfo.tokensRewarded))
553 }
554
555 // Collect unstaked tokens
556 if latestDelegatorInfo.tokensUnstaked > 0.0 {
557 self.totalUnstakedVault.deposit(from: <-delegator.withdrawUnstakedTokens(amount: latestDelegatorInfo.tokensUnstaked))
558 }
559
560 // Update snapshot
561 nextEpochSnapshot.upsertDelegatorInfo(nodeID: nodeID, delegatorID: delegatorID)
562 nextEpochSnapshot.markDelegatorCollected(uuid: uuid)
563
564 index = index + 1
565 }
566
567 // If all delegators have been collected
568 let collectedCount = nextEpochSnapshot.getCollectedDelegatorCount()
569 if collectedCount == self.allDelegators.length && ifAdvanceEpoch == true {
570 self.advanceEpoch()
571 }
572
573 emit DelegatorsCollected(startIndex: startIndex, endIndex: endIndex, uncollectedCount: self.allDelegators.length - collectedCount)
574 }
575
576 access(contract) fun insertMigratedDelegatorUUID(nodeID: String, delegatorID: UInt32, uuid: UInt64) {
577 if self.migratedDelegatorIDs.containsKey(nodeID) == false {
578 self.migratedDelegatorIDs[nodeID] = {}
579 }
580
581 assert(self.migratedDelegatorIDs[nodeID]!.containsKey(delegatorID) == false, message: "Reinsert delegator uuid")
582
583 self.migratedDelegatorIDs[nodeID]!.insert(key: delegatorID, uuid)
584 }
585
586 /// Snapshot rewards info after all delegator's rewards & unstaked tokens have been collected
587 access(self) fun snapshotRewardsInfo() {
588 pre {
589 FlowEpoch.currentEpochCounter > self.quoteEpochCounter: "Rewards info can only be snapshotted after new chain epoch and before new protocol epoch start"
590 }
591 // Protocol epoch N
592 let currEpochSnapshot = self.borrowCurrentQuoteEpochSnapshot()
593 // Chain epoch N+1
594 let nextEpochSnapshot = self.borrowCurrentChainEpochSnapshot()
595
596 assert(
597 nextEpochSnapshot.getCollectedDelegatorCount() == self.allDelegators.length,
598 message: "All delegators should have been collected"
599 )
600
601 // Checkpoint received reward for protocol epoch N
602 if LiquidStakingConfig.protocolFee > 0.0 {
603 let feeVault <- self.totalRewardedVault.withdraw(amount: self.totalRewardedVault.balance * LiquidStakingConfig.protocolFee)
604 self.depositToProtocolFees(flowVault: <-feeVault, purpose: "epoch reward cut -> protocol fee")
605 }
606 currEpochSnapshot.setReceivedReward(received: self.totalRewardedVault.balance)
607
608 // Checkpoint estimated rewards to be received for protocol epoch N+1
609 let estimatedNextEpochAmount = LiquidStakingConfig.calcEstimatedStakingPayout(stakedAmount: nextEpochSnapshot.allDelegatorStaked)
610 nextEpochSnapshot.setEstimatedReward(estimated: estimatedNextEpochAmount)
611
612 emit RewardsInfoCheckpointed(currEpoch: self.quoteEpochCounter, received: currEpochSnapshot.receivedReward, estimated: currEpochSnapshot.estimatedReward, estimateNextReward: nextEpochSnapshot.estimatedReward)
613 }
614
615 /// Check protocol's approved nodes against FlowIDTableStaking.getStakedNodeIDs()
616 access(self) fun filterApprovedNodeListOnEpochStart() {
617 let stakableNodeList = FlowIDTableStaking.getStakedNodeIDs()
618 let currentApprovedNodeIDList = self.approvedNodeIDList.keys
619
620 for nodeID in currentApprovedNodeIDList {
621 if stakableNodeList.contains(nodeID) {
622 continue
623 }
624
625 if nodeID == self.defaultNodeIDToStake {
626 self.defaultNodeIDToStake = ""
627 }
628
629 self.removeApprovedNodeID(nodeID: nodeID)
630
631 emit ApprovedNodeCanceled(nodeID: nodeID)
632 }
633
634 if self.defaultNodeIDToStake == "" && self.approvedNodeIDList.length > 0 {
635 self.defaultNodeIDToStake = self.approvedNodeIDList.keys[0]
636 }
637 }
638
639 access(self) view fun borrowManagedDelegator(uuid: UInt64): auth(FlowIDTableStaking.DelegatorOwner) &FlowIDTableStaking.NodeDelegator? {
640 return &self.allDelegators[uuid] as auth(FlowIDTableStaking.DelegatorOwner) &FlowIDTableStaking.NodeDelegator?
641 }
642 access(self) fun removeManagedDelegator(uuid: UInt64) {
643 let tmpDelegator <- self.allDelegators[uuid] <- nil
644 destroy tmpDelegator
645 }
646
647 access(self) view fun borrowApprovedDelegatorFromNode(_ nodeID: String): auth(FlowIDTableStaking.DelegatorOwner) &FlowIDTableStaking.NodeDelegator? {
648 if (self.approvedNodeIDList.containsKey(nodeID) && self.approvedDelegatorIDs.containsKey(nodeID)) == false {
649 return nil
650 }
651 let uuid = self.approvedDelegatorIDs[nodeID]!
652 return self.borrowManagedDelegator(uuid: uuid)
653 }
654
655 /// Register a new delegator object on an approved staking node
656 access(self) fun registerApprovedDelegator(_ nodeID: String, _ initialCommit: @{FungibleToken.Vault}) {
657 pre {
658 self.approvedNodeIDList.containsKey(nodeID): "Cannot register delegator on nodes out of approved list"
659 !self.approvedDelegatorIDs.containsKey(nodeID): "Delegator object on the given node has already existed"
660 }
661 assert(
662 FlowIDTableStaking.getStakedNodeIDs().contains(nodeID),
663 message: "Cannot stake to the inactive node: ".concat(nodeID)
664 )
665 assert(
666 initialCommit.balance >= FlowIDTableStaking.getDelegatorMinimumStakeRequirement(),
667 message: "Initial commitment for delegator registration < system threshold"
668 )
669
670 let nodeDelegator <- FlowIDTableStaking.registerNewDelegator(nodeID: nodeID, tokensCommitted: <- initialCommit)
671 emit RegisterNewDelegator(nodeID: nodeDelegator.nodeID, delegatorID: nodeDelegator.id)
672
673 let uuid = nodeDelegator.uuid
674 self.approvedDelegatorIDs[nodeDelegator.nodeID] = uuid
675 self.allDelegators[uuid] <-! nodeDelegator
676 }
677
678 access(self) fun removeApprovedNodeID(nodeID: String) {
679 pre {
680 self.approvedNodeIDList.containsKey(nodeID): "Nonexistent nodeID to remove"
681 self.defaultNodeIDToStake != nodeID: "Default commit node should be reappointed before removing"
682 }
683 // No delegator on this nodeID
684 if self.approvedDelegatorIDs.containsKey(nodeID) == false {
685 self.approvedNodeIDList.remove(key: nodeID)
686 return
687 }
688
689 let uuid = self.approvedDelegatorIDs[nodeID]!
690 let delegatorRef = self.borrowManagedDelegator(uuid: uuid)!
691 let delegatorInfo = FlowIDTableStaking.DelegatorInfo(nodeID: delegatorRef.nodeID, delegatorID: delegatorRef.id)
692 // Committed tokens should be restaked to other approved nodes, especially if the default staking node is to be removed.
693 assert(delegatorInfo.tokensCommitted == 0.0, message: "Committed tokens should be moved out before removing")
694
695 self.approvedNodeIDList.remove(key: nodeID)
696
697 // Move delegator record from approved list to migrated list, so it won't receive future delegates and will be unstaked in priority.
698 self.approvedDelegatorIDs.remove(key: nodeID)
699 self.insertMigratedDelegatorUUID(nodeID: delegatorRef.nodeID, delegatorID: delegatorRef.id, uuid: uuid)
700 }
701
702 access(all) fun depositToProtocolFees(flowVault: @{FungibleToken.Vault}, purpose: String) {
703 emit DepositProtocolFees(amount: flowVault.balance, purpose: purpose)
704 self.protocolFeeVault.deposit(from: <-flowVault)
705 }
706
707 /// Contribute additional $flow to rewardedVault for whatever reason (e.g. partnership nodes' node-cut reimbursement, donation, etc.).
708 /// This will boost $stFlow price (and also liquid staking apr) in the next epoch
709 access(all) fun addReward(rewardedVault: @FlowToken.Vault) {
710 pre {
711 FlowEpoch.currentEpochCounter == DelegatorManager.quoteEpochCounter: "Cannot addReward until protocol epoch syncs"
712 }
713 emit ExternalRewardsContributed(amount: rewardedVault.balance)
714 self.totalRewardedVault.deposit(from: <-rewardedVault)
715 }
716
717 /// Amount of flowTokens the liquid staking protocol is fully backed by
718 access(all) view fun getTotalValidStakingAmount(): UFix64 {
719 let currentEpochSnapshot = self.borrowCurrentQuoteEpochSnapshot()
720 let totalValidStakingAmount = currentEpochSnapshot.allDelegatorStaked
721 + currentEpochSnapshot.allDelegatorCommitted
722 + currentEpochSnapshot.canceledStakedTokens
723 + currentEpochSnapshot.redelegatedTokensUnderUnstaking
724 + currentEpochSnapshot.redelegatedTokensRequestToUnstake
725 + self.totalRewardedVault.balance
726 - currentEpochSnapshot.allDelegatorRequestedToUnstake
727 - self.requestedToUnstake
728 return totalValidStakingAmount
729 }
730
731 access(all) view fun borrowEpochSnapshot(at: UInt64): &EpochSnapshot {
732 return (&self.epochSnapshotHistory[at] as &EpochSnapshot?) ?? panic("EpochSnapshot index out of range")
733 }
734
735 access(all) view fun borrowCurrentChainEpochSnapshot(): &EpochSnapshot {
736 return self.borrowEpochSnapshot(at: FlowEpoch.currentEpochCounter)
737 }
738
739 access(all) view fun borrowCurrentQuoteEpochSnapshot(): &EpochSnapshot {
740 return self.borrowEpochSnapshot(at: self.quoteEpochCounter)
741 }
742
743 access(all) view fun getDelegatorUUIDByID(nodeID: String, delegatorID: UInt32): UInt64? {
744 if self.migratedDelegatorIDs.containsKey(nodeID) {
745 if self.migratedDelegatorIDs[nodeID]!.containsKey(delegatorID) {
746 return self.migratedDelegatorIDs[nodeID]![delegatorID]
747 }
748 }
749 if let delegator = self.borrowApprovedDelegatorFromNode(nodeID) {
750 if delegator.id == delegatorID {
751 return self.approvedDelegatorIDs[nodeID]!
752 }
753 }
754 return nil
755 }
756
757 access(all) view fun getApprovedNodeList(): {String: UFix64} {
758 return self.approvedNodeIDList
759 }
760
761 /// Get all approved delegator uuids keyed by nodeID
762 /// Up to 400 nodes, do not worry about the gas-limit
763 access(all) view fun getApprovedDelegatorIDs(): {String: UInt64} {
764 return self.approvedDelegatorIDs
765 }
766
767 /// NodeID list that involves with migrated delegators
768 /// Up to 400 nodes, do not worry about the gas-limit
769 access(all) view fun getMigratedNodeIDList(): [String] {
770 return self.migratedDelegatorIDs.keys
771 }
772
773 access(all) view fun getMigratedDelegatorLength(nodeID: String): Int {
774 return self.migratedDelegatorIDs[nodeID]!.length
775 }
776
777 /// [from, to)
778 access(all) view fun getSlicedMigratedDelegatorIDList(nodeID: String, from: Int, to: Int): [UInt32] {
779 var upTo = to
780 if upTo > self.migratedDelegatorIDs[nodeID]!.length {
781 upTo = self.migratedDelegatorIDs[nodeID]!.length
782 }
783 return self.migratedDelegatorIDs[nodeID]!.keys.slice(from: from, upTo: upTo)
784 }
785
786 access(all) view fun getProtocolFeeBalance(): UFix64 {
787 return self.protocolFeeVault.balance
788 }
789
790 access(all) view fun getDelegatorInfoByUUID(delegatorUUID: UInt64): FlowIDTableStaking.DelegatorInfo {
791 let delegator = self.borrowManagedDelegator(uuid: delegatorUUID)
792 ?? panic("delegator not managed by liquid staking protocol")
793 let nodeID = delegator.nodeID
794 let delegatorID = delegator.id
795 return FlowIDTableStaking.DelegatorInfo(nodeID: nodeID, delegatorID: delegatorID)
796 }
797
798 access(all) view fun getApprovedDelegatorInfoByNodeID(nodeID: String): FlowIDTableStaking.DelegatorInfo {
799 let delegator = self.borrowApprovedDelegatorFromNode(nodeID)
800 ?? panic("approved delegator not found on given node")
801 let nodeID = delegator.nodeID
802 let delegatorID = delegator.id
803 return FlowIDTableStaking.DelegatorInfo(nodeID: nodeID, delegatorID: delegatorID)
804 }
805
806 /// [from, to)
807 access(all) view fun getSlicedDelegatorUUIDList(from: Int, to: Int): [UInt64] {
808 let UUIDs = self.allDelegators.keys
809 var upTo = to
810 if upTo > UUIDs.length {
811 upTo = UUIDs.length
812 }
813 return UUIDs.slice(from: from, upTo: upTo)
814 }
815
816 access(all) view fun getTotalUnstakedVaultBalance(): UFix64 {
817 return self.totalUnstakedVault.balance
818 }
819
820 access(all) view fun getDelegatorsLength(): Int {
821 return self.allDelegators.length
822 }
823
824 /// Used together with offchain strategy bots
825 access(all) resource DelegationStrategy {
826
827 /// Transfer committed tokens among delegators.
828 /// Utilized by strategy bots to redistribute newly staked $flow to different nodes for the sake of protocol decentralization,
829 /// and also to get rid of single point of failure.
830 access(all) fun transferCommittedTokens(fromNodeID: String, toNodeID: String, amount: UFix64) {
831 pre {
832 FlowEpoch.currentEpochCounter == DelegatorManager.quoteEpochCounter: "Cannot transfer comitted tokens until protocol epoch syncs"
833 fromNodeID != toNodeID: "Cannot transfer tokens among same nodes"
834 }
835 let fromDelegator = DelegatorManager.borrowApprovedDelegatorFromNode(fromNodeID)
836 ?? panic("cannot borrow from approved delegator of fromNode")
837 let toDelegator = DelegatorManager.borrowApprovedDelegatorFromNode(toNodeID)
838 ?? panic("cannot borrow from approved delegator of toNode")
839 let fromDelegatorInfo = FlowIDTableStaking.DelegatorInfo(nodeID: fromNodeID, delegatorID: fromDelegator.id)
840
841 assert(fromDelegatorInfo.tokensCommitted >= amount, message: "try to transfer more than fromNode.committed")
842
843 // withdraw committed
844 fromDelegator.requestUnstaking(amount: amount)
845 let transferVault <- fromDelegator.withdrawUnstakedTokens(amount: amount)
846
847 // deposit committed
848 toDelegator.delegateNewTokens(from: <- transferVault)
849
850 // update snapshot
851 DelegatorManager.borrowCurrentQuoteEpochSnapshot().upsertDelegatorInfo(nodeID: fromDelegator.nodeID, delegatorID: fromDelegator.id)
852 DelegatorManager.borrowCurrentQuoteEpochSnapshot().upsertDelegatorInfo(nodeID: toDelegator.nodeID, delegatorID: toDelegator.id)
853
854 emit StrategyTransferCommittedTokens(from: fromNodeID, to: toNodeID, amount: amount)
855 }
856
857 /// Process unstaking request from the given delegator
858 ///
859 /// Flow chain's underlying requestUnstaking() will first unstake from committed tokens, if any.
860 /// Unlike requestUnstaking(), this function will first unstake as many as possible from staked tokens instead of committed tokens
861 /// It's used by off-chain bots to implement different unstaking strategies.
862 access(all) fun processUnstakeRequest(requestUnstakeAmount: UFix64, delegatorUUID: UInt64) {
863 pre {
864 FlowEpoch.currentEpochCounter == DelegatorManager.quoteEpochCounter: "Cannot process unstake request until protocol epoch syncs"
865 DelegatorManager.requestedToUnstake > 0.0: "No pending unstake request to handle"
866 }
867 let delegator = DelegatorManager.borrowManagedDelegator(uuid: delegatorUUID)!
868 let delegatorInfo = FlowIDTableStaking.DelegatorInfo(nodeID: delegator.nodeID, delegatorID: delegator.id)
869 let tokensStakedLeft = delegatorInfo.tokensStaked - delegatorInfo.tokensRequestedToUnstake
870 let tokensCommitted = delegatorInfo.tokensCommitted
871
872 var unstakeAmount = requestUnstakeAmount
873 // Try unstaking all
874 if unstakeAmount == UFix64.max {
875 if DelegatorManager.requestedToUnstake >= tokensStakedLeft + tokensCommitted {
876 unstakeAmount = tokensStakedLeft + tokensCommitted
877 } else {
878 unstakeAmount = DelegatorManager.requestedToUnstake
879 }
880 }
881
882 assert(DelegatorManager.requestedToUnstake >= unstakeAmount, message: "Handle unstake requests out of limit")
883
884 // Request unstaking
885 if unstakeAmount <= tokensStakedLeft {
886 // Consuming only from staked tokens as it's enough to cover unstaking request
887 // Due to flowchain's underlying staking mechanism & api, below implementation has to be as it is: first requestUnstaking and then stake back.
888 delegator.requestUnstaking(amount: unstakeAmount + tokensCommitted)
889 // stake committed tokens back
890 let committedVault <- delegator.withdrawUnstakedTokens(amount: tokensCommitted)
891 delegator.delegateNewTokens(from: <- committedVault)
892 } else {
893 // Consuming all staked tokens and any rest from committed tokens
894 delegator.requestUnstaking(amount: tokensStakedLeft + tokensCommitted)
895 // stake remaining committed tokens back
896 let committedVault <- delegator.withdrawUnstakedTokens(amount: tokensStakedLeft + tokensCommitted - unstakeAmount)
897 delegator.delegateNewTokens(from: <- committedVault)
898 }
899
900 // update unprocessed unstaked requests
901 DelegatorManager.requestedToUnstake = DelegatorManager.requestedToUnstake - unstakeAmount
902
903 // update snapshot
904 DelegatorManager.borrowCurrentQuoteEpochSnapshot().upsertDelegatorInfo(nodeID: delegator.nodeID, delegatorID: delegator.id)
905
906 emit StrategyProcessUnstakeRequest(amount: requestUnstakeAmount, nodeIDToUnstake: delegator.nodeID, delegatorIDToUnstake: delegator.id, leftoverAmount: DelegatorManager.requestedToUnstake)
907 }
908
909 /// Clean empty migrated delegator and outdated approved delegator
910 access(all) fun cleanDelegators(delegatorUUID: UInt64) {
911 pre {
912 FlowEpoch.currentEpochCounter == DelegatorManager.quoteEpochCounter: "Cannot cleanup delegators until protocol epoch syncs"
913 }
914 let delegator = DelegatorManager.borrowManagedDelegator(uuid: delegatorUUID)!
915 let nodeID = delegator.nodeID
916 let delegatorID = delegator.id
917 let delegatorInfo = FlowIDTableStaking.DelegatorInfo(nodeID: nodeID, delegatorID: delegatorID)
918
919 assert(
920 delegatorInfo.tokensCommitted +
921 delegatorInfo.tokensStaked +
922 delegatorInfo.tokensUnstaking +
923 delegatorInfo.tokensRewarded +
924 delegatorInfo.tokensUnstaked +
925 delegatorInfo.tokensRequestedToUnstake
926 ==
927 0.0, message: "Please clean up before deleting"
928 )
929
930 var removed = false
931 // remove migrated delegator
932 if DelegatorManager.migratedDelegatorIDs.containsKey(nodeID) {
933 if DelegatorManager.migratedDelegatorIDs[nodeID]!.containsKey(delegatorID) {
934 DelegatorManager.migratedDelegatorIDs[nodeID]!.remove(key: delegatorID)
935 DelegatorManager.removeManagedDelegator(uuid: delegatorUUID)
936 DelegatorManager.borrowCurrentQuoteEpochSnapshot().removeDelegatorInfo(nodeID: nodeID, delegatorID: delegatorID)
937 removed = true
938 }
939 }
940 // remove old approved delegator
941 if DelegatorManager.approvedDelegatorIDs.containsKey(nodeID) {
942 if DelegatorManager.approvedDelegatorIDs[nodeID]! == delegatorUUID {
943 if DelegatorManager.approvedNodeIDList.containsKey(nodeID) == false {
944 DelegatorManager.approvedDelegatorIDs.remove(key: nodeID)
945 DelegatorManager.removeManagedDelegator(uuid: delegatorUUID)
946 DelegatorManager.borrowCurrentQuoteEpochSnapshot().removeDelegatorInfo(nodeID: nodeID, delegatorID: delegatorID)
947 removed = true
948 }
949 }
950 }
951
952 if removed {
953 emit DelegatorRemoved(nodeID: nodeID, delegatorID: delegatorID, uuid: delegatorUUID)
954 }
955 }
956
957 // Compound rewards collected in previous protocol epoch
958 access(all) fun compoundRewards() {
959 pre {
960 FlowEpoch.currentEpochCounter == DelegatorManager.quoteEpochCounter: "Cannot compound until protocol epoch syncs"
961 }
962 DelegatorManager.compoundRewards()
963 }
964 }
965
966 /// Protocol Admin
967 access(all) resource Admin {
968
969 /// Initialize approved staking node list
970 access(all) fun initApprovedNodeIDList(nodeIDs: {String: UFix64}, defaultNodeIDToStake: String) {
971 pre {
972 nodeIDs.containsKey(defaultNodeIDToStake): "Default staking node id must be in the list"
973 DelegatorManager.approvedNodeIDList.length == 0: "Can only be initialized once"
974 }
975 for id in nodeIDs.keys {
976 DelegatorManager.approvedNodeIDList.insert(key: id, nodeIDs[id]!)
977 }
978 DelegatorManager.defaultNodeIDToStake = defaultNodeIDToStake
979
980 emit SetApprovedNodeList(nodeIDs: nodeIDs, defaultNodeIDToStake: defaultNodeIDToStake)
981 }
982
983 access(all) fun upsertApprovedNodeID(nodeID: String, weight: UFix64) {
984 let oldWeight = DelegatorManager.approvedNodeIDList.insert(key: nodeID, weight)
985
986 emit UpsertApprovedNode(nodeID: nodeID, oldWeight: oldWeight, newWeight: weight)
987 }
988
989 access(all) fun removeApprovedNodeID(nodeID: String) {
990 DelegatorManager.removeApprovedNodeID(nodeID: nodeID)
991
992 emit ApprovedNodeRemoved(nodeID: nodeID)
993 }
994
995 /// Select a node among approved node list to be the default staking node
996 /// Delegation strategy will distribute its commited tokens to other staking nodes
997 access(all) fun setDefaultNodeIDToStake(nodeID: String) {
998 pre {
999 DelegatorManager.approvedNodeIDList.containsKey(nodeID): "Default staking node id must be in approved node list"
1000 }
1001 if nodeID != DelegatorManager.defaultNodeIDToStake {
1002 DelegatorManager.defaultNodeIDToStake = nodeID
1003 emit SetDefaultStakeNode(from: DelegatorManager.defaultNodeIDToStake, to: nodeID)
1004 }
1005 }
1006
1007 /// Create Strategy
1008 access(all) fun createStrategy(): @DelegationStrategy {
1009 return <- create DelegationStrategy()
1010 }
1011
1012 /// Redelegate @amount of staked $Flow, @amount doesn't include committed $Flow. Due to flowchain's underlying staking mechanism:
1013 /// - committed tokens can be immediately canceled and restaked
1014 /// - staked $Flow will be in unstaking mode in the next chain epoch and become unstaked (and available for restake) in next+1 chain epoch
1015 access(all) fun redelegate(nodeID: String, delegatorID: UInt32, amount: UFix64) {
1016 pre {
1017 FlowEpoch.currentEpochCounter == DelegatorManager.quoteEpochCounter: "Cannot redelegate until protocol epoch syncs"
1018 }
1019 let uuid = DelegatorManager.getDelegatorUUIDByID(nodeID: nodeID, delegatorID: delegatorID)!
1020 let delegator = DelegatorManager.borrowManagedDelegator(uuid: uuid)!
1021 let delegatorInfo = DelegatorManager.getDelegatorInfoByUUID(delegatorUUID: uuid)
1022
1023 // cancel and restake any committed tokens directly
1024 let redelegateCommittedAmount = delegatorInfo.tokensCommitted
1025 if redelegateCommittedAmount > 0.0 {
1026 delegator.requestUnstaking(amount: redelegateCommittedAmount)
1027 let committedVault <- delegator.withdrawUnstakedTokens(amount: redelegateCommittedAmount)
1028 DelegatorManager.depositToCommitted(flowVault: <-(committedVault as! @FlowToken.Vault))
1029 }
1030
1031 // request to unstake
1032 delegator.requestUnstaking(amount: amount)
1033
1034 DelegatorManager.borrowCurrentQuoteEpochSnapshot().addRedelegatedTokensRequestToUnstake(amount: amount)
1035 // update snapshotted delegator info
1036 DelegatorManager.borrowCurrentQuoteEpochSnapshot().upsertDelegatorInfo(nodeID: delegator.nodeID, delegatorID: delegator.id)
1037
1038 emit RedelegateRequested(nodeID: nodeID, delegatorID: delegatorID, redelegateCommittedAmount: redelegateCommittedAmount, redelegateRequestToUnstake: amount)
1039 }
1040
1041 /// Protocol fee vault control
1042 access(all) fun borrowProtocolFeeVault(): auth(FungibleToken.Withdraw) &FlowToken.Vault {
1043 return &DelegatorManager.protocolFeeVault as auth(FungibleToken.Withdraw) &FlowToken.Vault
1044 }
1045
1046 /// Manually register a new delegator resource on the given approved node
1047 access(all) fun registerApprovedDelegator(nodeID: String, initialCommit: @{FungibleToken.Vault}) {
1048 DelegatorManager.registerApprovedDelegator(nodeID, <-initialCommit)
1049 }
1050 }
1051
1052 init() {
1053 self.adminPath = /storage/liquidStakingAdmin
1054 self.delegationStrategyPath = /storage/liquidStakingDelegationStrategy
1055
1056 self.approvedNodeIDList = {}
1057 self.defaultNodeIDToStake = ""
1058
1059 self.allDelegators <- {}
1060 self.approvedDelegatorIDs = {}
1061 self.migratedDelegatorIDs = {}
1062
1063 self.requestedToUnstake = 0.0
1064 self.protocolFeeVault <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
1065 self.totalUnstakedVault <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
1066 self.totalRewardedVault <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
1067
1068 self.quoteEpochCounter = 0
1069 self.epochSnapshotHistory = {}
1070 self.epochSnapshotHistory[0] = EpochSnapshot(epochCounter: 0)
1071
1072 self._reservedFields = {}
1073
1074 self.account.storage.save(<-create Admin(), to: self.adminPath)
1075 }
1076}
1077