Smart Contract

BloctoTokenStaking

A.0f9df91c9121c460.BloctoTokenStaking

Valid From

86,900,119

Deployed

1w ago
Feb 15, 2026, 04:38:48 PM UTC

Dependents

18 imports
1/*
2    BloctoTokenStaking
3
4    The BloctoToken Staking contract manages stakers' information.
5    Forked from FlowIDTableStaking contract.
6 */
7
8import FungibleToken from 0xf233dcee88fe0abe
9import BloctoToken from 0x0f9df91c9121c460
10
11access(all)
12contract BloctoTokenStaking {
13    // An entitlement for Staker access
14    access(all) entitlement StakerEntitlement
15    // An entitlement for Admin access
16    access(all) entitlement AdminEntitlement
17    /****** Staking Events ******/
18    access(all)
19    event NewEpoch(epoch: UInt64, totalStaked: UFix64, totalRewardPayout: UFix64)
20    
21    /// Staker Events
22    access(all)
23    event NewStakerCreated(stakerID: UInt64, amountCommitted: UFix64)
24    
25    access(all)
26    event TokensCommitted(stakerID: UInt64, amount: UFix64)
27    
28    access(all)
29    event TokensStaked(stakerID: UInt64, amount: UFix64)
30    
31    access(all)
32    event TokensUnstaking(stakerID: UInt64, amount: UFix64)
33    
34    access(all)
35    event TokensUnstaked(stakerID: UInt64, amount: UFix64)
36    
37    access(all)
38    event NodeRemovedAndRefunded(stakerID: UInt64, amount: UFix64)
39    
40    access(all)
41    event RewardsPaid(stakerID: UInt64, amount: UFix64)
42    
43    access(all)
44    event MoveToken(stakerID: UInt64)
45    
46    access(all)
47    event UnstakedTokensWithdrawn(stakerID: UInt64, amount: UFix64)
48    
49    access(all)
50    event RewardTokensWithdrawn(stakerID: UInt64, amount: UFix64)
51    
52    /// Contract Field Change Events
53    access(all)
54    event NewWeeklyPayout(newPayout: UFix64)
55
56    /// Contract Field Change Events
57    access(all)
58    event AdminCreated()
59    
60    /// Holds the identity table for all the stakers in the network.
61    /// Includes stakers that aren't actively participating
62    /// key = staker ID (also corresponds to BloctoPass ID)
63    /// value = the record of that staker's info, tokens, and delegators
64    access(contract)
65    var stakers: @{UInt64: StakerRecord}
66    
67    /// The total amount of tokens that are staked for all the stakers
68    access(contract)
69    var totalTokensStaked: UFix64
70    
71    /// The total amount of tokens that are paid as rewards every epoch
72    /// could be manually changed by the admin resource
73    access(contract)
74    var epochTokenPayout: UFix64
75    
76    /// Indicates if the staking auction is currently enabled
77    access(contract)
78    var stakingEnabled: Bool
79    
80    /// Paths for storing staking resources
81    access(all)
82    let StakingAdminStoragePath: StoragePath
83    
84    /*********** Staking Composite Type Definitions *************/
85    /// Contains information that is specific to a staker
86    access(all)
87    resource StakerRecord {
88        
89        /// The unique ID of the staker
90        /// Corresponds to the BloctoPass NFT ID
91        access(all)
92        let id: UInt64
93        
94        /// The total tokens that only this staker currently has staked
95        access(all)
96        var tokensStaked: @BloctoToken.Vault
97        
98        /// The tokens that this staker has committed to stake for the next epoch.
99        access(all)
100        var tokensCommitted: @BloctoToken.Vault
101        
102        /// Tokens that this staker is able to withdraw whenever they want
103        access(all)
104        var tokensUnstaked: @BloctoToken.Vault
105        
106        /// Staking rewards are paid to this bucket
107        /// Can be withdrawn whenever
108        access(all)
109        var tokensRewarded:  @BloctoToken.Vault
110        
111        /// The amount of tokens that this staker has requested to unstake for the next epoch
112        access(all)
113        var tokensRequestedToUnstake: UFix64
114        
115        init(id: UInt64) {
116            pre {
117                BloctoTokenStaking.stakers[id] == nil: "The ID cannot already exist in the record"
118            }
119            self.id = id
120            self.tokensCommitted <- BloctoToken.createEmptyVault(vaultType: Type<@BloctoToken.Vault>())
121            self.tokensStaked <- BloctoToken.createEmptyVault(vaultType: Type<@BloctoToken.Vault>())
122            self.tokensUnstaked <- BloctoToken.createEmptyVault(vaultType: Type<@BloctoToken.Vault>())
123            self.tokensRewarded <- BloctoToken.createEmptyVault(vaultType: Type<@BloctoToken.Vault>())
124            self.tokensRequestedToUnstake = 0.0
125            emit NewStakerCreated(stakerID: self.id, amountCommitted: self.tokensCommitted.balance)
126        }
127
128        /// Utility Function that checks a staker's overall committed balance from its borrowed record
129        access(contract)
130        fun stakerFullCommittedBalance(): UFix64 {
131            if (self.tokensCommitted.balance + self.tokensStaked.balance) < self.tokensRequestedToUnstake {
132                return 0.0
133            } else {
134                return self.tokensCommitted.balance + self.tokensStaked.balance - self.tokensRequestedToUnstake
135            }
136        }
137
138        /// setter for tokensRequestedToUnstake
139        access(contract)
140        fun setTokensRequestedToUnstake(_ amount: UFix64){
141            self.tokensRequestedToUnstake = amount
142        }
143
144        /// Utility Function that auth tokensStaked withdraw
145        access(contract)
146        fun authTokensStakedWithdraw(): auth(FungibleToken.Withdraw) &BloctoToken.Vault{
147            return &self.tokensStaked
148        }
149
150        /// Utility Function that auth tokensCommitted withdraw
151        access(contract)
152        fun authTokensCommittedWithdraw(): auth(FungibleToken.Withdraw) &BloctoToken.Vault{
153            return &self.tokensCommitted
154        }
155
156        /// Utility Function that auth tokensUnstaked withdraw
157        access(contract)
158        fun authTokensUnstakedWithdraw(): auth(FungibleToken.Withdraw) &BloctoToken.Vault{
159            return &self.tokensUnstaked
160        }
161
162        /// Utility Function that auth tokensRewarded withdraw
163        access(contract)
164        fun authTokensRewardedWithdraw(): auth(FungibleToken.Withdraw) &BloctoToken.Vault{
165            return &self.tokensRewarded
166        }
167
168    }
169
170    /// Struct to create to get read-only info about a staker
171    access(all)
172    struct StakerInfo {
173        access(all)
174        let id: UInt64
175        
176        access(all)
177        let tokensStaked: UFix64
178        
179        access(all)
180        let tokensCommitted: UFix64
181        
182        access(all)
183        let tokensUnstaked: UFix64
184        
185        access(all)
186        let tokensRewarded: UFix64
187        
188        access(all)
189        let tokensRequestedToUnstake: UFix64
190        
191        view init(stakerID: UInt64) {
192            let stakerRecord = BloctoTokenStaking.borrowStakerRecord(stakerID)
193
194            self.id = stakerRecord.id
195            self.tokensStaked = stakerRecord.tokensStaked.balance
196            self.tokensCommitted = stakerRecord.tokensCommitted.balance
197            self.tokensUnstaked = stakerRecord.tokensUnstaked.balance
198            self.tokensRewarded = stakerRecord.tokensRewarded.balance
199            self.tokensRequestedToUnstake = stakerRecord.tokensRequestedToUnstake
200        }
201        
202        access(all)
203        view fun totalTokensInRecord(): UFix64 {
204            return self.tokensStaked + self.tokensCommitted + self.tokensUnstaked + self.tokensRewarded
205        }
206    }
207    
208    access(all)
209    resource interface StakerPublic {
210        access(all)
211        let id: UInt64
212    }
213    
214    /// Resource that the staker operator controls for staking
215    access(all)
216    resource Staker: StakerPublic {
217        
218        /// Unique ID for the staker operator
219        access(all)
220        let id: UInt64
221
222        init(id: UInt64) {
223            self.id = id
224        }
225
226        /// Add new tokens to the system to stake during the next epoch
227        access(StakerEntitlement)
228        fun stakeNewTokens(_ tokens: @{FungibleToken.Vault}) {
229            pre {
230                BloctoTokenStaking.stakingEnabled: "Cannot stake if the staking auction isn't in progress"
231            }
232
233            // Borrow the staker's record from the staking contract
234            let stakerRecord = BloctoTokenStaking.borrowStakerRecord(self.id)
235            emit TokensCommitted(stakerID: stakerRecord.id, amount: tokens.balance)
236
237            // Add the new tokens to tokens committed
238            stakerRecord.tokensCommitted.deposit(from: <-tokens)
239        }
240
241        /// Stake tokens that are in the tokensUnstaked bucket
242        access(StakerEntitlement)
243        fun stakeUnstakedTokens(amount: UFix64) {
244            pre {
245                BloctoTokenStaking.stakingEnabled: "Cannot stake if the staking auction isn't in progress"
246            }
247            let stakerRecord = BloctoTokenStaking.borrowStakerRecord(self.id)
248            var remainingAmount = amount
249
250            // If there are any tokens that have been requested to unstake for the current epoch,
251            // cancel those first before staking new unstaked tokens
252            if remainingAmount <= stakerRecord.tokensRequestedToUnstake  {
253                stakerRecord.setTokensRequestedToUnstake(stakerRecord.tokensRequestedToUnstake - remainingAmount)
254                remainingAmount = 0.0
255            } else if remainingAmount > stakerRecord.tokensRequestedToUnstake  {
256                remainingAmount = remainingAmount - stakerRecord.tokensRequestedToUnstake
257                stakerRecord.setTokensRequestedToUnstake(0.0)
258            }
259
260            // Commit the remaining amount from the tokens unstaked bucket
261            stakerRecord.tokensCommitted.deposit(from: <- stakerRecord.authTokensUnstakedWithdraw().withdraw(amount: remainingAmount))
262            emit TokensCommitted(stakerID: stakerRecord.id, amount: remainingAmount)
263        }
264        
265        /// Stake tokens that are in the tokensRewarded bucket
266        access(StakerEntitlement)
267        fun stakeRewardedTokens(amount: UFix64) {
268            pre {
269                BloctoTokenStaking.stakingEnabled: "Cannot stake if the staking auction isn't in progress"
270            }
271
272            let stakerRecord = BloctoTokenStaking.borrowStakerRecord(self.id)
273            stakerRecord.tokensCommitted.deposit(from: <- stakerRecord.authTokensRewardedWithdraw().withdraw(amount: amount))
274            emit TokensCommitted(stakerID: stakerRecord.id, amount: amount)
275        }
276
277        /// Request amount tokens to be removed from staking at the end of the next epoch
278        access(StakerEntitlement)
279        fun requestUnstaking(amount: UFix64) {
280            pre {
281                BloctoTokenStaking.stakingEnabled: "Cannot unstake if the staking auction isn't in progress"
282            }
283            let stakerRecord = BloctoTokenStaking.borrowStakerRecord(self.id)
284
285            // If the request is greater than the total number of tokens
286            // that can be unstaked, revert
287            assert (
288                stakerRecord.tokensStaked.balance +
289                stakerRecord.tokensCommitted.balance
290                >= amount + stakerRecord.tokensRequestedToUnstake,
291                message: "Not enough tokens to unstake!"
292            )
293
294            // Get the balance of the tokens that are currently committed
295            let amountCommitted = stakerRecord.tokensCommitted.balance
296
297            // If the request can come from committed, withdraw from committed to unstaked
298            if amountCommitted >= amount {
299
300                // withdraw the requested tokens from committed since they have not been staked yet
301                stakerRecord.tokensUnstaked.deposit(from: <-stakerRecord.authTokensCommittedWithdraw().withdraw(amount: amount))
302            } else {
303                let amountCommitted = stakerRecord.tokensCommitted.balance
304
305                // withdraw the requested tokens from committed since they have not been staked yet
306                stakerRecord.tokensUnstaked.deposit(from: <-stakerRecord.authTokensCommittedWithdraw().withdraw(amount: amountCommitted))
307
308                // update request to show that leftover amount is requested to be unstaked
309                stakerRecord.setTokensRequestedToUnstake(stakerRecord.tokensRequestedToUnstake + (amount - amountCommitted))
310            }
311        }
312
313        /// Requests to unstake all of the staker operators staked and committed tokens
314        /// as well as all the staked and committed tokens of all of their delegators
315        access(StakerEntitlement)
316        fun unstakeAll() {
317            pre {
318                BloctoTokenStaking.stakingEnabled:
319                    "Cannot unstake if the staking auction isn't in progress"
320            }
321            let stakerRecord = BloctoTokenStaking.borrowStakerRecord(self.id)
322
323            /// if the request can come from committed, withdraw from committed to unstaked
324            /// withdraw the requested tokens from committed since they have not been staked yet
325            stakerRecord.tokensUnstaked.deposit(from: <- stakerRecord.authTokensCommittedWithdraw().withdraw(amount: stakerRecord.tokensCommitted.balance))
326            
327            /// update request to show that leftover amount is requested to be unstaked
328            stakerRecord.setTokensRequestedToUnstake(stakerRecord.tokensStaked.balance)
329        }
330
331        /// Withdraw tokens from the unstaked bucket
332        access(StakerEntitlement)
333        fun withdrawUnstakedTokens(amount: UFix64): @{FungibleToken.Vault} {
334            let stakerRecord = BloctoTokenStaking.borrowStakerRecord(self.id)
335            emit UnstakedTokensWithdrawn(stakerID: stakerRecord.id, amount: amount)
336            return <- stakerRecord.authTokensUnstakedWithdraw().withdraw(amount: amount)
337        }
338
339        /// Withdraw tokens from the rewarded bucket
340        access(StakerEntitlement)
341        fun withdrawRewardedTokens(amount: UFix64): @{FungibleToken.Vault} {
342            let stakerRecord = BloctoTokenStaking.borrowStakerRecord(self.id)
343
344            emit RewardTokensWithdrawn(stakerID: stakerRecord.id, amount: amount)
345
346            return <- stakerRecord.authTokensRewardedWithdraw().withdraw(amount: amount)
347        }
348    }
349    
350    /// Admin resource that has the ability to create new staker objects and pay rewards
351    /// to stakers at the end of an epoch
352    access(all)
353    resource Admin {
354        
355        /// A staker record is created when a BloctoPass NFT is created
356        /// It returns the resource for stakers that they can store in their account storage
357        access(AdminEntitlement)
358        fun addStakerRecord(id: UInt64): @Staker {
359            pre {
360                BloctoTokenStaking.stakingEnabled:
361                    "Cannot register a staker operator if the staking auction isn't in progress"
362            }
363            let newStakerRecord <- create StakerRecord(id: id)
364            
365            // Insert the staker to the table
366            BloctoTokenStaking.stakers[id] <-! newStakerRecord
367            
368            // return a new Staker object that the staker operator stores in their account
369            return <-create Staker(id: id)
370        }
371        
372        /// Starts the staking auction, the period when stakers and delegators
373        /// are allowed to perform staking related operations
374        access(AdminEntitlement)
375        fun startNewEpoch() {
376            BloctoTokenStaking.stakingEnabled = true
377            BloctoTokenStaking.setEpoch(BloctoTokenStaking.getEpoch() + 1)
378            emit NewEpoch(epoch: BloctoTokenStaking.getEpoch(), totalStaked: BloctoTokenStaking.getTotalStaked(), totalRewardPayout: BloctoTokenStaking.epochTokenPayout)
379        }
380
381        /// Starts the staking auction, the period when stakers and delegators
382        /// are allowed to perform staking related operations
383        access(AdminEntitlement)
384        fun startStakingAuction() {
385            BloctoTokenStaking.stakingEnabled = true
386        }
387
388        /// Ends the staking Auction by removing any unapproved stakers
389        /// and setting stakingEnabled to false
390        access(AdminEntitlement)
391        fun endStakingAuction() {
392            BloctoTokenStaking.stakingEnabled = false
393        }
394
395        /// Called at the end of the epoch to pay rewards to staker operators
396        /// based on the tokens that they have staked
397        access(AdminEntitlement)
398        fun payRewards(_ stakerIDs: [UInt64]) {
399            pre {
400                !BloctoTokenStaking.stakingEnabled: "Cannot pay rewards if the staking auction is still in progress"
401            }
402            let BloctoTokenMinter = BloctoTokenStaking.account.storage.borrow<auth(BloctoToken.MinterEntitlement) &BloctoToken.Minter>(from: /storage/bloctoTokenStakingMinter)
403                ?? panic("Could not borrow minter reference")
404
405            // calculate the total number of tokens staked
406            var totalStaked = BloctoTokenStaking.getTotalStaked()
407            if totalStaked == 0.0 {
408                return
409            }
410            var totalRewardScale = BloctoTokenStaking.epochTokenPayout / totalStaked
411            let epoch = BloctoTokenStaking.getEpoch()
412            let epochPath = BloctoTokenStaking.getStakingRewardPath(epoch: epoch)
413            var stakingRewardRecordsRefOpt = BloctoTokenStaking.account.storage.borrow<auth(Mutate) &{String: Bool}>(from: epochPath)
414            if stakingRewardRecordsRefOpt == nil {
415                BloctoTokenStaking.account.storage.save<{String: Bool}>({} as {String: Bool}, to: epochPath)
416                stakingRewardRecordsRefOpt = BloctoTokenStaking.account.storage.borrow<auth(Mutate) &{String: Bool}>(from: epochPath)
417            }
418            var stakingRewardRecordsRef = stakingRewardRecordsRefOpt!
419
420            /// iterate through stakers to pay
421            for stakerID in stakerIDs {
422                // add reward record
423                let key = BloctoTokenStaking.getStakingRewardKey(epoch: epoch, stakerID: stakerID)
424                if stakingRewardRecordsRef[key] != nil && stakingRewardRecordsRef[key]! {
425                    continue
426                }
427                stakingRewardRecordsRef[key] = true
428                let stakerRecord = BloctoTokenStaking.borrowStakerRecord(stakerID)
429                if stakerRecord.tokensStaked.balance == 0.0 { continue }
430                let rewardAmount = stakerRecord.tokensStaked.balance * totalRewardScale
431                if rewardAmount == 0.0 { continue }
432                emit RewardsPaid(stakerID: stakerRecord.id, amount: rewardAmount)
433
434                /// Deposit the staker Rewards into their tokensRewarded bucket
435                stakerRecord.tokensRewarded.deposit(from: <- BloctoTokenMinter.mintTokens(amount: rewardAmount))
436            }
437        }
438
439        /// Called at the end of the epoch to move tokens between buckets
440        /// for stakers
441        /// Tokens that have been committed are moved to the staked bucket
442        /// Tokens that were unstaking during the last epoch are fully unstaked
443        /// Unstaking requests are filled by moving those tokens from staked to unstaking
444        access(AdminEntitlement)
445        fun moveTokens(_ stakerIDs: [UInt64]) {
446            pre {
447                !BloctoTokenStaking.stakingEnabled: "Cannot move tokens if the staking auction is still in progress"
448            }
449            for stakerID in stakerIDs {
450                // get staker record
451                let stakerRecord = BloctoTokenStaking.borrowStakerRecord(stakerID)
452
453                // Update total number of tokens staked by all the stakers of each type
454                BloctoTokenStaking.totalTokensStaked = BloctoTokenStaking.totalTokensStaked + stakerRecord.tokensCommitted.balance
455
456                // mark the committed tokens as staked
457                if stakerRecord.tokensCommitted.balance > 0.0 {
458                    emit TokensStaked(stakerID: stakerRecord.id, amount: stakerRecord.tokensCommitted.balance)
459                    stakerRecord.tokensStaked.deposit(from: <-stakerRecord.authTokensCommittedWithdraw().withdraw(amount: stakerRecord.tokensCommitted.balance))
460                }
461
462                // unstake the requested tokens and move them to tokensUnstaking
463                if stakerRecord.tokensRequestedToUnstake > 0.0 {
464                    emit TokensUnstaked(stakerID: stakerRecord.id, amount: stakerRecord.tokensRequestedToUnstake)
465                    stakerRecord.tokensUnstaked.deposit(from: <-stakerRecord.authTokensStakedWithdraw().withdraw(amount: stakerRecord.tokensRequestedToUnstake))
466
467                    // subtract their requested tokens from the total staked for their staker type
468                    BloctoTokenStaking.totalTokensStaked = BloctoTokenStaking.totalTokensStaked - stakerRecord.tokensRequestedToUnstake
469                    
470                    // Reset the tokens requested field so it can be used for the next epoch
471                    stakerRecord.setTokensRequestedToUnstake(0.0)
472                }
473                emit MoveToken(stakerID: stakerID)
474            }
475        }
476
477        /// Changes the total weekly payout to a new value
478        access(AdminEntitlement)
479        fun setEpochTokenPayout(_ newPayout: UFix64) {
480            BloctoTokenStaking.epochTokenPayout = newPayout
481            emit NewWeeklyPayout(newPayout: newPayout)
482        }
483
484        access(AdminEntitlement)
485        fun createNewAdmin(): @Admin {
486            emit AdminCreated()
487            return <-create Admin()
488        }
489    }
490
491    /// borrow a reference to to one of the stakers in the record
492    access(account)
493    view fun borrowStakerRecord(_ stakerID: UInt64): &StakerRecord {
494        pre {
495            BloctoTokenStaking.stakers[stakerID] != nil:
496                "Specified staker ID does not exist in the record"
497        }
498        return (&BloctoTokenStaking.stakers[stakerID] as &StakerRecord?)!
499    }
500
501    /// Gets an array of all the stakerIDs that are staked.
502    /// Only stakers that are participating in the current epoch
503    /// can be staked, so this is an array of all the active stakers
504    access(all)
505    fun getStakedStakerIDs(): [UInt64] {
506        var stakers: [UInt64] = []
507        for stakerID in BloctoTokenStaking.getStakerIDs() {
508            let stakerRecord = BloctoTokenStaking.borrowStakerRecord(stakerID)
509            if stakerRecord.tokensStaked.balance > 0.0 {
510                stakers.append(stakerID)
511            }
512        }
513        return stakers
514    }
515
516    /// Gets an slice of stakerIDs who's staking balance > 0
517    access(all)
518    fun getStakedStakerIDsSlice(start: UInt64, end: UInt64): [UInt64] {
519        // all staker ids
520        var allStakerIDs: [UInt64] = BloctoTokenStaking.getStakerIDs()
521
522        // output
523        var stakers: [UInt64] = []
524
525        // filter staker ids by staking balance
526        var current = start
527        while current < end {
528            let stakerID = allStakerIDs[current]
529            let stakerRecord = BloctoTokenStaking.borrowStakerRecord(stakerID)
530            if stakerRecord.tokensStaked.balance > 0.0 {
531                stakers.append(stakerID)
532            }
533            current = current + 1
534        }
535        return stakers
536    }
537
538    /// Gets an array of all the staker IDs that have ever registered
539    access(all)
540    fun getStakerIDs(): [UInt64] {
541        return BloctoTokenStaking.stakers.keys
542    }
543
544    /// Gets an slice of stakerIDs who's staking balance > 0
545    access(all)
546    fun getStakerIDsSlice(start: UInt64, end: UInt64): [UInt64] {
547        // all staker ids
548        var allStakerIDs: [UInt64] = BloctoTokenStaking.getStakerIDs()
549
550        // output
551        var stakers: [UInt64] = []
552
553        // filter staker ids by staking balance
554        var current = start
555        while current < end {
556            stakers.append(allStakerIDs[current])
557            current = current + 1
558        }
559        return stakers
560    }
561
562    /// Gets staker id count
563    access(all)
564    fun getStakerIDCount(): Int {
565        return BloctoTokenStaking.stakers.keys.length
566    }
567
568    /// Gets the token payout value for the current epoch
569    access(all)
570    fun getEpochTokenPayout(): UFix64 {
571        return self.epochTokenPayout
572    }
573
574    access(all)
575    fun getStakingEnabled(): Bool {
576        return self.stakingEnabled
577    }
578
579    /// Gets the total number of BLT that is currently staked
580    /// by all of the staked stakers in the current epoch
581    access(all)
582    fun getTotalStaked(): UFix64 {
583        return BloctoTokenStaking.totalTokensStaked
584    }
585
586    /// Epoch
587    access(all)
588    fun getEpoch(): UInt64 {
589        return self.account.storage.copy<UInt64>(from: /storage/bloctoTokenStakingEpoch) ?? 0
590    }
591
592    access(contract)
593    fun setEpoch(_ epoch: UInt64) {
594        self.account.storage.load<UInt64>(from: /storage/bloctoTokenStakingEpoch)
595        self.account.storage.save<UInt64>(epoch, to: /storage/bloctoTokenStakingEpoch)
596    }
597    
598    access(contract)
599    fun getStakingRewardKey(epoch: UInt64, stakerID: UInt64): String {
600        // key: {EPOCH}_{STAKER_ID}
601        return epoch.toString().concat("_").concat(stakerID.toString())
602    }
603
604    access(contract)
605    fun getStakingRewardPath(epoch: UInt64): StoragePath {
606        // path: /storage/bloctoTokenStakingStakingRewardRecords_{EPOCH}
607        return StoragePath(identifier: "bloctoTokenStakingStakingRewardRecords".concat("_").concat(epoch.toString()))!
608    }
609
610    /// staking reward records
611    access(all)
612    fun hasSentStakingReward(epoch: UInt64, stakerID: UInt64): Bool {
613        let stakingRewardRecordsRef = self.account.storage.borrow<&{String: Bool}>(from: BloctoTokenStaking.getStakingRewardPath(epoch: epoch))
614        if stakingRewardRecordsRef == nil {
615            return false
616        }
617        let key = BloctoTokenStaking.getStakingRewardKey(epoch: epoch, stakerID: stakerID)
618        if stakingRewardRecordsRef![key] == nil {
619            return false
620        }
621
622        return stakingRewardRecordsRef![key]!
623    }
624
625    init() {
626        self.stakingEnabled = true
627
628        self.stakers <- {}
629
630        self.StakingAdminStoragePath = /storage/bloctoTokenStakingAdmin
631
632        self.totalTokensStaked = 0.0
633        self.epochTokenPayout = 1.0
634
635        self.account.storage.save(<-create Admin(), to: self.StakingAdminStoragePath)
636    }
637}