Smart Contract

FlowRewards

A.a45ead1cf1ca9eda.FlowRewards

Valid From

86,859,113

Deployed

3d ago
Feb 24, 2026, 11:41:03 PM UTC

Dependents

154 imports
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