Smart Contract
BloctoTokenStaking
A.0f9df91c9121c460.BloctoTokenStaking
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}