timechain_runtime/configs/
tokenomics.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
//! Tokenomics configurations.
use smallvec::smallvec;

use polkadot_sdk::*;

use frame_support::weights::{
	constants::WEIGHT_REF_TIME_PER_SECOND, WeightToFeeCoefficient, WeightToFeeCoefficients,
	WeightToFeePolynomial,
};

use frame_support::{
	parameter_types,
	traits::{ConstU32, WithdrawReasons},
};
#[cfg(feature = "testnet")]
use frame_support::{
	traits::tokens::fungible::Balanced,
	traits::{Imbalance, OnUnbalanced},
	PalletId,
};

#[cfg(feature = "testnet")]
use sp_runtime::traits::AccountIdConversion;
use sp_runtime::{
	traits::{Bounded, ConvertInto},
	FixedPointNumber, Perbill, Perquintill,
};

pub use pallet_transaction_payment::{FungibleAdapter, Multiplier, TargetedFeeAdjustment};

// Local module imports
use crate::{
	weights, Balance, Balances, ExtrinsicBaseWeight, Runtime, RuntimeEvent, RuntimeFreezeReason,
	RuntimeHoldReason, System, ANLOG, MAX_BLOCK_LENGTH,
};
#[cfg(feature = "testnet")]
use crate::{Authorship, RuntimeCredit};
#[cfg(feature = "testnet")]
use time_primitives::AccountId;
use time_primitives::{MICROANLOG, MILLIANLOG};

/// Handles converting a weight scalar to a fee value, based on the scale and granularity of the
/// node's balance type.
///
/// This should typically create a mapping between the following ranges:
///   - [0, `frame_system::MaximumBlockWeight`]
///   - [Balance::min, Balance::max]
///
/// Yet, it can be used for any other sort of change to weight-fee. Some examples being:
///   - Setting it to `0` will essentially disable the weight fee.
///   - Setting it to `1` will cause the literal `#[weight = x]` values to be charged.
pub struct WeightToFee;

pub const MIN_LINEAR_WEIGHT_FEE: Balance = MILLIANLOG;
pub const MAX_QUADRATIC_WEIGHT_FEE: Balance = 90_000 * ANLOG;

pub const MAXIMUM_BLOCK_WEIGHT_SECONDS: u64 = 2;

/// By introducing a second-degree term, the fee will grow faster as the weight increases.
/// This can be useful for discouraging transactions that consume excessive resources.
/// While a first-degree polynomial gives a linear fee increase, adding a quadratic term
/// will make fees grow non-linearly, meaning larger weights result in disproportionately larger fees.
/// This change can help manage network congestion by making resource-heavy operations more expensive.
impl WeightToFeePolynomial for WeightToFee {
	type Balance = Balance;
	fn polynomial() -> WeightToFeeCoefficients<Self::Balance> {
		// Quadratic term maps max weight to max quadratic fee
		let q_2: Balance = MAX_QUADRATIC_WEIGHT_FEE * MAX_QUADRATIC_WEIGHT_FEE;
		let p_2 = WEIGHT_REF_TIME_PER_SECOND.saturating_mul(MAXIMUM_BLOCK_WEIGHT_SECONDS) as u128;
		// Linear term map linear minimum to base weight
		let p_1 = MIN_LINEAR_WEIGHT_FEE;
		let q_1 = Balance::from(ExtrinsicBaseWeight::get().ref_time());
		smallvec![
			WeightToFeeCoefficient {
				degree: 2,
				negative: false,
				coeff_frac: Perbill::from_rational(p_2 % q_2, q_2),
				coeff_integer: p_2 / q_2,
			},
			WeightToFeeCoefficient {
				degree: 1,
				negative: false,
				coeff_frac: Perbill::from_rational(p_1 % q_1, q_1),
				coeff_integer: p_1 / q_1,
			}
		]
	}
}

pub const MIN_LINEAR_LENGTH_FEE: Balance = MICROANLOG;
pub const MAX_QUADRATIC_LENGTH_FEE: Balance = 10_000 * ANLOG;

