Smart Contract

FRC20StakingManager

A.d2abb5dbf5e08666.FRC20StakingManager

Valid From

86,128,956

Deployed

2d ago
Feb 24, 2026, 11:54:28 PM UTC

Dependents

16 imports
1/**
2> Author: Fixes Lab <https://github.com/fixes-world/>
3
4# FRC20 Staking Manager
5
6The staking manager contract is responsible for managing the staking pools and the staking process.
7It allows users to stake their FRC20 tokens, unstake them, claim rewards, and donate tokens to the staking pools.
8
9*/
10// Third Party Imports
11import FungibleToken from 0xf233dcee88fe0abe
12import FlowToken from 0x1654653399040a61
13import NonFungibleToken from 0x1d7e57aa55817448
14import MetadataViews from 0x1d7e57aa55817448
15import ViewResolver from 0x1d7e57aa55817448
16import Burner from 0xf233dcee88fe0abe
17// Fixes Imports
18import Fixes from 0xd2abb5dbf5e08666
19import FixesInscriptionFactory from 0xd2abb5dbf5e08666
20import FixesHeartbeat from 0xd2abb5dbf5e08666
21import FRC20Indexer from 0xd2abb5dbf5e08666
22import FRC20FTShared from 0xd2abb5dbf5e08666
23import FRC20SemiNFT from 0xd2abb5dbf5e08666
24import FRC20AccountsPool from 0xd2abb5dbf5e08666
25import FRC20Staking from 0xd2abb5dbf5e08666
26import FRC20StakingVesting from 0xd2abb5dbf5e08666
27import FRC20StakingForwarder from 0xd2abb5dbf5e08666
28
29access(all) contract FRC20StakingManager {
30
31    access(all) entitlement Admin
32    access(all) entitlement Manage
33
34    /* --- Events --- */
35    /// Event emitted when the contract is initialized
36    access(all) event ContractInitialized()
37    /// Event emitted when the whitelist is updated
38    access(all) event StakingWhitelistUpdated(address: Address, isWhitelisted: Bool)
39    /// Event emitted when a staking pool is enabled
40    access(all) event StakingPoolEnabled(tick: String, address: Address, by: Address)
41    /// Event emitted when a staking pool resources are updated
42    access(all) event StakingPoolResourcesUpdated(tick: String, address: Address, by: Address)
43    /// Event emitted when a reward strategy is registered
44    access(all) event StakingPoolRewardStrategyRegistered(tick: String, rewardTick: String, by: Address)
45    /// Event emitted when a staking pool is donated
46    access(all) event StakingPoolDonated(tick: String, rewardTick: String, amount: UFix64, by: Address)
47    /// Event emitted when a staking pool is donated as vesting
48    access(all) event StakingPoolDonatedAsVesting(tick: String, rewardTick: String, amount: UFix64, by: Address, batchAmount: UInt32, interval: UFix64)
49
50    /* --- Variable, Enums and Structs --- */
51    access(all)
52    let StakingAdminStoragePath: StoragePath
53    access(all)
54    let StakingAdminPublicPath: PublicPath
55    access(all)
56    let StakingControllerStoragePath: StoragePath
57
58    /* --- Interfaces & Resources --- */
59
60    /// Staking Admin Public Resource interface
61    ///
62    access(all) resource interface StakingAdminPublic {
63        access(all)
64        view fun isWhitelisted(address: Address): Bool
65    }
66
67    /// Staking Admin Resource, represents a staking admin and store in admin's account
68    ///
69    access(all) resource StakingAdmin: StakingAdminPublic {
70        access(self)
71        let whitelist: {Address: Bool}
72
73        init() {
74            self.whitelist = {}
75        }
76
77        access(all)
78        view fun isWhitelisted(address: Address): Bool {
79            return self.whitelist[address] ?? false
80        }
81
82        access(Admin)
83        fun updateWhitelist(address: Address, isWhitelisted: Bool) {
84            self.whitelist[address] = isWhitelisted
85
86            emit StakingWhitelistUpdated(address: address, isWhitelisted: isWhitelisted)
87        }
88    }
89
90    /// Staking Controller Resource, represents a staking controller
91    ///
92    access(all) resource StakingController {
93
94        /// Returns the address of the controller
95        ///
96        access(all)
97        view fun getControllerAddress(): Address {
98            return self.owner?.address ?? panic("The controller is not stored in the account")
99        }
100
101        /// Create a new staking pool
102        ///
103        access(Manage)
104        fun enableAndCreateFRC20Staking(
105            tick: String,
106            newAccount: Capability<auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account>,
107        ){
108            pre {
109                FRC20StakingManager.isWhitelisted(self.getControllerAddress()): "The controller is not whitelisted"
110            }
111
112            // singleton resources
113            let frc20Indexer = FRC20Indexer.getIndexer()
114            let acctsPool = FRC20AccountsPool.borrowAccountsPool()
115
116            // Check if the token is already registered
117            let tokenMeta = frc20Indexer.getTokenMeta(tick: tick.toLower()) ?? panic("The token is not registered")
118            // no need to check if deployer is whitelisted, because the controller is whitelisted
119
120            // create the account for the staking at the accounts pool
121            acctsPool.setupNewChildForStaking(
122                tick: tokenMeta.tick,
123                newAccount
124            )
125
126            // ensure all market resources are available
127            self.ensureStakingResourcesAvailable(tick: tokenMeta.tick)
128
129            // emit the event
130            emit StakingPoolEnabled(
131                tick: tokenMeta.tick,
132                address: acctsPool.getFRC20StakingAddress(tick: tokenMeta.tick) ?? panic("The staking account was not created"),
133                by: self.getControllerAddress()
134            )
135        }
136
137        /// Ensure all staking resources are available
138        ///
139        access(Manage)
140        fun ensureStakingResourcesAvailable(tick: String) {
141            pre {
142                FRC20StakingManager.isWhitelisted(self.getControllerAddress()): "The controller is not whitelisted"
143            }
144
145            let isUpdated = FRC20StakingManager._ensureStakingResourcesAvailable(tick: tick)
146
147            if isUpdated {
148                // singleton resources
149                let acctsPool = FRC20AccountsPool.borrowAccountsPool()
150
151                emit StakingPoolResourcesUpdated(
152                    tick: tick,
153                    address: acctsPool.getFRC20StakingAddress(tick: tick) ?? panic("The staking account was not created"),
154                    by: self.getControllerAddress()
155                )
156            }
157        }
158
159        /// Donate all shared pool FLOW tokens to the vesting pool
160        ///
161        access(Manage)
162        fun donateAllSharedPoolFlowTokenToVesting(
163            tick: String,
164            childType: FRC20AccountsPool.ChildAccountType,
165            vestingBatchAmount: UInt32,
166            vestingInterval: UFix64,
167        ) {
168            pre {
169                FRC20StakingManager.isWhitelisted(self.getControllerAddress()): "The controller is not whitelisted"
170            }
171
172            // singleton resources
173            let acctsPool = FRC20AccountsPool.borrowAccountsPool()
174            // try to borrow the account to check if it was created
175            let childAcctRef = acctsPool.borrowChildAccount(type: childType, nil)
176                ?? panic("The shared pool account was not created")
177
178            let flowVaultRef = childAcctRef.storage
179                .borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(from: /storage/flowTokenVault)
180                ?? panic("The flow vault is not found")
181            let storageUsageBalance = UFix64(childAcctRef.storage.used) / UFix64(childAcctRef.storage.capacity) * flowVaultRef.balance
182            // Keep the 2x of the storage usage balance
183            let availbleBalance = flowVaultRef.balance - storageUsageBalance * 2.0 - 0.01
184            // ensure the flow balance is enough
185            assert(
186                availbleBalance > 0.0,
187                message: "The flow balance is not enough"
188            )
189
190            // withdraw the flow tokens
191            self._donateTokenToVesting(
192                vault: <- flowVaultRef.withdraw(amount: availbleBalance),
193                tick: tick,
194                vestingBatchAmount: vestingBatchAmount,
195                vestingInterval: vestingInterval
196            )
197        }
198
199        /// Donate FLOW in the child account to the vesting pool
200        ///
201        access(Manage)
202        fun donateFlowTokenToVesting(
203            tick: String,
204            amount: UFix64,
205            vestingBatchAmount: UInt32,
206            vestingInterval: UFix64,
207        ) {
208            pre {
209                FRC20StakingManager.isWhitelisted(self.getControllerAddress()): "The controller is not whitelisted"
210            }
211
212            // singleton resources
213            let acctsPool = FRC20AccountsPool.borrowAccountsPool()
214            // try to borrow the account to check if it was created
215            let childAcctRef = acctsPool.borrowChildAccount(type: FRC20AccountsPool.ChildAccountType.Staking, tick)
216                ?? panic("The staking account was not created")
217
218            let flowVaultRef = childAcctRef.storage
219                .borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(from: /storage/flowTokenVault)
220                ?? panic("The flow vault is not found")
221            let storageUsageBalance = UFix64(childAcctRef.storage.used) / UFix64(childAcctRef.storage.capacity) * flowVaultRef.balance
222            // Keep the 2x of the storage usage balance
223            let availbleBalance = flowVaultRef.balance - storageUsageBalance * 2.0 - 0.01
224            // ensure the flow balance is enough
225            assert(
226                availbleBalance >= amount,
227                message: "The flow balance is not enough"
228            )
229
230            // withdraw the flow tokens
231            self._donateTokenToVesting(
232                vault: <- flowVaultRef.withdraw(amount: amount),
233                tick: tick,
234                vestingBatchAmount: vestingBatchAmount,
235                vestingInterval: vestingInterval
236            )
237        }
238
239        /// Donate FLOW in the child account to the vesting pool
240        ///
241        access(self)
242        fun _donateTokenToVesting(
243            vault: @{FungibleToken.Vault},
244            tick: String,
245            vestingBatchAmount: UInt32,
246            vestingInterval: UFix64,
247        ) {
248            let systemAddr = FRC20StakingManager.account.address
249            // convert to change
250            let changeToDonate <- FRC20FTShared.wrapFungibleVaultChange(
251                ftVault: <- vault,
252                from: systemAddr
253            )
254            // call the internal method to donate
255            FRC20StakingManager.donateToVestingFromChange(
256                changeToDonate: <- changeToDonate,
257                tick: tick,
258                vestingBatchAmount: vestingBatchAmount,
259                vestingInterval: vestingInterval
260            )
261        }
262
263        /// Register a reward strategy
264        ///
265        access(Manage)
266        fun registerRewardStrategy(
267            stakeTick: String,
268            rewardTick: String,
269        ) {
270            // singleton resources
271            let acctPool = FRC20AccountsPool.borrowAccountsPool()
272            let frc20Indexer = FRC20Indexer.getIndexer()
273
274            let poolAddr = acctPool.getFRC20StakingAddress(tick: stakeTick)
275                ?? panic("The staking pool is not enabled")
276            let pool = FRC20Staking.borrowPool(poolAddr)
277                ?? panic("The staking pool is not found")
278
279            // Check if the reward token is registered
280            let isFlowFT = rewardTick == "" || CompositeType(rewardTick) != nil
281            assert(
282                isFlowFT || frc20Indexer.getTokenMeta(tick: rewardTick) != nil,
283                message: "The reward token is not registered"
284            )
285            // Check if the reward strategy is already registered
286            assert(
287                pool.getRewardDetails(rewardTick) == nil,
288                message: "The reward strategy is already registered"
289            )
290            assert(
291                pool.tick == stakeTick,
292                message: "The staking pool tick is not the same as the requested"
293            )
294
295            // Check if the controller is whitelisted or staked enough tokens
296            let controlleAddr = self.getControllerAddress()
297            assert(
298                FRC20StakingManager.isEligibleForRegistering(stakeTick: stakeTick, addr: controlleAddr),
299                message: "The controller is not whitelisted or staked enough tokens"
300            )
301
302            pool.registerRewardStrategy(rewardTick: rewardTick)
303
304            emit StakingPoolRewardStrategyRegistered(
305                tick: stakeTick,
306                rewardTick: rewardTick,
307                by: self.getControllerAddress()
308            )
309        }
310    }
311
312    /* --- Contract access methods  --- */
313
314    access(contract)
315    fun _ensureStakingResourcesAvailable(tick: String): Bool {
316        let acctsPool = FRC20AccountsPool.borrowAccountsPool()
317
318        // try to borrow the account to check if it was created
319        let childAcctRef = acctsPool.borrowChildAccount(type: FRC20AccountsPool.ChildAccountType.Staking, tick)
320            ?? panic("The staking account was not created")
321
322        var isUpdated = false
323        // The staking pool should have the following resources in the account:
324        // - FRC20Staking.Pool: Pool resource
325        // - FRC20FTShared.SharedStore: Staking Pool configuration
326        // - FRC20FTShared.Hooks: Hooks for the staking pool
327        // - FixesHeartbeat.IHeartbeatHook: Register to FixesHeartbeat with the scope of "Staking:<tick>"
328        // - FRC20StakingVesting.Vault: Vesting vault for the staking pool
329        // - FRC20StakingForwarder.Forwarder: Forward $FLOW to the staking pool
330
331        if let pool = childAcctRef.storage.borrow<&FRC20Staking.Pool>(from: FRC20Staking.StakingPoolStoragePath) {
332            assert(
333                pool.tick == tick,
334                message: "The staking pool tick is not the same as the requested"
335            )
336        } else {
337            // create the staking and save it in the account
338            let pool <- FRC20Staking.createPool(tick)
339            // save the staking in the account
340            childAcctRef.storage.save(<- pool, to: FRC20Staking.StakingPoolStoragePath)
341            // reference of stake pool
342            let poolRef = childAcctRef.storage.borrow<&FRC20Staking.Pool>(from: FRC20Staking.StakingPoolStoragePath)
343                ?? panic("The staking pool is not found")
344            poolRef.initialize()
345
346            // link the staking to the public path
347            childAcctRef.capabilities.unpublish(FRC20Staking.StakingPoolPublicPath)
348            childAcctRef.capabilities.publish(
349                childAcctRef.capabilities.storage.issue<&FRC20Staking.Pool>(FRC20Staking.StakingPoolStoragePath),
350                at: FRC20Staking.StakingPoolPublicPath
351            )
352
353            isUpdated = true
354        }
355
356        // create the shared store and save it in the account
357        if childAcctRef.storage.borrow<&AnyResource>(from: FRC20FTShared.SharedStoreStoragePath) == nil {
358            let sharedStore <- FRC20FTShared.createSharedStore()
359            childAcctRef.storage.save(<- sharedStore, to: FRC20FTShared.SharedStoreStoragePath)
360            // link the shared store to the public path
361            childAcctRef.capabilities.unpublish(FRC20FTShared.SharedStorePublicPath)
362            childAcctRef.capabilities.publish(
363                childAcctRef.capabilities.storage.issue<&FRC20FTShared.SharedStore>(FRC20FTShared.SharedStoreStoragePath),
364                at: FRC20FTShared.SharedStorePublicPath
365            )
366
367            isUpdated = true || isUpdated
368        }
369
370        // create the hooks and save it in the account
371        if childAcctRef.storage.borrow<&AnyResource>(from: FRC20FTShared.TransactionHookStoragePath) == nil {
372            let hooks <- FRC20FTShared.createHooks()
373            childAcctRef.storage.save(<- hooks, to: FRC20FTShared.TransactionHookStoragePath)
374
375            isUpdated = true || isUpdated
376        }
377
378        // link the hooks to the public path
379        if childAcctRef
380            .capabilities.get<&FRC20FTShared.Hooks>(FRC20FTShared.TransactionHookPublicPath)
381            .borrow() == nil {
382            // link the hooks to the public path
383            childAcctRef.capabilities.unpublish(FRC20FTShared.TransactionHookPublicPath)
384            childAcctRef.capabilities.publish(
385                childAcctRef.capabilities.storage.issue<&FRC20FTShared.Hooks>(FRC20FTShared.TransactionHookStoragePath),
386                at: FRC20FTShared.TransactionHookPublicPath
387            )
388
389            isUpdated = true || isUpdated
390        }
391
392        // Register to FixesHeartbeat
393        let heartbeatScope = "Staking:".concat(tick)
394        if !FixesHeartbeat.hasHook(scope: heartbeatScope, hookAddr: childAcctRef.address) {
395            FixesHeartbeat.addHook(
396                scope: heartbeatScope,
397                hookAddr: childAcctRef.address,
398                hookPath: FRC20FTShared.TransactionHookPublicPath
399            )
400
401            isUpdated = true || isUpdated
402        }
403
404        // Add hooks to the shared hooks reference
405
406        // borrow the hooks reference
407        let hooksRef = childAcctRef.storage
408            .borrow<auth(FRC20FTShared.Manage) &FRC20FTShared.Hooks>(from: FRC20FTShared.TransactionHookStoragePath)
409            ?? panic("The hooks were not created")
410
411        // --- Vesting ---
412
413        if childAcctRef.storage.borrow<&AnyResource>(from: FRC20StakingVesting.storagePath) == nil {
414            let vesting <- FRC20StakingVesting.createVestingVault()
415            childAcctRef.storage.save(<- vesting, to: FRC20StakingVesting.storagePath)
416
417            isUpdated = true || isUpdated
418        }
419
420        if childAcctRef
421            .capabilities.get<&FRC20StakingVesting.Vault>(FRC20StakingVesting.publicPath)
422            .borrow() == nil {
423            // link the vesting to the public path
424            childAcctRef.capabilities.unpublish(FRC20StakingVesting.publicPath)
425            childAcctRef.capabilities.publish(
426                childAcctRef.capabilities.storage.issue<&FRC20StakingVesting.Vault>(FRC20StakingVesting.storagePath),
427                at: FRC20StakingVesting.publicPath
428            )
429
430            isUpdated = true || isUpdated
431        }
432
433        // add vesting to hooks
434
435        let vestingHookCap = childAcctRef
436            .capabilities.get<&FRC20StakingVesting.Vault>(FRC20StakingVesting.publicPath)
437        let vestingHookRef = vestingHookCap.borrow()
438            ?? panic("Could not borrow the vesting hook reference")
439        if !hooksRef.hasHook(vestingHookRef.getType()) {
440            hooksRef.addHook(vestingHookCap)
441
442            isUpdated = true || isUpdated
443        }
444
445        // --- Forwarder ---
446
447        // This is the standard receiver path of FlowToken
448        let flowReceiverPath = /public/flowTokenReceiver
449        // check if the forwarder is already created
450        if childAcctRef.storage.borrow<&AnyResource>(from: FRC20StakingForwarder.StakingForwarderStoragePath) == nil {
451            let forwarder <- FRC20StakingForwarder.createNewForwarder(childAcctRef.address)
452            childAcctRef.storage.save(<- forwarder, to: FRC20StakingForwarder.StakingForwarderStoragePath)
453
454            // link public interface
455            childAcctRef.capabilities.publish(
456                childAcctRef.capabilities.storage.issue<&FRC20StakingForwarder.Forwarder>(FRC20StakingForwarder.StakingForwarderStoragePath),
457                at: FRC20StakingForwarder.StakingForwarderPublicPath
458            )
459
460            isUpdated = true || isUpdated
461        }
462
463         // Unlink the existing receiver capability for flowReceiverPath
464        if childAcctRef.capabilities.get<&{FungibleToken.Receiver}>(flowReceiverPath).check() {
465            // link the forwarder to the public path
466            childAcctRef.capabilities.unpublish(flowReceiverPath)
467            // Link the new forwarding receiver capability
468            childAcctRef.capabilities.publish(
469                childAcctRef.capabilities.storage.issue<&{FungibleToken.Receiver}>(FRC20StakingForwarder.StakingForwarderStoragePath),
470                at: flowReceiverPath
471            )
472
473            // link the FlowToken to the forwarder fallback path
474            let fallbackPath = Fixes.getFallbackFlowTokenPublicPath()
475            childAcctRef.capabilities.unpublish(fallbackPath)
476            childAcctRef.capabilities.publish(
477                childAcctRef.capabilities.storage.issue<&FlowToken.Vault>(/storage/flowTokenVault),
478                at: fallbackPath
479            )
480
481            isUpdated = true || isUpdated
482        }
483        return isUpdated
484    }
485
486    /** ---- Public Methods - Controller ---- */
487
488    /// Check if the given address is whitelisted
489    ///
490    access(all)
491    view fun isWhitelisted(_ address: Address): Bool {
492        if address == self.account.address {
493            return true
494        }
495        let admin = self.account
496            .capabilities.get<&StakingAdmin>(self.StakingAdminPublicPath)
497            .borrow()
498            ?? panic("Could not borrow the admin reference")
499        return admin.isWhitelisted(address: address)
500    }
501
502    /// Check if the given address is eligible for registering
503    ///
504    access(all)
505    view fun isEligibleForRegistering(stakeTick: String, addr: Address): Bool {
506        // singleton resources
507        let acctsPool = FRC20AccountsPool.borrowAccountsPool()
508        // Get the staking pool address
509        let poolAddress = acctsPool.getFRC20StakingAddress(tick: stakeTick)
510            ?? panic("The staking pool is not enabled")
511        // borrow the staking pool
512        let pool = FRC20Staking.borrowPool(poolAddress)
513            ?? panic("The staking pool is not found")
514        let totalStakedBalance = pool.getDetails().totalStaked
515                // if the controller staked more than 10% of the total staked tokens, then it is valid
516        return self.isEligibleByStakePower(stakeTick: stakeTick, addr: addr, threshold: totalStakedBalance * 0.1)
517    }
518
519    /// Check if the given address is eligible by stake power
520    ///
521    access(all)
522    view fun isEligibleByStakePower(stakeTick: String, addr: Address, threshold: UFix64): Bool {
523        // Check if the controller is whitelisted or staked enough tokens
524        var isValid = self.isWhitelisted(addr)
525        // Check if the controller is staked enough tokens
526        if !isValid {
527            if let delegator = FRC20Staking.borrowDelegator(addr) {
528                let controllerStakedBalance = delegator.getStakedBalance(tick: stakeTick)
529                // if the controller staked more than 10% of the total staked tokens, then it is valid
530                isValid = controllerStakedBalance >= threshold
531            }
532        }
533        return isValid
534    }
535
536    /// Create a new staking controller
537    ///
538    access(all)
539    fun createController(): @StakingController {
540        return <- create StakingController()
541    }
542
543    /** ---- Public Methods - User ---- */
544
545    /// Borrow the platform staking pool.
546    ///
547    access(all)
548    view fun borrowPlatformStakingPool(): &FRC20Staking.Pool {
549        // singleton resources
550        let acctsPool = FRC20AccountsPool.borrowAccountsPool()
551        // Get the staking pool address
552        let stakeTick = FRC20FTShared.getPlatformStakingTickerName()
553        let poolAddress = acctsPool.getFRC20StakingAddress(tick: stakeTick)
554            ?? panic("The staking pool is not enabled")
555        // borrow the staking pool
556        return FRC20Staking.borrowPool(poolAddress) ?? panic("The staking pool is not found")
557    }
558
559    /// Stake tokens
560    ///
561    access(all)
562    fun stake(ins: auth(Fixes.Extractable) &Fixes.Inscription) {
563        // singleton resources
564        let frc20Indexer = FRC20Indexer.getIndexer()
565        let acctsPool = FRC20AccountsPool.borrowAccountsPool()
566
567        // inscription data
568        let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
569        let op = meta["op"]?.toLower() ?? panic("The token operation is not found")
570        assert(
571            op == "withdraw",
572            message: "The token operation should be withdraw."
573        )
574        let usage = meta["usage"]?.toLower() ?? panic("The token usage is not found")
575        assert(
576            usage == "staking",
577            message: "The token usage should be staking."
578        )
579
580        let fromAddr = ins.owner?.address ?? panic("The inscription owner is not found")
581        assert(
582            FRC20Staking.borrowDelegator(fromAddr) != nil,
583            message: "The inscription owner is not a delegator"
584        )
585
586        let tick = meta["tick"]?.toLower() ?? panic("The token tick is not found")
587
588        /// Check if the staking is already enabled
589        let stakingAddress = acctsPool.getFRC20StakingAddress(tick: tick)
590            ?? panic("The staking pool is not enabled")
591        let stakingPool = FRC20Staking.borrowPool(stakingAddress)
592            ?? panic("The staking pool is not found")
593        /// Withdraw the frc20 tokens, will validate the inscription
594        let changeToStake <- frc20Indexer.withdrawChange(ins: ins)
595        /// Stake the tokens
596        stakingPool.stake(<- changeToStake)
597    }
598
599    /// Unstake tokens
600    ///
601    access(all)
602    fun unstake(
603        _ semiNFTColCap: Capability<auth(NonFungibleToken.Update, NonFungibleToken.Withdraw) &FRC20SemiNFT.Collection>,
604        nftId: UInt64
605    ) {
606        pre {
607            semiNFTColCap.check(): "The semiNFT collection is not valid"
608        }
609        let semiNFTCol = semiNFTColCap.borrow()
610            ?? panic("Could not borrow the semiNFT collection")
611        let nft = semiNFTCol.borrowFRC20SemiNFT(id: nftId)
612            ?? panic("The semiNFT is not found")
613        let poolAddress = nft.getFromAddress()
614
615        // singleton resources
616        let acctsPool = FRC20AccountsPool.borrowAccountsPool()
617
618        let stakingPool = FRC20Staking.borrowPool(poolAddress)
619            ?? panic("The staking pool is not found")
620        /// Unstake the tokens
621        stakingPool.unstake(semiNFTCol, nftId: nftId)
622    }
623
624    /// Claim all unlocked unstaking changes
625    ///
626    access(all)
627    fun claimUnlockedUnstakingChange(
628        ins: auth(Fixes.Extractable) &Fixes.Inscription
629    ) {
630        pre {
631            ins.isExtractable(): "The inscription is not extractable"
632        }
633        // singleton resources
634        let frc20Indexer = FRC20Indexer.getIndexer()
635        let acctsPool = FRC20AccountsPool.borrowAccountsPool()
636
637        assert(
638            frc20Indexer.isValidFRC20Inscription(ins: ins),
639            message: "The inscription is not a valid FRC20 inscription"
640        )
641
642        // inscription data
643        let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
644        let op = meta["op"]?.toLower() ?? panic("The token operation is not found")
645        assert(
646            op == "deposit",
647            message: "The token operation should be deposit."
648        )
649
650        let fromAddr = ins.owner?.address ?? panic("The inscription owner is not found")
651        assert(
652            FRC20Staking.borrowDelegator(fromAddr) != nil,
653            message: "The inscription owner is not a delegator"
654        )
655
656        let tick = meta["tick"]?.toLower() ?? panic("The token tick is not found")
657        let poolAddress = acctsPool.getFRC20StakingAddress(tick: tick)
658            ?? panic("The staking pool is not enabled")
659        let stakingPool = FRC20Staking.borrowPool(poolAddress)
660            ?? panic("The staking pool is not found")
661        if let unstakedChange <- stakingPool.claimUnlockedUnstakingChange(delegator: fromAddr) {
662            // deposit the unstaked change
663            frc20Indexer.depositChange(ins: ins, change: <- unstakedChange)
664        } else {
665            panic("No any unlocked staked FRC20 tokens can be claimed.")
666        }
667    }
668
669    /// Donate FRC20 to the staking pool
670    ///
671    access(all)
672    fun donateToStakingPool(
673        tick: String,
674        ins: auth(Fixes.Extractable) &Fixes.Inscription
675    ) {
676        // singleton resources
677        let acctsPool = FRC20AccountsPool.borrowAccountsPool()
678
679        /// Check if the staking is already enabled
680        let stakingAddress = acctsPool.getFRC20StakingAddress(tick: tick)
681            ?? panic("The staking pool is not enabled")
682        let stakingPool = FRC20Staking.borrowPool(stakingAddress)
683            ?? panic("The staking pool is not found")
684
685        let changeToDonate <- self._withdrawDonateChange(tick: tick, ins: ins)
686
687        let rewardTick = changeToDonate.getOriginalTick()
688        let rewardStrategy = stakingPool.borrowRewardStrategy(rewardTick)
689            ?? panic("The reward strategy is not registered")
690
691        let amountToDonate = changeToDonate.getBalance()
692        assert(
693            amountToDonate > 0.0,
694            message: "The amount to donate should be greater than zero"
695        )
696        // Donate the tokens
697        rewardStrategy.addIncome(income: <- changeToDonate)
698
699        // emit the event
700        emit StakingPoolDonated(
701            tick: tick,
702            rewardTick: rewardTick,
703            amount: amountToDonate,
704            by: ins.owner?.address ?? panic("The inscription owner is not found")
705        )
706    }
707
708    /// Donate FRC20 to the vesting reward pool
709    ///
710    access(all)
711    fun donateToVesting(
712        tick: String,
713        ins: auth(Fixes.Extractable) &Fixes.Inscription,
714        vestingBatchAmount: UInt32,
715        vestingInterval: UFix64,
716    ) {
717        // call the internal method
718        self.donateToVestingFromChange(
719            changeToDonate: <- self._withdrawDonateChange(tick: tick, ins: ins),
720            tick: tick,
721            vestingBatchAmount: vestingBatchAmount,
722            vestingInterval: vestingInterval
723        )
724    }
725
726    /// Donate Change to the staking's vesting pool
727    /// (Internal Method)
728    ///
729    access(account)
730    fun donateToVestingFromChange(
731        changeToDonate: @FRC20FTShared.Change,
732        tick: String,
733        vestingBatchAmount: UInt32,
734        vestingInterval: UFix64,
735    ) {
736        // singleton resources
737        let acctsPool = FRC20AccountsPool.borrowAccountsPool()
738
739        let stakingAddress = acctsPool.getFRC20StakingAddress(tick: tick) ?? panic("The staking pool is not enabled")
740        let vestingVault = FRC20StakingVesting.borrowVaultRef(stakingAddress)
741            ?? panic("The vesting vault is not found")
742
743        let rewardTick = changeToDonate.getOriginalTick()
744        let amountToDonate = changeToDonate.getBalance()
745        let fromAddr = changeToDonate.from
746        assert(
747            amountToDonate > 0.0,
748            message: "The amount to donate should be greater than zero"
749        )
750
751        // Donate the tokens to the vesting vault
752        vestingVault.addVesting(
753            stakeTick: tick,
754            rewardChange: <- changeToDonate,
755            vestingBatchAmount: vestingBatchAmount,
756            vestingInterval: vestingInterval
757        )
758
759        emit StakingPoolDonatedAsVesting(
760            tick: tick,
761            rewardTick: rewardTick,
762            amount: amountToDonate,
763            by: fromAddr,
764            batchAmount: vestingBatchAmount,
765            interval: vestingInterval
766        )
767    }
768
769    /// Withdraw the donate change from FRC20 Indexer
770    /// (Internal Method)
771    ///
772    access(contract)
773    fun _withdrawDonateChange(
774        tick: String,
775        ins: auth(Fixes.Extractable) &Fixes.Inscription,
776    ): @FRC20FTShared.Change {
777        // singleton resources
778        let frc20Indexer = FRC20Indexer.getIndexer()
779        let acctsPool = FRC20AccountsPool.borrowAccountsPool()
780
781        /// Check if the staking is already enabled
782        let stakingAddress = acctsPool.getFRC20StakingAddress(tick: tick)
783            ?? panic("The staking pool is not enabled")
784        let stakingPool = FRC20Staking.borrowPool(stakingAddress)
785            ?? panic("The staking pool is not found")
786
787        // inscription data
788        let meta = FixesInscriptionFactory.parseMetadata(ins.borrowData())
789        let op = meta["op"]?.toLower() ?? panic("The token operation is not found")
790        assert(
791            op == "withdraw",
792            message: "The token operation should be withdraw."
793        )
794        let usage = meta["usage"]?.toLower() ?? panic("The token usage is not found")
795        assert(
796            usage == "donate",
797            message: "The token usage should be donate."
798        )
799
800        let fromAddr = ins.owner?.address ?? panic("The inscription owner is not found")
801        let insTick = meta["tick"]?.toLower()
802
803        var rewardStrategy: &FRC20Staking.RewardStrategy? = nil
804        if insTick == nil || insTick == "" {
805            rewardStrategy = stakingPool.borrowRewardStrategy("")
806        } else {
807            rewardStrategy = stakingPool.borrowRewardStrategy(insTick!)
808        }
809        // Check if the reward strategy is registered
810        assert(
811            rewardStrategy != nil,
812            message: "The reward strategy is not registered"
813        )
814        // the final reward tick
815        let rewardTick = rewardStrategy!.rewardTick
816
817        // Withdraw the frc20 tokens, will validate the inscription
818        var changeToDonate: @FRC20FTShared.Change? <- nil
819        if rewardTick == "" {
820            changeToDonate <-! frc20Indexer.extractFlowVaultChangeFromInscription(
821                ins,
822                amount: ins.getInscriptionValue() - ins.getMinCost()
823            )
824            // extract the FLOW tokens
825            frc20Indexer.returnChange(change: <- FRC20FTShared.wrapFungibleVaultChange(
826                ftVault: <- ins.extract(),
827                from: ins.owner?.address ?? panic("The inscription owner is not found")
828            ))
829        } else {
830            changeToDonate <-! frc20Indexer.withdrawChange(ins: ins)
831        }
832        return <- (changeToDonate ?? panic("The change to donate is not found"))
833    }
834
835    /// Claim rewards
836    ///
837    access(all)
838    fun claimRewards(
839        _ semiNFTColRef: auth(NonFungibleToken.Update) &FRC20SemiNFT.Collection,
840        nftIds: [UInt64]
841    ) {
842        // singleton resources
843        let frc20Indexer = FRC20Indexer.getIndexer()
844        let acctsPool = FRC20AccountsPool.borrowAccountsPool()
845
846        let ownerAddr = semiNFTColRef.owner?.address
847            ?? panic("The semiNFT collection owner is not found")
848
849        // loop through all nftIds
850        for nftId in nftIds {
851            let nft = semiNFTColRef.borrowFRC20SemiNFT(id: nftId)
852                ?? panic("The semiNFT is not found")
853            let poolAddress = nft.getFromAddress()
854
855            // borrow the staking pool
856            let stakingPool = FRC20Staking.borrowPool(poolAddress)
857                ?? panic("The staking pool is not found")
858
859            // get the reward ticks
860            let rewardTicks = stakingPool.getRewardNames()
861            // loop through all reward ticks
862            for rewardTick in rewardTicks {
863                // get the reward strategy
864                let rewardStrategy = stakingPool.borrowRewardStrategy(rewardTick)
865                    ?? panic("The reward strategy is not registered")
866                // claim the reward, the from should be same as the nft owner
867                let rewardChange <- rewardStrategy.claim(byNft: nft)
868                if rewardChange.getBalance() > 0.0 {
869                    // The reward is not empty, return the change to the user
870                    frc20Indexer.returnChange(change: <- rewardChange)
871                } else {
872                    Burner.burn(<- rewardChange)
873                }
874            } // end reward Ticks
875        } // end nftIds
876    }
877
878    /// init method
879    init() {
880        let identifier = "FRC20Staking_".concat(self.account.address.toString())
881        self.StakingAdminStoragePath = StoragePath(identifier: identifier.concat("_admin"))!
882        self.StakingAdminPublicPath = PublicPath(identifier: identifier.concat("_admin"))!
883        self.StakingControllerStoragePath = StoragePath(identifier: identifier.concat("_controller"))!
884
885        // create the admin account
886        let admin <- create StakingAdmin()
887        self.account.storage.save(<-admin, to: self.StakingAdminStoragePath)
888        self.account.capabilities.publish(
889            self.account.capabilities.storage.issue<&StakingAdmin>(self.StakingAdminStoragePath),
890            at: self.StakingAdminPublicPath
891        )
892
893        // create the controller
894        let controller <- create StakingController()
895        self.account.storage.save(<-controller, to: self.StakingControllerStoragePath)
896
897        emit ContractInitialized()
898    }
899}
900