Smart Contract

StakingManager

A.70466175fbacfb56.StakingManager

Valid From

86,138,636

Deployed

3d ago
Feb 24, 2026, 11:53:46 PM UTC

Dependents

15 imports
1import Crypto
2import EVM from 0xe467b9dd11fa00df
3import FlowStakingCollection from 0x8d0e87b65159ae63
4import FlowToken from 0x1654653399040a61
5import FungibleToken from 0xf233dcee88fe0abe
6
7// Simple StakingCollecion wrapper with an admin role.
8access(all) contract StakingManager {
9    access(all) entitlement StakingOperator
10    access(all) entitlement StakingAdmin
11    access(all) entitlement StakingPauser
12
13    // Events
14    // Pause and unpause of the contract
15    access(all) event Paused();
16    access(all) event Unpaused();
17    // Update of reward fee
18    access(all) event RewardFeeSet(oldFee: UFix64, newFee: UFix64);
19
20    /// Signifies that the operator capability has been granted to an account. Note that current implementation only expects one operator.
21    access(all) event OperatorCapGranted(newOperator: Address)
22
23    /// Signifies that the pauser capability has been granted to an account.
24    access(all) event PauserCapGranted(newPauser: Address)
25    /// Signifies that the pauser capability has been granted to an account.
26    access(all) event PauserCapRevoked(oldPauser: Address)
27
28    /// Signifies that the admin changed the staking contract EVM address.
29    access(all) event EvmPoolChanged(old: EVM.EVMAddress, new: EVM.EVMAddress)
30
31    // Fields
32    access(all) var paused: Bool
33    access(all) var rewardFee: UFix64
34
35    // Structs
36    /// Represents a delegation target. 
37    /// Consists of a node ID and delegator ID to be used with FlowStakingCollection methods when delegating.
38    access(all) struct Delegation {
39        access(all) let nodeID: String
40        access(all) let delegatorID: UInt32
41
42        view init(nodeID: String, delegatorID: UInt32) {
43            self.nodeID = nodeID
44            self.delegatorID = delegatorID
45        }
46
47        /// Returns `true` if `_other` is equal to this instance of Delegation, `false` otherwise.
48        /// TODO: rework this when Equatable interface is available
49        access(all) view fun isEqual(_other: Delegation): Bool {
50            return self.nodeID == _other.nodeID && self.delegatorID == _other.delegatorID
51        }
52    }
53
54    /// Admin resource. Controls the unstaking vault, pause state of the StakingManager and can transfer ownership of itself and the operator capability.
55    access(all) resource Admin {
56        /// Pause the staking contract.
57        /// When paused, Staker resource cannot perform any actions.
58        /// Events:
59        ///     - `StakingManager.Paused`: emitted when the staker pause state is changed
60        access(StakingPauser) fun Pause() {
61            StakingManager.paused = true
62            emit StakingManager.Paused();
63        }
64
65        /// Unpause the staking contract.
66        /// Events:
67        ///     - `StakingManager.Unpaused`: emitted when the staker pause state is changed
68        // TODO: can this be upcast correctly?
69        access(StakingAdmin) fun Unpause() {
70            StakingManager.paused = false
71            emit StakingManager.Unpaused();
72        }
73
74        /// Publish a Pauser capability to an account.
75        /// Args:
76        ///     - `pauser`: Address that will be granted the Pauser capability
77        /// Events:
78        ///     - `StakingManager.PauserCapGranted`: emitted when the staker pause state is changed
79        access(StakingAdmin) fun AddPauser(pauser: Address) {
80            pre {
81                getAccount(pauser) != nil: "Pauser address does not belong to an account"
82                StakingManager.account.storage.check<@StakingManager.Admin>(from: StakingManager.StakingManagerAdminStoragePath): "Cannot borrow Admin resource from StakingManager owner"
83            }
84
85            StakingManager.PublishPauserCapability(pauserAddr: pauser)
86        }
87
88        /// Remove a Pauser capability from an account.
89        /// Events:
90        ///     - `StakingManager.Unpaused`: emitted when the staker pause state is changed
91        // TODO: can this be upcast correctly?
92        access(StakingAdmin) fun RemovePauser(pauser: Address) {
93            let pauserTag = pauser.toString()
94            var capRemoved = false
95            StakingManager.account.capabilities.storage.forEachController(
96                forPath: StakingManager.StakingManagerAdminStoragePath,
97                fun(c: &StorageCapabilityController): Bool {
98                    switch c.tag {
99                        case pauserTag:
100                            c.delete()
101                            capRemoved = true
102                            return false
103                    }
104                    return true
105            })
106
107            if(!capRemoved) {
108                panic("Did not remove capability from specified pauser - capability with tag not found")
109            }
110            emit StakingManager.PauserCapRevoked(oldPauser: pauser);
111        }
112
113        /// Add a delegation target to the Staker object.
114        /// Args:
115        ///     - `_stakingNodeId`: The new staking node ID
116        ///     - `_delegatorId`: The new delegator ID
117        /// Returns:
118        ///     The new delegation target's ID in the delegation target array.
119        access(StakingAdmin) fun AddDelegation(_stakingNodeId: String, _delegatorId: UInt32): Int {
120            let staker = StakingManager.account.storage.borrow<&StakingManager.Staker>(
121                from: StakingManager.StakingManagerStakerStoragePath)
122                ?? panic("Cannot borrow Staker object from StakingManager account")
123
124            // TODO: add check on owner or in IDTableStaking that this node id + delegator exists
125            let newDelegation = Delegation(nodeID: _stakingNodeId, delegatorID: _delegatorId)
126
127            let delegations = staker.GetDelegationsRef()
128            assert(
129                // TODO: replace with `.contains` when Equatable interface is available 
130                delegations.filter(view fun(d: Delegation): Bool {
131                    return newDelegation.isEqual(_other: d)
132                }).length == 0,
133                message: "Cannot add duplicate delegation")
134
135            let outputIdx = delegations.length;
136            delegations.append(newDelegation)
137            return outputIdx;
138        }
139
140        /// Remove a delegation target to the Staker object by its index in the Delegations array.
141        /// Args:
142        ///     - `delegationIdx`: The node ID of the delegation target that will be removed
143        /// Returns:
144        ///     The removed delegation.
145        access(StakingAdmin) fun RemoveDelegation(delegationIdx: UInt): Delegation {
146            let staker = StakingManager.account.storage.borrow<auth(Insert | Mutate) &StakingManager.Staker>(
147                from: StakingManager.StakingManagerStakerStoragePath)
148                ?? panic("Cannot borrow Staker object from StakingManager account")
149
150            // NOTE: aborts if `delegationIdx` is out of bounds
151            let delegation = staker.GetDelegationsRef().remove(at: delegationIdx)
152            return delegation;
153        }
154
155        /// Transfers the operator role to a new address.
156        /// Args:
157        ///     - `newOperator`: The new address to transfer the operator role to.
158        /// Preconditions:
159        ///     - the `newOperator` address must have a Flow account
160        access(StakingAdmin) fun TransferOperator(newOperator: Address) {
161            pre {
162                getAccount(newOperator) != nil: "New operator address does not belong to an account"
163            }
164
165            var capRemoved = false
166            StakingManager.account.capabilities.storage.forEachController(
167                forPath: StakingManager.StakingManagerStakerStoragePath, 
168                fun(c: &StorageCapabilityController): Bool {
169                    switch c.tag {
170                        case StakingManager.EVMStakerCapTag:
171                            c.delete()
172                            capRemoved = true
173                            return false
174                    }
175                    return true
176            })
177
178            if(!capRemoved) {
179                panic("Did not remove capability from current operator - capability with EVM staker tag not found")
180            }
181
182            StakingManager.PublishOperatorCapability(operatorAddr: newOperator, tag: StakingManager.EVMStakerCapTag)
183        }
184
185        // Updates the staking contract address field in the Staking struct.
186        access(StakingAdmin) fun SetEvmPoolAddress(newPoolAddressHex: String) {
187            let staker = StakingManager.account.storage.borrow<&StakingManager.Staker>(
188                from: StakingManager.StakingManagerStakerStoragePath)
189                ?? panic("Cannot borrow Staker object from StakingManager account")
190            staker.SetEvmPoolAddress(newAddress: EVM.addressFromString(newPoolAddressHex))
191        }
192
193        // Updates the reward fee value.
194        access(StakingAdmin) fun SetRewardFee(newRewardFee: UFix64) {
195            assert(newRewardFee > 0.0 && newRewardFee < 1.0,
196                message: "Reward fee value must be between 0 and 1")
197            let oldFee = StakingManager.rewardFee
198            StakingManager.rewardFee = newRewardFee;
199            emit StakingManager.RewardFeeSet(oldFee: oldFee, newFee: newRewardFee)
200        }
201    }
202
203    // Staking resource containing the capacity to use the owner's StakingCollection object with fixed delegator and node IDs.
204    access(all) resource Staker {
205        access(all) let Delegations: [Delegation];
206        access(all) var EvmPoolAddress: EVM.EVMAddress
207
208        /// Returns a mutable reference to the Delegations array. Used in the Admin resource to modify delegation targets.
209        access(contract) fun GetDelegationsRef(): auth(Mutate) &[Delegation] {
210            return &self.Delegations;
211        }
212
213        /// Updates the StakingContractEvm field.
214        access(contract) fun SetEvmPoolAddress(newAddress: EVM.EVMAddress) {
215            let oldAddress = self.EvmPoolAddress
216            self.EvmPoolAddress = newAddress;
217            emit StakingManager.EvmPoolChanged(old: oldAddress, new: newAddress)
218        }
219
220        // TODO: maybe having a separate method for every operation is overkill. Could expose a ref to StakingCollection authorized with CollectionOwner to the operator. Need to find out what kind of damage that can do if operator leaks.
221
222        /// Stake a set amount to the specified delegation target.
223        access(StakingOperator) fun Stake(amount: UFix64, delegationTargetIdx: UInt) {
224            pre {
225                !StakingManager.paused : "StakingManager must not be paused";
226                // TODO: is this a valid check for array index?
227                self.Delegations[delegationTargetIdx] != nil: "delegationTargetIdx is out of bounds"
228            }
229            let coll = StakingManager.account.storage.borrow<auth(FlowStakingCollection.CollectionOwner) &FlowStakingCollection.StakingCollection>(from: FlowStakingCollection.StakingCollectionStoragePath) ?? panic("Could not borrow StakingCollection from StakingManager owner")
230            let delegation = self.Delegations[delegationTargetIdx]
231            coll.stakeNewTokens(
232                nodeID: delegation.nodeID,
233                delegatorID: delegation.delegatorID,
234                amount: amount)
235        }
236
237        /// Restake the previously unstaked tokens from the specified delegation target.
238        access(StakingOperator) fun RestakeUnstaked(amount: UFix64, delegationTargetIdx: UInt) {
239            pre {
240                !StakingManager.paused : "StakingManager must not be paused"
241                self.Delegations[delegationTargetIdx] != nil: "delegationTargetIdx is out of bounds"
242            }
243            let coll = StakingManager.account.storage.borrow<auth(FlowStakingCollection.CollectionOwner) &FlowStakingCollection.StakingCollection>(from: FlowStakingCollection.StakingCollectionStoragePath) ?? panic("Could not borrow StakingCollection from StakingManager owner")
244            let delegation = self.Delegations[delegationTargetIdx]
245            coll.stakeUnstakedTokens(
246                nodeID: delegation.nodeID,
247                delegatorID: delegation.delegatorID,
248                amount: amount)
249        }
250
251        /// Restake the received reward tokens from the specified delegation target.
252        access(StakingOperator) fun RestakeRewards(amount: UFix64, delegationTargetIdx: UInt) {
253            pre {
254                !StakingManager.paused : "StakingManager must not be paused"
255                self.Delegations[delegationTargetIdx] != nil: "delegationTargetIdx is out of bounds"
256            }
257            let coll = StakingManager.account.storage.borrow<auth(FlowStakingCollection.CollectionOwner) &FlowStakingCollection.StakingCollection>(from: FlowStakingCollection.StakingCollectionStoragePath) ?? panic("Could not borrow StakingCollection from StakingManager owner")
258            let delegation = self.Delegations[delegationTargetIdx]
259            coll.stakeRewardedTokens(
260                nodeID: delegation.nodeID,
261                delegatorID: delegation.delegatorID,
262                amount: amount)
263        }
264
265        /// Unstake a set amount from the specified delegation target
266        /// NOTE: unstake will only arrive in the vault after 2 epochs
267        access(StakingOperator) fun RequestUnstake(amount: UFix64, delegationTargetIdx: UInt) {
268            pre {
269                !StakingManager.paused : "StakingManager must not be paused"
270                self.Delegations[delegationTargetIdx] != nil: "delegationTargetIdx is out of bounds"
271            }
272            let coll = StakingManager.account.storage.borrow<auth(FlowStakingCollection.CollectionOwner) &FlowStakingCollection.StakingCollection>(from: FlowStakingCollection.StakingCollectionStoragePath) ?? panic("Could not borrow StakingCollection from StakingManager owner")
273            let delegation = self.Delegations[delegationTargetIdx]
274            coll.requestUnstaking(
275                nodeID: delegation.nodeID,
276                delegatorID: delegation.delegatorID,
277                amount: amount)
278        }
279
280        /// Withdraw the unstaked tokens to the owner's unstake vault from the specified delegation target
281        access(StakingOperator) fun WithdrawUnstaked(amount: UFix64, delegationTargetIdx: UInt) {
282            pre {
283                !StakingManager.paused : "StakingManager must not be paused"
284                self.Delegations[delegationTargetIdx] != nil: "delegationTargetIdx is out of bounds"
285            }
286            let coll = StakingManager.account.storage.borrow<auth(FlowStakingCollection.CollectionOwner) &FlowStakingCollection.StakingCollection>(from: FlowStakingCollection.StakingCollectionStoragePath) ?? panic("Could not borrow StakingCollection from StakingManager owner")
287            let delegation = self.Delegations[delegationTargetIdx]
288            coll.withdrawUnstakedTokens(
289                nodeID: delegation.nodeID,
290                delegatorID: delegation.delegatorID,
291                amount: amount)
292        }
293
294        /// Withdraw the rewarded tokens to the owner's unstake vault from the specified delegation target, subtracting the fee proportion specified in the contract
295        access(StakingOperator) fun WithdrawRewarded(amount: UFix64, delegationTargetIdx: UInt) {
296            pre {
297                !StakingManager.paused : "StakingManager must not be paused"
298                self.Delegations[delegationTargetIdx] != nil: "delegationTargetIdx is out of bounds"
299            }
300            let coll = StakingManager.account.storage.borrow<auth(FlowStakingCollection.CollectionOwner) &FlowStakingCollection.StakingCollection>(from: FlowStakingCollection.StakingCollectionStoragePath) ?? panic("Could not borrow StakingCollection from StakingManager owner")
301            let delegation = self.Delegations[delegationTargetIdx]
302            coll.withdrawRewardedTokens(
303                nodeID: delegation.nodeID,
304                delegatorID: delegation.delegatorID,
305                amount: amount)
306
307            // after rewards withdrawn, move fee to the treasury
308            let feeAmount = amount * StakingManager.rewardFee
309            let operatorVaultRef = StakingManager.account.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(
310                from: StakingManager.StakingManagerVaultStoragePath)
311                ?? panic("Could not borrow staking FLOW vault from StakingManager owner")
312            let treasuryCOA = StakingManager.account.storage.borrow<&EVM.CadenceOwnedAccount>(
313                from: StakingManager.StakingManagerTreasuryStoragePath)
314                ?? panic("Could not borrow treasury COA from StakingManager owner")
315            treasuryCOA.deposit(from: <- operatorVaultRef.withdraw(amount: feeAmount) as! @FlowToken.Vault)
316        }
317
318        /// Transfer `amount` from the operator's COA to the operator's staking vault to allow for the tokens to be staked.
319        access(StakingOperator) fun TransferStakedFromEVM(amount: UFix64) {
320            pre {
321                !StakingManager.paused : "StakingManager must not be paused"
322            }
323            let ownerCOA = StakingManager.account.storage.borrow<auth(EVM.Withdraw) &EVM.CadenceOwnedAccount>(
324                from: StakingManager.StakingManagerCOAStoragePath)
325                ?? panic("Could not borrow staking COA from StakingManager owner")
326
327            assert(ownerCOA.balance().inFLOW() >= amount,
328                message: "Insufficient balance of staking COA")
329
330            let operatorVaultRef = StakingManager.account.storage.borrow<&FlowToken.Vault>(
331                from: StakingManager.StakingManagerVaultStoragePath)
332                ?? panic("Could not borrow staking FLOW vault from StakingManager owner")
333
334            let balance = EVM.Balance(attoflow: 0);
335            balance.setFLOW(flow: amount)
336            operatorVaultRef.deposit(from: <- ownerCOA.withdraw(balance: balance))
337        }
338
339        /// Transfer `amount` from the operator's staking vault directly to the FlowEVMPool staking contract to allow for redistribution of rewards.
340        access(StakingOperator) fun TransferUnstakedToEVM(amount: UFix64, gasLimit: UInt64) {
341            pre {
342                !StakingManager.paused : "StakingManager must not be paused"
343            }
344            // TODO: assume owner's vault is already funded
345            let operatorVaultRef = StakingManager.account.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(
346                from: StakingManager.StakingManagerVaultStoragePath)
347                ?? panic("Could not borrow staking FLOW vault from StakingManager owner")
348            assert(operatorVaultRef.isAvailableToWithdraw(amount: amount),
349                message: "Insufficient balance of staking FLOW vault: need ".concat(amount.toString()).concat(", have ").concat(operatorVaultRef.balance.toString()))
350
351            let ownerCOA = StakingManager.account.storage.borrow<auth(EVM.Call) &EVM.CadenceOwnedAccount>(
352                from: StakingManager.StakingManagerCOAStoragePath)
353                ?? panic("Could not borrow staking COA from StakingManager owner")
354
355            ownerCOA.deposit(from: <- operatorVaultRef.withdraw(amount: amount) as! @FlowToken.Vault)
356
357            let balance = EVM.Balance(attoflow: 0);
358            balance.setFLOW(flow: amount)
359            let callResult = ownerCOA.call(
360                to: self.EvmPoolAddress,
361                data: [],
362                // gasLimit: 10000, // TODO: determine precise value or pass as var
363                gasLimit: gasLimit,
364                value: balance,
365            )
366            assert(
367                callResult.status == EVM.Status.successful, 
368                message: "Transfer call to pool failed with status".concat(callResult.status.rawValue.toString()).concat(", error ").concat(callResult.errorCode.toString()).concat(": ").concat(callResult.errorMessage))
369        }
370
371        /// Transfer `amount` from COA bridge address to pool
372        access(StakingOperator) fun TransferToEVMPool(amount: UFix64, gasLimit: UInt64) {
373            let ownerCOA = StakingManager.account.storage.borrow<auth(EVM.Call) &EVM.CadenceOwnedAccount>(
374                from: StakingManager.StakingManagerCOAStoragePath)
375                ?? panic("Could not borrow staking COA from StakingManager owner")
376            let balance = EVM.Balance(attoflow: 0);
377            balance.setFLOW(flow: amount)
378            let callResult = ownerCOA.call(
379                to: self.EvmPoolAddress,
380                data: [],
381                gasLimit: gasLimit,
382                value: balance,
383            )
384            assert(
385                callResult.status == EVM.Status.successful, 
386                message: "Transfer call to pool failed with status".concat(callResult.status.rawValue.toString()).concat(", error ").concat(callResult.errorCode.toString()).concat(": ").concat(callResult.errorMessage))
387        }
388
389        init(
390            evmPoolAddress: EVM.EVMAddress
391        ) {
392            self.Delegations = [];
393            self.EvmPoolAddress = evmPoolAddress
394
395            // make sure owner has a separate FLOW vault in StakingManagerVaultStoragePath, store a capability
396            if !StakingManager.account.storage.check<@FlowToken.Vault>(
397                from: StakingManager.StakingManagerVaultStoragePath) {
398                let defaultVault = StakingManager.account.storage.borrow<&FlowToken.Vault>(
399                    from: /storage/flowTokenVault) ?? panic("Cannot borrow main FLOW vault from owner")
400
401                StakingManager.account.storage.save(<- defaultVault.createEmptyVault(), to: StakingManager.StakingManagerVaultStoragePath)
402            }
403        }
404    }
405
406    access(all) let StakingManagerStakerStoragePath: StoragePath
407
408    access(all) let StakingManagerOperatorStoragePath: StoragePath
409    access(all) let StakingManagerAdminStoragePath: StoragePath
410    access(all) let StakingManagerVaultStoragePath: StoragePath
411    access(all) let StakingManagerCOAStoragePath: StoragePath
412    access(all) let StakingManagerPauserStoragePath: StoragePath;
413    access(all) let StakingManagerTreasuryStoragePath: StoragePath;
414
415    access(all) let OperatorCapInboxName: String
416    access(all) let PauserCapInboxName: String
417    access(self) let EVMStakerCapTag: String
418
419    /// Issues an operator capability and publishes it to the specified operator address. If `tag` is provided, the capability is tagged with it.
420    ///
421    /// - Parameters:
422    ///   - `operatorAddr`: The address to which the operator capability is published.
423    ///   - `tag`: An optional tag for the capability (default is nil).
424    access (self) fun PublishOperatorCapability(operatorAddr: Address, tag: String?) {
425        pre {
426            self.account.storage.check<@StakingManager.Staker>(from: self.StakingManagerStakerStoragePath): "Cannot borrow Staker resource from StakingManager owner"
427        }
428
429        let stakerOperatorCap = self.account.capabilities.storage.issue<auth(StakingManager.StakingOperator) &StakingManager.Staker>(
430            self.StakingManagerStakerStoragePath)
431        if !stakerOperatorCap.check() {
432            panic("Cannot issue capability for Staker resource")
433        }
434
435        if(tag != nil) {
436            let capController = self.account.capabilities.storage.getController(byCapabilityID: stakerOperatorCap.id) ?? panic("Cannot get capability controller for Staker resource.")
437            capController.setTag(tag!)
438        }
439
440        self.account.inbox.publish(stakerOperatorCap, name: self.OperatorCapInboxName, recipient: operatorAddr)
441        emit StakingManager.OperatorCapGranted(newOperator: operatorAddr);
442    }
443
444    /// Issues a pauser capability and publishes it to the specified address. The capability is tagged with the address of the pauser.
445    ///
446    /// - Parameters:
447    ///   - `pauserAddr`: The address to which the pauser capability is published.
448    access (self) fun PublishPauserCapability(pauserAddr: Address) {
449        pre {
450            self.account.storage.check<@StakingManager.Admin>(from: self.StakingManagerAdminStoragePath): "Cannot borrow Admin resource from StakingManager owner"
451        }
452
453        let pauserCap = self.account.capabilities.storage.issue<auth(StakingManager.StakingPauser) &StakingManager.Admin>(
454            self.StakingManagerAdminStoragePath)
455        if !pauserCap.check() {
456            panic("Cannot issue Pauser capability for Admin resource")
457        }
458
459        let capController = self.account.capabilities.storage.getController(byCapabilityID: pauserCap.id) ?? panic("Cannot get capability controller for Admin resource.")
460        capController.setTag(pauserAddr.toString())
461
462        self.account.inbox.publish(pauserCap, name: self.PauserCapInboxName, recipient: pauserAddr)
463        emit StakingManager.PauserCapGranted(newPauser: pauserAddr);
464    }
465
466    init(operator: Address, evmPoolAddressHex: String) {
467        pre {
468            getAccount(operator) != nil: "Operator address does not belong to an account"
469        }
470        // set up constants storage addresses for the contract
471        // start off paused
472        self.paused = true
473        // default reward fee - 10%
474        self.rewardFee = 0.1
475
476        self.StakingManagerStakerStoragePath = /storage/StakingManager
477        self.StakingManagerOperatorStoragePath = /storage/stakingManagerOperator
478        self.StakingManagerAdminStoragePath = /storage/stakingManagerAdmin
479        self.StakingManagerVaultStoragePath = /storage/stakingManagerVault
480        self.StakingManagerCOAStoragePath = /storage/stakingManagerCOA
481        self.StakingManagerTreasuryStoragePath = /storage/stakingManagerTreasury
482        self.StakingManagerPauserStoragePath = /storage/stakingManagerPauser
483        self.OperatorCapInboxName = "StakingManagerOperatorCap"
484        self.PauserCapInboxName = "StakingManagerPauserCap"
485        self.EVMStakerCapTag = "FlowEVMStaker"
486
487        // owner needs StakingCollection for staking
488        if(!self.account.storage.check<@FlowStakingCollection.StakingCollection>(
489            from: FlowStakingCollection.StakingCollectionStoragePath)) {
490            log("Cannot deploy StakingManager to account without a StakingCollection resource - creating StakingCollection")
491
492            // Create private capability for the token holder vault
493            var stakingFlowVaultCap: Capability<auth(FungibleToken.Withdraw) &FlowToken.Vault>? = nil
494            let path = self.StakingManagerVaultStoragePath
495            // Create separate staking vault
496            if(!self.account.storage.check<auth(FungibleToken.Withdraw) &FlowToken.Vault>(from: path)) {
497                let flowVault = self.account.storage.borrow<&FlowToken.Vault>(
498                    from: /storage/flowTokenVault) ?? panic("Cannot borrow main FLOW vault from owner")
499                    
500                self.account.storage.save(<-flowVault.createEmptyVault(), to: path)
501            }
502            stakingFlowVaultCap = self.account.capabilities.storage.issue<auth(FungibleToken.Withdraw) &FlowToken.Vault>(path)
503
504            // Create a new Staking Collection and put it in storage
505            self.account.storage.save(
506                <- FlowStakingCollection.createStakingCollection(
507                    unlockedVault: stakingFlowVaultCap!,
508                    tokenHolder: nil
509                ),
510                to: FlowStakingCollection.StakingCollectionStoragePath
511            )
512
513            // Publish a capability to the created staking collection.
514            let stakingCollectionCap = self.account.capabilities.storage.issue<&FlowStakingCollection.StakingCollection>(
515                FlowStakingCollection.StakingCollectionStoragePath
516            )
517
518            self.account.capabilities.publish(
519                stakingCollectionCap,
520                at: FlowStakingCollection.StakingCollectionPublicPath
521            )
522        } else {
523            log("Proceeding with existing StakingCollection resource")
524        }
525
526        // fail if owner has no COA at the main COA path - it is required for staking
527        assert(
528            self.account.storage.check<@EVM.CadenceOwnedAccount>(
529                from: self.StakingManagerCOAStoragePath), 
530            message: "Cannot deploy StakingManager to account without a COA resource.")
531        // fail if owner has no COA at the treasury path - it is required for staking
532        assert(
533            self.account.storage.check<@EVM.CadenceOwnedAccount>(
534                from: self.StakingManagerTreasuryStoragePath), 
535            message: "Cannot deploy StakingManager to account without a treasury resource.")
536
537        // store staker and admin
538        let evmPoolAddress = EVM.addressFromString(evmPoolAddressHex)
539
540        self.account.storage.save(
541            <- create self.Staker(evmPoolAddress: evmPoolAddress), 
542            to: StakingManager.StakingManagerStakerStoragePath)
543        self.account.storage.save(
544            <- create StakingManager.Admin(), 
545            to: StakingManager.StakingManagerAdminStoragePath)
546
547        // publish StakingOperator capability to operator address
548        StakingManager.PublishOperatorCapability(operatorAddr: operator, tag: StakingManager.EVMStakerCapTag)
549    }
550}