From 49f0e3a9a1ade61cea56014ffd6d23cc2f416c92 Mon Sep 17 00:00:00 2001 From: Omar Date: Sat, 2 Mar 2024 17:27:45 +0300 Subject: [PATCH 01/11] [Tests]: Minor test fix --- tests/tests/protocol.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests/protocol.rs b/tests/tests/protocol.rs index 0720f8d6..83bd5178 100644 --- a/tests/tests/protocol.rs +++ b/tests/tests/protocol.rs @@ -601,8 +601,8 @@ fn cant_add_an_allowed_pool_where_neither_of_the_resources_is_the_protocol_resou let fungible2 = ResourceBuilder::new_fungible(OwnerRole::None) .mint_initial_supply(100, env)?; let (pool, ..) = OciswapV2PoolInterfaceScryptoTestStub::instantiate( - fungible2.resource_address(env)?, fungible1.resource_address(env)?, + fungible2.resource_address(env)?, pdec!(1), dec!(0.03), dec!(0.03), From 7c0ddc09023512c94b1f27852ef6e14ea5c9a76a Mon Sep 17 00:00:00 2001 From: Omar Date: Tue, 5 Mar 2024 11:47:03 +0300 Subject: [PATCH 02/11] [Defiplaza v2 Adapter v1]: Some cleanups and tests --- Cargo.lock | 13 +++ packages/defiplaza-v2-adapter-v1/src/lib.rs | 40 +++------ packages/ignition/src/blueprint.rs | 2 +- tests/tests/defiplaza_v2.rs | 94 +++++++++++++++++++++ 4 files changed, 122 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e0461d07..6c9a7f05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -580,6 +580,19 @@ dependencies = [ "transaction", ] +[[package]] +name = "defiplaza-v2-adapter-v1" +version = "0.1.0" +dependencies = [ + "common", + "ports-interface", + "radix-engine-interface", + "sbor", + "scrypto", + "scrypto-interface", + "transaction", +] + [[package]] name = "deranged" version = "0.3.11" diff --git a/packages/defiplaza-v2-adapter-v1/src/lib.rs b/packages/defiplaza-v2-adapter-v1/src/lib.rs index 38ca81fa..0a99dda0 100644 --- a/packages/defiplaza-v2-adapter-v1/src/lib.rs +++ b/packages/defiplaza-v2-adapter-v1/src/lib.rs @@ -6,8 +6,6 @@ use ports_interface::prelude::*; use scrypto::prelude::*; use scrypto_interface::*; -// TODO: Remove all logging. - macro_rules! define_error { ( $( @@ -59,7 +57,8 @@ pub mod adapter { /// The pair config of the various pools is constant but there is no /// getter function that can be used to get it on ledger. As such, the /// protocol owner or manager must submit this information to the - /// adapter for its operation. + /// adapter for its operation. This does not change, so, once set we + /// do not expect to remove it again. pair_config: KeyValueStore, } @@ -230,11 +229,6 @@ pub mod adapter { // // In the case of equilibrium we do not contribute the second bucket // and instead just the first bucket. - info!("Doing the first one"); - info!( - "Shortage before first contribution: {:?}", - pool.get_state().shortage - ); let (first_pool_units, second_change) = match shortage_state { ShortageState::Equilibrium => ( pool.add_liquidity(first_bucket, None).0, @@ -244,10 +238,6 @@ pub mod adapter { pool.add_liquidity(first_bucket, Some(second_bucket)) } }; - info!( - "Shortage after first contribution: {:?}", - pool.get_state().shortage - ); // Step 5: Calculate and store the original target of the second // liquidity position. This is calculated as the amount of assets @@ -256,17 +246,19 @@ pub mod adapter { let second_original_target = second_bucket.amount(); // Step 6: Add liquidity with the second resource & no co-liquidity. - info!("Doing the second one"); let (second_pool_units, change) = pool.add_liquidity(second_bucket, None); - info!( - "Shortage after second contribution: {:?}", - pool.get_state().shortage - ); - // TODO: Should we subtract the change from the second original - // target? Seems like we should if the price if not the same in - // some way? + // We've been told that the change should be zero. Therefore, we + // assert for it to make sure that everything is as we expect it + // to be. + assert_eq!( + change + .as_ref() + .map(|bucket| bucket.amount()) + .unwrap_or(Decimal::ZERO), + Decimal::ZERO + ); // A sanity check to make sure that everything is correct. The pool // units obtained from the first contribution should be different @@ -433,7 +425,6 @@ pub mod adapter { Global::::from(base_pool), Global::::from(quote_pool), ); - info!("bid ask = {bid_ask:?}"); let average_price = bid_ask .bid @@ -441,7 +432,6 @@ pub mod adapter { .and_then(|value| value.checked_div(dec!(2))) .expect(OVERFLOW_ERROR); - info!("average_price = {average_price}"); Price { base: base_resource_address, quote: quote_resource_address, @@ -488,8 +478,8 @@ impl From for AnyValue { // source code is licensed under the MIT license which allows us to do such // copies and modification of code. // -// This module exposes two main functions which are the entrypoints into this -// module's functionality which calculate the incoming and outgoing spot prices. +// The `calculate_pair_prices` function is the entrypoint into the module and is +// the function to calculate the current bid and ask prices of the pairs. #[allow(clippy::arithmetic_side_effects)] mod price_math { use super::*; @@ -564,8 +554,6 @@ mod price_math { let bid = incoming_spot; let ask = outgoing_spot; - info!("Shortage = {:?}", pair_state.shortage); - // TODO: What to do at equilibrium? match pair_state.shortage { Shortage::Equilibrium | Shortage::BaseShortage => { diff --git a/packages/ignition/src/blueprint.rs b/packages/ignition/src/blueprint.rs index 6f1cb30f..edbe013a 100644 --- a/packages/ignition/src/blueprint.rs +++ b/packages/ignition/src/blueprint.rs @@ -579,7 +579,7 @@ mod ignition { value .checked_round(17, RoundingMode::ToPositiveInfinity) }) - .expect(OVERFLOW_ERROR); + .unwrap_or(Decimal::MAX); assert!( pool_reported_value_of_user_resource_in_protocol_resource <= maximum_amount, diff --git a/tests/tests/defiplaza_v2.rs b/tests/tests/defiplaza_v2.rs index 78711179..ff3cabe6 100644 --- a/tests/tests/defiplaza_v2.rs +++ b/tests/tests/defiplaza_v2.rs @@ -819,3 +819,97 @@ fn user_resources_are_contributed_in_full_when_oracle_price_is_lower_than_pool_p Ok(()) } + +#[test] +fn pool_reported_price_and_quote_reported_price_are_similar_with_base_resource_as_input( +) -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut defiplaza_v2, + .. + } = ScryptoTestEnv::new_with_configuration(Configuration { + maximum_allowed_relative_price_difference: dec!(0.05), + ..Default::default() + })?; + + let pool = defiplaza_v2.pools.bitcoin; + let (base_resource, quote_resource) = pool.get_tokens(env)?; + let input_amount = dec!(100); + let input_resource = base_resource; + let output_resource = if input_resource == base_resource { + quote_resource + } else { + base_resource + }; + + let pool_reported_price = defiplaza_v2 + .adapter + .price(ComponentAddress::try_from(pool).unwrap(), env)?; + + // Act + let (output_amount, remainder, ..) = + pool.quote(input_amount, input_resource == quote_resource, env)?; + + // Assert + let input_amount = input_amount - remainder; + let quote_reported_price = Price { + price: output_amount / input_amount, + base: input_resource, + quote: output_resource, + }; + let relative_difference = pool_reported_price + .relative_difference("e_reported_price) + .unwrap(); + + assert!(relative_difference <= dec!(0.0001)); + + Ok(()) +} + +#[test] +fn pool_reported_price_and_quote_reported_price_are_similar_with_quote_resource_as_input( +) -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut defiplaza_v2, + .. + } = ScryptoTestEnv::new_with_configuration(Configuration { + maximum_allowed_relative_price_difference: dec!(0.05), + ..Default::default() + })?; + + let pool = defiplaza_v2.pools.bitcoin; + let (base_resource, quote_resource) = pool.get_tokens(env)?; + let input_amount = dec!(100); + let input_resource = quote_resource; + let output_resource = if input_resource == base_resource { + quote_resource + } else { + base_resource + }; + + let pool_reported_price = defiplaza_v2 + .adapter + .price(ComponentAddress::try_from(pool).unwrap(), env)?; + + // Act + let (output_amount, remainder, ..) = + pool.quote(input_amount, input_resource == quote_resource, env)?; + + // Assert + let input_amount = input_amount - remainder; + let quote_reported_price = Price { + price: output_amount / input_amount, + base: input_resource, + quote: output_resource, + }; + let relative_difference = pool_reported_price + .relative_difference("e_reported_price) + .unwrap(); + + assert!(relative_difference <= dec!(0.0001)); + + Ok(()) +} From ecd6018ea778734d08a331415c47ba23730ba1ab Mon Sep 17 00:00:00 2001 From: Omar Date: Tue, 5 Mar 2024 11:59:01 +0300 Subject: [PATCH 03/11] [Misc]: Fix cargo.lock issue --- Cargo.lock | 71 ++++++++++++++++++++++++++---------------------------- 1 file changed, 34 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c9a7f05..b7db6fc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -288,10 +288,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.88" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02f341c093d19155a6e41631ce5971aac4e9a868262212153124c15fa22d1cdc" +checksum = "a0ba8f7aaa012f30d5b2861462f6708eccd49c3c39863fe083a308035f63d723" dependencies = [ + "jobserver", "libc", ] @@ -580,19 +581,6 @@ dependencies = [ "transaction", ] -[[package]] -name = "defiplaza-v2-adapter-v1" -version = "0.1.0" -dependencies = [ - "common", - "ports-interface", - "radix-engine-interface", - "sbor", - "scrypto", - "scrypto-interface", - "transaction", -] - [[package]] name = "deranged" version = "0.3.11" @@ -933,9 +921,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", @@ -1116,11 +1104,20 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +[[package]] +name = "jobserver" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -1222,9 +1219,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", @@ -2842,9 +2839,9 @@ dependencies = [ [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -2873,9 +2870,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2883,9 +2880,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", @@ -2898,9 +2895,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -2910,9 +2907,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2920,9 +2917,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", @@ -2933,9 +2930,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wasm-encoder" @@ -3067,9 +3064,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", From 230101eb86a8a9dab4d66c1328369147e1b0629b Mon Sep 17 00:00:00 2001 From: Omar Date: Tue, 5 Mar 2024 12:25:02 +0300 Subject: [PATCH 04/11] [Defiplaza v2 Adapter v1]: Small correction --- packages/defiplaza-v2-adapter-v1/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/defiplaza-v2-adapter-v1/src/lib.rs b/packages/defiplaza-v2-adapter-v1/src/lib.rs index 0a99dda0..ac106f4e 100644 --- a/packages/defiplaza-v2-adapter-v1/src/lib.rs +++ b/packages/defiplaza-v2-adapter-v1/src/lib.rs @@ -13,7 +13,7 @@ macro_rules! define_error { )* ) => { $( - pub const $name: &'static str = concat!("[DefiPlaza v2 Adapter v2]", " ", $item); + pub const $name: &'static str = concat!("[DefiPlaza v2 Adapter v1]", " ", $item); )* }; } From 0428c5a3a7998efc06aafe8ff2650af34d10c4be Mon Sep 17 00:00:00 2001 From: Omar Date: Wed, 6 Mar 2024 18:02:22 +0300 Subject: [PATCH 05/11] [Defiplaza v2 Adapter v1]: Rename `add_pair_config` to plural --- packages/defiplaza-v2-adapter-v1/src/lib.rs | 4 ++-- tests/src/environment.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/defiplaza-v2-adapter-v1/src/lib.rs b/packages/defiplaza-v2-adapter-v1/src/lib.rs index ac106f4e..ee007db5 100644 --- a/packages/defiplaza-v2-adapter-v1/src/lib.rs +++ b/packages/defiplaza-v2-adapter-v1/src/lib.rs @@ -43,7 +43,7 @@ pub mod adapter { protocol_manager => updatable_by: [protocol_manager, protocol_owner]; }, methods { - add_pair_config => restrict_to: [protocol_manager, protocol_owner]; + add_pair_configs => restrict_to: [protocol_manager, protocol_owner]; /* User methods */ price => PUBLIC; resource_addresses => PUBLIC; @@ -96,7 +96,7 @@ pub mod adapter { .globalize() } - pub fn add_pair_config( + pub fn add_pair_configs( &mut self, pair_config: IndexMap, ) { diff --git a/tests/src/environment.rs b/tests/src/environment.rs index 066c8be1..e1d271ef 100644 --- a/tests/src/environment.rs +++ b/tests/src/environment.rs @@ -541,7 +541,7 @@ impl ScryptoTestEnv { )?; // Registering all of pair configs to the adapter. - defiplaza_v2_adapter_v1.add_pair_config( + defiplaza_v2_adapter_v1.add_pair_configs( defiplaza_v2_pools .iter() .map(|pool| ComponentAddress::try_from(pool).unwrap()) @@ -1308,7 +1308,7 @@ impl ScryptoUnitEnv { .lock_fee_from_faucet() .call_method( defiplaza_v2_adapter_v1, - "add_pair_config", + "add_pair_configs", (defiplaza_v2_pools .iter() .map(|address| { From f97f4fafbbb1e34cd41b09a894a3cb1a338beb5a Mon Sep 17 00:00:00 2001 From: Omar Date: Wed, 6 Mar 2024 18:04:47 +0300 Subject: [PATCH 06/11] [Defiplaza v2 Adapter v1]: Rename first and second to shortage and surplus assets --- packages/defiplaza-v2-adapter-v1/src/lib.rs | 47 +++++++++++---------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/packages/defiplaza-v2-adapter-v1/src/lib.rs b/packages/defiplaza-v2-adapter-v1/src/lib.rs index ee007db5..6e3fcfab 100644 --- a/packages/defiplaza-v2-adapter-v1/src/lib.rs +++ b/packages/defiplaza-v2-adapter-v1/src/lib.rs @@ -199,7 +199,7 @@ pub mod adapter { let shortage = pair_state.shortage; let shortage_state = ShortageState::from(shortage); - let [(first_resource_address, first_bucket), (second_resource_address, second_bucket)] = + let [(shortage_asset_resource_address, shortage_asset_bucket), (surplus_asset_resource_address, surplus_asset_bucket)] = match shortage_state { ShortageState::Equilibrium => [ (base_resource_address, base_bucket), @@ -218,9 +218,9 @@ pub mod adapter { // Step 3: Calculate tate.target_ratio * bucket1.amount() where // bucket1 is the bucket currently in shortage or the resource that // will be contributed first. - let first_original_target = pair_state + let shortage_asset_original_target = pair_state .target_ratio - .checked_mul(first_bucket.amount()) + .checked_mul(shortage_asset_bucket.amount()) .expect(OVERFLOW_ERROR); // Step 4: Contribute to the pool. The first bucket to provide the @@ -229,25 +229,28 @@ pub mod adapter { // // In the case of equilibrium we do not contribute the second bucket // and instead just the first bucket. - let (first_pool_units, second_change) = match shortage_state { - ShortageState::Equilibrium => ( - pool.add_liquidity(first_bucket, None).0, - Some(second_bucket), - ), - ShortageState::Shortage(_) => { - pool.add_liquidity(first_bucket, Some(second_bucket)) - } - }; + let (shortage_asset_pool_units, surplus_asset_change) = + match shortage_state { + ShortageState::Equilibrium => ( + pool.add_liquidity(shortage_asset_bucket, None).0, + Some(surplus_asset_bucket), + ), + ShortageState::Shortage(_) => pool.add_liquidity( + shortage_asset_bucket, + Some(surplus_asset_bucket), + ), + }; // Step 5: Calculate and store the original target of the second // liquidity position. This is calculated as the amount of assets // that are in the remainder (change) bucket. - let second_bucket = second_change.expect(UNEXPECTED_ERROR); - let second_original_target = second_bucket.amount(); + let surplus_asset_bucket = + surplus_asset_change.expect(UNEXPECTED_ERROR); + let surplus_asset_original_target = surplus_asset_bucket.amount(); // Step 6: Add liquidity with the second resource & no co-liquidity. - let (second_pool_units, change) = - pool.add_liquidity(second_bucket, None); + let (surplus_asset_pool_units, change) = + pool.add_liquidity(surplus_asset_bucket, None); // We've been told that the change should be zero. Therefore, we // assert for it to make sure that everything is as we expect it @@ -264,16 +267,16 @@ pub mod adapter { // units obtained from the first contribution should be different // from those obtained in the second contribution. assert_ne!( - first_pool_units.resource_address(), - second_pool_units.resource_address(), + shortage_asset_pool_units.resource_address(), + surplus_asset_pool_units.resource_address(), ); // The procedure for adding liquidity to the pool is now complete. // We can now construct the output. OpenLiquidityPositionOutput { pool_units: IndexedBuckets::from_buckets([ - first_pool_units, - second_pool_units, + shortage_asset_pool_units, + surplus_asset_pool_units, ]), change: change .map(IndexedBuckets::from_bucket) @@ -282,8 +285,8 @@ pub mod adapter { adapter_specific_information: DefiPlazaV2AdapterSpecificInformation { original_targets: indexmap! { - first_resource_address => first_original_target, - second_resource_address => second_original_target + shortage_asset_resource_address => shortage_asset_original_target, + surplus_asset_resource_address => surplus_asset_original_target }, } .into(), From 727818d4c532e41f11c458953da4a0cd61d6b6d6 Mon Sep 17 00:00:00 2001 From: Omar Date: Wed, 6 Mar 2024 23:27:33 +0300 Subject: [PATCH 07/11] [Caviarnine v1 Adapter v1]: Optimize fees for opening positions. --- packages/caviarnine-v1-adapter-v1/src/lib.rs | 11 +- tests/tests/caviarnine_v1.rs | 293 +++++++++++++++++++ 2 files changed, 297 insertions(+), 7 deletions(-) diff --git a/packages/caviarnine-v1-adapter-v1/src/lib.rs b/packages/caviarnine-v1-adapter-v1/src/lib.rs index 6895d31b..9097e364 100644 --- a/packages/caviarnine-v1-adapter-v1/src/lib.rs +++ b/packages/caviarnine-v1-adapter-v1/src/lib.rs @@ -472,7 +472,7 @@ pub mod adapter { } let (receipt, change_x, change_y) = - pool.add_liquidity(bucket_x, bucket_y, positions); + pool.add_liquidity(bucket_x, bucket_y, positions.clone()); let receipt_global_id = { let resource_address = receipt.resource_address(); @@ -483,14 +483,11 @@ pub mod adapter { let adapter_specific_information = CaviarnineV1AdapterSpecificInformation { - bin_contributions: pool - .get_redemption_bin_values( - receipt_global_id.local_id().clone(), - ) + bin_contributions: positions .into_iter() - .map(|(tick, amount_x, amount_y)| { + .map(|(bin, amount_x, amount_y)| { ( - tick, + bin, ResourceIndexedData { resource_x: amount_x, resource_y: amount_y, diff --git a/tests/tests/caviarnine_v1.rs b/tests/tests/caviarnine_v1.rs index 9bf45fb9..1cf187a8 100644 --- a/tests/tests/caviarnine_v1.rs +++ b/tests/tests/caviarnine_v1.rs @@ -1466,3 +1466,296 @@ fn user_resources_are_contributed_in_full_when_oracle_price_is_lower_than_pool_p Ok(()) } + +#[test] +fn bin_amounts_reported_on_receipt_match_whats_reported_by_caviarnine( +) -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut caviarnine_v1, + resources, + .. + } = ScryptoTestEnv::new_with_configuration(Configuration { + maximum_allowed_relative_price_difference: dec!(0.05), + ..Default::default() + })?; + + let user_resource = resources.bitcoin; + let pool = caviarnine_v1.pools.bitcoin; + + let [user_resource_bucket, xrd_bucket] = + [user_resource, XRD].map(|resource| { + ResourceManager(resource) + .mint_fungible(dec!(100), env) + .unwrap() + }); + + // Act + let OpenLiquidityPositionOutput { + pool_units, + adapter_specific_information, + .. + } = caviarnine_v1.adapter.open_liquidity_position( + pool.try_into().unwrap(), + (user_resource_bucket, xrd_bucket), + env, + )?; + + // Assert + let mut caviarnine_reported_redemption_value = pool + .get_redemption_bin_values( + pool_units + .non_fungible_local_ids(env)? + .first() + .unwrap() + .clone(), + env, + )?; + caviarnine_reported_redemption_value.sort_by(|a, b| a.0.cmp(&b.0)); + let adapter_reported_redemption_value = adapter_specific_information + .as_typed::() + .unwrap() + .bin_contributions; + + assert_eq!( + caviarnine_reported_redemption_value.len(), + adapter_reported_redemption_value.len(), + ); + + for ( + i, + ( + caviarnine_reported_bin, + caviarnine_reported_amount_x, + caviarnine_reported_amount_y, + ), + ) in caviarnine_reported_redemption_value.into_iter().enumerate() + { + let Some(ResourceIndexedData { + resource_x: adapter_reported_amount_x, + resource_y: adapter_reported_amount_y, + }) = adapter_reported_redemption_value + .get(&caviarnine_reported_bin) + .copied() + else { + panic!( + "Bin {} does not have an entry in the adapter data", + caviarnine_reported_bin + ) + }; + + assert_eq!( + round_down_to_5_decimal_places(caviarnine_reported_amount_x), + round_down_to_5_decimal_places(adapter_reported_amount_x), + "Failed at bin with index: {i}" + ); + assert_eq!( + round_down_to_5_decimal_places(caviarnine_reported_amount_y), + round_down_to_5_decimal_places(adapter_reported_amount_y), + "Failed at bin with index: {i}" + ); + } + + Ok(()) +} + +#[test] +fn bin_amounts_reported_on_receipt_match_whats_reported_by_caviarnine_with_price_movement1( +) -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut caviarnine_v1, + resources, + .. + } = ScryptoTestEnv::new_with_configuration(Configuration { + maximum_allowed_relative_price_difference: dec!(0.05), + ..Default::default() + })?; + + let user_resource = resources.bitcoin; + let mut pool = caviarnine_v1.pools.bitcoin; + + let _ = ResourceManager(user_resource) + .mint_fungible(dec!(1_000_000_000), env) + .and_then(|bucket| pool.swap(bucket, env))?; + + let [user_resource_bucket, xrd_bucket] = + [user_resource, XRD].map(|resource| { + ResourceManager(resource) + .mint_fungible(dec!(100), env) + .unwrap() + }); + + // Act + let OpenLiquidityPositionOutput { + pool_units, + adapter_specific_information, + .. + } = caviarnine_v1.adapter.open_liquidity_position( + pool.try_into().unwrap(), + (user_resource_bucket, xrd_bucket), + env, + )?; + + // Assert + let mut caviarnine_reported_redemption_value = pool + .get_redemption_bin_values( + pool_units + .non_fungible_local_ids(env)? + .first() + .unwrap() + .clone(), + env, + )?; + caviarnine_reported_redemption_value.sort_by(|a, b| a.0.cmp(&b.0)); + let adapter_reported_redemption_value = adapter_specific_information + .as_typed::() + .unwrap() + .bin_contributions; + + assert_eq!( + caviarnine_reported_redemption_value.len(), + adapter_reported_redemption_value.len(), + ); + + for ( + i, + ( + caviarnine_reported_bin, + caviarnine_reported_amount_x, + caviarnine_reported_amount_y, + ), + ) in caviarnine_reported_redemption_value.into_iter().enumerate() + { + let Some(ResourceIndexedData { + resource_x: adapter_reported_amount_x, + resource_y: adapter_reported_amount_y, + }) = adapter_reported_redemption_value + .get(&caviarnine_reported_bin) + .copied() + else { + panic!( + "Bin {} does not have an entry in the adapter data", + caviarnine_reported_bin + ) + }; + + assert_eq!( + round_down_to_5_decimal_places(caviarnine_reported_amount_x), + round_down_to_5_decimal_places(adapter_reported_amount_x), + "Failed at bin with index: {i}" + ); + assert_eq!( + round_down_to_5_decimal_places(caviarnine_reported_amount_y), + round_down_to_5_decimal_places(adapter_reported_amount_y), + "Failed at bin with index: {i}" + ); + } + + Ok(()) +} + +#[test] +fn bin_amounts_reported_on_receipt_match_whats_reported_by_caviarnine_with_price_movement2( +) -> Result<(), RuntimeError> { + // Arrange + let Environment { + environment: ref mut env, + mut caviarnine_v1, + resources, + .. + } = ScryptoTestEnv::new_with_configuration(Configuration { + maximum_allowed_relative_price_difference: dec!(0.05), + ..Default::default() + })?; + + let user_resource = resources.bitcoin; + let mut pool = caviarnine_v1.pools.bitcoin; + + let _ = ResourceManager(XRD) + .mint_fungible(dec!(1_000_000_000), env) + .and_then(|bucket| pool.swap(bucket, env))?; + + let [user_resource_bucket, xrd_bucket] = + [user_resource, XRD].map(|resource| { + ResourceManager(resource) + .mint_fungible(dec!(100), env) + .unwrap() + }); + + // Act + let OpenLiquidityPositionOutput { + pool_units, + adapter_specific_information, + .. + } = caviarnine_v1.adapter.open_liquidity_position( + pool.try_into().unwrap(), + (user_resource_bucket, xrd_bucket), + env, + )?; + + // Assert + let mut caviarnine_reported_redemption_value = pool + .get_redemption_bin_values( + pool_units + .non_fungible_local_ids(env)? + .first() + .unwrap() + .clone(), + env, + )?; + caviarnine_reported_redemption_value.sort_by(|a, b| a.0.cmp(&b.0)); + let adapter_reported_redemption_value = adapter_specific_information + .as_typed::() + .unwrap() + .bin_contributions; + + assert_eq!( + caviarnine_reported_redemption_value.len(), + adapter_reported_redemption_value.len(), + ); + + for ( + i, + ( + caviarnine_reported_bin, + caviarnine_reported_amount_x, + caviarnine_reported_amount_y, + ), + ) in caviarnine_reported_redemption_value.into_iter().enumerate() + { + let Some(ResourceIndexedData { + resource_x: adapter_reported_amount_x, + resource_y: adapter_reported_amount_y, + }) = adapter_reported_redemption_value + .get(&caviarnine_reported_bin) + .copied() + else { + panic!( + "Bin {} does not have an entry in the adapter data", + caviarnine_reported_bin + ) + }; + + assert_eq!( + round_down_to_5_decimal_places(caviarnine_reported_amount_x), + round_down_to_5_decimal_places(adapter_reported_amount_x), + "Failed at bin with index: {i}" + ); + assert_eq!( + round_down_to_5_decimal_places(caviarnine_reported_amount_y), + round_down_to_5_decimal_places(adapter_reported_amount_y), + "Failed at bin with index: {i}" + ); + } + + Ok(()) +} + +fn round_down_to_5_decimal_places(decimal: Decimal) -> Decimal { + decimal + .checked_round(5, RoundingMode::ToNegativeInfinity) + .unwrap() +} From a89d5387fd9b682831874e22335627a71b07c002 Mon Sep 17 00:00:00 2001 From: Omar Date: Thu, 7 Mar 2024 11:51:12 +0300 Subject: [PATCH 08/11] [Tests]: Add basis for defiplaza v2 fee tests --- tests/src/environment.rs | 14 +-- tests/tests/defiplaza_v2.rs | 194 ++++++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+), 10 deletions(-) diff --git a/tests/src/environment.rs b/tests/src/environment.rs index e1d271ef..f6cc53cc 100644 --- a/tests/src/environment.rs +++ b/tests/src/environment.rs @@ -430,16 +430,10 @@ impl ScryptoTestEnv { Self::publish_package("defiplaza-v2-adapter-v1", &mut env)?; let defiplaza_v2_pools = resource_addresses.try_map(|resource_address| { - let (resource_x, resource_y) = if XRD < *resource_address { - (XRD, *resource_address) - } else { - (*resource_address, XRD) - }; - let mut defiplaza_pool = DefiPlazaV2PoolInterfaceScryptoTestStub::instantiate_pair( OwnerRole::None, - resource_x, - resource_y, + *resource_address, + XRD, // This pair config is obtained from DefiPlaza's // repo. PairConfig { @@ -454,9 +448,9 @@ impl ScryptoTestEnv { )?; let resource_x = - ResourceManager(resource_x).mint_fungible(dec!(100_000_000), &mut env)?; + ResourceManager(*resource_address).mint_fungible(dec!(100_000_000), &mut env)?; let resource_y = - ResourceManager(resource_y).mint_fungible(dec!(100_000_000), &mut env)?; + ResourceManager(XRD).mint_fungible(dec!(100_000_000), &mut env)?; let (_, change1) = defiplaza_pool.add_liquidity(resource_x, None, &mut env)?; diff --git a/tests/tests/defiplaza_v2.rs b/tests/tests/defiplaza_v2.rs index ff3cabe6..7644d463 100644 --- a/tests/tests/defiplaza_v2.rs +++ b/tests/tests/defiplaza_v2.rs @@ -913,3 +913,197 @@ fn pool_reported_price_and_quote_reported_price_are_similar_with_quote_resource_ Ok(()) } + +#[test] +#[ignore = "Awaiting defiplaza response"] +fn exact_fee_test1() { + test_exact_defiplaza_fees_amounts( + // Initial supply for the pool. + AssetIndexedData { + protocol_resource: dec!(100_000), + user_resource: dec!(100_000), + }, + // Initial price of the pool + dec!(1), + // The fee percentage of the pool + dec!(0.03), + // User contribution to the pool. This would mean that the user would + // own 0.1% of the pool + AssetIndexedData { + user_resource: dec!(100), + protocol_resource: dec!(100), + }, + // The swaps to perform - the asset you see is the input asset + vec![(Asset::ProtocolResource, dec!(1_000))], + // The fees to expect - with 0.1% pool ownership of the pool and fees of + // 3% then we expect to see 0.03 of the protocol resource in fees (as it + // was the input in the swap) and none of the user resource in fees. + AssetIndexedData { + user_resource: EqualityCheck::ExactlyEquals(dec!(0)), + protocol_resource: EqualityCheck::ExactlyEquals(dec!(0.03)), + }, + ) + .expect("Should not fail!") +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum Asset { + UserResource, + ProtocolResource, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct AssetIndexedData { + user_resource: T, + protocol_resource: T, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum EqualityCheck { + ExactlyEquals(T), + ApproximatelyEquals { value: T, acceptable_difference: T }, +} + +fn test_exact_defiplaza_fees_amounts( + // The initial amount of liquidity to provide when creating the liquidity + // pool. + initial_liquidity: AssetIndexedData, + // The price to set as the initial price of the pool. + initial_price: Decimal, + // The fee percentage of the pool + fee_percentage: Decimal, + // The contribution that the user will make to the pool + user_contribution: AssetIndexedData, + // The swaps to perform on the pool. + swaps: Vec<(Asset, Decimal)>, + // Equality checks to perform when closing the liquidity position. + expected_fees: AssetIndexedData>, +) -> Result<(), RuntimeError> { + let Environment { + environment: ref mut env, + mut defiplaza_v2, + resources: ResourceInformation { bitcoin, .. }, + .. + } = ScryptoTestEnv::new_with_configuration(Configuration { + maximum_allowed_relative_price_difference: dec!(0.05), + ..Default::default() + })?; + + let resources = AssetIndexedData { + user_resource: bitcoin, + protocol_resource: XRD, + }; + + // Creating a new defiplaza pair so we can initialize it the way that we + // desire and without any constraints from the environment. + let mut pool = DefiPlazaV2PoolInterfaceScryptoTestStub::instantiate_pair( + OwnerRole::None, + resources.user_resource, + resources.protocol_resource, + PairConfig { + k_in: dec!("0.4"), + k_out: dec!("1"), + fee: fee_percentage, + decay_factor: dec!("0.9512"), + }, + initial_price, + defiplaza_v2.package, + env, + )?; + + // Providing the desired initial contribution to the pool. + [ + (resources.user_resource, initial_liquidity.user_resource), + ( + resources.protocol_resource, + initial_liquidity.protocol_resource, + ), + ] + .map(|(resource_address, amount)| { + let bucket = ResourceManager(resource_address) + .mint_fungible(amount, env) + .unwrap(); + let (_, change) = pool.add_liquidity(bucket, None, env).unwrap(); + let change_amount = change + .map(|bucket| bucket.amount(env).unwrap()) + .unwrap_or(Decimal::ZERO); + assert_eq!(change_amount, Decimal::ZERO); + }); + + // Providing the user's contribution to the pool through the adapter + let [bucket_x, bucket_y] = [ + ( + resources.protocol_resource, + user_contribution.protocol_resource, + ), + (resources.user_resource, user_contribution.user_resource), + ] + .map(|(resource_address, amount)| { + ResourceManager(resource_address) + .mint_fungible(amount, env) + .unwrap() + }); + let OpenLiquidityPositionOutput { + pool_units, + change, + adapter_specific_information, + .. + } = defiplaza_v2.adapter.open_liquidity_position( + pool.try_into().unwrap(), + (bucket_x, bucket_y), + env, + )?; + + // Asset the user got back no change in this contribution + for bucket in change.into_values() { + let amount = bucket.amount(env)?; + assert_eq!(amount, Decimal::ZERO); + } + + // Performing the swaps specified by the user + for (asset, amount) in swaps.into_iter() { + let address = match asset { + Asset::ProtocolResource => resources.protocol_resource, + Asset::UserResource => resources.user_resource, + }; + let bucket = + ResourceManager(address).mint_fungible(amount, env).unwrap(); + let _ = pool.swap(bucket, env)?; + } + + // Close the liquidity position + let CloseLiquidityPositionOutput { fees, .. } = + defiplaza_v2.adapter.close_liquidity_position( + pool.try_into().unwrap(), + pool_units.into_values().collect(), + adapter_specific_information, + env, + )?; + + // Assert that the fees is what's expected. + for (resource_address, equality_check) in [ + (resources.protocol_resource, expected_fees.protocol_resource), + (resources.user_resource, expected_fees.user_resource), + ] { + // Get the fees + let resource_fees = fees.get(&resource_address).copied().unwrap(); + + // Perform the assertion + match equality_check { + EqualityCheck::ExactlyEquals(value) => { + assert_eq!(resource_fees, value) + } + EqualityCheck::ApproximatelyEquals { + value, + acceptable_difference, + } => { + assert!( + (resource_fees - value).checked_abs().unwrap() + <= acceptable_difference + ) + } + } + } + + Ok(()) +} From d3f9ce82d4768f46c2a3e6a6ac9c2afda17d0a4b Mon Sep 17 00:00:00 2001 From: Omar Date: Thu, 7 Mar 2024 13:30:21 +0300 Subject: [PATCH 09/11] [Tests]: Fix tests --- tests/tests/caviarnine_v1.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/tests/caviarnine_v1.rs b/tests/tests/caviarnine_v1.rs index e14b8b34..59dea3d9 100644 --- a/tests/tests/caviarnine_v1.rs +++ b/tests/tests/caviarnine_v1.rs @@ -1511,6 +1511,9 @@ fn bin_amounts_reported_on_receipt_match_whats_reported_by_caviarnine( let mut caviarnine_reported_redemption_value = pool .get_redemption_bin_values( pool_units + .into_values() + .next() + .unwrap() .non_fungible_local_ids(env)? .first() .unwrap() @@ -1608,6 +1611,9 @@ fn bin_amounts_reported_on_receipt_match_whats_reported_by_caviarnine_with_price let mut caviarnine_reported_redemption_value = pool .get_redemption_bin_values( pool_units + .into_values() + .next() + .unwrap() .non_fungible_local_ids(env)? .first() .unwrap() @@ -1705,6 +1711,9 @@ fn bin_amounts_reported_on_receipt_match_whats_reported_by_caviarnine_with_price let mut caviarnine_reported_redemption_value = pool .get_redemption_bin_values( pool_units + .into_values() + .next() + .unwrap() .non_fungible_local_ids(env)? .first() .unwrap() From 381a3c06236a0dc889c8e35d8cedd5cadf171726 Mon Sep 17 00:00:00 2001 From: Omar Date: Mon, 11 Mar 2024 14:44:36 +0300 Subject: [PATCH 10/11] [Defiplaza v2 Adapter v1]: Fix fee calculations and add exact fee tests. --- Cargo.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b7db6fc0..892e2728 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -209,9 +209,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.3" +version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[package]] name = "bytecount" @@ -288,9 +288,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.89" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0ba8f7aaa012f30d5b2861462f6708eccd49c3c39863fe083a308035f63d723" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" dependencies = [ "jobserver", "libc", @@ -304,9 +304,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.34" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" dependencies = [ "android-tzdata", "iana-time-zone", @@ -317,9 +317,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" +checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651" dependencies = [ "clap_builder", "clap_derive", @@ -327,9 +327,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", @@ -491,9 +491,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.118" +version = "1.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2673ca5ae28334544ec2a6b18ebe666c42a2650abfb48abbd532ed409a44be2b" +checksum = "635179be18797d7e10edb9cd06c859580237750c7351f39ed9b298bfc17544ad" dependencies = [ "cc", "cxxbridge-flags", @@ -503,9 +503,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.118" +version = "1.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df46fe0eb43066a332586114174c449a62c25689f85a08f28fdcc8e12c380b9" +checksum = "9324397d262f63ef77eb795d900c0d682a34a43ac0932bec049ed73055d52f63" dependencies = [ "cc", "codespan-reporting", @@ -518,15 +518,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.118" +version = "1.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "886acf875df67811c11cd015506b3392b9e1820b1627af1a6f4e93ccdfc74d11" +checksum = "a87ff7342ffaa54b7c61618e0ce2bbcf827eba6d55b923b83d82551acbbecfe5" [[package]] name = "cxxbridge-macro" -version = "1.0.118" +version = "1.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d151cc139c3080e07f448f93a1284577ab2283d2a44acd902c6fba9ec20b6de" +checksum = "70b5b86cf65fa0626d85720619d80b288013477a91a0389fa8bc716bf4903ad1" dependencies = [ "proc-macro2", "quote", From 42cd445ee3911e067fbbac262cf2a2f2ea267cf9 Mon Sep 17 00:00:00 2001 From: Omar Date: Mon, 11 Mar 2024 14:45:14 +0300 Subject: [PATCH 11/11] [Defiplaza v2 Adapter v1]: Update the fee calculation and add tests for fees --- libraries/common/src/indexed_buckets.rs | 7 + packages/defiplaza-v2-adapter-v1/src/lib.rs | 301 +++++++++++++----- tests/tests/defiplaza_v2.rs | 336 +++++++++++++++----- 3 files changed, 502 insertions(+), 142 deletions(-) diff --git a/libraries/common/src/indexed_buckets.rs b/libraries/common/src/indexed_buckets.rs index b675e82f..b20cfe5d 100644 --- a/libraries/common/src/indexed_buckets.rs +++ b/libraries/common/src/indexed_buckets.rs @@ -130,6 +130,13 @@ impl IndexedBuckets { pub fn into_inner(self) -> IndexMap { self.0 } + + pub fn combine(mut self, other: Self) -> Self { + for bucket in other.0.into_values() { + self.insert(bucket) + } + self + } } impl Default for IndexedBuckets { diff --git a/packages/defiplaza-v2-adapter-v1/src/lib.rs b/packages/defiplaza-v2-adapter-v1/src/lib.rs index 6e3fcfab..8ff2c0de 100644 --- a/packages/defiplaza-v2-adapter-v1/src/lib.rs +++ b/packages/defiplaza-v2-adapter-v1/src/lib.rs @@ -326,89 +326,248 @@ pub mod adapter { (pool_units_bucket1, pool_units_bucket2) }; - // Getting the base and quote assets - let (base_resource_address, quote_resource_address) = - pool.get_tokens(); - // Decoding the adapter specific information as the type we expect // it to be. let DefiPlazaV2AdapterSpecificInformation { original_targets } = adapter_specific_information.as_typed().unwrap(); - let [old_base_target, old_quote_target] = - [base_resource_address, quote_resource_address].map( - |address| original_targets.get(&address).copied().unwrap(), - ); - - // Step 1: Get the pair's state - let pair_state = pool.get_state(); - - // Step 2 & 3: Determine which of the resources is in shortage and - // based on that determine what the new target should be. - let claimed_tokens = IndexedBuckets::from_buckets( - [pool_units1, pool_units2].into_iter().flat_map(|bucket| { - let resource_manager = bucket.resource_manager(); - let entry = ComponentAddress::try_from( - resource_manager - .get_metadata::<_, GlobalAddress>("pool") - .unwrap() - .unwrap(), - ) - .unwrap(); - let mut two_resource_pool = - Global::::from(entry); - let (bucket1, bucket2) = two_resource_pool.redeem(bucket); - [bucket1, bucket2] - }), - ); - let base_bucket = - claimed_tokens.get(&base_resource_address).unwrap(); - let quote_bucket = - claimed_tokens.get("e_resource_address).unwrap(); - let base_bucket_amount = base_bucket.amount(); - let quote_bucket_amount = quote_bucket.amount(); + // We have gotten two pools units, one of the base pool and + // another for the quote pool. We need to determine which is + // which and overall split things according to shortage and + // surplus. So, instead of referring to them as base and quote + // we would figure out what is in shortage and what is surplus. + + // First thing we do is store the pool units in a map where the + // key the address of the pool and the value is the pool units + // bucket. We find the address of the pool through metadata on + // the pool units since there is currently no other way to find + // this information. + let mut pool_component_to_pool_unit_mapping = + [pool_units1, pool_units2] + .into_iter() + .map(|bucket| { + let resource_manager = bucket.resource_manager(); + let pool_address = ComponentAddress::try_from( + resource_manager + .get_metadata::<_, GlobalAddress>("pool") + .unwrap() + .unwrap(), + ) + .unwrap(); + (pool_address, bucket) + }) + .collect::>(); + + // With the way we combined them above we now want to split them + // into base pool units and quote pool units. This is simple to + // do, just get the address of the base and quote pools and then + // do a simple `remove` from the above map. + let (base_pool_component, quote_pool_component) = pool.get_pools(); + let base_pool_units = pool_component_to_pool_unit_mapping + .remove(&base_pool_component) + .unwrap(); + let quote_pool_units = pool_component_to_pool_unit_mapping + .remove("e_pool_component) + .unwrap(); - let shortage = pair_state.shortage; - let shortage_state = ShortageState::from(shortage); - let (new_base_target, new_quote_target) = match shortage_state { + // At this point we have the the base and quote token addresses, + // pool addresses, and pool units. We can now split things as + // shortage and quote and stop referring to things as base and + // quote. + let (base_resource_address, quote_resource_address) = + pool.get_tokens(); + let pair_state = pool.get_state(); + let (claimed_resources, fees) = match ShortageState::from( + pair_state.shortage, + ) { + // The pool is in equilibrium, none of the assets are in + // shortage so there is no need to multiply anything by the + // target ratio. ShortageState::Equilibrium => { - (base_bucket_amount, quote_bucket_amount) + // Claiming the assets from the pools. + let [resources_claimed_from_base_resource_pool, resources_claimed_from_quote_resource_pool] = + [ + (base_pool_component, base_pool_units), + (quote_pool_component, quote_pool_units), + ] + .map( + |(pool_component_address, pool_units_bucket)| { + let mut pool = Global::::from( + pool_component_address, + ); + let (bucket1, bucket2) = + pool.redeem(pool_units_bucket); + IndexedBuckets::from_buckets([bucket1, bucket2]) + }, + ); + + // The target of the two resources is just the amount we + // got back when closing the liquidity position. + let new_target_of_base_resource = + resources_claimed_from_base_resource_pool + .get(&base_resource_address) + .map(|bucket| bucket.amount()) + .expect(UNEXPECTED_ERROR); + let new_target_of_quote_resource = + resources_claimed_from_quote_resource_pool + .get("e_resource_address) + .map(|bucket| bucket.amount()) + .expect(UNEXPECTED_ERROR); + + // Now that we have the target for the base and quote + // resources we can calculate the fees. + let base_resource_fees = original_targets + .get(&base_resource_address) + .expect(UNEXPECTED_ERROR) + .checked_sub(new_target_of_base_resource) + .expect(OVERFLOW_ERROR) + .max(dec!(0)); + let quote_resource_fees = original_targets + .get("e_resource_address) + .expect(UNEXPECTED_ERROR) + .checked_sub(new_target_of_quote_resource) + .expect(OVERFLOW_ERROR) + .max(dec!(0)); + + let fees = indexmap! { + base_resource_address => base_resource_fees, + quote_resource_address => quote_resource_fees, + }; + + let claimed_resources = + resources_claimed_from_base_resource_pool.combine( + resources_claimed_from_quote_resource_pool, + ); + + (claimed_resources, fees) + } + // One of the assets is in shortage and the other is in + // surplus. Determine which is which and sort the info. + ShortageState::Shortage(asset) => { + let ( + ( + shortage_asset_pool_component, + shortage_asset_pool_units, + shortage_asset_resource_address, + ), + ( + surplus_asset_pool_component, + surplus_asset_pool_units, + surplus_asset_resource_address, + ), + ) = match asset { + Asset::Base => ( + ( + base_pool_component, + base_pool_units, + base_resource_address, + ), + ( + quote_pool_component, + quote_pool_units, + quote_resource_address, + ), + ), + Asset::Quote => ( + ( + quote_pool_component, + quote_pool_units, + quote_resource_address, + ), + ( + base_pool_component, + base_pool_units, + base_resource_address, + ), + ), + }; + + // We have now split them into shortage and surplus and + // we can now close the liquidity positions and compute + // the new targets for the base and shortage assets. + let [resources_claimed_from_shortage_asset_pool, resources_claimed_from_surplus_asset_pool] = + [ + ( + shortage_asset_pool_component, + shortage_asset_pool_units, + ), + ( + surplus_asset_pool_component, + surplus_asset_pool_units, + ), + ] + .map( + |(pool_component_address, pool_units_bucket)| { + let mut pool = Global::::from( + pool_component_address, + ); + let (bucket1, bucket2) = + pool.redeem(pool_units_bucket); + IndexedBuckets::from_buckets([bucket1, bucket2]) + }, + ); + + // The target of the shortage asset can be calculated by + // multiplying the amount we got back from closing the + // position in the shortage pool by the target ratio of + // the pool in the current state. + let new_target_of_shortage_asset = + resources_claimed_from_shortage_asset_pool + .get(&shortage_asset_resource_address) + .map(|bucket| bucket.amount()) + .expect(UNEXPECTED_ERROR) + .checked_mul(pair_state.target_ratio) + .expect(OVERFLOW_ERROR); + + // The target of the surplus asset is simple, its the + // amount we got back when we closed the position in + // the surplus pool. + let new_target_of_surplus_asset = + resources_claimed_from_surplus_asset_pool + .get(&surplus_asset_resource_address) + .map(|bucket| bucket.amount()) + .expect(UNEXPECTED_ERROR); + + // Now that we have the target for the shortage and + // surplus assets we can calculate the fees earned on + // those assets. Its calculated by subtracting the + // new targets from the original targets. + let shortage_asset_fees = new_target_of_shortage_asset + .checked_sub( + original_targets + .get(&shortage_asset_resource_address) + .copied() + .expect(UNEXPECTED_ERROR), + ) + .expect(OVERFLOW_ERROR) + .max(dec!(0)); + let surplus_asset_fees = new_target_of_surplus_asset + .checked_sub( + original_targets + .get(&surplus_asset_resource_address) + .copied() + .expect(UNEXPECTED_ERROR), + ) + .expect(OVERFLOW_ERROR) + .max(dec!(0)); + + let fees = indexmap! { + shortage_asset_resource_address => shortage_asset_fees, + surplus_asset_resource_address => surplus_asset_fees, + }; + + let claimed_resources = + resources_claimed_from_shortage_asset_pool + .combine(resources_claimed_from_surplus_asset_pool); + + (claimed_resources, fees) } - ShortageState::Shortage(Asset::Base) => ( - base_bucket_amount - .checked_mul(pair_state.target_ratio) - .expect(OVERFLOW_ERROR), - quote_bucket_amount, - ), - ShortageState::Shortage(Asset::Quote) => ( - base_bucket_amount, - quote_bucket_amount - .checked_mul(pair_state.target_ratio) - .expect(OVERFLOW_ERROR), - ), }; - // Steps 4 and 5 - let base_fees = std::cmp::max( - new_base_target - .checked_sub(old_base_target) - .expect(OVERFLOW_ERROR), - Decimal::ZERO, - ); - let quote_fees = std::cmp::max( - new_quote_target - .checked_sub(old_quote_target) - .expect(OVERFLOW_ERROR), - Decimal::ZERO, - ); - CloseLiquidityPositionOutput { - resources: claimed_tokens, + resources: claimed_resources, others: vec![], - fees: indexmap! { - base_resource_address => base_fees, - quote_resource_address => quote_fees, - }, + fees, } } diff --git a/tests/tests/defiplaza_v2.rs b/tests/tests/defiplaza_v2.rs index 7644d463..bb3f4fb4 100644 --- a/tests/tests/defiplaza_v2.rs +++ b/tests/tests/defiplaza_v2.rs @@ -915,37 +915,231 @@ fn pool_reported_price_and_quote_reported_price_are_similar_with_quote_resource_ } #[test] -#[ignore = "Awaiting defiplaza response"] fn exact_fee_test1() { test_exact_defiplaza_fees_amounts( // Initial supply for the pool. + None, + // Initial price of the pool + dec!(1), + // The pair config of the pool* + PairConfig { + k_in: dec!(0.5), + k_out: dec!(1), + fee: dec!(0.02), + decay_factor: dec!(0.9512), + }, + // User contribution to the pool. This would mean that the user would + // own 100% of the pool. + AssetIndexedData { + user_resource: dec!(5000), + protocol_resource: dec!(5000), + }, + // The swaps to perform - the asset you see is the input asset + vec![(Asset::UserResource, dec!(5000))], + ) + .expect("Should not fail!") +} + +#[test] +fn exact_fee_test2() { + test_exact_defiplaza_fees_amounts( + // Initial supply for the pool. + None, + // Initial price of the pool + dec!(1), + // The pair config of the pool* + PairConfig { + k_in: dec!(0.5), + k_out: dec!(1), + fee: dec!(0.02), + decay_factor: dec!(0.9512), + }, + // User contribution to the pool. This would mean that the user would + // own 100% of the pool. + AssetIndexedData { + user_resource: dec!(5000), + protocol_resource: dec!(5000), + }, + // The swaps to perform - the asset you see is the input asset + vec![(Asset::ProtocolResource, dec!(5000))], + ) + .expect("Should not fail!") +} + +#[test] +fn exact_fee_test3() { + test_exact_defiplaza_fees_amounts( + // Initial supply for the pool. + None, + // Initial price of the pool + dec!(1), + // The pair config of the pool* + PairConfig { + k_in: dec!(0.5), + k_out: dec!(1), + fee: dec!(0.02), + decay_factor: dec!(0.9512), + }, + // User contribution to the pool. This would mean that the user would + // own 100% of the pool. + AssetIndexedData { + user_resource: dec!(5000), + protocol_resource: dec!(5000), + }, + // The swaps to perform - the asset you see is the input asset + vec![ + (Asset::UserResource, dec!(5000)), + (Asset::ProtocolResource, dec!(1000)), + ], + ) + .expect("Should not fail!") +} + +#[test] +fn exact_fee_test4() { + test_exact_defiplaza_fees_amounts( + // Initial supply for the pool. + None, + // Initial price of the pool + dec!(1), + // The pair config of the pool* + PairConfig { + k_in: dec!(0.5), + k_out: dec!(1), + fee: dec!(0.02), + decay_factor: dec!(0.9512), + }, + // User contribution to the pool. This would mean that the user would + // own 100% of the pool. + AssetIndexedData { + user_resource: dec!(5000), + protocol_resource: dec!(5000), + }, + // The swaps to perform - the asset you see is the input asset + vec![ + (Asset::ProtocolResource, dec!(5000)), + (Asset::UserResource, dec!(1000)), + ], + ) + .expect("Should not fail!") +} + +#[test] +fn exact_fee_test5() { + test_exact_defiplaza_fees_amounts( + // Initial supply for the pool. + None, + // Initial price of the pool + dec!(1), + // The pair config of the pool* + PairConfig { + k_in: dec!(0.5), + k_out: dec!(1), + fee: dec!(0.02), + decay_factor: dec!(0.9512), + }, + // User contribution to the pool. This would mean that the user would + // own 100% of the pool. AssetIndexedData { - protocol_resource: dec!(100_000), - user_resource: dec!(100_000), + user_resource: dec!(5000), + protocol_resource: dec!(5000), }, + // The swaps to perform - the asset you see is the input asset + vec![ + (Asset::UserResource, dec!(5000)), + (Asset::ProtocolResource, dec!(10_000)), + ], + ) + .expect("Should not fail!") +} + +#[test] +fn exact_fee_test6() { + test_exact_defiplaza_fees_amounts( + // Initial supply for the pool. + None, // Initial price of the pool dec!(1), - // The fee percentage of the pool - dec!(0.03), + // The pair config of the pool* + PairConfig { + k_in: dec!(0.5), + k_out: dec!(1), + fee: dec!(0.02), + decay_factor: dec!(0.9512), + }, // User contribution to the pool. This would mean that the user would - // own 0.1% of the pool + // own 100% of the pool. AssetIndexedData { - user_resource: dec!(100), - protocol_resource: dec!(100), + user_resource: dec!(5000), + protocol_resource: dec!(5000), }, // The swaps to perform - the asset you see is the input asset - vec![(Asset::ProtocolResource, dec!(1_000))], - // The fees to expect - with 0.1% pool ownership of the pool and fees of - // 3% then we expect to see 0.03 of the protocol resource in fees (as it - // was the input in the swap) and none of the user resource in fees. + vec![ + (Asset::ProtocolResource, dec!(5000)), + (Asset::UserResource, dec!(10_000)), + ], + ) + .expect("Should not fail!") +} + +#[test] +fn exact_fee_test7() { + test_exact_defiplaza_fees_amounts( + // Initial supply for the pool. + None, + // Initial price of the pool + dec!(1), + // The pair config of the pool* + PairConfig { + k_in: dec!(0.5), + k_out: dec!(1), + fee: dec!(0.02), + decay_factor: dec!(0.9512), + }, + // User contribution to the pool. This would mean that the user would + // own 100% of the pool. AssetIndexedData { - user_resource: EqualityCheck::ExactlyEquals(dec!(0)), - protocol_resource: EqualityCheck::ExactlyEquals(dec!(0.03)), + user_resource: dec!(5000), + protocol_resource: dec!(5000), }, + // The swaps to perform - the asset you see is the input asset + vec![ + (Asset::ProtocolResource, dec!(3996)), + (Asset::UserResource, dec!(898)), + (Asset::ProtocolResource, dec!(7953)), + (Asset::ProtocolResource, dec!(3390)), + (Asset::ProtocolResource, dec!(4297)), + (Asset::ProtocolResource, dec!(2252)), + (Asset::UserResource, dec!(5835)), + (Asset::ProtocolResource, dec!(5585)), + (Asset::UserResource, dec!(7984)), + (Asset::ProtocolResource, dec!(8845)), + (Asset::ProtocolResource, dec!(4511)), + (Asset::UserResource, dec!(1407)), + (Asset::UserResource, dec!(4026)), + (Asset::UserResource, dec!(8997)), + (Asset::ProtocolResource, dec!(1950)), + (Asset::UserResource, dec!(8016)), + (Asset::UserResource, dec!(8322)), + (Asset::UserResource, dec!(5149)), + (Asset::ProtocolResource, dec!(6411)), + (Asset::ProtocolResource, dec!(1013)), + (Asset::ProtocolResource, dec!(3333)), + (Asset::ProtocolResource, dec!(4130)), + (Asset::UserResource, dec!(2786)), + (Asset::UserResource, dec!(5828)), + (Asset::UserResource, dec!(8974)), + (Asset::UserResource, dec!(6476)), + (Asset::ProtocolResource, dec!(8942)), + (Asset::UserResource, dec!(2159)), + (Asset::UserResource, dec!(8387)), + (Asset::UserResource, dec!(2830)), + ], ) .expect("Should not fail!") } +#[allow(dead_code)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] enum Asset { UserResource, @@ -958,26 +1152,24 @@ struct AssetIndexedData { protocol_resource: T, } -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -enum EqualityCheck { - ExactlyEquals(T), - ApproximatelyEquals { value: T, acceptable_difference: T }, -} - +/// This test will open a position for the user in a Defiplaza liquidity +/// pool and then perform a bunch of swaps to generate fees and then asset +/// that the amount of fees obtained as reported by the adapter matches the +/// amount that we expect the fees to be. An important note, Defiplaza fees +/// are collected on the output token and not the input token, so they're a +/// percentage of the output amount. fn test_exact_defiplaza_fees_amounts( // The initial amount of liquidity to provide when creating the liquidity // pool. - initial_liquidity: AssetIndexedData, + initial_liquidity: Option>, // The price to set as the initial price of the pool. initial_price: Decimal, - // The fee percentage of the pool - fee_percentage: Decimal, + // The pair configuration of the defiplaza pool + pair_configuration: PairConfig, // The contribution that the user will make to the pool user_contribution: AssetIndexedData, // The swaps to perform on the pool. swaps: Vec<(Asset, Decimal)>, - // Equality checks to perform when closing the liquidity position. - expected_fees: AssetIndexedData>, ) -> Result<(), RuntimeError> { let Environment { environment: ref mut env, @@ -1000,35 +1192,32 @@ fn test_exact_defiplaza_fees_amounts( OwnerRole::None, resources.user_resource, resources.protocol_resource, - PairConfig { - k_in: dec!("0.4"), - k_out: dec!("1"), - fee: fee_percentage, - decay_factor: dec!("0.9512"), - }, + pair_configuration, initial_price, defiplaza_v2.package, env, )?; // Providing the desired initial contribution to the pool. - [ - (resources.user_resource, initial_liquidity.user_resource), - ( - resources.protocol_resource, - initial_liquidity.protocol_resource, - ), - ] - .map(|(resource_address, amount)| { - let bucket = ResourceManager(resource_address) - .mint_fungible(amount, env) - .unwrap(); - let (_, change) = pool.add_liquidity(bucket, None, env).unwrap(); - let change_amount = change - .map(|bucket| bucket.amount(env).unwrap()) - .unwrap_or(Decimal::ZERO); - assert_eq!(change_amount, Decimal::ZERO); - }); + if let Some(initial_liquidity) = initial_liquidity { + [ + (resources.user_resource, initial_liquidity.user_resource), + ( + resources.protocol_resource, + initial_liquidity.protocol_resource, + ), + ] + .map(|(resource_address, amount)| { + let bucket = ResourceManager(resource_address) + .mint_fungible(amount, env) + .unwrap(); + let (_, change) = pool.add_liquidity(bucket, None, env).unwrap(); + let change_amount = change + .map(|bucket| bucket.amount(env).unwrap()) + .unwrap_or(Decimal::ZERO); + assert_eq!(change_amount, Decimal::ZERO); + }); + } // Providing the user's contribution to the pool through the adapter let [bucket_x, bucket_y] = [ @@ -1061,6 +1250,7 @@ fn test_exact_defiplaza_fees_amounts( } // Performing the swaps specified by the user + let mut expected_fee_amounts = IndexMap::::new(); for (asset, amount) in swaps.into_iter() { let address = match asset { Asset::ProtocolResource => resources.protocol_resource, @@ -1068,7 +1258,14 @@ fn test_exact_defiplaza_fees_amounts( }; let bucket = ResourceManager(address).mint_fungible(amount, env).unwrap(); - let _ = pool.swap(bucket, env)?; + let (output, _) = pool.swap(bucket, env)?; + let output_resource_address = output.resource_address(env)?; + let swap_output_amount = output.amount(env)?; + let fee = swap_output_amount / (Decimal::ONE - pair_configuration.fee) + * pair_configuration.fee; + *expected_fee_amounts + .entry(output_resource_address) + .or_default() += fee; } // Close the liquidity position @@ -1081,28 +1278,25 @@ fn test_exact_defiplaza_fees_amounts( )?; // Assert that the fees is what's expected. - for (resource_address, equality_check) in [ - (resources.protocol_resource, expected_fees.protocol_resource), - (resources.user_resource, expected_fees.user_resource), - ] { - // Get the fees - let resource_fees = fees.get(&resource_address).copied().unwrap(); - - // Perform the assertion - match equality_check { - EqualityCheck::ExactlyEquals(value) => { - assert_eq!(resource_fees, value) - } - EqualityCheck::ApproximatelyEquals { - value, - acceptable_difference, - } => { - assert!( - (resource_fees - value).checked_abs().unwrap() - <= acceptable_difference - ) - } - } + for resource_address in + [resources.protocol_resource, resources.user_resource] + { + let expected_fees = expected_fee_amounts + .get(&resource_address) + .copied() + .unwrap_or_default(); + let fees = fees.get(&resource_address).copied().unwrap_or_default(); + + let resource_name = if resource_address == resources.protocol_resource { + "protocol" + } else { + "user" + }; + + assert!( + expected_fees - fees <= dec!(0.000001), + "{resource_name} resource assertion failed. Expected: {expected_fees}, Actual: {fees}" + ); } Ok(())