1#![deny(missing_docs)]
19
20use crate::{CoreIndex, SaleInfoRecord};
21use sp_arithmetic::{traits::One, FixedU64};
22use sp_core::{Get, RuntimeDebug};
23use sp_runtime::{FixedPointNumber, FixedPointOperand, Saturating};
24
25#[derive(Copy, Clone)]
27pub struct SalePerformance<Balance> {
28 pub sellout_price: Option<Balance>,
32
33 pub end_price: Balance,
35
36 pub ideal_cores_sold: CoreIndex,
38
39 pub cores_offered: CoreIndex,
41
42 pub cores_sold: CoreIndex,
44}
45
46#[derive(Copy, Clone, RuntimeDebug, Eq, PartialEq)]
48pub struct AdaptedPrices<Balance> {
49 pub end_price: Balance,
51
52 pub target_price: Balance,
61}
62
63impl<Balance: Copy> SalePerformance<Balance> {
64 pub fn from_sale<BlockNumber>(record: &SaleInfoRecord<Balance, BlockNumber>) -> Self {
66 Self {
67 sellout_price: record.sellout_price,
68 end_price: record.end_price,
69 ideal_cores_sold: record.ideal_cores_sold,
70 cores_offered: record.cores_offered,
71 cores_sold: record.cores_sold,
72 }
73 }
74
75 #[cfg(test)]
76 fn new(sellout_price: Option<Balance>, end_price: Balance) -> Self {
77 Self { sellout_price, end_price, ideal_cores_sold: 0, cores_offered: 0, cores_sold: 0 }
78 }
79}
80
81pub trait AdaptPrice<Balance> {
83 fn leadin_factor_at(when: FixedU64) -> FixedU64;
87
88 fn adapt_price(performance: SalePerformance<Balance>) -> AdaptedPrices<Balance>;
92}
93
94impl<Balance: Copy> AdaptPrice<Balance> for () {
95 fn leadin_factor_at(_: FixedU64) -> FixedU64 {
96 FixedU64::one()
97 }
98 fn adapt_price(performance: SalePerformance<Balance>) -> AdaptedPrices<Balance> {
99 let price = performance.sellout_price.unwrap_or(performance.end_price);
100 AdaptedPrices { end_price: price, target_price: price }
101 }
102}
103
104pub struct CenterTargetPrice<Balance>(core::marker::PhantomData<Balance>);
109
110impl<Balance: FixedPointOperand> AdaptPrice<Balance> for CenterTargetPrice<Balance> {
111 fn leadin_factor_at(when: FixedU64) -> FixedU64 {
112 if when <= FixedU64::from_rational(1, 2) {
113 FixedU64::from(100).saturating_sub(when.saturating_mul(180.into()))
114 } else {
115 FixedU64::from(19).saturating_sub(when.saturating_mul(18.into()))
116 }
117 }
118
119 fn adapt_price(performance: SalePerformance<Balance>) -> AdaptedPrices<Balance> {
120 let Some(sellout_price) = performance.sellout_price else {
121 return AdaptedPrices {
122 end_price: performance.end_price,
123 target_price: FixedU64::from(10).saturating_mul_int(performance.end_price),
124 }
125 };
126
127 let price = FixedU64::from_rational(1, 10).saturating_mul_int(sellout_price);
128 let price = if price == Balance::zero() {
129 sellout_price
131 } else {
132 price
133 };
134
135 AdaptedPrices { end_price: price, target_price: sellout_price }
136 }
137}
138
139pub struct MinimumPrice<Balance, MinPrice>(core::marker::PhantomData<(Balance, MinPrice)>);
146
147impl<Balance: FixedPointOperand, MinPrice: Get<Balance>> AdaptPrice<Balance>
148 for MinimumPrice<Balance, MinPrice>
149{
150 fn leadin_factor_at(when: FixedU64) -> FixedU64 {
151 CenterTargetPrice::<Balance>::leadin_factor_at(when)
152 }
153
154 fn adapt_price(performance: SalePerformance<Balance>) -> AdaptedPrices<Balance> {
155 let mut proposal = CenterTargetPrice::<Balance>::adapt_price(performance);
156 let min_price = MinPrice::get();
157 if proposal.end_price < min_price {
158 proposal.end_price = min_price;
159 }
160 if proposal.target_price < proposal.end_price {
162 proposal.target_price = proposal.end_price;
163 }
164 proposal
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use sp_core::ConstU64;
171
172 use super::*;
173
174 #[test]
175 fn linear_no_panic() {
176 for sellout in 0..11 {
177 for price in 0..10 {
178 let sellout_price = if sellout == 11 { None } else { Some(sellout) };
179 CenterTargetPrice::adapt_price(SalePerformance::new(sellout_price, price));
180 }
181 }
182 }
183
184 #[test]
185 fn leadin_price_bound_check() {
186 assert_eq!(
187 CenterTargetPrice::<u64>::leadin_factor_at(FixedU64::from(0)),
188 FixedU64::from(100)
189 );
190 assert_eq!(
191 CenterTargetPrice::<u64>::leadin_factor_at(FixedU64::from_rational(1, 4)),
192 FixedU64::from(55)
193 );
194
195 assert_eq!(
196 CenterTargetPrice::<u64>::leadin_factor_at(FixedU64::from_float(0.5)),
197 FixedU64::from(10)
198 );
199
200 assert_eq!(
201 CenterTargetPrice::<u64>::leadin_factor_at(FixedU64::from_rational(3, 4)),
202 FixedU64::from_float(5.5)
203 );
204 assert_eq!(CenterTargetPrice::<u64>::leadin_factor_at(FixedU64::one()), FixedU64::one());
205 }
206
207 #[test]
208 fn no_op_sale_is_good() {
209 let prices = CenterTargetPrice::adapt_price(SalePerformance::new(None, 1));
210 assert_eq!(prices.target_price, 10);
211 assert_eq!(prices.end_price, 1);
212 }
213
214 #[test]
215 fn price_stays_stable_on_optimal_sale() {
216 let mut performance = SalePerformance::new(Some(1000), 100);
218 for _ in 0..10 {
219 let prices = CenterTargetPrice::adapt_price(performance);
220 performance.sellout_price = Some(1000);
221 performance.end_price = prices.end_price;
222
223 assert!(prices.end_price <= 101);
224 assert!(prices.end_price >= 99);
225 assert!(prices.target_price <= 1001);
226 assert!(prices.target_price >= 999);
227 }
228 }
229
230 #[test]
231 fn price_adjusts_correctly_upwards() {
232 let performance = SalePerformance::new(Some(10_000), 100);
233 let prices = CenterTargetPrice::adapt_price(performance);
234 assert_eq!(prices.target_price, 10_000);
235 assert_eq!(prices.end_price, 1000);
236 }
237
238 #[test]
239 fn price_adjusts_correctly_downwards() {
240 let performance = SalePerformance::new(Some(100), 100);
241 let prices = CenterTargetPrice::adapt_price(performance);
242 assert_eq!(prices.target_price, 100);
243 assert_eq!(prices.end_price, 10);
244 }
245
246 #[test]
247 fn price_never_goes_to_zero_and_recovers() {
248 let sellout_price = 1;
250 let mut performance = SalePerformance::new(Some(sellout_price), 1);
251 for _ in 0..11 {
252 let prices = CenterTargetPrice::adapt_price(performance);
253 performance.sellout_price = Some(sellout_price);
254 performance.end_price = prices.end_price;
255
256 assert!(prices.end_price <= sellout_price);
257 assert!(prices.end_price > 0);
258 }
259 }
260
261 #[test]
262 fn renewal_price_is_correct_on_no_sale() {
263 let performance = SalePerformance::new(None, 100);
264 let prices = CenterTargetPrice::adapt_price(performance);
265 assert_eq!(prices.target_price, 1000);
266 assert_eq!(prices.end_price, 100);
267 }
268
269 #[test]
270 fn renewal_price_is_sell_out() {
271 let performance = SalePerformance::new(Some(1000), 100);
272 let prices = CenterTargetPrice::adapt_price(performance);
273 assert_eq!(prices.target_price, 1000);
274 }
275
276 #[test]
277 fn minimum_price_works() {
278 let performance = SalePerformance::new(Some(10), 10);
279 let prices = MinimumPrice::<u64, ConstU64<10>>::adapt_price(performance);
280 assert_eq!(prices.end_price, 10);
281 assert_eq!(prices.target_price, 10);
282 }
283
284 #[test]
285 fn minimum_price_does_not_affect_valid_target_price() {
286 let performance = SalePerformance::new(Some(12), 10);
287 let prices = MinimumPrice::<u64, ConstU64<10>>::adapt_price(performance);
288 assert_eq!(prices.end_price, 10);
289 assert_eq!(prices.target_price, 12);
290 }
291
292 #[test]
293 fn no_minimum_price_works_as_center_target_price() {
294 let performances = [
295 (Some(100), 10),
296 (None, 20),
297 (Some(1000), 10),
298 (Some(10), 10),
299 (Some(1), 1),
300 (Some(0), 10),
301 ];
302 for (sellout, end) in performances {
303 let performance = SalePerformance::new(sellout, end);
304 let prices_minimum = MinimumPrice::<u64, ConstU64<0>>::adapt_price(performance);
305 let prices = CenterTargetPrice::adapt_price(performance);
306 assert_eq!(prices, prices_minimum);
307 }
308 }
309}