Smart Contract
FlowRewardsModels
A.a45ead1cf1ca9eda.FlowRewardsModels
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