pub struct LengthToFee;
impl WeightToFeePolynomial for LengthToFee {
	type Balance = Balance;
	fn polynomial() -> WeightToFeeCoefficients<Self::Balance> {
		// Quadratic term maps max weight to max quadratic fee
		let q_2 = MAX_QUADRATIC_LENGTH_FEE * MAX_QUADRATIC_WEIGHT_FEE;
		let p_2 = MAX_BLOCK_LENGTH as u128;
		// Linear minimum is mapped to size of smallest transaction
		let p_1 = MIN_LINEAR_LENGTH_FEE;
		let q_1 = 2;
		smallvec![
			WeightToFeeCoefficient {
				degree: 2,
				negative: false,
				coeff_frac: Perbill::from_rational(p_2 % q_2, q_2),
				coeff_integer: p_2 / q_2,
			},
			WeightToFeeCoefficient {
				degree: 1,
				negative: false,
				coeff_frac: Perbill::from_rational(p_1 % q_1, q_1),
				coeff_integer: p_1 / q_1,
			}
		]
	}
}

#[cfg(not(feature = "runtime-benchmarks"))]
parameter_types! {
	/// Minimum allowed account balance under which account will be reaped
	pub const ExistentialDeposit: Balance = 1 * ANLOG;
}

#[cfg(feature = "runtime-benchmarks")]
parameter_types! {
	// Use more u32 friendly value for benchmark runtime and AtLeast32Bit
	pub const ExistentialDeposit: Balance = 500;
}

/// Virtual treasury wallet
pub struct Treasury;
#[cfg(feature = "testnet")]
impl Treasury {
	/// Return internal virtual wallet id
	fn account_id() -> AccountId {
		PalletId(*b"timetrsy").into_account_truncating()
	}
}

/// Unbalance handler to provide rewards to treasury wallet
#[cfg(feature = "testnet")]
impl OnUnbalanced<RuntimeCredit> for Treasury {
	fn on_nonzero_unbalanced(amount: RuntimeCredit) {
		// Requires treasury account to exist, otherwise will burn rewards.
		let _ = Balances::resolve(&Self::account_id(), amount);
	}
}

/// Unbalance handler to provide rewards to block authors
pub struct Author;
#[cfg(feature = "testnet")]
impl OnUnbalanced<RuntimeCredit> for Author {
	fn on_nonzero_unbalanced(amount: RuntimeCredit) {
		if let Some(author) = Authorship::author() {
			// Failure ignored, as block authors account should exist.
			let _ = Balances::resolve(&author, amount);
		}
	}
}

pub struct DealWithFees;
#[cfg(feature = "testnet")]
impl OnUnbalanced<RuntimeCredit> for DealWithFees {
	fn on_unbalanceds(mut fees_then_tips: impl Iterator<Item = RuntimeCredit>) {
		if let Some(fees) = fees_then_tips.next() {
			// for fees, 80% to treasury, 20% to author
			let mut split = fees.ration(80, 20);
			if let Some(tips) = fees_then_tips.next() {
				// for tips, if any, 80% to treasury, 20% to author (though this can be anything)
				tips.ration_merge_into(80, 20, &mut split);
			}
			Treasury::on_unbalanced(split.0);
			Author::on_unbalanced(split.1);
		}
	}
}

parameter_types! {
	// For weight estimation, we assume that the most locks on an individual account will be 50.
	// This number may need to be adjusted in the future if this assumption no longer holds true.
	pub const MaxLocks: u32 = 50;
	pub const MaxReserves: u32 = 50;
}

/// ## <a id="config.Balances">[`Balances`] Config</a>
///
/// Add balance tracking and transfers
impl pallet_balances::Config for Runtime {
	type RuntimeHoldReason = RuntimeHoldReason;
	type RuntimeFreezeReason = RuntimeFreezeReason;
	type MaxLocks = MaxLocks;
	type MaxReserves = MaxReserves;
	type ReserveIdentifier = [u8; 8];
	type Balance = Balance;
	type DustRemoval = ();
	type RuntimeEvent = RuntimeEvent;
	type ExistentialDeposit = ExistentialDeposit;
	type AccountStore = frame_system::Pallet<Runtime>;
	type WeightInfo = weights::pallet_balances::WeightInfo<Runtime>;
	type FreezeIdentifier = RuntimeFreezeReason;
	type MaxFreezes = ConstU32<1>;
	type DoneSlashHandler = ();
}

