referrerpolicy=no-referrer-when-downgrade

pallet_broker/
adapt_price.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18#![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/// Performance of a past sale.
26#[derive(Copy, Clone)]
27pub struct SalePerformance<Balance> {
28	/// The price at which the last core was sold.
29	///
30	/// Will be `None` if no cores have been offered.
31	pub sellout_price: Option<Balance>,
32
33	/// The minimum price that was achieved in this sale.
34	pub end_price: Balance,
35
36	/// The number of cores we want to sell, ideally.
37	pub ideal_cores_sold: CoreIndex,
38
39	/// Number of cores which are/have been offered for sale.
40	pub cores_offered: CoreIndex,
41
42	/// Number of cores which have been sold; never more than cores_offered.
43	pub cores_sold: CoreIndex,
44}
45
46/// Result of `AdaptPrice::adapt_price`.
47#[derive(Copy, Clone, RuntimeDebug, Eq, PartialEq)]
48pub struct AdaptedPrices<Balance> {
49	/// New minimum price to use.
50	pub end_price: Balance,
51
52	/// Price the controller is optimizing for.
53	///
54	/// This is the price "expected" by the controller based on the previous sale. We assume that
55	/// sales in this period will be around this price, assuming stable market conditions.
56	///
57	/// Think of it as the expected market price. This can be used for determining what to charge
58	/// for renewals, that don't yet have any price information for example. E.g. for expired
59	/// legacy leases.
60	pub target_price: Balance,
61}
62
63impl<Balance: Copy> SalePerformance<Balance> {
64	/// Construct performance via data from a `SaleInfoRecord`.
65	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
81/// Type for determining how to set price.
82pub trait AdaptPrice<Balance> {
83	/// Return the factor by which the regular price must be multiplied during the leadin period.
84	///
85	/// - `when`: The amount through the leadin period; between zero and one.
86	fn leadin_factor_at(when: FixedU64) -> FixedU64;
87
88	/// Return adapted prices for next sale.
89	///
90	/// Based on the previous sale's performance.
91	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
104/// Simple implementation of `AdaptPrice` with two linear phases.
105///
106/// One steep one downwards to the target price, which is 1/10 of the maximum price and a more flat
107/// one down to the minimum price, which is 1/100 of the maximum price.
108pub 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			// We could not recover from a price equal 0 ever.
130			sellout_price
131		} else {
132			price
133		};
134
135		AdaptedPrices { end_price: price, target_price: sellout_price }
136	}
137}
138
139/// `AdaptPrice` like `CenterTargetPrice`, but with a minimum price.
140///
141/// This price adapter behaves exactly like `CenterTargetPrice`, except that it takes a minimum
142/// price and makes sure that the returned `end_price` is never lower than that.
143///
144/// Target price will also get adjusted if necessary (it will never be less than the end_price).
145pub 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		// Fix target price if necessary:
161		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		// Check price stays stable if sold at the optimal price:
217		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		// Check price stays stable if sold at the optimal price:
249		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}