From 59a7a50958c6c8b7afe3d74c4a3352bd756dd67e Mon Sep 17 00:00:00 2001 From: Omar Date: Tue, 20 Feb 2024 21:50:45 +0300 Subject: [PATCH] [Caviarnine v1 Adapter v1]: Use a more fee-efficient calculation. --- packages/caviarnine-v1-adapter-v1/src/lib.rs | 269 +++++---- tests/tests/caviarnine_v1.rs | 364 +++++++++++ tests/tests/caviarnine_v1_simulation.rs | 599 +++++++++++++++++++ 3 files changed, 1124 insertions(+), 108 deletions(-) diff --git a/packages/caviarnine-v1-adapter-v1/src/lib.rs b/packages/caviarnine-v1-adapter-v1/src/lib.rs index d4696e80..809be5ad 100644 --- a/packages/caviarnine-v1-adapter-v1/src/lib.rs +++ b/packages/caviarnine-v1-adapter-v1/src/lib.rs @@ -31,7 +31,6 @@ macro_rules! define_error { define_error! { RESOURCE_DOES_NOT_BELONG_ERROR => "One or more of the resources do not belong to pool."; - NO_ACTIVE_BIN_ERROR => "Pool has no active bin."; NO_ACTIVE_AMOUNTS_ERROR => "Pool has no active amounts."; NO_PRICE_ERROR => "Pool has no price."; OVERFLOW_ERROR => "Overflow error."; @@ -65,7 +64,7 @@ macro_rules! pool { pub const PREFERRED_TOTAL_NUMBER_OF_HIGHER_AND_LOWER_BINS: u32 = 30 * 2; #[blueprint_with_traits] -#[types(ComponentAddress, PoolInformation)] +#[types(ComponentAddress, PoolInformation, Decimal, PreciseDecimal)] pub mod adapter { struct CaviarnineV1Adapter { /// A cache of the information of the pool, this is done so that we do @@ -676,121 +675,175 @@ fn calculate_bin_amounts_due_to_price_action( let bin_lower_price = tick_to_spot(lower_tick)?; let bin_upper_price = tick_to_spot(upper_tick)?; - // Determine the starting and ending prices to use in the math. - - let (starting_price, ending_price) = { - let bin_composition_when_position_opened = match ( - bin_amount.resource_x == Decimal::ZERO, - bin_amount.resource_y == Decimal::ZERO, - ) { - (true, true) => return None, - (true, false) => Composition::EntirelyY, - (false, true) => Composition::EntirelyX, - (false, false) => Composition::Composite, - }; - - // Determine what we expect the composition of this bin to - // be based on the current price. - let expected_bin_composition_now = match tick.cmp(&active_tick) { - // Case A: The current price is inside this bin. Since - // we are the current active bin then it's expected that - // this bin has both X and Y assets. - Ordering::Equal => Composition::Composite, - // Case B: The current price of the pool is greater than - // the upper bound of the bin. We're outside of that - // range and there should only be Y assets in the bin. - Ordering::Less => Composition::EntirelyY, - // Case C: The current price of the pool is smaller than - // the lower bound of the bin. We're outside of that - // range and there should only be X assets in the bin. - Ordering::Greater => Composition::EntirelyX, - }; - - match ( - bin_composition_when_position_opened, - expected_bin_composition_now, - ) { - // The bin was entirely made of X and is still the same. We - // have not touched it. The starting and ending price of the - // "swap" is the same. - (Composition::EntirelyX, Composition::EntirelyX) => { - (bin_lower_price, bin_lower_price) - } - (Composition::EntirelyY, Composition::EntirelyY) => { - (bin_upper_price, bin_upper_price) - } - // The bin was entirely made up of one asset and is now made - // up of another. - (Composition::EntirelyX, Composition::EntirelyY) => { - (bin_lower_price, bin_upper_price) - } - (Composition::EntirelyY, Composition::EntirelyX) => { - (bin_upper_price, bin_lower_price) - } - // The bin was entirely made up of one of the assets and - // is now made up of both of them. - (Composition::EntirelyX, Composition::Composite) => { - (bin_lower_price, current_price) - } - (Composition::EntirelyY, Composition::Composite) => { - (bin_upper_price, current_price) - } - // The bin was made up of both assets and is now just made - // up of one of them. - (Composition::Composite, Composition::EntirelyX) => { - (price_when_position_was_opened, bin_lower_price) - } - (Composition::Composite, Composition::EntirelyY) => { - (price_when_position_was_opened, bin_upper_price) - } - // The bin was made up of both assets and is still made up - // of both assets. - (Composition::Composite, Composition::Composite) => { - (price_when_position_was_opened, current_price) - } - } + let bin_composition_when_position_opened = match ( + bin_amount.resource_x == Decimal::ZERO, + bin_amount.resource_y == Decimal::ZERO, + ) { + (true, true) => return None, + (true, false) => Composition::EntirelyY, + (false, true) => Composition::EntirelyX, + (false, false) => Composition::Composite, }; - // Small fee optimization - if the starting and ending price are the - // same then do not calculate the L. In this case the change is 0 - // and the amount is the same. - let (new_x, new_y) = if starting_price == ending_price { - (bin_amount.resource_x, bin_amount.resource_y) - } else { - let liquidity = - calculate_liquidity(bin_amount, bin_lower_price, bin_upper_price)?; - - let change_x = liquidity.checked_mul( - Decimal::ONE - .checked_div(ending_price.checked_sqrt()?)? - .checked_sub( - Decimal::ONE.checked_div(starting_price.checked_sqrt()?)?, - )?, - )?; - let change_y = liquidity.checked_mul( - ending_price - .checked_sqrt()? - .checked_sub(starting_price.checked_sqrt()?)?, - )?; - - let new_x = max(bin_amount.resource_x.checked_add(change_x)?, Decimal::ZERO); - let new_y = max(bin_amount.resource_y.checked_add(change_y)?, Decimal::ZERO); - - (new_x, new_y) + // Determine what we expect the composition of this bin to + // be based on the current price. + let expected_bin_composition_now = match tick.cmp(&active_tick) { + // Case A: The current price is inside this bin. Since + // we are the current active bin then it's expected that + // this bin has both X and Y assets. + Ordering::Equal => Composition::Composite, + // Case B: The current price of the pool is greater than + // the upper bound of the bin. We're outside of that + // range and there should only be Y assets in the bin. + Ordering::Less => Composition::EntirelyY, + // Case C: The current price of the pool is smaller than + // the lower bound of the bin. We're outside of that + // range and there should only be X assets in the bin. + Ordering::Greater => Composition::EntirelyX, }; - Some(( - tick, - ResourceIndexedData { - resource_x: new_x, - resource_y: new_y, - }, - )) + let new_contents = match ( + bin_composition_when_position_opened, + expected_bin_composition_now, + ) { + // The bin was entirely made of X and is still the same. + // Thus, this bin "has not been touched" and should in + // theory contain the same amount as before. Difference + // found can therefore be attributed to fees. The other + // case is when the bin was made of up just Y and still + // is just Y. + (Composition::EntirelyX, Composition::EntirelyX) => { + Some((bin_amount.resource_x, bin_amount.resource_y)) + } + (Composition::EntirelyY, Composition::EntirelyY) => { + Some((bin_amount.resource_x, bin_amount.resource_y)) + } + // The bin was entirely made up of one asset and is now + // made up of another. We therefore want to do a full + // "swap" of that amount. For this calculation we use + // y = sqrt(pa * pb) * x + (Composition::EntirelyX, Composition::EntirelyY) => Some(( + dec!(0), + bin_lower_price + .checked_mul(bin_upper_price) + .and_then(|value| value.checked_sqrt()) + .and_then(|value| value.checked_mul(bin_amount.resource_x)) + .expect(OVERFLOW_ERROR), + )), + (Composition::EntirelyY, Composition::EntirelyX) => Some(( + bin_lower_price + .checked_mul(bin_upper_price) + .and_then(|value| value.checked_sqrt()) + .and_then(|value| bin_amount.resource_y.checked_div(value)) + .expect(OVERFLOW_ERROR), + dec!(0), + )), + // The bin was entirely made up of one of the assets and + // is now made up of both of them. + (Composition::EntirelyX, Composition::Composite) => { + let (starting_price, ending_price) = (bin_lower_price, current_price); + calculate_bin_amount_using_liquidity( + bin_amount, + bin_lower_price, + bin_upper_price, + starting_price, + ending_price, + ) + } + (Composition::EntirelyY, Composition::Composite) => { + let (starting_price, ending_price) = (bin_upper_price, current_price); + calculate_bin_amount_using_liquidity( + bin_amount, + bin_lower_price, + bin_upper_price, + starting_price, + ending_price, + ) + } + // The bin was made up of both assets and is now just made + // up of one of them. + (Composition::Composite, Composition::EntirelyX) => { + let (starting_price, ending_price) = + (price_when_position_was_opened, bin_lower_price); + calculate_bin_amount_using_liquidity( + bin_amount, + bin_lower_price, + bin_upper_price, + starting_price, + ending_price, + ) + } + (Composition::Composite, Composition::EntirelyY) => { + let (starting_price, ending_price) = + (price_when_position_was_opened, bin_upper_price); + calculate_bin_amount_using_liquidity( + bin_amount, + bin_lower_price, + bin_upper_price, + starting_price, + ending_price, + ) + } + // The bin was made up of both assets and is still made up + // of both assets. + (Composition::Composite, Composition::Composite) => { + let (starting_price, ending_price) = + (price_when_position_was_opened, current_price); + calculate_bin_amount_using_liquidity( + bin_amount, + bin_lower_price, + bin_upper_price, + starting_price, + ending_price, + ) + } + }; + + new_contents.map(|contents| { + ( + tick, + ResourceIndexedData { + resource_x: contents.0, + resource_y: contents.1, + }, + ) + }) }, ) .collect() } +fn calculate_bin_amount_using_liquidity( + bin_amount: ResourceIndexedData, + bin_lower_price: Decimal, + bin_upper_price: Decimal, + starting_price: Decimal, + ending_price: Decimal, +) -> Option<(Decimal, Decimal)> { + let liquidity = + calculate_liquidity(bin_amount, bin_lower_price, bin_upper_price)?; + + let change_x = liquidity.checked_mul( + Decimal::ONE + .checked_div(ending_price.checked_sqrt()?)? + .checked_sub( + Decimal::ONE.checked_div(starting_price.checked_sqrt()?)?, + )?, + )?; + let change_y = liquidity.checked_mul( + ending_price + .checked_sqrt()? + .checked_sub(starting_price.checked_sqrt()?)?, + )?; + + let new_x = + max(bin_amount.resource_x.checked_add(change_x)?, Decimal::ZERO); + let new_y = + max(bin_amount.resource_y.checked_add(change_y)?, Decimal::ZERO); + + Some((new_x, new_y)) +} + #[cfg(test)] mod test { use super::*; diff --git a/tests/tests/caviarnine_v1.rs b/tests/tests/caviarnine_v1.rs index 7c1108a8..d11f1a27 100644 --- a/tests/tests/caviarnine_v1.rs +++ b/tests/tests/caviarnine_v1.rs @@ -854,3 +854,367 @@ pub fn price_and_active_tick_reported_by_adapter_must_match_whats_reported_by_po Ok(()) } + +#[test] +fn positions_can_be_opened_at_current_price_and_closed_at_a_100x_price_decrease_on_100_bps_pools( +) { + test_effect_of_price_action_on_fees(-100, 100) +} + +fn test_effect_of_price_action_on_fees(multiplier: i32, bin_span: u32) { + let ScryptoUnitEnv { + environment: mut test_runner, + resources, + protocol, + caviarnine_v1, + .. + } = ScryptoUnitEnv::new_with_configuration(Configuration { + maximum_allowed_relative_price_difference: dec!(0.03), + ..Default::default() + }); + let (_, private_key, account_address, _) = protocol.protocol_owner_badge; + + let resource_x = XRD; + let resource_y = resources.bitcoin; + + let pool_address = test_runner + .execute_manifest( + ManifestBuilder::new() + .lock_fee_from_faucet() + .caviarnine_v1_pool_new( + caviarnine_v1.package, + rule!(allow_all), + rule!(allow_all), + resource_x, + resource_y, + bin_span, + None, + ) + .build(), + vec![], + ) + .expect_commit_success() + .new_component_addresses() + .first() + .copied() + .unwrap(); + + // We're allowed to contribute to 200 bins. So, we will contribute to all of + // them. This ensures that the maximum amount of price range is covered by + // our liquidity. + { + let positions = vec![(27000u32, dec!(100_000_000), dec!(100_000_000))] + .into_iter() + .chain((1..=99).flat_map(|i| { + vec![ + ( + 27000 - i * bin_span, + dec!(0), + dec!(100_000_000) - Decimal::from(i), + ), + ( + 27000 + i * bin_span, + dec!(100_000_000) + Decimal::from(i), + dec!(0), + ), + ] + })) + .collect::>(); + let x_amount_required = positions + .iter() + .map(|v| v.1) + .reduce(|acc, item| acc + item) + .unwrap_or_default(); + let y_amount_required = positions + .iter() + .map(|v| v.2) + .reduce(|acc, item| acc + item) + .unwrap_or_default(); + + test_runner + .execute_manifest_with_enabled_modules( + ManifestBuilder::new() + .mint_fungible(resource_x, x_amount_required) + .mint_fungible(resource_y, y_amount_required) + .take_all_from_worktop(resource_x, "resources_x") + .take_all_from_worktop(resource_y, "resources_y") + .with_name_lookup(|builder, namer| { + let resources_x = namer.bucket("resources_x"); + let resources_y = namer.bucket("resources_y"); + + builder.caviarnine_v1_pool_add_liquidity( + pool_address, + resources_x, + resources_y, + positions, + ) + }) + .deposit_batch(account_address) + .build(), + EnabledModules::for_notarized_transaction() + & !EnabledModules::AUTH + & !EnabledModules::COSTING, + ) + .expect_commit_success(); + } + + // Adding this pool to Ignition. + test_runner + .execute_manifest_without_auth( + ManifestBuilder::new() + .lock_fee_from_faucet() + .call_method( + protocol.ignition, + "add_allowed_pool", + (pool_address,), + ) + .build(), + ) + .expect_commit_success(); + + // Adding this pool to Ignition. + test_runner + .execute_manifest_without_auth( + ManifestBuilder::new() + .lock_fee_from_faucet() + .call_method( + protocol.ignition, + "add_allowed_pool", + (pool_address,), + ) + .build(), + ) + .expect_commit_success(); + + // Updating the price in the Oracle component. + let price = test_runner + .execute_manifest( + ManifestBuilder::new() + .lock_fee_from_faucet() + .caviarnine_v1_pool_get_price(pool_address) + .build(), + vec![], + ) + .expect_commit_success() + .output::>(1) + .unwrap(); + test_runner + .execute_manifest_without_auth( + ManifestBuilder::new() + .lock_fee_from_faucet() + .call_method( + protocol.oracle, + "set_price", + (resource_x, resource_y, price), + ) + .call_method( + protocol.oracle, + "set_price", + (resource_y, resource_x, 1 / price), + ) + .build(), + ) + .expect_commit_success(); + + // Minting some of the resource and depositing them into the user's + // account. + test_runner + .execute_manifest_without_auth( + ManifestBuilder::new() + .lock_fee_from_faucet() + .mint_fungible(resource_y, dec!(100_000)) + .deposit_batch(account_address) + .build(), + ) + .expect_commit_success(); + + let receipt = test_runner.construct_and_execute_notarized_transaction( + ManifestBuilder::new() + .lock_fee(account_address, dec!(10)) + .withdraw_from_account(account_address, resource_y, dec!(1000)) + .take_all_from_worktop(resource_y, "bucket") + .with_bucket("bucket", |builder, bucket| { + builder.call_method( + protocol.ignition, + "open_liquidity_position", + ( + bucket, + pool_address, + LockupPeriod::from_months(6).unwrap(), + ), + ) + }) + .deposit_batch(account_address) + .build(), + &private_key, + ); + receipt.expect_commit_success(); + println!( + "Open - Multiplier = {}x, Bin Span = {}, Cost = {} XRD, Execution Cost = {} XRD", + multiplier, + bin_span, + receipt.fee_summary.total_cost(), + receipt.fee_summary.total_execution_cost_in_xrd + ); + + // Set the current time to be 6 months from now. + { + let current_time = + test_runner.get_current_time(TimePrecisionV2::Minute); + let maturity_instant = + current_time + .add_seconds( + *LockupPeriod::from_months(6).unwrap().seconds() as i64 + ) + .unwrap(); + let db = test_runner.substate_db_mut(); + let mut writer = SystemDatabaseWriter::new(db); + + writer + .write_typed_object_field( + CONSENSUS_MANAGER.as_node_id(), + ModuleId::Main, + ConsensusManagerField::ProposerMilliTimestamp.field_index(), + ConsensusManagerProposerMilliTimestampFieldPayload::from_content_source( + ProposerMilliTimestampSubstate { + epoch_milli: maturity_instant.seconds_since_unix_epoch * 1000, + }, + ), + ) + .unwrap(); + + writer + .write_typed_object_field( + CONSENSUS_MANAGER.as_node_id(), + ModuleId::Main, + ConsensusManagerField::ProposerMinuteTimestamp.field_index(), + ConsensusManagerProposerMinuteTimestampFieldPayload::from_content_source( + ProposerMinuteTimestampSubstate { + epoch_minute: i32::try_from( + maturity_instant.seconds_since_unix_epoch / 60, + ) + .unwrap(), + }, + ), + ) + .unwrap(); + } + + // Move the price according to the specified multiplier + let new_price = if multiplier.is_positive() { + let target_price = price * multiplier; + let input_resource = resource_y; + let amount_in_each_swap = dec!(100_000_000); + + let mut new_price = price; + while new_price < target_price { + let reported_price = test_runner + .execute_manifest_without_auth( + ManifestBuilder::new() + .lock_fee_from_faucet() + .mint_fungible(input_resource, amount_in_each_swap) + .take_all_from_worktop(input_resource, "bucket") + .with_bucket("bucket", |builder, bucket| { + builder + .caviarnine_v1_pool_swap(pool_address, bucket) + }) + .deposit_batch(account_address) + .caviarnine_v1_pool_get_price(pool_address) + .build(), + ) + .expect_commit_success() + .output::>(5) + .unwrap(); + + if reported_price == new_price { + break; + } else { + new_price = reported_price + } + } + + new_price + } else { + let target_price = price / multiplier * dec!(-1); + let input_resource = resource_x; + let amount_in_each_swap = dec!(1_000_000_000); + + let mut new_price = price; + while new_price > target_price { + let reported_price = test_runner + .execute_manifest_without_auth( + ManifestBuilder::new() + .lock_fee_from_faucet() + .mint_fungible(input_resource, amount_in_each_swap) + .take_all_from_worktop(input_resource, "bucket") + .with_bucket("bucket", |builder, bucket| { + builder + .caviarnine_v1_pool_swap(pool_address, bucket) + }) + .deposit_batch(account_address) + .caviarnine_v1_pool_get_price(pool_address) + .build(), + ) + .expect_commit_success() + .output::>(5) + .unwrap(); + + if reported_price == new_price { + break; + } else { + new_price = reported_price + } + } + + new_price + }; + + // Submit the new price to the oracle + test_runner + .execute_manifest_without_auth( + ManifestBuilder::new() + .lock_fee_from_faucet() + .call_method( + protocol.oracle, + "set_price", + (resource_x, resource_y, new_price), + ) + .call_method( + protocol.oracle, + "set_price", + (resource_y, resource_x, 1 / new_price), + ) + .build(), + ) + .expect_commit_success(); + + // Close the position + let receipt = test_runner.construct_and_execute_notarized_transaction( + ManifestBuilder::new() + .lock_fee(account_address, dec!(10)) + .withdraw_from_account( + account_address, + caviarnine_v1.liquidity_receipt, + dec!(1), + ) + .take_all_from_worktop(caviarnine_v1.liquidity_receipt, "bucket") + .with_bucket("bucket", |builder, bucket| { + builder.call_method( + protocol.ignition, + "close_liquidity_position", + (bucket,), + ) + }) + .deposit_batch(account_address) + .build(), + &private_key, + ); + receipt.expect_commit_success(); + println!( + "Close - Multiplier = {}x, Bin Span = {}, Cost = {} XRD, Execution Cost = {} XRD", + multiplier, + bin_span, + receipt.fee_summary.total_cost(), + receipt.fee_summary.total_execution_cost_in_xrd + ); +} diff --git a/tests/tests/caviarnine_v1_simulation.rs b/tests/tests/caviarnine_v1_simulation.rs index c609fd80..36accb9a 100644 --- a/tests/tests/caviarnine_v1_simulation.rs +++ b/tests/tests/caviarnine_v1_simulation.rs @@ -390,6 +390,605 @@ fn can_open_and_close_positions_to_all_mainnet_caviarnine_pools() { } } +macro_rules! define_price_test { + ( + $($multiplier: expr),* $(,)? + ) => { + paste::paste! { + $( + #[test] + fn []( + ) { + let env = ScryptoUnitEnv::new(); + let pool_information = mainnet_state::pool_information(&env.resources); + let pool_information = ResourceInformation { + bitcoin: (pool_information.bitcoin, 8), + ethereum: (pool_information.ethereum, 18), + usdc: (pool_information.usdc, 6), + usdt: (pool_information.usdt, 6), + }; + test_effect_of_price_action_on_fees(- $multiplier, env, pool_information.bitcoin.0, pool_information.bitcoin.1 ) + } + #[test] + fn []( + ) { + let env = ScryptoUnitEnv::new(); + let pool_information = mainnet_state::pool_information(&env.resources); + let pool_information = ResourceInformation { + bitcoin: (pool_information.bitcoin, 8), + ethereum: (pool_information.ethereum, 18), + usdc: (pool_information.usdc, 6), + usdt: (pool_information.usdt, 6), + }; + test_effect_of_price_action_on_fees(- $multiplier, env, pool_information.ethereum.0, pool_information.ethereum.1 ) + } + #[test] + fn []( + ) { + let env = ScryptoUnitEnv::new(); + let pool_information = mainnet_state::pool_information(&env.resources); + let pool_information = ResourceInformation { + bitcoin: (pool_information.bitcoin, 8), + ethereum: (pool_information.ethereum, 18), + usdc: (pool_information.usdc, 6), + usdt: (pool_information.usdt, 6), + }; + test_effect_of_price_action_on_fees(- $multiplier, env, pool_information.usdc.0, pool_information.usdc.1 ) + } + #[test] + fn []( + ) { + let env = ScryptoUnitEnv::new(); + let pool_information = mainnet_state::pool_information(&env.resources); + let pool_information = ResourceInformation { + bitcoin: (pool_information.bitcoin, 8), + ethereum: (pool_information.ethereum, 18), + usdc: (pool_information.usdc, 6), + usdt: (pool_information.usdt, 6), + }; + test_effect_of_price_action_on_fees(- $multiplier, env, pool_information.usdt.0, pool_information.usdt.1 ) + } + )* + $( + #[test] + fn []( + ) { + let env = ScryptoUnitEnv::new(); + let pool_information = mainnet_state::pool_information(&env.resources); + let pool_information = ResourceInformation { + bitcoin: (pool_information.bitcoin, 8), + ethereum: (pool_information.ethereum, 18), + usdc: (pool_information.usdc, 6), + usdt: (pool_information.usdt, 6), + }; + test_effect_of_price_action_on_fees($multiplier, env, pool_information.bitcoin.0, pool_information.bitcoin.1 ) + } + #[test] + fn []( + ) { + let env = ScryptoUnitEnv::new(); + let pool_information = mainnet_state::pool_information(&env.resources); + let pool_information = ResourceInformation { + bitcoin: (pool_information.bitcoin, 8), + ethereum: (pool_information.ethereum, 18), + usdc: (pool_information.usdc, 6), + usdt: (pool_information.usdt, 6), + }; + test_effect_of_price_action_on_fees($multiplier, env, pool_information.ethereum.0, pool_information.ethereum.1 ) + } + #[test] + fn []( + ) { + let env = ScryptoUnitEnv::new(); + let pool_information = mainnet_state::pool_information(&env.resources); + let pool_information = ResourceInformation { + bitcoin: (pool_information.bitcoin, 8), + ethereum: (pool_information.ethereum, 18), + usdc: (pool_information.usdc, 6), + usdt: (pool_information.usdt, 6), + }; + test_effect_of_price_action_on_fees($multiplier, env, pool_information.usdc.0, pool_information.usdc.1 ) + } + #[test] + fn []( + ) { + let env = ScryptoUnitEnv::new(); + let pool_information = mainnet_state::pool_information(&env.resources); + let pool_information = ResourceInformation { + bitcoin: (pool_information.bitcoin, 8), + ethereum: (pool_information.ethereum, 18), + usdc: (pool_information.usdc, 6), + usdt: (pool_information.usdt, 6), + }; + test_effect_of_price_action_on_fees($multiplier, env, pool_information.usdt.0, pool_information.usdt.1 ) + } + )* + } + }; +} + +define_price_test! { + 100, + 90, + 80, + 70, + 60, + 50, + 40, + 30, + 20, + 10, +} + +fn test_effect_of_price_action_on_fees( + multiplier: i32, + env: ScryptoUnitEnv, + pool_information: mainnet_state::PoolInformation, + divisibility: u8, +) { + let ScryptoUnitEnv { + environment: mut test_runner, + protocol, + caviarnine_v1, + .. + } = env; + let (_, private_key, account_address, _) = protocol.protocol_owner_badge; + + let mainnet_state::PoolInformation { + resource_x, + resource_y, + active_tick, + bin_span, + bins_below, + bins_above, + price, + } = pool_information; + + let user_resource_address = if resource_x == XRD { + resource_y + } else { + resource_x + }; + + let pool_address = test_runner + .execute_manifest( + ManifestBuilder::new() + .lock_fee_from_faucet() + .caviarnine_v1_pool_new( + caviarnine_v1.package, + rule!(allow_all), + rule!(allow_all), + resource_x, + resource_y, + bin_span, + None, + ) + .build(), + vec![], + ) + .expect_commit_success() + .new_component_addresses() + .first() + .copied() + .unwrap(); + + // Providing liquidity identical to the mainnet pool + { + // Providing the liquidity to the pools. + let (divisibility_x, divisibility_y) = if resource_x == XRD { + (18, divisibility) + } else { + (divisibility, 18) + }; + + let mut amount_in_bins = { + let mut amount = indexmap! {}; + + for (tick, amount_x) in bins_above { + let (x, _) = amount.entry(tick).or_insert((dec!(0), dec!(0))); + *x = amount_x + *x; + } + + for (tick, amount_y) in bins_below { + let (_, y) = amount.entry(tick).or_insert((dec!(0), dec!(0))); + *y = amount_y + *y; + } + + amount + }; + + let amount_x = amount_in_bins + .values() + .map(|x| x.0) + .reduce(|acc, item| acc + item) + .unwrap_or_default(); + let amount_y = amount_in_bins + .values() + .map(|x| x.1) + .reduce(|acc, item| acc + item) + .unwrap_or_default(); + + let amount_x = amount_x + .checked_round(divisibility_x, RoundingMode::ToPositiveInfinity) + .unwrap(); + let amount_y = amount_y + .checked_round(divisibility_y, RoundingMode::ToPositiveInfinity) + .unwrap(); + + let active_amounts = + amount_in_bins.remove(&active_tick.unwrap()).unwrap(); + let positions = + vec![(active_tick.unwrap(), active_amounts.0, active_amounts.1)] + .into_iter() + .chain(amount_in_bins.into_iter().map(|(k, v)| { + ( + k, + v.0.checked_round(divisibility_x, RoundingMode::ToZero) + .unwrap(), + v.1.checked_round(divisibility_y, RoundingMode::ToZero) + .unwrap(), + ) + })) + .collect::>(); + + let price_in_simulated_pool = test_runner + .execute_manifest_without_auth( + ManifestBuilder::new() + .lock_fee_from_faucet() + .mint_fungible(resource_x, amount_x) + .mint_fungible(resource_y, amount_y) + .take_all_from_worktop(resource_x, "resource_x") + .take_all_from_worktop(resource_y, "resource_y") + .with_bucket("resource_x", |builder, bucket_x| { + builder.with_bucket( + "resource_y", + |builder, bucket_y| { + builder.caviarnine_v1_pool_add_liquidity( + pool_address, + bucket_x, + bucket_y, + positions, + ) + }, + ) + }) + .deposit_batch(account_address) + .caviarnine_v1_pool_get_price(pool_address) + .build(), + ) + .expect_commit_success() + .output::>(7) + .unwrap(); + + // If this assertion passes, then the pool we've created should be in + // the same state as the mainnet one. + assert_eq!(price_in_simulated_pool, price.unwrap()); + } + + // We're allowed to contribute to 200 bins. So, we will contribute to all of + // them. This ensures that the maximum amount of price range is covered by + // our liquidity. + { + let positions = vec![(27000u32, dec!(100_000_000), dec!(100_000_000))] + .into_iter() + .chain((1..=99).flat_map(|i| { + vec![ + ( + 27000 - i * bin_span, + dec!(0), + dec!(100_000_000) - Decimal::from(i), + ), + ( + 27000 + i * bin_span, + dec!(100_000_000) + Decimal::from(i), + dec!(0), + ), + ] + })) + .collect::>(); + let x_amount_required = positions + .iter() + .map(|v| v.1) + .reduce(|acc, item| acc + item) + .unwrap_or_default(); + let y_amount_required = positions + .iter() + .map(|v| v.2) + .reduce(|acc, item| acc + item) + .unwrap_or_default(); + + test_runner + .execute_manifest_with_enabled_modules( + ManifestBuilder::new() + .mint_fungible(resource_x, x_amount_required) + .mint_fungible(resource_y, y_amount_required) + .take_all_from_worktop(resource_x, "resources_x") + .take_all_from_worktop(resource_y, "resources_y") + .with_name_lookup(|builder, namer| { + let resources_x = namer.bucket("resources_x"); + let resources_y = namer.bucket("resources_y"); + + builder.caviarnine_v1_pool_add_liquidity( + pool_address, + resources_x, + resources_y, + positions, + ) + }) + .deposit_batch(account_address) + .build(), + EnabledModules::for_notarized_transaction() + & !EnabledModules::AUTH + & !EnabledModules::COSTING, + ) + .expect_commit_success(); + } + + // Adding this pool to Ignition. + test_runner + .execute_manifest_without_auth( + ManifestBuilder::new() + .lock_fee_from_faucet() + .call_method( + protocol.ignition, + "add_allowed_pool", + (pool_address,), + ) + .build(), + ) + .expect_commit_success(); + + // Adding this pool to Ignition. + test_runner + .execute_manifest_without_auth( + ManifestBuilder::new() + .lock_fee_from_faucet() + .call_method( + protocol.ignition, + "add_allowed_pool", + (pool_address,), + ) + .build(), + ) + .expect_commit_success(); + + // Updating the price in the Oracle component. + let price = test_runner + .execute_manifest( + ManifestBuilder::new() + .lock_fee_from_faucet() + .caviarnine_v1_pool_get_price(pool_address) + .build(), + vec![], + ) + .expect_commit_success() + .output::>(1) + .unwrap(); + test_runner + .execute_manifest_without_auth( + ManifestBuilder::new() + .lock_fee_from_faucet() + .call_method( + protocol.oracle, + "set_price", + (resource_x, resource_y, price), + ) + .call_method( + protocol.oracle, + "set_price", + (resource_y, resource_x, 1 / price), + ) + .build(), + ) + .expect_commit_success(); + + // Minting some of the resource and depositing them into the user's + // account. + test_runner + .execute_manifest_without_auth( + ManifestBuilder::new() + .lock_fee_from_faucet() + .mint_fungible(resource_y, dec!(100_000)) + .deposit_batch(account_address) + .build(), + ) + .expect_commit_success(); + + let receipt = test_runner.construct_and_execute_notarized_transaction( + ManifestBuilder::new() + .lock_fee(account_address, dec!(10)) + .withdraw_from_account( + account_address, + user_resource_address, + dec!(1000), + ) + .take_all_from_worktop(user_resource_address, "bucket") + .with_bucket("bucket", |builder, bucket| { + builder.call_method( + protocol.ignition, + "open_liquidity_position", + ( + bucket, + pool_address, + LockupPeriod::from_months(6).unwrap(), + ), + ) + }) + .deposit_batch(account_address) + .build(), + &private_key, + ); + receipt.expect_commit_success(); + println!( + "Open - Multiplier = {}x, Bin Span = {}, Cost = {} XRD, Execution Cost = {} XRD", + multiplier, + bin_span, + receipt.fee_summary.total_cost(), + receipt.fee_summary.total_execution_cost_in_xrd + ); + + // Set the current time to be 6 months from now. + { + let current_time = + test_runner.get_current_time(TimePrecisionV2::Minute); + let maturity_instant = + current_time + .add_seconds( + *LockupPeriod::from_months(6).unwrap().seconds() as i64 + ) + .unwrap(); + let db = test_runner.substate_db_mut(); + let mut writer = SystemDatabaseWriter::new(db); + + writer + .write_typed_object_field( + CONSENSUS_MANAGER.as_node_id(), + ModuleId::Main, + ConsensusManagerField::ProposerMilliTimestamp.field_index(), + ConsensusManagerProposerMilliTimestampFieldPayload::from_content_source( + ProposerMilliTimestampSubstate { + epoch_milli: maturity_instant.seconds_since_unix_epoch * 1000, + }, + ), + ) + .unwrap(); + + writer + .write_typed_object_field( + CONSENSUS_MANAGER.as_node_id(), + ModuleId::Main, + ConsensusManagerField::ProposerMinuteTimestamp.field_index(), + ConsensusManagerProposerMinuteTimestampFieldPayload::from_content_source( + ProposerMinuteTimestampSubstate { + epoch_minute: i32::try_from( + maturity_instant.seconds_since_unix_epoch / 60, + ) + .unwrap(), + }, + ), + ) + .unwrap(); + } + + // Move the price according to the specified multiplier + let new_price = if multiplier.is_positive() { + let target_price = price * multiplier; + let input_resource = resource_y; + let amount_in_each_swap = dec!(100_000_000); + + let mut new_price = price; + while new_price < target_price { + let reported_price = test_runner + .execute_manifest_without_auth( + ManifestBuilder::new() + .lock_fee_from_faucet() + .mint_fungible(input_resource, amount_in_each_swap) + .take_all_from_worktop(input_resource, "bucket") + .with_bucket("bucket", |builder, bucket| { + builder + .caviarnine_v1_pool_swap(pool_address, bucket) + }) + .deposit_batch(account_address) + .caviarnine_v1_pool_get_price(pool_address) + .build(), + ) + .expect_commit_success() + .output::>(5) + .unwrap(); + + if reported_price == new_price { + break; + } else { + new_price = reported_price + } + } + + new_price + } else { + let target_price = price / multiplier * dec!(-1); + let input_resource = resource_x; + let amount_in_each_swap = dec!(1_000_000_000); + + let mut new_price = price; + while new_price > target_price { + let reported_price = test_runner + .execute_manifest_without_auth( + ManifestBuilder::new() + .lock_fee_from_faucet() + .mint_fungible(input_resource, amount_in_each_swap) + .take_all_from_worktop(input_resource, "bucket") + .with_bucket("bucket", |builder, bucket| { + builder + .caviarnine_v1_pool_swap(pool_address, bucket) + }) + .deposit_batch(account_address) + .caviarnine_v1_pool_get_price(pool_address) + .build(), + ) + .expect_commit_success() + .output::>(5) + .unwrap(); + + if reported_price == new_price { + break; + } else { + new_price = reported_price + } + } + + new_price + }; + + // Submit the new price to the oracle + test_runner + .execute_manifest_without_auth( + ManifestBuilder::new() + .lock_fee_from_faucet() + .call_method( + protocol.oracle, + "set_price", + (resource_x, resource_y, new_price), + ) + .call_method( + protocol.oracle, + "set_price", + (resource_y, resource_x, 1 / new_price), + ) + .build(), + ) + .expect_commit_success(); + + // Close the position + let receipt = test_runner.construct_and_execute_notarized_transaction( + ManifestBuilder::new() + .lock_fee(account_address, dec!(10)) + .withdraw_from_account( + account_address, + caviarnine_v1.liquidity_receipt, + dec!(1), + ) + .take_all_from_worktop(caviarnine_v1.liquidity_receipt, "bucket") + .with_bucket("bucket", |builder, bucket| { + builder.call_method( + protocol.ignition, + "close_liquidity_position", + (bucket,), + ) + }) + .deposit_batch(account_address) + .build(), + &private_key, + ); + receipt.expect_commit_success(); + println!( + "Close - Multiplier = {}x, Bin Span = {}, Cost = {} XRD, Execution Cost = {} XRD", + multiplier, + bin_span, + receipt.fee_summary.total_cost(), + receipt.fee_summary.total_execution_cost_in_xrd + ); +} + mod mainnet_state { use super::*; use std::sync::*;