parameter_types! {
	/// Multiplier for operational fees, set to 5.
	pub const OperationalFeeMultiplier: u8 = 5;

	/// The target block fullness level, set to 25%.
	/// This determines the block saturation level, and fees will adjust based on this value.
	pub const TargetBlockFullness: Perquintill = Perquintill::from_percent(25);

	/// Adjustment variable for fee calculation, set to 1/100,000.
	/// This value influences how rapidly the fee multiplier changes.
	pub AdjustmentVariable: Multiplier = Multiplier::saturating_from_rational(1, 100_000);

	/// Minimum fee multiplier, set to 1/1,000,000,000.
	/// This represents the smallest possible fee multiplier to prevent fees from dropping too low.
	pub MinimumMultiplier: Multiplier = Multiplier::saturating_from_rational(1, 1_000_000_000u128);

	/// Maximum fee multiplier, set to the maximum possible value of the `Multiplier` type.
	pub MaximumMultiplier: Multiplier = Bounded::max_value();
}

/// Parameterized slow adjusting fee updated based on
/// <https://research.web3.foundation/en/latest/polkadot/overview/2-token-economics.html#-2.-slow-adjusting-mechanism>
pub type SlowAdjustingFeeUpdate<R> = TargetedFeeAdjustment<
	R,
	TargetBlockFullness,
	AdjustmentVariable,
	MinimumMultiplier,
	MaximumMultiplier,
>;

// Can't use `FungibleAdapter` here until Treasury pallet migrates to fungibles
// <https://github.com/paritytech/polkadot-sdk/issues/226>
#[allow(deprecated)]
/// ## <a id="config.TransactionPayment">TransactionPayment Config</a>
///
/// Charge users for their transactions according to the transactions weight.
/// - [`WeightToFee`](#associatedtype.WeightToFee) is a custom curve, for details see `crate::tokenomics`
/// - [`LengthToFee`](#associatedtype.LengthToFee) is a custom curve, for details see `crate::tokenomics`
impl pallet_transaction_payment::Config for Runtime {
	/// The event type that will be emitted for transaction payment events.
	type RuntimeEvent = RuntimeEvent;

	#[cfg(not(feature = "testnet"))]
	/// Specify how to charge transaction fees.
	/// Currently we have disabled fee distribution on mainnet.
	type OnChargeTransaction = FungibleAdapter<Balances, ()>;

	#[cfg(feature = "testnet")]
	/// Specify how to charge transaction fees.
	type OnChargeTransaction = FungibleAdapter<Balances, DealWithFees>;

	/// The multiplier applied to operational transaction fees.
	type OperationalFeeMultiplier = OperationalFeeMultiplier;

	/// Use our custom weight to fee curve
	type WeightToFee = WeightToFee;

	/// Use our custom length to fee curve
	type LengthToFee = LengthToFee;

	/// Defines how the fee multiplier is updated based on the block fullness.
	/// The `TargetedFeeAdjustment` adjusts the fee multiplier to maintain the target block fullness.
	type FeeMultiplierUpdate = SlowAdjustingFeeUpdate<Self>;

	/// Benchmarked weights associated with transaction payments
	type WeightInfo = pallet_transaction_payment::weights::SubstrateWeight<Runtime>;
}

parameter_types! {
	pub const MinVestedTransfer: Balance = 1 * ANLOG;
	pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons =
		WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE);
}

/// ## <a id="config.Vesting">`Vesting` Config</a>
///
/// Allow tokens to be locked following schedule
impl pallet_vesting::Config for Runtime {
	type RuntimeEvent = RuntimeEvent;
	type Currency = Balances;
	type BlockNumberToBalance = ConvertInto;
	type MinVestedTransfer = MinVestedTransfer;
	type WeightInfo = pallet_vesting::weights::SubstrateWeight<Runtime>;
	type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons;
	type BlockNumberProvider = System;
	// `VestingInfo` encode length is 36bytes. 28 schedules gets encoded as 1009 bytes, which is the
	// highest number of schedules that encodes less than 2^10.
	const MAX_VESTING_SCHEDULES: u32 = 28;
}

#[cfg(test)]
mod test {
	use polkadot_sdk::*;
	use time_primitives::{MICROANLOG, MILLIANLOG};

	use frame_support::{
		dispatch::{DispatchClass, DispatchInfo},
		traits::OnFinalize,
		weights::{Weight, WeightToFee as WeightToFeeT},
	};
	use pallet_transaction_payment::Multiplier;
	use separator::Separatable;
	use sp_runtime::BuildStorage;
	//use pallet_elections::WeightInfo as W4;
	//
	//use pallet_members::WeightInfo as W2;
	//use pallet_tasks::WeightInfo;

