Smart Contract

FlowRewardsModels

A.a45ead1cf1ca9eda.FlowRewardsModels

Valid From

85,389,711

Deployed

2d ago
Feb 25, 2026, 07:03:31 PM UTC

Dependents

3 imports
1import FlowRewards from 0xa45ead1cf1ca9eda
2import FlowRewardsRegistry from 0xa45ead1cf1ca9eda
3import Clock from 0xa45ead1cf1ca9eda
4
5/// This contract defines reward boost and distribution models used in the FlowRewards contract
6///
7access(all) contract FlowRewardsModels {
8
9    /// Creates a new MultilinearBoostModel with the provided stages
10    ///
11    /// @param stages: The stages defining the boost and time boundaries for boost. These should be contiguous and
12    ///    ordered by start time - a condition enforced in resource init
13    ///
14    /// @return The newly created MultilinearBoostModel
15    ///
16    access(all) fun createBoostModel(stages: [Stage]): @{FlowRewards.BoostModel} {
17        return <- create MultilinearBoostModel(stages: stages)
18    }
19
20    access(all) fun createDistributionModel(start: UFix64, end: UFix64): @{FlowRewards.DistributionModel} {
21        return <- create LinearDistributionModel(start: start, end: end)
22    }
23
24    /// Each stage defines a start and end time and a boost applicable for that period
25    ///
26    access(all) struct Stage {
27        /// The reward boost factor for this stage
28        access(all) let boost: UFix64
29        /// The start timestamp boundary for this stage
30        access(all) let start: UFix64
31        /// The end timestamp boundary for this stage
32        access(all) let end: UFix64
33
34        init(boost: UFix64, start: UFix64, end: UFix64) {
35            pre {
36                start < end: "Start must be before end"
37            }
38            self.boost = boost
39            self.start = start
40            self.end = end
41        }
42
43        /// Calculates the boost amount for a given lock amount amount over this stage
44        ///
45        /// @param lockAmount: The lock amount amount to calculate boost amount against
46        /// @param start: The start time which the lock amount should begin boosting. If this exceeds the stage end, no
47        ///     boost amount will be calculated
48        /// @param upTo: The end time up to which the lock amount should boost. If this precedes the stage start, no
49        ///    boost amount will be calculated. If this exceeds the stage end, boost will be calculated up to the stage
50        //      end
51        ///
52        /// @return The boost amount for the lock amount over this stage
53        ///
54        access(all) view fun calculateBoostAmount(lockAmount: UFix64, start: UFix64, upTo: UFix64): UFix64 {
55            // Return early if defined bounds do not overlap with this stage
56            if upTo <= self.start || self.end <= start {
57                return 0.0
58            }
59
60            let boostStart = start < self.start ? self.start : start
61            let boostEnd = upTo < self.end ? upTo : self.end
62            if boostEnd <= boostStart {
63                return 0.0
64            }
65            let time = boostEnd - boostStart
66
67            let boostAmount = lockAmount * self.boost
68            let proportionalTime = time / 31_536_000.0
69            return boostAmount * proportionalTime
70        }
71    }
72
73    /// The MultilinearBoostModel defines a series of stages, each with fixed boosts and calculates rewards over those
74    /// stages
75    ///
76    access(all) resource MultilinearBoostModel : FlowRewards.BoostModel {
77        /// The stages defining the long-term boost and time boundaries for boost calculation
78        access(all) let stages: [Stage]
79
80        init(stages: [Stage]) {
81            pre {
82                stages.length > 0: "Must have at least one stage"
83            }
84            for i, stage in stages {
85                if i < stages.length - 1 {
86                    assert(stage.end == stages[i + 1].start, message: "Stages must be contiguous")
87                }
88            }
89            self.stages = stages
90        }
91
92        /// Returns the start time of the first stage
93        ///
94        /// @return The start timestamp of the first stage
95        ///
96        access(all) view fun getBoostStart(): UFix64 {
97            return self.stages[0].start
98        }
99
100        /// Returns the end time of the last stage
101        ///
102        /// @return The end timestamp of the last stage
103        ///
104        access(all) view fun getBoostEnd(): UFix64 {
105            return self.stages[self.stages.length - 1].end
106        }
107
108        /// Returns the boost factor for the model at the start of the boost period
109        ///
110        /// @return The starting boost factor for the model
111        ///
112        access(all) view fun getStartingBoostFactor(): UFix64 {
113            return self.getBoostFactor(atTime: self.getBoostStart())
114        }
115
116        /// Calculates the boost factor for a lockup executed at the specified time
117        ///
118        /// @param start: The time at which the lockup was executed
119        ///
120        /// @return The boost factor for the lockup executed at the specified time
121        ///
122        access(all) view fun getBoostFactor(atTime: UFix64?): UFix64 {
123            let atTime = atTime ?? Clock.time()
124            let modelEnd = self.getBoostEnd()
125            // Cannot boost after model end, return 0.0
126            if modelEnd <= atTime {
127                return 0.0
128            }
129
130            let modelStart = self.getBoostStart()
131            // If the lockup occurred before the model start, boost starts at the model start
132            let boostStart = atTime <= modelStart ? modelStart : atTime
133            let amount = 1_000.0
134            var boostAmount = 0.0
135
136            boostAmount = self._calculateBoostAmount(amount: amount, start: boostStart, upTo: modelEnd)
137
138
139            // Determine the boost factor relative to the lock amount of 1_000.0
140            return boostAmount / amount
141        }
142
143        /// Calculates the boost amount for a given summary up to a specified time
144        ///
145        /// @param summary: The reward summary to calculate boost amount for
146        /// @param upTo: The end time up to which to calculate boost. If nil, the current block timestamp is used. If
147        ///    the specified time is before the model start, no boost amount will be calculated. If the specified time
148        ///    is beyond the model end, the boost will be calculated up to the model end
149        ///
150        /// @return The total boost amount for the summary up to the specified time
151        ///
152        access(all) view fun calculateBoostAmount(
153            summary: &{FlowRewardsRegistry.Summary},
154            upTo: UFix64?
155        ): UFix64 {
156            let modelStart = self.getBoostStart()
157            let modelEnd = self.getBoostEnd()
158
159            var boostEnd = upTo ?? Clock.time()
160            // Return early if boost period has not started as specified
161            if boostEnd <= modelStart {
162                return 0.0
163            }
164            // Bound the boost end to the model end if requested threshold is beyond the model end
165            boostEnd = modelEnd < boostEnd ? modelEnd : boostEnd
166
167            var total = 0.0
168            for lockup in summary.lockups {
169                let whenLocked = lockup.timestamp
170                // If the lockup occurred after the model end, skip it
171                if modelEnd <= whenLocked { continue }
172
173                // If the lockup occurred before the model start, boost starts at the model start
174                let boostStart = modelStart >= whenLocked ? modelStart : whenLocked
175
176                total = total + self._calculateBoostAmount(amount: lockup.amount,start: boostStart, upTo: boostEnd)
177            }
178
179            return total
180        }
181
182        /// Calculates the boost for a given lock amount amount up to a specified time iterating over all stages
183        ///
184        access(self) view fun _calculateBoostAmount(amount: UFix64, start: UFix64, upTo: UFix64): UFix64 {
185            var total = 0.0
186            // Iterate over stages to calculate boost amount over each stage's boost
187            for i, stage in self.stages {
188                // Add the boost amount for this stage to the total
189                total = total + stage.calculateBoostAmount(
190                    lockAmount: amount,
191                    start: start,
192                    upTo: upTo
193                )
194                if upTo <= stage.end {
195                    break
196                }
197            }
198            return total
199        }
200    }
201
202    /// This resource defines a linear distribution model of locked + rewarded FLOW over a specified time period
203    ///
204    access(all) resource LinearDistributionModel : FlowRewards.DistributionModel {
205        /// The start time of the distribution period
206        access(all) let start: UFix64
207        /// The end time of the distribution period
208        access(all) let end: UFix64
209
210        init(start: UFix64, end: UFix64) {
211            pre {
212                start < end: "Start must be before end"
213            }
214            self.start = start
215            self.end = end
216        }
217
218        /// Returns the time at which the distribtuion model starts distributing funds
219        ///
220        /// @return The start time of the distribution period
221        ///
222        access(all) view fun getDistributionStart(): UFix64 {
223            return self.start
224        }
225
226        /// Returns the time at which the distribtuion model stops distributing view funds
227        ///
228        /// @return The end time of the distribution period
229        ///
230        access(all) view fun getDistributionEnd(): UFix64 {
231            return self.end
232        }
233
234        /// Returns the amount that can be distributed at a given time according to the distribution period defined in
235        /// this model at a linear rate
236        /// NOTE: Any implementing contracts should be defined in this contract account as the summary is passed by
237        /// reference
238        ///
239        /// @param maxDistribution: The maximum amount of the same denomination that can be distributed
240        /// @param atTime: The time at which to calculate the distribution. If nil, the current block timestamp is used
241        ///     If the specified time is before the model start, no distribution will be calculated. If the specified
242        ///     time is beyond the model end, the distribution will be calculated up to the model end
243        ///
244        /// @return The total amount of locked and/or rewarded FLOW that can be distributed at the specified time
245        ///
246        access(all) view fun calculateDistribution(
247            maxDistribution: UFix64,
248            atTime: UFix64?
249        ): UFix64 {
250            let distributionStart = self.getDistributionStart()
251            let distributionEnd = self.getDistributionEnd()
252
253            var distributionTime = atTime ?? Clock.time()
254            // Return early if distribution period has not started as specified
255            if distributionTime < distributionStart {
256                return 0.0
257            } else if distributionTime >= distributionEnd {
258                // If the distribution period has ended, return the max distribution
259                return maxDistribution
260            }
261
262            // Calculate how far into the distribution period we are as a percentage at distributionTime
263            let distributionPeriod = distributionEnd - distributionStart
264            let timeElapsed = distributionTime - distributionStart
265            let distributionPercentage = timeElapsed / distributionPeriod
266
267            // Return the proportion of maxDistribution that can be distributed at this time
268            return maxDistribution * distributionPercentage
269        }
270    }
271}
272