From 4233302ebebbe0db555aa1196b52d7b06887abf7 Mon Sep 17 00:00:00 2001 From: Jakub Bogucki Date: Wed, 15 Mar 2023 20:58:11 +0100 Subject: [PATCH] Set version v2.0.0 --- .github/workflows/Release.yml | 14 +- Cargo.lock | 186 ++++++- Cargo.toml | 10 +- contracts/factory/src/testing.rs | 5 +- contracts/gauge-adapter/Cargo.toml | 2 +- contracts/nominated-trader/Cargo.toml | 2 +- contracts/pair/src/contract.rs | 92 +--- contracts/pair/src/testing.rs | 201 +------- .../{pair_stable => pair_lsd}/.cargo/config | 0 .../{pair_stable => pair_lsd}/.editorconfig | 0 .../{pair_stable => pair_lsd}/Cargo.toml | 3 +- contracts/{pair_stable => pair_lsd}/README.md | 0 .../{pair_stable => pair_lsd}/rustfmt.toml | 0 .../src/bin/pair_stable_schema.rs | 0 .../{pair_stable => pair_lsd}/src/contract.rs | 275 ++++++----- .../{pair_stable => pair_lsd}/src/lib.rs | 0 .../{pair_stable => pair_lsd}/src/math.rs | 52 +- .../src/mock_querier.rs | 0 .../{pair_stable => pair_lsd}/src/msg.rs | 0 .../src/multitest/mock_hub.rs | 0 .../src/multitest/mod.rs | 1 + .../pair_lsd/src/multitest/simulation.rs | 452 ++++++++++++++++++ .../src/multitest/suite.rs | 94 +++- .../src/multitest/target_rate.rs | 298 +++++++++++- .../{pair_stable => pair_lsd}/src/state.rs | 27 +- .../{pair_stable => pair_lsd}/src/testing.rs | 221 +-------- .../{pair_stable => pair_lsd}/src/utils.rs | 165 ++++++- .../tests/3pool_tests.rs | 31 +- .../{pair_stable => pair_lsd}/tests/helper.rs | 9 +- .../tests/integration.rs | 46 +- packages/wyndex/src/factory.rs | 3 + packages/wyndex/src/pair.rs | 47 +- packages/wyndex/src/pair/error.rs | 10 + utils/stable-price-solver/Cargo.toml | 11 + utils/stable-price-solver/src/main.rs | 126 +++++ 35 files changed, 1622 insertions(+), 761 deletions(-) rename contracts/{pair_stable => pair_lsd}/.cargo/config (100%) rename contracts/{pair_stable => pair_lsd}/.editorconfig (100%) rename contracts/{pair_stable => pair_lsd}/Cargo.toml (95%) rename contracts/{pair_stable => pair_lsd}/README.md (100%) rename contracts/{pair_stable => pair_lsd}/rustfmt.toml (100%) rename contracts/{pair_stable => pair_lsd}/src/bin/pair_stable_schema.rs (100%) rename contracts/{pair_stable => pair_lsd}/src/contract.rs (89%) rename contracts/{pair_stable => pair_lsd}/src/lib.rs (100%) rename contracts/{pair_stable => pair_lsd}/src/math.rs (86%) rename contracts/{pair_stable => pair_lsd}/src/mock_querier.rs (100%) rename contracts/{pair_stable => pair_lsd}/src/msg.rs (100%) rename contracts/{pair_stable => pair_lsd}/src/multitest/mock_hub.rs (100%) rename contracts/{pair_stable => pair_lsd}/src/multitest/mod.rs (72%) create mode 100644 contracts/pair_lsd/src/multitest/simulation.rs rename contracts/{pair_stable => pair_lsd}/src/multitest/suite.rs (85%) rename contracts/{pair_stable => pair_lsd}/src/multitest/target_rate.rs (64%) rename contracts/{pair_stable => pair_lsd}/src/state.rs (82%) rename contracts/{pair_stable => pair_lsd}/src/testing.rs (90%) rename contracts/{pair_stable => pair_lsd}/src/utils.rs (64%) rename contracts/{pair_stable => pair_lsd}/tests/3pool_tests.rs (96%) rename contracts/{pair_stable => pair_lsd}/tests/helper.rs (98%) rename contracts/{pair_stable => pair_lsd}/tests/integration.rs (97%) create mode 100644 utils/stable-price-solver/Cargo.toml create mode 100644 utils/stable-price-solver/src/main.rs diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml index 32f7233..745d65d 100644 --- a/.github/workflows/Release.yml +++ b/.github/workflows/Release.yml @@ -24,12 +24,12 @@ jobs: artifacts/cw_placeholder.wasm artifacts/cw_splitter.wasm artifacts/gauge_adapter - artifacts/nominated_trader.wasm - artifacts/wyndex_multi_hop.wasm + artifacts/nominated_trader.wasm + artifacts/wyndex_multi_hop.wasm artifacts/wyndex_stake.wasm - artifacts/checksums.txt - artifacts/raw_migration.wasm - artifacts/wyndex_pair_stable.wasm - artifacts/junoswap_staking.wasm - artifacts/wyndex_factory.wasm + artifacts/checksums.txt + artifacts/raw_migration.wasm + artifacts/wyndex_pair_lsd.wasm + artifacts/junoswap_staking.wasm + artifacts/wyndex_factory.wasm artifacts/wyndex_pair.wasm diff --git a/Cargo.lock b/Cargo.lock index ad3eace..0f00fc6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,6 +13,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + [[package]] name = "anyhow" version = "1.0.69" @@ -106,6 +115,43 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d7ae14b20b94cb02149ed21a86c423859cbe18dc7ed69845cace50e52b40a5" +dependencies = [ + "bitflags", + "clap_derive", + "clap_lex", + "is-terminal", + "once_cell", + "strsim", + "termcolor", +] + +[[package]] +name = "clap_derive" +version = "4.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44bec8e5c9d09e439c4335b1af0abaab56dcf3b94999a936e1bb47b9134288f0" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350b9cf31731f9957399229e9b2adc51eeabdfbe9d71d9a0552275fd12710d09" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "const-oid" version = "0.9.2" @@ -288,7 +334,7 @@ dependencies = [ [[package]] name = "cw-placeholder" -version = "1.3.0" +version = "2.0.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -299,7 +345,7 @@ dependencies = [ [[package]] name = "cw-splitter" -version = "1.3.0" +version = "2.0.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -608,6 +654,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "log", + "regex", +] + [[package]] name = "errno" version = "0.2.8" @@ -662,7 +718,7 @@ checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" [[package]] name = "gauge-adapter" -version = "1.3.0" +version = "2.0.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -678,7 +734,7 @@ dependencies = [ "wyndex", "wyndex-factory", "wyndex-pair", - "wyndex-pair-stable", + "wyndex-pair-lsd", "wyndex-stake", ] @@ -723,6 +779,18 @@ dependencies = [ "ahash", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + [[package]] name = "hex" version = "0.4.3" @@ -757,6 +825,18 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "is-terminal" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857" +dependencies = [ + "hermit-abi", + "io-lifetimes", + "rustix", + "windows-sys 0.45.0", +] + [[package]] name = "itertools" version = "0.10.5" @@ -774,7 +854,7 @@ checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "junoswap-staking" -version = "1.3.0" +version = "2.0.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -834,9 +914,24 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + [[package]] name = "nominated-trader" -version = "1.3.0" +version = "2.0.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -852,7 +947,7 @@ dependencies = [ "wyndex-factory", "wyndex-multi-hop", "wyndex-pair", - "wyndex-pair-stable", + "wyndex-pair-lsd", "wyndex-stake", ] @@ -878,6 +973,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "os_str_bytes" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" + [[package]] name = "pkcs8" version = "0.9.0" @@ -983,6 +1084,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[package]] +name = "quickcheck" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" +dependencies = [ + "env_logger", + "log", + "rand", +] + [[package]] name = "quote" version = "1.0.23" @@ -1039,7 +1151,7 @@ dependencies = [ [[package]] name = "raw-migration" -version = "1.3.0" +version = "2.0.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -1072,6 +1184,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + [[package]] name = "regex-syntax" version = "0.6.28" @@ -1260,6 +1383,14 @@ dependencies = [ "der", ] +[[package]] +name = "stable-price-solver" +version = "0.1.0" +dependencies = [ + "clap", + "quickcheck", +] + [[package]] name = "stake-cw20" version = "0.2.6" @@ -1284,6 +1415,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.4.1" @@ -1314,6 +1451,15 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + [[package]] name = "test-case" version = "2.2.2" @@ -1457,6 +1603,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -1557,7 +1712,7 @@ dependencies = [ [[package]] name = "wyndex" -version = "1.3.0" +version = "2.0.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1572,7 +1727,7 @@ dependencies = [ [[package]] name = "wyndex-factory" -version = "1.3.0" +version = "2.0.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -1593,7 +1748,7 @@ dependencies = [ [[package]] name = "wyndex-multi-hop" -version = "1.3.0" +version = "2.0.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -1612,7 +1767,7 @@ dependencies = [ [[package]] name = "wyndex-pair" -version = "1.3.0" +version = "2.0.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1629,8 +1784,8 @@ dependencies = [ ] [[package]] -name = "wyndex-pair-stable" -version = "1.3.0" +name = "wyndex-pair-lsd" +version = "2.0.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -1646,12 +1801,13 @@ dependencies = [ "proptest", "wyndex", "wyndex-factory", + "wyndex-pair", "wyndex-stake", ] [[package]] name = "wyndex-stake" -version = "1.3.0" +version = "2.0.0" dependencies = [ "anyhow", "cosmwasm-schema", diff --git a/Cargo.toml b/Cargo.toml index 1a62ef3..ed03290 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [workspace] -members = ["packages/*", "contracts/*", "tests"] +members = ["packages/*", "contracts/*", "tests", "utils/*"] [workspace.package] -version = "1.3.0" +version = "2.0.0" edition = "2021" license = "GPL 3.0" repository = "https://github.com/cosmorama/wynddex" @@ -10,12 +10,12 @@ repository = "https://github.com/cosmorama/wynddex" [workspace.dependencies] anyhow = "1" wyndex = { path = "./packages/wyndex", default-features = false } -wynd-curve-utils = { git = "https://github.com/cosmorama/wynddao.git", tag="v1.6.0", package = "wynd-utils"} +wynd-curve-utils = { git = "https://github.com/cosmorama/wynddao.git", tag = "v1.6.0", package = "wynd-utils" } cw20-base = { version = "1.0", package = "cw20-base", features = ["library"] } wyndex-factory = { path = "./contracts/factory" } cw-placeholder = { path = "./contracts/cw-placeholder" } wyndex-pair = { path = "./contracts/pair" } -wyndex-pair-stable = { path = "./contracts/pair_stable" } +wyndex-pair-lsd = { path = "./contracts/pair_lsd" } wyndex-multi-hop = { path = "./contracts/multi-hop" } wyndex-stake = { path = "./contracts/stake" } cosmwasm-schema = "1.1" @@ -46,7 +46,7 @@ incremental = false codegen-units = 1 incremental = false -[profile.release.package.wyndex-pair-stable] +[profile.release.package.wyndex-pair-lsd] codegen-units = 1 incremental = false diff --git a/contracts/factory/src/testing.rs b/contracts/factory/src/testing.rs index da2c46f..7e45e42 100644 --- a/contracts/factory/src/testing.rs +++ b/contracts/factory/src/testing.rs @@ -35,6 +35,7 @@ fn default_stake_config() -> DefaultStakeConfig { fn pair_type_to_string() { assert_eq!(PairType::Xyk {}.to_string(), "xyk"); assert_eq!(PairType::Stable {}.to_string(), "stable"); + assert_eq!(PairType::Lsd {}.to_string(), "lsd"); } #[test] @@ -108,7 +109,7 @@ fn proper_initialization() { pair_configs: vec![ PairConfig { code_id: 325u64, - pair_type: PairType::Stable {}, + pair_type: PairType::Lsd {}, fee_config: FeeConfig { total_fee_bps: 100, protocol_fee_bps: 10, @@ -489,7 +490,7 @@ fn create_pair() { env.clone(), info.clone(), ExecuteMsg::CreatePair { - pair_type: PairType::Stable {}, + pair_type: PairType::Lsd {}, asset_infos: asset_infos.clone(), init_params: None, total_fee_bps: None, diff --git a/contracts/gauge-adapter/Cargo.toml b/contracts/gauge-adapter/Cargo.toml index b244866..b8449fa 100644 --- a/contracts/gauge-adapter/Cargo.toml +++ b/contracts/gauge-adapter/Cargo.toml @@ -36,4 +36,4 @@ cw20 = { workspace = true } cw20-base = { workspace = true } wyndex-factory = { workspace = true } wyndex-pair = { workspace = true } -wyndex-pair-stable = { workspace = true } +wyndex-pair-lsd = { workspace = true } diff --git a/contracts/nominated-trader/Cargo.toml b/contracts/nominated-trader/Cargo.toml index c87562a..4dd17e2 100644 --- a/contracts/nominated-trader/Cargo.toml +++ b/contracts/nominated-trader/Cargo.toml @@ -36,5 +36,5 @@ cw20-base = { workspace = true } wyndex-factory = { workspace = true } wyndex-pair = { workspace = true } wyndex-multi-hop = { workspace = true } -wyndex-pair-stable = { workspace = true } +wyndex-pair-lsd = { workspace = true } wyndex-stake = { workspace = true } diff --git a/contracts/pair/src/contract.rs b/contracts/pair/src/contract.rs index b6d49f3..e5eddcb 100644 --- a/contracts/pair/src/contract.rs +++ b/contracts/pair/src/contract.rs @@ -17,7 +17,7 @@ use wyndex::asset::{ use wyndex::decimal2decimal256; use wyndex::factory::{ConfigResponse as FactoryConfig, PairType}; use wyndex::fee_config::FeeConfig; -use wyndex::oracle::PricePoint; + use wyndex::pair::{ add_referral, assert_max_spread, check_asset_infos, check_assets, check_cw20_in_pool, create_lp_token, get_share_in_assets, handle_referral, handle_reply, migration_check, @@ -463,29 +463,6 @@ pub fn provide_liquidity( if let Some((price0_cumulative_new, price1_cumulative_new, block_time)) = accumulate_prices(&env, &config, pools[0].amount, pools[1].amount)? { - // Calculate new pool amounts - let new_pool0 = pools[0].amount + deposits[0].amount; - let new_pool1 = pools[1].amount + deposits[1].amount; - let price_precision = Uint128::from(10u128.pow(TWAP_PRECISION.into())); - wyndex::oracle::store_price( - deps.storage, - &env, - &config.pair_info.asset_infos, - vec![ - // new price for pool0 asset is amount of pool1 asset divided by amount of pool0 asset, - PricePoint::new( - pools[1].info.clone(), - pools[0].info.clone(), - price_precision.multiply_ratio(new_pool1, new_pool0), - ), - // price for pool1 asset is the inverse of that - PricePoint::new( - pools[0].info.clone(), - pools[1].info.clone(), - price_precision.multiply_ratio(new_pool0, new_pool1), - ), - ], - )?; config.price0_cumulative_last = price0_cumulative_new; config.price1_cumulative_last = price1_cumulative_new; config.block_time_last = block_time; @@ -525,32 +502,6 @@ pub fn withdraw_liquidity( if let Some((price0_cumulative_new, price1_cumulative_new, block_time)) = accumulate_prices(&env, &config, pools[0].amount, pools[1].amount)? { - // Calculate new pool amounts - let mut new_pools = pools - .iter() - .zip(refund_assets.iter()) - .map(|(p, r)| p.amount - r.amount); - let (new_pool0, new_pool1) = (new_pools.next().unwrap(), new_pools.next().unwrap()); - let price_precision = Uint128::from(10u128.pow(TWAP_PRECISION.into())); - wyndex::oracle::store_price( - deps.storage, - &env, - &config.pair_info.asset_infos, - vec![ - // new price for pool0 asset is amount of pool1 asset divided by amount of pool0 asset, - PricePoint::new( - pools[1].info.clone(), - pools[0].info.clone(), - price_precision.multiply_ratio(new_pool1, new_pool0), - ), - // price for pool1 asset is the inverse of that - PricePoint::new( - pools[0].info.clone(), - pools[1].info.clone(), - price_precision.multiply_ratio(new_pool0, new_pool1), - ), - ], - )?; config.price0_cumulative_last = price0_cumulative_new; config.price1_cumulative_last = price1_cumulative_new; config.block_time_last = block_time; @@ -772,41 +723,6 @@ fn do_swap( if let Some((price0_cumulative_new, price1_cumulative_new, block_time)) = accumulate_prices(env, config, pools[0].amount, pools[1].amount)? { - // Calculate new pool amounts - let (new_pool0, new_pool1) = if pools[0].info.equal(&ask_pool.info) { - // subtract fee and return amount from ask pool - // add offer amount to offer pool - ( - pools[0].amount - protocol_fee_amount - return_amount, - pools[1].amount + offer_amount, - ) - } else { - // same as above, but with inverted indices - ( - pools[0].amount + offer_amount, - pools[1].amount - protocol_fee_amount - return_amount, - ) - }; - let price_precision = Uint128::from(10u128.pow(TWAP_PRECISION.into())); - wyndex::oracle::store_price( - deps.storage, - env, - &config.pair_info.asset_infos, - vec![ - // new price for pool0 asset is amount of pool1 asset divided by amount of pool0 asset, - PricePoint::new( - pools[1].info.clone(), - pools[0].info.clone(), - price_precision.multiply_ratio(new_pool1, new_pool0), - ), - // price for pool1 asset is the inverse of that - PricePoint::new( - pools[0].info.clone(), - pools[1].info.clone(), - price_precision.multiply_ratio(new_pool0, new_pool1), - ), - ], - )?; config.price0_cumulative_last = price0_cumulative_new; config.price1_cumulative_last = price1_cumulative_new; config.block_time_last = block_time; @@ -940,12 +856,6 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { referral_commission, )?), QueryMsg::CumulativePrices {} => to_binary(&query_cumulative_prices(deps, env)?), - QueryMsg::HistoricalPrices { duration } => to_binary(&wyndex::oracle::query_historical( - deps, - &env, - CONFIG.load(deps.storage)?.pair_info.asset_infos, - duration, - )?), QueryMsg::Config {} => to_binary(&query_config(deps)?), _ => Err(StdError::generic_err("Query is not supported")), } diff --git a/contracts/pair/src/testing.rs b/contracts/pair/src/testing.rs index b4fdfcd..5417237 100644 --- a/contracts/pair/src/testing.rs +++ b/contracts/pair/src/testing.rs @@ -1,7 +1,7 @@ use cosmwasm_std::testing::{mock_env, mock_info, MOCK_CONTRACT_ADDR}; use cosmwasm_std::{ - attr, coin, from_binary, to_binary, Addr, BankMsg, BlockInfo, Coin, CosmosMsg, Decimal, - DepsMut, Env, ReplyOn, Response, StdError, SubMsg, Timestamp, Uint128, WasmMsg, + attr, to_binary, Addr, BankMsg, BlockInfo, Coin, CosmosMsg, Decimal, DepsMut, Env, ReplyOn, + Response, StdError, SubMsg, Timestamp, Uint128, WasmMsg, }; use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg, MinterResponse}; use cw_utils::MsgInstantiateContractResponse; @@ -13,16 +13,15 @@ use wyndex::factory::PairType; use wyndex::fee_config::FeeConfig; use wyndex::pair::MigrateMsg; use wyndex::pair::{ - assert_max_spread, ContractError, Cw20HookMsg, ExecuteMsg, HistoricalPricesResponse, - HistoryDuration, InstantiateMsg, PairInfo, PoolResponse, QueryMsg, ReverseSimulationResponse, - SimulationResponse, StakeConfig, TWAP_PRECISION, + assert_max_spread, ContractError, Cw20HookMsg, ExecuteMsg, InstantiateMsg, PairInfo, + PoolResponse, ReverseSimulationResponse, SimulationResponse, StakeConfig, TWAP_PRECISION, }; +use crate::contract::compute_offer_amount; use crate::contract::{ accumulate_prices, compute_swap, execute, instantiate, migrate, query_pool, query_reverse_simulation, query_share, query_simulation, }; -use crate::contract::{compute_offer_amount, query}; use crate::state::{Config, CONFIG}; // TODO: Copied here just as a temporary measure use crate::mock_querier::mock_dependencies; @@ -987,196 +986,6 @@ fn withdraw_liquidity() { ); } -#[test] -fn query_historical() { - let mut deps = mock_dependencies(&[]); - let mut env = mock_env(); - - let user = "user"; - - // setup some cw20 tokens, so the queries don't fail - deps.querier.with_token_balances(&[ - ( - &"asset0000".into(), - &[(&MOCK_CONTRACT_ADDR.into(), &0u128.into())], - ), - ( - &"liquidity0000".into(), - &[(&MOCK_CONTRACT_ADDR.into(), &0u128.into())], - ), - ]); - - let uusd = AssetInfoValidated::Native("uusd".to_string()); - let token = AssetInfoValidated::Token(Addr::unchecked("asset0000")); - - // instantiate the contract - let msg = InstantiateMsg { - asset_infos: vec![uusd.clone().into(), token.clone().into()], - token_code_id: 10u64, - factory_addr: String::from("factory"), - init_params: None, - staking_config: default_stake_config(), - trading_starts: 0, - fee_config: FeeConfig { - total_fee_bps: 0, - protocol_fee_bps: 0, - }, - circuit_breaker: None, - }; - instantiate(deps.as_mut(), env.clone(), mock_info("owner", &[]), msg).unwrap(); - - // Store the liquidity token - store_liquidity_token(deps.as_mut(), "liquidity0000".to_string()); - - // query price history before any price changes - let history: HistoricalPricesResponse = from_binary( - &query( - deps.as_ref(), - env.clone(), - QueryMsg::HistoricalPrices { - duration: HistoryDuration::Day, - }, - ) - .unwrap(), - ) - .unwrap(); - assert_eq!( - history.historical_prices, - vec![ - (uusd.clone(), token.clone(), vec![]), - (token.clone(), uusd.clone(), vec![]) - ], - "price history should be empty, no price changes yet" - ); - - // provide liquidity to get a first price - let msg = ExecuteMsg::ProvideLiquidity { - assets: vec![ - Asset { - info: uusd.clone().into(), - amount: 1_000_000u128.into(), - }, - Asset { - info: token.clone().into(), - amount: 1_000_000u128.into(), - }, - ], - slippage_tolerance: None, - receiver: None, - }; - // need to set balance manually to simulate funds being sent - deps.querier - .with_balance(&[(&MOCK_CONTRACT_ADDR.into(), &[coin(1_000_000u128, "uusd")])]); - execute( - deps.as_mut(), - env.clone(), - mock_info(user, &[coin(1_000_000u128, "uusd")]), - msg, - ) - .unwrap(); - - // query price history after first price change - let history: HistoricalPricesResponse = from_binary( - &query( - deps.as_ref(), - env.clone(), - QueryMsg::HistoricalPrices { - duration: HistoryDuration::Day, - }, - ) - .unwrap(), - ) - .unwrap(); - assert_eq!( - history.historical_prices, - vec![ - ( - uusd.clone(), - token.clone(), - vec![(env.block.time.seconds(), 1_000_000u128.into())] - ), - ( - token.clone(), - uusd.clone(), - vec![(env.block.time.seconds(), 1_000_000u128.into())] - ) - ], - ); - - // forward time half an hour - const HALF_HOUR: u64 = 30 * 60; - env.block.time = env.block.time.plus_seconds(HALF_HOUR); - - // swap to get a second price - let msg = ExecuteMsg::Swap { - offer_asset: Asset { - info: uusd.clone().into(), - amount: 1_000u128.into(), - }, - to: None, - max_spread: None, - belief_price: None, - ask_asset_info: None, - referral_address: None, - referral_commission: None, - }; - // need to set balance manually to simulate funds being sent - deps.querier - .with_balance(&[(&MOCK_CONTRACT_ADDR.into(), &[coin(1_001_000u128, "uusd")])]); - deps.querier.with_token_balances(&[ - ( - &"asset0000".into(), - &[(&MOCK_CONTRACT_ADDR.into(), &1_000_000u128.into())], - ), - ( - &"liquidity0000".into(), - &[(&MOCK_CONTRACT_ADDR.into(), &0u128.into())], - ), - ]); - execute( - deps.as_mut(), - env.clone(), - mock_info(user, &[coin(1_000u128, "uusd")]), - msg, - ) - .unwrap(); - - // query price history after swap price change - let history: HistoricalPricesResponse = from_binary( - &query( - deps.as_ref(), - env.clone(), - QueryMsg::HistoricalPrices { - duration: HistoryDuration::Day, - }, - ) - .unwrap(), - ) - .unwrap(); - // swap increased uusd, so uusd price decreases and token price (in uusd) increases - assert_eq!( - history.historical_prices, - vec![ - ( - uusd.clone(), - token.clone(), - vec![ - (env.block.time.seconds() - HALF_HOUR, 1_000_000u128.into()), - (env.block.time.seconds(), 1_002_000u128.into()) - ] - ), - ( - token, - uusd, - vec![ - (env.block.time.seconds() - HALF_HOUR, 1_000_000u128.into()), - (env.block.time.seconds(), 998_002u128.into()) - ] - ) - ], - ); -} - #[test] fn try_native_to_token() { let total_share = Uint128::new(30000000000u128); diff --git a/contracts/pair_stable/.cargo/config b/contracts/pair_lsd/.cargo/config similarity index 100% rename from contracts/pair_stable/.cargo/config rename to contracts/pair_lsd/.cargo/config diff --git a/contracts/pair_stable/.editorconfig b/contracts/pair_lsd/.editorconfig similarity index 100% rename from contracts/pair_stable/.editorconfig rename to contracts/pair_lsd/.editorconfig diff --git a/contracts/pair_stable/Cargo.toml b/contracts/pair_lsd/Cargo.toml similarity index 95% rename from contracts/pair_stable/Cargo.toml rename to contracts/pair_lsd/Cargo.toml index 1c452c2..e8a0177 100644 --- a/contracts/pair_stable/Cargo.toml +++ b/contracts/pair_lsd/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "wyndex-pair-stable" +name = "wyndex-pair-lsd" version = { workspace = true } authors = ["Cosmorama "] edition = { workspace = true } @@ -41,3 +41,4 @@ derivative = { workspace = true } proptest = { workspace = true } wyndex-factory = { workspace = true } wyndex-stake = { workspace = true } +wyndex-pair = { workspace = true } diff --git a/contracts/pair_stable/README.md b/contracts/pair_lsd/README.md similarity index 100% rename from contracts/pair_stable/README.md rename to contracts/pair_lsd/README.md diff --git a/contracts/pair_stable/rustfmt.toml b/contracts/pair_lsd/rustfmt.toml similarity index 100% rename from contracts/pair_stable/rustfmt.toml rename to contracts/pair_lsd/rustfmt.toml diff --git a/contracts/pair_stable/src/bin/pair_stable_schema.rs b/contracts/pair_lsd/src/bin/pair_stable_schema.rs similarity index 100% rename from contracts/pair_stable/src/bin/pair_stable_schema.rs rename to contracts/pair_lsd/src/bin/pair_stable_schema.rs diff --git a/contracts/pair_stable/src/contract.rs b/contracts/pair_lsd/src/contract.rs similarity index 89% rename from contracts/pair_stable/src/contract.rs rename to contracts/pair_lsd/src/contract.rs index 324ad52..553f204 100644 --- a/contracts/pair_stable/src/contract.rs +++ b/contracts/pair_lsd/src/contract.rs @@ -23,7 +23,8 @@ use wyndex::pair::{ add_referral, assert_max_spread, check_asset_infos, check_assets, check_cw20_in_pool, create_lp_token, get_share_in_assets, handle_referral, handle_reply, migration_check, mint_token_message, save_tmp_staking_config, take_referral, ConfigResponse, Cw20HookMsg, - InstantiateMsg, MigrateMsg, StablePoolParams, StablePoolUpdateParams, + InstantiateMsg, MigrateMsg, SpotPricePredictionResponse, SpotPriceResponse, StablePoolParams, + StablePoolUpdateParams, }; use wyndex::pair::{ CumulativePricesResponse, ExecuteMsg, PairInfo, PoolResponse, QueryMsg, @@ -37,16 +38,17 @@ use crate::math::{ }; use crate::msg::{TargetQuery, TargetValueResponse}; use crate::state::{ - get_precision, store_precisions, Config, CIRCUIT_BREAKER, CONFIG, FROZEN, OWNERSHIP_PROPOSAL, + get_precision, store_precisions, Config, LsdData, CIRCUIT_BREAKER, CONFIG, FROZEN, + OWNERSHIP_PROPOSAL, }; use crate::utils::{ - accumulate_prices, adjust_precision, calc_new_prices, compute_current_amp, compute_swap, - select_pools, SwapResult, + accumulate_prices, adjust_precision, calc_spot_price, compute_current_amp, compute_swap, + find_spot_price, select_pools, SwapResult, }; use wyndex::pair::ContractError; /// Contract name that is used for migration. -const CONTRACT_NAME: &str = "wyndex-pair-stable"; +const CONTRACT_NAME: &str = "wyndex-pair-lsd"; /// Contract version that is used for migration. const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -62,8 +64,9 @@ pub fn instantiate( ) -> Result { let asset_infos = check_asset_infos(deps.api, &msg.asset_infos)?; - if asset_infos.len() > 5 || asset_infos.len() < 2 { - return Err(ContractError::InvalidNumberOfAssets { min: 2, max: 5 }); + // Only 2 assets makes sense for lsd (Asset and Asset-LSD) + if asset_infos.len() != 2 { + return Err(ContractError::InvalidNumberOfAssets { min: 2, max: 2 }); } if msg.init_params.is_none() { @@ -78,17 +81,21 @@ pub fn instantiate( return Err(ContractError::IncorrectAmp { max_amp: MAX_AMP }); } - ensure!( - params.target_rate_epoch <= WEEK, - ContractError::InvalidTargetRateEpoch {} - ); - ensure!( - params.lsd_hub.is_none() - || asset_infos.len() == 2 - && asset_infos.iter().any(|a| a.is_native_token()) - && asset_infos.iter().any(|a| !a.is_native_token()), - ContractError::InvalidAssetsForTargetRate {} - ); + let lsd_data: Option = if let Some(info) = params.lsd { + ensure!( + info.target_rate_epoch <= WEEK, + ContractError::InvalidTargetRateEpoch {} + ); + Some(LsdData { + asset: info.asset.validate(deps.api)?, + lsd_hub: deps.api.addr_validate(&info.hub)?, + target_rate: Decimal::one(), + target_rate_epoch: info.target_rate_epoch, + last_target_query: 0, + }) + } else { + None + }; set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; @@ -121,7 +128,7 @@ pub fn instantiate( liquidity_token: Addr::unchecked(""), staking_addr: Addr::unchecked(""), asset_infos, - pair_type: PairType::Stable {}, + pair_type: PairType::Lsd {}, fee_config: msg.fee_config, }, factory_addr, @@ -133,13 +140,7 @@ pub fn instantiate( greatest_precision, cumulative_prices, trading_starts: msg.trading_starts, - lsd_hub: params - .lsd_hub - .map(|addr| deps.api.addr_validate(&addr)) - .transpose()?, - target_rate: Decimal::one(), - target_rate_epoch: params.target_rate_epoch, - last_target_query: 0, // will be queried on first interaction + lsd: lsd_data, }; CONFIG.save(deps.storage, &config)?; @@ -592,25 +593,7 @@ pub fn provide_liquidity( }) .collect::>(); - if accumulate_prices(deps.as_ref(), &env, &mut config, &old_pools)? { - // calculate pools with deposited balances - let new_pools = assets_collection - .into_iter() - .map(|(mut asset, pool)| { - // add deposit amount back to pool amount, so we can calculate the new price - asset.amount += pool; - asset - }) - .collect::>(); - let new_prices = calc_new_prices(deps.as_ref(), &env, &config, &new_pools)?; - wyndex::oracle::store_price( - deps.storage, - &env, - &config.pair_info.asset_infos, - new_prices, - )?; - CONFIG.save(deps.storage, &config)?; - } else if save_config { + if accumulate_prices(deps.as_ref(), &env, &mut config, &old_pools)? || save_config { CONFIG.save(deps.storage, &config)?; } @@ -699,26 +682,7 @@ pub fn withdraw_liquidity( .collect::>>()?; let save_config = update_target_rate(deps.querier, &mut config, &env)?; - if accumulate_prices(deps.as_ref(), &env, &mut config, &old_pools)? { - // calculate pools with withdrawn balances - let new_pools = pools - .into_iter() - .zip(refund_assets.iter()) - .map(|(mut pool, refund)| { - pool.amount -= refund.amount; - let precision = get_precision(deps.storage, &pool.info)?; - pool.to_decimal_asset(precision) - }) - .collect::>>()?; - let new_prices = calc_new_prices(deps.as_ref(), &env, &config, &new_pools)?; - wyndex::oracle::store_price( - deps.storage, - &env, - &config.pair_info.asset_infos, - new_prices, - )?; - CONFIG.save(deps.storage, &config)?; - } else if save_config { + if accumulate_prices(deps.as_ref(), &env, &mut config, &old_pools)? || save_config { CONFIG.save(deps.storage, &config)?; } @@ -1001,37 +965,38 @@ pub fn swap( } } - if accumulate_prices(deps.as_ref(), &env, &mut config, &pools)? { - // calculate pools with deposited / withdrawn balances - let new_pools = pools - .into_iter() - .map(|mut pool| -> StdResult { - if pool.info.equal(&offer_asset.info) { - // add offer amount to pool (it was already subtracted right at the beginning) - pool.amount = pool.amount.checked_add(Decimal256::with_precision( - offer_asset.amount, - offer_precision, - )?)?; - } else if pool.info.equal(&ask_pool.info) { - // subtract fee and return amount from ask pool - let ask_precision = get_precision(deps.storage, &ask_pool.info)?; - pool.amount = pool.amount.checked_sub(Decimal256::with_precision( - return_amount + protocol_fee_amount, - ask_precision, - )?)?; - } - Ok(pool) - }) - .collect::>>()?; - let new_prices = calc_new_prices(deps.as_ref(), &env, &config, &new_pools)?; - wyndex::oracle::store_price( - deps.storage, - &env, - &config.pair_info.asset_infos, - new_prices, - )?; - CONFIG.save(deps.storage, &config)?; - } else if save_config { + // if accumulate_prices(deps.as_ref(), &env, &mut config, &pools)? { + // // calculate pools with deposited / withdrawn balances + // let new_pools = pools + // .into_iter() + // .map(|mut pool| -> StdResult { + // if pool.info.equal(&offer_asset.info) { + // // add offer amount to pool (it was already subtracted right at the beginning) + // pool.amount = pool.amount.checked_add(Decimal256::with_precision( + // offer_asset.amount, + // offer_precision, + // )?)?; + // } else if pool.info.equal(&ask_pool.info) { + // // subtract fee and return amount from ask pool + // let ask_precision = get_precision(deps.storage, &ask_pool.info)?; + // pool.amount = pool.amount.checked_sub(Decimal256::with_precision( + // return_amount + protocol_fee_amount, + // ask_precision, + // )?)?; + // } + // Ok(pool) + // }) + // .collect::>>()?; + // let new_prices = calc_new_prices(deps.as_ref(), &env, &config, &new_pools)?; + // wyndex::oracle::store_price( + // deps.storage, + // &env, + // &config.pair_info.asset_infos, + // new_prices, + // )?; + // CONFIG.save(deps.storage, &config)?; + // } + if save_config { CONFIG.save(deps.storage, &config)?; } @@ -1134,14 +1099,24 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { referral_commission, )?), QueryMsg::CumulativePrices {} => to_binary(&query_cumulative_prices(deps, env)?), - QueryMsg::HistoricalPrices { duration } => to_binary(&wyndex::oracle::query_historical( - deps, - &env, - CONFIG.load(deps.storage)?.pair_info.asset_infos, - duration, - )?), QueryMsg::Config {} => to_binary(&query_config(deps, env)?), QueryMsg::QueryComputeD {} => to_binary(&query_compute_d(deps, env)?), + QueryMsg::SpotPrice { offer, ask } => to_binary(&query_spot_price(deps, env, offer, ask)?), + QueryMsg::SpotPricePrediction { + offer, + ask, + max_trade, + target_price, + iterations, + } => to_binary(&query_spot_price_prediction( + deps, + env, + offer, + ask, + max_trade, + target_price, + iterations, + )?), } } @@ -1322,7 +1297,7 @@ pub fn query_reverse_simulation( &pools, compute_current_amp(&config, &env)?, config.greatest_precision, - config.target_rate, + &config, )?; let offer_amount = new_offer_pool_amount.checked_sub( @@ -1391,6 +1366,82 @@ pub fn query_config(deps: Deps, env: Env) -> StdResult { }) } +/// Returns information about cumulative prices for the assets in the pool using a [`CumulativePricesResponse`] object. +pub fn query_spot_price( + deps: Deps, + env: Env, + offer: AssetInfo, + ask: AssetInfo, +) -> Result { + let from = offer.validate(deps.api)?; + let to = ask.validate(deps.api)?; + + let config = CONFIG.load(deps.storage)?; + let (assets, _) = pool_info(deps.querier, &config)?; + let decimal_assets = assets + .iter() + .cloned() + .map(|asset| { + let precision = get_precision(deps.storage, &asset.info)?; + asset.to_decimal_asset(precision) + }) + .collect::>>()?; + + let price = calc_spot_price(deps, &env, &config, &from, &to, &decimal_assets)?; + Ok(SpotPriceResponse { price }) +} + +/// Returns information about cumulative prices for the assets in the pool using a [`CumulativePricesResponse`] object. +pub fn query_spot_price_prediction( + deps: Deps, + env: Env, + offer: AssetInfo, + ask: AssetInfo, + max_trade: Uint128, + target_price: Decimal, + iterations: u8, +) -> Result { + let from = offer.validate(deps.api)?; + let to = ask.validate(deps.api)?; + + ensure!( + max_trade > Uint128::zero(), + ContractError::SpotPriceInvalidMaxTrade {} + ); + ensure!( + target_price > Decimal::zero(), + ContractError::SpotPriceInvalidTargetPrice {} + ); + ensure!( + iterations > 0 && iterations <= 100, + ContractError::SpotPriceInvalidIterations {} + ); + + let config = CONFIG.load(deps.storage)?; + let (assets, _) = pool_info(deps.querier, &config)?; + let decimal_assets = assets + .iter() + .cloned() + .map(|asset| { + let precision = get_precision(deps.storage, &asset.info)?; + asset.to_decimal_asset(precision) + }) + .collect::>>()?; + + let trade = find_spot_price( + deps, + &env, + &config, + from, + to, + decimal_assets, + max_trade, + target_price, + iterations, + )?; + Ok(SpotPricePredictionResponse { trade }) +} + /// Returns the total amount of assets in the pool as well as the total amount of LP tokens currently minted. pub fn pool_info( querier: QuerierWrapper, @@ -1525,18 +1576,18 @@ fn update_target_rate( config: &mut Config, env: &Env, ) -> StdResult { - let now = env.block.time.seconds(); - if now < config.last_target_query + config.target_rate_epoch { - // target rate is up to date - return Ok(false); - } + if let Some(lsd) = &mut config.lsd { + let now = env.block.time.seconds(); + if now < lsd.last_target_query + lsd.target_rate_epoch { + // target rate is up to date + return Ok(false); + } - if let Some(hub) = &config.lsd_hub { let response: TargetValueResponse = - querier.query_wasm_smart(hub, &TargetQuery::TargetValue {})?; + querier.query_wasm_smart(&lsd.lsd_hub, &TargetQuery::TargetValue {})?; - config.target_rate = response.target_value; - config.last_target_query = now; + lsd.target_rate = response.target_value; + lsd.last_target_query = now; Ok(true) } else { diff --git a/contracts/pair_stable/src/lib.rs b/contracts/pair_lsd/src/lib.rs similarity index 100% rename from contracts/pair_stable/src/lib.rs rename to contracts/pair_lsd/src/lib.rs diff --git a/contracts/pair_stable/src/math.rs b/contracts/pair_lsd/src/math.rs similarity index 86% rename from contracts/pair_stable/src/math.rs rename to contracts/pair_lsd/src/math.rs index df1a561..c9d28ea 100644 --- a/contracts/pair_stable/src/math.rs +++ b/contracts/pair_lsd/src/math.rs @@ -1,6 +1,5 @@ -use std::ops::Mul; - -use cosmwasm_std::{Decimal, Decimal256, Fraction, StdError, StdResult, Uint128, Uint256, Uint64}; +use crate::state::Config; +use cosmwasm_std::{Decimal256, Fraction, StdError, StdResult, Uint128, Uint256, Uint64}; use itertools::Itertools; use wyndex::asset::{AssetInfoValidated, Decimal256Ext, DecimalAsset}; @@ -78,7 +77,7 @@ pub(crate) fn calc_y( pools: &[DecimalAsset], amp: Uint64, target_precision: u8, - target_rate: Decimal, + config: &Config, ) -> StdResult { if to.equal(&from_asset.info) { return Err(StdError::generic_err( @@ -94,13 +93,13 @@ pub(crate) fn calc_y( .map(|asset| { ( &asset.info, - apply_rate(&asset.info, asset.amount, Decimal256::from(target_rate)), + apply_rate_decimal(&asset.info, asset.amount, config), ) }) .collect_vec(); - if !from_asset.info.is_native_token() { - new_amount *= Decimal256::from(target_rate); + if config.is_lsd(&from_asset.info) { + new_amount *= Decimal256::from(config.target_rate()); } let n_coins = Uint64::from(pools.len() as u8); @@ -137,10 +136,10 @@ pub(crate) fn calc_y( y = (y * y + c) / (y + y + b - d); if y >= y_prev { if y - y_prev <= Uint256::from(1u8) { - return Ok(inverse_rate(to, y.try_into()?, target_rate)); + return Ok(inverse_rate(to, y.try_into()?, config)); } } else if y < y_prev && y_prev - y <= Uint256::from(1u8) { - return Ok(inverse_rate(to, y.try_into()?, target_rate)); + return Ok(inverse_rate(to, y.try_into()?, config)); } } @@ -149,25 +148,34 @@ pub(crate) fn calc_y( } /// Applies the target rate to the amount if the asset is the LSD token. -/// -pub(crate) fn apply_rate>( - asset: &AssetInfoValidated, - amount: I, - target_rate: R, -) -> I { - if asset.is_native_token() { - amount +pub(crate) fn apply_rate(asset: &AssetInfoValidated, amount: Uint128, config: &Config) -> Uint128 { + if config.is_lsd(asset) { + amount * config.target_rate() } else { - amount * target_rate + amount } } -fn inverse_rate(to: &AssetInfoValidated, y: Uint128, target_rate: Decimal) -> Uint128 { - if to.is_native_token() { - y +/// Applies the target rate to the amount if the asset is the LSD token. +pub(crate) fn apply_rate_decimal( + asset: &AssetInfoValidated, + amount: Decimal256, + config: &Config, +) -> Decimal256 { + if config.is_lsd(asset) { + amount * Decimal256::from(config.target_rate()) } else { + amount + } +} + +fn inverse_rate(to: &AssetInfoValidated, y: Uint128, config: &Config) -> Uint128 { + if config.is_lsd(to) { // y / target_rate - y.multiply_ratio(target_rate.denominator(), target_rate.numerator()) + let t = config.target_rate(); + y.multiply_ratio(t.denominator(), t.numerator()) + } else { + y } } diff --git a/contracts/pair_stable/src/mock_querier.rs b/contracts/pair_lsd/src/mock_querier.rs similarity index 100% rename from contracts/pair_stable/src/mock_querier.rs rename to contracts/pair_lsd/src/mock_querier.rs diff --git a/contracts/pair_stable/src/msg.rs b/contracts/pair_lsd/src/msg.rs similarity index 100% rename from contracts/pair_stable/src/msg.rs rename to contracts/pair_lsd/src/msg.rs diff --git a/contracts/pair_stable/src/multitest/mock_hub.rs b/contracts/pair_lsd/src/multitest/mock_hub.rs similarity index 100% rename from contracts/pair_stable/src/multitest/mock_hub.rs rename to contracts/pair_lsd/src/multitest/mock_hub.rs diff --git a/contracts/pair_stable/src/multitest/mod.rs b/contracts/pair_lsd/src/multitest/mod.rs similarity index 72% rename from contracts/pair_stable/src/multitest/mod.rs rename to contracts/pair_lsd/src/multitest/mod.rs index 5131939..2114423 100644 --- a/contracts/pair_stable/src/multitest/mod.rs +++ b/contracts/pair_lsd/src/multitest/mod.rs @@ -1,3 +1,4 @@ mod mock_hub; +mod simulation; mod suite; mod target_rate; diff --git a/contracts/pair_lsd/src/multitest/simulation.rs b/contracts/pair_lsd/src/multitest/simulation.rs new file mode 100644 index 0000000..d961d30 --- /dev/null +++ b/contracts/pair_lsd/src/multitest/simulation.rs @@ -0,0 +1,452 @@ +use std::str::FromStr; + +use cosmwasm_std::{assert_approx_eq, coin, Addr, Decimal, Fraction, Uint128}; +use cw_multi_test::AppResponse; +use wyndex::pair::LsdInfo; +use wyndex::{ + asset::{Asset, AssetInfo, AssetInfoExt}, + factory::PairType, + pair::StablePoolParams, +}; + +use crate::multitest::target_rate::arbitrage_to; + +use super::suite::{Suite, SuiteBuilder}; + +const DAY: u64 = 24 * 60 * 60; + +const TRADER: &str = "trader"; + +/// Simulates a year of trading where the exchange rate increases every day for different amp values. +/// This uses a constant trading volume per day. +#[test] +#[ignore = "only for finding good amp parameter"] +fn simulate_changing_rate() { + let liquidity_discount = Decimal::percent(4); + let tvl = 100_000_000_000_000_000u128; // total value locked in the pool + let trade_volume = 1_000_000_000_000_000u128; // how much juno is traded + + let juno_info = AssetInfo::Native("juno".to_string()); + + const AMPS: [u64; 13] = [ + 100, 1000, 5000, 10_000, 15_000, 20_000, 50_000, 100_000, 200_000, 300_000, 400_000, + 500_000, 1_000_000, + ]; + for amp in AMPS { + // these will be measured + let mut max_price_change = Decimal::zero(); + let mut max_slippage = Uint128::zero(); + + let mut target_rate = Decimal::one() - liquidity_discount; + let mut suite = SuiteBuilder::new() + .with_funds(TRADER, &[coin(tvl / 2, "juno")]) + .with_initial_target_rate(target_rate) + .build(); + let start_time = suite.app.block_info().time.seconds(); + + let wy_juno = suite.instantiate_token("owner", "wyJUNO"); + let wy_juno_info = AssetInfo::Token(wy_juno.to_string()); + + // create the pair + let pair = suite + .create_pair_and_provide_liquidity( + PairType::Lsd {}, + Some(StablePoolParams { + amp, + owner: Some("owner".to_string()), + lsd: Some(LsdInfo { + asset: wy_juno_info.clone(), + hub: suite.mock_hub.to_string(), + target_rate_epoch: DAY, + }), + }), + (juno_info.clone(), 150_000_000_000_000_000), + (wy_juno_info.clone(), 100_000_000_000_000_000), + vec![coin(150_000_000_000_000_000, "juno")], + ) + .unwrap(); + + suite.wait(DAY); + + // simulate whole year of trading + for _ in 0..365 { + let price = query_price(&mut suite, &pair, &wy_juno_info); + + // trade some amounts around + let swap_response = suite + .swap( + &pair, + TRADER, + juno_info.with_balance(trade_volume), + wy_juno_info.clone(), + None, + None, + None, + ) + .unwrap(); + let spread = get_spread(swap_response); + max_slippage = std::cmp::max(max_slippage, spread); + + let price_after_swap = query_price(&mut suite, &pair, &wy_juno_info); + max_price_change = std::cmp::max( + max_price_change, + (std::cmp::max(price_after_swap, price) / std::cmp::min(price, price_after_swap)) + - Decimal::one(), + ); + + let lsd_balance = suite.query_cw20_balance(TRADER, &wy_juno).unwrap(); + suite + .swap( + &pair, + TRADER, + wy_juno_info.with_balance(lsd_balance), + juno_info.clone(), + None, + None, + None, + ) + .unwrap(); + + // update target rate + target_rate = update_target_rate(&mut suite, start_time, liquidity_discount); + suite.wait(DAY); + + // target rate was increased, so we arbitrage it by putting in more juno + arbitrage_to(&mut suite, &pair, &juno_info, target_rate); + } + println!( + "amp {}, max_slippage {}%, max_price_change {}%", + amp, + Decimal::from_ratio(max_slippage, trade_volume) + * Decimal::from_atomics(100u128, 0).unwrap(), + max_price_change * Decimal::from_atomics(100u128, 0).unwrap(), + ); + } +} + +/// This simulates the slippage of a trade at different prices relative to the target rate. +#[test] +#[ignore = "only for finding good amp parameter"] +fn simulate_slippage_vs_uniswap() { + // input parameters + let juno_amount = 50_000_000_000_000_000u128; // how much juno is in the pool + let trade_volume = 1_000_000_000_000_000u128; // how much juno is traded + + let amps = [ + 8u64, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + ]; + + let price_diffs = [ + Decimal::percent(1), + Decimal::percent(3), + Decimal::percent(5), + ]; + + for price_diff in price_diffs { + println!("amp,price_diff,direction,stableswap slippage,uniswap slippage"); + for amp in amps { + let sub_results = compare_to_uniswap(amp, juno_amount, trade_volume, price_diff, false); + println!( + "{},-{},Juno => wyJuno,{}%,{}%", + amp, + price_diff, + Decimal::from_ratio(sub_results.stable_juno_to_lsd_slippage, trade_volume) + * Decimal::from_atomics(100u128, 0).unwrap(), + Decimal::from_ratio(sub_results.uniswap_juno_to_lsd_slippage, trade_volume) + * Decimal::from_atomics(100u128, 0).unwrap(), + ); + println!( + "{},-{},wyJuno => Juno,{}%,{}%", + amp, + price_diff, + Decimal::from_ratio(sub_results.stable_lsd_to_juno_slippage, trade_volume) + * Decimal::from_atomics(100u128, 0).unwrap(), + Decimal::from_ratio(sub_results.uniswap_lsd_to_juno_slippage, trade_volume) + * Decimal::from_atomics(100u128, 0).unwrap(), + ); + + let add_results = compare_to_uniswap(amp, juno_amount, trade_volume, price_diff, true); + println!( + "{},{},Juno => wyJuno,{}%,{}%", + amp, + price_diff, + Decimal::from_ratio(add_results.stable_juno_to_lsd_slippage, trade_volume) + * Decimal::from_atomics(100u128, 0).unwrap(), + Decimal::from_ratio(add_results.uniswap_juno_to_lsd_slippage, trade_volume) + * Decimal::from_atomics(100u128, 0).unwrap(), + ); + println!( + "{},{},wyJuno => Juno,{}%,{}%", + amp, + price_diff, + Decimal::from_ratio(add_results.stable_lsd_to_juno_slippage, trade_volume) + * Decimal::from_atomics(100u128, 0).unwrap(), + Decimal::from_ratio(add_results.uniswap_lsd_to_juno_slippage, trade_volume) + * Decimal::from_atomics(100u128, 0).unwrap(), + ); + } + } +} + +struct SwapSlippage { + stable_juno_to_lsd_slippage: Uint128, + uniswap_juno_to_lsd_slippage: Uint128, + stable_lsd_to_juno_slippage: Uint128, + uniswap_lsd_to_juno_slippage: Uint128, +} + +/// This simulates the slippage of a trade at the given difference to the target rate for the given amp +fn compare_to_uniswap( + amp: u64, + juno_amount: u128, + trade_volume: u128, + price_diff: Decimal, + add: bool, +) -> SwapSlippage { + // the exchange rate of the lsd token (we keep this constant for this simulation) + let expected_target_rate = Decimal::one(); + let actual_target_rate = if add { + Decimal::one() + price_diff + } else { + Decimal::one() - price_diff + }; + + const TRADER: &str = "trader"; + let juno_info = AssetInfo::Native("juno".to_string()); + + // find lsd amount for stable swap + let lsd_amount = binary_search_lsd_provision( + amp, + expected_target_rate, + actual_target_rate, + &juno_info.with_balance(juno_amount), + ) + .u128(); + + let mut suite = SuiteBuilder::new() + .with_funds(TRADER, &[coin(juno_amount, "juno")]) + .with_initial_target_rate(expected_target_rate) + .build(); + + // create lsd token for the stable pair + let wy_juno = suite.instantiate_token("owner", "wyJUNO"); + let wy_juno_info = AssetInfo::Token(wy_juno.to_string()); + // and one for the uniswap pair + let wy_juno2 = suite.instantiate_token("owner", "wyJUNO"); + let wy_juno2_info = AssetInfo::Token(wy_juno2.to_string()); + + // create the stable pair + let stable_pair = suite + .create_pair_and_provide_liquidity( + PairType::Lsd {}, + Some(StablePoolParams { + amp, + owner: Some("owner".to_string()), + lsd: Some(LsdInfo { + asset: wy_juno_info.clone(), + hub: suite.mock_hub.to_string(), + target_rate_epoch: DAY, + }), + }), + (juno_info.clone(), juno_amount), + (wy_juno_info.clone(), lsd_amount), + vec![coin(juno_amount, "juno")], + ) + .unwrap(); + + // create the uniswap pair for comparison + let uniswap_pair = suite + .create_pair_and_provide_liquidity( + PairType::Xyk {}, + None, + (juno_info.clone(), juno_amount), + ( + wy_juno2_info.clone(), + Uint128::new(juno_amount) + .multiply_ratio( + actual_target_rate.denominator(), + actual_target_rate.numerator(), + ) + .u128(), + ), // juno_amount / lsd_amount = actual_target_rate <=> lsd_amount = juno_amount / actual_target_rate + vec![coin(juno_amount, "juno")], + ) + .unwrap(); + + // check that the prices are correct + assert_approx_eq!( + query_price(&mut suite, &stable_pair, &wy_juno_info).numerator(), + actual_target_rate.numerator(), + "0.000002" + ); + assert_approx_eq!( + query_price(&mut suite, &uniswap_pair, &wy_juno2_info).numerator(), + actual_target_rate.numerator(), + "0.000002" + ); + + // using simulation here to avoid `MaxSpreadAssertion` error + let sim = suite + .query_simulation( + &stable_pair, + juno_info.with_balance(trade_volume), + wy_juno_info.clone(), + ) + .unwrap(); + // calculate slippage as: expected amount minus actual amount + let swap_output = sim.return_amount + sim.commission_amount; + let juno_wy_juno_slippage = + Uint128::new(trade_volume).saturating_sub(swap_output * actual_target_rate); + + let sim = suite + .query_simulation( + &uniswap_pair, + juno_info.with_balance(trade_volume), + wy_juno2_info.clone(), + ) + .unwrap(); + // calculate slippage as: expected amount minus actual amount + let xyk_swap_output = sim.return_amount + sim.commission_amount; + let xyk_juno_wy_juno_slippage = + Uint128::new(trade_volume).saturating_sub(xyk_swap_output * actual_target_rate); + + let sim = suite + .query_simulation( + &stable_pair, + wy_juno_info.with_balance(trade_volume), + juno_info.clone(), + ) + .unwrap(); + // calculate slippage as: expected amount minus actual amount + // we expect to receive the fair market price, which is `actual_target_rate` + let optimal_output = Uint128::new(trade_volume) * actual_target_rate; + let stable_swap_output = sim.return_amount + sim.commission_amount; + let stable_slippage = optimal_output.saturating_sub(stable_swap_output); + + let sim = suite + .query_simulation( + &uniswap_pair, + wy_juno2_info.with_balance(trade_volume), + juno_info, + ) + .unwrap(); + // calculate slippage as: expected amount minus actual amount + // we expect to receive the fair market price, which is `actual_target_rate` + let xyk_swap_output = sim.return_amount + sim.commission_amount; + let xyk_slippage = optimal_output.saturating_sub(xyk_swap_output); + assert_eq!(sim.spread_amount, xyk_slippage); + + SwapSlippage { + stable_juno_to_lsd_slippage: juno_wy_juno_slippage, + uniswap_juno_to_lsd_slippage: xyk_juno_wy_juno_slippage, + stable_lsd_to_juno_slippage: stable_slippage, + uniswap_lsd_to_juno_slippage: xyk_slippage, + } +} + +fn update_target_rate(suite: &mut Suite, start_time: u64, liquidity_discount: Decimal) -> Decimal { + // juno APR currently is around 35%, so we expect 35% / 365 + let daily_apr = Decimal::percent(35) * Decimal::from_ratio(1u128, 365u128); + let elapsed_time = suite.app.block_info().time.seconds() - start_time; + // compound interest formula gives us the expected exchange rate after elapsed_time + let expected_value = (Decimal::one() + daily_apr).pow((elapsed_time / DAY) as u32); + // to get the target rate, we apply the liquidity discount + let target_rate = expected_value * (Decimal::one() - liquidity_discount); + + suite.change_target_value(target_rate).unwrap(); + target_rate +} + +/// Query price (including commission) for one LSD token +fn query_price(suite: &mut Suite, pair: &Addr, input_asset: &AssetInfo) -> Decimal { + // query price for one LSD token + let simulation = suite + .query_simulation(pair, input_asset.with_balance(1_000_000u128), None) + .unwrap(); + + Decimal::from_ratio( + simulation.return_amount + simulation.commission_amount, + 1_000_000u128, + ) +} + +fn get_spread(swap_response: AppResponse) -> Uint128 { + Uint128::from_str(get_attribute(&swap_response, "spread_amount").unwrap()).unwrap() +} + +fn get_attribute<'a>(swap_response: &'a AppResponse, key: &str) -> Option<&'a str> { + swap_response + .events + .iter() + .find_map(|e| e.attributes.iter().find(|a| a.key == key)) + .map(|a| a.value.as_str()) +} + +/// Uses binary search to find the amount of LSD tokens to provide together with the given amount ot juno +/// that will result in the given price. +fn binary_search_lsd_provision( + amp: u64, + expected_target_rate: Decimal, + price: Decimal, + juno: &Asset, +) -> Uint128 { + binary_search( + Uint128::one(), + juno.amount * Uint128::new(1000), + price, + |lsd_amount| { + let mut suite = SuiteBuilder::new() + .with_funds(TRADER, &[coin(juno.amount.u128(), "juno")]) + .with_initial_target_rate(expected_target_rate) + .build(); + + let wy_juno = suite.instantiate_token("owner", "wyJUNO"); + let wy_juno_info = AssetInfo::Token(wy_juno.to_string()); + + // create the pair + let pair = suite + .create_pair_and_provide_liquidity( + PairType::Lsd {}, + Some(StablePoolParams { + amp, + owner: Some("owner".to_string()), + lsd: Some(LsdInfo { + asset: wy_juno_info.clone(), + hub: suite.mock_hub.to_string(), + target_rate_epoch: DAY, + }), + }), + (juno.info.clone(), juno.amount.u128()), + (wy_juno_info.clone(), lsd_amount.u128()), + vec![coin(juno.amount.u128(), "juno")], + ) + .unwrap(); + + query_price(&mut suite, &pair, &wy_juno_info) + }, + ) +} + +/// A function that does binary search with a minimum and maximum Uint128 value +fn binary_search( + mut min: Uint128, + mut max: Uint128, + find: Decimal, + f: impl Fn(Uint128) -> Decimal, +) -> Uint128 { + const TWO: Uint128 = Uint128::new(2); + let mut half = max / TWO + min / TWO; + let mut current = f(half); + + while min <= max { + match current.cmp(&find) { + std::cmp::Ordering::Equal => return half, + std::cmp::Ordering::Greater => min = half + Uint128::one(), + std::cmp::Ordering::Less => max = half - Uint128::one(), + } + half = max / TWO + min / TWO; + current = f(half); + } + half +} diff --git a/contracts/pair_stable/src/multitest/suite.rs b/contracts/pair_lsd/src/multitest/suite.rs similarity index 85% rename from contracts/pair_stable/src/multitest/suite.rs rename to contracts/pair_lsd/src/multitest/suite.rs index 79f93ac..a30a9c2 100644 --- a/contracts/pair_stable/src/multitest/suite.rs +++ b/contracts/pair_lsd/src/multitest/suite.rs @@ -13,7 +13,7 @@ use wyndex::factory::{ use wyndex::fee_config::FeeConfig; use wyndex::pair::{ Cw20HookMsg, ExecuteMsg as PairExecuteMsg, PairInfo, QueryMsg, SimulationResponse, - StablePoolParams, + SpotPricePredictionResponse, SpotPriceResponse, StablePoolParams, StablePoolUpdateParams, }; use super::mock_hub; @@ -56,6 +56,19 @@ fn store_pair(app: &mut App) -> u64 { app.store_code(contract) } +fn store_xyk_pair(app: &mut App) -> u64 { + let contract = Box::new( + ContractWrapper::new_with_empty( + wyndex_pair::contract::execute, + wyndex_pair::contract::instantiate, + wyndex_pair::contract::query, + ) + .with_reply_empty(wyndex_pair::contract::reply), + ); + + app.store_code(contract) +} + fn store_cw20(app: &mut App) -> u64 { let contract = Box::new(ContractWrapper::new( cw20_base::contract::execute, @@ -136,7 +149,8 @@ impl SuiteBuilder { let owner = Addr::unchecked("owner"); let cw20_code_id = store_cw20(&mut app); - let pair_code_id = store_pair(&mut app); + let stable_pair_code_id = store_pair(&mut app); + let xyk_pair_code_id = store_xyk_pair(&mut app); let factory_code_id = store_factory(&mut app); let staking_code_id = store_staking(&mut app); let factory = app @@ -144,15 +158,26 @@ impl SuiteBuilder { factory_code_id, owner.clone(), &FactoryInstantiateMsg { - pair_configs: vec![PairConfig { - code_id: pair_code_id, - pair_type: PairType::Stable {}, - fee_config: FeeConfig { - total_fee_bps: self.total_fee_bps, - protocol_fee_bps: self.protocol_fee_bps, + pair_configs: vec![ + PairConfig { + code_id: stable_pair_code_id, + pair_type: PairType::Lsd {}, + fee_config: FeeConfig { + total_fee_bps: self.total_fee_bps, + protocol_fee_bps: self.protocol_fee_bps, + }, + is_disabled: false, }, - is_disabled: false, - }], + PairConfig { + code_id: xyk_pair_code_id, + pair_type: PairType::Xyk {}, + fee_config: FeeConfig { + total_fee_bps: self.total_fee_bps, + protocol_fee_bps: self.protocol_fee_bps, + }, + is_disabled: false, + }, + ], token_code_id: cw20_code_id, fee_address: None, owner: owner.to_string(), @@ -505,6 +530,17 @@ impl Suite { ) } + pub fn update_config(&mut self, params: StablePoolUpdateParams) -> AnyResult { + self.app.execute_contract( + Addr::unchecked("sender"), + self.mock_hub.clone(), + &PairExecuteMsg::UpdateConfig { + params: to_binary(¶ms)?, + }, + &[], + ) + } + pub fn query_simulation( &self, pair: &Addr, @@ -531,6 +567,44 @@ impl Suite { Ok(res) } + pub fn query_spot_price( + &self, + pair: &Addr, + offer: &AssetInfo, + ask: &AssetInfo, + ) -> AnyResult { + let res: SpotPriceResponse = self.app.wrap().query_wasm_smart( + pair.clone(), + &QueryMsg::SpotPrice { + offer: offer.clone(), + ask: ask.clone(), + }, + )?; + Ok(res.price) + } + + pub fn query_predict_spot_price( + &self, + pair: &Addr, + offer: &AssetInfo, + ask: &AssetInfo, + max_trade: Uint128, + target_price: Decimal, + iterations: u8, + ) -> AnyResult> { + let res: SpotPricePredictionResponse = self.app.wrap().query_wasm_smart( + pair.clone(), + &QueryMsg::SpotPricePrediction { + offer: offer.clone(), + ask: ask.clone(), + max_trade, + target_price, + iterations, + }, + )?; + Ok(res.trade) + } + pub fn query_balance(&self, sender: &str, denom: &str) -> AnyResult { let amount = self .app diff --git a/contracts/pair_stable/src/multitest/target_rate.rs b/contracts/pair_lsd/src/multitest/target_rate.rs similarity index 64% rename from contracts/pair_stable/src/multitest/target_rate.rs rename to contracts/pair_lsd/src/multitest/target_rate.rs index b1048ac..aeac144 100644 --- a/contracts/pair_stable/src/multitest/target_rate.rs +++ b/contracts/pair_lsd/src/multitest/target_rate.rs @@ -2,6 +2,7 @@ use std::str::FromStr; use cosmwasm_std::{assert_approx_eq, coin, Addr, Decimal, Fraction, Uint128}; use cw_multi_test::{BankSudo, SudoMsg}; +use wyndex::pair::LsdInfo; use wyndex::{ asset::{AssetInfo, AssetInfoExt}, factory::PairType, @@ -26,12 +27,15 @@ fn basic_provide_and_swap() { let pair = suite .create_pair_and_provide_liquidity( - PairType::Stable {}, + PairType::Lsd {}, Some(StablePoolParams { amp: 45, owner: Some("owner".to_string()), - lsd_hub: Some(suite.mock_hub.to_string()), - target_rate_epoch: DAY, + lsd: Some(LsdInfo { + asset: wy_juno_info.clone(), + hub: suite.mock_hub.to_string(), + target_rate_epoch: DAY, + }), }), (juno_info.clone(), 150_000_000_000_000_000), (wy_juno_info.clone(), 100_000_000_000_000_000), @@ -39,6 +43,18 @@ fn basic_provide_and_swap() { ) .unwrap(); + // check spot price is 1 wyJUNO -> 1.5 JUNO + let spot = suite + .query_spot_price(&pair, &wy_juno_info, &juno_info) + .unwrap(); + assert_eq!(spot, Decimal::percent(150)); + + // check spot price is 1 JUNO -> 0.666666 wyJUNO + let spot = suite + .query_spot_price(&pair, &juno_info, &wy_juno_info) + .unwrap(); + assert_eq!(spot, Decimal::from_ratio(666_667u128, 1_000_000u128)); + let sim = suite .query_simulation(&pair, wy_juno_info.with_balance(10u128), None) .unwrap(); @@ -89,12 +105,15 @@ fn simple_provide_liquidity() { let pair = suite .create_pair_and_provide_liquidity( - PairType::Stable {}, + PairType::Lsd {}, Some(StablePoolParams { amp: 45, owner: Some("owner".to_string()), - lsd_hub: Some(suite.mock_hub.to_string()), - target_rate_epoch: DAY, + lsd: Some(LsdInfo { + asset: wy_juno_info.clone(), + hub: suite.mock_hub.to_string(), + target_rate_epoch: DAY, + }), }), (juno_info, 150_000_000_000_000_000), (wy_juno_info, 100_000_000_000_000_000), @@ -153,12 +172,15 @@ fn provide_liquidity_multiple() { let pair = suite .create_pair( "owner", - PairType::Stable {}, + PairType::Lsd {}, Some(StablePoolParams { amp: 45, owner: Some("owner".to_string()), - lsd_hub: Some(suite.mock_hub.to_string()), - target_rate_epoch: DAY, + lsd: Some(LsdInfo { + asset: wy_juno_info.clone(), + hub: suite.mock_hub.to_string(), + target_rate_epoch: DAY, + }), }), &[juno_info.clone(), wy_juno_info.clone()], ) @@ -260,12 +282,15 @@ fn provide_liquidity_changing_rate() { let pair = suite .create_pair_and_provide_liquidity( - PairType::Stable {}, + PairType::Lsd {}, Some(StablePoolParams { amp: 45, owner: Some("owner".to_string()), - lsd_hub: Some(suite.mock_hub.to_string()), - target_rate_epoch: DAY, + lsd: Some(LsdInfo { + asset: wy_juno_info.clone(), + hub: suite.mock_hub.to_string(), + target_rate_epoch: DAY, + }), }), (juno_info.clone(), 150_000_000_000_000_000), (wy_juno_info, 100_000_000_000_000_000), @@ -321,12 +346,15 @@ fn changing_target_rate() { let pair = suite .create_pair_and_provide_liquidity( - PairType::Stable {}, + PairType::Lsd {}, Some(StablePoolParams { amp: 45, owner: Some("owner".to_string()), - lsd_hub: Some(suite.mock_hub.to_string()), - target_rate_epoch: DAY, + lsd: Some(LsdInfo { + asset: wy_juno_info.clone(), + hub: suite.mock_hub.to_string(), + target_rate_epoch: DAY, + }), }), (juno_info.clone(), 150_000_000_000_000_000), (wy_juno_info.clone(), 100_000_000_000_000_000), @@ -421,12 +449,15 @@ fn drastic_rate_change() { let pair = suite .create_pair_and_provide_liquidity( - PairType::Stable {}, + PairType::Lsd {}, Some(StablePoolParams { amp: 45, owner: Some("owner".to_string()), - lsd_hub: Some(suite.mock_hub.to_string()), - target_rate_epoch: DAY, + lsd: Some(LsdInfo { + asset: wy_juno_info.clone(), + hub: suite.mock_hub.to_string(), + target_rate_epoch: DAY, + }), }), (juno_info.clone(), 200_000_000_000_000_000), (wy_juno_info.clone(), 100_000_000_000_000_000), @@ -440,14 +471,32 @@ fn drastic_rate_change() { assert_eq!(sim.return_amount.u128(), 200_000); assert_eq!(sim.spread_amount.u128(), 0); + // check spot price is 1 wyJUNO -> 2 JUNO + let spot = suite + .query_spot_price(&pair, &wy_juno_info, &juno_info) + .unwrap(); + assert_eq!(spot, Decimal::percent(200)); + // change target rate to 1.2 and wait for cache to expire let target_rate = Decimal::from_atomics(12u128, 1).unwrap(); suite.change_target_value(target_rate).unwrap(); suite.wait(DAY); + // check spot price is still 1 wyJUNO -> 2 JUNO (shows it is not always target rate) + let spot = suite + .query_spot_price(&pair, &wy_juno_info, &juno_info) + .unwrap(); + assert_eq!(spot, Decimal::percent(200)); + // we have too much JUNO in the pool, so we arbitrage it away arbitrage_to(&mut suite, &pair, &wy_juno_info, target_rate); + // check spot price is now 1 wyJUNO -> 1.2 JUNO + let spot = suite + .query_spot_price(&pair, &wy_juno_info, &juno_info) + .unwrap(); + assert_eq!(spot, Decimal::percent(120)); + suite .swap( &pair, @@ -465,15 +514,33 @@ fn drastic_rate_change() { "0.000002" ); + // check spot price is slightly less 1 wyJUNO -> 1.2 JUNO + let spot = suite + .query_spot_price(&pair, &wy_juno_info, &juno_info) + .unwrap(); + assert_eq!(spot, Decimal::from_atomics(1_199_999u128, 6).unwrap()); + // change target rate to 2.5 and wait for cache to expire let target_rate = Decimal::from_atomics(25u128, 1).unwrap(); suite.change_target_value(target_rate).unwrap(); suite.wait(DAY); + // check spot price is unchanged + let spot = suite + .query_spot_price(&pair, &wy_juno_info, &juno_info) + .unwrap(); + assert_eq!(spot, Decimal::from_atomics(1_199_999u128, 6).unwrap()); + // we have too much wyJUNO in the pool, so we arbitrage it away // the next swap will fail with spread assertion if we don't arbitrage_to(&mut suite, &pair, &juno_info, target_rate); + // check spot price is now 1 wyJUNO -> 2.5 JUNO + let spot = suite + .query_spot_price(&pair, &wy_juno_info, &juno_info) + .unwrap(); + assert_eq!(spot, Decimal::from_atomics(2_500_001u128, 6).unwrap()); + let prev_balance = suite.query_balance("sender", "juno").unwrap(); suite .swap( @@ -493,8 +560,201 @@ fn drastic_rate_change() { ); } +#[test] +fn changing_spot_price() { + let target_rate = Decimal::from_atomics(15u128, 1).unwrap(); + let mut suite = SuiteBuilder::new() + .with_funds("sender", &[coin(1_000_000_000, "juno")]) + .with_funds("arbitrageur", &[coin(1_000_000_000, "juno")]) + .with_initial_target_rate(target_rate) + .build(); + + let juno_info = AssetInfo::Native("juno".to_string()); + let wy_juno = suite.instantiate_token("owner", "wyJUNO"); + let wy_juno_info = AssetInfo::Token(wy_juno.to_string()); + + let pair = suite + .create_pair_and_provide_liquidity( + PairType::Lsd {}, + Some(StablePoolParams { + amp: 45, + owner: Some("owner".to_string()), + lsd: Some(LsdInfo { + asset: wy_juno_info.clone(), + hub: suite.mock_hub.to_string(), + target_rate_epoch: DAY, + }), + }), + (juno_info.clone(), 150_000_000), + (wy_juno_info.clone(), 100_000_000), + vec![coin(150_000_000, "juno")], + ) + .unwrap(); + + // check spot price is about 1 wyJUNO -> 1.5 JUNO + let spot = suite + .query_spot_price(&pair, &wy_juno_info, &juno_info) + .unwrap(); + assert_eq!(spot, Decimal::from_atomics(1_499_674u128, 6).unwrap()); + + // small swap (3% of juno) stays close to target + suite + .swap( + &pair, + "sender", + juno_info.with_balance(4_500_000u128), + None, + None, + None, + None, + ) + .unwrap(); + assert_approx_eq!( + suite.query_cw20_balance("sender", &wy_juno).unwrap(), + 3_000_000u128, + "0.001" + ); + + // check spot price is slightly higher than before + let spot = suite + .query_spot_price(&pair, &wy_juno_info, &juno_info) + .unwrap(); + assert_eq!(spot, Decimal::from_atomics(1_501_633u128, 6).unwrap()); + + // big swap (double juno) changes price a lot target + suite + .swap( + &pair, + "sender", + juno_info.with_balance(150_000_000u128), + None, + None, + // allow a huge slippage... + Decimal::percent(50), + None, + ) + .unwrap(); + + // check spot price is much larger (TODO: verify exact value with math equations) + let spot = suite + .query_spot_price(&pair, &wy_juno_info, &juno_info) + .unwrap(); + assert_eq!(spot, Decimal::from_atomics(3_483_313u128, 6).unwrap()); +} + +#[test] +fn predict_swap_spot_price() { + let iterations = 10u8; + let target_rate = Decimal::from_atomics(15u128, 1).unwrap(); + let mut suite = SuiteBuilder::new() + .with_funds("sender", &[coin(1_000_000_000, "juno")]) + .with_funds("arbitrageur", &[coin(1_000_000_000, "juno")]) + .with_initial_target_rate(target_rate) + .build(); + + let juno_info = AssetInfo::Native("juno".to_string()); + let wy_juno = suite.instantiate_token("owner", "wyJUNO"); + let wy_juno_info = AssetInfo::Token(wy_juno.to_string()); + + let pair = suite + .create_pair_and_provide_liquidity( + PairType::Lsd {}, + Some(StablePoolParams { + amp: 6, + owner: Some("owner".to_string()), + lsd: Some(LsdInfo { + asset: wy_juno_info.clone(), + hub: suite.mock_hub.to_string(), + target_rate_epoch: DAY, + }), + }), + (juno_info.clone(), 1_500_000_000), + (wy_juno_info.clone(), 1_000_000_000), + vec![coin(1_500_000_000, "juno")], + ) + .unwrap(); + + // check spot price is about 1 JUNO -> 0.666 wyJUNO + let spot = suite + .query_spot_price(&pair, &juno_info, &wy_juno_info) + .unwrap(); + // this is within 0.01% + assert_approx_eq!( + spot * Uint128::new(1_000_000), + Uint128::new(666666), + "0.0001" + ); + + // aiming for a price above current will return None + let amount = Uint128::new(100_000_000); + let to_swap = suite + .query_predict_spot_price( + &pair, + &juno_info, + &wy_juno_info, + amount, + Decimal::percent(70), + iterations, + ) + .unwrap(); + assert_eq!(to_swap, None); + + // aiming for a price far below current will return full amount + let swap_all = suite + .query_predict_spot_price( + &pair, + &juno_info, + &wy_juno_info, + amount, + Decimal::percent(60), + iterations, + ) + .unwrap(); + let swap_all = swap_all.unwrap(); + assert_eq!(swap_all, amount); + + // aiming for a price slightly below current will return some partial value + let target = Decimal::permille(656); + let to_swap = suite + .query_predict_spot_price(&pair, &juno_info, &wy_juno_info, amount, target, iterations) + .unwrap(); + // must be Some + let to_swap = to_swap.unwrap(); + // must be less than amount + assert!(to_swap < swap_all); + + // verify this value does lead to proper spot price after swap + suite + .swap( + &pair, + "sender", + juno_info.with_balance(to_swap), + None, + None, + Decimal::percent(5), + None, + ) + .unwrap(); + + // check spot price is very close to desired (seems to be about 0.3% above... fee?) + let spot = suite + .query_spot_price(&pair, &juno_info, &wy_juno_info) + .unwrap(); + // this is within 0.01% + assert_approx_eq!( + spot * Uint128::new(1_000_000), + target * Uint128::new(1_000_000), + "0.0001" + ); +} + /// Helper function that swaps until the target rate is reached. -fn arbitrage_to(suite: &mut Suite, pair: &Addr, offer_asset: &AssetInfo, mut target_rate: Decimal) { +pub fn arbitrage_to( + suite: &mut Suite, + pair: &Addr, + offer_asset: &AssetInfo, + mut target_rate: Decimal, +) { if !offer_asset.is_native_token() { // we have too much of the lsd token, so we swap it for the native one // but we need to invert the rate (since it is given as native tokens / lsd tokens) diff --git a/contracts/pair_stable/src/state.rs b/contracts/pair_lsd/src/state.rs similarity index 82% rename from contracts/pair_stable/src/state.rs rename to contracts/pair_lsd/src/state.rs index 9be71ea..6965e57 100644 --- a/contracts/pair_stable/src/state.rs +++ b/contracts/pair_lsd/src/state.rs @@ -31,9 +31,32 @@ pub struct Config { /// The block time until which trading is disabled pub trading_starts: u64, + pub lsd: Option, +} + +impl Config { + pub fn target_rate(&self) -> Decimal { + match &self.lsd { + Some(lsd) => lsd.target_rate, + None => Decimal::one(), + } + } + + pub fn is_lsd(&self, asset: &AssetInfoValidated) -> bool { + self.lsd + .as_ref() + .map(|l| &l.asset == asset) + .unwrap_or(false) + } +} + +#[cw_serde] +pub struct LsdData { + /// Which asset is the LSD (and thus has the target_rate) + pub asset: AssetInfoValidated, /// Address of the liquid staking hub contract for this pool. - /// If set, this is used to get the target value to concentrate liquidity around. - pub lsd_hub: Option, + /// This is used to get the target value to concentrate liquidity around. + pub lsd_hub: Addr, /// The target rate to concentrate liquidity around. Defaults to `1.0`. /// If `lsd_hub` is set, this is updated once per `target_rate_epoch`. pub target_rate: Decimal, diff --git a/contracts/pair_stable/src/testing.rs b/contracts/pair_lsd/src/testing.rs similarity index 90% rename from contracts/pair_stable/src/testing.rs rename to contracts/pair_lsd/src/testing.rs index aee0c55..b34086e 100644 --- a/contracts/pair_stable/src/testing.rs +++ b/contracts/pair_lsd/src/testing.rs @@ -1,4 +1,4 @@ -use crate::contract::{execute, instantiate, migrate, query}; +use crate::contract::{execute, instantiate, migrate}; use crate::state::CONFIG; use wyndex::fee_config::FeeConfig; // TODO: Copied here just as a temporary measure @@ -6,16 +6,16 @@ use crate::mock_querier::mock_dependencies; use cosmwasm_std::testing::{mock_env, mock_info, MOCK_CONTRACT_ADDR}; use cosmwasm_std::{ - attr, coin, from_binary, to_binary, Addr, BankMsg, BlockInfo, Coin, CosmosMsg, Decimal, - DepsMut, Env, ReplyOn, Response, StdError, SubMsg, Timestamp, Uint128, WasmMsg, + attr, to_binary, Addr, BankMsg, BlockInfo, Coin, CosmosMsg, Decimal, DepsMut, Env, ReplyOn, + Response, StdError, SubMsg, Timestamp, Uint128, WasmMsg, }; use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg, MinterResponse}; use cw20_base::msg::InstantiateMsg as TokenInstantiateMsg; use cw_utils::MsgInstantiateContractResponse; use wyndex::asset::{Asset, AssetInfo, AssetInfoValidated}; use wyndex::pair::{ - ContractError, Cw20HookMsg, ExecuteMsg, HistoricalPricesResponse, HistoryDuration, - InstantiateMsg, MigrateMsg, QueryMsg, StablePoolParams, StakeConfig, + ContractError, Cw20HookMsg, ExecuteMsg, InstantiateMsg, MigrateMsg, StablePoolParams, + StakeConfig, }; fn mock_env_with_block_time(time: u64) -> Env { @@ -75,8 +75,7 @@ fn proper_initialization() { to_binary(&StablePoolParams { amp: 100, owner: None, - lsd_hub: None, - target_rate_epoch: 0, + lsd: None, }) .unwrap(), ), @@ -176,8 +175,7 @@ fn test_freezing_a_pool_blocking_actions_then_unfreeze() { to_binary(&StablePoolParams { amp: 100, owner: None, - lsd_hub: None, - target_rate_epoch: 0, + lsd: None, }) .unwrap(), ), @@ -531,8 +529,7 @@ fn provide_liquidity() { to_binary(&StablePoolParams { amp: 100, owner: None, - lsd_hub: None, - target_rate_epoch: 0, + lsd: None, }) .unwrap(), ), @@ -899,8 +896,7 @@ fn withdraw_liquidity() { to_binary(&StablePoolParams { amp: 100, owner: None, - lsd_hub: None, - target_rate_epoch: 0, + lsd: None, }) .unwrap(), ), @@ -997,203 +993,6 @@ fn withdraw_liquidity() { ); } -#[test] -fn query_historical() { - let mut deps = mock_dependencies(&[]); - let mut env = mock_env(); - - let user = "user"; - - // setup some cw20 tokens, so the queries don't fail - deps.querier.with_token_balances(&[ - ( - &"asset0000".into(), - &[(&MOCK_CONTRACT_ADDR.into(), &0u128.into())], - ), - ( - &"liquidity0000".into(), - &[(&MOCK_CONTRACT_ADDR.into(), &0u128.into())], - ), - ]); - - let uusd = AssetInfoValidated::Native("uusd".to_string()); - let token = AssetInfoValidated::Token(Addr::unchecked("asset0000")); - - // instantiate the contract - let msg = InstantiateMsg { - asset_infos: vec![uusd.clone().into(), token.clone().into()], - token_code_id: 10u64, - factory_addr: String::from("factory"), - init_params: Some( - to_binary(&StablePoolParams { - amp: 100, - owner: None, - lsd_hub: None, - target_rate_epoch: 0, - }) - .unwrap(), - ), - staking_config: default_stake_config(), - trading_starts: 0, - fee_config: FeeConfig { - total_fee_bps: 0, - protocol_fee_bps: 0, - }, - circuit_breaker: None, - }; - instantiate(deps.as_mut(), env.clone(), mock_info("owner", &[]), msg).unwrap(); - - // Store the liquidity token - store_liquidity_token(deps.as_mut(), "liquidity0000".to_string()); - - // query price history before any price changes - let history: HistoricalPricesResponse = from_binary( - &query( - deps.as_ref(), - env.clone(), - QueryMsg::HistoricalPrices { - duration: HistoryDuration::Day, - }, - ) - .unwrap(), - ) - .unwrap(); - assert_eq!( - history.historical_prices, - vec![ - (uusd.clone(), token.clone(), vec![]), - (token.clone(), uusd.clone(), vec![]) - ], - "price history should be empty, no price changes yet" - ); - - // provide liquidity to get a first price - let msg = ExecuteMsg::ProvideLiquidity { - assets: vec![ - Asset { - info: uusd.clone().into(), - amount: 1_000_000u128.into(), - }, - Asset { - info: token.clone().into(), - amount: 1_000_000u128.into(), - }, - ], - slippage_tolerance: None, - receiver: None, - }; - // need to set balance manually to simulate funds being sent - deps.querier - .with_balance(&[(&MOCK_CONTRACT_ADDR.into(), &[coin(1_000_000u128, "uusd")])]); - execute( - deps.as_mut(), - env.clone(), - mock_info(user, &[coin(1_000_000u128, "uusd")]), - msg, - ) - .unwrap(); - - // query price history after first price change - let history: HistoricalPricesResponse = from_binary( - &query( - deps.as_ref(), - env.clone(), - QueryMsg::HistoricalPrices { - duration: HistoryDuration::Day, - }, - ) - .unwrap(), - ) - .unwrap(); - assert_eq!( - history.historical_prices, - vec![ - ( - uusd.clone(), - token.clone(), - vec![(env.block.time.seconds(), 934_113u128.into())] - ), - ( - token.clone(), - uusd.clone(), - vec![(env.block.time.seconds(), 934_113u128.into())] - ) - ], - ); - - // forward time half an hour - const HALF_HOUR: u64 = 30 * 60; - env.block.time = env.block.time.plus_seconds(HALF_HOUR); - - // swap to get a second price - let msg = ExecuteMsg::Swap { - offer_asset: Asset { - info: uusd.clone().into(), - amount: 10_000u128.into(), - }, - to: None, - max_spread: None, - belief_price: None, - ask_asset_info: None, - referral_address: None, - referral_commission: None, - }; - // need to set balance manually to simulate funds being sent - deps.querier - .with_balance(&[(&MOCK_CONTRACT_ADDR.into(), &[coin(1_010_000u128, "uusd")])]); - deps.querier.with_token_balances(&[ - ( - &"asset0000".into(), - &[(&MOCK_CONTRACT_ADDR.into(), &1_000_000u128.into())], - ), - ( - &"liquidity0000".into(), - &[(&MOCK_CONTRACT_ADDR.into(), &0u128.into())], - ), - ]); - execute( - deps.as_mut(), - env.clone(), - mock_info(user, &[coin(10_000u128, "uusd")]), - msg, - ) - .unwrap(); - - // query price history after swap price change - let history: HistoricalPricesResponse = from_binary( - &query( - deps.as_ref(), - env.clone(), - QueryMsg::HistoricalPrices { - duration: HistoryDuration::Day, - }, - ) - .unwrap(), - ) - .unwrap(); - assert_eq!( - history.historical_prices, - vec![ - ( - uusd.clone(), - token.clone(), - vec![ - (env.block.time.seconds() - HALF_HOUR, 934_113u128.into()), - (env.block.time.seconds(), 928_761u128.into()) - ] - ), - ( - token, - uusd, - vec![ - (env.block.time.seconds() - HALF_HOUR, 934_113u128.into()), - (env.block.time.seconds(), 939_112u128.into()) - ] - ) - ], - ); -} - #[cfg(feature = "requires-python-sim")] mod disabled { use super::*; @@ -1792,7 +1591,7 @@ mod disabled { asset_infos: vec![asset_x, asset_y], contract_addr: Addr::unchecked(MOCK_CONTRACT_ADDR), liquidity_token: Addr::unchecked("lp_token"), - pair_type: PairType::Stable {}, + pair_type: PairType::Lsd {}, }, factory_addr: Addr::unchecked("factory"), block_time_last: case.block_time_last, diff --git a/contracts/pair_stable/src/utils.rs b/contracts/pair_lsd/src/utils.rs similarity index 64% rename from contracts/pair_stable/src/utils.rs rename to contracts/pair_lsd/src/utils.rs index 6b1be8a..0a07e6e 100644 --- a/contracts/pair_stable/src/utils.rs +++ b/contracts/pair_lsd/src/utils.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Decimal256, Deps, Env, StdResult, Storage, Uint128, Uint64}; +use cosmwasm_std::{Decimal, Decimal256, Deps, Env, StdResult, Storage, Uint128, Uint256, Uint64}; use itertools::Itertools; use std::cmp::Ordering; use wyndex::oracle::PricePoint; @@ -146,7 +146,7 @@ pub(crate) fn compute_swap( pools, compute_current_amp(config, env)?, token_precision, - config.target_rate, + config, )?; let return_amount = ask_pool.amount.to_uint128_with_precision(token_precision)? - new_ask_pool; @@ -155,10 +155,8 @@ pub(crate) fn compute_swap( .to_uint128_with_precision(token_precision)?; // We consider swap rate to be target_rate in stable swap thus any difference is considered as spread. - let spread_amount = - apply_rate(&offer_asset.info, offer_asset_amount, config.target_rate).saturating_sub( - apply_rate(&ask_pool.info, return_amount, config.target_rate), - ); + let spread_amount = apply_rate(&offer_asset.info, offer_asset_amount, config) + .saturating_sub(apply_rate(&ask_pool.info, return_amount, config)); Ok(SwapResult { return_amount, @@ -223,6 +221,8 @@ pub fn accumulate_prices( /// an empty vector if one of the pools is empty. /// /// * **pools** array with assets available in the pool *after* the latest operation. +// note: will be used for oracles +#[allow(dead_code)] pub fn calc_new_prices( deps: Deps, env: &Env, @@ -247,12 +247,161 @@ pub fn calc_new_prices( &ask_pool, pools, )?; - prices.push(PricePoint::new(from.clone(), to.clone(), return_amount)); } - Ok(prices) } else { Ok(vec![]) } } + +pub fn calc_spot_price( + deps: Deps, + env: &Env, + config: &Config, + offer: &AssetInfoValidated, + ask: &AssetInfoValidated, + pools: &[DecimalAsset], +) -> Result { + let offer_asset = DecimalAsset { + info: offer.clone(), + // This is 1 unit (adjusted for number of decimals) + amount: Decimal256::one(), + }; + let (offer_pool, ask_pool) = select_pools(Some(offer), Some(ask), pools)?; + + // try swapping one unit to see how much we get + let SwapResult { return_amount, .. } = compute_swap( + deps.storage, + env, + config, + &offer_asset, + &offer_pool, + &ask_pool, + pools, + )?; + + // Return amount is in number of base units. To make it decimal, we must divide by precision + let decimals = get_precision(deps.storage, &ask_pool.info)?; + let price = Decimal::from_atomics(return_amount, decimals as u32).unwrap(); + Ok(price) +} + +#[allow(clippy::too_many_arguments)] +pub fn find_spot_price( + deps: Deps, + env: &Env, + config: &Config, + offer: AssetInfoValidated, + ask: AssetInfoValidated, + pools: Vec, + max_trade: Uint128, + target_price: Decimal, + iterations: u8, +) -> Result, ContractError> { + // normalize the max_trade with precision + let decimals = get_precision(deps.storage, &offer)?; + let mut trade = Decimal256::from_atomics(max_trade, decimals as u32).unwrap(); + + // check min boundary (is price already too high) + let current = calc_spot_price(deps, env, config, &offer, &ask, &pools)?; + if current <= target_price { + return Ok(None); + } + + // check max boundary (if i swap all assets, is price still good enough) + let max_pools = pools_after_swap(config, &offer, &ask, &pools, trade); + let all_in = calc_spot_price(deps, env, config, &offer, &ask, &max_pools)?; + + // if this does not fit, recurse to find it (otherwise just use the max trade) + if all_in < target_price { + trade = recurse_bisect_spot_price( + deps, + env, + config, + &offer, + &ask, + &pools, + Decimal256::zero(), + trade, + target_price, + iterations, + )?; + } + + let amount = trade * Uint256::from(10_u128.pow(decimals as u32)); + Ok(Some(amount.try_into().unwrap())) +} + +#[allow(clippy::too_many_arguments)] +pub fn recurse_bisect_spot_price( + deps: Deps, + env: &Env, + config: &Config, + offer: &AssetInfoValidated, + ask: &AssetInfoValidated, + pools: &[DecimalAsset], + min_trade: Decimal256, + max_trade: Decimal256, + target_price: Decimal, + iterations: u8, +) -> Result { + // at the end, return mid-point + let half = (min_trade + max_trade) / Decimal256::percent(200); + if iterations == 0 { + return Ok(half); + } + + // find price at midpoint + let mid_pools = pools_after_swap(config, offer, ask, pools, half); + let mid_price = calc_spot_price(deps, env, config, offer, ask, &mid_pools)?; + // and refine bounds up or down + let (min_trade, max_trade) = match mid_price.cmp(&target_price) { + std::cmp::Ordering::Equal => return Ok(half), + std::cmp::Ordering::Greater => (half, max_trade), + std::cmp::Ordering::Less => (min_trade, half), + }; + + // recurse with one less iteration + recurse_bisect_spot_price( + deps, + env, + config, + offer, + ask, + pools, + min_trade, + max_trade, + target_price, + iterations - 1, + ) +} + +/// Pretend we swapped amount from token into to token. +/// Return the pools value as if this happened to use for future calculations +fn pools_after_swap( + config: &Config, + offer: &AssetInfoValidated, + ask: &AssetInfoValidated, + pools: &[DecimalAsset], + mut amount: Decimal256, +) -> Vec { + pools + .iter() + .cloned() + .map(|mut asset| { + if config.is_lsd(&asset.info) { + amount /= Decimal256::from(config.target_rate()); + } + if &asset.info == offer { + asset.amount += amount; + asset + } else if &asset.info == ask { + asset.amount -= amount; + asset + } else { + asset + } + }) + .collect() +} diff --git a/contracts/pair_stable/tests/3pool_tests.rs b/contracts/pair_lsd/tests/3pool_tests.rs similarity index 96% rename from contracts/pair_stable/tests/3pool_tests.rs rename to contracts/pair_lsd/tests/3pool_tests.rs index 12bdecc..e639c15 100644 --- a/contracts/pair_stable/tests/3pool_tests.rs +++ b/contracts/pair_lsd/tests/3pool_tests.rs @@ -9,6 +9,7 @@ use crate::helper::{Helper, TestCoin}; mod helper; +#[ignore = "Only support 2 pools"] #[test] fn provide_and_withdraw_no_fee() { let owner = Addr::unchecked("owner"); @@ -131,6 +132,7 @@ fn provide_and_withdraw_no_fee() { assert_eq!(0, helper.coin_balance(&test_coins[2], &user3)); } +#[ignore = "Only support 2 pools"] #[test] fn provide_with_different_precision() { let owner = Addr::unchecked("owner"); @@ -188,6 +190,7 @@ fn provide_with_different_precision() { assert_eq!(100000000, helper.coin_balance(&test_coins[2], &user2)); } +#[ignore = "Only support 2 pools"] #[test] fn swap_different_precisions() { let owner = Addr::unchecked("owner"); @@ -237,6 +240,7 @@ fn swap_different_precisions() { assert_eq!(99_949011, helper.coin_balance(&test_coins[2], &user)); } +#[ignore = "Only support 2 pools"] #[test] fn check_swaps() { let owner = Addr::unchecked("owner"); @@ -293,7 +297,7 @@ fn check_wrong_initializations() { let err = Helper::new(&owner, vec![TestCoin::native("uluna")], 100u64, None).unwrap_err(); assert_eq!( - ContractError::InvalidNumberOfAssets { min: 2, max: 5 }, + ContractError::InvalidNumberOfAssets { min: 2, max: 2 }, err.downcast().unwrap() ); @@ -313,17 +317,13 @@ fn check_wrong_initializations() { .unwrap_err(); assert_eq!( - ContractError::InvalidNumberOfAssets { min: 2, max: 5 }, + ContractError::InvalidNumberOfAssets { min: 2, max: 2 }, err.downcast().unwrap() ); let err = Helper::new( &owner, - vec![ - TestCoin::native("uluna"), - TestCoin::native("uluna"), - TestCoin::cw20("USDC"), - ], + vec![TestCoin::native("uluna"), TestCoin::native("uluna")], 100u64, None, ) @@ -333,23 +333,9 @@ fn check_wrong_initializations() { err.root_cause().to_string(), "Doubling assets in asset infos" ); - - // 5 assets in the pool is okay - Helper::new( - &owner, - vec![ - TestCoin::native("one"), - TestCoin::cw20("two"), - TestCoin::native("three"), - TestCoin::cw20("four"), - TestCoin::native("five"), - ], - 100u64, - None, - ) - .unwrap(); } +#[ignore = "Only support 2 pools"] #[test] fn check_withdraw_charges_fees() { let owner = Addr::unchecked("owner"); @@ -425,6 +411,7 @@ fn check_withdraw_charges_fees() { ); } +#[ignore = "Only support 2 pools"] #[test] fn check_5pool_prices() { let owner = Addr::unchecked("owner"); diff --git a/contracts/pair_stable/tests/helper.rs b/contracts/pair_lsd/tests/helper.rs similarity index 98% rename from contracts/pair_stable/tests/helper.rs rename to contracts/pair_lsd/tests/helper.rs index 148c4d4..cc5bf55 100644 --- a/contracts/pair_stable/tests/helper.rs +++ b/contracts/pair_lsd/tests/helper.rs @@ -18,7 +18,7 @@ use wyndex::pair::{ ReverseSimulationResponse, SimulationResponse, StablePoolParams, }; use wyndex::querier::NATIVE_TOKEN_PRECISION; -use wyndex_pair_stable::contract::{execute, instantiate, query, reply}; +use wyndex_pair_lsd::contract::{execute, instantiate, query, reply}; const INIT_BALANCE: u128 = 1_000_000_000_000; @@ -156,7 +156,7 @@ impl Helper { protocol_fee_bps: 5000, total_fee_bps: swap_fee.unwrap_or(5u16), }, - pair_type: PairType::Stable {}, + pair_type: PairType::Lsd {}, is_disabled: false, }], token_code_id, @@ -187,14 +187,13 @@ impl Helper { .map(|(_, asset_info)| asset_info) .collect_vec(); let init_pair_msg = wyndex::factory::ExecuteMsg::CreatePair { - pair_type: PairType::Stable {}, + pair_type: PairType::Lsd {}, asset_infos: asset_infos.clone(), init_params: Some( to_binary(&StablePoolParams { amp, owner: None, - lsd_hub: None, - target_rate_epoch: 0, + lsd: None, }) .unwrap(), ), diff --git a/contracts/pair_stable/tests/integration.rs b/contracts/pair_lsd/tests/integration.rs similarity index 97% rename from contracts/pair_stable/tests/integration.rs rename to contracts/pair_lsd/tests/integration.rs index 756043a..a07ed8a 100644 --- a/contracts/pair_stable/tests/integration.rs +++ b/contracts/pair_lsd/tests/integration.rs @@ -17,7 +17,7 @@ use cw20::{BalanceResponse, Cw20Coin, Cw20ExecuteMsg, Cw20QueryMsg, MinterRespon use cw20_base::msg::InstantiateMsg as TokenInstantiateMsg; use cw_multi_test::{App, ContractWrapper, Executor}; use wyndex::querier::query_token_balance; -use wyndex_pair_stable::math::{MAX_AMP, MAX_AMP_CHANGE, MIN_AMP_CHANGING_TIME}; +use wyndex_pair_lsd::math::{MAX_AMP, MAX_AMP_CHANGE, MIN_AMP_CHANGING_TIME}; const OWNER: &str = "owner"; @@ -41,11 +41,11 @@ fn store_token_code(app: &mut App) -> u64 { fn store_pair_code(app: &mut App) -> u64 { let pair_contract = Box::new( ContractWrapper::new_with_empty( - wyndex_pair_stable::contract::execute, - wyndex_pair_stable::contract::instantiate, - wyndex_pair_stable::contract::query, + wyndex_pair_lsd::contract::execute, + wyndex_pair_lsd::contract::instantiate, + wyndex_pair_lsd::contract::query, ) - .with_reply_empty(wyndex_pair_stable::contract::reply), + .with_reply_empty(wyndex_pair_lsd::contract::reply), ); app.store_code(pair_contract) @@ -90,7 +90,7 @@ fn instantiate_factory(router: &mut App, owner: &Addr) -> Addr { pair_configs: vec![PairConfig { code_id: pair_contract_code_id, fee_config, - pair_type: PairType::Stable {}, + pair_type: PairType::Lsd {}, is_disabled: false, }], token_code_id: token_contract_code_id, @@ -114,7 +114,7 @@ fn instantiate_pair(router: &mut App, owner: &Addr) -> Addr { AssetInfo::Native("uluna".to_string()), ]; let msg = FactoryExecuteMsg::CreatePair { - pair_type: PairType::Stable {}, + pair_type: PairType::Lsd {}, asset_infos: asset_infos.clone(), init_params: None, total_fee_bps: None, @@ -130,14 +130,13 @@ fn instantiate_pair(router: &mut App, owner: &Addr) -> Addr { ); let msg = FactoryExecuteMsg::CreatePair { - pair_type: PairType::Stable {}, + pair_type: PairType::Lsd {}, asset_infos: asset_infos.clone(), init_params: Some( to_binary(&StablePoolParams { amp: 100, owner: None, - lsd_hub: None, - target_rate_epoch: 0, + lsd: None, }) .unwrap(), ), @@ -219,14 +218,13 @@ fn instantiate_mixed_pair( AssetInfo::Token(cw20_token.to_string()), ]; let msg = FactoryExecuteMsg::CreatePair { - pair_type: PairType::Stable {}, + pair_type: PairType::Lsd {}, asset_infos: asset_infos.clone(), init_params: Some( to_binary(&StablePoolParams { amp: 100, owner: None, - lsd_hub: None, - target_rate_epoch: 0, + lsd: None, }) .unwrap(), ), @@ -538,7 +536,7 @@ fn provide_lp_for_single_token() { protocol_fee_bps: 0, total_fee_bps: 0, }, - pair_type: PairType::Stable {}, + pair_type: PairType::Lsd {}, is_disabled: false, }], token_code_id, @@ -560,7 +558,7 @@ fn provide_lp_for_single_token() { .unwrap(); let msg = FactoryExecuteMsg::CreatePair { - pair_type: PairType::Stable {}, + pair_type: PairType::Lsd {}, asset_infos: vec![ AssetInfo::Token(token_x_instance.to_string()), AssetInfo::Token(token_y_instance.to_string()), @@ -569,8 +567,7 @@ fn provide_lp_for_single_token() { to_binary(&StablePoolParams { amp: 100, owner: None, - lsd_hub: None, - target_rate_epoch: 0, + lsd: None, }) .unwrap(), ), @@ -861,7 +858,7 @@ fn test_compatibility_of_tokens_with_different_precision() { protocol_fee_bps: 0, total_fee_bps: 0, }, - pair_type: PairType::Stable {}, + pair_type: PairType::Lsd {}, is_disabled: false, }], token_code_id, @@ -883,7 +880,7 @@ fn test_compatibility_of_tokens_with_different_precision() { .unwrap(); let msg = FactoryExecuteMsg::CreatePair { - pair_type: PairType::Stable {}, + pair_type: PairType::Lsd {}, asset_infos: vec![ AssetInfo::Token(token_x_instance.to_string()), AssetInfo::Token(token_y_instance.to_string()), @@ -892,8 +889,7 @@ fn test_compatibility_of_tokens_with_different_precision() { to_binary(&StablePoolParams { amp: 100, owner: None, - lsd_hub: None, - target_rate_epoch: 0, + lsd: None, }) .unwrap(), ), @@ -1309,14 +1305,13 @@ fn provide_liquidity_with_one_cw20_asset() { AssetInfo::Token(token2.to_string()), ]; let msg = FactoryExecuteMsg::CreatePair { - pair_type: PairType::Stable {}, + pair_type: PairType::Lsd {}, asset_infos: asset_infos.clone(), init_params: Some( to_binary(&StablePoolParams { amp: 100, owner: None, - lsd_hub: None, - target_rate_epoch: 0, + lsd: None, }) .unwrap(), ), @@ -1513,8 +1508,7 @@ fn update_pair_config() { to_binary(&StablePoolParams { amp: 100, owner: None, - lsd_hub: None, - target_rate_epoch: 0, + lsd: None, }) .unwrap(), ), diff --git a/packages/wyndex/src/factory.rs b/packages/wyndex/src/factory.rs index 6f4a71b..a8152d8 100644 --- a/packages/wyndex/src/factory.rs +++ b/packages/wyndex/src/factory.rs @@ -24,6 +24,8 @@ pub enum PairType { Xyk {}, /// Stable pair type Stable {}, + /// LSD pair type + Lsd {}, /// Custom pair type Custom(String), } @@ -34,6 +36,7 @@ impl Display for PairType { match self { PairType::Xyk {} => fmt.write_str("xyk"), PairType::Stable {} => fmt.write_str("stable"), + PairType::Lsd {} => fmt.write_str("lsd"), PairType::Custom(pair_type) => fmt.write_str(format!("custom-{}", pair_type).as_str()), } } diff --git a/packages/wyndex/src/pair.rs b/packages/wyndex/src/pair.rs index 5dc7675..34ed726 100644 --- a/packages/wyndex/src/pair.rs +++ b/packages/wyndex/src/pair.rs @@ -279,12 +279,27 @@ pub enum QueryMsg { /// Returns information about the cumulative prices in a [`CumulativePricesResponse`] object #[returns(CumulativePricesResponse)] CumulativePrices {}, - /// Returns a price history of the given duration - #[returns(HistoricalPricesResponse)] - HistoricalPrices { duration: HistoryDuration }, /// Returns current D invariant in as a [`u128`] value #[returns(Uint128)] QueryComputeD {}, + /// Return current spot price of input in terms of output + #[returns(SpotPriceResponse)] + SpotPrice { offer: AssetInfo, ask: AssetInfo }, + /// Returns amount of tokens that can be exchanged such that sport remains <= target_price. + /// The last token of offer should return target_price of ask. + /// Returns None if price is already above expected. + #[returns(SpotPricePredictionResponse)] + SpotPricePrediction { + offer: AssetInfo, + ask: AssetInfo, + /// The maximum amount of offer to be sold + max_trade: Uint128, + /// The lowest spot price any offer token should be sold at + target_price: Decimal, + /// The maximum number of iterations used to bisect the space. + /// (higher numbers gives more accuracy at higher gas cost) + iterations: u8, + }, } #[cw_serde] @@ -368,10 +383,19 @@ pub struct StablePoolParams { pub amp: u64, /// The contract owner pub owner: Option, + /// Information on LSD, if supported (TODO: always require?) + pub lsd: Option, +} + +#[cw_serde] +pub struct LsdInfo { + /// Which asset is the LSD (and thus has the target_rate) + pub asset: AssetInfo, /// Address of the liquid staking hub contract for this pool. - /// If set, this is used to get the target value to concentrate liquidity around. - pub lsd_hub: Option, + /// This is used to get the target value to concentrate liquidity around. + pub hub: String, + /// The minimum amount of time in seconds between two target value queries pub target_rate_epoch: u64, } @@ -389,3 +413,16 @@ pub enum StablePoolUpdateParams { StartChangingAmp { next_amp: u64, next_amp_time: u64 }, StopChangingAmp {}, } + +/// This structure holds the parameters that are returned from a reverse swap simulation response. +#[cw_serde] +pub struct SpotPriceResponse { + pub price: Decimal, +} + +#[cw_serde] +pub struct SpotPricePredictionResponse { + /// Represents units to buy until spot price hits target (in query). + /// Returns None, result is already below the spot price + pub trade: Option, +} diff --git a/packages/wyndex/src/pair/error.rs b/packages/wyndex/src/pair/error.rs index 7fee2ca..39fe8ec 100644 --- a/packages/wyndex/src/pair/error.rs +++ b/packages/wyndex/src/pair/error.rs @@ -102,8 +102,18 @@ pub enum ContractError { "Invalid number of assets. Expected at least 2 and at most {max} assets, but got {provided}" )] TooManyAssets { max: usize, provided: usize }, + #[error("Contract has been frozen")] ContractFrozen {}, + + #[error("Spot price parameters incorrect - max_trade must be bigger then 0")] + SpotPriceInvalidMaxTrade {}, + + #[error("Spot price parameters incorrect - target_price must be bigger then 0")] + SpotPriceInvalidTargetPrice {}, + + #[error("Spot price parameters incorrect - iterations must be bigger then 0 and less or equal then 100")] + SpotPriceInvalidIterations {}, } impl From for StdError { diff --git a/utils/stable-price-solver/Cargo.toml b/utils/stable-price-solver/Cargo.toml new file mode 100644 index 0000000..2ec52ad --- /dev/null +++ b/utils/stable-price-solver/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "stable-price-solver" +description = "Finds the x-value corresponding to a price for the StableSwap price formula with a given A, D" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { version = "4.1.8", features = ["derive"] } +quickcheck = "1.0.3" diff --git a/utils/stable-price-solver/src/main.rs b/utils/stable-price-solver/src/main.rs new file mode 100644 index 0000000..076c9b3 --- /dev/null +++ b/utils/stable-price-solver/src/main.rs @@ -0,0 +1,126 @@ +use std::cmp::Ordering; + +use clap::Parser; + +/// The maximum x-value to search for +const MAX_X: f64 = 999999999999999999999999999f64; +/// The maximum difference between two values +const PRECISION: f64 = 0.0000000000000001f64; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// The `A` parameter of the stable swap formula (amplifier) + #[arg(short, long)] + amplifier: f64, + + /// The `D` parameter of the stable swap formula + #[arg(short, long, default_value_t = 1.0)] + d: f64, + + /// The target rate of the stable swap formula + #[arg(short, long, default_value_t = 1.0)] + target_rate: f64, + + /// The price to solve for + #[arg(short, long)] + price: f64, +} + +fn main() { + let args = Args::parse(); + + println!( + "{}", + find_x(args.amplifier, args.d, args.target_rate, args.price) + ); +} + +fn find_x(a: f64, d: f64, e: f64, price: f64) -> f64 { + binary_search(0.0, MAX_X, price, |x| stable_swap_price(a, d, e, x)) +} + +/// Calculate the formula of the first derivative of the stable swap formula, negated. +/// Semantically that means that the result is the price of the stable swap at the given x. +/// +/// p(x) = -(-2 E x sqrt(E x (4 A D^3 + E x (-4 A D + 4 A E x + D)^2)) - 8 A D E^2 x^2 + 8 A E^3 x^3 - D^3 + 2 D E^2 x^2)/(4 x sqrt(E x (4 A D^3 + E x (-4 A D + 4 A E x + D)^2))) +fn stable_swap_price(a: f64, d: f64, e: f64, x: f64) -> f64 { + assert!(a > 0.0); + assert!(d > 0.0); + assert!(e > 0.0); + assert!(x > 0.0); + -(-2f64 + * e + * x + * f64::sqrt( + e * x * (4f64 * a * d.powi(3) + e * x * (-4f64 * a * d + 4f64 * a * e * x + d).powi(2)), + ) + - 8f64 * a * d * (e * x).powi(2) + + 8f64 * a * (e * x).powi(3) + - d.powi(3) + + 2f64 * d * (e * x).powi(2)) + / (4f64 + * x + * f64::sqrt( + e * x + * (4f64 * a * d.powi(3) + + e * x * (-4f64 * a * d + 4f64 * a * e * x + d).powi(2)), + )) +} + +/// A function that does binary search with a minimum and maximum value +/// This assumes that `f` is monotonically *decreasing*. +fn binary_search(mut min: f64, mut max: f64, find: f64, f: impl Fn(f64) -> f64) -> f64 { + let mut half = max / 2.0 + min / 2.0; + + let mut last = f64::NAN; + let mut current = f(half); + + while min <= max { + let diff = (current - find).abs(); + if diff < PRECISION || last == current { + return half; + } + match current.partial_cmp(&find) { + Some(Ordering::Greater) => min = half, + Some(Ordering::Less) => max = half, + _ => return half, + } + half = max / 2.0 + min / 2.0; + last = current; + current = f(half); + } + half +} + +#[cfg(test)] +macro_rules! assert_f64_eq { + ($a: expr, $b: expr) => { + let a = $a; + let b = $b; + assert!((a - b).abs() < 0.00000000001, "{} != {}", a, b) + }; +} + +#[test] +fn price_is_one_at_middle() { + for a in [0.1, 1.0, 1.5, 2.0, 10.0, 100.0] { + for d in [0.1, 1.0, 1.5, 2.0, 10.0, 100.0] { + assert_f64_eq!(stable_swap_price(a, d, 1.0, d as f64 / 2.0), 1.0); + } + } +} + +#[test] +fn find_x_correct() { + for a in [1.0, 1.5, 2.0, 10.0, 100.0] { + for d in [1.0, 1.5, 2.0, 10.0, 100.0] { + for e in [1.0, 1.5, 2.0, 10.0, 100.0] { + for price in [0.1, 0.5, 0.9, 1.0, 1.1, 1.5, 1.9, 2.0] { + let x = find_x(a, d, e, price); + assert_f64_eq!(stable_swap_price(a, d, e, x), price); + } + } + } + } +}