Smart Contract

DelegatorManager

A.d6f80565193ad727.DelegatorManager

Deployed

1w ago
Feb 19, 2026, 10:46:17 AM UTC

Dependents

26 imports
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