	use super::{MinimumMultiplier, SlowAdjustingFeeUpdate, TargetBlockFullness};
	use crate::{
		ExtrinsicBaseWeight, Runtime, RuntimeBlockWeights, System, TransactionPayment,
		MAXIMUM_BLOCK_WEIGHT,
	};

	#[test]
	// Test that the fee for `MAXIMUM_BLOCK_WEIGHT` of weight has sane bounds.
	fn full_block_fee_is_correct() {
		// A full block should cost between 5,000 and 10,000 MILLIANLOG.
		let full_block = crate::WeightToFee::weight_to_fee(&MAXIMUM_BLOCK_WEIGHT);
		println!("FULL BLOCK Fee: {}", full_block);
		assert!(full_block >= 5_000 * MILLIANLOG);
		assert!(full_block <= 10_000 * MILLIANLOG);
	}

	#[test]
	// This function tests that the fee for `ExtrinsicBaseWeight` of weight is correct
	fn extrinsic_base_fee_is_correct() {
		// `ExtrinsicBaseWeight` should cost MICROANLOG
		println!("Base: {}", ExtrinsicBaseWeight::get());
		let x = crate::WeightToFee::weight_to_fee(&ExtrinsicBaseWeight::get());
		let y = MILLIANLOG;
		assert!(x.max(y) - x.min(y) < MICROANLOG);
	}

	fn run_with_system_weight<F>(w: Weight, mut assertions: F)
	where
		F: FnMut(),
	{
		let mut t: sp_io::TestExternalities = frame_system::GenesisConfig::<Runtime>::default()
			.build_storage()
			.unwrap()
			.into();
		t.execute_with(|| {
			System::set_block_consumed_resources(w, 0);
			assertions()
		});
	}

	#[test]
	fn multiplier_can_grow_from_zero() {
		let minimum_multiplier = MinimumMultiplier::get();
		let target = TargetBlockFullness::get()
			* RuntimeBlockWeights::get().get(DispatchClass::Normal).max_total.unwrap();
		// if the min is too small, then this will not change, and we are doomed forever.
		// the weight is 1/100th bigger than target.
		run_with_system_weight(target.saturating_mul(101) / 100, || {
			use sp_runtime::traits::Convert;

			let next = SlowAdjustingFeeUpdate::<Runtime>::convert(minimum_multiplier);
			assert!(next > minimum_multiplier, "{:?} !>= {:?}", next, minimum_multiplier);
		})
	}

	#[test]
	fn multiplier_growth_simulator() {
		// assume the multiplier is initially set to its minimum. We update it with values twice the
		//target (target is 25%, thus 50%) and we see at which point it reaches 1.
		let mut multiplier = MinimumMultiplier::get();
		let block_weight = RuntimeBlockWeights::get().get(DispatchClass::Normal).max_total.unwrap();
		let mut blocks = 0;
		let mut fees_paid = 0;
		let info = DispatchInfo {
			call_weight: Weight::MAX,
			..Default::default()
		};

		let mut t: sp_io::TestExternalities = frame_system::GenesisConfig::<Runtime>::default()
			.build_storage()
			.unwrap()
			.into();
		// set the minimum
		t.execute_with(|| {
			frame_system::Pallet::<Runtime>::set_block_consumed_resources(Weight::MAX, 0);
			pallet_transaction_payment::NextFeeMultiplier::<Runtime>::set(MinimumMultiplier::get());
		});

		while multiplier <= Multiplier::from_u32(1) {
			t.execute_with(|| {
				// imagine this tx was called.
				let fee = TransactionPayment::compute_fee(0, &info, 0);
				fees_paid += fee;

				// this will update the multiplier.
				System::set_block_consumed_resources(block_weight, 0);
				TransactionPayment::on_finalize(1);
				let next = TransactionPayment::next_fee_multiplier();

				assert!(next > multiplier, "{:?} !>= {:?}", next, multiplier);
				multiplier = next;

				println!(
					"block = {} / multiplier {:?} / fee = {:?} / fess so far {:?}",
					blocks,
					multiplier,
					fee.separated_string(),
					fees_paid.separated_string()
				);
			});
			blocks += 1;
		}
	}
}