diff --git a/Cargo.lock b/Cargo.lock index 2eaa44129..65981c141 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4461,7 +4461,7 @@ dependencies = [ [[package]] name = "hydra-dx-math" -version = "7.7.2" +version = "7.8.0" dependencies = [ "approx", "criterion", @@ -4618,7 +4618,7 @@ dependencies = [ [[package]] name = "hydradx-runtime" -version = "205.0.0" +version = "206.0.0" dependencies = [ "cumulus-pallet-aura-ext", "cumulus-pallet-dmp-queue", @@ -8291,7 +8291,7 @@ dependencies = [ [[package]] name = "pallet-stableswap" -version = "3.4.2" +version = "3.4.3" dependencies = [ "bitflags 1.3.2", "frame-benchmarking", @@ -11226,7 +11226,7 @@ dependencies = [ [[package]] name = "runtime-integration-tests" -version = "1.17.1" +version = "1.17.2" dependencies = [ "cumulus-pallet-aura-ext", "cumulus-pallet-dmp-queue", diff --git a/README.md b/README.md index efbf05bff..0becb3ce6 100644 --- a/README.md +++ b/README.md @@ -37,27 +37,15 @@ cargo build --release ## Run -### Local Testnet +### Chopsticks -Relay chain repository (polkadot) has to be built in `../polkadot` -Install `polkadot-launch` utility used to start network. - -``` -npm install -g polkadot-launch -``` +The easiest way to run and interact with HydraDX node is to use [Chopsticks](/~https://github.com/acalanetwork/chopsticks) -Start local testnet with 4 relay chain validators and HydraDX as a parachain with 2 collators. - -``` -cd ./rococo-local -polkadot-launch config.json +```Bash +npx @acala-network/chopsticks@latest --config=launch-configs/chopsticks/hydradx.yml ``` -Observe HydraDX logs - -``` -multitail 99*.log -``` +Now you have a test node running at [`ws://localhost:8000`](https://polkadot.js.org/apps/?rpc=ws%3A%2F%2Flocalhost%3A8000#/explorer) ### Local Testnet with Zombienet @@ -73,105 +61,11 @@ zombienet spawn config-zombienet.json ### Interaction with the node -Go to the polkadot apps at https://dotapps.io - -Then open settings screen -> developer and paste - -*NOTE - FixedU128 type is not yet implemented for polkadot apps. Balance is a measure so price can be reasonably selected. If using polkadot apps to create pool:* -- 1 Mega Units equals 1:1 price -- 20 Mega Units equals 20:1 price -- 50 Kilo Units equals 0.05:1 price - -``` -{ - "types": [ - { - "AssetPair": { - "asset_in": "AssetId", - "asset_out": "AssetId" - }, - "Amount": "i128", - "AmountOf": "Amount", - "Address": "AccountId", - "OrmlAccountData": { - "free": "Balance", - "frozen": "Balance", - "reserved": "Balance" - }, - "BlockNumber": "u32", - "BalanceInfo": { - "amount": "Balance", - "assetId": "AssetId" - }, - "Chain": { - "genesisHash": "Vec", - "lastBlockHash": "Vec" - }, - "CurrencyId": "AssetId", - "CurrencyIdOf": "AssetId", - "Intention": { - "who": "AccountId", - "asset_sell": "AssetId", - "asset_buy": "AssetId", - "amount": "Balance", - "discount": "bool", - "sell_or_buy": "IntentionType" - }, - "IntentionId": "Hash", - "IntentionType": { - "_enum": [ - "SELL", - "BUY" - ] - }, - "LookupSource": "AccountId", - "OrderedSet": "Vec", - "Price": "Balance", - "Fee": { - "numerator": "u32", - "denominator": "u32" - }, - "VestingScheduleOf": { - "start": "BlockNumber", - "period": "BlockNumber", - "period_count": "u32", - "per_period": "Balance" - } - } - ], - "alias": { - "tokens": { - "AccountData": "OrmlAccountData" - } - } -} -``` - -Connect to the -- Hacknet: `wss://hack.hydradx.io:9944` -- [Stakenet](https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Frpc-01.snakenet.hydradx.io): `wss://rpc-01.snakenet.hydradx.io` -- or local node – if you are on chromium based browser, set chrome://flags/#allow-insecure-localhost - -### Performance check - -Prerequisites: rust/cargo, python 3.8+ +Go to the polkadot apps at https://polkadot.js.org/apps -With the following script it is possible to run a simple performance check. It might be useful -to determine whether your machine is suitable to run HydraDX node. - -From the top-level node directory: - -```bash -./scripts/check_performance.sh -``` - -This will run series of benchmarks ( which may take a while). -The output will show benchmark results of HydraDX pallets and comparison against reference values. - -The most interesting information would be the difference between the HydraDx benchmark value and the local machine's benchmark. - -If the difference is >= 0, performance is similar or better. -However, if the difference < 0 - your machine might not be suitable to run HydraDX node. Contact HydraDX devs to discuss the results. +Connect to +- Mainnet: `wss://rpc.hydradx.cloud` +- local node: `ws://localhost:8000` (if you are using chopsticks) ### Testing of storage migrations and runtime upgrades @@ -183,5 +77,16 @@ try-runtime --runtime ./target/release/wbuild/hydradx-runtime/hydradx_runtime.wa ``` or against HydraDX testnet on Rococo using `--uri wss://rococo-hydradx-rpc.hydration.dev:443` -### Honorable contributions -[@apopiak](/~https://github.com/apopiak) for great reviews [#87](/~https://github.com/galacticcouncil/HydraDX-node/pull/87) and support. + +## Security +Useful resources: + +* /~https://github.com/galacticcouncil/HydraDX-security +* https://apidocs.bsx.fi/HydraDX +* https://docs.hydradx.io/ +* https://docs.hydradx.io/omnipool_design +* https://docs.hydradx.io/fees + +Bug bounty: [https://immunefi.com/bounty/hydradx/](https://immunefi.com/bounty/hydradx/) + +Reponsible disclosure: security@hydradx.io diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index e149e86ac..c054c120d 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "runtime-integration-tests" -version = "1.17.1" +version = "1.17.2" description = "Integration tests" authors = ["GalacticCouncil"] edition = "2021" @@ -193,6 +193,7 @@ std = [ "polkadot-runtime/std", "hydradx-runtime/std", "pallet-staking/std", + "scraper/std", ] # we don't include integration tests when benchmarking feature is enabled diff --git a/math/Cargo.toml b/math/Cargo.toml index b41fdb3fd..812024ecf 100644 --- a/math/Cargo.toml +++ b/math/Cargo.toml @@ -6,7 +6,7 @@ license = 'Apache-2.0' name = "hydra-dx-math" description = "A collection of utilities to make performing liquidity pool calculations more convenient." repository = '/~https://github.com/galacticcouncil/hydradx-math' -version = "7.7.2" +version = "7.8.0" [dependencies] primitive-types = {default-features = false, version = '0.12.0'} diff --git a/math/src/stableswap/math.rs b/math/src/stableswap/math.rs index bf7a9b6c9..ba001673f 100644 --- a/math/src/stableswap/math.rs +++ b/math/src/stableswap/math.rs @@ -21,20 +21,30 @@ const PRECISION: u8 = 1; /// D - number of iterations to use for Newton's formula to calculate parameter D ( it should be >=1 otherwise it wont converge at all and will always fail /// Y - number of iterations to use for Newton's formula to calculate reserve Y ( it should be >=1 otherwise it wont converge at all and will always fail pub fn calculate_out_given_in( - balances: &[AssetReserve], + initial_reserves: &[AssetReserve], idx_in: usize, idx_out: usize, amount_in: Balance, amplification: Balance, ) -> Option { - if idx_in >= balances.len() || idx_out >= balances.len() { + if idx_in >= initial_reserves.len() || idx_out >= initial_reserves.len() { return None; } - let reserves = normalize_reserves(balances); - let amount_in = normalize_value(amount_in, balances[idx_in].decimals, TARGET_PRECISION, Rounding::Down); + let reserves = normalize_reserves(initial_reserves); + let amount_in = normalize_value( + amount_in, + initial_reserves[idx_in].decimals, + TARGET_PRECISION, + Rounding::Up, + ); let new_reserve_out = calculate_y_given_in::(amount_in, idx_in, idx_out, &reserves, amplification)?; let amount_out = reserves[idx_out].checked_sub(new_reserve_out)?; - let amount_out = normalize_value(amount_out, TARGET_PRECISION, balances[idx_out].decimals, Rounding::Down); + let amount_out = normalize_value( + amount_out, + TARGET_PRECISION, + initial_reserves[idx_out].decimals, + Rounding::Down, + ); Some(amount_out.saturating_sub(1u128)) } @@ -42,33 +52,43 @@ pub fn calculate_out_given_in( /// D - number of iterations to use for Newton's formula ( it should be >=1 otherwise it wont converge at all and will always fail /// Y - number of iterations to use for Newton's formula to calculate reserve Y ( it should be >=1 otherwise it wont converge at all and will always fail pub fn calculate_in_given_out( - balances: &[AssetReserve], + initial_reserves: &[AssetReserve], idx_in: usize, idx_out: usize, amount_out: Balance, amplification: Balance, ) -> Option { - if idx_in >= balances.len() || idx_out >= balances.len() { + if idx_in >= initial_reserves.len() || idx_out >= initial_reserves.len() { return None; } - let reserves = normalize_reserves(balances); - let amount_out = normalize_value(amount_out, balances[idx_out].decimals, TARGET_PRECISION, Rounding::Down); + let reserves = normalize_reserves(initial_reserves); + let amount_out = normalize_value( + amount_out, + initial_reserves[idx_out].decimals, + TARGET_PRECISION, + Rounding::Down, + ); let new_reserve_in = calculate_y_given_out::(amount_out, idx_in, idx_out, &reserves, amplification)?; let amount_in = new_reserve_in.checked_sub(reserves[idx_in])?; - let amount_in = normalize_value(amount_in, TARGET_PRECISION, balances[idx_in].decimals, Rounding::Up); + let amount_in = normalize_value( + amount_in, + TARGET_PRECISION, + initial_reserves[idx_in].decimals, + Rounding::Up, + ); Some(amount_in.saturating_add(1u128)) } /// Calculate amount to be received from the pool given the amount to be sent to the pool with fee applied. pub fn calculate_out_given_in_with_fee( - balances: &[AssetReserve], + initial_reserves: &[AssetReserve], idx_in: usize, idx_out: usize, amount_in: Balance, amplification: Balance, fee: Permill, ) -> Option<(Balance, Balance)> { - let amount_out = calculate_out_given_in::(balances, idx_in, idx_out, amount_in, amplification)?; + let amount_out = calculate_out_given_in::(initial_reserves, idx_in, idx_out, amount_in, amplification)?; let fee_amount = calculate_fee_amount(amount_out, fee, Rounding::Down); let amount_out = amount_out.checked_sub(fee_amount)?; Some((amount_out, fee_amount)) @@ -76,14 +96,14 @@ pub fn calculate_out_given_in_with_fee( /// Calculate amount to be sent to the pool given the amount to be received from the pool with fee applied. pub fn calculate_in_given_out_with_fee( - balances: &[AssetReserve], + initial_reserves: &[AssetReserve], idx_in: usize, idx_out: usize, amount_out: Balance, amplification: Balance, fee: Permill, ) -> Option<(Balance, Balance)> { - let amount_in = calculate_in_given_out::(balances, idx_in, idx_out, amount_out, amplification)?; + let amount_in = calculate_in_given_out::(initial_reserves, idx_in, idx_out, amount_out, amplification)?; let fee_amount = calculate_fee_amount(amount_in, fee, Rounding::Up); let amount_in = amount_in.checked_add(fee_amount)?; Some((amount_in, fee_amount)) @@ -119,8 +139,8 @@ pub fn calculate_shares( let (d0, d1) = to_u256!(initial_d, updated_d); - let adjusted_balances = if share_issuance > 0 { - let adjusted_balances: Vec = updated_reserves + let adjusted_reserves = if share_issuance > 0 { + updated_reserves .iter() .enumerate() .map(|(idx, asset_reserve)| -> Option { @@ -133,12 +153,11 @@ pub fn calculate_shares( asset_reserve.decimals, )) }) - .collect::>>()?; - adjusted_balances + .collect::>>()? } else { updated_reserves.to_vec() }; - let adjusted_d = calculate_d::(&adjusted_balances, amplification)?; + let adjusted_d = calculate_d::(&adjusted_reserves, amplification)?; if share_issuance == 0 { // if first liquidity added @@ -186,7 +205,7 @@ pub fn calculate_shares_for_amount( let initial_d = calculate_d::(initial_reserves, amplification)?; let updated_d = calculate_d::(&updated_reserves, amplification)?; let (d1, d0) = to_u256!(updated_d, initial_d); - let adjusted_balances: Vec = updated_reserves + let adjusted_reserves: Vec = updated_reserves .iter() .enumerate() .map(|(idx, asset_reserve)| -> Option { @@ -201,7 +220,7 @@ pub fn calculate_shares_for_amount( }) .collect::>>()?; - let adjusted_d = calculate_d::(&adjusted_balances, amplification)?; + let adjusted_d = calculate_d::(&adjusted_reserves, amplification)?; let (d_diff, issuance_hp) = to_u256!(initial_d.checked_sub(adjusted_d)?, share_issuance); let share_amount = issuance_hp .checked_mul(d_diff)? @@ -211,6 +230,7 @@ pub fn calculate_shares_for_amount( } /// Given amount of shares and asset reserves, calculate corresponding amount of selected asset to be withdrawn. +/// Returns amount of asset to be withdrawn and fee amount. Note that fee amount is not deducted from amount of asset to be withdrawn. pub fn calculate_withdraw_one_asset( reserves: &[AssetReserve], shares: Balance, @@ -367,13 +387,13 @@ pub fn calculate_add_one_asset( let dy = y1.checked_sub(asset_reserve)?; let dy_0 = y.checked_sub(asset_reserve)?; let fee = dy.checked_sub(dy_0)?; - let amount_in = normalize_value(dy, TARGET_PRECISION, asset_in_decimals, Rounding::Down); + let amount_in = normalize_value(dy, TARGET_PRECISION, asset_in_decimals, Rounding::Up); let fee = normalize_value(fee, TARGET_PRECISION, asset_in_decimals, Rounding::Down); Some((amount_in, fee)) } pub fn calculate_d(reserves: &[AssetReserve], amplification: Balance) -> Option { - let balances = normalize_reserves(reserves); - calculate_d_internal::(&balances, amplification) + let n_reserves = normalize_reserves(reserves); + calculate_d_internal::(&n_reserves, amplification) } const fn calculate_ann(n: usize, amplification: Balance) -> Option { @@ -385,18 +405,18 @@ pub(crate) fn calculate_y_given_in( amount: Balance, idx_in: usize, idx_out: usize, - balances: &[Balance], + reserves: &[Balance], amplification: Balance, ) -> Option { - if idx_in >= balances.len() || idx_out >= balances.len() { + if idx_in >= reserves.len() || idx_out >= reserves.len() { return None; } - let new_reserve_in = balances[idx_in].checked_add(amount)?; + let new_reserve_in = reserves[idx_in].checked_add(amount)?; - let d = calculate_d_internal::(balances, amplification)?; + let d = calculate_d_internal::(reserves, amplification)?; - let xp: Vec = balances + let xp: Vec = reserves .iter() .enumerate() .filter(|(idx, _)| *idx != idx_out) @@ -411,16 +431,16 @@ pub(crate) fn calculate_y_given_out( amount: Balance, idx_in: usize, idx_out: usize, - balances: &[Balance], + reserves: &[Balance], amplification: Balance, ) -> Option { - if idx_in >= balances.len() || idx_out >= balances.len() { + if idx_in >= reserves.len() || idx_out >= reserves.len() { return None; } - let new_reserve_out = balances[idx_out].checked_sub(amount)?; + let new_reserve_out = reserves[idx_out].checked_sub(amount)?; - let d = calculate_d_internal::(balances, amplification)?; - let xp: Vec = balances + let d = calculate_d_internal::(reserves, amplification)?; + let xp: Vec = reserves .iter() .enumerate() .filter(|(idx, _)| *idx != idx_in) @@ -624,54 +644,54 @@ pub(crate) fn normalize_value(amount: Balance, decimals: u8, target_decimals: u8 } pub fn calculate_share_prices( - balances: &[AssetReserve], + reserves: &[AssetReserve], amplification: Balance, issuance: Balance, ) -> Option> { - let n = balances.len(); + let n = reserves.len(); if n <= 1 { return None; } - let d = calculate_d::(balances, amplification)?; + let d = calculate_d::(reserves, amplification)?; let mut r = Vec::with_capacity(n); for idx in 0..n { - let price = calculate_share_price::(balances, amplification, issuance, idx, Some(d))?; + let price = calculate_share_price::(reserves, amplification, issuance, idx, Some(d))?; r.push(price); } Some(r) } pub fn calculate_share_price( - balances: &[AssetReserve], + reserves: &[AssetReserve], amplification: Balance, issuance: Balance, asset_idx: usize, provided_d: Option, ) -> Option<(Balance, Balance)> { - let n = balances.len() as u128; + let n = reserves.len() as u128; if n <= 1 { return None; } let d = if let Some(v) = provided_d { v } else { - calculate_d::(balances, amplification)? + calculate_d::(reserves, amplification)? }; - let reserves = normalize_reserves(balances); + let n_reserves = normalize_reserves(reserves); - let c = reserves + let c = n_reserves .iter() .try_fold(FixedU128::one(), |acc, reserve| { acc.checked_mul(&FixedU128::checked_from_rational(d, n.checked_mul(*reserve)?)?) })? .checked_mul_int(d)?; - let ann = calculate_ann(reserves.len(), amplification)?; + let ann = calculate_ann(n_reserves.len(), amplification)?; - let (d, c, xi, n, ann, issuance) = to_u256!(d, c, reserves[asset_idx], n, ann, issuance); + let (d, c, xi, n, ann, issuance) = to_u256!(d, c, n_reserves[asset_idx], n, ann, issuance); let xann = xi.checked_mul(ann)?; let p1 = d.checked_mul(xann)?; @@ -681,7 +701,7 @@ pub fn calculate_share_price( let num = p1.checked_add(p2)?.checked_sub(p3)?; let denom = issuance.checked_mul(xann.checked_add(c)?)?; - let p_diff = U256::from(10u128.saturating_pow(18u8.saturating_sub(balances[asset_idx].decimals) as u32)); + let p_diff = U256::from(10u128.saturating_pow(18u8.saturating_sub(reserves[asset_idx].decimals) as u32)); let (num, denom) = if let Some(v) = denom.checked_mul(p_diff) { (num, v) } else { @@ -698,27 +718,27 @@ pub fn calculate_share_price( } pub fn calculate_spot_price( - balances: &[AssetReserve], + reserves: &[AssetReserve], amplification: Balance, d: Balance, asset_idx: usize, ) -> Option<(Balance, Balance)> { - let n = balances.len(); + let n = reserves.len(); if n <= 1 || asset_idx > n { return None; } let ann = calculate_ann(n, amplification)?; - let mut reserves = normalize_reserves(balances); + let mut n_reserves = normalize_reserves(reserves); - let x0 = reserves[0]; - let xi = reserves[asset_idx]; + let x0 = n_reserves[0]; + let xi = n_reserves[asset_idx]; let (n, d, ann, x0, xi) = to_u256!(n, d, ann, x0, xi); - reserves.sort(); - let reserves: Vec = reserves.iter().map(|v| U256::from(*v)).collect(); - let c = reserves + n_reserves.sort(); + let reserves_hp: Vec = n_reserves.iter().map(|v| U256::from(*v)).collect(); + let c = reserves_hp .iter() .try_fold(d, |acc, val| acc.checked_mul(d)?.checked_div(val.checked_mul(n)?))?; diff --git a/math/src/stableswap/tests/invariants.rs b/math/src/stableswap/tests/invariants.rs index aa1ff6b88..98122f94b 100644 --- a/math/src/stableswap/tests/invariants.rs +++ b/math/src/stableswap/tests/invariants.rs @@ -36,6 +36,8 @@ fn decimals() -> impl Strategy { prop_oneof![Just(6), Just(8), Just(10), Just(12), Just(18)] } +// Note that his can generate very unbalanced pools. Should be adjusted to generate more balanced pools. +// In such case, we can see some outliers in the tests. fn some_pool(size: usize) -> impl Strategy> { prop::collection::vec( (asset_reserve(), decimals()).prop_map(|(v, dec)| AssetReserve::new(to_precision(v, dec), dec)), @@ -176,8 +178,6 @@ proptest! { .collect(); let d1 = calculate_d_internal::(&updated_balances, amp).unwrap(); assert!(d1 >= d0); - let diff = d1 - d0; - assert!(diff <= 5000u128); } } diff --git a/math/src/stableswap/tests/multi_assets.rs b/math/src/stableswap/tests/multi_assets.rs index 9655a3cc7..dfa7a3c36 100644 --- a/math/src/stableswap/tests/multi_assets.rs +++ b/math/src/stableswap/tests/multi_assets.rs @@ -714,7 +714,7 @@ fn calculate_exact_amount_of_shares() { amp, Permill::zero(), ); - assert_eq!(result, Some((1_000_000_000_000_000, 0))); + assert_eq!(result, Some((1_000_000_000_000_001, 0))); } #[test] @@ -745,7 +745,7 @@ fn calculate_exact_amount_of_shares_with_fee() { amp, Permill::from_percent(1), ); - assert_eq!(result, Some((1005001605353593, 2501371204363))); + assert_eq!(result, Some((1005001605353594, 2501371204363))); } #[test] diff --git a/pallets/stableswap/Cargo.toml b/pallets/stableswap/Cargo.toml index e9b2cd21b..59ed741ea 100644 --- a/pallets/stableswap/Cargo.toml +++ b/pallets/stableswap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = 'pallet-stableswap' -version = '3.4.2' +version = '3.4.3' description = 'AMM for correlated assets' authors = ['GalacticCouncil'] edition = '2021' diff --git a/pallets/stableswap/README.md b/pallets/stableswap/README.md index aa6930e0c..c38d60cdd 100644 --- a/pallets/stableswap/README.md +++ b/pallets/stableswap/README.md @@ -4,6 +4,18 @@ Curve/stableswap AMM implementation. +### Overview + +Curve style AMM, designed to provide highly efficient and low-slippage trades for stablecoins. + +#### Stableswap Hooks + +Stableswap pallet supports multiple hooks which are triggerred on certain operations: +- on_liquidity_changed - called when liquidity is added or removed from the pool +- on_trade - called when trade is executed + +This is currently used to update on-chain oracle. + #### Terminology * **LP** - liquidity provider @@ -14,7 +26,7 @@ Curve/stableswap AMM implementation. Maximum number of assets in pool is 5. -A pool can be created only by allowed `CreatePoolOrigin`. +A pool can be created only by allowed `AuthorityOrigin`. First LP to provided liquidity must add initial liquidity of all pool assets. Subsequent calls to add_liquidity, LP can provide only 1 asset. diff --git a/pallets/stableswap/src/benchmarks.rs b/pallets/stableswap/src/benchmarks.rs index 839ac6b49..f0b6cf13d 100644 --- a/pallets/stableswap/src/benchmarks.rs +++ b/pallets/stableswap/src/benchmarks.rs @@ -142,7 +142,7 @@ benchmarks! { }: _(RawOrigin::Signed(lp_provider.clone()), pool_id, desired_shares,asset_id, 1221886049851226) verify { assert_eq!(T::Currency::free_balance(pool_id, &lp_provider), desired_shares); - assert_eq!(T::Currency::free_balance(asset_id, &lp_provider), 999998791384905220211); + assert_eq!(T::Currency::free_balance(asset_id, &lp_provider), 999998791384905220210); } remove_liquidity_one_asset{ diff --git a/pallets/stableswap/src/lib.rs b/pallets/stableswap/src/lib.rs index e68e2f627..cede825ca 100644 --- a/pallets/stableswap/src/lib.rs +++ b/pallets/stableswap/src/lib.rs @@ -17,6 +17,18 @@ //! //! Curve/stableswap AMM implementation. //! +//! ## Overview +//! +//! Curve style AMM at is designed to provide highly efficient and low-slippage trades for stablecoins. +//! +//! ### Stableswap Hooks +//! +//! Stableswap pallet supports multiple hooks which are triggerred on certain operations: +//! - on_liquidity_changed - called when liquidity is added or removed from the pool +//! - on_trade - called when trade is executed +//! +//! This is currently used to update on-chain oracle. +//! //! ### Terminology //! //! * **LP** - liquidity provider @@ -37,7 +49,6 @@ //! //! When LP decides to withdraw liquidity, it receives selected asset. //! - #![cfg_attr(not(feature = "std"), no_std)] extern crate core; @@ -75,8 +86,8 @@ mod benchmarks; #[cfg(feature = "runtime-benchmarks")] pub use crate::types::BenchmarkHelper; -/// Stableswap share token and account id identifier. -/// Used as identifier to create share token unique names and account ids. +/// Stableswap account id identifier. +/// Used as identifier to create share token unique names and account id. pub const POOL_IDENTIFIER: &[u8] = b"sts"; pub const MAX_ASSETS_IN_POOL: u32 = 5; @@ -125,7 +136,7 @@ pub mod pallet { /// Account ID constructor - pool account are derived from unique pool id type ShareAccountId: AccountIdFor; - /// Asset registry mechanism + /// Asset registry mechanism to check if asset is registered and retrieve asset decimals. type AssetInspection: InspectRegistry; /// The origin which can create a new pool @@ -177,7 +188,7 @@ pub mod pallet { amplification: NonZeroU16, fee: Permill, }, - /// Pool parameters has been updated. + /// Pool fee has been updated. FeeUpdated { pool_id: T::AssetId, fee: Permill }, /// Liquidity of an asset was added to a pool. LiquidityAdded { @@ -215,7 +226,7 @@ pub mod pallet { fee: Balance, }, - /// Aseet's tradable state has been updated. + /// Asset's tradable state has been updated. TradableStateUpdated { pool_id: T::AssetId, asset_id: T::AssetId, @@ -250,9 +261,6 @@ pub mod pallet { /// Asset is not in the pool. AssetNotInPool, - /// Asset is already in the pool. - AssetInPool, - /// Share asset is not registered in Registry. ShareAssetNotRegistered, @@ -289,9 +297,6 @@ pub mod pallet { /// Initial liquidity of asset must be > 0. InvalidInitialLiquidity, - /// Account balance is too low. - BalanceTooLow, - /// Amplification is outside configured range. InvalidAmplification, @@ -307,10 +312,7 @@ pub mod pallet { /// New amplification is equal to the previous value. SameAmplification, - /// Desired amount not reached. - MinimumAmountNotReached, - - /// Slippage + /// Slippage protection. SlippageLimit, /// Failed to retrieve asset decimals. @@ -319,7 +321,7 @@ pub mod pallet { #[pallet::call] impl Pallet { - /// Create a stableswap pool with given list of asset + /// Create a stable pool with given list of assets. /// /// All assets must be correctly registered in `T::AssetRegistry`. /// Note that this does not seed the pool with liquidity. Use `add_liquidity` to provide @@ -366,9 +368,7 @@ pub mod pallet { Ok(()) } - /// Update pool's fees. - /// - /// Updates pool's trade fee and/or withdraw fee. + /// Update pool's fee. /// /// if pool does not exist, `PoolNotFound` is returned. /// @@ -454,11 +454,11 @@ pub mod pallet { /// Add liquidity to selected pool. /// - /// First call of `add_liquidity` adds "initial liquidity" of all assets. + /// First call of `add_liquidity` must provide "initial liquidity" of all assets. /// /// If there is liquidity already in the pool, LP can provide liquidity of any number of pool assets. /// - /// LP must have sufficient amount of each assets. + /// LP must have sufficient amount of each asset. /// /// Origin is given corresponding amount of shares. /// @@ -493,6 +493,10 @@ pub mod pallet { /// Add liquidity to selected pool given exact amount of shares to receive. /// + /// Similar to `add_liquidity` but LP specifies exact amount of shares to receive. + /// + /// This functionality is used mainly by on-chain routing when a swap between Omnipool asset and stable asset is performed. + /// /// Parameters: /// - `origin`: liquidity provider /// - `pool_id`: Pool Id @@ -528,7 +532,7 @@ pub mod pallet { /// /// Withdraws liquidity of selected asset from a pool. /// - /// Share amount is burn and LP receives corresponding amount of chosen asset. + /// Share amount is burned and LP receives corresponding amount of chosen asset. /// /// Withdraw fee is applied to the asset amount. /// @@ -557,23 +561,23 @@ pub mod pallet { Self::is_asset_allowed(pool_id, asset_id, Tradability::REMOVE_LIQUIDITY), Error::::NotAllowed ); - ensure!(share_amount > Balance::zero(), Error::::InvalidAssetAmount); let current_share_balance = T::Currency::free_balance(pool_id, &who); - ensure!(current_share_balance >= share_amount, Error::::InsufficientShares); - ensure!( current_share_balance == share_amount || current_share_balance.saturating_sub(share_amount) >= T::MinPoolLiquidity::get(), Error::::InsufficientShareBalance ); + // Retrive pool state. let pool = Pools::::get(pool_id).ok_or(Error::::PoolNotFound)?; let asset_idx = pool.find_asset(asset_id).ok_or(Error::::AssetNotInPool)?; let pool_account = Self::pool_account(pool_id); - let balances = pool.balances::(&pool_account).ok_or(Error::::UnknownDecimals)?; + let initial_reserves = pool + .reserves_with_decimals::(&pool_account) + .ok_or(Error::::UnknownDecimals)?; let share_issuance = T::Currency::total_issuance(pool_id); ensure!( @@ -583,8 +587,10 @@ pub mod pallet { ); let amplification = Self::get_amplification(&pool); + + //Calculate how much asset user will receive. Note that the fee is already subtracted from the amount. let (amount, fee) = hydra_dx_math::stableswap::calculate_withdraw_one_asset::( - &balances, + &initial_reserves, share_amount, asset_idx, share_issuance, @@ -593,37 +599,14 @@ pub mod pallet { ) .ok_or(ArithmeticError::Overflow)?; - ensure!(amount >= min_amount_out, Error::::MinimumAmountNotReached); + ensure!(amount >= min_amount_out, Error::::SlippageLimit); + // Burn shares and transfer asset to user. T::Currency::withdraw(pool_id, &who, share_amount)?; T::Currency::transfer(asset_id, &pool_account, &who, amount)?; - let updated_share_issuance = T::Currency::total_issuance(pool_id); - let updated_balances = pool.balances::(&pool_account).ok_or(Error::::UnknownDecimals)?; - - let share_prices = hydra_dx_math::stableswap::calculate_share_prices::( - &updated_balances, - amplification, - updated_share_issuance, - ) - .ok_or(ArithmeticError::Overflow)?; - - let assets = pool.assets.clone(); - - let state = PoolState { - assets: pool.assets.into_inner(), - before: balances.into_iter().map(|v| v.into()).collect(), - after: updated_balances.into_iter().map(|v| v.into()).collect(), - delta: assets - .into_iter() - .map(|v| if v == asset_id { amount } else { 0 }) - .collect(), - issuance_before: share_issuance, - issuance_after: updated_share_issuance, - share_prices, - }; - - T::Hooks::on_liquidity_changed(pool_id, state)?; + // All done and updated. let's call the on_liquidity_changed hook. + Self::call_on_liquidity_change_hook(pool_id, &initial_reserves, share_issuance)?; Self::deposit_event(Event::LiquidityRemoved { pool_id, @@ -638,6 +621,8 @@ pub mod pallet { /// Remove liquidity from selected pool by specifying exact amount of asset to receive. /// + /// Similar to `remove_liquidity_one_asset` but LP specifies exact amount of asset to receive instead of share amount. + /// /// Parameters: /// - `origin`: liquidity provider /// - `pool_id`: Pool Id @@ -663,17 +648,21 @@ pub mod pallet { Self::is_asset_allowed(pool_id, asset_id, Tradability::REMOVE_LIQUIDITY), Error::::NotAllowed ); - ensure!(amount > Balance::zero(), Error::::InvalidAssetAmount); + + // Retrieve pool state. let pool = Pools::::get(pool_id).ok_or(Error::::PoolNotFound)?; let asset_idx = pool.find_asset(asset_id).ok_or(Error::::AssetNotInPool)?; let pool_account = Self::pool_account(pool_id); - let balances = pool.balances::(&pool_account).ok_or(Error::::UnknownDecimals)?; + let initial_reserves = pool + .reserves_with_decimals::(&pool_account) + .ok_or(Error::::UnknownDecimals)?; let share_issuance = T::Currency::total_issuance(pool_id); let amplification = Self::get_amplification(&pool); + // Calculate how much shares user needs to provide to receive `amount` of asset. let shares = hydra_dx_math::stableswap::calculate_shares_for_amount::( - &balances, + &initial_reserves, asset_idx, amount, amplification, @@ -685,52 +674,31 @@ pub mod pallet { ensure!(shares <= max_share_amount, Error::::SlippageLimit); let current_share_balance = T::Currency::free_balance(pool_id, &who); - ensure!( current_share_balance == shares || current_share_balance.saturating_sub(shares) >= T::MinPoolLiquidity::get(), Error::::InsufficientShareBalance ); + + // Burn shares and transfer asset to user. T::Currency::withdraw(pool_id, &who, shares)?; T::Currency::transfer(asset_id, &pool_account, &who, amount)?; - let updated_share_issuance = T::Currency::total_issuance(pool_id); - let updated_balances = pool.balances::(&pool_account).ok_or(Error::::UnknownDecimals)?; - let share_prices = hydra_dx_math::stableswap::calculate_share_prices::( - &updated_balances, - amplification, - updated_share_issuance, - ) - .ok_or(ArithmeticError::Overflow)?; - - let assets = pool.assets.clone(); - let state = PoolState { - assets: pool.assets.into_inner(), - before: balances.into_iter().map(|v| v.into()).collect(), - after: updated_balances.into_iter().map(|v| v.into()).collect(), - delta: assets - .into_iter() - .map(|v| if v == asset_id { amount } else { 0 }) - .collect(), - issuance_before: share_issuance, - issuance_after: updated_share_issuance, - share_prices, - }; - - T::Hooks::on_liquidity_changed(pool_id, state)?; + // All done and updated. let's call the on_liquidity_changed hook. + Self::call_on_liquidity_change_hook(pool_id, &initial_reserves, share_issuance)?; Self::deposit_event(Event::LiquidityRemoved { pool_id, who, shares, amounts: vec![AssetAmount { asset_id, amount }], - fee: 0u128, // TODO: Fix + fee: 0u128, // dev note: figure out the actual fee amount in this case. For now, we dont need it. }); Ok(()) } - /// Execute a swap of `asset_in` for `asset_out` by specifying how much to put in. + /// Execute a swap of `asset_in` for `asset_out`. /// /// Parameters: /// - `origin`: origin of the caller @@ -774,8 +742,9 @@ pub mod pallet { let pool = Pools::::get(pool_id).ok_or(Error::::PoolNotFound)?; let pool_account = Self::pool_account(pool_id); - let amplification = Self::get_amplification(&pool); - let initial_reserves = pool.balances::(&pool_account).ok_or(Error::::UnknownDecimals)?; + let initial_reserves = pool + .reserves_with_decimals::(&pool_account) + .ok_or(Error::::UnknownDecimals)?; let (amount_out, fee_amount) = Self::calculate_out_amount(pool_id, asset_in, asset_out, amount_in)?; ensure!(amount_out >= min_buy_amount, Error::::BuyLimitNotReached); @@ -783,39 +752,8 @@ pub mod pallet { T::Currency::transfer(asset_in, &who, &pool_account, amount_in)?; T::Currency::transfer(asset_out, &pool_account, &who, amount_out)?; - let share_issuance = T::Currency::total_issuance(pool_id); - let assets = pool.assets.clone(); - - let updated_balances = pool.balances::(&pool_account).ok_or(Error::::UnknownDecimals)?; - let share_prices = hydra_dx_math::stableswap::calculate_share_prices::( - &updated_balances, - amplification, - share_issuance, - ) - .ok_or(ArithmeticError::Overflow)?; - - let state = PoolState { - assets: pool.assets.into_inner(), - before: initial_reserves.into_iter().map(|v| v.into()).collect(), - after: updated_balances.into_iter().map(|v| v.into()).collect(), - delta: assets - .into_iter() - .map(|v| { - if v == asset_in { - amount_in - } else if v == asset_out { - amount_out - } else { - 0 - } - }) - .collect(), - issuance_before: share_issuance, - issuance_after: share_issuance, - share_prices, - }; - - T::Hooks::on_trade(pool_id, asset_in, asset_out, state)?; + //All done and updated. Let's call on_trade hook. + Self::call_on_trade_hook(pool_id, asset_in, asset_out, &initial_reserves)?; Self::deposit_event(Event::SellExecuted { who, @@ -830,7 +768,7 @@ pub mod pallet { Ok(()) } - /// Execute a swap of `asset_in` for `asset_out` by specifying how much to get out. + /// Execute a swap of `asset_in` for `asset_out`. /// /// Parameters: /// - `origin`: @@ -869,8 +807,9 @@ pub mod pallet { let pool = Pools::::get(pool_id).ok_or(Error::::PoolNotFound)?; let pool_account = Self::pool_account(pool_id); - let amplification = Self::get_amplification(&pool); - let initial_reserves = pool.balances::(&pool_account).ok_or(Error::::UnknownDecimals)?; + let initial_reserves = pool + .reserves_with_decimals::(&pool_account) + .ok_or(Error::::UnknownDecimals)?; let (amount_in, fee_amount) = Self::calculate_in_amount(pool_id, asset_in, asset_out, amount_out)?; @@ -886,39 +825,8 @@ pub mod pallet { T::Currency::transfer(asset_in, &who, &pool_account, amount_in)?; T::Currency::transfer(asset_out, &pool_account, &who, amount_out)?; - let share_issuance = T::Currency::total_issuance(pool_id); - let assets = pool.assets.clone(); - - let updated_balances = pool.balances::(&pool_account).ok_or(Error::::UnknownDecimals)?; - let share_prices = hydra_dx_math::stableswap::calculate_share_prices::( - &updated_balances, - amplification, - share_issuance, - ) - .ok_or(ArithmeticError::Overflow)?; - - let state = PoolState { - assets: pool.assets.into_inner(), - before: initial_reserves.iter().map(|v| v.into()).collect(), - after: updated_balances.iter().map(|v| v.into()).collect(), - delta: assets - .into_iter() - .map(|v| { - if v == asset_in { - amount_in - } else if v == asset_out { - amount_out - } else { - 0 - } - }) - .collect(), - issuance_before: share_issuance, - issuance_after: share_issuance, - share_prices, - }; - - T::Hooks::on_trade(pool_id, asset_in, asset_out, state)?; + //All done and updated. Let's call on_trade_hook. + Self::call_on_trade_hook(pool_id, asset_in, asset_out, &initial_reserves)?; Self::deposit_event(Event::BuyExecuted { who, @@ -966,6 +874,8 @@ pub mod pallet { } impl Pallet { + /// Calculates out amount given in amount. + /// Returns (out_amount, fee_amount) on success. Note that fee amount is already subtracted from the out amount. fn calculate_out_amount( pool_id: T::AssetId, asset_in: T::AssetId, @@ -978,14 +888,19 @@ impl Pallet { let index_out = pool.find_asset(asset_out).ok_or(Error::::AssetNotInPool)?; let pool_account = Self::pool_account(pool_id); - let balances = pool.balances::(&pool_account).ok_or(Error::::UnknownDecimals)?; + let initial_reserves = pool + .reserves_with_decimals::(&pool_account) + .ok_or(Error::::UnknownDecimals)?; - ensure!(!balances[index_in].is_zero(), Error::::InsufficientLiquidity); - ensure!(!balances[index_out].is_zero(), Error::::InsufficientLiquidity); + ensure!(!initial_reserves[index_in].is_zero(), Error::::InsufficientLiquidity); + ensure!( + !initial_reserves[index_out].is_zero(), + Error::::InsufficientLiquidity + ); let amplification = Self::get_amplification(&pool); hydra_dx_math::stableswap::calculate_out_given_in_with_fee::( - &balances, + &initial_reserves, index_in, index_out, amount_in, @@ -995,6 +910,8 @@ impl Pallet { .ok_or_else(|| ArithmeticError::Overflow.into()) } + /// Calculates in amount given out amount. + /// Returns (in_amount, fee_amount) on success. Note that fee amount is already added to the in amount. fn calculate_in_amount( pool_id: T::AssetId, asset_in: T::AssetId, @@ -1007,17 +924,19 @@ impl Pallet { let index_out = pool.find_asset(asset_out).ok_or(Error::::AssetNotInPool)?; let pool_account = Self::pool_account(pool_id); - let balances = pool.balances::(&pool_account).ok_or(Error::::UnknownDecimals)?; + let initial_reserves = pool + .reserves_with_decimals::(&pool_account) + .ok_or(Error::::UnknownDecimals)?; ensure!( - balances[index_out].amount > amount_out, + initial_reserves[index_out].amount > amount_out, Error::::InsufficientLiquidity ); - ensure!(!balances[index_in].is_zero(), Error::::InsufficientLiquidity); + ensure!(!initial_reserves[index_in].is_zero(), Error::::InsufficientLiquidity); let amplification = Self::get_amplification(&pool); hydra_dx_math::stableswap::calculate_in_given_out_with_fee::( - &balances, + &initial_reserves, index_in, index_out, amount_out, @@ -1126,6 +1045,7 @@ impl Pallet { added_amounts.push(0); } } + // If something is left in added_assets, it means that user provided liquidity for asset that is not in the pool. ensure!(added_assets.is_empty(), Error::::AssetNotInPool); let amplification = Self::get_amplification(&pool); @@ -1153,25 +1073,8 @@ impl Pallet { T::Currency::transfer(asset.asset_id, who, &pool_account, asset.amount)?; } - let updated_issuance = share_issuance.saturating_add(share_amount); - let share_prices = hydra_dx_math::stableswap::calculate_share_prices::( - &updated_reserves, - amplification, - updated_issuance, - ) - .ok_or(ArithmeticError::Overflow)?; - - let state = PoolState { - assets: pool.assets.into_inner(), - before: initial_reserves.into_iter().map(|v| v.into()).collect(), - after: updated_reserves.into_iter().map(|v| v.into()).collect(), - delta: added_amounts, - issuance_before: share_issuance, - issuance_after: updated_issuance, - share_prices, - }; - - T::Hooks::on_liquidity_changed(pool_id, state)?; + // All done and updated. let's call the on_liquidity_changed hook. + Self::call_on_liquidity_change_hook(pool_id, &initial_reserves, share_issuance)?; Ok(share_amount) } @@ -1184,20 +1087,26 @@ impl Pallet { asset_id: T::AssetId, max_asset_amount: Balance, ) -> Result { + ensure!( + Self::is_asset_allowed(pool_id, asset_id, Tradability::ADD_LIQUIDITY), + Error::::NotAllowed + ); let pool = Pools::::get(pool_id).ok_or(Error::::PoolNotFound)?; let asset_idx = pool.find_asset(asset_id).ok_or(Error::::AssetNotInPool)?; let share_issuance = T::Currency::total_issuance(pool_id); let amplification = Self::get_amplification(&pool); let pool_account = Self::pool_account(pool_id); - let balances = pool.balances::(&pool_account).ok_or(Error::::UnknownDecimals)?; + let initial_reserves = pool + .reserves_with_decimals::(&pool_account) + .ok_or(Error::::UnknownDecimals)?; // Ensure that initial liquidity has been already provided - for reserve in balances.iter() { + for reserve in initial_reserves.iter() { ensure!(!reserve.amount.is_zero(), Error::::InvalidInitialLiquidity); } let (amount_in, _) = hydra_dx_math::stableswap::calculate_add_one_asset::( - &balances, + &initial_reserves, shares, asset_idx, share_issuance, @@ -1219,31 +1128,8 @@ impl Pallet { T::Currency::deposit(pool_id, who, shares)?; T::Currency::transfer(asset_id, who, &pool_account, amount_in)?; - let updated_balances = pool.balances::(&pool_account).ok_or(Error::::UnknownDecimals)?; - let updated_issuance = share_issuance.saturating_add(shares); - let share_prices = hydra_dx_math::stableswap::calculate_share_prices::( - &updated_balances, - amplification, - updated_issuance, - ) - .ok_or(ArithmeticError::Overflow)?; - - let state = PoolState { - assets: pool.assets.clone().into(), - before: balances.into_iter().map(|v| v.into()).collect(), - after: updated_balances.into_iter().map(|v| v.into()).collect(), - delta: pool - .assets - .iter() - .enumerate() - .map(|(idx, _)| if idx == asset_idx { amount_in } else { 0 }) - .collect(), - issuance_before: share_issuance, - issuance_after: updated_issuance, - share_prices, - }; - - T::Hooks::on_liquidity_changed(pool_id, state)?; + //All done and update. let's call the on_liquidity_changed hook. + Self::call_on_liquidity_change_hook(pool_id, &initial_reserves, share_issuance)?; Ok(amount_in) } @@ -1337,4 +1223,66 @@ impl Pallet { Ok(share_amount) } + + // Trigger on_liquidity_changed hook. Initial reserves and issuance are required to calculate delta. + // We need new updated reserves and new share price of each asset in pool, so for this, we can simply query the storage after the update. + fn call_on_liquidity_change_hook( + pool_id: T::AssetId, + initial_reserves: &[AssetReserve], + initial_issuance: Balance, + ) -> DispatchResult { + let state = Self::get_pool_state(pool_id, initial_reserves, Some(initial_issuance))?; + T::Hooks::on_liquidity_changed(pool_id, state) + } + + // Trigger on_trade hook. Initial reserves are required to calculate delta. + // We need new updated reserves and new share price of each asset in pool, so for this, we can simply query the storage after the update. + fn call_on_trade_hook( + pool_id: T::AssetId, + asset_in: T::AssetId, + asset_out: T::AssetId, + initial_reserves: &[AssetReserve], + ) -> DispatchResult { + let state = Self::get_pool_state(pool_id, initial_reserves, None)?; + T::Hooks::on_trade(pool_id, asset_in, asset_out, state) + } + + // Get pool state info for on_liquidity_changed and on_trade hooks. + fn get_pool_state( + pool_id: T::AssetId, + initial_reserves: &[AssetReserve], + initial_issuance: Option, + ) -> Result, DispatchError> { + let pool = Pools::::get(pool_id).ok_or(Error::::PoolNotFound)?; + let pool_account = Self::pool_account(pool_id); + let amplification = Self::get_amplification(&pool); + let share_issuance = T::Currency::total_issuance(pool_id); + let updated_reserves = pool + .reserves_with_decimals::(&pool_account) + .ok_or(Error::::UnknownDecimals)?; + let share_prices = hydra_dx_math::stableswap::calculate_share_prices::( + &updated_reserves, + amplification, + share_issuance, + ) + .ok_or(ArithmeticError::Overflow)?; + + let deltas: Vec = initial_reserves + .iter() + .zip(updated_reserves.iter()) + .map(|(initial, updated)| initial.amount.abs_diff(updated.amount)) + .collect(); + + let state = PoolState { + assets: pool.assets.into_inner(), + before: initial_reserves.iter().map(|v| v.into()).collect(), + after: updated_reserves.iter().map(|v| v.into()).collect(), + delta: deltas, + issuance_before: initial_issuance.unwrap_or(share_issuance), + issuance_after: share_issuance, + share_prices, + }; + + Ok(state) + } } diff --git a/pallets/stableswap/src/tests/creation.rs b/pallets/stableswap/src/tests/creation.rs index 4e2490c5d..d191f6a0a 100644 --- a/pallets/stableswap/src/tests/creation.rs +++ b/pallets/stableswap/src/tests/creation.rs @@ -367,3 +367,33 @@ fn create_pool_should_add_account_to_whitelist() { assert!(DUSTER_WHITELIST.with(|v| v.borrow().contains(&pool_account))); }); } + +#[test] +fn create_pool_should_fail_when_number_of_assets_exceeds_maximum() { + let asset_a: AssetId = 1; + let asset_b: AssetId = 2; + let asset_c: AssetId = 3; + let asset_d: AssetId = 4; + let asset_e: AssetId = 5; + let asset_f: AssetId = 6; + let pool_id: AssetId = 100; + + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, 1, 200 * ONE), (ALICE, 2, 200 * ONE)]) + .with_registered_asset("pool".as_bytes().to_vec(), pool_id, 12) + .with_registered_asset("one".as_bytes().to_vec(), asset_a, 12) + .with_registered_asset("two".as_bytes().to_vec(), asset_b, 12) + .build() + .execute_with(|| { + assert_noop!( + Stableswap::create_pool( + RuntimeOrigin::root(), + pool_id, + vec![asset_a, asset_b, asset_c, asset_d, asset_e, asset_f], + 100, + Permill::from_percent(0), + ), + Error::::MaxAssetsExceeded + ); + }); +} diff --git a/pallets/stableswap/src/tests/invariants.rs b/pallets/stableswap/src/tests/invariants.rs index 52c0995f4..83e702a87 100644 --- a/pallets/stableswap/src/tests/invariants.rs +++ b/pallets/stableswap/src/tests/invariants.rs @@ -9,6 +9,7 @@ use hydra_dx_math::stableswap::calculate_d; use hydra_dx_math::stableswap::types::AssetReserve; use proptest::prelude::*; use proptest::proptest; +use sp_core::U256; use sp_runtime::traits::BlockNumberProvider; pub const ONE: Balance = 1_000_000_000_000; @@ -19,6 +20,10 @@ fn trade_amount() -> impl Strategy { 1_000_000..100_000 * ONE } +fn share_amount() -> impl Strategy { + 1_000 * ONE * 1_000_000..100_000 * ONE * 1_000_000 +} + fn asset_reserve() -> impl Strategy { RESERVE_RANGE.0..RESERVE_RANGE.1 } @@ -112,6 +117,156 @@ proptest! { } } +proptest! { + #![proptest_config(ProptestConfig::with_cases(1000))] + #[test] + fn test_share_price_in_add_withdraw_asset( + initial_liquidity in asset_reserve(), + added_liquidity in trade_amount(), + amplification in some_amplification(), + trade_fee in trade_fee() + ) { + let asset_a: AssetId = 1000; + let asset_b: AssetId = 2000; + + ExtBuilder::default() + .with_endowed_accounts(vec![ + (BOB, asset_a, added_liquidity), + (BOB, asset_b, added_liquidity * 1000), + (ALICE, asset_a, initial_liquidity), + (ALICE, asset_b, initial_liquidity), + ]) + .with_registered_asset("one".as_bytes().to_vec(), asset_a,12) + .with_registered_asset("two".as_bytes().to_vec(), asset_b,12) + .with_pool( + ALICE, + PoolInfo:: { + assets: vec![asset_a,asset_b].try_into().unwrap(), + initial_amplification: amplification, + final_amplification: amplification, + initial_block: 0, + final_block: 0, + fee: trade_fee, + }, + InitialLiquidity{ account: ALICE, + assets: vec![ + AssetAmount::new(asset_a, initial_liquidity), + AssetAmount::new(asset_b, initial_liquidity), + ]}, + ) + .build() + .execute_with(|| { + let pool_id = get_pool_id_at(0); + let pool_account = pool_account(pool_id); + + let share_price_initial = get_share_price(pool_id, 0); + let initial_shares = Tokens::total_issuance(pool_id); + assert_ok!(Stableswap::add_liquidity( + RuntimeOrigin::signed(BOB), + pool_id, + vec![ + AssetAmount::new(asset_a, added_liquidity), + ] + )); + let final_shares = Tokens::total_issuance(pool_id); + let delta_s = final_shares - initial_shares; + let exec_price = FixedU128::from_rational(added_liquidity , delta_s); + assert!(share_price_initial <= exec_price); + + let share_price_initial = get_share_price(pool_id, 0); + let a_initial = Tokens::free_balance(asset_a, &pool_account); + assert_ok!(Stableswap::withdraw_asset_amount( + RuntimeOrigin::signed(BOB), + pool_id, + asset_a, + added_liquidity / 2, + u128::MAX, + )); + let a_final = Tokens::free_balance(asset_a, &pool_account); + let delta_a = a_initial - a_final; + let exec_price = FixedU128::from_rational(delta_a, delta_s); + assert!(share_price_initial >= exec_price); + }); + } +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(1000))] + #[test] + fn test_share_price_in_add_shares_withdraw_asset( + initial_liquidity in asset_reserve(), + added_liquidity in trade_amount(), + shares_amount in share_amount(), + amplification in some_amplification(), + trade_fee in trade_fee() + ) { + let asset_a: AssetId = 1000; + let asset_b: AssetId = 2000; + + ExtBuilder::default() + .with_endowed_accounts(vec![ + (BOB, asset_a, added_liquidity * 1_000_000_000), + (BOB, asset_b, added_liquidity * 1000), + (ALICE, asset_a, initial_liquidity), + (ALICE, asset_b, initial_liquidity), + ]) + .with_registered_asset("one".as_bytes().to_vec(), asset_a,12) + .with_registered_asset("two".as_bytes().to_vec(), asset_b,12) + .with_pool( + ALICE, + PoolInfo:: { + assets: vec![asset_a,asset_b].try_into().unwrap(), + initial_amplification: amplification, + final_amplification: amplification, + initial_block: 0, + final_block: 0, + fee: trade_fee, + }, + InitialLiquidity{ account: ALICE, + assets: vec![ + AssetAmount::new(asset_a, initial_liquidity), + AssetAmount::new(asset_b, initial_liquidity), + ]}, + ) + .build() + .execute_with(|| { + let pool_id = get_pool_id_at(0); + let pool_account = pool_account(pool_id); + + let share_price_initial = get_share_price(pool_id, 0); + let initial_shares = Tokens::total_issuance(pool_id); + let initial_a = Tokens::free_balance(asset_a, &BOB); + assert_ok!(Stableswap::add_liquidity_shares( + RuntimeOrigin::signed(BOB), + pool_id, + shares_amount, + asset_a, + u128::MAX, + )); + let final_a = Tokens::free_balance(asset_a, &BOB); + let added_liquidity = initial_a - final_a; + let final_shares = Tokens::total_issuance(pool_id); + let delta_s = final_shares - initial_shares; + let exec_price = FixedU128::from_rational(added_liquidity , delta_s); + assert!(share_price_initial <= exec_price); + + let share_price_initial = get_share_price(pool_id, 0); + let a_initial = Tokens::free_balance(asset_a, &pool_account); + assert_ok!(Stableswap::withdraw_asset_amount( + RuntimeOrigin::signed(BOB), + pool_id, + asset_a, + added_liquidity / 2, + u128::MAX, + )); + let a_final = Tokens::free_balance(asset_a, &pool_account); + let delta_a = a_initial - a_final; + let exec_price = FixedU128::from_rational(delta_a, delta_s); + assert!(share_price_initial >= exec_price); + }); + } +} + proptest! { #![proptest_config(ProptestConfig::with_cases(1000))] #[test] @@ -612,3 +767,441 @@ proptest! { }); } } + +fn to_precision(value: hydra_dx_math::types::Balance, precision: u8) -> hydra_dx_math::types::Balance { + value * 10u128.pow(precision as u32) +} + +fn decimals() -> impl Strategy { + prop_oneof![Just(6), Just(8), Just(10), Just(12), Just(18)] +} + +const RESERVE_RANGE_NO_DECIMALS: (hydra_dx_math::types::Balance, hydra_dx_math::types::Balance) = + (10_000, 1_000_000_000); +fn reserve() -> impl Strategy { + RESERVE_RANGE_NO_DECIMALS.0..RESERVE_RANGE_NO_DECIMALS.1 +} + +fn balanced_pool(size: usize) -> impl Strategy> { + let reserve_amount = reserve(); + prop::collection::vec( + (reserve_amount, decimals()).prop_map(|(v, dec)| AssetReserve::new(to_precision(v, dec), dec)), + size, + ) +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(1000))] + #[test] + fn add_remove_liquidity_invariants( + pool in balanced_pool(3), + liquidity_to_add in reserve(), + amplification in some_amplification(), + ) { + let trade_fee = Permill::from_percent(0); + + let assets_to_register: Vec<(Vec, u32, u8)> = pool.iter().enumerate().map(|(asset_id, v)| ( asset_id.to_string().into_bytes(), asset_id as u32, v.decimals)).collect(); + + let pool_assets: Vec = assets_to_register.iter().map(|(_, asset_id, _)| *asset_id).collect(); + let initial_liquidity: Vec> = pool.iter().enumerate().map(|(asset_id, v)| AssetAmount::new(asset_id as AssetId, v.amount)).collect(); + let mut endowed_accounts: Vec<(AccountId, AssetId, hydra_dx_math::types::Balance)> = pool.iter().enumerate().map(|(asset_id, v)| (ALICE, asset_id as u32, v.amount)).collect(); + + let (_, asset_a, dec)= assets_to_register.first().unwrap().clone(); + let added_liquidity = to_precision(liquidity_to_add , dec); + endowed_accounts.push((BOB, asset_a, added_liquidity * 1000)); + + ExtBuilder::default() + .with_endowed_accounts(endowed_accounts) + .with_registered_assets(assets_to_register) + .with_pool( + ALICE, + PoolInfo:: { + assets: pool_assets.try_into().unwrap(), + initial_amplification: amplification, + final_amplification: amplification, + initial_block: 0, + final_block: 0, + fee: trade_fee, + }, + InitialLiquidity{ account: ALICE, + assets: initial_liquidity,} + ) + .build() + .execute_with(|| { + let pool_id = get_pool_id_at(0); + let pool_account = pool_account(pool_id); + + let pool = Pools::::get(pool_id).unwrap(); + let initial_reserves = pool.reserves_with_decimals::(&pool_account).unwrap(); + let initial_d = hydra_dx_math::stableswap::calculate_d::<128u8>(&initial_reserves, amplification.get().into()).unwrap(); + + let share_price_initial = get_share_price(pool_id, 0); + let initial_shares = Tokens::total_issuance(pool_id); + assert_ok!(Stableswap::add_liquidity( + RuntimeOrigin::signed(BOB), + pool_id, + vec![ + AssetAmount::new(asset_a, added_liquidity), + ] + )); + let final_shares = Tokens::total_issuance(pool_id); + let delta_s = final_shares - initial_shares; + let exec_price = FixedU128::from_rational(added_liquidity , delta_s); + assert!(share_price_initial <= exec_price); + + let pool = Pools::::get(pool_id).unwrap(); + let final_reserves = pool.reserves_with_decimals::(&pool_account).unwrap(); + let intermediate_d = hydra_dx_math::stableswap::calculate_d::<128u8>(&final_reserves, amplification.get().into()).unwrap(); + assert!(intermediate_d > initial_d); + + let d = U256::from(initial_d) ; + let d_plus = U256::from(intermediate_d) ; + let s = U256::from(initial_shares) ; + let s_plus = U256::from(final_shares) ; + assert!(d_plus * s >= d * s_plus); + assert!(d * s_plus >= (d_plus - 10u128.pow(18)) * s); + + let share_price_initial = get_share_price(pool_id, 0); + let a_initial = Tokens::free_balance(asset_a, &pool_account); + assert_ok!(Stableswap::remove_liquidity_one_asset( + RuntimeOrigin::signed(BOB), + pool_id, + asset_a, + delta_s, + 0u128, + )); + let a_final = Tokens::free_balance(asset_a, &pool_account); + let delta_a = a_initial - a_final; + let exec_price = FixedU128::from_rational(delta_a, delta_s); + assert!(share_price_initial >= exec_price); + + let final_shares = Tokens::total_issuance(pool_id); + let pool = Pools::::get(pool_id).unwrap(); + let final_reserves = pool.reserves_with_decimals::(&pool_account).unwrap(); + let final_d = hydra_dx_math::stableswap::calculate_d::<128u8>(&final_reserves, amplification.get().into()).unwrap(); + assert!(final_d < intermediate_d); + + let d = d_plus; + let d_plus = U256::from(final_d); + let s = s_plus; + let s_plus = U256::from(final_shares); + assert!(d * (s_plus + 10u128.pow(18) ) >= d_plus * s); + assert!(d_plus * s >= d * s_plus); + + }); + } +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(1000))] + #[test] + fn add_remove_liquidity_invariants_with_5_assets( + pool in balanced_pool(5), + liquidity_to_add in reserve(), + amplification in some_amplification(), + ) { + let trade_fee = Permill::from_percent(0); + + let assets_to_register: Vec<(Vec, u32, u8)> = pool.iter().enumerate().map(|(asset_id, v)| ( asset_id.to_string().into_bytes(), asset_id as u32, v.decimals)).collect(); + + let pool_assets: Vec = assets_to_register.iter().map(|(_, asset_id, _)| *asset_id).collect(); + let initial_liquidity: Vec> = pool.iter().enumerate().map(|(asset_id, v)| AssetAmount::new(asset_id as AssetId, v.amount)).collect(); + let mut endowed_accounts: Vec<(AccountId, AssetId, hydra_dx_math::types::Balance)> = pool.iter().enumerate().map(|(asset_id, v)| (ALICE, asset_id as u32, v.amount)).collect(); + + let (_, asset_a, dec)= assets_to_register.first().unwrap().clone(); + let added_liquidity = to_precision(liquidity_to_add , dec); + endowed_accounts.push((BOB, asset_a, added_liquidity * 1000)); + + ExtBuilder::default() + .with_endowed_accounts(endowed_accounts) + .with_registered_assets(assets_to_register) + .with_pool( + ALICE, + PoolInfo:: { + assets: pool_assets.try_into().unwrap(), + initial_amplification: amplification, + final_amplification: amplification, + initial_block: 0, + final_block: 0, + fee: trade_fee, + }, + InitialLiquidity{ account: ALICE, + assets: initial_liquidity,} + ) + .build() + .execute_with(|| { + let pool_id = get_pool_id_at(0); + let pool_account = pool_account(pool_id); + + let pool = Pools::::get(pool_id).unwrap(); + let initial_reserves = pool.reserves_with_decimals::(&pool_account).unwrap(); + let initial_d = hydra_dx_math::stableswap::calculate_d::<128u8>(&initial_reserves, amplification.get().into()).unwrap(); + + let share_price_initial = get_share_price(pool_id, 0); + let initial_shares = Tokens::total_issuance(pool_id); + assert_ok!(Stableswap::add_liquidity( + RuntimeOrigin::signed(BOB), + pool_id, + vec![ + AssetAmount::new(asset_a, added_liquidity), + ] + )); + let final_shares = Tokens::total_issuance(pool_id); + let delta_s = final_shares - initial_shares; + let exec_price = FixedU128::from_rational(added_liquidity , delta_s); + assert!(share_price_initial <= exec_price); + + let pool = Pools::::get(pool_id).unwrap(); + let final_reserves = pool.reserves_with_decimals::(&pool_account).unwrap(); + let intermediate_d = hydra_dx_math::stableswap::calculate_d::<128u8>(&final_reserves, amplification.get().into()).unwrap(); + assert!(intermediate_d > initial_d); + + let d = U256::from(initial_d) ; + let d_plus = U256::from(intermediate_d) ; + let s = U256::from(initial_shares) ; + let s_plus = U256::from(final_shares) ; + assert!(d_plus * s >= d * s_plus); + assert!(d * s_plus >= (d_plus - 10u128.pow(18)) * s); + + let share_price_initial = get_share_price(pool_id, 0); + let a_initial = Tokens::free_balance(asset_a, &pool_account); + assert_ok!(Stableswap::remove_liquidity_one_asset( + RuntimeOrigin::signed(BOB), + pool_id, + asset_a, + delta_s, + 0u128, + )); + let a_final = Tokens::free_balance(asset_a, &pool_account); + let delta_a = a_initial - a_final; + let exec_price = FixedU128::from_rational(delta_a, delta_s); + assert!(share_price_initial >= exec_price); + + let final_shares = Tokens::total_issuance(pool_id); + let pool = Pools::::get(pool_id).unwrap(); + let final_reserves = pool.reserves_with_decimals::(&pool_account).unwrap(); + let final_d = hydra_dx_math::stableswap::calculate_d::<128u8>(&final_reserves, amplification.get().into()).unwrap(); + assert!(final_d < intermediate_d); + + let d = d_plus; + let d_plus = U256::from(final_d); + let s = s_plus; + let s_plus = U256::from(final_shares); + assert!(d * (s_plus + 10u128.pow(18) ) >= d_plus * s); + assert!(d_plus * s >= d * s_plus); + + }); + } +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(1000))] + #[test] + fn add_remove_liquidity_with_shares_invariants( + pool in balanced_pool(3), + shares_to_add in share_amount(), + amplification in some_amplification(), + ) { + let trade_fee = Permill::from_percent(0); + + let assets_to_register: Vec<(Vec, u32, u8)> = pool.iter().enumerate().map(|(asset_id, v)| ( asset_id.to_string().into_bytes(), asset_id as u32, v.decimals)).collect(); + + let pool_assets: Vec = assets_to_register.iter().map(|(_, asset_id, _)| *asset_id).collect(); + let initial_liquidity: Vec> = pool.iter().enumerate().map(|(asset_id, v)| AssetAmount::new(asset_id as AssetId, v.amount)).collect(); + let mut endowed_accounts: Vec<(AccountId, AssetId, hydra_dx_math::types::Balance)> = pool.iter().enumerate().map(|(asset_id, v)| (ALICE, asset_id as u32, v.amount)).collect(); + + let (_, asset_a, dec)= assets_to_register.first().unwrap().clone(); + let added_liquidity = to_precision(1_000_000_000_000, dec); + endowed_accounts.push((BOB, asset_a, added_liquidity * 1000)); + + ExtBuilder::default() + .with_endowed_accounts(endowed_accounts) + .with_registered_assets(assets_to_register) + .with_pool( + ALICE, + PoolInfo:: { + assets: pool_assets.try_into().unwrap(), + initial_amplification: amplification, + final_amplification: amplification, + initial_block: 0, + final_block: 0, + fee: trade_fee, + }, + InitialLiquidity{ account: ALICE, + assets: initial_liquidity,} + ) + .build() + .execute_with(|| { + let pool_id = get_pool_id_at(0); + let pool_account = pool_account(pool_id); + + let pool = Pools::::get(pool_id).unwrap(); + let initial_reserves = pool.reserves_with_decimals::(&pool_account).unwrap(); + let initial_d = hydra_dx_math::stableswap::calculate_d::<128u8>(&initial_reserves, amplification.get().into()).unwrap(); + + let share_price_initial = get_share_price(pool_id, 0); + let initial_shares = Tokens::total_issuance(pool_id); + let initial_a = Tokens::free_balance(asset_a, &BOB); + assert_ok!(Stableswap::add_liquidity_shares( + RuntimeOrigin::signed(BOB), + pool_id, + shares_to_add, + asset_a, + u128::MAX, + )); + let final_a = Tokens::free_balance(asset_a, &BOB); + let added_liquidity = initial_a - final_a; + let final_shares = Tokens::total_issuance(pool_id); + let delta_s = final_shares - initial_shares; + assert!(delta_s == shares_to_add); + let exec_price = FixedU128::from_rational(added_liquidity , delta_s); + assert!(share_price_initial <= exec_price); + + let pool = Pools::::get(pool_id).unwrap(); + let final_reserves = pool.reserves_with_decimals::(&pool_account).unwrap(); + let intermediate_d = hydra_dx_math::stableswap::calculate_d::<128u8>(&final_reserves, amplification.get().into()).unwrap(); + assert!(intermediate_d > initial_d); + + let d = U256::from(initial_d) ; + let d_plus = U256::from(intermediate_d) ; + let s = U256::from(initial_shares) ; + let s_plus = U256::from(final_shares) ; + assert!(d_plus * s >= d * s_plus); + assert!(d * s_plus >= (d_plus - 10u128.pow(18)) * s); + + let share_price_initial = get_share_price(pool_id, 0); + let a_initial = Tokens::free_balance(asset_a, &pool_account); + assert_ok!(Stableswap::withdraw_asset_amount( + RuntimeOrigin::signed(BOB), + pool_id, + asset_a, + added_liquidity / 2, + u128::MAX, + )); + let a_final = Tokens::free_balance(asset_a, &pool_account); + let delta_a = a_initial - a_final; + let exec_price = FixedU128::from_rational(delta_a, delta_s); + assert!(share_price_initial >= exec_price); + + let final_shares = Tokens::total_issuance(pool_id); + let pool = Pools::::get(pool_id).unwrap(); + let final_reserves = pool.reserves_with_decimals::(&pool_account).unwrap(); + let final_d = hydra_dx_math::stableswap::calculate_d::<128u8>(&final_reserves, amplification.get().into()).unwrap(); + assert!(final_d < intermediate_d); + + let d = d_plus; + let d_plus = U256::from(final_d); + let s = s_plus; + let s_plus = U256::from(final_shares); + assert!(d * (s_plus + 10u128.pow(18) ) >= d_plus * s); + assert!(d_plus * s >= d * s_plus); + + }); + } +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(1000))] + #[test] + fn add_remove_liquidity_with_shares_invariants_with_5_assets( + pool in balanced_pool(5), + shares_to_add in share_amount(), + amplification in some_amplification(), + ) { + let trade_fee = Permill::from_percent(0); + + let assets_to_register: Vec<(Vec, u32, u8)> = pool.iter().enumerate().map(|(asset_id, v)| ( asset_id.to_string().into_bytes(), asset_id as u32, v.decimals)).collect(); + + let pool_assets: Vec = assets_to_register.iter().map(|(_, asset_id, _)| *asset_id).collect(); + let initial_liquidity: Vec> = pool.iter().enumerate().map(|(asset_id, v)| AssetAmount::new(asset_id as AssetId, v.amount)).collect(); + let mut endowed_accounts: Vec<(AccountId, AssetId, hydra_dx_math::types::Balance)> = pool.iter().enumerate().map(|(asset_id, v)| (ALICE, asset_id as u32, v.amount)).collect(); + + let (_, asset_a, dec)= assets_to_register.first().unwrap().clone(); + let added_liquidity = to_precision(1_000_000_000_000, dec); + endowed_accounts.push((BOB, asset_a, added_liquidity * 1000)); + + ExtBuilder::default() + .with_endowed_accounts(endowed_accounts) + .with_registered_assets(assets_to_register) + .with_pool( + ALICE, + PoolInfo:: { + assets: pool_assets.try_into().unwrap(), + initial_amplification: amplification, + final_amplification: amplification, + initial_block: 0, + final_block: 0, + fee: trade_fee, + }, + InitialLiquidity{ account: ALICE, + assets: initial_liquidity,} + ) + .build() + .execute_with(|| { + let pool_id = get_pool_id_at(0); + let pool_account = pool_account(pool_id); + + let pool = Pools::::get(pool_id).unwrap(); + let initial_reserves = pool.reserves_with_decimals::(&pool_account).unwrap(); + let initial_d = hydra_dx_math::stableswap::calculate_d::<128u8>(&initial_reserves, amplification.get().into()).unwrap(); + + let share_price_initial = get_share_price(pool_id, 0); + let initial_shares = Tokens::total_issuance(pool_id); + let initial_a = Tokens::free_balance(asset_a, &BOB); + assert_ok!(Stableswap::add_liquidity_shares( + RuntimeOrigin::signed(BOB), + pool_id, + shares_to_add, + asset_a, + u128::MAX, + )); + let final_a = Tokens::free_balance(asset_a, &BOB); + let added_liquidity = initial_a - final_a; + let final_shares = Tokens::total_issuance(pool_id); + let delta_s = final_shares - initial_shares; + assert!(delta_s == shares_to_add); + let exec_price = FixedU128::from_rational(added_liquidity , delta_s); + assert!(share_price_initial <= exec_price); + + let pool = Pools::::get(pool_id).unwrap(); + let final_reserves = pool.reserves_with_decimals::(&pool_account).unwrap(); + let intermediate_d = hydra_dx_math::stableswap::calculate_d::<128u8>(&final_reserves, amplification.get().into()).unwrap(); + assert!(intermediate_d > initial_d); + + let d = U256::from(initial_d) ; + let d_plus = U256::from(intermediate_d) ; + let s = U256::from(initial_shares) ; + let s_plus = U256::from(final_shares) ; + assert!(d_plus * s >= d * s_plus); + assert!(d * s_plus >= (d_plus - 10u128.pow(18)) * s); + + let share_price_initial = get_share_price(pool_id, 0); + let a_initial = Tokens::free_balance(asset_a, &pool_account); + assert_ok!(Stableswap::withdraw_asset_amount( + RuntimeOrigin::signed(BOB), + pool_id, + asset_a, + added_liquidity / 2, + u128::MAX, + )); + let a_final = Tokens::free_balance(asset_a, &pool_account); + let delta_a = a_initial - a_final; + let exec_price = FixedU128::from_rational(delta_a, delta_s); + assert!(share_price_initial >= exec_price); + + let final_shares = Tokens::total_issuance(pool_id); + let pool = Pools::::get(pool_id).unwrap(); + let final_reserves = pool.reserves_with_decimals::(&pool_account).unwrap(); + let final_d = hydra_dx_math::stableswap::calculate_d::<128u8>(&final_reserves, amplification.get().into()).unwrap(); + assert!(final_d < intermediate_d); + + let d = d_plus; + let d_plus = U256::from(final_d); + let s = s_plus; + let s_plus = U256::from(final_shares); + assert!(d * (s_plus + 10u128.pow(18) ) >= d_plus * s); + assert!(d_plus * s >= d * s_plus); + + }); + } +} diff --git a/pallets/stableswap/src/tests/mock.rs b/pallets/stableswap/src/tests/mock.rs index 6340cc018..37c2c51b2 100644 --- a/pallets/stableswap/src/tests/mock.rs +++ b/pallets/stableswap/src/tests/mock.rs @@ -228,6 +228,13 @@ impl ExtBuilder { self } + pub fn with_registered_assets(mut self, assets: Vec<(Vec, AssetId, u8)>) -> Self { + for (name, asset, decimals) in assets.into_iter() { + self.registered_assets.push((name, asset, decimals)); + } + self + } + pub fn with_pool( mut self, who: AccountId, diff --git a/pallets/stableswap/src/tests/mod.rs b/pallets/stableswap/src/tests/mod.rs index e22f01a02..1e68db3d6 100644 --- a/pallets/stableswap/src/tests/mod.rs +++ b/pallets/stableswap/src/tests/mod.rs @@ -25,7 +25,7 @@ macro_rules! to_precision { pub(crate) fn get_share_price(pool_id: AssetId, asset_idx: usize) -> FixedU128 { let pool_account = pool_account(pool_id); let pool = >::get(pool_id).unwrap(); - let balances = pool.balances::(&pool_account).unwrap(); + let balances = pool.reserves_with_decimals::(&pool_account).unwrap(); let amp = Pallet::::get_amplification(&pool); let issuance = Tokens::total_issuance(pool_id); let share_price = @@ -36,7 +36,7 @@ pub(crate) fn get_share_price(pool_id: AssetId, asset_idx: usize) -> FixedU128 { pub(crate) fn asset_spot_price(pool_id: AssetId, asset_id: AssetId) -> FixedU128 { let pool_account = pool_account(pool_id); let pool = >::get(pool_id).unwrap(); - let balances = pool.balances::(&pool_account).unwrap(); + let balances = pool.reserves_with_decimals::(&pool_account).unwrap(); let amp = Pallet::::get_amplification(&pool); let asset_idx = pool.find_asset(asset_id).unwrap(); let d = hydra_dx_math::stableswap::calculate_d::(&balances, amp).unwrap(); diff --git a/pallets/stableswap/src/tests/remove_liquidity.rs b/pallets/stableswap/src/tests/remove_liquidity.rs index 3981f6f86..732fd7698 100644 --- a/pallets/stableswap/src/tests/remove_liquidity.rs +++ b/pallets/stableswap/src/tests/remove_liquidity.rs @@ -440,7 +440,7 @@ fn remove_liquidity_fail_when_desired_min_limit_is_not_reached() { let shares = Tokens::free_balance(pool_id, &BOB); assert_noop!( Stableswap::remove_liquidity_one_asset(RuntimeOrigin::signed(BOB), pool_id, asset_c, shares, 200 * ONE,), - Error::::MinimumAmountNotReached + Error::::SlippageLimit, ); }); } diff --git a/pallets/stableswap/src/trade_execution.rs b/pallets/stableswap/src/trade_execution.rs index 8c296346a..2d5300904 100644 --- a/pallets/stableswap/src/trade_execution.rs +++ b/pallets/stableswap/src/trade_execution.rs @@ -24,7 +24,7 @@ impl TradeExecution::AssetNotInPool.into()))?; let pool_account = Self::pool_account(pool_id); let balances = pool - .balances::(&pool_account) + .reserves_with_decimals::(&pool_account) .ok_or_else(|| ExecutorError::Error(Error::::UnknownDecimals.into()))?; let share_issuance = T::Currency::total_issuance(pool_id); @@ -75,7 +75,7 @@ impl TradeExecution::AssetNotInPool.into()))?; let pool_account = Self::pool_account(pool_id); let balances = pool - .balances::(&pool_account) + .reserves_with_decimals::(&pool_account) .ok_or_else(|| ExecutorError::Error(Error::::UnknownDecimals.into()))?; let share_issuance = T::Currency::total_issuance(pool_id); let amplification = Self::get_amplification(&pool); @@ -99,7 +99,7 @@ impl TradeExecution::AssetNotInPool.into()))?; let pool_account = Self::pool_account(pool_id); let balances = pool - .balances::(&pool_account) + .reserves_with_decimals::(&pool_account) .ok_or_else(|| ExecutorError::Error(Error::::UnknownDecimals.into()))?; let share_issuance = T::Currency::total_issuance(pool_id); let amplification = Self::get_amplification(&pool); diff --git a/pallets/stableswap/src/types.rs b/pallets/stableswap/src/types.rs index f6645417a..85d965f05 100644 --- a/pallets/stableswap/src/types.rs +++ b/pallets/stableswap/src/types.rs @@ -46,7 +46,7 @@ impl PoolInfo where AssetId: Ord + Copy, { - pub fn find_asset(&self, asset: AssetId) -> Option { + pub(crate) fn find_asset(&self, asset: AssetId) -> Option { self.assets.iter().position(|v| *v == asset) } @@ -54,7 +54,7 @@ where self.assets.len() >= 2 && has_unique_elements(&mut self.assets.iter()) } - pub fn balances(&self, account: &T::AccountId) -> Option> + pub(crate) fn reserves_with_decimals(&self, account: &T::AccountId) -> Option> where T::AssetId: From, { diff --git a/runtime/hydradx/Cargo.toml b/runtime/hydradx/Cargo.toml index 28f8de74a..501190821 100644 --- a/runtime/hydradx/Cargo.toml +++ b/runtime/hydradx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydradx-runtime" -version = "205.0.0" +version = "206.0.0" authors = ["GalacticCouncil"] edition = "2021" license = "Apache 2.0" diff --git a/runtime/hydradx/src/lib.rs b/runtime/hydradx/src/lib.rs index 6bc8956b5..9fdc47c1c 100644 --- a/runtime/hydradx/src/lib.rs +++ b/runtime/hydradx/src/lib.rs @@ -107,7 +107,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("hydradx"), impl_name: create_runtime_str!("hydradx"), authoring_version: 1, - spec_version: 205, + spec_version: 206, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1,