Smart Contract
FRC20StakingManager
A.d2abb5dbf5e08666.FRC20StakingManager
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