diff --git a/Cargo.lock b/Cargo.lock index 3a9d68c..69290f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3649,13 +3649,14 @@ dependencies = [ [[package]] name = "ghost-networks" -version = "0.1.13" +version = "0.1.14" dependencies = [ "frame-benchmarking", "frame-support", "frame-system", "ghost-core-primitives", "ghost-traits", + "num-traits", "pallet-balances", "pallet-staking", "pallet-staking-reward-curve", diff --git a/Cargo.toml b/Cargo.toml index 3b08734..b5ecdad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,7 @@ bs58 = { version = "0.5.0" } prometheus-parse = { version = "0.2.2" } rustc-hex = { version = "2.1.0", default-features = false } log = { version = "0.4", default-features = false } +num-traits = { version = "0.2.17", default-features = false } libsecp256k1 = { version = "0.7", default-features = false } bip39 = { package = "parity-bip39", version = "2.0.1" } sha3 = { version = "0.10", default-features = false } diff --git a/pallets/networks/Cargo.toml b/pallets/networks/Cargo.toml index 6b95d2c..13f1883 100644 --- a/pallets/networks/Cargo.toml +++ b/pallets/networks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ghost-networks" -version = "0.1.13" +version = "0.1.14" license.workspace = true authors.workspace = true edition.workspace = true @@ -10,6 +10,7 @@ repository.workspace = true [dependencies] scale-info = { workspace = true, features = ["derive"] } codec = { workspace = true, features = ["max-encoded-len"] } +num-traits = { workspace = true } frame-benchmarking = { workspace = true, optional = true } frame-support = { workspace = true } @@ -30,6 +31,7 @@ default = ["std"] std = [ "scale-info/std", "codec/std", + "num-traits/std", "frame-support/std", "frame-system/std", "frame-benchmarking?/std", diff --git a/pallets/networks/src/lib.rs b/pallets/networks/src/lib.rs index 44292be..0f80760 100644 --- a/pallets/networks/src/lib.rs +++ b/pallets/networks/src/lib.rs @@ -21,9 +21,11 @@ pub use ghost_traits::networks::{ NetworkDataBasicHandler, NetworkDataInspectHandler, NetworkDataMutateHandler, }; +mod math; mod weights; pub use crate::weights::WeightInfo; +use math::MulDiv; pub use module::*; #[cfg(any(feature = "runtime-benchmarks", test))] @@ -73,7 +75,15 @@ pub struct BridgedInflationCurve(core::marker::PhantomData<(Rewa impl pallet_staking::EraPayout for BridgedInflationCurve where - Balance: Default + AtLeast32BitUnsigned + Clone + Copy + From, + Balance: Default + + Copy + + From> + + AtLeast32BitUnsigned + + num_traits::ops::wrapping::WrappingAdd + + num_traits::ops::overflowing::OverflowingAdd + + sp_std::ops::AddAssign + + sp_std::ops::Not + + sp_std::ops::BitAnd, RewardCurve: Get<&'static PiecewiseLinear<'static>>, T: Config, { @@ -82,30 +92,27 @@ where total_issuance: Balance, _era_duration_in_millis: u64, ) -> (Balance, Balance) { - let piecewise_linear = RewardCurve::get(); - let bridge_adjustment = BridgedImbalance::::get(); + let reward_curve = RewardCurve::get(); + let bridged_imbalance = BridgedImbalance::::get(); let accumulated_commission = AccumulatedCommission::::get(); - let bridged_out: u128 = bridge_adjustment.bridged_out.try_into().unwrap_or_default(); - let bridged_in: u128 = bridge_adjustment.bridged_in.try_into().unwrap_or_default(); - let accumulated_commission: u128 = accumulated_commission.try_into().unwrap_or_default(); - - let accumulated_balance = Balance::from(accumulated_commission); - let adjusted_issuance = match bridged_out > bridged_in { - true => total_issuance.saturating_add(Balance::from(bridged_out - bridged_in)), - false => total_issuance.saturating_sub(Balance::from(bridged_in - bridged_out)), - }; + let accumulated_commission: Balance = accumulated_commission.into(); + let adjusted_issuance: Balance = total_issuance + .saturating_add(bridged_imbalance.bridged_out.into()) + .saturating_sub(bridged_imbalance.bridged_in.into()); NullifyNeeded::::set(true); - match piecewise_linear - .calculate_for_fraction_times_denominator(total_staked, adjusted_issuance) - .checked_mul(&accumulated_balance) - .and_then(|product| product.checked_div(&adjusted_issuance)) - { - Some(payout) => (payout, accumulated_balance.saturating_sub(payout)), - None => (Balance::default(), Balance::default()), - } + let estimated_reward = + reward_curve.calculate_for_fraction_times_denominator(total_staked, adjusted_issuance); + let payout = MulDiv::::calculate( + estimated_reward, + accumulated_commission, + adjusted_issuance, + ); + let rest_payout = accumulated_commission.saturating_sub(payout); + + (payout, rest_payout) } } diff --git a/pallets/networks/src/math.rs b/pallets/networks/src/math.rs new file mode 100644 index 0000000..9051d27 --- /dev/null +++ b/pallets/networks/src/math.rs @@ -0,0 +1,120 @@ +use crate::AtLeast32BitUnsigned; + +pub struct MulDiv(core::marker::PhantomData); +impl MulDiv +where + Balance: Copy + + AtLeast32BitUnsigned + + num_traits::ops::wrapping::WrappingAdd + + num_traits::ops::overflowing::OverflowingAdd + + sp_std::ops::AddAssign + + sp_std::ops::Not + + sp_std::ops::BitAnd, +{ + fn least_significant_bits(&self, a: Balance) -> Balance { + a & ((1 << 64) - 1) + } + + fn most_significant_bits(&self, a: Balance) -> Balance { + a >> 64 + } + + fn two_complement(&self, a: Balance) -> Balance { + let one: Balance = 1u32.into(); + (!a).wrapping_add(&one) + } + + fn adjusted_ratio(&self, a: Balance) -> Balance { + let one: Balance = 1u32.into(); + (self.two_complement(a) / a).wrapping_add(&one) + } + + fn modulo(&self, a: Balance) -> Balance { + self.two_complement(a) % a + } + + fn overflow_resistant_addition( + &self, + a0: Balance, + a1: Balance, + b0: Balance, + b1: Balance, + ) -> (Balance, Balance) { + let (r0, overflow) = a0.overflowing_add(&b0); + let overflow: Balance = overflow.then(|| 1u32).unwrap_or_default().into(); + let r1 = a1.wrapping_add(&b1).wrapping_add(&overflow); + (r0, r1) + } + + fn overflow_resistant_multiplication(&self, a: Balance, b: Balance) -> (Balance, Balance) { + let (a0, a1) = ( + self.least_significant_bits(a), + self.most_significant_bits(a), + ); + let (b0, b1) = ( + self.least_significant_bits(b), + self.most_significant_bits(b), + ); + let (x, y) = (a1 * b0, b1 * a0); + + let (r0, r1) = (a0 * b0, a1 * b1); + let (r0, r1) = self.overflow_resistant_addition( + r0, + r1, + self.least_significant_bits(x) << 64, + self.most_significant_bits(x), + ); + let (r0, r1) = self.overflow_resistant_addition( + r0, + r1, + self.least_significant_bits(y) << 64, + self.most_significant_bits(y), + ); + + (r0, r1) + } + + fn overflow_resistant_division( + &self, + mut a0: Balance, + mut a1: Balance, + b: Balance, + ) -> (Balance, Balance) { + if b == 1u32.into() { + return (a0, a1); + } + + let zero: Balance = 0u32.into(); + let (q, r) = (self.adjusted_ratio(b), self.modulo(b)); + let (mut x0, mut x1) = (zero, zero); + + while a1 != zero { + let (t0, t1) = self.overflow_resistant_multiplication(a1, q); + let (new_x0, new_x1) = self.overflow_resistant_addition(x0, x1, t0, t1); + x0 = new_x0; + x1 = new_x1; + + let (t0, t1) = self.overflow_resistant_multiplication(a1, r); + let (new_a0, new_a1) = self.overflow_resistant_addition(t0, t1, a0, zero); + a0 = new_a0; + a1 = new_a1; + } + + self.overflow_resistant_addition(x0, x1, a0 / b, zero) + } + + fn mul_div(&self, a: Balance, b: Balance, c: Balance) -> Balance { + let (t0, t1) = self.overflow_resistant_multiplication(a, b); + self.overflow_resistant_division(t0, t1, c).0 + } + + pub fn calculate(a: Balance, b: Balance, c: Balance) -> Balance { + let zero: Balance = 0u32.into(); + if a == zero || b == zero || c == zero { + return zero; + } + + let inner = MulDiv(core::marker::PhantomData); + inner.mul_div(a, b, c) + } +} diff --git a/pallets/networks/src/tests.rs b/pallets/networks/src/tests.rs index 7188e14..9e3d223 100644 --- a/pallets/networks/src/tests.rs +++ b/pallets/networks/src/tests.rs @@ -1295,8 +1295,10 @@ fn accumulated_commission_could_be_nullified() { #[test] fn bridged_inlation_reward_works() { ExtBuilder::build().execute_with(|| { - let amount: u128 = 1337 * 1_000_000_000; - let commission: u128 = amount / 100; // 1% commission + let amount_full: u128 = 1337 * 1_000_000_000; + let commission: u128 = amount_full / 100; // 1% commission + let amount: u128 = amount_full - commission; + let total_staked_ideal: u128 = 69; let total_staked_not_ideal: u128 = 68; let total_issuance: u128 = 100; @@ -1538,8 +1540,10 @@ fn bridged_inlation_reward_works() { #[test] fn bridged_inflation_era_payout_triggers_need_of_nullification() { ExtBuilder::build().execute_with(|| { - let amount: u128 = 1337 * 1_000_000_000; - let commission: u128 = amount / 100; // 1% commission + let amount_full: u128 = 1337 * 1_000_000_000; + let commission: u128 = amount_full / 100; // 1% commission + let amount: u128 = amount_full - commission; + let total_staked_ideal: u128 = 69; let total_issuance: u128 = 100; @@ -1570,3 +1574,165 @@ fn trigger_nullification_works_as_expected() { assert_eq!(NullifyNeeded::::get(), false); }); } + +#[test] +fn check_substrate_guarantees_not_to_overflow() { + ExtBuilder::build().execute_with(|| { + let reward_curve = RewardCurve::get(); + let mut n: u128 = 69; + let mut d: u128 = 100; + + loop { + n = match n.checked_mul(1_000_000) { + Some(value) => value, + None => break, + }; + d = match d.checked_mul(1_000_000) { + Some(value) => value, + None => break, + }; + assert_eq!( + reward_curve.calculate_for_fraction_times_denominator(n, d), + d + ); + } + }); +} + +#[test] +fn check_muldiv_guarantees_not_to_overflow() { + ExtBuilder::build().execute_with(|| { + let mut a: u128 = 2; + let mut b: u128 = 3; + let mut c: u128 = 6; + let mut result: u128 = 1; + + loop { + a = match a.checked_mul(1_000_000) { + Some(value) => value, + None => break, + }; + b = match b.checked_mul(1_000_000) { + Some(value) => value, + None => break, + }; + c = match c.checked_mul(1_000_000) { + Some(value) => value, + None => break, + }; + result = match result.checked_mul(1_000_000) { + Some(value) => value, + None => break, + }; + + assert_eq!(MulDiv::::calculate(a, b, c), result); + } + + assert_eq!( + MulDiv::::calculate(u128::MAX, u128::MAX, u128::MAX), + u128::MAX + ); + assert_eq!(MulDiv::::calculate(u128::MAX, 0, 0), 0); + assert_eq!(MulDiv::::calculate(0, u128::MAX, 0), 0); + assert_eq!(MulDiv::::calculate(0, 0, u128::MAX), 0); + }); +} + +#[test] +fn check_bridged_inflation_curve_for_overflow() { + ExtBuilder::build().execute_with(|| { + let amount_full: u128 = 1337 * 1_000_000_000; + let commission: u128 = amount_full / 100; // 1% commission + let amount: u128 = amount_full - commission; + + let tollerance: u128 = commission / 100; // 1% tollerance + let precomputed_payout: u128 = 13177568884; + let precomputed_rest: u128 = 192431116; + assert_eq!(precomputed_payout + precomputed_rest, commission); + + let mut total_staked_ideal: u128 = 69_000; + let mut total_staked_not_ideal: u128 = 68_000; + let mut total_issuance: u128 = 100_000; + + assert_ok!(GhostNetworks::accumulate_commission(&commission)); + assert_ok!(GhostNetworks::accumulate_incoming_imbalance(&amount)); + + loop { + total_staked_ideal = match total_staked_ideal.checked_mul(1_000_000) { + Some(value) => value, + None => break, + }; + total_staked_not_ideal = match total_staked_not_ideal.checked_mul(1_000_000) { + Some(value) => value, + None => break, + }; + total_issuance = match total_issuance.checked_mul(1_000_000) { + Some(value) => value, + None => break, + }; + assert_eq!( + BridgedInflationCurve::::era_payout( + total_staked_ideal, + total_issuance + amount, + 0 + ), + (commission, 0) + ); + + let (payout, rest) = BridgedInflationCurve::::era_payout( + total_staked_not_ideal, + total_issuance + amount, + 0, + ); + + let payout_deviation = if precomputed_payout > payout { + precomputed_payout - payout + } else { + payout - precomputed_payout + }; + + let rest_deviation = if precomputed_rest > rest { + precomputed_rest - rest + } else { + rest - precomputed_rest + }; + + assert!(payout_deviation < tollerance); + assert!(rest_deviation < tollerance); + } + }); +} + +#[test] +fn check_bridged_inflation_curve_for_big_commissions() { + ExtBuilder::build().execute_with(|| { + let mut amount_full: u128 = 1337; + + let total_staked_ideal: u128 = 69_000_000; + let total_issuance: u128 = 100_000_000; + + loop { + amount_full = match amount_full.checked_mul(1_000_000) { + Some(value) => value, + None => break, + }; + let commission: u128 = amount_full / 100; // 1% commission + let amount: u128 = amount_full - commission; + + AccumulatedCommission::::set(commission); + BridgedImbalance::::set(BridgeAdjustment { + bridged_in: amount, + bridged_out: 0, + }); + + assert_eq!( + BridgedInflationCurve::::era_payout( + total_staked_ideal, + total_issuance + amount, + 0 + ), + (commission, 0) + ); + } + }); +}