Smart Contract
FlowRewards
A.a45ead1cf1ca9eda.FlowRewards
1import NonFungibleToken from 0x1d7e57aa55817448
2import FungibleToken from 0xf233dcee88fe0abe
3import FlowToken from 0x1654653399040a61
4import ViewResolver from 0x1d7e57aa55817448
5import MetadataViews from 0x1d7e57aa55817448
6
7import Base64Util from 0xa45ead1cf1ca9eda
8
9import FlowRewardsRegistry from 0xa45ead1cf1ca9eda
10import FlowRewardsMetadataViews from 0xa45ead1cf1ca9eda
11import Clock from 0xa45ead1cf1ca9eda
12
13/// This contract defines a system by which users can lock FLOW tokens in exchange for rewards over a set period of
14/// time. NFT represent claims on these locked FLOW and can be used to track and withdraw rewards.
15///
16access(all) contract FlowRewards : NonFungibleToken, ViewResolver {
17
18 /* --- Fields --- */
19 //
20 /// The total supply of NFTs minted, values also serve as sequence number for the registry
21 access(all) var totalSupply: UInt64
22 /// The total FLOW locked via this contract
23 access(all) var totalValueLocked: UFix64
24 /// A mapping of NFT IDs to their respective sequence number
25 access(all) let sequenceRegistry: {UInt64: UInt64}
26 /// The minimum amount that can be locked in a single lockup
27 access(all) var minLockupAmount: UFix64
28 /// The tier thresholds of locked & rewarded FLOW total amounts
29 access(all) let tiers: [UFix64]
30 /// The names associated with each tier
31 access(all) let tierNames: [String]
32 /// The interval at which to create new registries in the contract's account storage
33 access(all) let balancerInterval: UInt64
34 /// The minimum timespan between swapping models defining the reward boost and the current model's end
35 access(all) let modelSwapBuffer: UFix64
36 /// The minimum time between distribution end and when reward pool FLOW can be withdrawn
37 access(all) var clawbackBuffer: UFix64
38 /// The maximum length of time either the lock or distribution periods can last
39 access(all) let maxPeriodDuration: UFix64
40 /// The maximum time between lock and distribution periods
41 access(all) let maxIntermissionDuration: UFix64
42 /// The reward boost model used to calculate rewards
43 access(self) var rewardBoostModel: @{BoostModel}?
44 /// Used to render SVG display for NFTs
45 access(self) var renderer: {FlowRewardsMetadataViews.Renderer}?
46 /// The reward distribution model used to distribute rewards
47 access(self) var distributionModel: @{DistributionModel}?
48 /// The pool of rewards to be distributed
49 access(self) let rewardPool: @FlowToken.Vault
50
51 /* --- Paths --- */
52 //
53 /// Default storage path for the Collection
54 access(all) let CollectionStoragePath: StoragePath
55 /// Default public path for the Collection
56 access(all) let CollectionPublicPath: PublicPath
57 /// Default private paths for the Collection
58 access(all) let CollectionPrivatePath: PrivatePath
59 /// Default storage path for ValetManager
60 access(all) let ValetManagerStoragePath: StoragePath
61 // Default public path for ValetManager
62 access(all) let ValetManagerPublicPath: PublicPath
63 /// Default storage path for the Admin
64 access(all) let AdminStoragePath: StoragePath
65
66 /* --- Entitlements --- */
67 //
68 access(all) entitlement Claim
69 access(all) entitlement Integrate
70 access(all) entitlement Configure
71 access(all) entitlement Parameterize
72 access(all) entitlement Fund
73
74 /* --- Events --- */
75 //
76 /// Emitted when a new NFT is minted containing the id, sequence, and Address of the initiator
77 access(all) event Minted(id: UInt64, sequence: UInt64, lockedVaultUUID: UInt64, initiator: Address)
78 /// Emitted when a lockup is added
79 access(all) event Locked(
80 nftID: UInt64,
81 nftSequence: UInt64,
82 amount: UFix64,
83 lockedUUID: UInt64,
84 toVaultUUID: UInt64,
85 nftTVL: UFix64,
86 contractTVL: UFix64,
87 initiator: Address
88 )
89 /// Emitted when someone claims rewards against a locked NFT
90 access(all) event DistributionClaimed(
91 id: UInt64,
92 amountClaimed: UFix64,
93 amountRemaining: UFix64,
94 distributedUUID: UInt64,
95 locked: Bool,
96 claimant: Address?
97 )
98 /// Emitted when the rewardPool is funded
99 access(all) event RewardPoolFunded(amount: UFix64, toUUID: UInt64, depositedUUID: UInt64, adminUUID: UInt64)
100 /// Emitted when the rewardPool balance is clawed back
101 access(all) event RewardPoolWithdrawn(amount: UFix64, fromUUID: UInt64, withdrawnUUID: UInt64, adminUUID: UInt64)
102 /// Emitted when the minimum lockup amount is set
103 access(all) event MinimumLockupAmountSet(old: UFix64, new: UFix64, adminUUID: UInt64)
104 /// Emitted when the clawback epoch is updated
105 access(all) event ClawbackBufferUpdated(old: UFix64, new: UFix64, adminUUID: UInt64)
106 /// Emitted when an IValet implementation is added to the ValetManager, enabling claims against NFTs as defined by the implementation
107 access(all) event ValetManagerUpdated(
108 valetType: String,
109 valetUUID: UInt64,
110 managerUUID: UInt64,
111 added: Bool
112 )
113 /// Emitted when a new reward model (BoostModel or DistributionModel) is set
114 access(all) event RewardModelSet(
115 oldType: String,
116 oldStart: UFix64,
117 oldEnd: UFix64,
118 oldUUID: UInt64,
119 newType: String,
120 newStart: UFix64,
121 newEnd: UFix64,
122 newUUID: UInt64,
123 adminUUID: UInt64
124 )
125 /// Emitted in the event the contract's renderer is updated
126 access(all) event RendererSet(type: String, adminUUID: UInt64)
127
128 /* =====================
129 Public Functions
130 ===================== */
131
132 /* --- Stages & Tiers --- */
133 //
134 /// Returns the total FLOW locked in the contract
135 ///
136 /// @return The total FLOW locked in the contract
137 ///
138 access(all) view fun getTotalValueLocked(): UFix64 {
139 return self.totalValueLocked
140 }
141
142 /// Returns the balance of the reward pool from which boost FLOW rewards are withdrawn
143 ///
144 /// @return The balance of the reward pool
145 ///
146 access(all) view fun getRewardPoolBalance(): UFix64 {
147 return self.rewardPool.balance
148 }
149
150 /// Returns whether FLOW can currently be locked
151 ///
152 /// @return Whether FLOW can currently be locked as a Bool
153 ///
154 access(all) view fun isLockPeriodActive(): Bool {
155 if let boostModel = self._borrowRewardBoostModel() {
156 let now = Clock.time()
157 return now >= boostModel.getBoostStart() && now <= boostModel.getBoostEnd()
158 } else {
159 return false
160 }
161 }
162
163 /// Returns the start time of the current lock period
164 ///
165 /// @return The start timestamp of the current lock period. If no lock period has been defined, nil is returned
166 ///
167 access(all) view fun getLockPeriodStart(): UFix64? {
168 return self._borrowRewardBoostModel()?.getBoostStart()
169 }
170
171 /// Returns the end time of the current lock period
172 ///
173 /// @return The end timestamp of the current lock period. If no lock period has been defined, nil is returned
174 ///
175 access(all) view fun getLockPeriodEnd(): UFix64? {
176 return self._borrowRewardBoostModel()?.getBoostEnd()
177 }
178
179 /// Returns the current boost factor
180 ///
181 /// @return The current boost factor as a UFix64 or nil if the boost model has not been defined
182 ///
183 access(all) view fun getCurrentBoostFactor(): UFix64? {
184 return self._borrowRewardBoostModel()?.getBoostFactor(atTime: nil) ?? nil
185 }
186
187 /// Returns whether locked & reward FLOW can currently be distributed
188 ///
189 /// @return Whether locked & reward FLOW can currently be distributed as a Bool
190 ///
191 access(all) view fun isDistributionPeriodActive(): Bool {
192 let distributionModel = self._borrowRewardDistributionModel()
193 if distributionModel == nil {
194 return false
195 }
196 let now = Clock.time()
197 return now >= distributionModel!.getDistributionStart()
198 }
199
200 /// Returns the start time of the current distribution period
201 ///
202 /// @return The start timestamp of the current distribution period. If no distribution period has been defined, nil
203 /// is returned
204 ///
205 access(all) view fun getDistributionPeriodStart(): UFix64? {
206 return self._borrowRewardDistributionModel()?.getDistributionStart()
207 }
208
209 /// Returns the end time of the current distribution period
210 ///
211 /// @return The end timestamp of the current distribution period. If no distribution period has been defined, nil
212 /// is returned
213 ///
214 access(all) view fun getDistributionPeriodEnd(): UFix64? {
215 return self._borrowRewardDistributionModel()?.getDistributionEnd()
216 }
217
218 /// Returns the clawback threshold, the time at which the funded reward pool can be withdrawn
219 ///
220 /// @return The clawback threshold timestamp, or nil if the distribution period has not been set
221 ///
222 access(all) view fun getClawbackThreshold(): UFix64? {
223 if let distributionEnd = self.getDistributionPeriodEnd() {
224 return distributionEnd + self.clawbackBuffer
225 }
226 return nil
227 }
228
229 /// Finds the tier number for a given amount, given the tiers are in ascending order. Tiers are determined by the
230 /// thresholds in the `tiers` field which as any Cadence array is 0-indexed
231 ///
232 /// @param forAmount: The amount to find the tier number for
233 ///
234 /// @return The tier number for the given amount
235 ///
236 access(all) view fun getTierNumber(forAmount: UFix64): Int {
237 var index = 0
238 if forAmount < self.tiers[index] {
239 index = 0
240 }
241 for i, tier in self.tiers {
242 if forAmount <= tier {
243 index = i
244 break
245 } else if i == self.tiers.length - 1 {
246 index = i
247 break
248 }
249 }
250 return index
251 }
252
253 /// Finds the tier name for a given amount, given the tiers are in ascending order. Tiers are determined by the
254 /// thresholds in the `tiers` field which as any Cadence array is 0-indexed
255 ///
256 /// @param forAmount: The amount to find the tier number for
257 ///
258 /// @return The tier name for the given amount
259 ///
260 access(all) view fun getTierName(forAmount: UFix64): String {
261 let number = self.getTierNumber(forAmount: forAmount)
262 assert(number >= 0, message: "Tier number=".concat(number.toString()).concat(" must be >= 0"))
263 assert(
264 number <= self.tierNames.length,
265 message: "Tier number=".concat(number.toString())
266 .concat(" exceeds tierName.length=").concat(self.tierNames.length.toString())
267 )
268 return self.tierNames[number]
269 }
270
271 /* --- Entry Queries --- */
272 //
273 /// Returns the total FLOW locked under a given NFT
274 ///
275 /// @param forNFT: The ID of the NFT to get the total locked FLOW for
276 ///
277 /// @return The total FLOW locked under the NFT, or nil if the NFT is not registered
278 ///
279 access(all) view fun getLockupTotal(forNFT: UInt64): UFix64? {
280 if let summary = self._borrowSummary(forNFT: forNFT) {
281 return summary.getLockupTotal()
282 }
283 return nil
284 }
285
286 /// Returns the total boost amount in rewards against the amount locked under a given NFT
287 ///
288 /// @param forNFT: The ID of the NFT for which to get the boost amount
289 ///
290 /// @return The total boost amount in rewards under the NFT. If the boost model has not yet been defined or the
291 /// NFT is not registered, nil is returned
292 ///
293 access(all) view fun getBoostFlowRewards(forNFT: UInt64): UFix64? {
294 if let summary = self._borrowSummary(forNFT: forNFT) {
295 return summary.getBoostRewards()
296 }
297 return nil
298 }
299
300 /// Returns the total final boost amount in rewards an NFT will recieve over the full lock period according to the
301 /// contract's boost model
302 ///
303 /// @param forNFT: The ID of the NFT to get the final boost amount for
304 ///
305 /// @return The total final boost amount in rewards under the NFT. If the boost model has not yet been defined or the
306 /// NFT is not registered, nil is returned
307 ///
308 access(all) view fun getFinalBoostRewardTotal(forNFT: UInt64): UFix64? {
309 if let summary = self._borrowSummary(forNFT: forNFT) {
310 return summary.getFinalBoostRewardTotal()
311 }
312 return nil
313 }
314
315 /// Returns the total overview of a given NFT
316 ///
317 /// @param forNFT: The ID of the NFT to get the overview for
318 ///
319 /// @return The total overview of the NFT. If the NFT is not registered or the boost model has not yet been
320 /// defined, nil is returned
321 ///
322 access(all) fun getOverview(forNFT: UInt64): FlowRewardsMetadataViews.LockOverview? {
323 let now = Clock.time()
324 if let summary = self._borrowSummary(forNFT: forNFT) {
325 let totalLocked = summary.getLockupTotal()
326 let totalRewards = summary.getFinalBoostRewardTotal()
327 let tier = UInt8(self.getTierNumber(forAmount: totalLocked))
328 let tierName = self.getTierName(forAmount: totalLocked)
329 let allowableLockedDistribution = self._getAllowableDistribution(summary, locked: true, atTime: now) ?? 0.0
330 let allowableRewardDistribution = self._getAllowableDistribution(summary, locked: false, atTime: now) ?? 0.0
331 return FlowRewardsMetadataViews.LockOverview(
332 id: forNFT,
333 sequence: summary.sequence,
334 tier: tier,
335 tierName: tierName,
336 lockups: summary.copyLockups(),
337 totalLocked: totalLocked,
338 totalRewards: totalRewards,
339 allowableLockedDistribution: allowableLockedDistribution,
340 lockedClaimed: summary.lockedClaimed,
341 allowableRewardDistribution: allowableRewardDistribution,
342 rewardsClaimed: summary.rewardsClaimed
343 )
344 }
345 return nil
346 }
347
348 /// Returns the eligible claims for a given NFT indexed on the Valet serving those claims
349 ///
350 /// @param forNFT: The ID of the NFT to get the eligible claims for
351 ///
352 /// @return A nested mapping indicating the claimable resources and their amounts in the inner mapping, itself
353 /// indexed on the type of the Valet serving those claims.
354 /// To execute claims, see `NFT.claim(fromValet: Type, claimType: Type): @AnyResource?`
355 ///
356 access(all) fun getEligibleClaims(forNFT: UInt64): {Type: {Type: UFix64}} {
357 return self._borrowValetManager()?.getEligibleClaims(forNFT: forNFT) ?? {}
358 }
359
360 /// Returns the simulated Overview of a lockup that occurred at `start` with `amount` up through `snapshot` or the
361 /// current block timestamp if `snapshot` is nil
362 ///
363 /// @param start: The timestamp at which the lockup occurred. If nil, current timestamp is used
364 /// @param amount: The amount locked in the lockup
365 /// @param snapshot: The snapshot time to simulate the lockup up to, or nil to simulate up to the end of the lock
366 /// period. This value must be a timestamp beyond the start time or the function will revert
367 ///
368 /// @return The simulated Overview of the lockup capturing how much would be locked, how much would be rewarded, and
369 ///
370 access(all) fun simulateLockup(
371 start: UFix64?,
372 amount: UFix64,
373 snapshot: UFix64?
374 ): FlowRewardsMetadataViews.LockOverview {
375 pre {
376 self.getLockPeriodEnd() != nil:
377 "Cannot simulate a lockup without a defined lock period - try again after the lock period has been set"
378 start ?? getCurrentBlock().timestamp < snapshot ?? self.getLockPeriodEnd()!:
379 "Start time must be before the snapshot"
380 }
381 let lockupTimestamp = start ?? getCurrentBlock().timestamp
382 let upTo = snapshot ?? self.getLockPeriodEnd()!
383 // Construct a summary with the lockup
384 let lockup = Lockup(timestamp: lockupTimestamp, amount: amount, lockedBy: self.account.address)
385 let summary = RewardSummary(0)
386 summary.addLockup(lockup)
387 let summaryRef = &summary as &RewardSummary
388
389 // Calculate the total boost amount up to the snapshot
390 let totalRewards = summary.getFinalBoostRewardTotal()
391 // Return the Overview
392 let tier = UInt8(self.getTierNumber(forAmount: amount))
393 let tierName = self.getTierName(forAmount: amount)
394 let allowableLockedDistribution = self._getAllowableDistribution(summaryRef, locked: true, atTime: snapshot) ?? 0.0
395 let allowableRewardDistribution = self._getAllowableDistribution(summaryRef, locked: false, atTime: snapshot) ?? 0.0
396 return FlowRewardsMetadataViews.LockOverview(
397 id: 0,
398 sequence: UInt64.max,
399 tier: tier,
400 tierName: tierName,
401 lockups: summary.copyLockups(),
402 totalLocked: amount,
403 totalRewards: totalRewards,
404 allowableLockedDistribution: allowableLockedDistribution,
405 lockedClaimed: 0.0,
406 allowableRewardDistribution: allowableRewardDistribution,
407 rewardsClaimed: 0.0
408 )
409 }
410
411 /// Returns the page the given NFT can be found via getPaginatedOverviews() or nil if the NFT does not exist
412 ///
413 /// @param id: The ID of the NFT to find the page for
414 ///
415 /// @return The page number the NFT can be found on
416 ///
417 access(all) fun getPageForNFT(id: UInt64): UInt? {
418 if let sequence = self.sequenceRegistry[id] {
419 return UInt(sequence / self.balancerInterval)
420 }
421 return nil
422 }
423
424 /// Returns the Overviews from the registry in paginated form as they're contained in stored Registry resources. If
425 /// caller wishes to retrieve all Overviews, they should iterate over the paginated results until nil is returned.
426 ///
427 /// @param startPage: The page 0-indexed number to retrieve where page size is dictated by the contract's
428 /// balancerInterval value
429 /// @param numPages: The number of pages to retrieve
430 ///
431 /// @return The paginated Overviews of the registry indexed on NFT ID, or nil if the page is out of bounds
432 ///
433 access(all) fun getPaginatedOverviews(
434 startPage: UInt,
435 numPages: UInt
436 ): {UInt64: FlowRewardsMetadataViews.LockOverview}? {
437 var currPage = startPage
438 let endPage = currPage + numPages
439 var sequence = self.balancerInterval * UInt64(currPage)
440 if self.totalSupply <= sequence {
441 return nil
442 }
443 let overviews: {UInt64: FlowRewardsMetadataViews.LockOverview} = {}
444 while currPage < endPage {
445 // Find the registry for the page's starting sequence
446 let registry = self._borrowRegistry(forSequence: sequence)
447 if registry == nil {
448 // No registry found, end the loop
449 break
450 }
451 let now = Clock.time()
452 registry!.forEachSummary(fun (id: UInt64): Bool {
453 let summary = registry!.borrowSummary(id: id)! as! &RewardSummary
454 let totalLocked = summary.getLockupTotal()
455 let totalRewards = summary.getFinalBoostRewardTotal()
456 let tier = UInt8(self.getTierNumber(forAmount: totalLocked))
457 let tierName = self.getTierName(forAmount: totalLocked)
458 let allowableLockedDistribution = self._getAllowableDistribution(summary, locked: true, atTime: now) ?? 0.0
459 let allowableRewardDistribution = self._getAllowableDistribution(summary, locked: false, atTime: now) ?? 0.0
460 overviews[id] = FlowRewardsMetadataViews.LockOverview(
461 id: id,
462 sequence: summary.sequence,
463 tier: tier,
464 tierName: tierName,
465 lockups: summary.copyLockups(),
466 totalLocked: totalLocked,
467 totalRewards: totalRewards,
468 allowableLockedDistribution: allowableLockedDistribution,
469 lockedClaimed: summary.lockedClaimed,
470 allowableRewardDistribution: allowableRewardDistribution,
471 rewardsClaimed: summary.rewardsClaimed
472 )
473 return true
474 })
475 // Move to the next page & increment start sequence by the balancer interval
476 currPage = currPage + 1
477 sequence = sequence + self.balancerInterval
478 }
479
480 return overviews
481 }
482
483 /* --- Path Derivation --- */
484 //
485 /// Derive the storage path of the Registry in which the NFT is stored
486 ///
487 /// @param forSequence: The sequence number of the NFT to derive the Registry storage path for
488 ///
489 /// @return The derived storage path of the Registry - does not guarantee a Registry exists at this path
490 ///
491 access(all) view fun deriveRegistryStoragePath(forSequence: UInt64): StoragePath {
492 let prefix = "flowRewardsRegistry_"
493 let sequenceStr = (forSequence - forSequence % self.balancerInterval).toString()
494 return StoragePath(identifier: prefix .concat(sequenceStr))!
495 }
496
497 /* --- Metadata Resolution --- */
498 //
499 /// Non-standard MetadataViews resolution for FlowRewards NFT Display to allow for proxying EVM token URI requests
500 ///
501 /// @param forNFT: The ID of the NFT to get the display for
502 ///
503 /// @return The resolved MetadataViews.Display for the given NFT, or nil if the NFT is not registered
504 ///
505 access(all)
506 fun resolveNFTDisplay(forNFT: UInt64): MetadataViews.Display? {
507 if let summary = self._borrowSummary(forNFT: forNFT) {
508 let encodedSVG = Base64Util.encode(FlowRewards._renderSVG(id: forNFT, summary: summary))
509 return MetadataViews.Display(
510 name: "Flow Rewards NFT #".concat(forNFT.toString()),
511 description: "The OG Flow Rewards NFT",
512 thumbnail: MetadataViews.HTTPFile(
513 url: "data:image/svg+xml;base64,".concat(encodedSVG),
514 )
515 )
516 }
517 return nil
518 }
519
520 /// Non-standard MetadataViews resolution for FlowRewards NFT Traits to expose additional metadata
521 ///
522 /// @param forNFT: The ID of the NFT to get the traits for
523 ///
524 /// @return The resolved MetadataViews.Traits for the given NFT, or nil if the NFT is not registered
525 ///
526 access(all)
527 fun resolveNFTTraits(forNFT: UInt64): MetadataViews.Traits? {
528 if let overview = self.getOverview(forNFT: forNFT) {
529 let metadata: {String: AnyStruct} = {}
530 // Protect from underflow - this should never happen, but it's good to be safe
531 let claimableLocked = overview.lockedClaimed <= overview.allowableLockedDistribution ?
532 overview.allowableLockedDistribution - overview.lockedClaimed : 0.0
533 let claimableRewards = overview.rewardsClaimed <= overview.allowableRewardDistribution ?
534 overview.allowableRewardDistribution - overview.rewardsClaimed : 0.0
535 // Many platforms render straight from the returned trait name, hence the human-readable keys
536 metadata["Minted Timestamp"] = overview.lockups[0].timestamp
537 metadata["Sequence"] = overview.sequence
538 metadata["Tier Number"] = overview.tier
539 metadata["Tier Name"] = overview.tierName
540 metadata["Total Locked"] = overview.totalLocked
541 metadata["Total Rewards"] = overview.totalRewards
542 metadata["Locked Claimed"] = overview.lockedClaimed
543 metadata["Rewards Claimed"] = overview.rewardsClaimed
544 metadata["Claimable Locked Distribution"] = claimableLocked
545 metadata["Claimable Reward Distribution"] = claimableRewards
546 metadata["Total Remaining FLOW"] = overview.totalRemainingFlow
547
548 return MetadataViews.dictToTraits(dict: metadata, excludedNames: [])
549 }
550 return nil
551 }
552
553 /// Returns the contract MetadataViews supported by this contract
554 ///
555 /// @return The supported MetadataViews
556 ///
557 access(all) view fun getContractViews(resourceType: Type?): [Type] {
558 return [
559 Type<MetadataViews.NFTCollectionData>(),
560 Type<MetadataViews.NFTCollectionDisplay>(),
561 Type<MetadataViews.Royalties>(),
562 Type<MetadataViews.EVMBridgedMetadata>()
563 ]
564 }
565
566 /// Resolves a given MetadataView
567 ///
568 /// @param view: The type of view to resolve
569 ///
570 /// @return The resolved MetadataView as AnyStruct or nil if it's not supported
571 ///
572 access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
573 switch viewType {
574 case Type<MetadataViews.NFTCollectionData>():
575 return MetadataViews.NFTCollectionData(
576 storagePath: FlowRewards.CollectionStoragePath,
577 publicPath: FlowRewards.CollectionPublicPath,
578 publicCollection: Type<&Collection>(),
579 publicLinkedType: Type<&Collection>(),
580 createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} {
581 return <-FlowRewards.createEmptyCollection(nftType: Type<@FlowRewards.NFT>())
582 })
583 )
584 case Type<MetadataViews.NFTCollectionDisplay>():
585 return MetadataViews.NFTCollectionDisplay(
586 name: "Flow Rewards NFT",
587 description: "Lock & claim FLOW rewards and more",
588 externalURL: MetadataViews.ExternalURL("https://rewards.flow.com"),
589 squareImage: MetadataViews.Media(
590 file: MetadataViews.IPFSFile(cid: "QmYuLdXMbmDoUjGwKxfv3Jk3cgKV6mxYBAewNpxijkYYZn", path: nil),
591 mediaType: "image/png"
592 ),
593 bannerImage: MetadataViews.Media(
594 file: MetadataViews.IPFSFile(cid: "QmTBnCqNwgE8AiHPs9YxuVb8HSKWA2RvY5Q4NYm1tvW6wq", path: nil),
595 mediaType: "image/png"
596 ),
597 socials: {
598 "discord": MetadataViews.ExternalURL("https://discord.com/invite/flowblockchain"),
599 "instagram": MetadataViews.ExternalURL("https://www.instagram.com/flowblockchain"),
600 "github": MetadataViews.ExternalURL("https://github.com/onflow/crescendo-rewards-sc"),
601 "twitter": MetadataViews.ExternalURL("https://twitter.com/flow_blockchain")
602 }
603 )
604 case Type<MetadataViews.Royalties>():
605 return MetadataViews.Royalties([])
606 case Type<MetadataViews.EVMBridgedMetadata>():
607 return MetadataViews.EVMBridgedMetadata(
608 name: "Flow Rewards",
609 symbol: "RWDS",
610 uri: MetadataViews.HTTPFile(
611 url: "https://rewards.flow.com/contract-uri.json"
612 )
613 )
614 default:
615 return nil
616 }
617 }
618
619 /// Creates an empty Collection
620 ///
621 /// @return The created Collection
622 ///
623 access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {
624 return <-create Collection()
625 }
626
627 /* ======================
628 Constructs
629 ====================== */
630
631
632 /* --- NFT --- */
633 //
634 /// Exposes public methods on the NFT resource
635 ///
636 access(all) resource interface NFTPublic {
637 access(all) let id: UInt64
638 access(all) let sequence: UInt64
639 access(all) view fun getLockedVaultUUID(): UInt64
640 access(all) view fun getCurrentLockedAmount(): UFix64
641 access(all) view fun getTotalLockupAmount(): UFix64
642 access(all) fun addLockup(_ flow: @FlowToken.Vault)
643 }
644
645 /// The NFT resource representing a claim on locked FLOW and an endpoint against which metadata can be resolved
646 ///
647 access(all) resource NFT : NFTPublic, NonFungibleToken.NFT {
648 /// The ID of the NFT
649 access(all) let id: UInt64
650 /// The sequence number of the NFT - used to determine the sequence of minting and to balance registered
651 /// summaries in contract storage
652 access(all) let sequence: UInt64
653 /// The FLOW locked under the NFT
654 access(self) let lockedVault: @FlowToken.Vault
655
656 init() {
657 self.id = self.uuid
658
659 // Increment total supply and assign the sequence number
660 self.sequence = FlowRewards.totalSupply
661 FlowRewards.totalSupply = FlowRewards.totalSupply + 1
662
663 // Ensure there's a registry ready for the new NFT
664 FlowRewards._safeConfigureRegistry()
665
666 self.lockedVault <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
667 }
668
669 /// Returns the UUID of the locked pool wrapped in this NFT
670 ///
671 /// @return The UUID of the locked pool
672 ///
673 access(all) view fun getLockedVaultUUID(): UInt64 {
674 return self.lockedVault.uuid
675 }
676
677 /// Returns the total FLOW **currently** locked under the NFT
678 ///
679 /// @return The total FLOW locked under the NFT
680 ///
681 access(all) view fun getCurrentLockedAmount(): UFix64 {
682 return self.lockedVault.balance
683 }
684
685 /// Returns the total FLOW **historically** locked under the NFT
686 ///
687 /// @return The total FLOW locked under the NFT during its lifetime
688 ///
689 access(all) view fun getTotalLockupAmount(): UFix64 {
690 return FlowRewards.getLockupTotal(forNFT: self.id) ?? 0.0
691 }
692
693 /// Adds a lockup under this NFT
694 ///
695 /// @param flow: The FLOW to lock
696 ///
697 access(all) fun addLockup(_ flow: @FlowToken.Vault) {
698 pre {
699 self.owner != nil:
700 "The Rewards NFT to add the lockup to is not currently stored in an account - store it in the owner's account's collection and try again"
701 }
702 FlowRewards._lock(
703 <-flow,
704 nft: &self as auth(NonFungibleToken.Update) &NFT
705 )
706
707 // Announce that NFT metadata has been updated via NFT standard route
708 FlowRewards.emitNFTUpdated(&self as auth(NonFungibleToken.Update) &{NonFungibleToken.NFT})
709 }
710
711 /// Returns any the amount of claims this NFT is eligible for
712 ///
713 /// @return The amount of claims this NFT is eligible for
714 ///
715 access(all) fun getEligibleClaims(): {Type: {Type: UFix64}} {
716 return FlowRewards.getEligibleClaims(forNFT: self.id)
717 }
718
719 /// Claims any eligible rewards resources currently supported via the ValetManager
720 ///
721 /// @param type: The type of resource to claim
722 ///
723 /// @return The claimed resource if successful, nil otherwise
724 ///
725 access(Claim) fun claim(fromValet: Type, claimType: Type): @AnyResource? {
726 let nft = &self as &NFT
727 let manager = FlowRewards._borrowValetManager()
728 ?? panic("Claiming is not currently enabled - could not borrow reference to ValetManager")
729 let claim <- manager.claim(valetType: fromValet, forNFT: nft, claimType: claimType)
730 // Announce that NFT metadata has been updated via NFT standard route
731 if claim != nil {
732 FlowRewards.emitNFTUpdated(&self as auth(NonFungibleToken.Update) &{NonFungibleToken.NFT})
733 }
734 return <-claim
735 }
736
737 /// Returns the supported metadata views for this NFT
738 ///
739 access(all) view fun getViews(): [Type] {
740 return [
741 Type<MetadataViews.Display>(),
742 Type<MetadataViews.ExternalURL>(),
743 Type<MetadataViews.NFTView>(),
744 Type<MetadataViews.NFTCollectionData>(),
745 Type<MetadataViews.NFTCollectionDisplay>(),
746 Type<FlowRewardsMetadataViews.LockOverview>(),
747 Type<MetadataViews.Traits>(),
748 Type<MetadataViews.Royalties>(),
749 Type<MetadataViews.EVMBridgedMetadata>()
750 ]
751 }
752
753 /// Resolves the given metadata view or returns nil if it's not supported
754 ///
755 access(all) fun resolveView(_ view: Type): AnyStruct? {
756 switch view {
757 case Type<MetadataViews.Display>():
758 return FlowRewards.resolveNFTDisplay(forNFT: self.id)
759 case Type<MetadataViews.ExternalURL>():
760 return MetadataViews.ExternalURL("https://rewards.flow.com")
761 case Type<FlowRewardsMetadataViews.LockOverview>():
762 return FlowRewards.getOverview(forNFT: self.id)
763 case Type<MetadataViews.NFTView>():
764 let display = self.resolveView(Type<MetadataViews.Display>())! as! MetadataViews.Display
765 let externalURL = self.resolveView(Type<MetadataViews.ExternalURL>())! as! MetadataViews.ExternalURL
766 let collectionData = FlowRewards.resolveContractView(
767 resourceType: self.getType(),
768 viewType: Type<MetadataViews.NFTCollectionData>()
769 )! as! MetadataViews.NFTCollectionData
770 let collectionDisplay = FlowRewards.resolveContractView(
771 resourceType: self.getType(),
772 viewType: Type<MetadataViews.NFTCollectionDisplay>()
773 )! as! MetadataViews.NFTCollectionDisplay
774 let traits = self.resolveView(Type<MetadataViews.Traits>())! as! MetadataViews.Traits
775 return MetadataViews.NFTView(
776 id: self.id,
777 uuid: self.uuid,
778 display: display,
779 externalURL: externalURL,
780 collectionData: collectionData,
781 collectionDisplay: collectionDisplay,
782 royalties: nil,
783 traits: traits
784 )
785 case Type<MetadataViews.NFTCollectionData>():
786 return FlowRewards.resolveContractView(resourceType: self.getType(), viewType: view)
787 case Type<MetadataViews.NFTCollectionDisplay>():
788 return FlowRewards.resolveContractView(resourceType: self.getType(), viewType: view)
789 case Type<MetadataViews.Traits>():
790 return FlowRewards.resolveNFTTraits(forNFT: self.id)
791 // Used by VM Bridge when bridging to EVM as ERC721
792 case Type<MetadataViews.EVMBridgedMetadata>():
793 let contractMD = FlowRewards.resolveContractView(
794 resourceType: self.getType(),
795 viewType: Type<MetadataViews.EVMBridgedMetadata>()
796 )! as! MetadataViews.EVMBridgedMetadata
797 return MetadataViews.EVMBridgedMetadata(
798 name: contractMD.name,
799 symbol: contractMD.symbol,
800 uri: MetadataViews.URI(
801 baseURI:"https://rewards.flow.com/nft/token-uri?contractAddress="
802 .concat(FlowRewards.account.address.toString()).concat("&id="),
803 value: self.id.toString()
804 )
805 )
806 case Type<MetadataViews.Royalties>():
807 return FlowRewards.resolveContractView(resourceType: self.getType(), viewType: view)
808 default:
809 return nil
810 }
811 }
812
813 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
814 return <-FlowRewards.createEmptyCollection(nftType: Type<@FlowRewards.NFT>())
815 }
816
817 /// Locks the funds within the NFT's locked pool
818 ///
819 access(contract) fun depositLockedFlowCallback(_ flow: @FlowToken.Vault) {
820 pre {
821 FlowRewards.isLockPeriodActive():
822 "Cannot lock FLOW outside of the lock period"
823 }
824 post {
825 self.getCurrentLockedAmount() == before(self.getCurrentLockedAmount()) + before(flow.balance):
826 "Error depositing amount=".concat(before(flow.balance).toString())
827 .concat(" into nft.id=").concat(self.id.toString())
828 FlowRewards.totalValueLocked == before(FlowRewards.totalValueLocked + flow.balance):
829 "Error updating totalValueLocked with amount=".concat(before(flow.balance).toString())
830 .concat(" during lock to nft.id=").concat(self.id.toString())
831 FlowRewards.getLockupTotal(forNFT: self.id) == self.getCurrentLockedAmount():
832 "Error updating lockup total with amount=".concat(self.getCurrentLockedAmount().toString())
833 .concat(" for nft.id=").concat(self.id.toString())
834 }
835 let amount = flow.balance
836 let vaultUUID = flow.uuid
837 FlowRewards.totalValueLocked = FlowRewards.totalValueLocked + amount
838
839 self.lockedVault.deposit(from: <-flow)
840
841 let initiator = self.owner?.address
842 ?? panic("NFT id=".concat(self.id.toString())
843 .concat(" must be stored in an account's collection when locking FLOW"))
844 let lockup = Lockup(
845 timestamp: Clock.time(),
846 amount: amount,
847 lockedBy: initiator
848 )
849 let summary = FlowRewards._borrowSummary(forNFT: self.id) ?? panic("NFT is not registered to registry")
850 summary.addLockup(lockup)
851
852 emit Locked(
853 nftID: self.id,
854 nftSequence: self.sequence,
855 amount: amount,
856 lockedUUID: vaultUUID,
857 toVaultUUID: self.lockedVault.uuid,
858 nftTVL: self.getCurrentLockedAmount(),
859 contractTVL: FlowRewards.getTotalValueLocked(),
860 initiator: initiator
861 )
862 }
863
864 /// Returns the locked FLOW to internal contract caller
865 ///
866 access(contract) fun claimLockedFlowCallback(): @{FungibleToken.Vault} {
867 pre {
868 FlowRewards.isDistributionPeriodActive():
869 "Cannot claim locked FLOW outside of the distribution period"
870 }
871 post {
872 FlowRewards.totalValueLocked == before(FlowRewards.totalValueLocked) - result.balance:
873 "Error updating totalValueLocked with amount=".concat(result.balance.toString())
874 .concat(" during claim for nft.id=").concat(self.id.toString())
875 self.getCurrentLockedAmount() == before(self.getCurrentLockedAmount()) - result.balance:
876 "Error claiming amount=".concat(result.balance.toString())
877 .concat(" from nft.id=").concat(self.id.toString())
878 }
879 let now = Clock.time()
880
881 // Within the distribution period, calculate eligible distribution
882 let summary = FlowRewards._borrowSummary(forNFT: self.id)
883 ?? panic("Could not find summary for NFT id=".concat(self.id.toString()))
884
885 // Get total & remaining locked & rewards
886 let totalLocked = summary.getLockupTotal()
887 var remainingLocked = totalLocked - summary.lockedClaimed
888
889 // All value has been distributed, nothing left to claim - return empty Vault
890 if summary.lockedClaimed == totalLocked {
891 return <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
892 }
893
894 // Assess distribution against both locked, subtracting any already claimed
895 var lockedDistribution = FlowRewards._getAllowableDistribution(summary, locked: true, atTime: now)!
896
897 lockedDistribution = lockedDistribution <= remainingLocked ? lockedDistribution : remainingLocked
898
899 // Ensure this and historical distributions do not exceed total locked
900 assert(lockedDistribution <= totalLocked, message: "Locked distribution exceeds amount locked for this NFT")
901
902 // Update the summary with the claimed distribution which returns the remaining locked amount
903 remainingLocked = summary.markLockedClaimed(delta: lockedDistribution)
904
905 // Decrease the total value locked in the contract, withdraw, emit and return the distribution
906 FlowRewards.totalValueLocked = FlowRewards.totalValueLocked - lockedDistribution
907 let distribution <- self.lockedVault.withdraw(amount: lockedDistribution)
908 assert(
909 remainingLocked == self.getCurrentLockedAmount(),
910 message: "Remaining=".concat(remainingLocked.toString())
911 .concat(" in summary does not match current locked amount=")
912 .concat(self.getCurrentLockedAmount().toString())
913 )
914
915 if distribution.balance > 0.0 {
916 emit DistributionClaimed(
917 id: self.id,
918 amountClaimed: lockedDistribution,
919 amountRemaining: remainingLocked,
920 distributedUUID: distribution.uuid,
921 locked: true,
922 claimant: self.owner?.address
923 )
924 }
925
926 return <-distribution
927 }
928 }
929
930 /* --- Collection --- */
931 //
932 /// Exposes public methods on the Collection resource
933 ///
934 access(all) resource interface CollectionPublic {
935 access(all) view fun getLength(): Int
936 access(all) view fun getIDs(): [UInt64]
937 access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}?
938 access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}?
939 access(all) fun forEachID(_ f: fun (UInt64): Bool): Void
940 access(all) fun deposit(token: @{NonFungibleToken.NFT})
941 }
942
943 /// The Collection resource for managing a collection of owned NFTs
944 ///
945 access(all) resource Collection : CollectionPublic, NonFungibleToken.Collection {
946 /// The NFTs owned by the Collection
947 access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
948
949 init() {
950 self.ownedNFTs <- {}
951 }
952
953 /// Initializes a lockup under the Collection, resulting in a new NFT in the Collection
954 ///
955 /// @param flow: The FLOW to lock
956 ///
957 /// @return The ID of the newly minted NFT
958 ///
959 access(all) fun initLock(_ flow: @FlowToken.Vault): UInt64 {
960 return FlowRewards._initLock(
961 <-flow,
962 initiator: &self as &Collection
963 )
964 }
965
966 /// Removes an NFT from the Collection and returns it
967 ///
968 /// @param withdrawID: The ID of the NFT to withdraw
969 ///
970 /// @return The withdrawn NFT
971 ///
972 access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
973 pre {
974 self.ownedNFTs[withdrawID] != nil:
975 "Cannot withdraw NFT with id=".concat(withdrawID.toString())
976 .concat(" because it does not exist in the withdrawer's collection!")
977 }
978 return <- self.ownedNFTs.remove(key: withdrawID)!
979 }
980
981 /// Returns a mapping of NFT types that this receiver may accept and whether or not it supports them
982 ///
983 /// @return A dictionary of types mapped to booleans indicating if this receiver supports it
984 ///
985 access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
986 return { Type<@FlowRewards.NFT>(): true }
987 }
988
989 /// Returns whether or not the given type is accepted by the collection
990 ///
991 /// @param type: An NFT type
992 ///
993 /// @return A boolean indicating if this receiver can recieve the desired NFT type
994 ///
995 access(all) view fun isSupportedNFTType(type: Type): Bool {
996 return self.getSupportedNFTTypes()[type] ?? false
997 }
998
999 /// Adds an NFT to the Collection
1000 ///
1001 /// @param token: The NFT to add
1002 ///
1003 access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
1004 pre {
1005 self.isSupportedNFTType(type: token.getType()):
1006 "Token of type=".concat(token.getType().identifier).concat(" is not supported by this Collection!")
1007 self.ownedNFTs[token.id] == nil: "NFT already exists in the collection!"
1008 }
1009 self.ownedNFTs[token.id] <-! token
1010 }
1011
1012 /// Returns the number of NFTs in the Collection
1013 ///
1014 /// @return The number of NFTs in the Collection
1015 ///
1016 access(all) view fun getLength(): Int {
1017 return self.ownedNFTs.length
1018 }
1019
1020 /// Iterates over the IDs of the NFTs in the Collection executing the given function
1021 ///
1022 /// @param f: The function to execute for each ID
1023 ///
1024 access(all) fun forEachID(_ f: fun (UInt64): Bool) {
1025 self.ownedNFTs.forEachKey(f)
1026 }
1027
1028 /// Returns the IDs of the NFTs in the Collection
1029 ///
1030 /// @return The IDs of the NFTs in the Collection
1031 ///
1032 access(all) view fun getIDs(): [UInt64] {
1033 return self.ownedNFTs.keys
1034 }
1035
1036 /// Returns a reference to an NFT in the Collection as a generic NFT, reverting if the NFT is not found
1037 ///
1038 /// @param id: The ID of the NFT to borrow
1039 ///
1040 /// @return A reference to the NFT if it exists in the Collection, reverts otherwise
1041 ///
1042 access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
1043 return &self.ownedNFTs[id]
1044 }
1045
1046 /// Returns an entitled reference to an NFT in the Collection as a generic NFT or nil if the NFT is not found
1047 ///
1048 /// @param id: The ID of the NFT to borrow
1049 ///
1050 /// @return A reference to the NFT with the Claim entitlement if it exists in the Collection, nil otherwise
1051 ///
1052 access(Claim) view fun borrowClaimableNFT(_ id: UInt64): auth(Claim) &NFT? {
1053 let nft: auth(Claim) &{NonFungibleToken.NFT}? = &self.ownedNFTs[id]
1054 return nft as! auth(Claim) &NFT?
1055 }
1056
1057 /// Returns a reference to an owned NFT as a Resolver, reverting if the NFT is not found
1058 ///
1059 /// @param id: The ID of the NFT to borrow
1060 ///
1061 /// @return A reference to the NFT if it exists in the Collection, reverts otherwise
1062 ///
1063 access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? {
1064 return self.borrowNFT(id) ?? panic(
1065 "Cannot borrow NFT with id=".concat(id.toString())
1066 .concat(" because it does not exist in the withdrawer's collection!")
1067 )
1068 }
1069
1070 /// Returns a new empty Collection
1071 ///
1072 /// @return A new empty Collection
1073 ///
1074 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
1075 return <-FlowRewards.createEmptyCollection(nftType: Type<@FlowRewards.NFT>())
1076 }
1077 }
1078
1079 /// Representation of a lockup entry
1080 ///
1081 access(all) struct Lockup : FlowRewardsRegistry.Lockup {
1082 /// The timestamp at which the lockup was executed
1083 access(all) let timestamp: UFix64
1084 /// The amount locked
1085 access(all) let amount: UFix64
1086 /// The Address of the initiator of the lockup
1087 access(all) let lockedBy: Address
1088
1089 view init(timestamp: UFix64, amount: UFix64, lockedBy: Address) {
1090 self.timestamp = timestamp
1091 self.amount = amount
1092 self.lockedBy = lockedBy
1093 }
1094 }
1095
1096 /// Data structure capturing lockup and claiming actions for a given NFT
1097 ///
1098 access(all) struct RewardSummary : FlowRewardsRegistry.Summary {
1099 /// A sequential array of lockup entries
1100 access(all) var lockups: [{FlowRewardsRegistry.Lockup}]
1101 /// The total amount lock amount that has been claimed
1102 access(all) var lockedClaimed: UFix64
1103 /// The total amount of rewards that have been claimed
1104 access(all) var rewardsClaimed: UFix64
1105
1106 access(all) var sequence: UInt64
1107
1108 view init(_ sequence: UInt64) {
1109 self.lockups = []
1110 self.lockedClaimed = 0.0
1111 self.rewardsClaimed = 0.0
1112 self.sequence = sequence
1113 }
1114
1115 /// Returns the total FLOW locked under the NFT
1116 ///
1117 /// @return The total FLOW locked under the NFT
1118 ///
1119 access(all) view fun getLockupTotal(): UFix64 {
1120 var total = 0.0
1121 for lockup in self.lockups {
1122 total = total + lockup.amount
1123 }
1124 return total
1125 }
1126
1127 /// Returns the total boost amount according to the current BoostModel assigned in FlowRewards
1128 ///
1129 /// @return The total boost amount in rewards based on the amount locked at the time of request
1130 ///
1131 access(all) view fun getBoostRewards(): UFix64 {
1132 let ref = &self as &RewardSummary
1133 return FlowRewards._borrowRewardBoostModel()?.calculateBoostAmount(summary: ref, upTo: nil) ?? 0.0
1134 }
1135
1136 /// Copies the lockups array. Useful since often this struct is accessed by reference and we can't currently
1137 /// dereference non-primitive types
1138 ///
1139 /// @return A copy of the lockups array
1140 ///
1141 access(all) view fun copyLockups(): [{FlowRewardsRegistry.Lockup}] {
1142 return self.lockups
1143 }
1144
1145 /// Return the total FLOW rewards this summary will generate over the full lock period given the contained
1146 /// Lockups
1147 ///
1148 /// @return The total FLOW rewards this summary will generate over the full lock period
1149 ///
1150 access(all) view fun getFinalBoostRewardTotal(): UFix64 {
1151 let ref = &self as &RewardSummary
1152 return FlowRewards._borrowRewardBoostModel()
1153 ?.calculateBoostAmount(
1154 summary: ref,
1155 upTo: FlowRewards.getLockPeriodEnd()!
1156 ) ?? 0.0
1157 }
1158
1159 /// Adds a lockup to the summary, ensuring that lockups are added in chronological order
1160 ///
1161 /// @param lockup: The lockup to add
1162 ///
1163 access(contract) fun addLockup(_ lockup: {FlowRewardsRegistry.Lockup}) {
1164 pre {
1165 lockup.getType() == Type<Lockup>(): "Invalid lockup type=".concat(lockup.getType().identifier)
1166 }
1167 self.lockups.append(lockup)
1168 }
1169
1170 /// Marks a portion of the locked amount as claimed
1171 ///
1172 /// @param delta: The amount to claim
1173 ///
1174 /// @return The remaining locked amount
1175 ///
1176 access(contract) fun markLockedClaimed(delta: UFix64): UFix64 {
1177 let totalLocked = self.getLockupTotal()
1178 assert(
1179 self.lockedClaimed + delta <= totalLocked,
1180 message: "Claimed locked amount=".concat(self.lockedClaimed.toString())
1181 .concat(" + delta=").concat(delta.toString())
1182 .concat(" exceeds total locked amount=").concat(totalLocked.toString())
1183 )
1184 self.lockedClaimed = self.lockedClaimed + delta
1185 return totalLocked - self.lockedClaimed
1186 }
1187
1188 /// Marks a portion of the rewards as claimed
1189 ///
1190 /// @param delta: The amount to claim
1191 ///
1192 /// @return The remaining rewards amount
1193 ///
1194 access(contract) fun markRewardsClaimed(delta: UFix64): UFix64 {
1195 let boostRewards = self.getBoostRewards()
1196 self.rewardsClaimed = self.rewardsClaimed + delta
1197 assert(
1198 self.rewardsClaimed <= boostRewards,
1199 message: "Claimed rewards amount=".concat(self.rewardsClaimed.toString())
1200 .concat(" exceeds total boost rewards=").concat(boostRewards.toString())
1201 )
1202 return boostRewards - self.rewardsClaimed
1203 }
1204 }
1205
1206 /// An interface for boost models used to calculate rewards against locked FLOW
1207 ///
1208 access(all) resource interface BoostModel {
1209 /// Returns the time at which the boost model start accumulating boost rewards
1210 access(all) view fun getBoostStart(): UFix64
1211 /// Returns the time at which the boost model stops accumulating boost rewards
1212 access(all) view fun getBoostEnd(): UFix64
1213 /// Returns the starting boost factor
1214 access(all) view fun getStartingBoostFactor(): UFix64
1215 /// Returns the boost factor for a lockup executed at start up to the time defined by upTo
1216 access(all) view fun getBoostFactor(atTime: UFix64?): UFix64
1217 /// Returns the boost amount against a given RewardSummary up to a given time
1218 access(all) view fun calculateBoostAmount(
1219 summary: &{FlowRewardsRegistry.Summary},
1220 upTo: UFix64?
1221 ): UFix64
1222 }
1223
1224 /// An interface for distribution models used to calculate distribution of amount of locked & rewarded FLOW
1225 ///
1226 access(all) resource interface DistributionModel {
1227 /// Returns the time at which the distribtuion model starts distributing funds
1228 access(all) view fun getDistributionStart(): UFix64
1229 /// Returns the time at which the distribtuion model stops distributing funds
1230 access(all) view fun getDistributionEnd(): UFix64
1231 /// Returns the amount of locked + rewarded FLOW that can be distributed against a given RewardSummary
1232 /// NOTE: Any implementing contracts should be defined in this contract account as the summary is passed by
1233 /// reference
1234 access(all) view fun calculateDistribution(
1235 maxDistribution: UFix64,
1236 atTime: UFix64?
1237 ): UFix64
1238 }
1239
1240 /* --- Valet & ValetManager --- */
1241 //
1242 /// An interface for a resource that enables the claim of some resource against an FlowRewards NFT
1243 ///
1244 access(all) resource interface Valet {
1245 /// Returns the type of claimable asset this Valet is configured for
1246 access(all) view fun getClaimableTypes(): [Type]
1247 /// Returns the eligible claims for a given NFT indexed on the claimable asset type
1248 /// NOTE: Although the value of the inner mapping is a UFix64, the amount may be used to represent a quantity
1249 /// of the claimable asset - e.g. {ExampleNFT: 1.0} may indicate 1 ExampleNFT can be claimed
1250 access(all) fun getEligibleClaims(forNFT: UInt64): {Type: UFix64}
1251 /// Claims a resource against a given NFT. It's up to the Valet implementation to define the claim logic. The
1252 /// Valet serving rewards against locked FLOW is a passthrough, but others may be added in the future which
1253 /// leverage this route to deliver arbitrary rewards to NFT owners
1254 access(Claim) fun claim(forNFT: &NFT, claimType: Type): @AnyResource? {
1255 post {
1256 result == nil || result!.getType() == claimType:
1257 "Claim of type=".concat(claimType.identifier)
1258 .concat(" failed - returned resource type=").concat(result!.getType().identifier)
1259 }
1260 }
1261 }
1262
1263 /// Exposes public methods on the ValetManager resource
1264 ///
1265 access(all) resource interface ValetManagerPublic {
1266 access(all) view fun getValetTypes(): [Type]
1267 access(all) fun getEligibleClaims(forNFT: UInt64): {Type: {Type: UFix64}}
1268 }
1269
1270 /// Manages the Valet implementations which can be used to claim various generic resources against NFTs. Configuring
1271 /// new Valet implementations allow for the addition of new claimable resources via existing NFTs without updates to
1272 /// core contract logic.
1273 ///
1274 access(all) resource ValetManager : ValetManagerPublic {
1275 /// A mapping of types to their respective Valets. These should be indexed on the type of claimable asset
1276 access(self) let valets: @{Type: {Valet}}
1277
1278 init() {
1279 self.valets <- {}
1280 }
1281
1282 /// Returns the types of configured Valet implementations
1283 ///
1284 /// @return An array of all configured Valet types
1285 ///
1286 access(all) view fun getValetTypes(): [Type] {
1287 return self.valets.keys
1288 }
1289
1290 /// Returns the eligible claims for a given NFT against all configured Valets
1291 ///
1292 /// @param forNFT: The ID of the NFT to get the eligible claims for
1293 ///
1294 /// @return The eligible claims for the NFT indexed on the type of valet with an inner mapping of the claimable
1295 /// asset type and the amount that can be claimed
1296 ///
1297 access(all) fun getEligibleClaims(forNFT: UInt64): {Type: {Type: UFix64}} {
1298 var claims: {Type: {Type: UFix64}} = {}
1299 let selfRef = &self as &ValetManager
1300 self.valets.forEachKey(fun (type: Type): Bool {
1301 let valetClaims = selfRef._borrowValet(type)!.getEligibleClaims(forNFT: forNFT)
1302 if valetClaims.length == 0 {
1303 return true
1304 }
1305 claims.insert(key: type, valetClaims)
1306
1307 return true
1308 })
1309 return claims
1310 }
1311
1312 /// Inserts the provided Valet, indexing on its type. Reverts if a Valet of the same type already exists
1313 ///
1314 access(Integrate) fun addValet(_ valet: @{Valet}) {
1315 pre {
1316 self.valets[valet.getType()] == nil: "Valet already exists for type"
1317 }
1318 emit ValetManagerUpdated(
1319 valetType: valet.getType().identifier,
1320 valetUUID: valet.uuid,
1321 managerUUID: self.uuid,
1322 added: true
1323 )
1324 self.valets[valet.getType()] <-! valet
1325 }
1326
1327 /// Upserts a Valet, replacing any existing Valet of the same type
1328 /// NOTE: Removing a Valet will destroy it and voids any open claims NFTs may have against the claimable
1329 /// resource via that Valet
1330 ///
1331 access(Integrate) fun upsertValet(_ valet: @{Valet}) {
1332 self.removeValet(valet.getType())
1333 self.addValet(<-valet)
1334 }
1335
1336 /// Removes and destroys a valet of the given type
1337 /// NOTE: Removing a Valet will destroy it and voids any open claims NFTs may have against the claimable
1338 /// resources via that Valet
1339 ///
1340 access(Integrate) fun removeValet(_ type: Type) {
1341 if self._borrowValet(type) == nil {
1342 return
1343 }
1344 let old <- self.valets.remove(key: type)!
1345 emit ValetManagerUpdated(
1346 valetType: type.identifier,
1347 valetUUID: old.uuid,
1348 managerUUID: self.uuid,
1349 added: false
1350 )
1351 destroy <-old
1352 }
1353
1354 /// Claims a resource against a given NFT. It's up to the Valet implementation to define the claim logic
1355 ///
1356 access(contract) fun claim(valetType: Type, forNFT: &NFT, claimType: Type): @AnyResource? {
1357 if let valet = self._borrowValet(valetType) {
1358 return <- valet.claim(forNFT: forNFT, claimType: claimType)
1359 }
1360 return nil
1361 }
1362
1363 /// Borrows a reference to a Valet for a given type or nil if it does not exist
1364 ///
1365 access(self) view fun _borrowValet(_ type: Type): auth(Claim) &{Valet}? {
1366 return &self.valets[type] as auth(Claim) &{Valet}? ?? nil
1367 }
1368 }
1369
1370 /* --- Admin --- */
1371 //
1372 /// This resource enables custodians to execute privileged operational functions on the contract
1373 ///
1374 access(all) resource Admin {
1375
1376 /// Sets the minimum amount that can be locked in a single lockup
1377 ///
1378 access(Configure) fun setMinimumLockupAmount(_ amount: UFix64) {
1379 pre {
1380 amount > 0.0: "Minimum lockup amount must be greater than 0.0"
1381 }
1382 emit MinimumLockupAmountSet(old: FlowRewards.minLockupAmount, new: amount, adminUUID: self.uuid)
1383 FlowRewards.minLockupAmount = amount
1384 }
1385
1386 /// Sets the BoostModel implementation as the current reward boost model by which rewards are calculated and
1387 /// lock & boost period is defined. Pre-conditions ensure that the lock period does not overlap with the
1388 /// distribution period (if set) and that the model is set at least 24 hours before the distribution period
1389 /// ends. They also ensure that the intermission between lock and distribution periods is within the maximum
1390 /// threshold (see FlowRewards.maxIntermissionDuration)
1391 ///
1392 /// The relationship between lock and distribution periods is such that the distribution period must start after
1393 /// the lock period ends and before the maximum intermission threshold from the lock period end.
1394 ///
1395 /// |---> Lock Period <---|<---> Intermission <---|<---> Distribution Period <---|
1396 ///
1397 access(Parameterize) fun setRewardBoostModel(_ model: @{BoostModel}) {
1398 pre {
1399 Clock.time() + FlowRewards.modelSwapBuffer <= model.getBoostEnd():
1400 "New BoostModel must end at least 24 hours from when it is set"
1401 Clock.time() + FlowRewards.modelSwapBuffer <= FlowRewards.getLockPeriodEnd() ?? UFix64.max:
1402 "Current model ends within 24 hours - the current model can no longer be swapped"
1403 model.getBoostEnd() <= FlowRewards.getDistributionPeriodStart() ?? UFix64.max:
1404 "Provided BoostModel's lock period overlaps with distribution period start= "
1405 .concat(FlowRewards.getDistributionPeriodStart()!.toString())
1406 model.getStartingBoostFactor() >= FlowRewards._borrowRewardBoostModel()?.getStartingBoostFactor() ?? 0.0:
1407 "New BoostModel must have a higher weighted boost than the current model - up only"
1408 FlowRewards.getDistributionPeriodStart() != nil ? model.getBoostEnd() >= FlowRewards.getDistributionPeriodStart()! - FlowRewards.maxIntermissionDuration : true:
1409 "Provided model lock period end exceeds the maximum intermission threshold="
1410 .concat((FlowRewards.getDistributionPeriodStart()! - FlowRewards.maxIntermissionDuration).toString())
1411 model.getBoostEnd() - model.getBoostStart() <= FlowRewards.maxPeriodDuration:
1412 "Lock period exceeds maximum duration="
1413 .concat(FlowRewards.maxPeriodDuration.toString())
1414 }
1415
1416 let oldRef = FlowRewards._borrowRewardBoostModel()
1417 emit RewardModelSet(
1418 oldType: oldRef?.getType()?.identifier ?? "",
1419 oldStart: oldRef?.getBoostStart() ?? 0.0,
1420 oldEnd: oldRef?.getBoostEnd() ?? 0.0,
1421 oldUUID: oldRef?.uuid ?? 0,
1422 newType: model.getType().identifier,
1423 newStart: model.getBoostStart(),
1424 newEnd: model.getBoostEnd(),
1425 newUUID: model.uuid,
1426 adminUUID: self.uuid
1427 )
1428 let old <- FlowRewards.rewardBoostModel <- model
1429 destroy old
1430 }
1431
1432 /// Sets the DistributionModel implementation as the current reward distribution model by which locked &
1433 /// rewarded FLOW are distributed. Pre-conditions ensure that the distribution period does not overlap with the
1434 /// lock period and that the model is set at least 24 hours before the distribution period ends. They also
1435 /// ensure that the intermission between lock and distribution periods is within the maximum threshold.
1436 ///
1437 /// The relationship between lock and distribution periods is such that the distribution period must start after
1438 /// the lock period ends and before the maximum intermission threshold from the lock period end.
1439 ///
1440 /// |---> Lock Period <---|<---> Intermission <---|<---> Distribution Period <---|
1441 ///
1442 access(Parameterize) fun setRewardDistributionModel(_ model: @{DistributionModel}) {
1443 pre {
1444 Clock.time() + FlowRewards.modelSwapBuffer <= model.getDistributionEnd():
1445 "New DistributionModel must end at least 24 hours from when it is set"
1446 Clock.time() + FlowRewards.modelSwapBuffer <= FlowRewards.getDistributionPeriodEnd() ?? UFix64.max:
1447 "Current model ends within 24 hours - the current model can no longer be swapped"
1448 model.getDistributionStart() >= FlowRewards.getLockPeriodEnd() ?? 0.0:
1449 "Provided model distribution period overlaps with lock period end= "
1450 .concat(FlowRewards.getLockPeriodEnd()!.toString())
1451 FlowRewards.getLockPeriodEnd() != nil ? model.getDistributionStart() <= FlowRewards.getLockPeriodEnd()! + FlowRewards.maxIntermissionDuration : true:
1452 "Provided model distribution period starts beyond the maximum intermission threshold="
1453 .concat((FlowRewards.getLockPeriodEnd()! + FlowRewards.maxIntermissionDuration).toString())
1454 model.getDistributionEnd() - model.getDistributionStart() <= FlowRewards.maxPeriodDuration:
1455 "Distribution period exceeds maximum duration="
1456 .concat(FlowRewards.maxPeriodDuration.toString())
1457 }
1458 let oldRef = FlowRewards._borrowRewardDistributionModel()
1459 emit RewardModelSet(
1460 oldType: oldRef?.getType()?.identifier ?? "",
1461 oldStart: oldRef?.getDistributionStart() ?? 0.0,
1462 oldEnd: oldRef?.getDistributionEnd() ?? 0.0,
1463 oldUUID: oldRef?.uuid ?? 0,
1464 newType: model.getType().identifier,
1465 newStart: model.getDistributionStart(),
1466 newEnd: model.getDistributionEnd(),
1467 newUUID: model.uuid,
1468 adminUUID: self.uuid
1469 )
1470
1471 let old <- FlowRewards.distributionModel <- model
1472 destroy old
1473 }
1474
1475 /// Deposits FLOW to the reward pool for distribution
1476 ///
1477 access(Fund) fun fundRewardPool(_ flow: @FlowToken.Vault) {
1478 let rewardPool = &FlowRewards.rewardPool as &FlowToken.Vault
1479 emit RewardPoolFunded(
1480 amount: flow.balance,
1481 toUUID: rewardPool.uuid,
1482 depositedUUID: flow.uuid,
1483 adminUUID: self.uuid
1484 )
1485 rewardPool.deposit(from: <-flow)
1486 }
1487
1488 /// Updates the Renderer implementation for the contract
1489 ///
1490 access(all) fun setRenderer(_ new: {FlowRewardsMetadataViews.Renderer}) {
1491 emit RendererSet(
1492 type: new.getType().identifier,
1493 adminUUID: self.uuid
1494 )
1495 FlowRewards.renderer = new
1496 }
1497
1498 /// Extends the clawback buffer by a given number of seconds
1499 ///
1500 access(all) fun extendClawbackBuffer(delta: UFix64) {
1501 let newClawbackBuffer = FlowRewards.clawbackBuffer + delta
1502 emit ClawbackBufferUpdated(old: FlowRewards.clawbackBuffer, new: newClawbackBuffer, adminUUID: self.uuid)
1503 FlowRewards.clawbackBuffer = newClawbackBuffer
1504 }
1505
1506 /// Withdraws all FLOW from the reward pool
1507 ///
1508 access(all) fun clawbackRewardPool(): @FlowToken.Vault {
1509 pre {
1510 FlowRewards.rewardPool.balance > 0.0: "Reward pool is empty"
1511 FlowRewards.getDistributionPeriodEnd() != nil:
1512 "Distribution period has not been set - cannot clawback rewards until after distribution period"
1513 Clock.time() >= FlowRewards.getClawbackThreshold()!:
1514 "Cannot clawback rewards until buffer=".concat(FlowRewards.clawbackBuffer.toString())
1515 .concat(" has passed beyond distribution period end - clawback threshold=")
1516 .concat((FlowRewards.getClawbackThreshold()!).toString())
1517 }
1518 let balance = FlowRewards.rewardPool.balance
1519 let withdrawn <-(FlowRewards.rewardPool.withdraw(amount: balance) as! @FlowToken.Vault)
1520
1521 emit RewardPoolWithdrawn(
1522 amount: balance,
1523 fromUUID: FlowRewards.rewardPool.uuid,
1524 withdrawnUUID: withdrawn.uuid,
1525 adminUUID: self.uuid
1526 )
1527
1528 return <-withdrawn
1529 }
1530 }
1531
1532 /* ======================
1533 Internal Functions
1534 ====================== */
1535
1536 access(self) view fun _borrowRewardPool(): &FlowToken.Vault {
1537 return &self.rewardPool
1538 }
1539
1540 /// Returns a reference to the RewardSummary from the storing Registry for a given NFT or nil if it does not exist
1541 /// or is not registered
1542 ///
1543 access(account) view fun _borrowSummary(forNFT: UInt64): &RewardSummary? {
1544 if let sequence = self.sequenceRegistry[forNFT] {
1545 if let registry = self._borrowRegistry(forSequence: sequence) {
1546 return registry.borrowSummary(id: forNFT) as! &RewardSummary?
1547 }
1548 }
1549 return nil
1550 }
1551
1552 /// Returns a reference to the current DistributionModel or nil if one is not set
1553 ///
1554 access(account) view fun _borrowRewardDistributionModel(): &{DistributionModel}? {
1555 return &self.distributionModel
1556 }
1557
1558 /// Returns the total FLOW distributable for a given summary, locked or rewarded, at a given time based on the
1559 /// current DistributionModel. Returns nil if the DistributionModel is not set
1560 ///
1561 access(account) fun _getAllowableDistribution(_ summary: &RewardSummary, locked: Bool, atTime: UFix64?): UFix64? {
1562 // Reference the distribution model, returning nil if not set
1563 if let distributionModel = self._borrowRewardDistributionModel() {
1564 // Assess target time
1565 let _atTime = atTime ?? Clock.time()
1566 // Assess clawback threshold
1567 let clawbackThreshold = distributionModel.getDistributionEnd() + self.clawbackBuffer
1568 // Gather amounts claimed
1569 let claimed = locked ? summary.lockedClaimed : summary.rewardsClaimed
1570
1571 if _atTime < distributionModel.getDistributionStart() {
1572 // Before distribution period - no distribution
1573 return 0.0
1574 } else if _atTime >= distributionModel.getDistributionEnd() && _atTime <= clawbackThreshold {
1575 // After distribution period - return remaining locked or rewards less claimed
1576 return (locked ? summary.getLockupTotal() : summary.getBoostRewards()) - claimed
1577 } else if !locked && _atTime > clawbackThreshold {
1578 // Dealing with rewards after clawback threshold - return 0.0
1579 return 0.0
1580 }
1581
1582 // Determined we're within distribution period - calculate distribution, subtracting any claimed
1583 let allowableDistribution = distributionModel.calculateDistribution(
1584 maxDistribution: locked ? summary.getLockupTotal() : summary.getBoostRewards(),
1585 atTime: _atTime
1586 )
1587 return allowableDistribution > claimed ? allowableDistribution - claimed : 0.0
1588 }
1589
1590 return nil
1591 }
1592
1593 /// Claims the distribution of locked FLOW against a given NFT
1594 ///
1595 access(account) fun _claimDistribution(forNFT: &NFT): @FlowToken.Vault? {
1596 pre {
1597 self.getDistributionPeriodStart() != nil: "Distribution period has not been set"
1598 self.isDistributionPeriodActive(): "Distribution period has not started"
1599 }
1600 let now = Clock.time()
1601
1602 // Within the distribution period, calculate eligible distribution
1603 let summary = self._borrowSummary(forNFT: forNFT.id) ?? panic("Could not find summary for NFT")
1604
1605 // Get total & remaining boost rewards
1606 let totalRewards = summary.getBoostRewards()
1607 var remainingRewards = totalRewards - summary.rewardsClaimed
1608
1609 // Get the locked distribution, if any
1610 let distribution <- forNFT.claimLockedFlowCallback() as! @FlowToken.Vault
1611
1612 // All value has been distributed, nothing left to claim - return nil
1613 if summary.rewardsClaimed == totalRewards && distribution.balance == 0.0 {
1614 destroy distribution
1615 return nil
1616 }
1617
1618 // Assess distribution against both locked & rewarded amounts, subtracting any already claimed
1619 var rewardDistribution = self._getAllowableDistribution(summary, locked: false, atTime: now)!
1620
1621 rewardDistribution = rewardDistribution <= remainingRewards ? rewardDistribution : remainingRewards
1622
1623 // Ensure this and historical distributions do not exceed totals
1624 assert(
1625 rewardDistribution <= totalRewards,
1626 message: "Reward distribution=".concat(rewardDistribution.toString())
1627 .concat(" exceeds reward total=").concat(totalRewards.toString())
1628 .concat("for NFT id=").concat(forNFT.id.toString())
1629 )
1630
1631 // Mark the totals as claimed
1632 remainingRewards = summary.markRewardsClaimed(delta: rewardDistribution)
1633
1634 // Withdraw the eligible distribution from reward pool, emit & return
1635 if rewardDistribution > 0.0 {
1636 distribution.deposit(
1637 from: <-self.rewardPool.withdraw(amount: rewardDistribution)
1638 )
1639 // Emit reward distribution here - lock distribution details should be emitted in nft.claimLockedFlowCallback
1640 emit DistributionClaimed(
1641 id: forNFT.id,
1642 amountClaimed: rewardDistribution,
1643 amountRemaining: remainingRewards,
1644 distributedUUID: distribution.uuid,
1645 locked: false,
1646 claimant: forNFT.owner?.address
1647 )
1648 }
1649
1650 return <-distribution
1651 }
1652
1653 /// Renders an SVG image from a stored template renderer, filling in the metadata for a given NFT to the template
1654 ///
1655 access(self) fun _renderSVG(id: UInt64, summary: &RewardSummary): String {
1656 let lockupTotal = summary.getLockupTotal()
1657 let tier = self.getTierName(forAmount: lockupTotal)
1658 let metadata = {
1659 "id": id,
1660 "tierName": tier,
1661 "sequence": summary.sequence,
1662 "totalLocked": lockupTotal,
1663 "totalRewards": summary.getFinalBoostRewardTotal(),
1664 "lockedClaimed": summary.lockedClaimed,
1665 "rewardsClaimed": summary.rewardsClaimed
1666 }
1667
1668 return self._borrowRenderer().render(metadata: metadata)
1669 }
1670
1671 /// Initializes a lockup under a new NFT, deposits to the provided Collection and returns the minted & registered
1672 /// NFT ID
1673 ///
1674 access(self) fun _initLock(
1675 _ flow: @FlowToken.Vault,
1676 initiator: &Collection
1677 ): UInt64 {
1678 pre {
1679 self.isLockPeriodActive(): "Lock period is not currently active"
1680 initiator.owner != nil: "Collection owner is nil - must have an owner to execute lock"
1681 }
1682 // Mint & deposit NFT to initiator's collection
1683 let nft <- self._mintNFT(initiator: initiator.owner!.address)
1684 let mintedID = nft.id
1685 initiator.deposit(token: <-nft)
1686
1687 // Reference the newly minted NFT and lock FLOW
1688 let nftRef = initiator.borrowNFT(mintedID) ?? panic("NFT was not deposited to Collection")
1689 self._lock(<-flow, nft: nftRef as! &NFT)
1690
1691 return mintedID
1692 }
1693
1694 /// Mints a new NFT and registers it in the current registry. Configures a new Registry if one is needed for the
1695 /// new NFT
1696 ///
1697 access(self) fun _mintNFT(initiator: Address): @NFT {
1698 let nft <- create NFT()
1699 self._register(nft: &nft as &NFT)
1700
1701 emit Minted(id: nft.id, sequence: nft.sequence, lockedVaultUUID: nft.getLockedVaultUUID(), initiator: initiator)
1702
1703 return <-nft
1704 }
1705
1706 /// Locks flow under a given NFT
1707 ///
1708 access(self) fun _lock(
1709 _ flow: @FlowToken.Vault,
1710 nft: &NFT
1711 ) {
1712 pre {
1713 self.isLockPeriodActive(): "Lock period is not currently active"
1714 flow.balance >= self.minLockupAmount:
1715 "Lockup is less than the minimum=".concat(self.minLockupAmount.toString())
1716 }
1717 nft.depositLockedFlowCallback(<-flow)
1718 }
1719
1720 /// Registers a new NFT in the Registry appropriate for the NFT's sequence number
1721 ///
1722 access(self) fun _register(nft: &NFT) {
1723 pre {
1724 self.sequenceRegistry[nft.id] == nil: "NFT already registered"
1725 }
1726 let registry = self._borrowRegistry(forSequence: nft.sequence)
1727 ?? panic("Could not find registry for NFT id=".concat(nft.id.toString()))
1728 // Associate the NFT ID with the sequence number & register the NFT with a new summary
1729 self.sequenceRegistry[nft.id] = nft.sequence
1730 registry.register(id: nft.id, summary: RewardSummary(nft.sequence))
1731 }
1732
1733 /// Configures a Registry if needed, ensuring that a registry exists at the current registry path
1734 ///
1735 access(self) fun _safeConfigureRegistry() {
1736 let currentRegistryPath = self._getPathForCurrentRegistry()
1737 if self.account.storage.type(at: currentRegistryPath) == nil {
1738 self.account.storage.save(<-FlowRewardsRegistry.createRegistry(), to: currentRegistryPath)
1739 }
1740 }
1741
1742 /// Returns the storage path of the current Registry
1743 ///
1744 access(self) view fun _getPathForCurrentRegistry(): StoragePath {
1745 return self.deriveRegistryStoragePath(forSequence: self.totalSupply)
1746 }
1747
1748 /// Borrows a reference to the Registry for a given sequence number
1749 ///
1750 access(self) view fun _borrowRegistry(forSequence: UInt64): &FlowRewardsRegistry.Registry? {
1751 return self.account.storage.borrow<&FlowRewardsRegistry.Registry>(
1752 from: self.deriveRegistryStoragePath(forSequence: forSequence)
1753 )
1754 }
1755
1756 /// Returns a reference to the current BoostModel or nil if one is not set
1757 ///
1758 access(self) view fun _borrowRewardBoostModel(): &{BoostModel}? {
1759 return &self.rewardBoostModel as &{BoostModel}?
1760 }
1761
1762 /// Returns a reference to the ValetManager or nil if one is not in storage
1763 ///
1764 access(self) view fun _borrowValetManager(): &ValetManager? {
1765 return self.account.storage.borrow<&ValetManager>(from: self.ValetManagerStoragePath)
1766 }
1767
1768 access(self) fun _borrowRenderer(): &{FlowRewardsMetadataViews.Renderer} {
1769 return &self.renderer as &{FlowRewardsMetadataViews.Renderer}?
1770 ?? panic("Contract Renderer has not been set")
1771 }
1772
1773 init() {
1774 self.totalSupply = 0
1775 self.totalValueLocked = 0.0
1776 self.sequenceRegistry = {}
1777 self.minLockupAmount = 0.0
1778 self.balancerInterval = 1000
1779 self.modelSwapBuffer = 60.0 * 60.0 * 24.0 // 24 hours
1780 self.clawbackBuffer = 0.0
1781
1782 self.tiers = [2_000.0, 25_000.0, 100_000.0, UFix64.max]
1783 self.tierNames = ["Member", "Supporter", "Patron", "OG"]
1784
1785 self.maxPeriodDuration = 60.0 * 60.0 * 24.0 * 90.0 // 90 days
1786 self.maxIntermissionDuration = 60.0 * 60.0 * 24.0 * 7.0 // 7 days
1787
1788 self.rewardPool <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>())
1789
1790 // Admin should set these after contract init
1791 self.renderer = nil
1792 self.rewardBoostModel <- nil
1793 self.distributionModel <- nil
1794
1795 self.CollectionStoragePath = /storage/flowRewardsCollection
1796 self.CollectionPublicPath = /public/flowRewardsCollectionPublic
1797 self.CollectionPrivatePath = /private/flowRewardsCollectionProvider
1798 self.ValetManagerStoragePath = /storage/flowRewardsValetManager
1799 self.ValetManagerPublicPath = /public/flowRewardsValetManager
1800 self.AdminStoragePath = /storage/flowRewardsAdmin
1801
1802 self.account.storage.save(<-create ValetManager(), to: self.ValetManagerStoragePath)
1803 self.account.storage.save(<-create Admin(), to: self.AdminStoragePath)
1804
1805 // Set the minimum lockup amount to 1.0 via the Admin resource so event is emitted
1806 let admin = self.account.storage.borrow<auth(Configure) &Admin>(from: self.AdminStoragePath)!
1807 admin.setMinimumLockupAmount(1.0)
1808
1809 // Set the clawback buffer to 30 days beyond the distribution period end
1810 admin.extendClawbackBuffer(delta: 60.0 * 60.0 * 24.0 * 30.0) // 30 days
1811 }
1812}
1813