diff --git a/.github/workflows/check_artifacts.yml b/.github/workflows/check_artifacts.yml index e00390bb8..c518fe5ec 100644 --- a/.github/workflows/check_artifacts.yml +++ b/.github/workflows/check_artifacts.yml @@ -68,7 +68,7 @@ jobs: fail-on-cache-miss: true - name: Install cosmwasm-check # Uses --debug for compilation speed - run: cargo install --debug --version 1.3.1 cosmwasm-check + run: cargo install --debug --version 1.4.0 cosmwasm-check - name: Cosmwasm check run: | cosmwasm-check $GITHUB_WORKSPACE/artifacts/*.wasm --available-capabilities staking,cosmwasm_1_1,injective,iterator,stargate diff --git a/Cargo.lock b/Cargo.lock index 935ba7354..5ffc9d261 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,16 +41,16 @@ dependencies = [ [[package]] name = "astro-satellite-package" version = "0.1.0" -source = "git+https://github.com/astroport-fi/astroport_ibc#f9f4def037d117275de31fffef36ddda388baf48" +source = "git+https://github.com/astroport-fi/astroport_ibc#ffb48ebfd7dbbc010cf86c9b02bad236c456fca0" dependencies = [ - "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/astroport-governance?branch=feat/merge_hidden_2023_05_22)", + "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/astroport-governance)", "cosmwasm-schema", "cosmwasm-std", ] [[package]] name = "astroport" -version = "3.3.2" +version = "3.6.0" dependencies = [ "astroport-circular-buffer", "cosmwasm-schema", @@ -60,7 +60,7 @@ dependencies = [ "cw20 0.15.1", "cw3", "injective-math", - "itertools", + "itertools 0.10.5", "test-case", "thiserror", "uint 0.9.5", @@ -91,16 +91,16 @@ dependencies = [ "cw20-ics20", "schemars", "semver", - "serde 1.0.181", + "serde 1.0.180", "thiserror", ] [[package]] name = "astroport-escrow-fee-distributor" version = "1.0.2" -source = "git+https://github.com/astroport-fi/astroport-governance#f0ef7c6dde76fc77ce360262923366a5cde3c3f8" +source = "git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main#784452baf414f13d8b9e7de461391eb765ff9043" dependencies = [ - "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/astroport-governance)", + "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main)", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -124,7 +124,7 @@ dependencies = [ "cw-utils 1.0.1", "cw2 0.15.1", "cw20 0.15.1", - "itertools", + "itertools 0.10.5", "prost 0.11.9", "protobuf", "thiserror", @@ -152,7 +152,7 @@ dependencies = [ "anyhow", "astroport", "astroport-factory", - "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/astroport-governance)", + "astroport-governance 1.4.0", "astroport-mocks", "astroport-native-coin-registry", "astroport-nft", @@ -164,7 +164,6 @@ dependencies = [ "astroport-whitelist", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test 0.15.1", "cw-storage-plus 0.15.1", "cw-utils 1.0.1", "cw1-whitelist", @@ -198,7 +197,7 @@ dependencies = [ [[package]] name = "astroport-governance" version = "1.2.0" -source = "git+https://github.com/astroport-fi/astroport-governance?branch=feat/merge_hidden_2023_05_22#1e865abe55093d249b69b538e2d54472b643d6c7" +source = "git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main#784452baf414f13d8b9e7de461391eb765ff9043" dependencies = [ "astroport", "cosmwasm-schema", @@ -219,9 +218,22 @@ dependencies = [ "cw20 0.15.1", ] +[[package]] +name = "astroport-governance" +version = "1.4.0" +source = "git+https://github.com/astroport-fi/hidden_astroport_governance#259fbc78d33f1b76e4213054babc95a1d9202f5c" +dependencies = [ + "astroport", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 0.15.1", + "cw20 0.15.1", + "thiserror", +] + [[package]] name = "astroport-liquidity-manager" -version = "1.0.1" +version = "1.0.2" dependencies = [ "anyhow", "astroport", @@ -239,7 +251,7 @@ dependencies = [ "cw20 0.15.1", "cw20-base 0.15.1", "derivative", - "itertools", + "itertools 0.10.5", "serde_json 1.0.104", "thiserror", ] @@ -252,7 +264,7 @@ dependencies = [ "astroport", "astroport-escrow-fee-distributor", "astroport-factory", - "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/astroport-governance)", + "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main)", "astroport-native-coin-registry", "astroport-pair", "astroport-pair-stable", @@ -268,8 +280,9 @@ dependencies = [ [[package]] name = "astroport-mocks" -version = "0.1.0" +version = "0.2.0" dependencies = [ + "anyhow", "astroport", "astroport-factory", "astroport-generator", @@ -278,6 +291,7 @@ dependencies = [ "astroport-pair-concentrated", "astroport-pair-concentrated-injective", "astroport-pair-stable", + "astroport-shared-multisig", "astroport-staking", "astroport-token", "astroport-vesting", @@ -286,9 +300,12 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test 0.16.5", + "cw-utils 1.0.1", + "cw20 0.15.1", + "cw3", "injective-cosmwasm", "schemars", - "serde 1.0.181", + "serde 1.0.180", ] [[package]] @@ -324,9 +341,9 @@ dependencies = [ [[package]] name = "astroport-nft" version = "1.0.0" -source = "git+https://github.com/astroport-fi/astroport-governance#f0ef7c6dde76fc77ce360262923366a5cde3c3f8" +source = "git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main#784452baf414f13d8b9e7de461391eb765ff9043" dependencies = [ - "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/astroport-governance)", + "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main)", "cosmwasm-schema", "cosmwasm-std", "cw2 0.15.1", @@ -351,13 +368,13 @@ dependencies = [ "cw-storage-plus 0.15.1", "cw2 0.15.1", "cw20 0.15.1", - "itertools", + "itertools 0.10.5", "thiserror", ] [[package]] name = "astroport-pair" -version = "1.4.0" +version = "1.5.0" dependencies = [ "astroport", "astroport-factory", @@ -423,7 +440,7 @@ dependencies = [ [[package]] name = "astroport-pair-concentrated" -version = "2.0.5" +version = "2.2.0" dependencies = [ "anyhow", "astroport", @@ -431,6 +448,7 @@ dependencies = [ "astroport-factory", "astroport-mocks", "astroport-native-coin-registry", + "astroport-pcl-common", "astroport-token", "cosmwasm-schema", "cosmwasm-std", @@ -439,14 +457,14 @@ dependencies = [ "cw2 0.15.1", "cw20 0.15.1", "derivative", - "itertools", + "itertools 0.10.5", "proptest", "thiserror", ] [[package]] name = "astroport-pair-concentrated-injective" -version = "2.0.5" +version = "2.2.0" dependencies = [ "anyhow", "astroport", @@ -455,6 +473,7 @@ dependencies = [ "astroport-mocks", "astroport-native-coin-registry", "astroport-pair-concentrated", + "astroport-pcl-common", "astroport-token", "cosmwasm-schema", "cosmwasm-std", @@ -467,7 +486,7 @@ dependencies = [ "injective-cosmwasm", "injective-math", "injective-testing", - "itertools", + "itertools 0.10.5", "proptest", "thiserror", "tiny-keccak 2.0.2", @@ -475,7 +494,7 @@ dependencies = [ [[package]] name = "astroport-pair-stable" -version = "3.1.1" +version = "3.3.0" dependencies = [ "anyhow", "astroport", @@ -491,13 +510,28 @@ dependencies = [ "cw2 0.15.1", "cw20 0.15.1", "derivative", - "itertools", + "itertools 0.10.5", "proptest", "prost 0.11.9", "sim", "thiserror", ] +[[package]] +name = "astroport-pcl-common" +version = "1.0.0" +dependencies = [ + "anyhow", + "astroport", + "astroport-factory", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.1.0", + "cw20 1.1.0", + "itertools 0.11.0", + "thiserror", +] + [[package]] name = "astroport-router" version = "1.1.2" @@ -519,16 +553,21 @@ dependencies = [ [[package]] name = "astroport-shared-multisig" -version = "0.1.0" +version = "1.0.0" dependencies = [ "astroport", + "astroport-generator", + "astroport-mocks", + "astroport-pair", + "astroport-pair-concentrated", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test 0.15.1", "cw-storage-plus 0.15.1", "cw-utils 1.0.1", "cw2 1.1.0", + "cw20 0.15.1", "cw3", + "itertools 0.10.5", "thiserror", ] @@ -718,7 +757,7 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" dependencies = [ - "serde 1.0.181", + "serde 1.0.180", ] [[package]] @@ -792,7 +831,7 @@ checksum = "63c337e097a089e5b52b5d914a7ff6613332777f38ea6d9d36e1887cd0baa72e" dependencies = [ "cosmwasm-schema-derive", "schemars", - "serde 1.0.181", + "serde 1.0.180", "serde_json 1.0.104", "thiserror", ] @@ -822,7 +861,7 @@ dependencies = [ "forward_ref", "hex", "schemars", - "serde 1.0.181", + "serde 1.0.180", "serde-json-wasm", "sha2 0.10.7", "thiserror", @@ -835,7 +874,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "800aaddd70ba915e19bf3d2d992aa3689d8767857727fdd3b414df4fd52d2aa1" dependencies = [ "cosmwasm-std", - "serde 1.0.181", + "serde 1.0.180", ] [[package]] @@ -904,7 +943,7 @@ dependencies = [ "cw-storage-plus 0.13.4", "cw-utils 0.13.4", "schemars", - "serde 1.0.181", + "serde 1.0.180", "thiserror", ] @@ -919,7 +958,7 @@ dependencies = [ "cw-storage-plus 1.1.0", "cw-utils 1.0.1", "schemars", - "serde 1.0.181", + "serde 1.0.180", "thiserror", ] @@ -935,10 +974,10 @@ dependencies = [ "cw-storage-plus 0.15.1", "cw-utils 0.15.1", "derivative", - "itertools", + "itertools 0.10.5", "prost 0.9.0", "schemars", - "serde 1.0.181", + "serde 1.0.180", "thiserror", ] @@ -952,11 +991,11 @@ dependencies = [ "cw-storage-plus 1.1.0", "cw-utils 1.0.1", "derivative", - "itertools", + "itertools 0.10.5", "k256", "prost 0.9.0", "schemars", - "serde 1.0.181", + "serde 1.0.180", "thiserror", ] @@ -968,7 +1007,7 @@ checksum = "7d7ee1963302b0ac2a9d42fe0faec826209c17452bfd36fbfd9d002a88929261" dependencies = [ "cosmwasm-std", "schemars", - "serde 1.0.181", + "serde 1.0.180", ] [[package]] @@ -979,7 +1018,7 @@ checksum = "648b1507290bbc03a8d88463d7cd9b04b1fa0155e5eef366c4fa052b9caaac7a" dependencies = [ "cosmwasm-std", "schemars", - "serde 1.0.181", + "serde 1.0.180", ] [[package]] @@ -990,7 +1029,7 @@ checksum = "dc6cf70ef7686e2da9ad7b067c5942cd3e88dd9453f7af42f54557f8af300fb0" dependencies = [ "cosmwasm-std", "schemars", - "serde 1.0.181", + "serde 1.0.180", ] [[package]] @@ -1001,7 +1040,7 @@ checksum = "3f0e92a069d62067f3472c62e30adedb4cab1754725c0f2a682b3128d2bf3c79" dependencies = [ "cosmwasm-std", "schemars", - "serde 1.0.181", + "serde 1.0.180", ] [[package]] @@ -1012,7 +1051,7 @@ checksum = "ef842a1792e4285beff7b3b518705f760fa4111dc1e296e53f3e92d1ef7f6220" dependencies = [ "cosmwasm-std", "schemars", - "serde 1.0.181", + "serde 1.0.180", "thiserror", ] @@ -1024,7 +1063,7 @@ checksum = "9dbaecb78c8e8abfd6b4258c7f4fbeb5c49a5e45ee4d910d3240ee8e1d714e1b" dependencies = [ "cosmwasm-std", "schemars", - "serde 1.0.181", + "serde 1.0.180", "thiserror", ] @@ -1039,7 +1078,7 @@ dependencies = [ "cw2 0.15.1", "schemars", "semver", - "serde 1.0.181", + "serde 1.0.180", "thiserror", ] @@ -1054,7 +1093,7 @@ dependencies = [ "cw2 1.1.0", "schemars", "semver", - "serde 1.0.181", + "serde 1.0.180", "thiserror", ] @@ -1067,7 +1106,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "schemars", - "serde 1.0.181", + "serde 1.0.180", ] [[package]] @@ -1083,7 +1122,7 @@ dependencies = [ "cw1", "cw2 0.15.1", "schemars", - "serde 1.0.181", + "serde 1.0.180", "thiserror", ] @@ -1096,7 +1135,7 @@ dependencies = [ "cosmwasm-std", "cw-storage-plus 0.11.1", "schemars", - "serde 1.0.181", + "serde 1.0.180", ] [[package]] @@ -1108,7 +1147,7 @@ dependencies = [ "cosmwasm-std", "cw-storage-plus 0.13.4", "schemars", - "serde 1.0.181", + "serde 1.0.180", ] [[package]] @@ -1121,7 +1160,7 @@ dependencies = [ "cosmwasm-std", "cw-storage-plus 0.15.1", "schemars", - "serde 1.0.181", + "serde 1.0.180", ] [[package]] @@ -1134,7 +1173,7 @@ dependencies = [ "cosmwasm-std", "cw-storage-plus 1.1.0", "schemars", - "serde 1.0.181", + "serde 1.0.180", "thiserror", ] @@ -1147,7 +1186,7 @@ dependencies = [ "cosmwasm-std", "cw-utils 0.11.1", "schemars", - "serde 1.0.181", + "serde 1.0.180", ] [[package]] @@ -1159,7 +1198,7 @@ dependencies = [ "cosmwasm-std", "cw-utils 0.13.4", "schemars", - "serde 1.0.181", + "serde 1.0.180", ] [[package]] @@ -1172,7 +1211,7 @@ dependencies = [ "cosmwasm-std", "cw-utils 0.15.1", "schemars", - "serde 1.0.181", + "serde 1.0.180", ] [[package]] @@ -1185,7 +1224,7 @@ dependencies = [ "cosmwasm-std", "cw-utils 1.0.1", "schemars", - "serde 1.0.181", + "serde 1.0.180", ] [[package]] @@ -1200,7 +1239,7 @@ dependencies = [ "cw2 0.11.1", "cw20 0.11.1", "schemars", - "serde 1.0.181", + "serde 1.0.180", "thiserror", ] @@ -1218,7 +1257,7 @@ dependencies = [ "cw20 0.15.1", "schemars", "semver", - "serde 1.0.181", + "serde 1.0.180", "thiserror", ] @@ -1236,7 +1275,7 @@ dependencies = [ "cw20 0.13.4", "schemars", "semver", - "serde 1.0.181", + "serde 1.0.180", "thiserror", ] @@ -1251,7 +1290,7 @@ dependencies = [ "cw-utils 1.0.1", "cw20 1.1.0", "schemars", - "serde 1.0.181", + "serde 1.0.180", "thiserror", ] @@ -1265,7 +1304,7 @@ dependencies = [ "cosmwasm-std", "cw-utils 0.15.1", "schemars", - "serde 1.0.181", + "serde 1.0.180", ] [[package]] @@ -1281,7 +1320,7 @@ dependencies = [ "cw2 0.15.1", "cw721", "schemars", - "serde 1.0.181", + "serde 1.0.180", "thiserror", ] @@ -1366,7 +1405,7 @@ dependencies = [ "hashbrown", "hex", "rand_core 0.6.4", - "serde 1.0.181", + "serde 1.0.180", "sha2 0.9.9", "zeroize", ] @@ -1441,7 +1480,7 @@ dependencies = [ "ethbloom", "ethereum-types-serialize", "fixed-hash", - "serde 1.0.181", + "serde 1.0.180", "uint 0.5.0", ] @@ -1451,7 +1490,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1873d77b32bc1891a79dad925f2acbc318ee942b38b9110f9dbc5fbeffcea350" dependencies = [ - "serde 1.0.181", + "serde 1.0.180", ] [[package]] @@ -1519,15 +1558,15 @@ checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" [[package]] name = "generator-controller" version = "1.3.0" -source = "git+https://github.com/astroport-fi/astroport-governance#f0ef7c6dde76fc77ce360262923366a5cde3c3f8" +source = "git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main#784452baf414f13d8b9e7de461391eb765ff9043" dependencies = [ - "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/astroport-governance)", + "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main)", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", "cw2 0.15.1", "cw20 0.15.1", - "itertools", + "itertools 0.10.5", "thiserror", ] @@ -1603,7 +1642,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" dependencies = [ - "serde 1.0.181", + "serde 1.0.180", ] [[package]] @@ -1630,7 +1669,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58e3cae7e99c7ff5a995da2cf78dd0a5383740eda71d98cf7b1910c301ac69b8" dependencies = [ - "serde 1.0.181", + "serde 1.0.180", ] [[package]] @@ -1641,9 +1680,9 @@ checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" [[package]] name = "injective-cosmwasm" -version = "0.2.12" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4abab15a0d19bad7146bb519280f3dbded7cb66981da92b6d3452370eb2cd654" +checksum = "4295a2d118cae0e21bba1c856464f6678b5db907cb085b3723d04efb65fa0d0d" dependencies = [ "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -1651,7 +1690,7 @@ dependencies = [ "hex", "injective-math", "schemars", - "serde 1.0.181", + "serde 1.0.180", "serde_repr", "subtle-encoding", "tiny-keccak 1.5.0", @@ -1668,7 +1707,7 @@ dependencies = [ "ethereum-types", "num 0.4.1", "schemars", - "serde 1.0.181", + "serde 1.0.180", "subtle-encoding", ] @@ -1686,7 +1725,7 @@ dependencies = [ "injective-math", "rand 0.4.6", "secp256k1", - "serde 1.0.181", + "serde 1.0.180", "tiny-keccak 1.5.0", ] @@ -1708,6 +1747,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -2054,7 +2102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "syn 1.0.109", @@ -2067,7 +2115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "syn 1.0.109", @@ -2357,7 +2405,7 @@ checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f" dependencies = [ "dyn-clone", "schemars_derive", - "serde 1.0.181", + "serde 1.0.180", "serde_json 1.0.104", ] @@ -2425,9 +2473,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.181" +version = "1.0.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d3e73c93c3240c0bda063c239298e633114c69a888c3e37ca8bb33f343e9890" +checksum = "0ea67f183f058fe88a4e3ec6e2788e003840893b91bac4559cabedd00863b3ed" dependencies = [ "serde_derive", ] @@ -2438,7 +2486,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16a62a1fad1e1828b24acac8f2b468971dade7b8c3c2e672bcadefefb1f8c137" dependencies = [ - "serde 1.0.181", + "serde 1.0.180", ] [[package]] @@ -2447,14 +2495,14 @@ version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff" dependencies = [ - "serde 1.0.181", + "serde 1.0.180", ] [[package]] name = "serde_derive" -version = "1.0.181" +version = "1.0.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be02f6cb0cd3a5ec20bbcfbcbd749f57daddb1a0882dc2e46a6c236c90b977ed" +checksum = "24e744d7782b686ab3b73267ef05697159cc0e5abbed3f47f9933165e5219036" dependencies = [ "proc-macro2", "quote", @@ -2490,7 +2538,7 @@ checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" dependencies = [ "itoa", "ryu", - "serde 1.0.181", + "serde 1.0.180", ] [[package]] @@ -2663,7 +2711,7 @@ dependencies = [ "num-traits", "prost 0.11.9", "prost-types", - "serde 1.0.181", + "serde 1.0.180", "serde_bytes", "subtle-encoding", "time", @@ -2671,36 +2719,36 @@ dependencies = [ [[package]] name = "test-case" -version = "3.1.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a1d6e7bde536b0412f20765b76e921028059adfd1b90d8974d33fd3c91b25df" +checksum = "c8f1e820b7f1d95a0cdbf97a5df9de10e1be731983ab943e56703ac1b8e9d425" dependencies = [ "test-case-macros", ] [[package]] name = "test-case-core" -version = "3.1.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d10394d5d1e27794f772b6fc854c7e91a2dc26e2cbf807ad523370c2a59c0cee" +checksum = "54c25e2cb8f5fcd7318157634e8838aa6f7e4715c96637f969fabaccd1ef5462" dependencies = [ "cfg-if", "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.28", ] [[package]] name = "test-case-macros" -version = "3.1.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeb9a44b1c6a54c1ba58b152797739dba2a83ca74e18168a68c980eb142f9404" +checksum = "37cfd7bbc88a0104e304229fba519bdc45501a30b760fb72240342f1289ad257" dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.28", "test-case-core", ] @@ -2827,7 +2875,7 @@ dependencies = [ "cw-storage-plus 0.11.1", "cw20 0.11.1", "schemars", - "serde 1.0.181", + "serde 1.0.180", "thiserror", ] @@ -2840,7 +2888,7 @@ dependencies = [ "cw-storage-plus 0.11.1", "cw20 0.11.1", "schemars", - "serde 1.0.181", + "serde 1.0.180", "valkyrie", ] @@ -2855,7 +2903,7 @@ dependencies = [ "cw20 0.11.1", "cw20-base 0.11.1", "schemars", - "serde 1.0.181", + "serde 1.0.180", "thiserror", ] @@ -2868,9 +2916,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "voting-escrow" version = "1.3.0" -source = "git+https://github.com/astroport-fi/astroport-governance#f0ef7c6dde76fc77ce360262923366a5cde3c3f8" +source = "git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main#784452baf414f13d8b9e7de461391eb765ff9043" dependencies = [ - "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/astroport-governance)", + "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main)", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -2883,9 +2931,9 @@ dependencies = [ [[package]] name = "voting-escrow-delegation" version = "1.0.0" -source = "git+https://github.com/astroport-fi/astroport-governance#f0ef7c6dde76fc77ce360262923366a5cde3c3f8" +source = "git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main#784452baf414f13d8b9e7de461391eb765ff9043" dependencies = [ - "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/astroport-governance)", + "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main)", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", diff --git a/contracts/cw20_ics20/Cargo.toml b/contracts/cw20_ics20/Cargo.toml index 2004476f5..4c129dd8b 100644 --- a/contracts/cw20_ics20/Cargo.toml +++ b/contracts/cw20_ics20/Cargo.toml @@ -30,7 +30,7 @@ semver = "1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } thiserror = { version = "1.0.23" } -astroport = { path = "../../packages/astroport" } +astroport = { path = "../../packages/astroport", version = "3" } [dev-dependencies] cw20-ics20-original = { version = "0.13.4", package = "cw20-ics20" } diff --git a/contracts/cw20_ics20/src/contract.rs b/contracts/cw20_ics20/src/contract.rs index b279b1ac6..370e07962 100644 --- a/contracts/cw20_ics20/src/contract.rs +++ b/contracts/cw20_ics20/src/contract.rs @@ -1,4 +1,5 @@ use astroport::asset::addr_opt_validate; +use astroport::cw20_ics20::TransferMsg; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ @@ -17,7 +18,7 @@ use crate::ibc::Ics20Packet; use crate::migrations::standard_v1; use crate::msg::{ AllowMsg, AllowedInfo, AllowedResponse, ChannelResponse, ConfigResponse, ExecuteMsg, InitMsg, - ListAllowedResponse, ListChannelsResponse, MigrateMsg, PortResponse, QueryMsg, TransferMsg, + ListAllowedResponse, ListChannelsResponse, MigrateMsg, PortResponse, QueryMsg, }; use crate::state::{ increase_channel_balance, AllowInfo, Config, ADMIN, ALLOW_LIST, CHANNEL_INFO, CHANNEL_STATE, diff --git a/contracts/cw20_ics20/src/ibc.rs b/contracts/cw20_ics20/src/ibc.rs index 706ffa528..ab0e52510 100644 --- a/contracts/cw20_ics20/src/ibc.rs +++ b/contracts/cw20_ics20/src/ibc.rs @@ -492,9 +492,10 @@ mod test { use crate::test_helpers::*; use crate::contract::{execute, migrate, query_channel}; - use crate::msg::{ExecuteMsg, MigrateMsg, TransferMsg}; + use crate::msg::{ExecuteMsg, MigrateMsg}; + use astroport::cw20_ics20::TransferMsg; use cosmwasm_std::testing::{mock_env, mock_info}; - use cosmwasm_std::{coins, to_vec, IbcEndpoint, IbcMsg, IbcTimeout, Timestamp}; + use cosmwasm_std::{coins, to_vec, Addr, IbcEndpoint, IbcMsg, IbcTimeout, Timestamp}; use cw20::Cw20ReceiveMsg; #[test] @@ -585,6 +586,10 @@ mod test { ) } + fn mock_relayer_addr() -> Addr { + Addr::unchecked("relayer") + } + #[test] fn send_receive_cw20() { let send_channel = "channel-9"; @@ -603,7 +608,7 @@ mod test { mock_receive_packet(send_channel, 1876543210, cw20_denom, "local-rcpt", None); // cannot receive this denom yet - let msg = IbcPacketReceiveMsg::new(recv_packet.clone()); + let msg = IbcPacketReceiveMsg::new(recv_packet.clone(), mock_relayer_addr()); let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); assert!(res.messages.is_empty()); let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); @@ -648,14 +653,14 @@ mod test { assert_eq!(state.total_sent, vec![Amount::cw20(987654321, cw20_addr)]); // cannot receive more than we sent - let msg = IbcPacketReceiveMsg::new(recv_high_packet); + let msg = IbcPacketReceiveMsg::new(recv_high_packet, mock_relayer_addr()); let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); assert!(res.messages.is_empty()); let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); assert_eq!(ack, no_funds); // we can receive less than we sent - let msg = IbcPacketReceiveMsg::new(recv_packet); + let msg = IbcPacketReceiveMsg::new(recv_packet, mock_relayer_addr()); let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); assert_eq!(1, res.messages.len()); assert_eq!( @@ -698,7 +703,7 @@ mod test { mock_receive_packet(send_channel, 1876543210, cw20_denom, "local-rcpt", None); // cannot receive this denom yet - let msg = IbcPacketReceiveMsg::new(recv_packet); + let msg = IbcPacketReceiveMsg::new(recv_packet, mock_relayer_addr()); let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); assert!(res.messages.is_empty()); let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); @@ -743,14 +748,14 @@ mod test { assert_eq!(state.total_sent, vec![Amount::cw20(987654321, cw20_addr)]); // cannot receive more than we sent - let msg = IbcPacketReceiveMsg::new(recv_high_packet); + let msg = IbcPacketReceiveMsg::new(recv_high_packet, mock_relayer_addr()); let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); assert!(res.messages.is_empty()); let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); assert_eq!(ack, no_funds); // We can receive less than we sent, but if a memo is set without a handler, we fail - let msg = IbcPacketReceiveMsg::new(recv_packet_with_memo); + let msg = IbcPacketReceiveMsg::new(recv_packet_with_memo, mock_relayer_addr()); let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); // No messages should be sent @@ -779,7 +784,7 @@ mod test { mock_receive_packet(send_channel, 1876543210, denom, "local-rcpt", None); // cannot receive this denom yet - let msg = IbcPacketReceiveMsg::new(recv_packet.clone()); + let msg = IbcPacketReceiveMsg::new(recv_packet.clone(), mock_relayer_addr()); let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); assert!(res.messages.is_empty()); let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); @@ -802,14 +807,14 @@ mod test { assert_eq!(state.total_sent, vec![Amount::native(987654321, denom)]); // cannot receive more than we sent - let msg = IbcPacketReceiveMsg::new(recv_high_packet); + let msg = IbcPacketReceiveMsg::new(recv_high_packet, mock_relayer_addr()); let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); assert!(res.messages.is_empty()); let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); assert_eq!(ack, no_funds); // we can receive less than we sent - let msg = IbcPacketReceiveMsg::new(recv_packet); + let msg = IbcPacketReceiveMsg::new(recv_packet, mock_relayer_addr()); let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); assert_eq!(1, res.messages.len()); assert_eq!( diff --git a/contracts/cw20_ics20/src/msg.rs b/contracts/cw20_ics20/src/msg.rs index 87c54959f..68cd27780 100644 --- a/contracts/cw20_ics20/src/msg.rs +++ b/contracts/cw20_ics20/src/msg.rs @@ -1,3 +1,4 @@ +use astroport::cw20_ics20::TransferMsg; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::Addr; use cw20::Cw20ReceiveMsg; @@ -46,21 +47,6 @@ pub enum ExecuteMsg { UpdateHookAddress { new_address: String }, } -/// This is the message we accept via Receive -#[cw_serde] -pub struct TransferMsg { - /// The local channel to send the packets on - pub channel: String, - /// The remote address to send to. - /// Don't use HumanAddress as this will likely have a different Bech32 prefix than we use - /// and cannot be validated locally - pub remote_address: String, - /// How long the packet lives in seconds. If not specified, use default_timeout - pub timeout: Option, - /// An optional memo to add to the IBC transfer - pub memo: Option, -} - #[cw_serde] #[derive(QueryResponses)] pub enum QueryMsg { diff --git a/contracts/factory/Cargo.toml b/contracts/factory/Cargo.toml index 4320f8e7b..319c09fe9 100644 --- a/contracts/factory/Cargo.toml +++ b/contracts/factory/Cargo.toml @@ -25,7 +25,7 @@ library = [] [dependencies] cosmwasm-std = "1.1" -astroport = { path = "../../packages/astroport", default-features = false } +astroport = { path = "../../packages/astroport", version = "3" } cw-storage-plus = "0.15" cw2 = "0.15" thiserror = "1.0" diff --git a/contracts/pair/Cargo.toml b/contracts/pair/Cargo.toml index 1ae846818..aa93ce3a1 100644 --- a/contracts/pair/Cargo.toml +++ b/contracts/pair/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-pair" -version = "1.4.0" +version = "1.5.0" authors = ["Astroport"] edition = "2021" description = "The Astroport constant product pool contract implementation" @@ -25,7 +25,7 @@ library = [] [dependencies] integer-sqrt = "0.1" -astroport = { path = "../../packages/astroport", default-features = false } +astroport = { path = "../../packages/astroport", version = "3" } cw2 = "0.15" cw20 = "0.15" cosmwasm-std = "1.1" diff --git a/contracts/pair/src/contract.rs b/contracts/pair/src/contract.rs index e0270612f..8bf979bbd 100644 --- a/contracts/pair/src/contract.rs +++ b/contracts/pair/src/contract.rs @@ -19,8 +19,8 @@ use astroport::asset::{ use astroport::factory::PairType; use astroport::generator::Cw20HookMsg as GeneratorHookMsg; use astroport::pair::{ - ConfigResponse, XYKPoolConfig, XYKPoolParams, XYKPoolUpdateParams, DEFAULT_SLIPPAGE, - MAX_ALLOWED_SLIPPAGE, + ConfigResponse, FeeShareConfig, XYKPoolConfig, XYKPoolParams, XYKPoolUpdateParams, + DEFAULT_SLIPPAGE, MAX_ALLOWED_SLIPPAGE, MAX_FEE_SHARE_BPS, }; use astroport::pair::{ CumulativePricesResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, MigrateMsg, PoolResponse, @@ -80,6 +80,7 @@ pub fn instantiate( price0_cumulative_last: Uint128::zero(), price1_cumulative_last: Uint128::zero(), track_asset_balances, + fee_share: None, }; if track_asset_balances { @@ -691,12 +692,39 @@ pub fn swap( messages.push(return_asset.into_msg(receiver.clone())?) } + // If this pool is configured to share fees, calculate the amount to send + // to the receiver and add the transfer message + // The calculation works as follows: We take the share percentage first, + // and the remainder is then split between LPs and maker + let mut fees_commission_amount = commission_amount; + let mut fee_share_amount = Uint128::zero(); + if let Some(fee_share) = config.fee_share.clone() { + // Calculate the fee share amount from the full commission amount + let share_fee_rate = Decimal::from_ratio(fee_share.bps, 10000u16); + fee_share_amount = fees_commission_amount * share_fee_rate; + + if !fee_share_amount.is_zero() { + // Subtract the fee share amount from the commission + fees_commission_amount = fees_commission_amount.saturating_sub(fee_share_amount); + + // Build send message for the shared amount + let fee_share_msg = Asset { + info: ask_pool.info.clone(), + amount: fee_share_amount, + } + .into_msg(fee_share.recipient)?; + messages.push(fee_share_msg); + } + } + // Compute the Maker fee let mut maker_fee_amount = Uint128::zero(); if let Some(fee_address) = fee_info.fee_address { - if let Some(f) = - calculate_maker_fee(&ask_pool.info, commission_amount, fee_info.maker_fee_rate) - { + if let Some(f) = calculate_maker_fee( + &ask_pool.info, + fees_commission_amount, + fee_info.maker_fee_rate, + ) { maker_fee_amount = f.amount; messages.push(f.into_msg(fee_address)?); } @@ -712,7 +740,7 @@ pub fn swap( BALANCES.save( deps.storage, &ask_pool.info, - &(ask_pool.amount - return_amount - maker_fee_amount), + &(ask_pool.amount - return_amount - maker_fee_amount - fee_share_amount), env.block.height, )?; } @@ -744,6 +772,7 @@ pub fn swap( attr("spread_amount", spread_amount), attr("commission_amount", commission_amount), attr("maker_fee_amount", maker_fee_amount), + attr("fee_share_amount", fee_share_amount), ])) } @@ -787,6 +816,44 @@ pub fn update_config( "enabled".to_owned(), )); } + XYKPoolUpdateParams::EnableFeeShare { + fee_share_bps, + fee_share_address, + } => { + // Enable fee sharing for this contract + // If fee sharing is already enabled, we should be able to overwrite + // the values currently set + + // Ensure the fee share isn't 0 and doesn't exceed the maximum allowed value + if fee_share_bps == 0 || fee_share_bps > MAX_FEE_SHARE_BPS { + return Err(ContractError::FeeShareOutOfBounds {}); + } + + // Set sharing config + config.fee_share = Some(FeeShareConfig { + bps: fee_share_bps, + recipient: deps.api.addr_validate(&fee_share_address)?, + }); + + CONFIG.save(deps.storage, &config)?; + + response.attributes.push(attr("action", "enable_fee_share")); + response + .attributes + .push(attr("fee_share_bps", fee_share_bps.to_string())); + response + .attributes + .push(attr("fee_share_address", fee_share_address)); + } + XYKPoolUpdateParams::DisableFeeShare => { + // Disable fee sharing for this contract by setting bps and + // address back to None + config.fee_share = None; + CONFIG.save(deps.storage, &config)?; + response + .attributes + .push(attr("action", "disable_fee_share")); + } } Ok(response) @@ -1069,6 +1136,7 @@ pub fn query_config(deps: Deps) -> StdResult { block_time_last: config.block_time_last, params: Some(to_binary(&XYKPoolConfig { track_asset_balances: config.track_asset_balances, + fee_share: config.fee_share, })?), owner: factory_config.owner, factory_addr: config.factory_addr, diff --git a/contracts/pair/src/error.rs b/contracts/pair/src/error.rs index a24c42ba4..49346ba79 100644 --- a/contracts/pair/src/error.rs +++ b/contracts/pair/src/error.rs @@ -1,4 +1,4 @@ -use astroport::asset::MINIMUM_LIQUIDITY_AMOUNT; +use astroport::{asset::MINIMUM_LIQUIDITY_AMOUNT, pair::MAX_FEE_SHARE_BPS}; use cosmwasm_std::{OverflowError, StdError}; use thiserror::Error; @@ -52,6 +52,12 @@ pub enum ContractError { #[error("Failed to parse or process reply message")] FailedToParseReply {}, + + #[error( + "Fee share is 0 or exceeds maximum allowed value of {} bps", + MAX_FEE_SHARE_BPS + )] + FeeShareOutOfBounds {}, } impl From for ContractError { diff --git a/contracts/pair/src/migration.rs b/contracts/pair/src/migration.rs index e286450fd..a002ed834 100644 --- a/contracts/pair/src/migration.rs +++ b/contracts/pair/src/migration.rs @@ -38,6 +38,7 @@ pub(crate) fn add_asset_balances_tracking_flag( price0_cumulative_last: old_config.price0_cumulative_last, price1_cumulative_last: old_config.price1_cumulative_last, track_asset_balances: false, + fee_share: None, }; CONFIG.save(storage, &new_config)?; diff --git a/contracts/pair/src/state.rs b/contracts/pair/src/state.rs index d4008a858..74b6de0d0 100644 --- a/contracts/pair/src/state.rs +++ b/contracts/pair/src/state.rs @@ -1,4 +1,7 @@ -use astroport::asset::{AssetInfo, PairInfo}; +use astroport::{ + asset::{AssetInfo, PairInfo}, + pair::FeeShareConfig, +}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Uint128}; use cw_storage_plus::{Item, SnapshotMap}; @@ -18,6 +21,8 @@ pub struct Config { pub price1_cumulative_last: Uint128, /// Whether asset balances are tracked over blocks or not. pub track_asset_balances: bool, + // The config for swap fee sharing + pub fee_share: Option, } /// Stores the config struct at the given key diff --git a/contracts/pair/src/testing.rs b/contracts/pair/src/testing.rs index 281cd7b9b..37ff5de9d 100644 --- a/contracts/pair/src/testing.rs +++ b/contracts/pair/src/testing.rs @@ -931,6 +931,7 @@ fn try_native_to_token() { attr("spread_amount", expected_spread_amount.to_string()), attr("commission_amount", expected_commission_amount.to_string()), attr("maker_fee_amount", expected_maker_fee_amount.to_string()), + attr("fee_share_amount", "0"), ] ); @@ -1121,6 +1122,7 @@ fn try_token_to_native() { attr("spread_amount", expected_spread_amount.to_string()), attr("commission_amount", expected_commission_amount.to_string()), attr("maker_fee_amount", expected_maker_fee_amount.to_string()), + attr("fee_share_amount", "0"), ] ); @@ -1418,6 +1420,7 @@ fn test_accumulate_prices() { price0_cumulative_last: Uint128::new(case.last0), price1_cumulative_last: Uint128::new(case.last1), track_asset_balances: false, + fee_share: None, }, Uint128::new(case.x_amount), Uint128::new(case.y_amount), diff --git a/contracts/pair/tests/integration.rs b/contracts/pair/tests/integration.rs index 61980944d..a7851199b 100644 --- a/contracts/pair/tests/integration.rs +++ b/contracts/pair/tests/integration.rs @@ -9,14 +9,15 @@ use astroport::factory::{ QueryMsg as FactoryQueryMsg, }; use astroport::pair::{ - ConfigResponse, CumulativePricesResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, QueryMsg, - XYKPoolConfig, XYKPoolParams, XYKPoolUpdateParams, TWAP_PRECISION, + ConfigResponse, CumulativePricesResponse, Cw20HookMsg, ExecuteMsg, FeeShareConfig, + InstantiateMsg, PoolResponse, QueryMsg, XYKPoolConfig, XYKPoolParams, XYKPoolUpdateParams, + MAX_FEE_SHARE_BPS, TWAP_PRECISION, }; use astroport::token::InstantiateMsg as TokenInstantiateMsg; use astroport_mocks::cw_multi_test::{App, BasicApp, ContractWrapper, Executor}; use astroport_mocks::{astroport_address, MockGeneratorBuilder, MockXykPairBuilder}; use astroport_pair::error::ContractError; -use cosmwasm_std::{attr, to_binary, Addr, Coin, Decimal, Uint128}; +use cosmwasm_std::{attr, to_binary, Addr, Coin, Decimal, Uint128, Uint64}; use cw20::{BalanceResponse, Cw20Coin, Cw20ExecuteMsg, Cw20QueryMsg, MinterResponse}; const OWNER: &str = "owner"; @@ -339,7 +340,8 @@ fn test_provide_and_withdraw_liquidity() { block_time_last: router.block_info().time.seconds(), params: Some( to_binary(&XYKPoolConfig { - track_asset_balances: false + track_asset_balances: false, + fee_share: None, }) .unwrap() ), @@ -1453,7 +1455,8 @@ fn update_pair_config() { block_time_last: 0, params: Some( to_binary(&XYKPoolConfig { - track_asset_balances: false + track_asset_balances: false, + fee_share: None, }) .unwrap() ), @@ -1493,7 +1496,199 @@ fn update_pair_config() { block_time_last: 0, params: Some( to_binary(&XYKPoolConfig { - track_asset_balances: true + track_asset_balances: true, + fee_share: None, + }) + .unwrap() + ), + owner: Addr::unchecked("owner"), + factory_addr: Addr::unchecked("contract0") + } + ); +} + +#[test] +fn enable_disable_fee_sharing() { + let owner = Addr::unchecked(OWNER); + let mut router = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ], + ); + + let token_contract_code_id = store_token_code(&mut router); + let pair_contract_code_id = store_pair_code(&mut router); + + let factory_code_id = store_factory_code(&mut router); + + let init_msg = FactoryInstantiateMsg { + fee_address: None, + pair_configs: vec![], + token_code_id: token_contract_code_id, + generator_address: Some(String::from("generator")), + owner: owner.to_string(), + whitelist_code_id: 234u64, + coin_registry_address: "coin_registry".to_string(), + }; + + let factory_instance = router + .instantiate_contract( + factory_code_id, + owner.clone(), + &init_msg, + &[], + "FACTORY", + None, + ) + .unwrap(); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ], + token_code_id: token_contract_code_id, + factory_addr: factory_instance.to_string(), + init_params: None, + }; + + let pair = router + .instantiate_contract( + pair_contract_code_id, + owner.clone(), + &msg, + &[], + String::from("PAIR"), + None, + ) + .unwrap(); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Config {}) + .unwrap(); + + assert_eq!( + res, + ConfigResponse { + block_time_last: 0, + params: Some( + to_binary(&XYKPoolConfig { + track_asset_balances: false, + fee_share: None, + }) + .unwrap() + ), + owner: Addr::unchecked("owner"), + factory_addr: Addr::unchecked("contract0") + } + ); + + // Attemt to set fee sharing higher than maximum + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&XYKPoolUpdateParams::EnableFeeShare { + fee_share_bps: MAX_FEE_SHARE_BPS + 1, + fee_share_address: "contract".to_string(), + }) + .unwrap(), + }; + assert_eq!( + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap_err() + .downcast_ref::() + .unwrap(), + &ContractError::FeeShareOutOfBounds {} + ); + + // Attemt to set fee sharing to 0 + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&XYKPoolUpdateParams::EnableFeeShare { + fee_share_bps: 0, + fee_share_address: "contract".to_string(), + }) + .unwrap(), + }; + assert_eq!( + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap_err() + .downcast_ref::() + .unwrap(), + &ContractError::FeeShareOutOfBounds {} + ); + + let fee_share_bps = 500; // 5% + let fee_share_contract = "contract".to_string(); + + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&XYKPoolUpdateParams::EnableFeeShare { + fee_share_bps, + fee_share_address: fee_share_contract.clone(), + }) + .unwrap(), + }; + + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap(); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Config {}) + .unwrap(); + assert_eq!( + res, + ConfigResponse { + block_time_last: 0, + params: Some( + to_binary(&XYKPoolConfig { + track_asset_balances: false, + fee_share: Some(FeeShareConfig { + bps: fee_share_bps, + recipient: Addr::unchecked(fee_share_contract), + }), + }) + .unwrap() + ), + owner: Addr::unchecked("owner"), + factory_addr: Addr::unchecked("contract0") + } + ); + + // Disable fee sharing + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&XYKPoolUpdateParams::DisableFeeShare).unwrap(), + }; + + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap(); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Config {}) + .unwrap(); + assert_eq!( + res, + ConfigResponse { + block_time_last: 0, + params: Some( + to_binary(&XYKPoolConfig { + track_asset_balances: false, + fee_share: None, }) .unwrap() ), @@ -1651,3 +1846,336 @@ fn test_imbalanced_withdraw_is_disabled() { "Generic error: Imbalanced withdraw is currently disabled" ); } + +#[test] +fn check_correct_fee_share() { + // Validate the resulting values + // We swapped 1_000000 of token X + // Fee is set to 0.3% of the swap amount resulting in 1000000 * 0.003 = 3000 + // User receives with 1000000 - 3000 = 997000 + // Of the 3000 fee, 10% is sent to the fee sharing contract resulting in 300 + // Of the 2700 fee left, 33.33% is sent to the maker resulting in 899 + // Of the 1801 fee left, all of it is left in the pool + + // Test with 10% fee share, 0.3% total fee and 33.33% maker fee + test_fee_share( + 3333u16, + 30u16, + 1000u16, + Uint128::from(300u64), + Uint128::from(899u64), + ); + + // Test with 5% fee share, 0.3% total fee and 50% maker fee + test_fee_share( + 5000u16, + 30u16, + 500u16, + Uint128::from(150u64), + Uint128::from(1425u64), + ); + + // Test with 5% fee share, 0.1% total fee and 33.33% maker fee + test_fee_share( + 3333u16, + 10u16, + 500u16, + Uint128::from(50u64), + Uint128::from(316u64), + ); +} + +fn test_fee_share( + maker_fee_bps: u16, + total_fee_bps: u16, + fee_share_bps: u16, + expected_fee_share: Uint128, + expected_maker_fee: Uint128, +) { + let owner = Addr::unchecked(OWNER); + + let mut app = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000000u128), + }, + ], + ); + + let token_code_id = store_token_code(&mut app); + + let x_amount = Uint128::new(1_000_000_000000); + let y_amount = Uint128::new(1_000_000_000000); + let x_offer = Uint128::new(1_000000); + let maker_address = "maker"; + + let token_name = "Xtoken"; + + let init_msg = TokenInstantiateMsg { + name: token_name.to_string(), + symbol: token_name.to_string(), + decimals: 6, + initial_balances: vec![Cw20Coin { + address: OWNER.to_string(), + amount: x_amount + x_offer, + }], + mint: Some(MinterResponse { + minter: String::from(OWNER), + cap: None, + }), + marketing: None, + }; + + let token_x_instance = app + .instantiate_contract( + token_code_id, + owner.clone(), + &init_msg, + &[], + token_name, + None, + ) + .unwrap(); + + let token_name = "Ytoken"; + + let init_msg = TokenInstantiateMsg { + name: token_name.to_string(), + symbol: token_name.to_string(), + decimals: 6, + initial_balances: vec![Cw20Coin { + address: OWNER.to_string(), + amount: y_amount, + }], + mint: Some(MinterResponse { + minter: String::from(OWNER), + cap: None, + }), + marketing: None, + }; + + let token_y_instance = app + .instantiate_contract( + token_code_id, + owner.clone(), + &init_msg, + &[], + token_name, + None, + ) + .unwrap(); + + let pair_code_id = store_pair_code(&mut app); + let factory_code_id = store_factory_code(&mut app); + + let init_msg = FactoryInstantiateMsg { + fee_address: Some(maker_address.to_string()), + pair_configs: vec![PairConfig { + code_id: pair_code_id, + maker_fee_bps, + pair_type: PairType::Xyk {}, + total_fee_bps, + is_disabled: false, + is_generator_disabled: false, + }], + token_code_id, + generator_address: Some(String::from("generator")), + owner: owner.to_string(), + whitelist_code_id: 234u64, + coin_registry_address: "coin_registry".to_string(), + }; + + let factory_instance = app + .instantiate_contract( + factory_code_id, + owner.clone(), + &init_msg, + &[], + "FACTORY", + None, + ) + .unwrap(); + + let msg = FactoryExecuteMsg::CreatePair { + asset_infos: vec![ + AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + ], + pair_type: PairType::Xyk {}, + init_params: Some( + to_binary(&XYKPoolParams { + track_asset_balances: Some(true), + }) + .unwrap(), + ), + }; + + app.execute_contract(owner.clone(), factory_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = FactoryQueryMsg::Pair { + asset_infos: vec![ + AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + ], + }; + + let res: PairInfo = app + .wrap() + .query_wasm_smart(&factory_instance, &msg) + .unwrap(); + + let pair_instance = res.contract_addr; + + let msg = Cw20ExecuteMsg::IncreaseAllowance { + spender: pair_instance.to_string(), + expires: None, + amount: x_amount + x_offer, + }; + + app.execute_contract(owner.clone(), token_x_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = Cw20ExecuteMsg::IncreaseAllowance { + spender: pair_instance.to_string(), + expires: None, + amount: y_amount, + }; + + app.execute_contract(owner.clone(), token_y_instance.clone(), &msg, &[]) + .unwrap(); + + let user = Addr::unchecked("user"); + + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + amount: x_amount, + }, + Asset { + info: AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + amount: y_amount, + }, + ], + slippage_tolerance: None, + auto_stake: None, + receiver: None, + }; + + app.execute_contract(owner.clone(), pair_instance.clone(), &msg, &[]) + .unwrap(); + + let fee_share_address = "contract_receiver".to_string(); + + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&XYKPoolUpdateParams::EnableFeeShare { + fee_share_bps, + fee_share_address: fee_share_address.clone(), + }) + .unwrap(), + }; + + app.execute_contract(owner.clone(), pair_instance.clone(), &msg, &[]) + .unwrap(); + + let swap_msg = Cw20ExecuteMsg::Send { + contract: pair_instance.to_string(), + msg: to_binary(&Cw20HookMsg::Swap { + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: Some(user.to_string()), + }) + .unwrap(), + amount: x_offer, + }; + + // try to swap after provide liquidity + app.execute_contract(owner.clone(), token_x_instance.clone(), &swap_msg, &[]) + .unwrap(); + + let y_expected_return = + x_offer - Uint128::from((x_offer * Decimal::from_ratio(total_fee_bps, 10000u64)).u128()); + + let msg = Cw20QueryMsg::Balance { + address: user.to_string(), + }; + + let res: BalanceResponse = app + .wrap() + .query_wasm_smart(&token_y_instance, &msg) + .unwrap(); + + assert_eq!(res.balance, y_expected_return); + + let msg = Cw20QueryMsg::Balance { + address: fee_share_address.to_string(), + }; + + let res: BalanceResponse = app + .wrap() + .query_wasm_smart(&token_y_instance, &msg) + .unwrap(); + + let acceptable_spread_amount = Uint128::new(1); + assert_eq!(res.balance, expected_fee_share - acceptable_spread_amount); + + let msg = Cw20QueryMsg::Balance { + address: maker_address.to_string(), + }; + // Assert maker fee is correct + let res: BalanceResponse = app + .wrap() + .query_wasm_smart(&token_y_instance, &msg) + .unwrap(); + + assert_eq!(res.balance, expected_maker_fee); + + app.update_block(|b| b.height += 1); + + // Assert LP balances are correct + let msg = QueryMsg::Pool {}; + let res: PoolResponse = app.wrap().query_wasm_smart(&pair_instance, &msg).unwrap(); + + let acceptable_spread_amount = Uint128::new(1); + assert_eq!(res.assets[0].amount, x_amount + x_offer); + assert_eq!( + res.assets[1].amount, + y_amount - y_expected_return - expected_maker_fee - expected_fee_share + + acceptable_spread_amount + ); + + // Assert LP balances tracked are correct + let msg = QueryMsg::AssetBalanceAt { + asset_info: AssetInfo::Token { + contract_addr: token_y_instance, + }, + block_height: Uint64::from(app.block_info().height), + }; + let res: Option = app.wrap().query_wasm_smart(&pair_instance, &msg).unwrap(); + + assert_eq!( + res.unwrap(), + y_amount - y_expected_return - expected_maker_fee - expected_fee_share + + acceptable_spread_amount + ); +} diff --git a/contracts/pair_astro_xastro/Cargo.toml b/contracts/pair_astro_xastro/Cargo.toml index ce2889335..7857e1459 100644 --- a/contracts/pair_astro_xastro/Cargo.toml +++ b/contracts/pair_astro_xastro/Cargo.toml @@ -24,7 +24,7 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -astroport = { path = "../../packages/astroport", default-features = false } +astroport = { path = "../../packages/astroport", version = "3" } astroport-pair-bonded = { path = "../../packages/pair_bonded" } cw2 = { version = "0.15" } cw20 = { version = "0.15" } diff --git a/contracts/pair_concentrated/Cargo.toml b/contracts/pair_concentrated/Cargo.toml index 1d4c0991d..1d80b4d23 100644 --- a/contracts/pair_concentrated/Cargo.toml +++ b/contracts/pair_concentrated/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-pair-concentrated" -version = "2.0.5" +version = "2.2.0" authors = ["Astroport"] edition = "2021" description = "The Astroport concentrated liquidity pair" @@ -24,9 +24,12 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -astroport = { path = "../../packages/astroport", default-features = false } -astroport-factory = { path = "../factory", features = ["library"] } -astroport-circular-buffer = { path = "../../packages/circular_buffer" } +astroport = { path = "../../packages/astroport", version = "3" } +astroport-factory = { path = "../factory", features = [ + "library", +], version = "1" } +astroport-circular-buffer = { path = "../../packages/circular_buffer", version = "0.1" } +astroport-pcl-common = { path = "../../packages/astroport_pcl_common", version = "1" } cw2 = "0.15" cw20 = "0.15" cosmwasm-std = "1.1" diff --git a/contracts/pair_concentrated/src/contract.rs b/contracts/pair_concentrated/src/contract.rs index b4b986d6b..25fb704b9 100644 --- a/contracts/pair_concentrated/src/contract.rs +++ b/contracts/pair_concentrated/src/contract.rs @@ -3,7 +3,7 @@ use std::vec; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - attr, from_binary, wasm_execute, wasm_instantiate, Addr, Binary, CosmosMsg, Decimal, + attr, from_binary, wasm_execute, wasm_instantiate, Addr, Attribute, Binary, CosmosMsg, Decimal, Decimal256, DepsMut, Env, MessageInfo, Reply, Response, StdError, StdResult, SubMsg, SubMsgResponse, SubMsgResult, Uint128, }; @@ -20,27 +20,30 @@ use astroport::asset::{ use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; use astroport::cosmwasm_ext::{AbsDiff, DecimalToInteger, IntegerToDecimal}; use astroport::factory::PairType; -use astroport::observation::{MIN_TRADE_SIZE, OBSERVATIONS_SIZE}; -use astroport::pair::{Cw20HookMsg, ExecuteMsg, InstantiateMsg}; +use astroport::observation::{PrecommitObservation, OBSERVATIONS_SIZE}; +use astroport::pair::{ + Cw20HookMsg, ExecuteMsg, FeeShareConfig, InstantiateMsg, MAX_FEE_SHARE_BPS, MIN_TRADE_SIZE, +}; use astroport::pair_concentrated::{ ConcentratedPoolParams, ConcentratedPoolUpdateParams, MigrateMsg, UpdatePoolParams, }; use astroport::querier::{query_factory_config, query_fee_info, query_supply}; use astroport::token::InstantiateMsg as TokenInstantiateMsg; use astroport_circular_buffer::BufferManager; +use astroport_pcl_common::state::{ + AmpGamma, Config, PoolParams, PoolState, Precisions, PriceState, +}; +use astroport_pcl_common::utils::{ + assert_max_spread, assert_slippage_tolerance, before_swap_check, calc_provide_fee, + check_asset_infos, check_assets, check_cw20_in_pool, check_pair_registered, compute_swap, + get_share_in_assets, mint_liquidity_token_message, +}; +use astroport_pcl_common::{calc_d, get_xcp}; use crate::error::ContractError; -use crate::math::{calc_d, get_xcp}; use crate::migration::migrate_config; -use crate::state::{ - store_precisions, AmpGamma, Config, PoolParams, PoolState, Precisions, PriceState, BALANCES, - CONFIG, OBSERVATIONS, OWNERSHIP_PROPOSAL, -}; -use crate::utils::{ - accumulate_swap_sizes, assert_max_spread, assert_slippage_tolerance, before_swap_check, - calc_provide_fee, check_asset_infos, check_assets, check_cw20_in_pool, check_pair_registered, - compute_swap, get_share_in_assets, mint_liquidity_token_message, query_pools, -}; +use crate::state::{BALANCES, CONFIG, OBSERVATIONS, OWNERSHIP_PROPOSAL}; +use crate::utils::{accumulate_swap_sizes, query_pools}; /// Contract name that is used for migration. const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); @@ -78,7 +81,7 @@ pub fn instantiate( let factory_addr = deps.api.addr_validate(&msg.factory_addr)?; - store_precisions(deps.branch(), &msg.asset_infos, &factory_addr)?; + Precisions::store_precisions(deps.branch(), &msg.asset_infos, &factory_addr)?; let mut pool_params = PoolParams::default(); pool_params.update_params(UpdatePoolParams { @@ -113,11 +116,11 @@ pub fn instantiate( pair_type: PairType::Custom("concentrated".to_string()), }, factory_addr, - block_time_last: env.block.time.seconds(), pool_params, pool_state, owner: None, track_asset_balances: params.track_asset_balances.unwrap_or_default(), + fee_share: None, }; if config.track_asset_balances { @@ -520,8 +523,8 @@ pub fn provide_liquidity( let mut slippage = Decimal256::zero(); - // if assets_diff[1] is zero then deposits are balanced thus no need to update price and check slippage - if !assets_diff[1].is_zero() { + // If deposit doesn't diverge too much from the balanced share, we don't update the price + if assets_diff[0] >= MIN_TRADE_SIZE && assets_diff[1] >= MIN_TRADE_SIZE { slippage = assert_slippage_tolerance( &deposits, share, @@ -735,6 +738,11 @@ fn swap( if fee_info.fee_address.is_some() { maker_fee_share = fee_info.maker_fee_rate.into(); } + // If this pool is configured to share fees + let mut share_fee_share = Decimal256::zero(); + if let Some(fee_share) = config.fee_share.clone() { + share_fee_share = Decimal256::from_ratio(fee_share.bps, 10000u16); + } let swap_result = compute_swap( &xs, @@ -743,9 +751,10 @@ fn swap( &config, &env, maker_fee_share, + share_fee_share, )?; xs[offer_ind] += offer_asset_dec.amount; - xs[ask_ind] -= swap_result.dy + swap_result.maker_fee; + xs[ask_ind] -= swap_result.dy + swap_result.maker_fee + swap_result.share_fee; let return_amount = swap_result.dy.to_uint(ask_asset_prec)?; let spread_amount = swap_result.spread_fee.to_uint(ask_asset_prec)?; @@ -760,13 +769,19 @@ fn swap( let total_share = query_supply(&deps.querier, &config.pair_info.liquidity_token)? .to_decimal256(LP_TOKEN_PRECISION)?; - let last_price = swap_result.calc_last_prices(offer_asset_dec.amount, offer_ind); - - // update_price() works only with internal representation - xs[1] *= config.pool_state.price_state.price_scale; - config - .pool_state - .update_price(&config.pool_params, &env, total_share, &xs, last_price)?; + // Skip very small trade sizes which could significantly mess up the price due to rounding errors, + // especially if token precisions are 18. + if (swap_result.dy + swap_result.maker_fee + swap_result.share_fee) >= MIN_TRADE_SIZE + && offer_asset_dec.amount >= MIN_TRADE_SIZE + { + let last_price = swap_result.calc_last_price(offer_asset_dec.amount, offer_ind); + + // update_price() works only with internal representation + xs[1] *= config.pool_state.price_state.price_scale; + config + .pool_state + .update_price(&config.pool_params, &env, total_share, &xs, last_price)?; + } let receiver = to.unwrap_or_else(|| sender.clone()); @@ -776,6 +791,17 @@ fn swap( } .into_msg(&receiver)?]; + // Send the shared fee + let mut fee_share_amount = Uint128::zero(); + if let Some(fee_share) = config.fee_share.clone() { + fee_share_amount = swap_result.share_fee.to_uint(ask_asset_prec)?; + if !fee_share_amount.is_zero() { + let fee = pools[ask_ind].info.with_balance(fee_share_amount); + messages.push(fee.into_msg(fee_share.recipient)?); + } + } + + // Send the maker fee let mut maker_fee = Uint128::zero(); if let Some(fee_address) = fee_info.fee_address { maker_fee = swap_result.maker_fee.to_uint(ask_asset_prec)?; @@ -785,15 +811,19 @@ fn swap( } } - // Store time series data. - // Skipping small unsafe values which can seriously mess oracle price due to rounding errors + // Store observation from precommit data + accumulate_swap_sizes(deps.storage, &env)?; + + // Store time series data in precommit observation. + // Skipping small unsafe values which can seriously mess oracle price due to rounding errors. + // This data will be reflected in observations in the next action. if offer_asset_dec.amount >= MIN_TRADE_SIZE && swap_result.dy >= MIN_TRADE_SIZE { let (base_amount, quote_amount) = if offer_ind == 0 { (offer_asset.amount, return_amount) } else { (return_amount, offer_asset.amount) }; - accumulate_swap_sizes(deps.storage, &env, base_amount, quote_amount)?; + PrecommitObservation::save(deps.storage, &env, base_amount, quote_amount)?; } CONFIG.save(deps.storage, &config)?; @@ -808,7 +838,10 @@ fn swap( BALANCES.save( deps.storage, &pools[ask_ind].info, - &(pools[ask_ind].amount.to_uint(ask_asset_prec)? - return_amount - maker_fee), + &(pools[ask_ind].amount.to_uint(ask_asset_prec)? + - return_amount + - maker_fee + - fee_share_amount), env.block.height, )?; } @@ -827,6 +860,7 @@ fn swap( swap_result.total_fee.to_uint(ask_asset_prec)?, ), attr("maker_fee_amount", maker_fee), + attr("fee_share_amount", fee_share_amount), ])) } @@ -847,6 +881,8 @@ fn update_config( return Err(ContractError::Unauthorized {}); } + let mut attrs: Vec = vec![]; + let action = match from_binary::(¶ms)? { ConcentratedPoolUpdateParams::Update(update_params) => { config.pool_params.update_params(update_params)?; @@ -876,10 +912,44 @@ fn update_config( "enable_asset_balances_tracking" } + ConcentratedPoolUpdateParams::EnableFeeShare { + fee_share_bps, + fee_share_address, + } => { + // Enable fee sharing for this contract + // If fee sharing is already enabled, we should be able to overwrite + // the values currently set + + // Ensure the fee share isn't 0 and doesn't exceed the maximum allowed value + if fee_share_bps == 0 || fee_share_bps > MAX_FEE_SHARE_BPS { + return Err(ContractError::FeeShareOutOfBounds {}); + } + + // Set sharing config + config.fee_share = Some(FeeShareConfig { + bps: fee_share_bps, + recipient: deps.api.addr_validate(&fee_share_address)?, + }); + + CONFIG.save(deps.storage, &config)?; + + attrs.push(attr("fee_share_bps", fee_share_bps.to_string())); + attrs.push(attr("fee_share_address", fee_share_address)); + "enable_fee_share" + } + ConcentratedPoolUpdateParams::DisableFeeShare => { + // Disable fee sharing for this contract by setting bps and + // address back to None + config.fee_share = None; + CONFIG.save(deps.storage, &config)?; + "disable_fee_share" + } }; CONFIG.save(deps.storage, &config)?; - Ok(Response::new().add_attribute("action", action)) + Ok(Response::new() + .add_attribute("action", action) + .add_attributes(attrs)) } #[cfg_attr(not(feature = "library"), entry_point)] diff --git a/contracts/pair_concentrated/src/error.rs b/contracts/pair_concentrated/src/error.rs index 6c06b2cf6..b72e859f8 100644 --- a/contracts/pair_concentrated/src/error.rs +++ b/contracts/pair_concentrated/src/error.rs @@ -1,9 +1,10 @@ -use crate::consts::MIN_AMP_CHANGING_TIME; -use astroport::asset::MINIMUM_LIQUIDITY_AMOUNT; -use astroport_circular_buffer::error::BufferError; -use cosmwasm_std::{ConversionOverflowError, Decimal, OverflowError, StdError}; +use cosmwasm_std::{ConversionOverflowError, OverflowError, StdError}; use thiserror::Error; +use astroport::{asset::MINIMUM_LIQUIDITY_AMOUNT, pair::MAX_FEE_SHARE_BPS}; +use astroport_circular_buffer::error::BufferError; +use astroport_pcl_common::error::PclError; + /// This enum describes pair contract errors #[derive(Error, Debug, PartialEq)] pub enum ContractError { @@ -19,6 +20,9 @@ pub enum ContractError { #[error("{0}")] CircularBuffer(#[from] BufferError), + #[error("{0}")] + PclError(#[from] PclError), + #[error("Unauthorized")] Unauthorized {}, @@ -28,35 +32,9 @@ pub enum ContractError { #[error("You need to provide init params")] InitParamsNotFound {}, - #[error("{0} parameter must be greater than {1} and less than or equal to {2}")] - IncorrectPoolParam(String, String, String), - - #[error( - "{0} error: The difference between the old and new amp or gamma values must not exceed {1} percent", - )] - MaxChangeAssertion(String, Decimal), - - #[error( - "Amp and gamma coefficients cannot be changed more often than once per {} seconds", - MIN_AMP_CHANGING_TIME - )] - MinChangingTimeAssertion {}, - #[error("Initial provide can not be one-sided")] InvalidZeroAmount {}, - #[error("Operation exceeds max spread limit")] - MaxSpreadAssertion {}, - - #[error("Provided spread amount exceeds allowed limit")] - AllowedSpreadAssertion {}, - - #[error("Doubling assets in asset infos")] - DoublingAssets {}, - - #[error("Generator address is not set in factory. Cannot auto-stake")] - AutoStakeError {}, - #[error("Initial liquidity must be more than {}", MINIMUM_LIQUIDITY_AMOUNT)] MinimumLiquidityAmountError {}, @@ -77,4 +55,10 @@ pub enum ContractError { #[error("Asset balances tracking is already enabled")] AssetBalancesTrackingIsAlreadyEnabled {}, + + #[error( + "Fee share is 0 or exceeds maximum allowed value of {} bps", + MAX_FEE_SHARE_BPS + )] + FeeShareOutOfBounds {}, } diff --git a/contracts/pair_concentrated/src/lib.rs b/contracts/pair_concentrated/src/lib.rs index e0d7f1b40..882b67dcc 100644 --- a/contracts/pair_concentrated/src/lib.rs +++ b/contracts/pair_concentrated/src/lib.rs @@ -1,9 +1,7 @@ pub mod contract; pub mod state; -pub mod consts; pub mod error; -pub mod math; mod migration; pub mod queries; pub mod utils; diff --git a/contracts/pair_concentrated/src/migration.rs b/contracts/pair_concentrated/src/migration.rs index 138404ddb..c2ebed661 100644 --- a/contracts/pair_concentrated/src/migration.rs +++ b/contracts/pair_concentrated/src/migration.rs @@ -1,13 +1,14 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, StdError, Storage, Uint128}; +use cw_storage_plus::Item; + use astroport::asset::{AssetInfo, PairInfo}; use astroport::factory::PairType; use astroport::observation::OBSERVATIONS_SIZE; use astroport_circular_buffer::BufferManager; -use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, StdError, Storage, Uint128}; -use cw_storage_plus::Item; +use astroport_pcl_common::state::{Config, PoolParams, PoolState}; -use crate::state::{Config, CONFIG, OBSERVATIONS}; -use crate::state::{PoolParams, PoolState}; +use crate::state::{CONFIG, OBSERVATIONS}; pub(crate) fn migrate_config(storage: &mut dyn Storage) -> Result<(), StdError> { #[cw_serde] @@ -67,11 +68,11 @@ pub(crate) fn migrate_config(storage: &mut dyn Storage) -> Result<(), StdError> let new_config = Config { pair_info, factory_addr: old_config.factory_addr, - block_time_last: old_config.block_time_last, pool_params: old_config.pool_params, pool_state: old_config.pool_state, owner: old_config.owner, track_asset_balances: false, + fee_share: None, }; CONFIG.save(storage, &new_config)?; diff --git a/contracts/pair_concentrated/src/queries.rs b/contracts/pair_concentrated/src/queries.rs index ab4f96d1c..053c9b150 100644 --- a/contracts/pair_concentrated/src/queries.rs +++ b/contracts/pair_concentrated/src/queries.rs @@ -17,14 +17,15 @@ use astroport::querier::{query_factory_config, query_fee_info, query_supply}; use crate::contract::LP_TOKEN_PRECISION; use crate::error::ContractError; -use crate::math::{calc_d, get_xcp}; +use astroport_pcl_common::state::Precisions; +use astroport_pcl_common::utils::{ + before_swap_check, compute_offer_amount, compute_swap, get_share_in_assets, +}; +use astroport_pcl_common::{calc_d, get_xcp}; -use crate::state::{Precisions, BALANCES, CONFIG, OBSERVATIONS}; +use crate::state::{BALANCES, CONFIG, OBSERVATIONS}; -use crate::utils::{ - before_swap_check, compute_offer_amount, compute_swap, get_share_in_assets, pool_info, - query_pools, -}; +use crate::utils::{pool_info, query_pools}; /// Exposes all the queries available in the contract. /// @@ -161,6 +162,11 @@ pub fn query_simulation( if fee_info.fee_address.is_some() { maker_fee_share = fee_info.maker_fee_rate.into(); } + // If this pool is configured to share fees + let mut share_fee_share = Decimal256::zero(); + if let Some(fee_share) = config.fee_share.clone() { + share_fee_share = Decimal256::from_ratio(fee_share.bps, 10000u16); + } let swap_result = compute_swap( &xs, @@ -169,6 +175,7 @@ pub fn query_simulation( &config, &env, maker_fee_share, + share_fee_share, )?; Ok(SimulationResponse { @@ -246,7 +253,7 @@ pub fn query_config(deps: Deps, env: Env) -> StdResult { let factory_config = query_factory_config(&deps.querier, &config.factory_addr)?; Ok(ConfigResponse { - block_time_last: config.block_time_last, + block_time_last: 0, // keeping this field for backwards compatibility params: Some(to_binary(&ConcentratedPoolConfig { amp: amp_gamma.amp, gamma: amp_gamma.gamma, @@ -258,6 +265,7 @@ pub fn query_config(deps: Deps, env: Env) -> StdResult { price_scale, ma_half_time: config.pool_params.ma_half_time, track_asset_balances: config.track_asset_balances, + fee_share: config.fee_share, })?), owner: config.owner.unwrap_or(factory_config.owner), factory_addr: config.factory_addr, diff --git a/contracts/pair_concentrated/src/state.rs b/contracts/pair_concentrated/src/state.rs index e9d520915..4d52d5059 100644 --- a/contracts/pair_concentrated/src/state.rs +++ b/contracts/pair_concentrated/src/state.rs @@ -1,413 +1,15 @@ -use std::fmt::Display; +use cosmwasm_std::Uint128; +use cw_storage_plus::{Item, SnapshotMap}; -use cosmwasm_schema::cw_serde; -use cosmwasm_std::{ - Addr, Decimal, Decimal256, DepsMut, Env, Order, StdError, StdResult, Storage, Uint128, -}; -use cw_storage_plus::{Item, Map, SnapshotMap}; - -use astroport::asset::{AssetInfo, PairInfo}; +use astroport::asset::AssetInfo; use astroport::common::OwnershipProposal; -use astroport::cosmwasm_ext::{AbsDiff, IntegerToDecimal}; use astroport::observation::Observation; -use astroport::pair_concentrated::{PromoteParams, UpdatePoolParams}; use astroport_circular_buffer::CircularBuffer; - -use crate::consts::{ - AMP_MAX, AMP_MIN, FEE_GAMMA_MAX, FEE_GAMMA_MIN, FEE_TOL, GAMMA_MAX, GAMMA_MIN, MAX_CHANGE, - MAX_FEE, MA_HALF_TIME_LIMITS, MIN_AMP_CHANGING_TIME, MIN_FEE, N_POW2, PRICE_SCALE_DELTA_MAX, - PRICE_SCALE_DELTA_MIN, REPEG_PROFIT_THRESHOLD_MAX, REPEG_PROFIT_THRESHOLD_MIN, TWO, -}; -use crate::error::ContractError; -use crate::math::{calc_d, get_xcp, half_float_pow}; - -/// This structure stores the concentrated pair parameters. -#[cw_serde] -pub struct Config { - /// The pair information stored in a [`PairInfo`] struct - pub pair_info: PairInfo, - /// The factory contract address - pub factory_addr: Addr, - /// The last timestamp when the pair contract updated the asset cumulative prices - pub block_time_last: u64, - /// Pool parameters - pub pool_params: PoolParams, - /// Pool state - pub pool_state: PoolState, - /// Pool's owner - pub owner: Option, - /// Whether asset balances are tracked over blocks or not. - pub track_asset_balances: bool, -} - -/// This structure stores the pool parameters which may be adjusted via the `update_pool_params`. -#[cw_serde] -#[derive(Default)] -pub struct PoolParams { - /// The minimum fee, charged when pool is fully balanced - pub mid_fee: Decimal, - /// The maximum fee, charged when pool is imbalanced - pub out_fee: Decimal, - /// Parameter that defines how gradual the fee changes from fee_mid to fee_out based on - /// distance from price_scale - pub fee_gamma: Decimal, - /// Minimum profit before initiating a new repeg - pub repeg_profit_threshold: Decimal, - /// Minimum amount to change price_scale when repegging - pub min_price_scale_delta: Decimal, - /// Half-time used for calculating the price oracle - pub ma_half_time: u64, -} - -/// Validates input value against its limits. -fn validate_param(name: &str, val: T, min: T, max: T) -> Result<(), ContractError> -where - T: PartialOrd + Display, -{ - if val >= min && val <= max { - Ok(()) - } else { - Err(ContractError::IncorrectPoolParam( - name.to_string(), - min.to_string(), - max.to_string(), - )) - } -} - -impl PoolParams { - /// Intended to update current pool parameters. Performs validation of the new parameters. - /// - /// * `update_params` - an object which contains new pool parameters. Any of the parameters may be omitted. - pub fn update_params(&mut self, update_params: UpdatePoolParams) -> Result<(), ContractError> { - if let Some(mid_fee) = update_params.mid_fee { - validate_param("mid_fee", mid_fee, MIN_FEE, MAX_FEE)?; - self.mid_fee = mid_fee; - } - - if let Some(out_fee) = update_params.out_fee { - validate_param("out_fee", out_fee, MIN_FEE, MAX_FEE)?; - if out_fee <= self.mid_fee { - return Err(StdError::generic_err(format!( - "out_fee {out_fee} must be more {}", - self.mid_fee - )) - .into()); - } - self.out_fee = out_fee; - } - - if let Some(fee_gamma) = update_params.fee_gamma { - validate_param("fee_gamma", fee_gamma, FEE_GAMMA_MIN, FEE_GAMMA_MAX)?; - self.fee_gamma = fee_gamma; - } - - if let Some(repeg_profit_threshold) = update_params.repeg_profit_threshold { - validate_param( - "repeg_profit_threshold", - repeg_profit_threshold, - REPEG_PROFIT_THRESHOLD_MIN, - REPEG_PROFIT_THRESHOLD_MAX, - )?; - self.repeg_profit_threshold = repeg_profit_threshold; - } - - if let Some(min_price_scale_delta) = update_params.min_price_scale_delta { - validate_param( - "min_price_scale_delta", - min_price_scale_delta, - PRICE_SCALE_DELTA_MIN, - PRICE_SCALE_DELTA_MAX, - )?; - self.min_price_scale_delta = min_price_scale_delta; - } - - if let Some(ma_half_time) = update_params.ma_half_time { - validate_param( - "ma_half_time", - ma_half_time, - *MA_HALF_TIME_LIMITS.start(), - *MA_HALF_TIME_LIMITS.end(), - )?; - self.ma_half_time = ma_half_time; - } - - Ok(()) - } - - pub fn fee(&self, xp: &[Decimal256]) -> Decimal256 { - let fee_gamma: Decimal256 = self.fee_gamma.into(); - let sum = xp[0] + xp[1]; - let mut k = xp[0] * xp[1] * N_POW2 / sum.pow(2); - k = fee_gamma / (fee_gamma + Decimal256::one() - k); - - if k <= FEE_TOL { - k = Decimal256::zero() - } - - k * Decimal256::from(self.mid_fee) - + (Decimal256::one() - k) * Decimal256::from(self.out_fee) - } -} - -/// Structure which stores Amp and Gamma. -#[cw_serde] -#[derive(Default, Copy)] -pub struct AmpGamma { - pub amp: Decimal, - pub gamma: Decimal, -} - -impl AmpGamma { - /// Validates the parameters and creates a new object of the [`AmpGamma`] structure. - pub fn new(amp: Decimal, gamma: Decimal) -> Result { - validate_param("amp", amp, AMP_MIN, AMP_MAX)?; - validate_param("gamma", gamma, GAMMA_MIN, GAMMA_MAX)?; - - Ok(AmpGamma { amp, gamma }) - } -} - -/// Internal structure which stores the price state. -/// This structure cannot be updated via update_config. -#[cw_serde] -#[derive(Default)] -pub struct PriceState { - /// Internal oracle price - pub oracle_price: Decimal256, - /// The last saved price - pub last_price: Decimal256, - /// Current price scale between 1st and 2nd assets. - /// I.e. such C that x = C * y where x - 1st asset, y - 2nd asset. - pub price_scale: Decimal256, - /// Last timestamp when the price_oracle was updated. - pub last_price_update: u64, - /// Keeps track of positive change in xcp due to fees accruing - pub xcp_profit: Decimal256, - /// Profits due to fees inclusive of realized losses from rebalancing - pub xcp_profit_real: Decimal256, -} - -/// Internal structure which stores the pool's state. -#[cw_serde] -pub struct PoolState { - /// Initial Amp and Gamma - pub initial: AmpGamma, - /// Future Amp and Gamma - pub future: AmpGamma, - /// Timestamp when Amp and Gamma should become equal to self.future - pub future_time: u64, - /// Timestamp when Amp and Gamma started being changed - pub initial_time: u64, - /// Current price state - pub price_state: PriceState, -} - -impl PoolState { - /// Validates Amp and Gamma promotion parameters. - /// Saves current values in self.initial and setups self.future. - /// If amp and gamma are being changed then current values will be used as initial values. - pub fn promote_params( - &mut self, - env: &Env, - params: PromoteParams, - ) -> Result<(), ContractError> { - let block_time = env.block.time.seconds(); - - // Validate time interval - if block_time < self.initial_time + MIN_AMP_CHANGING_TIME - || params.future_time < block_time + MIN_AMP_CHANGING_TIME - { - return Err(ContractError::MinChangingTimeAssertion {}); - } - - // Validate amp and gamma - let next_amp_gamma = AmpGamma::new(params.next_amp, params.next_gamma)?; - - // Calculate current amp and gamma - let cur_amp_gamma = self.get_amp_gamma(env); - - // Validate amp and gamma values are being changed by <= 10% - let one = Decimal::one(); - if (next_amp_gamma.amp / cur_amp_gamma.amp).diff(one) > MAX_CHANGE { - return Err(ContractError::MaxChangeAssertion( - "Amp".to_string(), - MAX_CHANGE, - )); - } - if (next_amp_gamma.gamma / cur_amp_gamma.gamma).diff(one) > MAX_CHANGE { - return Err(ContractError::MaxChangeAssertion( - "Gamma".to_string(), - MAX_CHANGE, - )); - } - - self.initial = cur_amp_gamma; - self.initial_time = block_time; - - self.future = next_amp_gamma; - self.future_time = params.future_time; - - Ok(()) - } - - /// Stops amp and gamma promotion. Saves current values in self.future. - pub fn stop_promotion(&mut self, env: &Env) { - self.future = self.get_amp_gamma(env); - self.future_time = env.block.time.seconds(); - } - - /// Calculates current amp and gamma. - /// This function handles parameters upgrade as well as downgrade. - /// If block time >= self.future_time then it returns self.future parameters. - pub fn get_amp_gamma(&self, env: &Env) -> AmpGamma { - let block_time = env.block.time.seconds(); - if block_time < self.future_time { - let total = (self.future_time - self.initial_time).to_decimal(); - let passed = (block_time - self.initial_time).to_decimal(); - let left = total - passed; - - // A1 = A0 + (A1 - A0) * (block_time - t_init) / (t_end - t_init) -> simplified to: - // A1 = ( A0 * (t_end - block_time) + A1 * (block_time - t_init) ) / (t_end - t_init) - let amp = (self.initial.amp * left + self.future.amp * passed) / total; - let gamma = (self.initial.gamma * left + self.future.gamma * passed) / total; - - AmpGamma { amp, gamma } - } else { - AmpGamma { - amp: self.future.amp, - gamma: self.future.gamma, - } - } - } - - /// The function is responsible for repegging mechanism. - /// It updates internal oracle price and adjusts price scale. - /// - /// * **total_lp** total LP tokens were minted - /// * **cur_xs** - internal representation of pool volumes - /// * **cur_price** - last price happened in the previous action (swap, provide or withdraw) - pub fn update_price( - &mut self, - pool_params: &PoolParams, - env: &Env, - total_lp: Decimal256, - cur_xs: &[Decimal256], - cur_price: Decimal256, - ) -> StdResult<()> { - let amp_gamma = self.get_amp_gamma(env); - let block_time = env.block.time.seconds(); - let price_state = &mut self.price_state; - - if price_state.last_price_update < block_time { - let arg = Decimal256::from_ratio( - block_time - price_state.last_price_update, - pool_params.ma_half_time, - ); - let alpha = half_float_pow(arg)?; - price_state.oracle_price = price_state.last_price * (Decimal256::one() - alpha) - + price_state.oracle_price * alpha; - price_state.last_price_update = block_time; - } - price_state.last_price = cur_price; - - let cur_d = calc_d(cur_xs, &_gamma)?; - let xcp = get_xcp(cur_d, price_state.price_scale); - - if !price_state.xcp_profit_real.is_zero() { - let xcp_profit_real = xcp / total_lp; - - // If xcp dropped and no ramping happens then this swap makes loss - if xcp_profit_real < price_state.xcp_profit_real && block_time >= self.future_time { - return Err(StdError::generic_err( - "XCP profit real value dropped. This action makes loss", - )); - } - - price_state.xcp_profit = - price_state.xcp_profit * xcp_profit_real / price_state.xcp_profit_real; - price_state.xcp_profit_real = xcp_profit_real; - } - - let xcp_profit = price_state.xcp_profit; - - let norm = (price_state.oracle_price / price_state.price_scale).diff(Decimal256::one()); - let scale_delta = Decimal256::from(pool_params.min_price_scale_delta) - .max(norm * Decimal256::from_ratio(1u8, 10u8)); - - if norm >= scale_delta - && price_state.xcp_profit_real - Decimal256::one() - > (xcp_profit - Decimal256::one()) / TWO - + Decimal256::from(pool_params.repeg_profit_threshold) - { - let numerator = price_state.price_scale * (norm - scale_delta) - + scale_delta * price_state.oracle_price; - let price_scale_new = numerator / norm; - - let xs = [ - cur_xs[0], - cur_xs[1] * price_scale_new / price_state.price_scale, - ]; - let new_d = calc_d(&xs, &_gamma)?; - - let new_xcp = get_xcp(new_d, price_scale_new); - let new_xcp_profit_real = new_xcp / total_lp; - - if TWO * new_xcp_profit_real > xcp_profit + Decimal256::one() { - price_state.price_scale = price_scale_new; - price_state.xcp_profit_real = new_xcp_profit_real; - }; - } - - Ok(()) - } -} - -/// Store all token precisions. -pub(crate) fn store_precisions( - deps: DepsMut, - asset_infos: &[AssetInfo], - factory_addr: &Addr, -) -> StdResult<()> { - for asset_info in asset_infos { - let precision = asset_info.decimals(&deps.querier, factory_addr)?; - PRECISIONS.save(deps.storage, asset_info.to_string(), &precision)?; - } - - Ok(()) -} - -pub(crate) struct Precisions(Vec<(String, u8)>); - -impl Precisions { - pub(crate) fn new(storage: &dyn Storage) -> StdResult { - let items = PRECISIONS - .range(storage, None, None, Order::Ascending) - .collect::>>()?; - - Ok(Self(items)) - } - - pub(crate) fn get_precision(&self, asset_info: &AssetInfo) -> Result { - self.0 - .iter() - .find_map(|(info, prec)| { - if info == &asset_info.to_string() { - Some(*prec) - } else { - None - } - }) - .ok_or_else(|| ContractError::InvalidAsset(asset_info.to_string())) - } -} +use astroport_pcl_common::state::Config; /// Stores pool parameters and state. pub const CONFIG: Item = Item::new("config"); -/// Stores map of AssetInfo (as String) -> precision -const PRECISIONS: Map = Map::new("precisions"); - /// Stores the latest contract ownership transfer proposal pub const OWNERSHIP_PROPOSAL: Item = Item::new("ownership_proposal"); @@ -422,432 +24,3 @@ pub const BALANCES: SnapshotMap<&AssetInfo, Uint128> = SnapshotMap::new( "balances_change", cw_storage_plus::Strategy::EveryBlock, ); - -#[cfg(test)] -mod test { - use std::str::FromStr; - - use cosmwasm_std::testing::mock_env; - use cosmwasm_std::Timestamp; - - use crate::math::calc_y; - - use super::*; - - fn f64_to_dec(val: f64) -> Decimal { - Decimal::from_str(&val.to_string()).unwrap() - } - fn f64_to_dec256(val: f64) -> Decimal256 { - Decimal256::from_str(&val.to_string()).unwrap() - } - fn dec_to_f64(val: Decimal256) -> f64 { - f64::from_str(&val.to_string()).unwrap() - } - - #[test] - #[should_panic(expected = "attempt to subtract with overflow")] - fn test_validator_odd_behaviour() { - let mut env = mock_env(); - env.block.time = Timestamp::from_seconds(86400); - - let mut state = PoolState { - initial: AmpGamma { - amp: Decimal::zero(), - gamma: Decimal::zero(), - }, - future: AmpGamma { - amp: f64_to_dec(100_f64), - gamma: f64_to_dec(0.0000001_f64), - }, - future_time: 0, - initial_time: 0, - price_state: Default::default(), - }; - - // Increase values - let promote_params = PromoteParams { - next_amp: f64_to_dec(110_f64), - next_gamma: f64_to_dec(0.00000011_f64), - future_time: env.block.time.seconds() + 100_000, - }; - state.promote_params(&env, promote_params).unwrap(); - - let AmpGamma { amp, gamma } = state.get_amp_gamma(&env); - assert_eq!(amp, f64_to_dec(100_f64)); - assert_eq!(gamma, f64_to_dec(0.0000001_f64)); - - // Simulating validator odd behavior - env.block.time = env.block.time.minus_seconds(1000); - state.get_amp_gamma(&env); - } - - #[test] - fn test_pool_state() { - let mut env = mock_env(); - env.block.time = Timestamp::from_seconds(86400); - - let mut state = PoolState { - initial: AmpGamma { - amp: Decimal::zero(), - gamma: Decimal::zero(), - }, - future: AmpGamma { - amp: f64_to_dec(100_f64), - gamma: f64_to_dec(0.0000001_f64), - }, - future_time: 0, - initial_time: 0, - price_state: Default::default(), - }; - - // Trying to promote params with future time in the past - let promote_params = PromoteParams { - next_amp: f64_to_dec(110_f64), - next_gamma: f64_to_dec(0.00000011_f64), - future_time: env.block.time.seconds() - 10000, - }; - let err = state.promote_params(&env, promote_params).unwrap_err(); - assert_eq!(err, ContractError::MinChangingTimeAssertion {}); - - // Increase values - let promote_params = PromoteParams { - next_amp: f64_to_dec(110_f64), - next_gamma: f64_to_dec(0.00000011_f64), - future_time: env.block.time.seconds() + 100_000, - }; - state.promote_params(&env, promote_params).unwrap(); - - let AmpGamma { amp, gamma } = state.get_amp_gamma(&env); - assert_eq!(amp, f64_to_dec(100_f64)); - assert_eq!(gamma, f64_to_dec(0.0000001_f64)); - - env.block.time = env.block.time.plus_seconds(50_000); - - let AmpGamma { amp, gamma } = state.get_amp_gamma(&env); - assert_eq!(amp, f64_to_dec(105_f64)); - assert_eq!(gamma, f64_to_dec(0.000000105_f64)); - - env.block.time = env.block.time.plus_seconds(100_001); - let AmpGamma { amp, gamma } = state.get_amp_gamma(&env); - assert_eq!(amp, f64_to_dec(110_f64)); - assert_eq!(gamma, f64_to_dec(0.00000011_f64)); - - // Decrease values - let promote_params = PromoteParams { - next_amp: f64_to_dec(108_f64), - next_gamma: f64_to_dec(0.000000106_f64), - future_time: env.block.time.seconds() + 100_000, - }; - state.promote_params(&env, promote_params).unwrap(); - - env.block.time = env.block.time.plus_seconds(50_000); - let AmpGamma { amp, gamma } = state.get_amp_gamma(&env); - assert_eq!(amp, f64_to_dec(109_f64)); - assert_eq!(gamma, f64_to_dec(0.000000108_f64)); - - env.block.time = env.block.time.plus_seconds(50_001); - let AmpGamma { amp, gamma } = state.get_amp_gamma(&env); - assert_eq!(amp, f64_to_dec(108_f64)); - assert_eq!(gamma, f64_to_dec(0.000000106_f64)); - - // Increase amp only - let promote_params = PromoteParams { - next_amp: f64_to_dec(118_f64), - next_gamma: f64_to_dec(0.000000106_f64), - future_time: env.block.time.seconds() + 100_000, - }; - state.promote_params(&env, promote_params).unwrap(); - - env.block.time = env.block.time.plus_seconds(50_000); - let AmpGamma { amp, gamma } = state.get_amp_gamma(&env); - assert_eq!(amp, f64_to_dec(113_f64)); - assert_eq!(gamma, f64_to_dec(0.000000106_f64)); - - env.block.time = env.block.time.plus_seconds(50_001); - let AmpGamma { amp, gamma } = state.get_amp_gamma(&env); - assert_eq!(amp, f64_to_dec(118_f64)); - assert_eq!(gamma, f64_to_dec(0.000000106_f64)); - } - - #[test] - fn check_fee_update() { - let mid_fee = 0.25f64; - let out_fee = 0.46f64; - let fee_gamma = 0.0002f64; - - let params = PoolParams { - mid_fee: f64_to_dec(mid_fee), - out_fee: f64_to_dec(out_fee), - fee_gamma: f64_to_dec(fee_gamma), - repeg_profit_threshold: Default::default(), - min_price_scale_delta: Default::default(), - ma_half_time: 0, - }; - - let xp = vec![f64_to_dec256(1_000_000f64), f64_to_dec256(1_000_000f64)]; - let result = params.fee(&xp); - assert_eq!(dec_to_f64(result), mid_fee); - - let xp = vec![f64_to_dec256(990_000f64), f64_to_dec256(1_000_000f64)]; - let result = params.fee(&xp); - assert_eq!(dec_to_f64(result), 0.2735420730476899); - - let xp = vec![f64_to_dec256(100_000f64), f64_to_dec256(1_000_000_f64)]; - let result = params.fee(&xp); - assert_eq!(dec_to_f64(result), out_fee); - } - - /// (cur_d, total_lp, new_price) - fn swap( - ext_xs: &mut [Decimal256], - offer_amount: Decimal256, - price_scale: Decimal256, - ask_ind: usize, - amp_gamma: &AmpGamma, - pool_params: &PoolParams, - ) -> Decimal256 { - let offer_ind = 1 - ask_ind; - - let mut xs = ext_xs.to_vec(); - println!("Before swap: {} {}", xs[0], xs[1]); - - // internal repr - xs[1] *= price_scale; - println!("Before swap (internal): {} {}", xs[0], xs[1]); - - let cur_d = calc_d(&xs, amp_gamma).unwrap(); - - let mut offer_amount_internal = offer_amount; - // internal repr - if offer_ind == 1 { - offer_amount_internal *= price_scale; - } - - xs[offer_ind] += offer_amount_internal; - let mut ask_amount = xs[ask_ind] - calc_y(&xs, cur_d, amp_gamma, ask_ind).unwrap(); - xs[ask_ind] -= ask_amount; - let fee = ask_amount * pool_params.fee(&xs); - println!("fee {fee} ({}%)", pool_params.fee(&xs)); - xs[ask_ind] += fee; - ask_amount -= fee; - - println!( - "Internal Swap {} x[{}] for {} x[{}] by {} price", - offer_amount_internal, - offer_ind, - ask_amount, - ask_ind, - ask_amount / offer_amount_internal - ); - - // external repr - let new_price = if ask_ind == 1 { - ask_amount /= price_scale; - offer_amount / ask_amount - } else { - ask_amount / offer_amount - }; - - println!( - "Swap {} x[{}] for {} x[{}] by {new_price} price", - offer_amount, offer_ind, ask_amount, ask_ind - ); - - ext_xs[offer_ind] += offer_amount; - ext_xs[ask_ind] -= ask_amount; - - let ext_d = calc_d(ext_xs, amp_gamma).unwrap(); - let cur_d = calc_d(&xs, amp_gamma).unwrap(); - - println!("Internal: d {cur_d}",); - println!("External: d {ext_d}",); - - println!("After swap: {} {}", ext_xs[0], ext_xs[1]); - println!( - "After swap (internal): {} {}", - ext_xs[0], - ext_xs[1] * price_scale - ); - - new_price - } - - fn to_future(env: &mut Env, by_secs: u64) { - env.block.time = env.block.time.plus_seconds(by_secs) - } - - fn to_internal_repr(xs: &[Decimal256], price_scale: Decimal256) -> Vec { - vec![xs[0], xs[1] * price_scale] - } - - #[test] - fn check_repeg() { - let (amp, gamma) = (40f64, 0.000145); - let amp_gamma = AmpGamma { - amp: f64_to_dec(amp), - gamma: f64_to_dec(gamma), - }; - let mut env = mock_env(); - env.block.time = Timestamp::from_seconds(0); - - let pool_params = PoolParams { - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - ma_half_time: 600, - }; - - let mut pool_state = PoolState { - initial: AmpGamma::default(), - future: amp_gamma, - future_time: 0, - initial_time: 0, - price_state: PriceState { - oracle_price: f64_to_dec256(2f64), - last_price: f64_to_dec256(2f64), - price_scale: f64_to_dec256(2f64), - last_price_update: env.block.time.seconds(), - xcp_profit: Decimal256::one(), - xcp_profit_real: Decimal256::one(), - }, - }; - - to_future(&mut env, 1); - - // external repr - let mut ext_xs = [f64_to_dec256(1_000_000f64), f64_to_dec256(500_000f64)]; - let mut xs = ext_xs.to_vec(); - xs[1] *= pool_state.price_state.price_scale; - let cur_d = calc_d(&xs, &_gamma).unwrap(); - let total_lp = get_xcp(cur_d, pool_state.price_state.price_scale); - - let offer_amount = f64_to_dec256(1000_f64); - let price = swap( - &mut ext_xs, - offer_amount, - pool_state.price_state.price_scale, - 0, - &_gamma, - &pool_params, - ); - pool_state - .update_price( - &pool_params, - &env, - total_lp, - &to_internal_repr(&ext_xs, pool_state.price_state.price_scale), - price, - ) - .unwrap(); - - to_future(&mut env, 600); - - let offer_amount = f64_to_dec256(10000_f64); - let price = swap( - &mut ext_xs, - offer_amount, - pool_state.price_state.price_scale, - 0, - &_gamma, - &pool_params, - ); - pool_state - .update_price( - &pool_params, - &env, - total_lp, - &to_internal_repr(&ext_xs, pool_state.price_state.price_scale), - price, - ) - .unwrap(); - - to_future(&mut env, 600); - - let offer_amount = f64_to_dec256(200_000_f64); - let price = swap( - &mut ext_xs, - offer_amount, - pool_state.price_state.price_scale, - 0, - &_gamma, - &pool_params, - ); - pool_state - .update_price( - &pool_params, - &env, - total_lp, - &to_internal_repr(&ext_xs, pool_state.price_state.price_scale), - price, - ) - .unwrap(); - - to_future(&mut env, 12000); - - let offer_amount = f64_to_dec256(1_000_f64); - let price = swap( - &mut ext_xs, - offer_amount, - pool_state.price_state.price_scale, - 0, - &_gamma, - &pool_params, - ); - - pool_state - .update_price( - &pool_params, - &env, - total_lp, - &to_internal_repr(&ext_xs, pool_state.price_state.price_scale), - price, - ) - .unwrap(); - - to_future(&mut env, 600); - - let offer_amount = f64_to_dec256(200_000_f64); - let price = swap( - &mut ext_xs, - offer_amount, - pool_state.price_state.price_scale, - 1, - &_gamma, - &pool_params, - ); - - pool_state - .update_price( - &pool_params, - &env, - total_lp, - &to_internal_repr(&ext_xs, pool_state.price_state.price_scale), - price, - ) - .unwrap(); - - to_future(&mut env, 60); - - let offer_amount = f64_to_dec256(2_000_f64); - let price = swap( - &mut ext_xs, - offer_amount, - pool_state.price_state.price_scale, - 1, - &_gamma, - &pool_params, - ); - - pool_state - .update_price( - &pool_params, - &env, - total_lp, - &to_internal_repr(&ext_xs, pool_state.price_state.price_scale), - price, - ) - .unwrap(); - } -} diff --git a/contracts/pair_concentrated/src/utils.rs b/contracts/pair_concentrated/src/utils.rs index 4074a5b23..56d0d1c8a 100644 --- a/contracts/pair_concentrated/src/utils.rs +++ b/contracts/pair_concentrated/src/utils.rs @@ -1,193 +1,15 @@ -use cosmwasm_std::{ - to_binary, wasm_execute, Addr, Api, CosmosMsg, Decimal, Decimal256, Env, Fraction, - QuerierWrapper, StdError, StdResult, Storage, Uint128, Uint256, -}; -use cw20::Cw20ExecuteMsg; -use itertools::Itertools; +use cosmwasm_std::{Addr, Env, QuerierWrapper, StdResult, Storage, Uint128}; -use astroport::asset::{Asset, AssetInfo, DecimalAsset}; -use astroport::cosmwasm_ext::AbsDiff; -use astroport::observation::Observation; -use astroport::querier::{query_factory_config, query_supply}; +use astroport::asset::{Asset, DecimalAsset}; +use astroport::observation::{Observation, PrecommitObservation}; +use astroport::querier::query_supply; use astroport_circular_buffer::error::BufferResult; use astroport_circular_buffer::BufferManager; -use astroport_factory::state::pair_key; +use astroport_pcl_common::state::{Config, Precisions}; +use astroport_pcl_common::utils::{safe_sma_buffer_not_full, safe_sma_calculation}; -use crate::consts::{DEFAULT_SLIPPAGE, MAX_ALLOWED_SLIPPAGE, N, OFFER_PERCENT, TWO}; use crate::error::ContractError; -use crate::math::{calc_d, calc_y}; -use crate::state::{Config, PoolParams, Precisions, PriceState, OBSERVATIONS}; - -/// Helper function to check the given asset infos are valid. -pub(crate) fn check_asset_infos( - api: &dyn Api, - asset_infos: &[AssetInfo], -) -> Result<(), ContractError> { - if !asset_infos.iter().all_unique() { - return Err(ContractError::DoublingAssets {}); - } - - asset_infos - .iter() - .try_for_each(|asset_info| asset_info.check(api)) - .map_err(Into::into) -} - -/// Helper function to check that the assets in a given array are valid. -pub(crate) fn check_assets(api: &dyn Api, assets: &[Asset]) -> Result<(), ContractError> { - let asset_infos = assets.iter().map(|asset| asset.info.clone()).collect_vec(); - check_asset_infos(api, &asset_infos) -} - -/// Checks that cw20 token is part of the pool. -/// -/// * **cw20_sender** is cw20 token address which is being checked. -pub(crate) fn check_cw20_in_pool(config: &Config, cw20_sender: &Addr) -> Result<(), ContractError> { - for asset_info in &config.pair_info.asset_infos { - match asset_info { - AssetInfo::Token { contract_addr } if contract_addr == cw20_sender => return Ok(()), - _ => {} - } - } - - Err(ContractError::Unauthorized {}) -} - -/// Mint LP tokens for a beneficiary and auto stake the tokens in the Generator contract (if auto staking is specified). -/// -/// * **recipient** LP token recipient. -/// -/// * **amount** amount of LP tokens that will be minted for the recipient. -/// -/// * **auto_stake** determines whether the newly minted LP tokens will -/// be automatically staked in the Generator on behalf of the recipient. -pub(crate) fn mint_liquidity_token_message( - querier: QuerierWrapper, - config: &Config, - contract_address: &Addr, - recipient: &Addr, - amount: Uint128, - auto_stake: bool, -) -> Result, ContractError> { - let lp_token = &config.pair_info.liquidity_token; - - // If no auto-stake - just mint to recipient - if !auto_stake { - return Ok(vec![wasm_execute( - lp_token, - &Cw20ExecuteMsg::Mint { - recipient: recipient.to_string(), - amount, - }, - vec![], - )? - .into()]); - } - - // Mint for the pair contract and stake into the Generator contract - let generator = query_factory_config(&querier, &config.factory_addr)?.generator_address; - - if let Some(generator) = generator { - Ok(vec![ - wasm_execute( - lp_token, - &Cw20ExecuteMsg::Mint { - recipient: contract_address.to_string(), - amount, - }, - vec![], - )? - .into(), - wasm_execute( - lp_token, - &Cw20ExecuteMsg::Send { - contract: generator.to_string(), - amount, - msg: to_binary(&astroport::generator::Cw20HookMsg::DepositFor( - recipient.to_string(), - ))?, - }, - vec![], - )? - .into(), - ]) - } else { - Err(ContractError::AutoStakeError {}) - } -} - -/// Return the amount of tokens that a specific amount of LP tokens would withdraw. -/// -/// * **pools** assets available in the pool. -/// -/// * **amount** amount of LP tokens to calculate underlying amounts for. -/// -/// * **total_share** total amount of LP tokens currently issued by the pool. -pub(crate) fn get_share_in_assets( - pools: &[DecimalAsset], - amount: Uint128, - total_share: Uint128, -) -> Vec { - let share_ratio = if !total_share.is_zero() { - Decimal256::from_ratio(amount, total_share) - } else { - Decimal256::zero() - }; - - pools - .iter() - .map(|pool| DecimalAsset { - info: pool.info.clone(), - amount: pool.amount * share_ratio, - }) - .collect() -} - -/// If `belief_price` and `max_spread` are both specified, we compute a new spread, -/// otherwise we just use the swap spread to check `max_spread`. -/// -/// * **belief_price** belief price used in the swap. -/// -/// * **max_spread** max spread allowed so that the swap can be executed successfuly. -/// -/// * **offer_amount** amount of assets to swap. -/// -/// * **return_amount** amount of assets a user wants to receive from the swap. -/// -/// * **spread_amount** spread used in the swap. -pub(crate) fn assert_max_spread( - belief_price: Option, - max_spread: Option, - offer_amount: Uint128, - return_amount: Uint128, - spread_amount: Uint128, -) -> Result<(), ContractError> { - let max_spread = max_spread.map(Decimal256::from).unwrap_or(DEFAULT_SLIPPAGE); - if max_spread > MAX_ALLOWED_SLIPPAGE { - return Err(ContractError::AllowedSpreadAssertion {}); - } - - if let Some(belief_price) = belief_price { - let expected_return = offer_amount - * belief_price.inv().ok_or_else(|| { - ContractError::Std(StdError::generic_err( - "Invalid belief_price. Check the input values.", - )) - })?; - - let spread_amount = expected_return.saturating_sub(return_amount); - - if return_amount < expected_return - && Decimal256::from_ratio(spread_amount, expected_return) > max_spread - { - return Err(ContractError::MaxSpreadAssertion {}); - } - } else if Decimal256::from_ratio(spread_amount, return_amount + spread_amount) > max_spread { - return Err(ContractError::MaxSpreadAssertion {}); - } - - Ok(()) -} +use crate::state::OBSERVATIONS; /// Returns the total amount of assets in the pool as well as the total amount of LP tokens currently minted. pub(crate) fn pool_info( @@ -221,350 +43,110 @@ pub(crate) fn query_pools( .collect() } -/// Checks whether it possible to make a swap or not. -pub(crate) fn before_swap_check(pools: &[DecimalAsset], offer_amount: Decimal256) -> StdResult<()> { - if offer_amount.is_zero() { - return Err(StdError::generic_err("Swap amount must not be zero")); - } - if pools.iter().any(|a| a.amount.is_zero()) { - return Err(StdError::generic_err("One of the pools is empty")); - } - - Ok(()) -} - -/// This structure is for internal use only. Represents swap's result. -pub struct SwapResult { - pub dy: Decimal256, - pub spread_fee: Decimal256, - pub maker_fee: Decimal256, - pub total_fee: Decimal256, -} - -impl SwapResult { - /// Calculates and returns **last price** where: - /// - last_price is a price for repeg algo - pub fn calc_last_prices(&self, offer_amount: Decimal256, offer_ind: usize) -> Decimal256 { - if offer_ind == 0 { - offer_amount / (self.dy + self.maker_fee) - } else { - (self.dy + self.maker_fee) / offer_amount - } - } -} - -/// Performs swap simulation to calculate price. -pub fn calc_last_prices(xs: &[Decimal256], config: &Config, env: &Env) -> StdResult { - let mut offer_amount = Decimal256::one().min(xs[0] * OFFER_PERCENT); - if offer_amount.is_zero() { - offer_amount = Decimal256::raw(1u128); - } - - let last_price = compute_swap(xs, offer_amount, 1, config, env, Decimal256::zero())? - .calc_last_prices(offer_amount, 0); - - Ok(last_price) -} - -/// Calculate swap result. -pub fn compute_swap( - xs: &[Decimal256], - offer_amount: Decimal256, - ask_ind: usize, - config: &Config, - env: &Env, - maker_fee_share: Decimal256, -) -> StdResult { - let offer_ind = 1 ^ ask_ind; - - let mut ixs = xs.to_vec(); - ixs[1] *= config.pool_state.price_state.price_scale; - - let amp_gamma = config.pool_state.get_amp_gamma(env); - let d = calc_d(&ixs, &_gamma)?; - - let offer_amount = if offer_ind == 1 { - offer_amount * config.pool_state.price_state.price_scale - } else { - offer_amount - }; - - ixs[offer_ind] += offer_amount; - - let new_y = calc_y(&ixs, d, &_gamma, ask_ind)?; - let mut dy = ixs[ask_ind] - new_y; - ixs[ask_ind] = new_y; - - let price = if ask_ind == 1 { - dy /= config.pool_state.price_state.price_scale; - config.pool_state.price_state.price_scale.inv().unwrap() - } else { - config.pool_state.price_state.price_scale - }; - - // Since price_scale moves slower than real price spread fee may become negative - let spread_fee = (offer_amount * price).saturating_sub(dy); - - let fee_rate = config.pool_params.fee(&ixs); - let total_fee = fee_rate * dy; - dy -= total_fee; - - Ok(SwapResult { - dy, - spread_fee, - maker_fee: total_fee * maker_fee_share, - total_fee, - }) -} - -/// Returns an amount of offer assets for a specified amount of ask assets. -pub fn compute_offer_amount( - xs: &[Decimal256], - mut want_amount: Decimal256, - ask_ind: usize, - config: &Config, - env: &Env, -) -> StdResult<(Decimal256, Decimal256, Decimal256)> { - let offer_ind = 1 ^ ask_ind; - - if ask_ind == 1 { - want_amount *= config.pool_state.price_state.price_scale - } - - let mut ixs = xs.to_vec(); - ixs[1] *= config.pool_state.price_state.price_scale; - - let amp_gamma = config.pool_state.get_amp_gamma(env); - let d = calc_d(&ixs, &_gamma)?; - - // It's hard to predict fee rate thus we use maximum possible fee rate - let before_fee = want_amount - * (Decimal256::one() - Decimal256::from(config.pool_params.out_fee)) - .inv() - .unwrap(); - let mut fee = before_fee - want_amount; - - ixs[ask_ind] -= before_fee; - - let new_y = calc_y(&ixs, d, &_gamma, offer_ind)?; - let mut dy = new_y - ixs[offer_ind]; - - let mut spread_fee = dy.saturating_sub(before_fee); - if offer_ind == 1 { - dy /= config.pool_state.price_state.price_scale; - spread_fee /= config.pool_state.price_state.price_scale; - fee /= config.pool_state.price_state.price_scale; - } - - Ok((dy, spread_fee, fee)) -} - -/// Calculate provide fee applied on the amount of LP tokens. Only charged for imbalanced provide. -/// * `deposits` - internal repr of deposit -/// * `xp` - internal repr of pools -pub fn calc_provide_fee( - deposits: &[Decimal256], - xp: &[Decimal256], - params: &PoolParams, -) -> Decimal256 { - let sum = deposits[0] + deposits[1]; - let avg = sum / N; - - deposits[0].diff(avg) * params.fee(xp) / sum -} - -/// This is an internal function that enforces slippage tolerance for provides. Returns actual slippage. -pub fn assert_slippage_tolerance( - deposits: &[Decimal256], - actual_share: Decimal256, - price_state: &PriceState, - slippage_tolerance: Option, -) -> Result { - let slippage_tolerance = slippage_tolerance - .map(Into::into) - .unwrap_or(DEFAULT_SLIPPAGE); - if slippage_tolerance > MAX_ALLOWED_SLIPPAGE { - return Err(ContractError::AllowedSpreadAssertion {}); - } - - let deposit_value = deposits[0] + deposits[1] * price_state.price_scale; - let lp_expected = (deposit_value / TWO * deposit_value / (TWO * price_state.price_scale)) - .sqrt() - / price_state.xcp_profit_real; - let slippage = lp_expected.saturating_sub(actual_share) / lp_expected; - - if slippage > slippage_tolerance { - return Err(ContractError::MaxSpreadAssertion {}); - } - - Ok(slippage) -} - -// Checks whether the pair is registered in the factory or not. -pub fn check_pair_registered( - querier: QuerierWrapper, - factory: &Addr, - asset_infos: &[AssetInfo], -) -> StdResult { - astroport_factory::state::PAIRS - .query(&querier, factory.clone(), &pair_key(asset_infos)) - .map(|inner| inner.is_some()) -} - -/// Internal function to calculate new moving average using Uint256. -/// Overflow is possible only if new average order size is greater than 2^128 - 1 which is unlikely. -fn safe_sma_calculation( - sma: Uint128, - oldest_amount: Uint128, - count: u32, - new_amount: Uint128, -) -> StdResult { - let res = (sma.full_mul(count) + Uint256::from(new_amount) - Uint256::from(oldest_amount)) - .checked_div(count.into())?; - res.try_into().map_err(StdError::from) -} - /// Calculate and save moving averages of swap sizes. -pub fn accumulate_swap_sizes( - storage: &mut dyn Storage, - env: &Env, - base_amount: Uint128, - quote_amount: Uint128, -) -> BufferResult<()> { - let mut buffer = BufferManager::new(storage, OBSERVATIONS)?; - - let new_observation; - if let Some(last_obs) = buffer.read_last(storage)? { - // Since this is circular buffer the next index contains the oldest value - let count = buffer.capacity(); - if let Some(oldest_obs) = buffer.read_single(storage, buffer.head() + 1)? { - let new_base_sma = safe_sma_calculation( - last_obs.base_sma, - oldest_obs.base_amount, - count, - base_amount, - )?; - let new_quote_sma = safe_sma_calculation( - last_obs.quote_sma, - oldest_obs.quote_amount, - count, - quote_amount, - )?; - new_observation = Observation { - base_amount, - quote_amount, - base_sma: new_base_sma, - quote_sma: new_quote_sma, - timestamp: env.block.time.seconds(), - }; +pub fn accumulate_swap_sizes(storage: &mut dyn Storage, env: &Env) -> BufferResult<()> { + if let Some(PrecommitObservation { + base_amount, + quote_amount, + precommit_ts, + }) = PrecommitObservation::may_load(storage)? + { + let mut buffer = BufferManager::new(storage, OBSERVATIONS)?; + + let new_observation; + if let Some(last_obs) = buffer.read_last(storage)? { + // Skip saving observation if it has been already saved + if last_obs.timestamp < precommit_ts { + // Since this is circular buffer the next index contains the oldest value + let count = buffer.capacity(); + if let Some(oldest_obs) = buffer.read_single(storage, buffer.head() + 1)? { + let new_base_sma = safe_sma_calculation( + last_obs.base_sma, + oldest_obs.base_amount, + count, + base_amount, + )?; + let new_quote_sma = safe_sma_calculation( + last_obs.quote_sma, + oldest_obs.quote_amount, + count, + quote_amount, + )?; + new_observation = Observation { + base_amount, + quote_amount, + base_sma: new_base_sma, + quote_sma: new_quote_sma, + timestamp: precommit_ts, + }; + } else { + // Buffer is not full yet + let count = buffer.head(); + let base_sma = safe_sma_buffer_not_full(last_obs.base_sma, count, base_amount)?; + let quote_sma = + safe_sma_buffer_not_full(last_obs.quote_sma, count, quote_amount)?; + new_observation = Observation { + base_amount, + quote_amount, + base_sma, + quote_sma, + timestamp: precommit_ts, + }; + } + + buffer.instant_push(storage, &new_observation)? + } } else { - // Buffer is not full yet - let count = Uint128::from(buffer.head()); - let new_base_sma = (last_obs.base_sma * count + base_amount) / (count + Uint128::one()); - let new_quote_sma = - (last_obs.quote_sma * count + quote_amount) / (count + Uint128::one()); - new_observation = Observation { - base_amount, - quote_amount, - base_sma: new_base_sma, - quote_sma: new_quote_sma, - timestamp: env.block.time.seconds(), - }; + // Buffer is empty + if env.block.time.seconds() > precommit_ts { + new_observation = Observation { + timestamp: precommit_ts, + base_sma: base_amount, + base_amount, + quote_sma: quote_amount, + quote_amount, + }; + + buffer.instant_push(storage, &new_observation)? + } } - } else { - // Buffer is empty - new_observation = Observation { - timestamp: env.block.time.seconds(), - base_sma: base_amount, - base_amount, - quote_sma: quote_amount, - quote_amount, - }; } - buffer.instant_push(storage, &new_observation) + Ok(()) } #[cfg(test)] mod tests { - use std::error::Error; - use std::fmt::Display; - use std::str::FromStr; - use cosmwasm_std::testing::{mock_env, MockStorage}; + use cosmwasm_std::{BlockInfo, Timestamp}; use super::*; - pub fn f64_to_dec(val: f64) -> T - where - T: FromStr, - T::Err: Error, - { - T::from_str(&val.to_string()).unwrap() - } - - pub fn dec_to_f64(val: impl Display) -> f64 { - f64::from_str(&val.to_string()).unwrap() - } - - #[test] - fn test_provide_fees() { - let params = PoolParams { - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - ..PoolParams::default() - }; - - let fee_rate = calc_provide_fee( - &[f64_to_dec(50_000f64), f64_to_dec(50_000f64)], - &[f64_to_dec(100_000f64), f64_to_dec(100_000f64)], - ¶ms, - ); - assert_eq!(dec_to_f64(fee_rate), 0.0); - - let fee_rate = calc_provide_fee( - &[f64_to_dec(99_000f64), f64_to_dec(1_000f64)], - &[f64_to_dec(100_000f64), f64_to_dec(100_000f64)], - ¶ms, - ); - assert_eq!(dec_to_f64(fee_rate), 0.001274); - - let fee_rate = calc_provide_fee( - &[f64_to_dec(99_000f64), f64_to_dec(1_000f64)], - &[f64_to_dec(1_000f64), f64_to_dec(99_000f64)], - ¶ms, - ); - assert_eq!(dec_to_f64(fee_rate), 0.002205); - } - #[test] - fn test_swap_obeservations() { + fn test_swap_observations() { let mut store = MockStorage::new(); - let env = mock_env(); + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(1); + + let next_block = |block: &mut BlockInfo| { + block.height += 1; + block.time = block.time.plus_seconds(1); + }; BufferManager::init(&mut store, OBSERVATIONS, 10).unwrap(); - for _ in 0..50 { - accumulate_swap_sizes( - &mut store, - &env, - Uint128::from(1000u128), - Uint128::from(500u128), - ) - .unwrap(); + for _ in 0..=50 { + accumulate_swap_sizes(&mut store, &env).unwrap(); + PrecommitObservation::save(&mut store, &env, 1000u128.into(), 500u128.into()).unwrap(); + next_block(&mut env.block); } let buffer = BufferManager::new(&store, OBSERVATIONS).unwrap(); + let obs = buffer.read_last(&store).unwrap().unwrap(); + assert_eq!(obs.timestamp, 50); assert_eq!(buffer.head(), 0); - assert_eq!( - buffer.read_last(&store).unwrap().unwrap().base_sma.u128(), - 1000u128 - ); - assert_eq!( - buffer.read_last(&store).unwrap().unwrap().quote_sma.u128(), - 500u128 - ); + assert_eq!(obs.base_sma.u128(), 1000u128); + assert_eq!(obs.quote_sma.u128(), 500u128); } } diff --git a/contracts/pair_concentrated/tests/helper.rs b/contracts/pair_concentrated/tests/helper.rs index cc8f87dfb..3a13b4dee 100644 --- a/contracts/pair_concentrated/tests/helper.rs +++ b/contracts/pair_concentrated/tests/helper.rs @@ -20,21 +20,37 @@ use astroport::asset::{native_asset_info, token_asset_info, Asset, AssetInfo, Pa use astroport::factory::{PairConfig, PairType}; use astroport::observation::OracleObservation; use astroport::pair::{ - ConfigResponse, CumulativePricesResponse, Cw20HookMsg, ExecuteMsg, ReverseSimulationResponse, - SimulationResponse, + ConfigResponse, CumulativePricesResponse, Cw20HookMsg, ExecuteMsg, PoolResponse, + ReverseSimulationResponse, SimulationResponse, }; use astroport::pair_concentrated::{ - ConcentratedPoolParams, ConcentratedPoolUpdateParams, QueryMsg, + ConcentratedPoolConfig, ConcentratedPoolParams, ConcentratedPoolUpdateParams, QueryMsg, }; use astroport_mocks::cw_multi_test::{App, AppResponse, Contract, ContractWrapper, Executor}; use astroport_pair_concentrated::contract::{execute, instantiate, reply}; use astroport_pair_concentrated::queries::query; -use astroport_pair_concentrated::state::Config; +use astroport_pcl_common::state::Config; const NATIVE_TOKEN_PRECISION: u8 = 6; const INIT_BALANCE: u128 = 1_000_000_000000; +pub fn common_pcl_params() -> ConcentratedPoolParams { + ConcentratedPoolParams { + amp: f64_to_dec(40f64), + gamma: f64_to_dec(0.000145), + mid_fee: f64_to_dec(0.0026), + out_fee: f64_to_dec(0.0045), + fee_gamma: f64_to_dec(0.00023), + repeg_profit_threshold: f64_to_dec(0.000002), + min_price_scale_delta: f64_to_dec(0.000146), + price_scale: Decimal::one(), + ma_half_time: 600, + track_asset_balances: None, + fee_share: None, + } +} + #[cw_serde] pub struct AmpGammaResponse { pub amp: Decimal, @@ -307,6 +323,16 @@ impl Helper { sender: &Addr, offer_asset: &Asset, max_spread: Option, + ) -> AnyResult { + self.swap_full_params(sender, offer_asset, max_spread, None) + } + + pub fn swap_full_params( + &mut self, + sender: &Addr, + offer_asset: &Asset, + max_spread: Option, + belief_price: Option, ) -> AnyResult { match &offer_asset.info { AssetInfo::Token { contract_addr } => { @@ -315,7 +341,7 @@ impl Helper { amount: offer_asset.amount, msg: to_binary(&Cw20HookMsg::Swap { ask_asset_info: None, - belief_price: None, + belief_price, max_spread, to: None, }) @@ -336,7 +362,7 @@ impl Helper { let msg = ExecuteMsg::Swap { offer_asset: offer_asset.clone(), ask_asset_info: None, - belief_price: None, + belief_price, max_spread, to: None, }; @@ -458,6 +484,12 @@ impl Helper { from_slice(&binary) } + pub fn query_pool(&self) -> StdResult { + self.app + .wrap() + .query_wasm_smart(&self.pair_addr, &QueryMsg::Pool {}) + } + pub fn query_lp_price(&self) -> StdResult { self.app .wrap() @@ -498,7 +530,7 @@ impl Helper { .app .wrap() .query_wasm_smart(&self.pair_addr, &QueryMsg::Config {})?; - let params: ConcentratedPoolParams = from_slice( + let params: ConcentratedPoolConfig = from_slice( &config_resp .params .ok_or_else(|| StdError::generic_err("Params not found in config response!"))?, diff --git a/contracts/pair_concentrated/tests/pair_concentrated_integration.rs b/contracts/pair_concentrated/tests/pair_concentrated_integration.rs index e331911f4..567b743a3 100644 --- a/contracts/pair_concentrated/tests/pair_concentrated_integration.rs +++ b/contracts/pair_concentrated/tests/pair_concentrated_integration.rs @@ -1,27 +1,28 @@ #![cfg(not(tarpaulin_include))] -use astroport_mocks::{astroport_address, MockConcentratedPairBuilder, MockGeneratorBuilder}; -use cosmwasm_std::{Addr, Coin, Decimal, StdError, Uint128}; - -use astroport_mocks::cw_multi_test::{BasicApp, Executor}; use std::cell::RefCell; use std::rc::Rc; use std::str::FromStr; +use cosmwasm_std::{Addr, Coin, Decimal, Decimal256, StdError, Uint128}; +use itertools::{max, Itertools}; + use astroport::asset::{ native_asset_info, Asset, AssetInfo, AssetInfoExt, MINIMUM_LIQUIDITY_AMOUNT, }; -use astroport::cosmwasm_ext::AbsDiff; +use astroport::cosmwasm_ext::{AbsDiff, IntegerToDecimal}; use astroport::observation::OracleObservation; - -use astroport::pair::{ExecuteMsg, PoolResponse}; +use astroport::pair::{ExecuteMsg, PoolResponse, MAX_FEE_SHARE_BPS}; use astroport::pair_concentrated::{ ConcentratedPoolParams, ConcentratedPoolUpdateParams, PromoteParams, QueryMsg, UpdatePoolParams, }; -use astroport_pair_concentrated::consts::{AMP_MAX, AMP_MIN, MA_HALF_TIME_LIMITS}; +use astroport_mocks::cw_multi_test::{BasicApp, Executor}; +use astroport_mocks::{astroport_address, MockConcentratedPairBuilder, MockGeneratorBuilder}; use astroport_pair_concentrated::error::ContractError; +use astroport_pcl_common::consts::{AMP_MAX, AMP_MIN, MA_HALF_TIME_LIMITS}; +use astroport_pcl_common::error::PclError; -use crate::helper::{dec_to_f64, f64_to_dec, AppExtension, Helper, TestCoin}; +use crate::helper::{common_pcl_params, dec_to_f64, f64_to_dec, AppExtension, Helper, TestCoin}; mod helper; @@ -31,19 +32,7 @@ fn check_observe_queries() { let test_coins = vec![TestCoin::native("uluna"), TestCoin::cw20("USDC")]; - let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::one(), - ma_half_time: 600, - track_asset_balances: None, - }; - let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); + let mut helper = Helper::new(&owner, test_coins.clone(), common_pcl_params()).unwrap(); let user = Addr::unchecked("user"); let offer_asset = helper.assets[&test_coins[0]].with_balance(100_000000u128); @@ -88,7 +77,7 @@ fn check_observe_queries() { res, OracleObservation { timestamp: helper.app.block_info().time.seconds(), - price: Decimal::from_str("0.99741246").unwrap() + price: Decimal::from_str("1.002627596167552265").unwrap() } ); } @@ -98,16 +87,8 @@ fn check_wrong_initialization() { let owner = Addr::unchecked("owner"); let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), price_scale: Decimal::from_ratio(2u8, 1u8), - ma_half_time: 600, - track_asset_balances: None, + ..common_pcl_params() }; let err = Helper::new(&owner, vec![TestCoin::native("uluna")], params.clone()).unwrap_err(); @@ -128,11 +109,11 @@ fn check_wrong_initialization() { .unwrap_err(); assert_eq!( - ContractError::IncorrectPoolParam( + ContractError::PclError(PclError::IncorrectPoolParam( "amp".to_string(), AMP_MIN.to_string(), AMP_MAX.to_string() - ), + )), err.downcast().unwrap(), ); @@ -147,11 +128,11 @@ fn check_wrong_initialization() { .unwrap_err(); assert_eq!( - ContractError::IncorrectPoolParam( + ContractError::PclError(PclError::IncorrectPoolParam( "ma_half_time".to_string(), MA_HALF_TIME_LIMITS.start().to_string(), MA_HALF_TIME_LIMITS.end().to_string() - ), + )), err.downcast().unwrap(), ); @@ -187,16 +168,8 @@ fn check_create_pair_with_unsupported_denom() { let valid_coins = vec![TestCoin::native("uluna"), TestCoin::cw20("USDC")]; let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), price_scale: Decimal::from_ratio(2u8, 1u8), - ma_half_time: 600, - track_asset_balances: None, + ..common_pcl_params() }; let err = Helper::new(&owner, wrong_coins.clone(), params.clone()).unwrap_err(); @@ -215,16 +188,8 @@ fn provide_and_withdraw() { let test_coins = vec![TestCoin::native("uluna"), TestCoin::cw20("USDC")]; let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), price_scale: Decimal::from_ratio(2u8, 1u8), - ma_half_time: 600, - track_asset_balances: None, + ..common_pcl_params() }; let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); @@ -266,7 +231,7 @@ fn provide_and_withdraw() { ); let err = helper - .provide_liquidity(&user1, &[random_coin]) + .provide_liquidity(&user1, &[random_coin.clone()]) .unwrap_err(); assert_eq!( "The asset random-coin does not belong to the pair", @@ -279,6 +244,22 @@ fn provide_and_withdraw() { err.root_cause().to_string() ); + // Try to provide 3 assets + let err = helper + .provide_liquidity( + &user1, + &[ + random_coin.clone(), + helper.assets[&test_coins[0]].with_balance(1u8), + helper.assets[&test_coins[1]].with_balance(1u8), + ], + ) + .unwrap_err(); + assert_eq!( + ContractError::InvalidNumberOfAssets(2), + err.downcast().unwrap() + ); + // Try to provide with zero amount let err = helper .provide_liquidity( @@ -299,6 +280,23 @@ fn provide_and_withdraw() { &[helper.assets[&test_coins[1]].with_balance(50_000_000000u128)], &user1, ); + + // Test very small initial provide + let err = helper + .provide_liquidity( + &user1, + &[ + helper.assets[&test_coins[0]].with_balance(1000u128), + helper.assets[&test_coins[1]].with_balance(500u128), + ], + ) + .unwrap_err(); + assert_eq!( + ContractError::MinimumLiquidityAmountError {}, + err.downcast().unwrap() + ); + + // This is normal provision helper.provide_liquidity(&user1, &assets).unwrap(); assert_eq!(70710_677118, helper.token_balance(&helper.lp_token, &user1)); @@ -403,16 +401,8 @@ fn check_imbalanced_provide() { let test_coins = vec![TestCoin::native("uluna"), TestCoin::cw20("USDC")]; let mut params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), price_scale: Decimal::from_ratio(2u8, 1u8), - ma_half_time: 600, - track_asset_balances: None, + ..common_pcl_params() }; let mut helper = Helper::new(&owner, test_coins.clone(), params.clone()).unwrap(); @@ -468,20 +458,7 @@ fn provide_with_different_precision() { TestCoin::cw20precise("BAR", 6), ]; - let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::one(), - ma_half_time: 600, - track_asset_balances: None, - }; - - let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); + let mut helper = Helper::new(&owner, test_coins.clone(), common_pcl_params()).unwrap(); let assets = vec![ helper.assets[&test_coins[0]].with_balance(100_00000u128), @@ -530,20 +507,7 @@ fn swap_different_precisions() { TestCoin::cw20precise("BAR", 6), ]; - let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::one(), - ma_half_time: 600, - track_asset_balances: None, - }; - - let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); + let mut helper = Helper::new(&owner, test_coins.clone(), common_pcl_params()).unwrap(); let assets = vec![ helper.assets[&test_coins[0]].with_balance(100_000_00000u128), @@ -586,20 +550,7 @@ fn check_reverse_swap() { let test_coins = vec![TestCoin::cw20("FOO"), TestCoin::cw20("BAR")]; - let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::one(), - ma_half_time: 600, - track_asset_balances: None, - }; - - let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); + let mut helper = Helper::new(&owner, test_coins.clone(), common_pcl_params()).unwrap(); let assets = vec![ helper.assets[&test_coins[0]].with_balance(100_000_000000u128), @@ -627,19 +578,7 @@ fn check_swaps_simple() { let test_coins = vec![TestCoin::native("uluna"), TestCoin::cw20("USDC")]; - let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::one(), - ma_half_time: 600, - track_asset_balances: None, - }; - let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); + let mut helper = Helper::new(&owner, test_coins.clone(), common_pcl_params()).unwrap(); let user = Addr::unchecked("user"); let offer_asset = helper.assets[&test_coins[0]].with_balance(100_000000u128); @@ -668,6 +607,24 @@ fn check_swaps_simple() { ]; helper.provide_liquidity(&owner, &assets).unwrap(); + // trying to swap cw20 without calling Cw20::Send method + let err = helper + .app + .execute_contract( + owner.clone(), + helper.pair_addr.clone(), + &ExecuteMsg::Swap { + offer_asset: helper.assets[&test_coins[1]].with_balance(1u8), + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: None, + }, + &[], + ) + .unwrap_err(); + assert_eq!(ContractError::Cw20DirectSwap {}, err.downcast().unwrap()); + let d = helper.query_d().unwrap(); assert_eq!(dec_to_f64(d), 200000f64); @@ -680,7 +637,7 @@ fn check_swaps_simple() { helper.give_me_money(&[offer_asset.clone()], &user); let err = helper.swap(&user, &offer_asset, None).unwrap_err(); assert_eq!( - ContractError::MaxSpreadAssertion {}, + ContractError::PclError(PclError::MaxSpreadAssertion {}), err.downcast().unwrap() ); @@ -733,19 +690,7 @@ fn check_swaps_with_price_update() { let test_coins = vec![TestCoin::native("uluna"), TestCoin::cw20("USDC")]; - let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::one(), - ma_half_time: 600, - track_asset_balances: None, - }; - let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); + let mut helper = Helper::new(&owner, test_coins.clone(), common_pcl_params()).unwrap(); helper.app.next_block(1000); @@ -787,19 +732,7 @@ fn provides_and_swaps() { let test_coins = vec![TestCoin::native("uluna"), TestCoin::cw20("USDC")]; - let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::one(), - ma_half_time: 600, - track_asset_balances: None, - }; - let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); + let mut helper = Helper::new(&owner, test_coins.clone(), common_pcl_params()).unwrap(); helper.app.next_block(1000); @@ -846,14 +779,7 @@ fn check_amp_gamma_change() { let params = ConcentratedPoolParams { amp: f64_to_dec(40f64), gamma: f64_to_dec(0.0001), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::one(), - ma_half_time: 600, - track_asset_balances: None, + ..common_pcl_params() }; let mut helper = Helper::new(&owner, test_coins, params).unwrap(); @@ -940,20 +866,7 @@ fn check_prices() { let test_coins = vec![TestCoin::native("uusd"), TestCoin::cw20("USDX")]; - let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::one(), - ma_half_time: 600, - track_asset_balances: None, - }; - - let helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); + let helper = Helper::new(&owner, test_coins.clone(), common_pcl_params()).unwrap(); let err = helper.query_prices().unwrap_err(); assert_eq!(StdError::generic_err("Querier contract error: Generic error: Not implemented.Use { \"observe\" : { \"seconds_ago\" : ... } } instead.") , err); @@ -965,20 +878,7 @@ fn update_owner() { let test_coins = vec![TestCoin::native("uusd"), TestCoin::cw20("USDX")]; - let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::one(), - ma_half_time: 600, - track_asset_balances: None, - }; - - let mut helper = Helper::new(&owner, test_coins, params).unwrap(); + let mut helper = Helper::new(&owner, test_coins, common_pcl_params()).unwrap(); let new_owner = String::from("new_owner"); @@ -1038,6 +938,39 @@ fn update_owner() { .unwrap_err(); assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); + // Drop ownership proposal + let err = helper + .app + .execute_contract( + Addr::unchecked("invalid_addr"), + helper.pair_addr.clone(), + &ExecuteMsg::DropOwnershipProposal {}, + &[], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); + + helper + .app + .execute_contract( + helper.owner.clone(), + helper.pair_addr.clone(), + &ExecuteMsg::DropOwnershipProposal {}, + &[], + ) + .unwrap(); + + // Propose new owner + helper + .app + .execute_contract( + Addr::unchecked(&helper.owner), + helper.pair_addr.clone(), + &msg, + &[], + ) + .unwrap(); + // Claim ownership helper .app @@ -1057,20 +990,9 @@ fn update_owner() { fn query_d_test() { let owner = Addr::unchecked("owner"); let test_coins = vec![TestCoin::native("uusd"), TestCoin::cw20("USDX")]; - let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::one(), - ma_half_time: 600, - track_asset_balances: None, - }; + // create pair with test_coins - let helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); + let helper = Helper::new(&owner, test_coins.clone(), common_pcl_params()).unwrap(); // query current pool D value before providing any liquidity let err = helper.query_d().unwrap_err(); @@ -1086,21 +1008,8 @@ fn asset_balances_tracking_without_in_params() { let user1 = Addr::unchecked("user1"); let test_coins = vec![TestCoin::native("uluna"), TestCoin::native("uusd")]; - let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::one(), - ma_half_time: 600, - track_asset_balances: None, - }; - // Instantiate pair without asset balances tracking - let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); + let mut helper = Helper::new(&owner, test_coins.clone(), common_pcl_params()).unwrap(); let assets = vec![ helper.assets[&test_coins[0]].with_balance(5_000000u128), @@ -1174,16 +1083,8 @@ fn asset_balances_tracking_with_in_params() { let test_coins = vec![TestCoin::native("uluna"), TestCoin::native("uusd")]; let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::one(), - ma_half_time: 600, track_asset_balances: Some(true), + ..common_pcl_params() }; // Instantiate pair without asset balances tracking @@ -1321,16 +1222,8 @@ fn provides_and_swaps_and_withdraw() { let test_coins = vec![TestCoin::native("uluna"), TestCoin::cw20("USDC")]; let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), price_scale: Decimal::from_ratio(1u8, 2u8), - ma_half_time: 600, - track_asset_balances: None, + ..common_pcl_params() }; let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); @@ -1438,15 +1331,8 @@ fn provide_withdraw_provide() { let params = ConcentratedPoolParams { amp: f64_to_dec(10f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), price_scale: Decimal::from_ratio(10u8, 1u8), - ma_half_time: 600, - track_asset_balances: None, + ..common_pcl_params() }; let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); @@ -1484,15 +1370,8 @@ fn provide_withdraw_slippage() { let params = ConcentratedPoolParams { amp: f64_to_dec(10f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), price_scale: Decimal::from_ratio(10u8, 1u8), - ma_half_time: 600, - track_asset_balances: None, + ..common_pcl_params() }; let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); @@ -1515,7 +1394,7 @@ fn provide_withdraw_slippage() { .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.02))) .unwrap_err(); assert_eq!( - ContractError::MaxSpreadAssertion {}, + ContractError::PclError(PclError::MaxSpreadAssertion {}), err.downcast().unwrap(), ); // With 3% slippage it should work @@ -1532,10 +1411,340 @@ fn provide_withdraw_slippage() { .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.02))) .unwrap_err(); assert_eq!( - ContractError::MaxSpreadAssertion {}, + ContractError::PclError(PclError::MaxSpreadAssertion {}), err.downcast().unwrap(), ); helper .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.5))) .unwrap(); } + +#[test] +fn test_frontrun_before_initial_provide() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![TestCoin::native("uusd"), TestCoin::native("uluna")]; + + let params = ConcentratedPoolParams { + amp: f64_to_dec(10f64), + price_scale: Decimal::from_ratio(10u8, 1u8), + ..common_pcl_params() + }; + + let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); + + // Random person tries to frontrun initial provide and imbalance pool upfront + helper + .app + .send_tokens( + owner.clone(), + helper.pair_addr.clone(), + &[helper.assets[&test_coins[0]] + .with_balance(10_000_000000u128) + .as_coin() + .unwrap()], + ) + .unwrap(); + + // Fully balanced provide + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(10_000000u128), + helper.assets[&test_coins[1]].with_balance(1_000000u128), + ]; + helper.provide_liquidity(&owner, &assets).unwrap(); + // Now pool became imbalanced with value (10010, 1) (or in internal representation (10010, 10)) + // while price scale stays 10 + + let arber = Addr::unchecked("arber"); + let offer_asset_luna = helper.assets[&test_coins[1]].with_balance(1_000000u128); + // Arber spinning pool back to balanced state + loop { + helper.app.next_block(10); + helper.give_me_money(&[offer_asset_luna.clone()], &arber); + // swapping until price satisfies an arber + if helper + .swap_full_params( + &arber, + &offer_asset_luna, + Some(f64_to_dec(0.02)), + Some(f64_to_dec(0.1)), // imagine market price is 10 -> i.e. inverted price is 1/10 + ) + .is_err() + { + break; + } + } + + // price scale changed, however it isn't equal to 10 because of repegging + // But next swaps will align price back to the market value + let config = helper.query_config().unwrap(); + let price_scale = config.pool_state.price_state.price_scale; + assert!( + dec_to_f64(price_scale) - 77.255853 < 1e-5, + "price_scale: {price_scale} is far from expected price", + ); + + // Arber collected significant profit (denominated in uusd) + // Essentially 10_000 - fees (which settled in the pool) + let arber_balance = helper.coin_balance(&test_coins[0], &arber); + assert_eq!(arber_balance, 9667_528248); + + // Pool's TVL increased from (10, 1) i.e. 20 to (320, 32) i.e. 640 considering market price is 10.0 + let pools = config + .pair_info + .query_pools(&helper.app.wrap(), &helper.pair_addr) + .unwrap(); + assert_eq!(pools[0].amount.u128(), 320_624088); + assert_eq!(pools[1].amount.u128(), 32_000000); +} + +#[test] +fn check_correct_fee_share() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![TestCoin::native("uluna"), TestCoin::cw20("USDC")]; + + let mut helper = Helper::new(&owner, test_coins.clone(), common_pcl_params()).unwrap(); + + let share_recipient = Addr::unchecked("share_recipient"); + // Attempt setting fee share with max+1 fee share + let action = ConcentratedPoolUpdateParams::EnableFeeShare { + fee_share_bps: MAX_FEE_SHARE_BPS + 1, + fee_share_address: share_recipient.to_string(), + }; + let err = helper.update_config(&owner, &action).unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::FeeShareOutOfBounds {} + ); + + // Attempt setting fee share with max+1 fee share + let action = ConcentratedPoolUpdateParams::EnableFeeShare { + fee_share_bps: 0, + fee_share_address: share_recipient.to_string(), + }; + let err = helper.update_config(&owner, &action).unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::FeeShareOutOfBounds {} + ); + + helper.app.next_block(1000); + + // Set to 5% fee share + let action = ConcentratedPoolUpdateParams::EnableFeeShare { + fee_share_bps: 1000, + fee_share_address: share_recipient.to_string(), + }; + helper.update_config(&owner, &action).unwrap(); + + let config = helper.query_config().unwrap(); + let fee_share = config.fee_share.unwrap(); + assert_eq!(fee_share.bps, 1000u16); + assert_eq!(fee_share.recipient, share_recipient.to_string()); + + helper.app.next_block(1000); + + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(100_000_000000u128), + helper.assets[&test_coins[1]].with_balance(100_000_000000u128), + ]; + helper.provide_liquidity(&owner, &assets).unwrap(); + + helper.app.next_block(1000); + + let user = Addr::unchecked("user"); + let offer_asset = helper.assets[&test_coins[0]].with_balance(100_000000u128); + helper.give_me_money(&[offer_asset.clone()], &user); + helper.swap(&user, &offer_asset, None).unwrap(); + + // Check that the shared fees are sent + let expected_fee_share = 26081u128; + let recipient_balance = helper.coin_balance(&test_coins[1], &share_recipient); + assert_eq!(recipient_balance, expected_fee_share); + + let provider = Addr::unchecked("provider"); + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(1_000_000000u128), + helper.assets[&test_coins[1]].with_balance(1_000_000000u128), + ]; + helper.give_me_money(&assets, &provider); + helper.provide_liquidity(&provider, &assets).unwrap(); + + let offer_asset = helper.assets[&test_coins[1]].with_balance(100_000000u128); + helper.give_me_money(&[offer_asset.clone()], &user); + helper.swap(&user, &offer_asset, None).unwrap(); + + helper + .withdraw_liquidity(&provider, 999_999354, vec![]) + .unwrap(); + + let offer_asset = helper.assets[&test_coins[0]].with_balance(100_000000u128); + helper.give_me_money(&[offer_asset.clone()], &user); + helper.swap(&user, &offer_asset, None).unwrap(); + + // Disable fee share + let action = ConcentratedPoolUpdateParams::DisableFeeShare {}; + helper.update_config(&owner, &action).unwrap(); + + let config = helper.query_config().unwrap(); + assert!(config.fee_share.is_none()); +} + +#[test] +fn check_small_trades() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![TestCoin::native("uusd"), TestCoin::native("uluna")]; + + let params = ConcentratedPoolParams { + price_scale: f64_to_dec(4.360000915600192), + ..common_pcl_params() + }; + + let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); + + // Fully balanced but small provide + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(8_000000u128), + helper.assets[&test_coins[1]].with_balance(1_834862u128), + ]; + helper.provide_liquidity(&owner, &assets).unwrap(); + + // Trying to mess the last price with lowest possible swap + for _ in 0..1000 { + helper.app.next_block(30); + let offer_asset = helper.assets[&test_coins[1]].with_balance(1u8); + helper + .swap_full_params(&owner, &offer_asset, None, Some(Decimal::MAX)) + .unwrap(); + } + + // Check that after price scale adjustments (even they are small) internal value is still nearly balanced + let config = helper.query_config().unwrap(); + let pool = helper + .query_pool() + .unwrap() + .assets + .into_iter() + .map(|asset| asset.amount.to_decimal256(6u8).unwrap()) + .collect_vec(); + + let ixs = [pool[0], pool[1] * config.pool_state.price_state.price_scale]; + let relative_diff = ixs[0].abs_diff(ixs[1]) / max(&ixs).unwrap(); + + assert!( + relative_diff < Decimal256::percent(3), + "Internal PCL value is off. Relative_diff: {}", + relative_diff + ); + + // Trying to mess the last price with lowest possible provide + for _ in 0..1000 { + helper.app.next_block(30); + let assets = vec![helper.assets[&test_coins[1]].with_balance(1u8)]; + helper + .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.5))) + .unwrap(); + } + + // Check that after price scale adjustments (even they are small) internal value is still nearly balanced + let config = helper.query_config().unwrap(); + let pool = helper + .query_pool() + .unwrap() + .assets + .into_iter() + .map(|asset| asset.amount.to_decimal256(6u8).unwrap()) + .collect_vec(); + + let ixs = [pool[0], pool[1] * config.pool_state.price_state.price_scale]; + let relative_diff = ixs[0].abs_diff(ixs[1]) / max(&ixs).unwrap(); + + assert!( + relative_diff < Decimal256::percent(3), + "Internal PCL value is off. Relative_diff: {}", + relative_diff + ); +} + +#[test] +fn check_small_trades_18decimals() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![ + TestCoin::cw20precise("ETH", 18), + TestCoin::cw20precise("USD", 18), + ]; + + let params = ConcentratedPoolParams { + price_scale: f64_to_dec(4.360000915600192), + ..common_pcl_params() + }; + + let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); + + // Fully balanced but small provide + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(8e18 as u128), + helper.assets[&test_coins[1]].with_balance(1_834862000000000000u128), + ]; + helper.provide_liquidity(&owner, &assets).unwrap(); + + // Trying to mess the last price with lowest possible swap + for _ in 0..1000 { + helper.app.next_block(30); + let offer_asset = helper.assets[&test_coins[1]].with_balance(1u8); + helper + .swap_full_params(&owner, &offer_asset, None, Some(Decimal::MAX)) + .unwrap(); + } + + // Check that after price scale adjustments (even they are small) internal value is still nearly balanced + let config = helper.query_config().unwrap(); + let pool = helper + .query_pool() + .unwrap() + .assets + .into_iter() + .map(|asset| asset.amount.to_decimal256(6u8).unwrap()) + .collect_vec(); + + let ixs = [pool[0], pool[1] * config.pool_state.price_state.price_scale]; + let relative_diff = ixs[0].abs_diff(ixs[1]) / max(&ixs).unwrap(); + + assert!( + relative_diff < Decimal256::percent(3), + "Internal PCL value is off. Relative_diff: {}", + relative_diff + ); + + // Trying to mess the last price with lowest possible provide + for _ in 0..1000 { + helper.app.next_block(30); + // 0.000001 USD. minimum provide is limited to LP token precision which is 6 decimals. + let assets = vec![helper.assets[&test_coins[1]].with_balance(1000000000000u128)]; + helper + .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.5))) + .unwrap(); + } + + // Check that after price scale adjustments (even they are small) internal value is still nearly balanced + let config = helper.query_config().unwrap(); + let pool = helper + .query_pool() + .unwrap() + .assets + .into_iter() + .map(|asset| asset.amount.to_decimal256(6u8).unwrap()) + .collect_vec(); + + let ixs = [pool[0], pool[1] * config.pool_state.price_state.price_scale]; + let relative_diff = ixs[0].abs_diff(ixs[1]) / max(&ixs).unwrap(); + + assert!( + relative_diff < Decimal256::percent(3), + "Internal PCL value is off. Relative_diff: {}", + relative_diff + ); +} diff --git a/contracts/pair_concentrated/tests/pair_concentrated_simulation.rs b/contracts/pair_concentrated/tests/pair_concentrated_simulation.rs index b0392269b..f3cd28f31 100644 --- a/contracts/pair_concentrated/tests/pair_concentrated_simulation.rs +++ b/contracts/pair_concentrated/tests/pair_concentrated_simulation.rs @@ -4,11 +4,12 @@ extern crate core; mod helper; -use crate::helper::{dec_to_f64, f64_to_dec, AppExtension, Helper, TestCoin}; +use crate::helper::{common_pcl_params, dec_to_f64, f64_to_dec, AppExtension, Helper, TestCoin}; use astroport::asset::AssetInfoExt; use astroport::cosmwasm_ext::AbsDiff; -use astroport::pair_concentrated::ConcentratedPoolParams; +use astroport::pair_concentrated::{ConcentratedPoolParams, ConcentratedPoolUpdateParams}; use astroport_pair_concentrated::error::ContractError; +use astroport_pcl_common::error::PclError; use cosmwasm_std::{Addr, Decimal, Decimal256}; use proptest::prelude::*; use std::collections::HashMap; @@ -22,22 +23,9 @@ fn simulate_case(case: Vec<(usize, u128, u64)>) { let test_coins = vec![TestCoin::native("uluna"), TestCoin::cw20("USDC")]; - let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::one(), - ma_half_time: 600, - track_asset_balances: None, - }; - let balances = vec![100_000_000_000000u128, 100_000_000_000000u128]; - let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); + let mut helper = Helper::new(&owner, test_coins.clone(), common_pcl_params()).unwrap(); let assets = vec![ helper.assets[&test_coins[0]].with_balance(balances[0]), @@ -56,7 +44,7 @@ fn simulate_case(case: Vec<(usize, u128, u64)>) { if let Err(err) = helper.swap(&user, &offer_asset, None) { let err: ContractError = err.downcast().unwrap(); match err { - ContractError::MaxSpreadAssertion {} => { + ContractError::PclError(PclError::MaxSpreadAssertion {}) => { // if swap fails because of spread then skip this case println!("exceeds spread limit"); } @@ -75,6 +63,58 @@ fn simulate_case(case: Vec<(usize, u128, u64)>) { } } +fn simulate_fee_share_case(case: Vec<(usize, u128, u64)>) { + let owner = Addr::unchecked("owner"); + let user = Addr::unchecked("user"); + + let test_coins = vec![TestCoin::native("uluna"), TestCoin::cw20("USDC")]; + + let balances = vec![100_000_000_000000u128, 100_000_000_000000u128]; + + let mut helper = Helper::new(&owner, test_coins.clone(), common_pcl_params()).unwrap(); + + // Set to 5% fee share + let action = ConcentratedPoolUpdateParams::EnableFeeShare { + fee_share_bps: 1000, + fee_share_address: "share_address".to_string(), + }; + helper.update_config(&owner, &action).unwrap(); + + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(balances[0]), + helper.assets[&test_coins[1]].with_balance(balances[1]), + ]; + helper.provide_liquidity(&owner, &assets).unwrap(); + + let mut i = 0; + for (offer_ind, dy, shift_time) in case { + let _ask_ind = 1 - offer_ind; + + println!("i: {i}, {offer_ind} {dy} {shift_time}"); + let offer_asset = helper.assets[&test_coins[offer_ind]].with_balance(dy); + // let balance_before = helper.coin_balance(&test_coins[ask_ind], &user); + helper.give_me_money(&[offer_asset.clone()], &user); + if let Err(err) = helper.swap(&user, &offer_asset, None) { + let err: ContractError = err.downcast().unwrap(); + match err { + ContractError::PclError(PclError::MaxSpreadAssertion {}) => { + // if swap fails because of spread then skip this case + println!("exceeds spread limit"); + } + _ => panic!("{err}"), + } + + i += 1; + continue; + }; + // let swap_amount = helper.coin_balance(&test_coins[ask_ind], &user) - balance_before; + i += 1; + + // Shift time so EMA will update oracle prices + helper.app.next_block(shift_time); + } +} + fn simulate_provide_case(case: Vec<(impl Into, u128, u128, u64)>) { let owner = Addr::unchecked("owner"); let loss_tolerance = 0.05; // allowed loss per provide due to integer math withing contract @@ -83,20 +123,8 @@ fn simulate_provide_case(case: Vec<(impl Into, u128, u128, u64)>) { let test_coins = vec![TestCoin::native("uluna"), TestCoin::cw20("USDC")]; let initial_price_scale = Decimal::one(); - let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: initial_price_scale, - ma_half_time: 600, - track_asset_balances: None, - }; - let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); + let mut helper = Helper::new(&owner, test_coins.clone(), common_pcl_params()).unwrap(); // owner makes the first provide cuz the pool charges small amount of fees let assets = vec![ @@ -118,7 +146,7 @@ fn simulate_provide_case(case: Vec<(impl Into, u128, u128, u64)>) { if let Err(err) = helper.provide_liquidity(&user, &assets) { let err: ContractError = err.downcast().unwrap(); match err { - ContractError::MaxSpreadAssertion {} => { + ContractError::PclError(PclError::MaxSpreadAssertion {}) => { // if swap fails because of spread then skip this case println!("spread limit exceeded"); } @@ -375,15 +403,8 @@ fn simulate_mixed_case(cases: Vec<(PclEvent, u64)>) { let params = ConcentratedPoolParams { amp: f64_to_dec(10f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), price_scale: Decimal::from_str("0.297172").unwrap(), - ma_half_time: 600, - track_asset_balances: None, + ..common_pcl_params() }; let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); @@ -408,7 +429,7 @@ fn simulate_mixed_case(cases: Vec<(PclEvent, u64)>) { if let Err(err) = helper.provide_liquidity(&user, &assets) { let err: ContractError = err.downcast().unwrap(); match err { - ContractError::MaxSpreadAssertion {} => { + ContractError::PclError(PclError::MaxSpreadAssertion {}) => { // if swap fails because of spread then skip this case println!("provide: spread limit exceeded"); } @@ -427,7 +448,7 @@ fn simulate_mixed_case(cases: Vec<(PclEvent, u64)>) { { let err: ContractError = err.downcast().unwrap(); match err { - ContractError::MaxSpreadAssertion {} => { + ContractError::PclError(PclError::MaxSpreadAssertion {}) => { let coin0_bal = helper.coin_balance(&test_coins[0], &helper.pair_addr); let coin1_bal = helper.coin_balance(&test_coins[1], &helper.pair_addr); // if swap fails because of spread then skip this case @@ -592,6 +613,14 @@ proptest! { } } +proptest! { + #[ignore] + #[test] + fn simulate_fee_share_transactions(case in generate_cases()) { + simulate_fee_share_case(case); + } +} + proptest! { #[ignore] #[test] diff --git a/contracts/pair_concentrated_inj/Cargo.toml b/contracts/pair_concentrated_inj/Cargo.toml index 226f4bbdb..cd77807df 100644 --- a/contracts/pair_concentrated_inj/Cargo.toml +++ b/contracts/pair_concentrated_inj/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-pair-concentrated-injective" -version = "2.0.5" +version = "2.2.0" authors = ["Astroport"] edition = "2021" description = "The Astroport concentrated liquidity pair which supports Injective orderbook integration" @@ -24,10 +24,11 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -astroport = { path = "../../packages/astroport", default-features = false, features = ["injective"] } -astroport-factory = { path = "../factory", features = ["library"] } -astroport-pair-concentrated = { path = "../pair_concentrated", features = ["library"] } -astroport-circular-buffer = { path = "../../packages/circular_buffer" } +astroport = { path = "../../packages/astroport", version = "3", features = ["injective"] } +astroport-factory = { path = "../factory", features = ["library"], version = "1" } +astroport-pair-concentrated = { path = "../pair_concentrated", features = ["library"], version = "2.0.5" } +astroport-circular-buffer = { path = "../../packages/circular_buffer", version = "0.1" } +astroport-pcl-common = { path = "../../packages/astroport_pcl_common", version = "1" } cw2 = "0.15" cw20 = "0.15" cosmwasm-std = "1.1" @@ -49,4 +50,4 @@ anyhow = "1.0" derivative = "2.2" astroport-native-coin-registry = { path = "../periphery/native_coin_registry" } injective-math = "0.1" -injective-testing = "0.1" \ No newline at end of file +injective-testing = "0.1.1" diff --git a/contracts/pair_concentrated_inj/src/consts.rs b/contracts/pair_concentrated_inj/src/consts.rs deleted file mode 100644 index f4ad3d9ed..000000000 --- a/contracts/pair_concentrated_inj/src/consts.rs +++ /dev/null @@ -1,64 +0,0 @@ -use cosmwasm_std::{Decimal, Decimal256}; -use std::ops::RangeInclusive; - -/// ## Adjustable constants -/// 0.05 -pub const DEFAULT_SLIPPAGE: Decimal256 = Decimal256::raw(50000000000000000); -/// 0.5 -pub const MAX_ALLOWED_SLIPPAGE: Decimal256 = Decimal256::raw(500000000000000000); -/// Percentage of 1st pool volume used as offer amount to forecast last price (0.01% or 0.0001). -pub const OFFER_PERCENT: Decimal256 = Decimal256::raw(100000000000000); - -/// ## Internal constants -/// Number of coins. (2.0) -pub const N: Decimal256 = Decimal256::raw(2000000000000000000); -/// Defines fee tolerance. If k coefficient is small enough then k = 0. (0.001) -pub const FEE_TOL: Decimal256 = Decimal256::raw(1000000000000000); -/// N ^ 2 -pub const N_POW2: Decimal256 = Decimal256::raw(4000000000000000000); -/// 1e-5 -pub const TOL: Decimal256 = Decimal256::raw(10000000000000); -/// halfpow tolerance (1e-10) -pub const HALFPOW_TOL: Decimal256 = Decimal256::raw(100000000); -/// 2.0 -pub const TWO: Decimal256 = Decimal256::raw(2000000000000000000); -/// Iterations limit for Newton's method -pub const MAX_ITER: usize = 64; -/// TWAP constant for external oracle prices -pub const TWAP_PRECISION_DEC: Decimal256 = Decimal256::raw((1e6 * 1e18) as u128); - -/// ## Validation constants -/// 0.001 -pub const MIN_FEE: Decimal = Decimal::raw(1000000000000000); -/// 0.5 -pub const MAX_FEE: Decimal = Decimal::raw(500000000000000000); - -/// 1e-8 -pub const FEE_GAMMA_MIN: Decimal = Decimal::raw(10000000000); -/// 0.02 -pub const FEE_GAMMA_MAX: Decimal = Decimal::raw(20000000000000000); - -pub const REPEG_PROFIT_THRESHOLD_MIN: Decimal = Decimal::zero(); -/// 0.01 -pub const REPEG_PROFIT_THRESHOLD_MAX: Decimal = Decimal::raw(10000000000000000); - -/// 0.00000000001 -pub const PRICE_SCALE_DELTA_MIN: Decimal = Decimal::raw(10000000); -pub const PRICE_SCALE_DELTA_MAX: Decimal = Decimal::one(); - -pub const MA_HALF_TIME_LIMITS: RangeInclusive = 1..=(7 * 86400); - -/// 0.1 -pub const AMP_MIN: Decimal = Decimal::raw(1e17 as u128); -/// 100000 -pub const AMP_MAX: Decimal = Decimal::raw(1e23 as u128); - -/// 0.0000001 -pub const GAMMA_MIN: Decimal = Decimal::raw(100000000000); -/// 0.02 -pub const GAMMA_MAX: Decimal = Decimal::raw(20000000000000000); - -/// The minimum time interval for updating Amplifier or Gamma -pub const MIN_AMP_CHANGING_TIME: u64 = 86400; -/// The maximum allowed change of Amplifier or Gamma (10%). -pub const MAX_CHANGE: Decimal = Decimal::raw(1e17 as u128); diff --git a/contracts/pair_concentrated_inj/src/contract.rs b/contracts/pair_concentrated_inj/src/contract.rs index eec92932f..372606e3e 100644 --- a/contracts/pair_concentrated_inj/src/contract.rs +++ b/contracts/pair_concentrated_inj/src/contract.rs @@ -18,8 +18,8 @@ use astroport::asset::{ use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; use astroport::cosmwasm_ext::{AbsDiff, DecimalToInteger, IntegerToDecimal}; use astroport::factory::PairType; -use astroport::observation::{MIN_TRADE_SIZE, OBSERVATIONS_SIZE}; -use astroport::pair::{Cw20HookMsg, InstantiateMsg}; +use astroport::observation::{PrecommitObservation, OBSERVATIONS_SIZE}; +use astroport::pair::{Cw20HookMsg, InstantiateMsg, MIN_TRADE_SIZE}; use astroport::pair_concentrated::UpdatePoolParams; use astroport::pair_concentrated_inj::{ ConcentratedInjObParams, ConcentratedObPoolUpdateParams, ExecuteMsg, @@ -27,23 +27,24 @@ use astroport::pair_concentrated_inj::{ use astroport::querier::{query_factory_config, query_fee_info, query_supply}; use astroport::token::InstantiateMsg as TokenInstantiateMsg; use astroport_circular_buffer::BufferManager; +use astroport_pcl_common::state::{ + AmpGamma, Config, PoolParams, PoolState, Precisions, PriceState, +}; +use astroport_pcl_common::utils::{ + assert_max_spread, assert_slippage_tolerance, before_swap_check, calc_provide_fee, + check_asset_infos, check_assets, check_pair_registered, compute_swap, get_share_in_assets, + mint_liquidity_token_message, +}; +use astroport_pcl_common::{calc_d, get_xcp}; use crate::error::ContractError; -use crate::math::{calc_d, get_xcp}; use crate::orderbook::state::OrderbookState; use crate::orderbook::utils::{ get_subaccount_balances, is_allowed_for_begin_blocker, is_contract_active, leave_orderbook, process_cumulative_trade, }; -use crate::state::{ - store_precisions, AmpGamma, Config, PoolParams, PoolState, Precisions, PriceState, CONFIG, - OBSERVATIONS, OWNERSHIP_PROPOSAL, -}; -use crate::utils::{ - accumulate_swap_sizes, assert_max_spread, assert_slippage_tolerance, before_swap_check, - calc_provide_fee, check_asset_infos, check_assets, check_pair_registered, compute_swap, - get_share_in_assets, mint_liquidity_token_message, query_contract_balances, query_pools, -}; +use crate::state::{CONFIG, OBSERVATIONS, OWNERSHIP_PROPOSAL}; +use crate::utils::{accumulate_swap_sizes, query_contract_balances, query_pools}; /// Contract name that is used for migration. pub(crate) const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); @@ -62,7 +63,7 @@ pub fn instantiate( _info: MessageInfo, msg: InstantiateMsg, ) -> Result, ContractError> { - check_asset_infos(&msg.asset_infos)?; + check_asset_infos(deps.api, &msg.asset_infos)?; if msg.asset_infos.len() != 2 { return Err(StdError::generic_err("asset_infos must contain exactly two elements").into()); @@ -83,7 +84,7 @@ pub fn instantiate( let factory_addr = deps.api.addr_validate(&msg.factory_addr)?; - store_precisions(deps.branch(), &msg.asset_infos, &factory_addr)?; + Precisions::store_precisions(deps.branch(), &msg.asset_infos, &factory_addr)?; let base_precision = msg.asset_infos[0].decimals(&deps.querier, &factory_addr)?; let ob_state = OrderbookState::new( @@ -135,6 +136,8 @@ pub fn instantiate( pool_params, pool_state, owner: None, + track_asset_balances: false, // TODO: decide whether to track asset balances in PCL inj pool + fee_share: None, // TODO: decide whether to enable fee sharing or not }; CONFIG.save(deps.storage, &config)?; @@ -394,7 +397,7 @@ where } } - check_assets(&assets)?; + check_assets(deps.api, &assets)?; info.funds .assert_coins_properly_sent(&assets, &config.pair_info.asset_infos)?; @@ -528,8 +531,8 @@ where let mut slippage = Decimal256::zero(); - // if assets_diff[1] is zero then deposits are balanced thus no need to update price - if !assets_diff[1].is_zero() { + // If deposit doesn't diverge too much from the balanced share, we don't update the price + if assets_diff[0] >= MIN_TRADE_SIZE && assets_diff[1] >= MIN_TRADE_SIZE { slippage = assert_slippage_tolerance( &deposits, share, @@ -615,7 +618,7 @@ fn withdraw_liquidity( let refund_assets = if assets.is_empty() { // Usual withdraw (balanced) - get_share_in_assets(&pools, amount.saturating_sub(Uint128::one()), total_share)? + get_share_in_assets(&pools, amount.saturating_sub(Uint128::one()), total_share) } else { return Err(StdError::generic_err("Imbalanced withdraw is currently disabled").into()); }; @@ -785,6 +788,7 @@ where &config, &env, maker_fee_share, + Decimal256::zero(), )?; xs[offer_ind] += offer_asset_dec.amount; xs[ask_ind] -= swap_result.dy + swap_result.maker_fee; @@ -802,13 +806,19 @@ where let total_share = query_supply(&deps.querier, &config.pair_info.liquidity_token)? .to_decimal256(LP_TOKEN_PRECISION)?; - let last_price = swap_result.calc_last_price(offer_asset_dec.amount, offer_ind); + // Skip very small trade sizes which could significantly mess up the price due to rounding errors, + // especially if token precisions are 18. + if (swap_result.dy + swap_result.maker_fee) >= MIN_TRADE_SIZE + && offer_asset_dec.amount >= MIN_TRADE_SIZE + { + let last_price = swap_result.calc_last_price(offer_asset_dec.amount, offer_ind); - // update_price() works only with internal representation - xs[1] *= config.pool_state.price_state.price_scale; - config - .pool_state - .update_price(&config.pool_params, &env, total_share, &xs, last_price)?; + // update_price() works only with internal representation + xs[1] *= config.pool_state.price_state.price_scale; + config + .pool_state + .update_price(&config.pool_params, &env, total_share, &xs, last_price)?; + } let receiver = to.unwrap_or_else(|| sender.clone()); @@ -828,15 +838,19 @@ where } } - // Store time series data. - // Skipping small unsafe values which can seriously mess oracle price due to rounding errors + // Store observation from precommit data + accumulate_swap_sizes(deps.storage, &env, &mut ob_state)?; + + // Store time series data in precommit observation. + // Skipping small unsafe values which can seriously mess oracle price due to rounding errors. + // This data will be reflected in observations in the next action. if offer_asset_dec.amount >= MIN_TRADE_SIZE && swap_result.dy >= MIN_TRADE_SIZE { let (base_amount, quote_amount) = if offer_ind == 0 { (offer_asset.amount, return_amount) } else { (return_amount, offer_asset.amount) }; - accumulate_swap_sizes(deps.storage, &env, &mut ob_state, base_amount, quote_amount)?; + PrecommitObservation::save(deps.storage, &env, base_amount, quote_amount)?; } CONFIG.save(deps.storage, &config)?; diff --git a/contracts/pair_concentrated_inj/src/error.rs b/contracts/pair_concentrated_inj/src/error.rs index a2a95b7d8..5758ad42c 100644 --- a/contracts/pair_concentrated_inj/src/error.rs +++ b/contracts/pair_concentrated_inj/src/error.rs @@ -1,8 +1,9 @@ -use crate::consts::MIN_AMP_CHANGING_TIME; +use cosmwasm_std::{ConversionOverflowError, OverflowError, StdError}; +use thiserror::Error; + use astroport::asset::MINIMUM_LIQUIDITY_AMOUNT; use astroport_circular_buffer::error::BufferError; -use cosmwasm_std::{ConversionOverflowError, Decimal, OverflowError, StdError}; -use thiserror::Error; +use astroport_pcl_common::error::PclError; /// This enum describes pair contract errors #[derive(Error, Debug, PartialEq)] @@ -19,41 +20,18 @@ pub enum ContractError { #[error("{0}")] CircularBuffer(#[from] BufferError), + #[error("{0}")] + PclError(#[from] PclError), + #[error("Unauthorized")] Unauthorized {}, #[error("You need to provide init params")] InitParamsNotFound {}, - #[error("{0} parameter must be greater than {1} and less than or equal to {2}")] - IncorrectPoolParam(String, String, String), - - #[error( - "{0} error: The difference between the old and new amp or gamma values must not exceed {1} percent", - )] - MaxChangeAssertion(String, Decimal), - - #[error( - "Amp and gamma coefficients cannot be changed more often than once per {} seconds", - MIN_AMP_CHANGING_TIME - )] - MinChangingTimeAssertion {}, - #[error("Initial provide can not be one-sided")] InvalidZeroAmount {}, - #[error("Operation exceeds max spread limit")] - MaxSpreadAssertion {}, - - #[error("Provided spread amount exceeds allowed limit")] - AllowedSpreadAssertion {}, - - #[error("Doubling assets in asset infos")] - DoublingAssets {}, - - #[error("Generator address is not set in factory. Cannot auto-stake")] - AutoStakeError {}, - #[error("Initial liquidity must be more than {}", MINIMUM_LIQUIDITY_AMOUNT)] MinimumLiquidityAmountError {}, diff --git a/contracts/pair_concentrated_inj/src/lib.rs b/contracts/pair_concentrated_inj/src/lib.rs index 54c50589d..3abe86649 100644 --- a/contracts/pair_concentrated_inj/src/lib.rs +++ b/contracts/pair_concentrated_inj/src/lib.rs @@ -1,9 +1,7 @@ pub mod contract; pub mod state; -pub mod consts; pub mod error; -pub mod math; pub mod queries; pub mod utils; diff --git a/contracts/pair_concentrated_inj/src/math/math_decimal.rs b/contracts/pair_concentrated_inj/src/math/math_decimal.rs deleted file mode 100644 index 9e665547c..000000000 --- a/contracts/pair_concentrated_inj/src/math/math_decimal.rs +++ /dev/null @@ -1,317 +0,0 @@ -use cosmwasm_std::{Decimal256, Fraction, StdError, StdResult, Uint128}; -use itertools::Itertools; - -use astroport::cosmwasm_ext::AbsDiff; - -use crate::consts::{HALFPOW_TOL, MAX_ITER, N, N_POW2, TOL}; -use crate::math::signed_decimal::SignedDecimal256; - -/// Internal constant to increase calculation accuracy. (1000.0) -const PADDING: Decimal256 = Decimal256::raw(1000000000000000000000); - -pub fn geometric_mean(x: &[Decimal256]) -> Decimal256 { - (x[0] * x[1]).sqrt() -} - -pub(crate) fn f( - d: SignedDecimal256, - x: &[SignedDecimal256], - a: Decimal256, - gamma: Decimal256, -) -> SignedDecimal256 { - let mul = x[0] * x[1]; - let d_pow2 = d.pow(2); - - let k0 = mul * N_POW2 / d_pow2; - let k = a * gamma.pow(2) * k0 / (SignedDecimal256::from(gamma + Decimal256::one()) - k0).pow(2); - - k * d * (x[0] + x[1]) + mul - k * d_pow2 - d_pow2 / N_POW2 -} - -/// df/dD -pub(crate) fn df_dd( - d: SignedDecimal256, - x: &[SignedDecimal256], - a: Decimal256, - gamma: Decimal256, -) -> SignedDecimal256 { - let mul = x[0] * x[1]; - let a_gamma_pow_2 = a * gamma.pow(2); // A * gamma^2 - - let k0 = mul * N_POW2 / d.pow(2); - - let gamma_one_k0 = SignedDecimal256::from(gamma + Decimal256::one()) - k0; // gamma + 1 - K0 - let gamma_one_k0_pow2 = gamma_one_k0.pow(2); // (gamma + 1 - K0)^2 - - let k = a_gamma_pow_2 * k0 / gamma_one_k0_pow2; - - let k_d_denom = PADDING * d.pow(3) * gamma_one_k0_pow2 * gamma_one_k0; - let k_d = -mul * N.pow(3) * a_gamma_pow_2 * (gamma + Decimal256::one() + k0); - - (k_d * d * PADDING / k_d_denom + k) * (x[0] + x[1]) - - (k_d * d * PADDING / k_d_denom + N * k) * d - - (d / N) -} - -pub(crate) fn newton_d( - x: &[Decimal256], - a: Decimal256, - gamma: Decimal256, -) -> StdResult { - let mut d_prev: SignedDecimal256 = (N * geometric_mean(x)).into(); - let x = x.iter().map(SignedDecimal256::from).collect_vec(); - - for _ in 0..MAX_ITER { - let d = d_prev - f(d_prev, &x, a, gamma) / df_dd(d_prev, &x, a, gamma); - if d.diff(d_prev) <= TOL { - return d.try_into(); - } - d_prev = d; - } - - Err(StdError::generic_err("newton_d is not converging")) -} - -/// df/dx -pub(crate) fn df_dx( - d: Decimal256, - x: &[SignedDecimal256], - a: Decimal256, - gamma: Decimal256, - i: usize, -) -> SignedDecimal256 { - let x_r = x[1 - i]; - let d_pow2 = d.pow(2); - - let k0 = x[0] * x[1] * N_POW2 / d_pow2; - let gamma_one_k0 = gamma + Decimal256::one() - k0; - let gamma_one_k0_pow2 = gamma_one_k0.pow(2); - let a_gamma_pow2 = a * gamma.pow(2); - - let k = a_gamma_pow2 * k0 / gamma_one_k0_pow2; - let k0_x = x_r * N_POW2; - let k_x = k0_x * a_gamma_pow2 * (gamma + Decimal256::one() + k0) - / (d_pow2 * gamma_one_k0 * gamma_one_k0_pow2); - - (k_x * (x[0] + x[1]) + k) * d + x_r - k_x * d_pow2 -} - -pub(crate) fn newton_y( - xs: &[Decimal256], - a: Decimal256, - gamma: Decimal256, - d: Decimal256, - j: usize, -) -> StdResult { - let mut x = xs.iter().map(SignedDecimal256::from).collect_vec(); - let x0 = d.pow(2) / (N_POW2 * x[1 - j]); - let mut xi_1 = x0; - x[j] = x0; - - for _ in 0..MAX_ITER { - let xi = xi_1 - f(d.into(), &x, a, gamma) / df_dx(d, &x, a, gamma, j); - if xi.diff(xi_1) <= TOL { - return xi.try_into(); - } - x[j] = xi; - xi_1 = xi; - } - - Err(StdError::generic_err("newton_y is not converging")) -} - -/// Calculates 0.5^power. -pub fn half_float_pow(power: Decimal256) -> StdResult { - let intpow = power.floor(); - let intpow_u128: Uint128 = (intpow.numerator() / intpow.denominator()).try_into()?; - - let half = Decimal256::from_ratio(1u8, 2u8); - let frac_pow = power - intpow; - - // 0.5 ^ int_power - let result = half.pow(intpow_u128.u128() as u32); - - let mut term = Decimal256::one(); - let mut sum = Decimal256::one(); - - for i in 1..(MAX_ITER as u128) { - let k = Decimal256::from_atomics(i, 0).unwrap(); - let mut c = k - Decimal256::one(); - - c = frac_pow.diff(c); - term = term * c * half / k; - sum -= term; - - if term < HALFPOW_TOL { - return Ok(result * sum); - } - } - - Err(StdError::generic_err("halfpow is not converging")) -} - -#[cfg(test)] -mod tests { - use std::fmt::Display; - use std::str::FromStr; - - use anyhow::{anyhow, Result as AnyResult}; - - use crate::math::math_f64::newton_d as newton_d_f64; - use crate::math::math_f64::newton_y as newton_y_f64; - - use super::*; - - fn f64_to_dec(val: f64) -> Decimal256 { - Decimal256::from_str(&val.to_string()).unwrap() - } - - fn dec_to_f64(val: impl Display) -> f64 { - f64::from_str(&val.to_string()).unwrap() - } - - fn assert_values(dec: impl Display, f64_val: f64) { - let dec_val = dec_to_f64(dec); - if (dec_val - f64_val).abs() > 0.001f64 { - assert_eq!(dec_val, f64_val) - } - } - - fn compute(x1: f64, x2: f64, a: f64, gamma: f64) -> AnyResult<()> { - println!("{x1}, {x2}, a: {a}"); - let xp = [x1, x2]; - - let x1_dec = f64_to_dec(x1); - let x2_dec = f64_to_dec(x2); - let xp_dec = [x1_dec, x2_dec]; - let a_dec = f64_to_dec(a); - let gamma_dec = f64_to_dec(gamma); - - let d_f64 = newton_d_f64(&xp, a, gamma); - let d_dec = newton_d(&xp_dec, a_dec, gamma_dec).unwrap(); - assert_values(d_dec, d_f64); - - let xp_swap = [0f64, x2 + 3.0]; - let y1_f64 = newton_y_f64(&xp_swap, a, gamma, d_f64, 0); - let xp_swap_dec = [Decimal256::zero(), x2_dec + f64_to_dec(3.0)]; - if let Ok(res) = newton_y(&xp_swap_dec, a_dec, gamma_dec, d_dec, 0) { - assert_values(res, y1_f64); - } else { - return Err(anyhow!("newton_y does not converge for i = 0")); - } - - let y2_f64 = newton_y_f64(&[x1 + 1.0, 0f64], a, gamma, d_f64, 1); - if let Ok(res) = newton_y( - &[x1_dec + f64_to_dec(1.0), Decimal256::zero()], - a_dec, - gamma_dec, - d_dec, - 1, - ) { - assert_values(res, y2_f64); - Ok(()) - } else { - Err(anyhow!("newton_y does not converge for i = 1")) - } - } - - #[test] - fn single_test() { - let gamma = 0.000145; - - compute(1000f64, 1000f64, 3500f64, gamma).unwrap(); - } - - #[test] - fn test_real_case() { - let x0 = 1173700.016159; - let x1 = 0.800244312479334221; - let offer_amount = 1.0; - let amp = 40.0; - let gamma = 0.000145; - let d = 2064.855164704653967332; - - println!("Pool before [{} {}]", x0, x1); - let new_x1 = newton_y( - &[f64_to_dec(x0 + offer_amount), f64_to_dec(x1)], - f64_to_dec(amp), - f64_to_dec(gamma), - f64_to_dec(d), - 1, - ) - .unwrap(); - let new_x1 = dec_to_f64(new_x1); - println!("Pool after [{} {}]", x0 + offer_amount, new_x1); - println!("Diff [{} {}]", offer_amount, new_x1 - x1); - assert!(new_x1 < x1, "new x1 {new_x1} should be less than x1 {x1}"); - } - - #[test] - fn test_derivatives() { - let a_f64 = 3500f64; - let gamma_f64 = 0.000145; - let d_f64 = 2000000f64; - let (x1, x2) = (1_000000f64, 1_000000f64); - - let a = f64_to_dec(a_f64); - let gamma = f64_to_dec(gamma_f64); - let d = f64_to_dec(d_f64); - let x: [SignedDecimal256; 2] = [f64_to_dec(x1).into(), f64_to_dec(x2).into()]; - - let der_f64 = crate::math::math_f64::df_dd(d_f64, &[x1, x2], a_f64, gamma_f64); - let der = df_dd(d.into(), &x, a, gamma); - assert_values(der, der_f64); - - let dx_f64 = crate::math::math_f64::df_dx(d_f64, &[x1, x2], a_f64, gamma_f64, 0); - let dx = df_dx(d, &x, a, gamma, 0); - assert_values(dx, dx_f64); - } - - #[test] - fn test_f() { - let a = f64_to_dec(40f64); - let gamma = f64_to_dec(0.000145); - let d = f64_to_dec(20000000f64); - let x: [SignedDecimal256; 2] = [ - f64_to_dec(1000000f64).into(), - f64_to_dec(100000000f64).into(), - ]; - - let val = f(d.into(), &x, a, gamma); - let val_f64 = - crate::math::math_f64::f(20000000f64, &[1000000f64, 100000000f64], 40f64, 0.000145); - let dec_val_f64 = dec_to_f64(val); - assert!( - (dec_val_f64 - val_f64).abs() > 1e-3, - "Assert failed: {dec_val_f64} !~ {val_f64}" - ) - } - - #[ignore] - #[test] - fn test_calculations() { - let gamma = 0.000145; - - let x_range: Vec = (1000u128..=100_000).step_by(10000).into_iter().collect(); - let mut a_range = (100u128..=10000u128).step_by(1000).collect_vec(); - a_range.push(1); - - for (&x1, &x2) in x_range.iter().cartesian_product(&x_range) { - for a in &a_range { - compute(x1 as f64, x2 as f64, *a as f64, gamma).unwrap(); - } - } - } - - #[test] - fn test_halfpow() { - let res = half_float_pow(f64_to_dec(3.231f64)).unwrap(); - assert_eq!(dec_to_f64(res), 0.10650551189033386); - - let res = half_float_pow(f64_to_dec(0.5012f64)).unwrap(); - assert_eq!(dec_to_f64(res), 0.7065188709002241); - - let res = half_float_pow(f64_to_dec(59.1f64)).unwrap(); - assert_eq!(dec_to_f64(res), 0f64); - } -} diff --git a/contracts/pair_concentrated_inj/src/math/math_f64.rs b/contracts/pair_concentrated_inj/src/math/math_f64.rs deleted file mode 100644 index fb1163405..000000000 --- a/contracts/pair_concentrated_inj/src/math/math_f64.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::consts::MAX_ITER; - -const N: f64 = 2.0; -const TOL: f64 = 1e-5; - -pub fn f(d: f64, x: &[f64], a: f64, gamma: f64) -> f64 { - let k0 = (x[0] * x[1] * N * N) / d.powi(2); - let k = a * gamma.powi(2) * k0 / (gamma + 1.0f64 - k0).powi(2); - - k * d * (x[0] + x[1]) + x[0] * x[1] - k * d.powi(2) - (d / N).powi(2) -} - -/// df/dD -pub fn df_dd(d: f64, x: &[f64], a: f64, gamma: f64) -> f64 { - let k0 = x[0] * x[1] * (N / d).powi(2); - let k = a * gamma.powi(2) * k0 / (gamma + 1.0 - k0).powi(2); - let k0_d = -x[0] * x[1] * (N / d).powi(3); - let k_d = a * gamma.powi(2) * (gamma + 1.0 + k0) / (gamma + 1.0 - k0).powi(3) * k0_d; - - (k_d * d + k) * (x[0] + x[1]) - (k_d * d + N * k) * d - (d / N) -} - -/// df/dx -pub fn df_dx(d: f64, x: &[f64], a: f64, gamma: f64, i: usize) -> f64 { - let x_r = x[1 - i]; - let k0 = x[0] * x[1] * (N / d).powi(2); - let k = a * gamma.powi(2) * k0 / (gamma + 1.0 - k0).powi(2); - let k0_x = x_r * (N / d).powi(2); - let k_x = a * gamma.powi(2) * (gamma + 1.0 + k0) / (gamma + 1.0 - k0).powi(3) * k0_x; - - (k_x * (x[0] + x[1]) + k) * d + x_r - k_x * d.powi(2) -} - -pub fn newton_y(xs: &[f64], a: f64, gamma: f64, d: f64, j: usize) -> f64 { - let mut x = xs.to_vec(); - let x_r = x[1 - j]; - let x0 = d.powi(2) / (N * N * x_r); - let mut xi_1 = x0; - x[j] = x0; - - println!("Computing x[{j}]. First approximation {x0}"); - - let mut i = 0; - let mut diff = 1.0; - let mut xi = 0.0; - - while diff > TOL && i < MAX_ITER { - xi = xi_1 - f(d, &x, a, gamma) / df_dx(d, &x, a, gamma, j); - x[j] = xi; - - diff = (xi - xi_1).abs(); - println!("{i}, {xi}, {xi_1}"); - xi_1 = xi; - i += 1; - } - - xi -} - -pub fn newton_d(x: &[f64], a: f64, gamma: f64) -> f64 { - let d0 = N * (x[0] * x[1]).sqrt(); - println!("Computing D. First approximation {d0}"); - let mut di_1 = d0; - let mut i = 0; - let mut diff = 1.0; - let mut di = 0.0; - - while diff > TOL && i < MAX_ITER { - di = di_1 - f(di_1, x, a, gamma) / df_dd(di_1, x, a, gamma); - diff = (di - di_1).abs(); - println!("{i}, {di}, {}", f(di, x, a, gamma)); - di_1 = di; - i += 1; - } - - di -} diff --git a/contracts/pair_concentrated_inj/src/math/mod.rs b/contracts/pair_concentrated_inj/src/math/mod.rs deleted file mode 100644 index 1d4cec758..000000000 --- a/contracts/pair_concentrated_inj/src/math/mod.rs +++ /dev/null @@ -1,43 +0,0 @@ -use cosmwasm_std::{Decimal256, StdResult}; - -use crate::consts::N; -use crate::math::math_decimal::{geometric_mean, newton_d, newton_y}; - -mod math_decimal; -#[cfg(test)] -mod math_f64; -mod signed_decimal; - -use crate::state::AmpGamma; -pub use math_decimal::half_float_pow; - -/// Calculate D invariant based on known pool volumes. -/// -/// * **xs** - internal representation of pool volumes. -/// * **amp_gamma** - an object which represents current Amp and Gamma parameters. -pub fn calc_d(xs: &[Decimal256], amp_gamma: &AmpGamma) -> StdResult { - newton_d(xs, amp_gamma.amp.into(), amp_gamma.gamma.into()) -} - -/// Calculate unknown pool's volume based on the other side of pools which is known and D. -/// -/// * **xs** - internal representation of pool volumes. -/// * **d** - current D invariant. -/// * **amp_gamma** - an object which represents current Amp and Gamma parameters. -/// * **ask_ind** - the index of pool which is unknown. -pub fn calc_y( - xs: &[Decimal256], - d: Decimal256, - amp_gamma: &AmpGamma, - ask_ind: usize, -) -> StdResult { - newton_y(xs, amp_gamma.amp.into(), amp_gamma.gamma.into(), d, ask_ind) -} - -/// Get current XCP. -/// * **d** - internal D invariant. -/// * **price_scale** - x_0/x_1 exchange rate. -pub fn get_xcp(d: Decimal256, price_scale: Decimal256) -> Decimal256 { - let xs = [d / N, d / (N * price_scale)]; - geometric_mean(&xs) -} diff --git a/contracts/pair_concentrated_inj/src/math/signed_decimal.rs b/contracts/pair_concentrated_inj/src/math/signed_decimal.rs deleted file mode 100644 index fab68ad1f..000000000 --- a/contracts/pair_concentrated_inj/src/math/signed_decimal.rs +++ /dev/null @@ -1,303 +0,0 @@ -use astroport::cosmwasm_ext::AbsDiff; -use cosmwasm_std::{Decimal256, StdError}; -use std::fmt::{Display, Formatter}; -use std::ops; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SignedDecimal256 { - val: Decimal256, - /// false - positive, true - negative - neg: bool, -} - -impl Display for SignedDecimal256 { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let sign = if self.neg { "-" } else { "" }; - f.write_str(&format!("{sign}{}", self.val)) - } -} - -impl SignedDecimal256 { - pub fn new(val: Decimal256, neg: bool) -> Self { - Self { val, neg } - } - pub fn pow(&self, exp: u32) -> Self { - if self.val.is_zero() { - Self::from(Decimal256::zero()) - } else { - let neg = if exp % 2 == 0 { false } else { self.neg }; - Self { - val: self.val.pow(exp), - neg, - } - } - } - pub fn diff(self, other: SignedDecimal256) -> Decimal256 { - if self.neg == other.neg { - self.val.diff(other.val) - } else { - self.val + other.val - } - } -} - -impl From for SignedDecimal256 { - fn from(val: Decimal256) -> Self { - Self { val, neg: false } - } -} - -impl From<&Decimal256> for SignedDecimal256 { - fn from(val: &Decimal256) -> Self { - Self::from(*val) - } -} - -impl TryInto for SignedDecimal256 { - type Error = StdError; - - fn try_into(self) -> Result { - if !self.neg || self.val.is_zero() { - Ok(self.val) - } else { - Err(StdError::generic_err(format!( - "Unable to convert negative value, {}", - self - ))) - } - } -} - -impl ops::Add for SignedDecimal256 { - type Output = Self; - - fn add(self, rhs: Self) -> Self::Output { - if self.neg == rhs.neg { - Self { - val: self.val + rhs.val, - ..self - } - } else if self.val > rhs.val { - Self { - val: self.val - rhs.val, - ..self - } - } else { - Self { - val: rhs.val - self.val, - ..rhs - } - } - } -} - -impl ops::Add for SignedDecimal256 { - type Output = SignedDecimal256; - - fn add(self, rhs: Decimal256) -> Self::Output { - self + SignedDecimal256::from(rhs) - } -} - -impl ops::Add for Decimal256 { - type Output = SignedDecimal256; - - fn add(self, rhs: SignedDecimal256) -> Self::Output { - rhs + self - } -} - -impl ops::Sub for SignedDecimal256 { - type Output = Self; - - fn sub(self, rhs: Self) -> Self::Output { - self + Self { - neg: !rhs.neg, - ..rhs - } - } -} - -impl ops::Sub for SignedDecimal256 { - type Output = SignedDecimal256; - - #[allow(clippy::suspicious_arithmetic_impl)] - fn sub(self, rhs: Decimal256) -> Self::Output { - self + Self { - val: rhs, - neg: true, - } - } -} - -impl ops::Sub for Decimal256 { - type Output = SignedDecimal256; - - fn sub(self, rhs: SignedDecimal256) -> Self::Output { - SignedDecimal256::from(self) - rhs - } -} - -impl ops::Mul for SignedDecimal256 { - type Output = SignedDecimal256; - - fn mul(self, rhs: Decimal256) -> Self::Output { - Self { - val: self.val * rhs, - ..self - } - } -} - -impl ops::Mul for SignedDecimal256 { - type Output = SignedDecimal256; - - fn mul(self, rhs: Self) -> Self::Output { - Self { - val: self.val * rhs.val, - neg: self.neg ^ rhs.neg, - } - } -} - -impl ops::Mul for Decimal256 { - type Output = SignedDecimal256; - - fn mul(self, rhs: SignedDecimal256) -> Self::Output { - rhs * self - } -} - -impl ops::Div for SignedDecimal256 { - type Output = SignedDecimal256; - - fn div(self, rhs: Self) -> Self::Output { - Self { - val: self.val / rhs.val, - neg: self.neg ^ rhs.neg, - } - } -} - -impl ops::Div for SignedDecimal256 { - type Output = SignedDecimal256; - - fn div(self, rhs: Decimal256) -> Self::Output { - self / SignedDecimal256::from(rhs) - } -} - -impl ops::Div for Decimal256 { - type Output = SignedDecimal256; - - fn div(self, rhs: SignedDecimal256) -> Self::Output { - Self::Output { - val: self / rhs.val, - neg: rhs.neg, - } - } -} - -impl ops::Neg for SignedDecimal256 { - type Output = SignedDecimal256; - - fn neg(self) -> Self::Output { - Self { - neg: !self.neg, - ..self - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::consts::TWO; - use cosmwasm_std::StdResult; - use std::str::FromStr; - - #[test] - fn test_signed_arithmetics() { - let val = Decimal256::from_str("0.1").unwrap(); - let pos = SignedDecimal256::from(val); - let neg = SignedDecimal256::new(val, true); - - let res: Decimal256 = (pos + neg).try_into().unwrap(); - assert_eq!(res, Decimal256::zero()); - - let res: Decimal256 = (pos - neg).try_into().unwrap(); - assert_eq!(res, Decimal256::from_str("0.2").unwrap()); - - assert_eq!( - neg + neg, - SignedDecimal256::new(Decimal256::from_str("0.2").unwrap(), true) - ); - - let res: Decimal256 = (neg - neg).try_into().unwrap(); - assert_eq!(res, Decimal256::zero()); - - let res = neg + neg; - assert_eq!(res.to_string(), "-0.2"); - } - - #[test] - fn test_signed_division() { - let pos = SignedDecimal256::from(Decimal256::from_str("1").unwrap()); - let neg = SignedDecimal256::new(Decimal256::from_str("2").unwrap(), true); - - assert_eq!( - pos / neg, - SignedDecimal256::new(Decimal256::from_str("0.5").unwrap(), true) - ); - - assert_eq!( - neg / pos, - SignedDecimal256::new(Decimal256::from_str("2").unwrap(), true) - ); - - assert_eq!(neg / neg, SignedDecimal256::new(Decimal256::one(), false)); - assert_eq!(pos / pos, SignedDecimal256::new(Decimal256::one(), false)); - } - - #[test] - fn test_mixed_decimals() { - let a = Decimal256::one(); - let b = SignedDecimal256::new(a, true); - - let res: Decimal256 = (b + a).try_into().unwrap(); - assert_eq!(res, Decimal256::zero()); - - let minus_two = SignedDecimal256::new(TWO, true); - let res: StdResult = minus_two.try_into(); - assert_eq!( - res.unwrap_err().to_string(), - "Generic error: Unable to convert negative value, -2" - ); - - assert_eq!(b / a, SignedDecimal256::new(Decimal256::one(), true)); - assert_eq!(a - b, SignedDecimal256::from(TWO)); - assert_eq!(b - a, minus_two); - assert_eq!(SignedDecimal256::from(a).diff(b), TWO) - } - - #[test] - fn test_pow() { - let a = SignedDecimal256::from(Decimal256::zero()); - let two = SignedDecimal256::from(TWO); - let minus_two = -two; - - assert_eq!(a.pow(10), SignedDecimal256::from(Decimal256::zero())); - assert_eq!( - two.pow(3), - SignedDecimal256::from(Decimal256::from_str("8").unwrap()) - ); - assert_eq!( - minus_two.pow(2), - SignedDecimal256::from(Decimal256::from_str("4").unwrap()) - ); - assert_eq!( - minus_two.pow(3), - SignedDecimal256::new(Decimal256::from_str("8").unwrap(), true) - ); - } -} diff --git a/contracts/pair_concentrated_inj/src/migrate.rs b/contracts/pair_concentrated_inj/src/migrate.rs index 2d2710b16..06211d10f 100644 --- a/contracts/pair_concentrated_inj/src/migrate.rs +++ b/contracts/pair_concentrated_inj/src/migrate.rs @@ -1,19 +1,18 @@ -use astroport::asset::PairInfo; -use astroport::factory::PairType; use cosmwasm_std::{attr, entry_point, DepsMut, Env, Response, StdError, StdResult}; use cw2::{set_contract_version, CONTRACT}; use cw_storage_plus::Item; use injective_cosmwasm::{InjectiveMsgWrapper, InjectiveQueryWrapper}; -use crate::contract::{CONTRACT_NAME, CONTRACT_VERSION}; -use crate::orderbook::state::OrderbookState; +use astroport::factory::PairType; use astroport::pair_concentrated_inj::MigrateMsg; -use astroport_pair_concentrated::state::Config as CLConfig; +use astroport_pcl_common::state::Config; -use crate::state::{AmpGamma, Config, PoolParams, PoolState, PriceState, CONFIG}; +use crate::contract::{CONTRACT_NAME, CONTRACT_VERSION}; +use crate::orderbook::state::OrderbookState; +use crate::state::CONFIG; const MIGRATE_FROM: &str = "astroport-pair-concentrated"; -const MIGRATION_VERSION: &str = "2.0.5"; +const MIGRATION_VERSION: &str = "2.2.0"; /// Manages the contract migration. #[cfg_attr(not(feature = "library"), entry_point)] @@ -35,7 +34,7 @@ pub fn migrate( ))); } - let config: CLConfig = Item::new("config").load(deps.storage)?; + let mut config: Config = Item::new("config").load(deps.storage)?; let base_precision = config.pair_info.asset_infos[0].decimals(&deps.querier, &config.factory_addr)?; let ob_state = OrderbookState::new( @@ -47,7 +46,8 @@ pub fn migrate( &config.pair_info.asset_infos, base_precision, )?; - CONFIG.save(deps.storage, &config.into())?; + config.pair_info.pair_type = PairType::Custom("concentrated_inj_orderbook".to_string()); + CONFIG.save(deps.storage, &config)?; ob_state.save(deps.storage)?; attrs.push(attr("action", "migrate_to_orderbook")); @@ -85,44 +85,3 @@ pub fn migrate( ]); Ok(Response::default().add_attributes(attrs)) } - -impl From for Config { - fn from(val: CLConfig) -> Config { - Config { - pair_info: PairInfo { - pair_type: PairType::Custom("concentrated_inj_orderbook".to_string()), - ..val.pair_info - }, - factory_addr: val.factory_addr, - pool_params: PoolParams { - mid_fee: val.pool_params.mid_fee, - out_fee: val.pool_params.out_fee, - fee_gamma: val.pool_params.fee_gamma, - repeg_profit_threshold: val.pool_params.repeg_profit_threshold, - min_price_scale_delta: val.pool_params.min_price_scale_delta, - ma_half_time: val.pool_params.ma_half_time, - }, - pool_state: PoolState { - initial: AmpGamma { - amp: val.pool_state.initial.amp, - gamma: val.pool_state.initial.gamma, - }, - future: AmpGamma { - amp: val.pool_state.future.amp, - gamma: val.pool_state.future.gamma, - }, - future_time: val.pool_state.future_time, - initial_time: val.pool_state.initial_time, - price_state: PriceState { - oracle_price: val.pool_state.price_state.oracle_price, - last_price: val.pool_state.price_state.last_price, - price_scale: val.pool_state.price_state.price_scale, - last_price_update: val.pool_state.price_state.last_price_update, - xcp_profit: val.pool_state.price_state.xcp_profit, - xcp_profit_real: val.pool_state.price_state.xcp_profit_real, - }, - }, - owner: val.owner, - } - } -} diff --git a/contracts/pair_concentrated_inj/src/orderbook/error.rs b/contracts/pair_concentrated_inj/src/orderbook/error.rs index 9ea90485c..b784fb92d 100644 --- a/contracts/pair_concentrated_inj/src/orderbook/error.rs +++ b/contracts/pair_concentrated_inj/src/orderbook/error.rs @@ -1,9 +1,12 @@ -use crate::error::ContractError; -use astroport::injective_ext::InjMathError; -use astroport_circular_buffer::error::BufferError; use cosmwasm_std::{ConversionOverflowError, Decimal256RangeExceeded, OverflowError, StdError}; use thiserror::Error; +use astroport::injective_ext::InjMathError; +use astroport_circular_buffer::error::BufferError; +use astroport_pcl_common::error::PclError; + +use crate::error::ContractError; + /// This enum describes pair contract errors #[derive(Error, Debug, PartialEq)] pub enum OrderbookError { @@ -22,6 +25,9 @@ pub enum OrderbookError { #[error("{0}")] ContractError(#[from] ContractError), + #[error("{0}")] + PclError(#[from] PclError), + #[error("{0}")] CircularBuffer(#[from] BufferError), diff --git a/contracts/pair_concentrated_inj/src/orderbook/sudo.rs b/contracts/pair_concentrated_inj/src/orderbook/sudo.rs index 9969e3404..4e84d6095 100644 --- a/contracts/pair_concentrated_inj/src/orderbook/sudo.rs +++ b/contracts/pair_concentrated_inj/src/orderbook/sudo.rs @@ -10,7 +10,6 @@ use astroport::asset::AssetInfoExt; use astroport::cosmwasm_ext::IntegerToDecimal; use astroport_circular_buffer::BufferManager; -use crate::math::calc_d; use crate::orderbook::error::OrderbookError; use crate::orderbook::msg::SudoMsg; use crate::orderbook::state::OrderbookState; @@ -18,8 +17,10 @@ use crate::orderbook::utils::{ cancel_all_orders, compute_swap, get_subaccount_balances, leave_orderbook, process_cumulative_trade, update_spot_orders, SpotOrdersFactory, }; -use crate::state::{Precisions, CONFIG, OBSERVATIONS}; +use crate::state::{CONFIG, OBSERVATIONS}; use crate::utils::query_pools; +use astroport_pcl_common::calc_d; +use astroport_pcl_common::state::Precisions; #[cfg_attr(not(feature = "library"), entry_point)] pub fn sudo( diff --git a/contracts/pair_concentrated_inj/src/orderbook/utils.rs b/contracts/pair_concentrated_inj/src/orderbook/utils.rs index 5f0ccdfb0..3e2cb0930 100644 --- a/contracts/pair_concentrated_inj/src/orderbook/utils.rs +++ b/contracts/pair_concentrated_inj/src/orderbook/utils.rs @@ -1,27 +1,29 @@ -use astroport::asset::{Asset, AssetInfo, AssetInfoExt, DecimalAsset, PairInfo}; +use std::cmp::Ordering; +use std::collections::HashMap; +use std::fmt::Debug; + use cosmwasm_std::{ Addr, CosmosMsg, CustomMsg, CustomQuery, Decimal, Decimal256, Env, QuerierWrapper, Response, StdError, StdResult, }; -use std::cmp::Ordering; -use std::collections::HashMap; -use std::fmt::Debug; +use injective_cosmwasm::{ + checked_address_to_subaccount_id, create_batch_update_orders_msg, create_withdraw_msg, + FundingMode, InjectiveMsgWrapper, InjectiveQuerier, MarketId, OrderType, SpotOrder, + SubaccountId, +}; use tiny_keccak::Hasher; +use astroport::asset::{Asset, AssetInfo, AssetInfoExt, DecimalAsset, PairInfo}; +use astroport::cosmwasm_ext::{AbsDiff, ConvertInto, IntegerToDecimal}; +use astroport::querier::{query_fee_info, query_supply}; +use astroport_pcl_common::calc_y; +use astroport_pcl_common::state::{AmpGamma, Config, Precisions}; + use crate::contract::LP_TOKEN_PRECISION; use crate::error::ContractError; -use crate::math::calc_y; use crate::orderbook::consts::{GAS_FEE_DENOM, SUBACC_NONCE}; use crate::orderbook::error::OrderbookError; use crate::orderbook::state::OrderbookState; -use crate::state::{AmpGamma, Config, Precisions}; -use astroport::cosmwasm_ext::{AbsDiff, ConvertInto, IntegerToDecimal}; -use astroport::querier::{query_fee_info, query_supply}; -use injective_cosmwasm::{ - checked_address_to_subaccount_id, create_batch_update_orders_msg, create_withdraw_msg, - FundingMode, InjectiveMsgWrapper, InjectiveQuerier, MarketId, OrderType, SpotOrder, - SubaccountId, -}; /// Calculate hash from two binary slices. pub fn calc_hash(a1: &[u8], a2: &[u8]) -> String { @@ -486,10 +488,12 @@ where #[cfg(test)] mod tests { - use super::*; - use astroport::asset::{native_asset_info, token_asset_info}; use cosmwasm_std::Addr; + use astroport::asset::{native_asset_info, token_asset_info}; + + use super::*; + #[test] fn test_calc_market_ids() { let asset_infos = vec![ diff --git a/contracts/pair_concentrated_inj/src/queries.rs b/contracts/pair_concentrated_inj/src/queries.rs index 55320b06b..6bdcb22bf 100644 --- a/contracts/pair_concentrated_inj/src/queries.rs +++ b/contracts/pair_concentrated_inj/src/queries.rs @@ -14,15 +14,17 @@ use astroport::pair::{ use astroport::pair_concentrated::ConcentratedPoolParams; use astroport::pair_concentrated_inj::{OrderbookStateResponse, QueryMsg}; use astroport::querier::{query_factory_config, query_fee_info, query_supply}; +use astroport_pcl_common::state::Precisions; +use astroport_pcl_common::utils::{ + before_swap_check, compute_offer_amount, compute_swap, get_share_in_assets, +}; +use astroport_pcl_common::{calc_d, get_xcp}; use crate::contract::LP_TOKEN_PRECISION; use crate::error::ContractError; -use crate::math::{calc_d, get_xcp}; use crate::orderbook::state::OrderbookState; -use crate::state::{Precisions, CONFIG, OBSERVATIONS}; -use crate::utils::{ - before_swap_check, compute_offer_amount, compute_swap, get_share_in_assets, query_pools, -}; +use crate::state::{CONFIG, OBSERVATIONS}; +use crate::utils::query_pools; /// Exposes all the queries available in the contract. /// @@ -130,7 +132,7 @@ fn query_share( )?; let total_share = query_supply(&deps.querier, &config.pair_info.liquidity_token)?; let refund_assets = - get_share_in_assets(&pools, amount.saturating_sub(Uint128::one()), total_share)?; + get_share_in_assets(&pools, amount.saturating_sub(Uint128::one()), total_share); let refund_assets = refund_assets .into_iter() @@ -197,6 +199,7 @@ pub fn query_simulation( &config, &env, maker_fee_share, + Decimal256::zero(), )?; Ok(SimulationResponse { @@ -303,7 +306,8 @@ where min_price_scale_delta: config.pool_params.min_price_scale_delta, price_scale, ma_half_time: config.pool_params.ma_half_time, - track_asset_balances: None, + track_asset_balances: Some(config.track_asset_balances), + fee_share: config.fee_share, })?), owner: config.owner.unwrap_or(factory_config.owner), factory_addr: config.factory_addr, @@ -344,11 +348,12 @@ mod testing { use std::error::Error; use std::str::FromStr; - use astroport::observation::{query_observation, Observation, OracleObservation}; - use astroport_circular_buffer::BufferManager; use cosmwasm_std::testing::{mock_dependencies, mock_env}; use cosmwasm_std::Timestamp; + use astroport::observation::{query_observation, Observation, OracleObservation}; + use astroport_circular_buffer::BufferManager; + use super::*; pub fn f64_to_dec(val: f64) -> T diff --git a/contracts/pair_concentrated_inj/src/state.rs b/contracts/pair_concentrated_inj/src/state.rs index 870e7d2fa..4b1c93023 100644 --- a/contracts/pair_concentrated_inj/src/state.rs +++ b/contracts/pair_concentrated_inj/src/state.rs @@ -1,863 +1,16 @@ -use std::fmt::Display; +use cw_storage_plus::Item; -use cosmwasm_schema::cw_serde; -use cosmwasm_std::{ - attr, Addr, Attribute, CustomQuery, Decimal, Decimal256, DepsMut, Env, Order, StdError, - StdResult, Storage, -}; -use cw_storage_plus::{Item, Map}; - -use astroport::asset::{AssetInfo, PairInfo}; use astroport::common::OwnershipProposal; -use astroport::cosmwasm_ext::{AbsDiff, IntegerToDecimal}; use astroport::observation::Observation; - -use astroport::pair_concentrated::{PromoteParams, UpdatePoolParams}; use astroport_circular_buffer::CircularBuffer; - -use crate::consts::{ - AMP_MAX, AMP_MIN, FEE_GAMMA_MAX, FEE_GAMMA_MIN, FEE_TOL, GAMMA_MAX, GAMMA_MIN, MAX_CHANGE, - MAX_FEE, MA_HALF_TIME_LIMITS, MIN_AMP_CHANGING_TIME, MIN_FEE, N_POW2, PRICE_SCALE_DELTA_MAX, - PRICE_SCALE_DELTA_MIN, REPEG_PROFIT_THRESHOLD_MAX, REPEG_PROFIT_THRESHOLD_MIN, TWO, -}; -use crate::error::ContractError; -use crate::math::{calc_d, get_xcp, half_float_pow}; - -/// This structure stores the concentrated pair parameters. -#[cw_serde] -pub struct Config { - /// The pair information stored in a [`PairInfo`] struct - pub pair_info: PairInfo, - /// The factory contract address - pub factory_addr: Addr, - /// Pool parameters - pub pool_params: PoolParams, - /// Pool state - pub pool_state: PoolState, - /// Pool's owner - pub owner: Option, -} - -/// This structure stores the pool parameters which may be adjusted via the `update_pool_params`. -#[cw_serde] -#[derive(Default)] -pub struct PoolParams { - /// The minimum fee, charged when pool is fully balanced - pub mid_fee: Decimal, - /// The maximum fee, charged when pool is imbalanced - pub out_fee: Decimal, - /// Parameter that defines how gradual the fee changes from fee_mid to fee_out based on - /// distance from price_scale - pub fee_gamma: Decimal, - /// Minimum profit before initiating a new repeg - pub repeg_profit_threshold: Decimal, - /// Minimum amount to change price_scale when repegging - pub min_price_scale_delta: Decimal, - /// Half-time used for calculating the price oracle - pub ma_half_time: u64, -} - -/// Validates input value against its limits. -fn validate_param(name: &str, val: T, min: T, max: T) -> Result<(), ContractError> -where - T: PartialOrd + Display, -{ - if val >= min && val <= max { - Ok(()) - } else { - Err(ContractError::IncorrectPoolParam( - name.to_string(), - min.to_string(), - max.to_string(), - )) - } -} - -impl PoolParams { - /// Intended to update current pool parameters. Performs validation of the new parameters. - /// Returns a vector of attributes with updated parameters. - /// - /// * `update_params` - an object which contains new pool parameters. Any of the parameters may be omitted. - pub fn update_params( - &mut self, - update_params: UpdatePoolParams, - ) -> Result, ContractError> { - let mut attributes = vec![]; - if let Some(mid_fee) = update_params.mid_fee { - validate_param("mid_fee", mid_fee, MIN_FEE, MAX_FEE)?; - self.mid_fee = mid_fee; - attributes.push(attr("mid_fee", mid_fee.to_string())); - } - - if let Some(out_fee) = update_params.out_fee { - validate_param("out_fee", out_fee, MIN_FEE, MAX_FEE)?; - if out_fee <= self.mid_fee { - return Err(StdError::generic_err(format!( - "out_fee {out_fee} must be more than mid_fee {}", - self.mid_fee - )) - .into()); - } - self.out_fee = out_fee; - attributes.push(attr("out_fee", out_fee.to_string())); - } - - if let Some(fee_gamma) = update_params.fee_gamma { - validate_param("fee_gamma", fee_gamma, FEE_GAMMA_MIN, FEE_GAMMA_MAX)?; - self.fee_gamma = fee_gamma; - attributes.push(attr("fee_gamma", fee_gamma.to_string())); - } - - if let Some(repeg_profit_threshold) = update_params.repeg_profit_threshold { - validate_param( - "repeg_profit_threshold", - repeg_profit_threshold, - REPEG_PROFIT_THRESHOLD_MIN, - REPEG_PROFIT_THRESHOLD_MAX, - )?; - self.repeg_profit_threshold = repeg_profit_threshold; - attributes.push(attr( - "repeg_profit_threshold", - repeg_profit_threshold.to_string(), - )); - } - - if let Some(min_price_scale_delta) = update_params.min_price_scale_delta { - validate_param( - "min_price_scale_delta", - min_price_scale_delta, - PRICE_SCALE_DELTA_MIN, - PRICE_SCALE_DELTA_MAX, - )?; - self.min_price_scale_delta = min_price_scale_delta; - attributes.push(attr( - "min_price_scale_delta", - min_price_scale_delta.to_string(), - )); - } - - if let Some(ma_half_time) = update_params.ma_half_time { - validate_param( - "ma_half_time", - ma_half_time, - *MA_HALF_TIME_LIMITS.start(), - *MA_HALF_TIME_LIMITS.end(), - )?; - self.ma_half_time = ma_half_time; - attributes.push(attr("ma_half_time", ma_half_time.to_string())); - } - - Ok(attributes) - } - - pub fn fee(&self, xp: &[Decimal256]) -> Decimal256 { - let fee_gamma: Decimal256 = self.fee_gamma.into(); - let sum = xp[0] + xp[1]; - let mut k = xp[0] * xp[1] * N_POW2 / sum.pow(2); - k = fee_gamma / (fee_gamma + Decimal256::one() - k); - - if k <= FEE_TOL { - k = Decimal256::zero() - } - - k * Decimal256::from(self.mid_fee) - + (Decimal256::one() - k) * Decimal256::from(self.out_fee) - } -} - -/// Structure which stores Amp and Gamma. -#[cw_serde] -#[derive(Default, Copy)] -pub struct AmpGamma { - pub amp: Decimal, - pub gamma: Decimal, -} - -impl AmpGamma { - /// Validates the parameters and creates a new object of the [`AmpGamma`] structure. - pub fn new(amp: Decimal, gamma: Decimal) -> Result { - validate_param("amp", amp, AMP_MIN, AMP_MAX)?; - validate_param("gamma", gamma, GAMMA_MIN, GAMMA_MAX)?; - - Ok(AmpGamma { amp, gamma }) - } -} - -/// Internal structure which stores the price state. -/// This structure cannot be updated via update_config. -#[cw_serde] -#[derive(Default)] -pub struct PriceState { - /// Internal oracle price - pub oracle_price: Decimal256, - /// The last saved price - pub last_price: Decimal256, - /// Current price scale between 1st and 2nd assets. - /// I.e. such C that x = C * y where x - 1st asset, y - 2nd asset. - pub price_scale: Decimal256, - /// Last timestamp when the price_oracle was updated. - pub last_price_update: u64, - /// Keeps track of positive change in xcp due to fees accruing - pub xcp_profit: Decimal256, - /// Profits due to fees inclusive of realized losses from rebalancing - pub xcp_profit_real: Decimal256, -} - -/// Internal structure which stores the pool's state. -#[cw_serde] -pub struct PoolState { - /// Initial Amp and Gamma - pub initial: AmpGamma, - /// Future Amp and Gamma - pub future: AmpGamma, - /// Timestamp when Amp and Gamma should become equal to self.future - pub future_time: u64, - /// Timestamp when Amp and Gamma started being changed - pub initial_time: u64, - /// Current price state - pub price_state: PriceState, -} - -impl PoolState { - /// Validates Amp and Gamma promotion parameters. - /// Saves current values in self.initial and setups self.future. - /// If amp and gamma are being changed then current values will be used as initial values. - pub fn promote_params( - &mut self, - env: &Env, - params: PromoteParams, - ) -> Result<(), ContractError> { - let block_time = env.block.time.seconds(); - - // Validate time interval - if block_time < self.initial_time + MIN_AMP_CHANGING_TIME - || params.future_time < block_time + MIN_AMP_CHANGING_TIME - { - return Err(ContractError::MinChangingTimeAssertion {}); - } - - // Validate amp and gamma - let next_amp_gamma = AmpGamma::new(params.next_amp, params.next_gamma)?; - - // Calculate current amp and gamma - let cur_amp_gamma = self.get_amp_gamma(env); - - // Validate amp and gamma values are being changed by <= 10% - let one = Decimal::one(); - if (next_amp_gamma.amp / cur_amp_gamma.amp).diff(one) > MAX_CHANGE { - return Err(ContractError::MaxChangeAssertion( - "Amp".to_string(), - MAX_CHANGE, - )); - } - if (next_amp_gamma.gamma / cur_amp_gamma.gamma).diff(one) > MAX_CHANGE { - return Err(ContractError::MaxChangeAssertion( - "Gamma".to_string(), - MAX_CHANGE, - )); - } - - self.initial = cur_amp_gamma; - self.initial_time = block_time; - - self.future = next_amp_gamma; - self.future_time = params.future_time; - - Ok(()) - } - - /// Stops amp and gamma promotion. Saves current values in self.future. - pub fn stop_promotion(&mut self, env: &Env) { - self.future = self.get_amp_gamma(env); - self.future_time = env.block.time.seconds(); - } - - /// Calculates current amp and gamma. - /// This function handles parameters upgrade as well as downgrade. - /// If block time >= self.future_time then it returns self.future parameters. - pub fn get_amp_gamma(&self, env: &Env) -> AmpGamma { - let block_time = env.block.time.seconds(); - if block_time < self.future_time { - let total = (self.future_time - self.initial_time).to_decimal(); - let passed = (block_time - self.initial_time).to_decimal(); - let left = total - passed; - - // A1 = A0 + (A1 - A0) * (block_time - t_init) / (t_end - t_init) -> simplified to: - // A1 = ( A0 * (t_end - block_time) + A1 * (block_time - t_init) ) / (t_end - t_init) - let amp = (self.initial.amp * left + self.future.amp * passed) / total; - let gamma = (self.initial.gamma * left + self.future.gamma * passed) / total; - - AmpGamma { amp, gamma } - } else { - AmpGamma { - amp: self.future.amp, - gamma: self.future.gamma, - } - } - } - - /// The function is responsible for repegging mechanism. - /// It updates internal oracle price and adjusts price scale. - /// - /// * **total_lp** total LP tokens were minted - /// * **cur_xs** - internal representation of pool volumes - /// * **cur_price** - last price happened in the previous action (swap, provide or withdraw) - pub fn update_price( - &mut self, - pool_params: &PoolParams, - env: &Env, - total_lp: Decimal256, - cur_xs: &[Decimal256], - cur_price: Decimal256, - ) -> StdResult<()> { - let amp_gamma = self.get_amp_gamma(env); - let block_time = env.block.time.seconds(); - let price_state = &mut self.price_state; - - if price_state.last_price_update < block_time { - let arg = Decimal256::from_ratio( - block_time - price_state.last_price_update, - pool_params.ma_half_time, - ); - let alpha = half_float_pow(arg)?; - price_state.oracle_price = price_state.last_price * (Decimal256::one() - alpha) - + price_state.oracle_price * alpha; - price_state.last_price_update = block_time; - } - price_state.last_price = cur_price; - - let cur_d = calc_d(cur_xs, &_gamma)?; - let xcp = get_xcp(cur_d, price_state.price_scale); - - if !price_state.xcp_profit_real.is_zero() { - let xcp_profit_real = xcp / total_lp; - - // If xcp dropped and no ramping happens then this swap makes loss - if xcp_profit_real < price_state.xcp_profit_real && block_time >= self.future_time { - return Err(StdError::generic_err( - "XCP profit real value dropped. This action makes loss", - )); - } - - price_state.xcp_profit = - price_state.xcp_profit * xcp_profit_real / price_state.xcp_profit_real; - price_state.xcp_profit_real = xcp_profit_real; - } - - let xcp_profit = price_state.xcp_profit; - - let norm = (price_state.oracle_price / price_state.price_scale).diff(Decimal256::one()); - let scale_delta = Decimal256::from(pool_params.min_price_scale_delta) - .max(norm * Decimal256::from_ratio(1u8, 10u8)); - - if norm >= scale_delta - && price_state.xcp_profit_real - Decimal256::one() - > (xcp_profit - Decimal256::one()) / TWO - + Decimal256::from(pool_params.repeg_profit_threshold) - { - let numerator = price_state.price_scale * (norm - scale_delta) - + scale_delta * price_state.oracle_price; - let price_scale_new = numerator / norm; - - let xs = [ - cur_xs[0], - cur_xs[1] * price_scale_new / price_state.price_scale, - ]; - let new_d = calc_d(&xs, &_gamma)?; - - let new_xcp = get_xcp(new_d, price_scale_new); - let new_xcp_profit_real = new_xcp / total_lp; - - if TWO * new_xcp_profit_real > xcp_profit + Decimal256::one() { - price_state.price_scale = price_scale_new; - price_state.xcp_profit_real = new_xcp_profit_real; - }; - } - - Ok(()) - } -} - -/// Store all token precisions. -pub(crate) fn store_precisions( - deps: DepsMut, - asset_infos: &[AssetInfo], - factory_addr: &Addr, -) -> StdResult<()> -where - C: CustomQuery, -{ - for asset_info in asset_infos { - let precision = asset_info.decimals(&deps.querier, factory_addr)?; - PRECISIONS.save(deps.storage, asset_info.to_string(), &precision)?; - } - - Ok(()) -} - -pub(crate) struct Precisions(Vec<(String, u8)>); - -impl Precisions { - pub(crate) fn new(storage: &dyn Storage) -> StdResult { - let items = PRECISIONS - .range(storage, None, None, Order::Ascending) - .collect::>>()?; - - Ok(Self(items)) - } - - pub(crate) fn get_precision(&self, asset_info: &AssetInfo) -> Result { - self.0 - .iter() - .find_map(|(info, prec)| { - if info == &asset_info.to_string() { - Some(*prec) - } else { - None - } - }) - .ok_or_else(|| ContractError::InvalidAsset(asset_info.to_string())) - } -} +use astroport_pcl_common::state::Config; /// Stores pool parameters and state. pub const CONFIG: Item = Item::new("config"); -/// Stores map of AssetInfo (as String) -> precision -const PRECISIONS: Map = Map::new("precisions"); - /// Stores the latest contract ownership transfer proposal pub const OWNERSHIP_PROPOSAL: Item = Item::new("ownership_proposal"); /// Circular buffer to store trade size observations pub const OBSERVATIONS: CircularBuffer = CircularBuffer::new("observations_state", "observations_buffer"); - -#[cfg(test)] -mod test { - use std::str::FromStr; - - use cosmwasm_std::testing::mock_env; - use cosmwasm_std::Timestamp; - - use crate::math::calc_y; - - use super::*; - - fn f64_to_dec(val: f64) -> Decimal { - Decimal::from_str(&val.to_string()).unwrap() - } - fn f64_to_dec256(val: f64) -> Decimal256 { - Decimal256::from_str(&val.to_string()).unwrap() - } - fn dec_to_f64(val: Decimal256) -> f64 { - f64::from_str(&val.to_string()).unwrap() - } - - #[test] - #[should_panic(expected = "attempt to subtract with overflow")] - fn test_validator_odd_behaviour() { - let mut env = mock_env(); - env.block.time = Timestamp::from_seconds(86400); - - let mut state = PoolState { - initial: AmpGamma { - amp: Decimal::zero(), - gamma: Decimal::zero(), - }, - future: AmpGamma { - amp: f64_to_dec(100_f64), - gamma: f64_to_dec(0.0000001_f64), - }, - future_time: 0, - initial_time: 0, - price_state: Default::default(), - }; - - // Increase values - let promote_params = PromoteParams { - next_amp: f64_to_dec(110_f64), - next_gamma: f64_to_dec(0.00000011_f64), - future_time: env.block.time.seconds() + 100_000, - }; - state.promote_params(&env, promote_params).unwrap(); - - let AmpGamma { amp, gamma } = state.get_amp_gamma(&env); - assert_eq!(amp, f64_to_dec(100_f64)); - assert_eq!(gamma, f64_to_dec(0.0000001_f64)); - - // Simulating validator odd behavior - env.block.time = env.block.time.minus_seconds(1000); - state.get_amp_gamma(&env); - } - - #[test] - fn test_pool_state() { - let mut env = mock_env(); - env.block.time = Timestamp::from_seconds(86400); - - let mut state = PoolState { - initial: AmpGamma { - amp: Decimal::zero(), - gamma: Decimal::zero(), - }, - future: AmpGamma { - amp: f64_to_dec(100_f64), - gamma: f64_to_dec(0.0000001_f64), - }, - future_time: 0, - initial_time: 0, - price_state: Default::default(), - }; - - // Trying to promote params with future time in the past - let promote_params = PromoteParams { - next_amp: f64_to_dec(110_f64), - next_gamma: f64_to_dec(0.00000011_f64), - future_time: env.block.time.seconds() - 10000, - }; - let err = state.promote_params(&env, promote_params).unwrap_err(); - assert_eq!(err, ContractError::MinChangingTimeAssertion {}); - - // Increase values - let promote_params = PromoteParams { - next_amp: f64_to_dec(110_f64), - next_gamma: f64_to_dec(0.00000011_f64), - future_time: env.block.time.seconds() + 100_000, - }; - state.promote_params(&env, promote_params).unwrap(); - - let AmpGamma { amp, gamma } = state.get_amp_gamma(&env); - assert_eq!(amp, f64_to_dec(100_f64)); - assert_eq!(gamma, f64_to_dec(0.0000001_f64)); - - env.block.time = env.block.time.plus_seconds(50_000); - - let AmpGamma { amp, gamma } = state.get_amp_gamma(&env); - assert_eq!(amp, f64_to_dec(105_f64)); - assert_eq!(gamma, f64_to_dec(0.000000105_f64)); - - env.block.time = env.block.time.plus_seconds(100_001); - let AmpGamma { amp, gamma } = state.get_amp_gamma(&env); - assert_eq!(amp, f64_to_dec(110_f64)); - assert_eq!(gamma, f64_to_dec(0.00000011_f64)); - - // Decrease values - let promote_params = PromoteParams { - next_amp: f64_to_dec(108_f64), - next_gamma: f64_to_dec(0.000000106_f64), - future_time: env.block.time.seconds() + 100_000, - }; - state.promote_params(&env, promote_params).unwrap(); - - env.block.time = env.block.time.plus_seconds(50_000); - let AmpGamma { amp, gamma } = state.get_amp_gamma(&env); - assert_eq!(amp, f64_to_dec(109_f64)); - assert_eq!(gamma, f64_to_dec(0.000000108_f64)); - - env.block.time = env.block.time.plus_seconds(50_001); - let AmpGamma { amp, gamma } = state.get_amp_gamma(&env); - assert_eq!(amp, f64_to_dec(108_f64)); - assert_eq!(gamma, f64_to_dec(0.000000106_f64)); - - // Increase amp only - let promote_params = PromoteParams { - next_amp: f64_to_dec(118_f64), - next_gamma: f64_to_dec(0.000000106_f64), - future_time: env.block.time.seconds() + 100_000, - }; - state.promote_params(&env, promote_params).unwrap(); - - env.block.time = env.block.time.plus_seconds(50_000); - let AmpGamma { amp, gamma } = state.get_amp_gamma(&env); - assert_eq!(amp, f64_to_dec(113_f64)); - assert_eq!(gamma, f64_to_dec(0.000000106_f64)); - - env.block.time = env.block.time.plus_seconds(50_001); - let AmpGamma { amp, gamma } = state.get_amp_gamma(&env); - assert_eq!(amp, f64_to_dec(118_f64)); - assert_eq!(gamma, f64_to_dec(0.000000106_f64)); - } - - #[test] - fn check_fee_update() { - let mid_fee = 0.25f64; - let out_fee = 0.46f64; - let fee_gamma = 0.0002f64; - - let params = PoolParams { - mid_fee: f64_to_dec(mid_fee), - out_fee: f64_to_dec(out_fee), - fee_gamma: f64_to_dec(fee_gamma), - repeg_profit_threshold: Default::default(), - min_price_scale_delta: Default::default(), - ma_half_time: 0, - }; - - let xp = vec![f64_to_dec256(1_000_000f64), f64_to_dec256(1_000_000f64)]; - let result = params.fee(&xp); - assert_eq!(dec_to_f64(result), mid_fee); - - let xp = vec![f64_to_dec256(990_000f64), f64_to_dec256(1_000_000f64)]; - let result = params.fee(&xp); - assert_eq!(dec_to_f64(result), 0.2735420730476899); - - let xp = vec![f64_to_dec256(100_000f64), f64_to_dec256(1_000_000_f64)]; - let result = params.fee(&xp); - assert_eq!(dec_to_f64(result), out_fee); - } - - /// (cur_d, total_lp, new_price) - fn swap( - ext_xs: &mut [Decimal256], - offer_amount: Decimal256, - price_scale: Decimal256, - ask_ind: usize, - amp_gamma: &AmpGamma, - pool_params: &PoolParams, - ) -> Decimal256 { - let offer_ind = 1 - ask_ind; - - let mut xs = ext_xs.to_vec(); - println!("Before swap: {} {}", xs[0], xs[1]); - - // internal repr - xs[1] *= price_scale; - println!("Before swap (internal): {} {}", xs[0], xs[1]); - - let cur_d = calc_d(&xs, amp_gamma).unwrap(); - - let mut offer_amount_internal = offer_amount; - // internal repr - if offer_ind == 1 { - offer_amount_internal *= price_scale; - } - - xs[offer_ind] += offer_amount_internal; - let mut ask_amount = xs[ask_ind] - calc_y(&xs, cur_d, amp_gamma, ask_ind).unwrap(); - xs[ask_ind] -= ask_amount; - let fee = ask_amount * pool_params.fee(&xs); - println!("fee {fee} ({}%)", pool_params.fee(&xs)); - xs[ask_ind] += fee; - ask_amount -= fee; - - println!( - "Internal Swap {} x[{}] for {} x[{}] by {} price", - offer_amount_internal, - offer_ind, - ask_amount, - ask_ind, - ask_amount / offer_amount_internal - ); - - // external repr - let new_price = if ask_ind == 1 { - ask_amount /= price_scale; - offer_amount / ask_amount - } else { - ask_amount / offer_amount - }; - - println!( - "Swap {} x[{}] for {} x[{}] by {new_price} price", - offer_amount, offer_ind, ask_amount, ask_ind - ); - - ext_xs[offer_ind] += offer_amount; - ext_xs[ask_ind] -= ask_amount; - - let ext_d = calc_d(ext_xs, amp_gamma).unwrap(); - let cur_d = calc_d(&xs, amp_gamma).unwrap(); - - println!("Internal: d {cur_d}",); - println!("External: d {ext_d}",); - - println!("After swap: {} {}", ext_xs[0], ext_xs[1]); - println!( - "After swap (internal): {} {}", - ext_xs[0], - ext_xs[1] * price_scale - ); - - new_price - } - - fn to_future(env: &mut Env, by_secs: u64) { - env.block.time = env.block.time.plus_seconds(by_secs) - } - - fn to_internal_repr(xs: &[Decimal256], price_scale: Decimal256) -> Vec { - vec![xs[0], xs[1] * price_scale] - } - - #[test] - fn check_repeg() { - let (amp, gamma) = (40f64, 0.000145); - let amp_gamma = AmpGamma { - amp: f64_to_dec(amp), - gamma: f64_to_dec(gamma), - }; - let mut env = mock_env(); - env.block.time = Timestamp::from_seconds(0); - - let pool_params = PoolParams { - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - ma_half_time: 600, - }; - - let mut pool_state = PoolState { - initial: AmpGamma::default(), - future: amp_gamma, - future_time: 0, - initial_time: 0, - price_state: PriceState { - oracle_price: f64_to_dec256(2f64), - last_price: f64_to_dec256(2f64), - price_scale: f64_to_dec256(2f64), - last_price_update: env.block.time.seconds(), - xcp_profit: Decimal256::one(), - xcp_profit_real: Decimal256::one(), - }, - }; - - to_future(&mut env, 1); - - // external repr - let mut ext_xs = [f64_to_dec256(1_000_000f64), f64_to_dec256(500_000f64)]; - let mut xs = ext_xs.to_vec(); - xs[1] *= pool_state.price_state.price_scale; - let cur_d = calc_d(&xs, &_gamma).unwrap(); - let total_lp = get_xcp(cur_d, pool_state.price_state.price_scale); - - let offer_amount = f64_to_dec256(1000_f64); - let price = swap( - &mut ext_xs, - offer_amount, - pool_state.price_state.price_scale, - 0, - &_gamma, - &pool_params, - ); - pool_state - .update_price( - &pool_params, - &env, - total_lp, - &to_internal_repr(&ext_xs, pool_state.price_state.price_scale), - price, - ) - .unwrap(); - - to_future(&mut env, 600); - - let offer_amount = f64_to_dec256(10000_f64); - let price = swap( - &mut ext_xs, - offer_amount, - pool_state.price_state.price_scale, - 0, - &_gamma, - &pool_params, - ); - pool_state - .update_price( - &pool_params, - &env, - total_lp, - &to_internal_repr(&ext_xs, pool_state.price_state.price_scale), - price, - ) - .unwrap(); - - to_future(&mut env, 600); - - let offer_amount = f64_to_dec256(200_000_f64); - let price = swap( - &mut ext_xs, - offer_amount, - pool_state.price_state.price_scale, - 0, - &_gamma, - &pool_params, - ); - pool_state - .update_price( - &pool_params, - &env, - total_lp, - &to_internal_repr(&ext_xs, pool_state.price_state.price_scale), - price, - ) - .unwrap(); - - to_future(&mut env, 12000); - - let offer_amount = f64_to_dec256(1_000_f64); - let price = swap( - &mut ext_xs, - offer_amount, - pool_state.price_state.price_scale, - 0, - &_gamma, - &pool_params, - ); - - pool_state - .update_price( - &pool_params, - &env, - total_lp, - &to_internal_repr(&ext_xs, pool_state.price_state.price_scale), - price, - ) - .unwrap(); - - to_future(&mut env, 600); - - let offer_amount = f64_to_dec256(200_000_f64); - let price = swap( - &mut ext_xs, - offer_amount, - pool_state.price_state.price_scale, - 1, - &_gamma, - &pool_params, - ); - - pool_state - .update_price( - &pool_params, - &env, - total_lp, - &to_internal_repr(&ext_xs, pool_state.price_state.price_scale), - price, - ) - .unwrap(); - - to_future(&mut env, 60); - - let offer_amount = f64_to_dec256(2_000_f64); - let price = swap( - &mut ext_xs, - offer_amount, - pool_state.price_state.price_scale, - 1, - &_gamma, - &pool_params, - ); - - pool_state - .update_price( - &pool_params, - &env, - total_lp, - &to_internal_repr(&ext_xs, pool_state.price_state.price_scale), - price, - ) - .unwrap(); - } -} diff --git a/contracts/pair_concentrated_inj/src/utils.rs b/contracts/pair_concentrated_inj/src/utils.rs index 606304117..ca81b033d 100644 --- a/contracts/pair_concentrated_inj/src/utils.rs +++ b/contracts/pair_concentrated_inj/src/utils.rs @@ -1,182 +1,19 @@ -use cosmwasm_std::{ - to_binary, wasm_execute, Addr, CosmosMsg, CustomMsg, CustomQuery, Decimal, Decimal256, Env, - Fraction, QuerierWrapper, StdError, StdResult, Storage, Uint128, Uint256, -}; -use cw20::Cw20ExecuteMsg; +use cosmwasm_std::{Addr, Env, QuerierWrapper, Storage}; use injective_cosmwasm::InjectiveQueryWrapper; use itertools::Itertools; -use astroport::asset::{Asset, AssetInfo, DecimalAsset}; -use astroport::cosmwasm_ext::{AbsDiff, IntegerToDecimal}; -use astroport::observation::Observation; -use astroport::querier::query_factory_config; +use astroport::asset::{Asset, DecimalAsset}; +use astroport::cosmwasm_ext::IntegerToDecimal; +use astroport::observation::{Observation, PrecommitObservation}; use astroport_circular_buffer::error::BufferResult; use astroport_circular_buffer::BufferManager; -use astroport_factory::state::pair_key; +use astroport_pcl_common::state::{Config, Precisions}; +use astroport_pcl_common::utils::{safe_sma_buffer_not_full, safe_sma_calculation}; -use crate::consts::{DEFAULT_SLIPPAGE, MAX_ALLOWED_SLIPPAGE, N, OFFER_PERCENT, TWO}; use crate::error::ContractError; -use crate::math::{calc_d, calc_y}; use crate::orderbook::state::OrderbookState; use crate::orderbook::utils::get_subaccount_balances_dec; -use crate::state::{Config, PoolParams, Precisions, PriceState, OBSERVATIONS}; - -/// Helper function to check the given asset infos are valid. -pub(crate) fn check_asset_infos(asset_infos: &[AssetInfo]) -> Result<(), ContractError> { - if !asset_infos.iter().all_unique() { - return Err(ContractError::DoublingAssets {}); - } - - Ok(()) -} - -/// Helper function to check that the assets in a given array are valid. -pub(crate) fn check_assets(assets: &[Asset]) -> Result<(), ContractError> { - let asset_infos = assets.iter().map(|asset| asset.info.clone()).collect_vec(); - check_asset_infos(&asset_infos) -} - -/// Mint LP tokens for a beneficiary and auto stake the tokens in the Generator contract (if auto staking is specified). -/// -/// * **recipient** LP token recipient. -/// -/// * **amount** amount of LP tokens that will be minted for the recipient. -/// -/// * **auto_stake** determines whether the newly minted LP tokens will -/// be automatically staked in the Generator on behalf of the recipient. -pub(crate) fn mint_liquidity_token_message( - querier: QuerierWrapper, - config: &Config, - contract_address: &Addr, - recipient: &Addr, - amount: Uint128, - auto_stake: bool, -) -> Result>, ContractError> -where - C: CustomQuery, - T: CustomMsg, -{ - let lp_token = &config.pair_info.liquidity_token; - - // If no auto-stake - just mint to recipient - if !auto_stake { - return Ok(vec![wasm_execute( - lp_token, - &Cw20ExecuteMsg::Mint { - recipient: recipient.to_string(), - amount, - }, - vec![], - )? - .into()]); - } - - // Mint for the pair contract and stake into the Generator contract - let generator = query_factory_config(&querier, &config.factory_addr)?.generator_address; - - if let Some(generator) = generator { - Ok(vec![ - wasm_execute( - lp_token, - &Cw20ExecuteMsg::Mint { - recipient: contract_address.to_string(), - amount, - }, - vec![], - )? - .into(), - wasm_execute( - lp_token, - &Cw20ExecuteMsg::Send { - contract: generator.to_string(), - amount, - msg: to_binary(&astroport::generator::Cw20HookMsg::DepositFor( - recipient.to_string(), - ))?, - }, - vec![], - )? - .into(), - ]) - } else { - Err(ContractError::AutoStakeError {}) - } -} - -/// Return the amount of tokens that a specific amount of LP tokens would withdraw. -/// -/// * **pools** assets available in the pool. -/// -/// * **amount** amount of LP tokens to calculate underlying amounts for. -/// -/// * **total_share** total amount of LP tokens currently issued by the pool. -pub(crate) fn get_share_in_assets( - pools: &[DecimalAsset], - amount: Uint128, - total_share: Uint128, -) -> StdResult> { - let share_ratio = if !total_share.is_zero() { - Decimal256::from_ratio(amount, total_share) - } else { - Decimal256::zero() - }; - - pools - .iter() - .map(|pool| { - Ok(DecimalAsset { - info: pool.info.clone(), - amount: pool.amount * share_ratio, - }) - }) - .collect() -} - -/// If `belief_price` and `max_spread` are both specified, we compute a new spread, -/// otherwise we just use the swap spread to check `max_spread`. -/// -/// * **belief_price** belief price used in the swap. -/// -/// * **max_spread** max spread allowed so that the swap can be executed successfuly. -/// -/// * **offer_amount** amount of assets to swap. -/// -/// * **return_amount** amount of assets a user wants to receive from the swap. -/// -/// * **spread_amount** spread used in the swap. -pub(crate) fn assert_max_spread( - belief_price: Option, - max_spread: Option, - offer_amount: Uint128, - return_amount: Uint128, - spread_amount: Uint128, -) -> Result<(), ContractError> { - let max_spread = max_spread.map(Decimal256::from).unwrap_or(DEFAULT_SLIPPAGE); - if max_spread > MAX_ALLOWED_SLIPPAGE { - return Err(ContractError::AllowedSpreadAssertion {}); - } - - if let Some(belief_price) = belief_price { - let expected_return = offer_amount - * belief_price.inv().ok_or_else(|| { - ContractError::Std(StdError::generic_err( - "Invalid belief_price. Check the input values.", - )) - })?; - - let spread_amount = expected_return.saturating_sub(return_amount); - - if return_amount < expected_return - && Decimal256::from_ratio(spread_amount, expected_return) > max_spread - { - return Err(ContractError::MaxSpreadAssertion {}); - } - } else if Decimal256::from_ratio(spread_amount, return_amount + spread_amount) > max_spread { - return Err(ContractError::MaxSpreadAssertion {}); - } - - Ok(()) -} +use crate::state::OBSERVATIONS; pub(crate) fn query_contract_balances( querier: QuerierWrapper, @@ -241,340 +78,107 @@ pub(crate) fn query_pools( Ok(contract_assets) } -/// Checks whether it possible to make a swap or not. -pub(crate) fn before_swap_check(pools: &[DecimalAsset], offer_amount: Decimal256) -> StdResult<()> { - if offer_amount.is_zero() { - return Err(StdError::generic_err("Swap amount must not be zero")); - } - if pools.iter().any(|a| a.amount.is_zero()) { - return Err(StdError::generic_err("One of the pools is empty")); - } - - Ok(()) -} - -/// This structure is for internal use only. Represents swap's result. -pub struct SwapResult { - pub dy: Decimal256, - pub spread_fee: Decimal256, - pub maker_fee: Decimal256, - pub total_fee: Decimal256, -} - -impl SwapResult { - /// Calculates **last price** and **last real price**. - /// Returns (last_price, last_real_price) where: - /// - last_price is a price for repeg algo, - pub fn calc_last_price(&self, offer_amount: Decimal256, offer_ind: usize) -> Decimal256 { - if offer_ind == 0 { - offer_amount / (self.dy + self.maker_fee) - } else { - (self.dy + self.maker_fee) / offer_amount - } - } -} - -/// Performs swap simulation to calculate a price. -pub fn calc_last_prices(xs: &[Decimal256], config: &Config, env: &Env) -> StdResult { - let mut offer_amount = Decimal256::one().min(xs[0] * OFFER_PERCENT); - if offer_amount.is_zero() { - offer_amount = Decimal256::raw(1u128); - } - - let last_price = compute_swap(xs, offer_amount, 1, config, env, Decimal256::zero())? - .calc_last_price(offer_amount, 0); - - Ok(last_price) -} - -/// Calculate swap result. -pub fn compute_swap( - xs: &[Decimal256], - offer_amount: Decimal256, - ask_ind: usize, - config: &Config, - env: &Env, - maker_fee_share: Decimal256, -) -> StdResult { - let offer_ind = 1 ^ ask_ind; - - let mut ixs = xs.to_vec(); - ixs[1] *= config.pool_state.price_state.price_scale; - - let amp_gamma = config.pool_state.get_amp_gamma(env); - let d = calc_d(&ixs, &_gamma)?; - - let offer_amount = if offer_ind == 1 { - offer_amount * config.pool_state.price_state.price_scale - } else { - offer_amount - }; - - ixs[offer_ind] += offer_amount; - - let new_y = calc_y(&ixs, d, &_gamma, ask_ind)?; - let mut dy = ixs[ask_ind] - new_y; - ixs[ask_ind] = new_y; - - let price = if ask_ind == 1 { - dy /= config.pool_state.price_state.price_scale; - config.pool_state.price_state.price_scale.inv().unwrap() - } else { - config.pool_state.price_state.price_scale - }; - - // Since price_scale moves slower than real price spread fee may become negative - let spread_fee = (offer_amount * price).saturating_sub(dy); - - let fee_rate = config.pool_params.fee(&ixs); - let total_fee = fee_rate * dy; - dy -= total_fee; - - Ok(SwapResult { - dy, - spread_fee, - maker_fee: total_fee * maker_fee_share, - total_fee, - }) -} - -/// Returns an amount of offer assets for a specified amount of ask assets. -pub fn compute_offer_amount( - xs: &[Decimal256], - mut want_amount: Decimal256, - ask_ind: usize, - config: &Config, - env: &Env, -) -> StdResult<(Decimal256, Decimal256, Decimal256)> { - let offer_ind = 1 - ask_ind; - - if ask_ind == 1 { - want_amount *= config.pool_state.price_state.price_scale - } - - let mut ixs = xs.to_vec(); - ixs[1] *= config.pool_state.price_state.price_scale; - - let amp_gamma = config.pool_state.get_amp_gamma(env); - let d = calc_d(&ixs, &_gamma)?; - - // It's hard to predict fee rate thus we use maximum possible fee rate - let before_fee = want_amount - * (Decimal256::one() - Decimal256::from(config.pool_params.out_fee)) - .inv() - .unwrap(); - let mut fee = before_fee - want_amount; - - ixs[ask_ind] -= before_fee; - - let new_y = calc_y(&ixs, d, &_gamma, offer_ind)?; - let mut dy = new_y - ixs[offer_ind]; - - let mut spread_fee = dy.saturating_sub(before_fee); - if offer_ind == 1 { - dy /= config.pool_state.price_state.price_scale; - spread_fee /= config.pool_state.price_state.price_scale; - fee /= config.pool_state.price_state.price_scale; - } - - Ok((dy, spread_fee, fee)) -} - -/// Internal function to calculate new moving average using Uint256. -/// Overflow is possible only if new average order size is greater than 2^128 - 1 which is unlikely. -fn safe_sma_calculation( - sma: Uint128, - oldest_amount: Uint128, - count: u32, - new_amount: Uint128, -) -> StdResult { - let res = (sma.full_mul(count) + Uint256::from(new_amount) - Uint256::from(oldest_amount)) - .checked_div(count.into())?; - res.try_into().map_err(StdError::from) -} - /// Calculate and save moving averages of swap sizes. pub fn accumulate_swap_sizes( storage: &mut dyn Storage, env: &Env, ob_state: &mut OrderbookState, - base_amount: Uint128, - quote_amount: Uint128, ) -> BufferResult<()> { - let mut buffer = BufferManager::new(storage, OBSERVATIONS)?; - - let new_observation; - if let Some(last_obs) = buffer.read_last(storage)? { - // Since this is circular buffer the next index contains the oldest value - let count = buffer.capacity(); - if let Some(oldest_obs) = buffer.read_single(storage, buffer.head() + 1)? { - let new_base_sma = safe_sma_calculation( - last_obs.base_sma, - oldest_obs.base_amount, - count, - base_amount, - )?; - let new_quote_sma = safe_sma_calculation( - last_obs.quote_sma, - oldest_obs.quote_amount, - count, - quote_amount, - )?; - new_observation = Observation { - base_amount, - quote_amount, - base_sma: new_base_sma, - quote_sma: new_quote_sma, - timestamp: env.block.time.seconds(), - }; + if let Some(PrecommitObservation { + base_amount, + quote_amount, + precommit_ts, + }) = PrecommitObservation::may_load(storage)? + { + let mut buffer = BufferManager::new(storage, OBSERVATIONS)?; + + let new_observation; + if let Some(last_obs) = buffer.read_last(storage)? { + // Skip saving observation if it has been already saved + if last_obs.timestamp < precommit_ts { + // Since this is circular buffer the next index contains the oldest value + let count = buffer.capacity(); + if let Some(oldest_obs) = buffer.read_single(storage, buffer.head() + 1)? { + let new_base_sma = safe_sma_calculation( + last_obs.base_sma, + oldest_obs.base_amount, + count, + base_amount, + )?; + let new_quote_sma = safe_sma_calculation( + last_obs.quote_sma, + oldest_obs.quote_amount, + count, + quote_amount, + )?; + new_observation = Observation { + base_amount, + quote_amount, + base_sma: new_base_sma, + quote_sma: new_quote_sma, + timestamp: precommit_ts, + }; + } else { + // Buffer is not full yet + let count = buffer.head(); + let base_sma = safe_sma_buffer_not_full(last_obs.base_sma, count, base_amount)?; + let quote_sma = + safe_sma_buffer_not_full(last_obs.quote_sma, count, quote_amount)?; + new_observation = Observation { + base_amount, + quote_amount, + base_sma, + quote_sma, + timestamp: precommit_ts, + }; + } + + // Enable orderbook if we have enough observations + if !ob_state.ready && (buffer.head() + 1) >= ob_state.min_trades_to_avg { + ob_state.ready(true) + } + + buffer.instant_push(storage, &new_observation)? + } } else { - // Buffer is not full yet - let count = Uint128::from(buffer.head()); - let new_base_sma = (last_obs.base_sma * count + base_amount) / (count + Uint128::one()); - let new_quote_sma = - (last_obs.quote_sma * count + quote_amount) / (count + Uint128::one()); - new_observation = Observation { - base_amount, - quote_amount, - base_sma: new_base_sma, - quote_sma: new_quote_sma, - timestamp: env.block.time.seconds(), - }; - } - - // Enable orderbook if we have enough observations - if !ob_state.ready && (buffer.head() + 1) >= ob_state.min_trades_to_avg { - ob_state.ready(true) + // Buffer is empty + if env.block.time.seconds() > precommit_ts { + new_observation = Observation { + timestamp: precommit_ts, + base_sma: base_amount, + base_amount, + quote_sma: quote_amount, + quote_amount, + }; + + buffer.instant_push(storage, &new_observation)? + } } - } else { - // Buffer is empty - new_observation = Observation { - timestamp: env.block.time.seconds(), - base_sma: base_amount, - base_amount, - quote_sma: quote_amount, - quote_amount, - }; } - buffer.instant_push(storage, &new_observation) -} - -/// Calculate provide fee applied on the amount of LP tokens. Only charged for imbalanced provide. -/// * `deposits` - internal repr of deposit -/// * `xp` - internal repr of pools -pub fn calc_provide_fee( - deposits: &[Decimal256], - xp: &[Decimal256], - params: &PoolParams, -) -> Decimal256 { - let sum = deposits[0] + deposits[1]; - let avg = sum / N; - let deviation = deposits[0].diff(avg) + deposits[1].diff(avg); - - deviation * params.fee(xp) / (sum * N) -} - -/// This is an internal function that enforces slippage tolerance for provides. Returns actual slippage. -pub fn assert_slippage_tolerance( - deposits: &[Decimal256], - actual_share: Decimal256, - price_state: &PriceState, - slippage_tolerance: Option, -) -> Result { - let slippage_tolerance = slippage_tolerance - .map(Into::into) - .unwrap_or(DEFAULT_SLIPPAGE); - if slippage_tolerance > MAX_ALLOWED_SLIPPAGE { - return Err(ContractError::AllowedSpreadAssertion {}); - } - - let deposit_value = deposits[0] + deposits[1] * price_state.price_scale; - let lp_expected = (deposit_value / TWO * deposit_value / (TWO * price_state.price_scale)) - .sqrt() - / price_state.xcp_profit_real; - let slippage = lp_expected.saturating_sub(actual_share) / lp_expected; - - if slippage > slippage_tolerance { - return Err(ContractError::MaxSpreadAssertion {}); - } - - Ok(slippage) -} - -/// Checks whether the pair is registered in the factory or not. -pub fn check_pair_registered( - querier: QuerierWrapper, - factory: &Addr, - asset_infos: &[AssetInfo], -) -> StdResult -where - C: CustomQuery, -{ - astroport_factory::state::PAIRS - .query(&querier, factory.clone(), &pair_key(asset_infos)) - .map(|inner| inner.is_some()) + Ok(()) } #[cfg(test)] mod tests { - use std::error::Error; - use std::fmt::Display; - use std::str::FromStr; - - use crate::orderbook::consts::MIN_TRADES_TO_AVG_LIMITS; use cosmwasm_std::testing::{mock_env, MockStorage}; + use cosmwasm_std::{BlockInfo, Timestamp}; use injective_cosmwasm::{MarketId, SubaccountId}; - use super::*; - - pub fn f64_to_dec(val: f64) -> T - where - T: FromStr, - T::Err: Error, - { - T::from_str(&val.to_string()).unwrap() - } - - pub fn dec_to_f64(val: impl Display) -> f64 { - f64::from_str(&val.to_string()).unwrap() - } - - #[test] - fn test_provide_fees() { - let params = PoolParams { - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - ..PoolParams::default() - }; - - let fee_rate = calc_provide_fee( - &[f64_to_dec(50_000f64), f64_to_dec(50_000f64)], - &[f64_to_dec(100_000f64), f64_to_dec(100_000f64)], - ¶ms, - ); - assert_eq!(dec_to_f64(fee_rate), 0.0); + use crate::orderbook::consts::MIN_TRADES_TO_AVG_LIMITS; - let fee_rate = calc_provide_fee( - &[f64_to_dec(99_000f64), f64_to_dec(1_000f64)], - &[f64_to_dec(100_000f64), f64_to_dec(100_000f64)], - ¶ms, - ); - assert_eq!(dec_to_f64(fee_rate), 0.001274); + use super::*; - let fee_rate = calc_provide_fee( - &[f64_to_dec(99_000f64), f64_to_dec(1_000f64)], - &[f64_to_dec(1_000f64), f64_to_dec(99_000f64)], - ¶ms, - ); - assert_eq!(dec_to_f64(fee_rate), 0.002205); + fn next_block(block: &mut BlockInfo) { + block.height += 1; + block.time = block.time.plus_seconds(1); } #[test] - fn test_swap_obeservations() { + fn test_swap_observations() { let mut store = MockStorage::new(); - let env = mock_env(); + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(1); let mut ob_state = OrderbookState { market_id: MarketId::unchecked("test"), subaccount: SubaccountId::unchecked("test"), @@ -590,34 +194,25 @@ mod tests { }; BufferManager::init(&mut store, OBSERVATIONS, 10).unwrap(); - for _ in 0..50 { - accumulate_swap_sizes( - &mut store, - &env, - &mut ob_state, - Uint128::from(1000u128), - Uint128::from(500u128), - ) - .unwrap(); + for _ in 0..=50 { + accumulate_swap_sizes(&mut store, &env, &mut ob_state).unwrap(); + PrecommitObservation::save(&mut store, &env, 1000u128.into(), 500u128.into()).unwrap(); + next_block(&mut env.block); } let buffer = BufferManager::new(&store, OBSERVATIONS).unwrap(); + let obs = buffer.read_last(&store).unwrap().unwrap(); + assert_eq!(obs.timestamp, 50); assert_eq!(buffer.head(), 0); - assert_eq!( - buffer.read_last(&store).unwrap().unwrap().base_sma.u128(), - 1000u128 - ); - assert_eq!( - buffer.read_last(&store).unwrap().unwrap().quote_sma.u128(), - 500u128 - ); + assert_eq!(obs.base_sma.u128(), 1000u128); + assert_eq!(obs.quote_sma.u128(), 500u128); } #[test] fn test_contract_ready() { let mut store = MockStorage::new(); - let env = mock_env(); + let mut env = mock_env(); let min_trades_to_avg = 10; let mut ob_state = OrderbookState { market_id: MarketId::unchecked("test"), @@ -634,27 +229,15 @@ mod tests { }; BufferManager::init(&mut store, OBSERVATIONS, min_trades_to_avg).unwrap(); - for _ in 0..min_trades_to_avg - 1 { - accumulate_swap_sizes( - &mut store, - &env, - &mut ob_state, - Uint128::from(1000u128), - Uint128::from(500u128), - ) - .unwrap(); + for _ in 0..min_trades_to_avg { + accumulate_swap_sizes(&mut store, &env, &mut ob_state).unwrap(); + PrecommitObservation::save(&mut store, &env, 1000u128.into(), 500u128.into()).unwrap(); + next_block(&mut env.block); } assert!(!ob_state.ready, "Contract should not be ready yet"); // last observation to make contract ready - accumulate_swap_sizes( - &mut store, - &env, - &mut ob_state, - Uint128::from(1000u128), - Uint128::from(500u128), - ) - .unwrap(); + accumulate_swap_sizes(&mut store, &env, &mut ob_state).unwrap(); assert!(ob_state.ready, "Contract should be ready"); } diff --git a/contracts/pair_concentrated_inj/tests/helper/mod.rs b/contracts/pair_concentrated_inj/tests/helper/mod.rs index 94100fe59..5691dbca2 100644 --- a/contracts/pair_concentrated_inj/tests/helper/mod.rs +++ b/contracts/pair_concentrated_inj/tests/helper/mod.rs @@ -7,7 +7,6 @@ use std::fmt::{Debug, Display}; use std::str::FromStr; use anyhow::Result as AnyResult; -use astroport_mocks::cw_multi_test::{AppResponse, Contract, ContractWrapper, Executor}; use cosmwasm_schema::cw_serde; use cosmwasm_schema::schemars::JsonSchema; use cosmwasm_std::{ @@ -31,13 +30,14 @@ use astroport::pair_concentrated::{ConcentratedPoolParams, ConcentratedPoolUpdat use astroport::pair_concentrated_inj::{ ConcentratedInjObParams, ExecuteMsg, OrderbookConfig, OrderbookStateResponse, QueryMsg, }; +use astroport_mocks::cw_multi_test::{AppResponse, Contract, ContractWrapper, Executor}; use astroport_pair_concentrated_injective::contract::{execute, instantiate, reply}; use astroport_pair_concentrated_injective::migrate::migrate; use astroport_pair_concentrated_injective::orderbook::state::OrderbookState; use astroport_pair_concentrated_injective::orderbook::sudo::sudo; use astroport_pair_concentrated_injective::orderbook::utils::calc_market_ids; use astroport_pair_concentrated_injective::queries::query; -use astroport_pair_concentrated_injective::state::Config; +use astroport_pcl_common::state::Config; use crate::helper::mocks::{mock_inj_app, InjApp, InjAppExt}; @@ -45,6 +45,22 @@ pub mod mocks; const INIT_BALANCE: u128 = u128::MAX; +pub fn common_pcl_params() -> ConcentratedPoolParams { + ConcentratedPoolParams { + amp: f64_to_dec(40f64), + gamma: f64_to_dec(0.000145), + mid_fee: f64_to_dec(0.0026), + out_fee: f64_to_dec(0.0045), + fee_gamma: f64_to_dec(0.00023), + repeg_profit_threshold: f64_to_dec(0.000002), + min_price_scale_delta: f64_to_dec(0.000146), + price_scale: Decimal::one(), + ma_half_time: 600, + track_asset_balances: None, + fee_share: None, + } +} + #[cw_serde] pub struct AmpGammaResponse { pub amp: Decimal, @@ -416,6 +432,16 @@ impl Helper { sender: &Addr, offer_asset: &Asset, max_spread: Option, + ) -> AnyResult { + self.swap_full_params(sender, offer_asset, max_spread, None) + } + + pub fn swap_full_params( + &mut self, + sender: &Addr, + offer_asset: &Asset, + max_spread: Option, + belief_price: Option, ) -> AnyResult { match &offer_asset.info { AssetInfo::Token { contract_addr } => { @@ -424,7 +450,7 @@ impl Helper { amount: offer_asset.amount, msg: to_binary(&Cw20HookMsg::Swap { ask_asset_info: None, - belief_price: None, + belief_price, max_spread, to: None, }) @@ -445,7 +471,7 @@ impl Helper { let msg = ExecuteMsg::Swap { offer_asset: offer_asset.clone(), ask_asset_info: None, - belief_price: None, + belief_price, max_spread, to: None, }; diff --git a/contracts/pair_concentrated_inj/tests/pair_inj_concentrated_integration.rs b/contracts/pair_concentrated_inj/tests/pair_inj_concentrated_integration.rs index 62d32b472..25518eb2b 100644 --- a/contracts/pair_concentrated_inj/tests/pair_inj_concentrated_integration.rs +++ b/contracts/pair_concentrated_inj/tests/pair_inj_concentrated_integration.rs @@ -3,25 +3,28 @@ use std::cell::RefCell; use std::rc::Rc; -use cosmwasm_std::{coins, Addr, Coin, Decimal, StdError, Uint128}; +use cosmwasm_std::{coins, Addr, Coin, Decimal, Decimal256, StdError, Uint128}; use injective_cosmwasm::InjectiveQuerier; use injective_testing::generate_inj_address; +use itertools::{max, Itertools}; use astroport::asset::{native_asset_info, AssetInfoExt, MINIMUM_LIQUIDITY_AMOUNT}; -use astroport::cosmwasm_ext::AbsDiff; +use astroport::cosmwasm_ext::{AbsDiff, IntegerToDecimal}; use astroport::factory::PairType; use astroport::pair_concentrated::{ ConcentratedPoolParams, ConcentratedPoolUpdateParams, PromoteParams, UpdatePoolParams, }; use astroport::pair_concentrated_inj::{ExecuteMsg, MigrateMsg, OrderbookConfig}; use astroport_mocks::cw_multi_test::Executor; -use astroport_pair_concentrated_injective::consts::{AMP_MAX, AMP_MIN, MA_HALF_TIME_LIMITS}; use astroport_pair_concentrated_injective::error::ContractError; use astroport_pair_concentrated_injective::orderbook::consts::MIN_TRADES_TO_AVG_LIMITS; +use astroport_pcl_common::consts::{AMP_MAX, AMP_MIN, MA_HALF_TIME_LIMITS}; +use astroport_pcl_common::error::PclError; use crate::helper::mocks::{mock_inj_app, InjAppExt, MockFundingMode}; use crate::helper::{ - dec_to_f64, f64_to_dec, orderbook_pair_contract, AppExtension, Helper, TestCoin, + common_pcl_params, dec_to_f64, f64_to_dec, orderbook_pair_contract, AppExtension, Helper, + TestCoin, }; mod helper; @@ -30,18 +33,7 @@ mod helper; fn check_wrong_initialization() { let owner = Addr::unchecked("owner"); - let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::from_ratio(2u8, 1u8), - ma_half_time: 600, - track_asset_balances: None, - }; + let params = common_pcl_params(); let mut wrong_params = params.clone(); wrong_params.amp = Decimal::zero(); @@ -55,11 +47,11 @@ fn check_wrong_initialization() { .unwrap_err(); assert_eq!( - ContractError::IncorrectPoolParam( + ContractError::PclError(PclError::IncorrectPoolParam( "amp".to_string(), AMP_MIN.to_string(), AMP_MAX.to_string() - ), + )), err.downcast().unwrap(), ); @@ -75,11 +67,11 @@ fn check_wrong_initialization() { .unwrap_err(); assert_eq!( - ContractError::IncorrectPoolParam( + ContractError::PclError(PclError::IncorrectPoolParam( "ma_half_time".to_string(), MA_HALF_TIME_LIMITS.start().to_string(), MA_HALF_TIME_LIMITS.end().to_string() - ), + )), err.downcast().unwrap(), ); @@ -116,16 +108,8 @@ fn provide_and_withdraw() { let test_coins = vec![TestCoin::native("uluna"), TestCoin::native("USDC")]; let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), price_scale: Decimal::from_ratio(2u8, 1u8), - ma_half_time: 600, - track_asset_balances: None, + ..common_pcl_params() }; let mut helper = Helper::new(&owner, test_coins.clone(), params, true).unwrap(); @@ -305,16 +289,8 @@ fn check_imbalanced_provide() { let test_coins = vec![TestCoin::native("uluna"), TestCoin::native("USDC")]; let mut params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), price_scale: Decimal::from_ratio(2u8, 1u8), - ma_half_time: 600, - track_asset_balances: None, + ..common_pcl_params() }; let mut helper = Helper::new(&owner, test_coins.clone(), params.clone(), true).unwrap(); @@ -360,20 +336,7 @@ fn provide_with_different_precision() { let test_coins = vec![TestCoin::native("FOO"), TestCoin::native("BAR")]; - let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::one(), - ma_half_time: 600, - track_asset_balances: None, - }; - - let mut helper = Helper::new(&owner, test_coins.clone(), params, true).unwrap(); + let mut helper = Helper::new(&owner, test_coins.clone(), common_pcl_params(), true).unwrap(); let assets = vec![ helper.assets[&test_coins[0]].with_balance(100_00000u128), @@ -419,20 +382,7 @@ fn swap_different_precisions() { let test_coins = vec![TestCoin::native("FOO"), TestCoin::native("BAR")]; - let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::one(), - ma_half_time: 600, - track_asset_balances: None, - }; - - let mut helper = Helper::new(&owner, test_coins.clone(), params, true).unwrap(); + let mut helper = Helper::new(&owner, test_coins.clone(), common_pcl_params(), true).unwrap(); let assets = vec![ helper.assets[&test_coins[0]].with_balance(100_000_00000u128), @@ -475,20 +425,7 @@ fn check_reverse_swap() { let test_coins = vec![TestCoin::native("uluna"), TestCoin::native("uusd")]; - let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::one(), - ma_half_time: 600, - track_asset_balances: None, - }; - - let mut helper = Helper::new(&owner, test_coins.clone(), params, true).unwrap(); + let mut helper = Helper::new(&owner, test_coins.clone(), common_pcl_params(), true).unwrap(); let assets = vec![ helper.assets[&test_coins[0]].with_balance(100_000_000000u128), @@ -516,19 +453,7 @@ fn check_swaps_simple() { let test_coins = vec![TestCoin::native("uluna"), TestCoin::native("USDC")]; - let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::one(), - ma_half_time: 600, - track_asset_balances: None, - }; - let mut helper = Helper::new(&owner, test_coins.clone(), params, true).unwrap(); + let mut helper = Helper::new(&owner, test_coins.clone(), common_pcl_params(), true).unwrap(); let user = Addr::unchecked("user"); let offer_asset = helper.assets[&test_coins[0]].with_balance(100_000000u128); @@ -568,7 +493,7 @@ fn check_swaps_simple() { helper.give_me_money(&[offer_asset.clone()], &user); let err = helper.swap(&user, &offer_asset, None).unwrap_err(); assert_eq!( - ContractError::MaxSpreadAssertion {}, + ContractError::PclError(PclError::MaxSpreadAssertion {}), err.downcast().unwrap() ); @@ -621,19 +546,7 @@ fn check_swaps_with_price_update() { let test_coins = vec![TestCoin::native("uluna"), TestCoin::native("USDC")]; - let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::one(), - ma_half_time: 600, - track_asset_balances: None, - }; - let mut helper = Helper::new(&owner, test_coins.clone(), params, true).unwrap(); + let mut helper = Helper::new(&owner, test_coins.clone(), common_pcl_params(), true).unwrap(); helper.app.next_block(1000); @@ -675,19 +588,7 @@ fn provides_and_swaps() { let test_coins = vec![TestCoin::native("uluna"), TestCoin::native("USDC")]; - let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::one(), - ma_half_time: 600, - track_asset_balances: None, - }; - let mut helper = Helper::new(&owner, test_coins.clone(), params, true).unwrap(); + let mut helper = Helper::new(&owner, test_coins.clone(), common_pcl_params(), true).unwrap(); helper.app.next_block(1000); @@ -734,14 +635,7 @@ fn check_amp_gamma_change() { let params = ConcentratedPoolParams { amp: f64_to_dec(40f64), gamma: f64_to_dec(0.0001), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::one(), - ma_half_time: 600, - track_asset_balances: None, + ..common_pcl_params() }; let mut helper = Helper::new(&owner, test_coins, params, true).unwrap(); @@ -828,20 +722,7 @@ fn check_prices() { let test_coins = vec![TestCoin::native("uusd"), TestCoin::native("USDC")]; - let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::one(), - ma_half_time: 600, - track_asset_balances: None, - }; - - let helper = Helper::new(&owner, test_coins.clone(), params, true).unwrap(); + let helper = Helper::new(&owner, test_coins.clone(), common_pcl_params(), true).unwrap(); let err = helper.query_prices().unwrap_err(); assert_eq!(StdError::generic_err("Querier contract error: Generic error: Not implemented.Use { \"observe\" : { \"seconds_ago\" : ... } } instead.") , err); @@ -853,20 +734,7 @@ fn update_owner() { let test_coins = vec![TestCoin::native("uusd"), TestCoin::native("USDC")]; - let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::one(), - ma_half_time: 600, - track_asset_balances: None, - }; - - let mut helper = Helper::new(&owner, test_coins, params, true).unwrap(); + let mut helper = Helper::new(&owner, test_coins, common_pcl_params(), true).unwrap(); let new_owner = String::from("new_owner"); @@ -947,16 +815,8 @@ fn check_orderbook_integration() { let test_coins = vec![TestCoin::native("inj"), TestCoin::native("astro")]; let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), price_scale: f64_to_dec(0.5), - ma_half_time: 600, - track_asset_balances: None, + ..common_pcl_params() }; let app = mock_inj_app(|_, _, _| {}); @@ -1016,6 +876,7 @@ fn check_orderbook_integration() { None, ) .unwrap(); + helper.next_block(false).unwrap(); } let err = helper @@ -1028,7 +889,7 @@ fn check_orderbook_integration() { let ob_state = helper.query_ob_config_smart().unwrap(); assert_eq!(ob_state.orders_number, 5); - assert_eq!(ob_state.need_reconcile, true); + assert_eq!(ob_state.need_reconcile, false); // sudo endpoint was already executed and liq. deployed in OB assert_eq!(ob_state.ready, true); let ob_config = helper.query_ob_config().unwrap(); @@ -1047,14 +908,14 @@ fn check_orderbook_integration() { .deposits .total_balance .into(); - assert_eq!(inj_deposit, 2489_766000000000000000); - assert_eq!(astro_deposit, 4979_553543); + assert_eq!(inj_deposit, 2489_981000000000000000); + assert_eq!(astro_deposit, 4979_051501); let inj_pool = helper.coin_balance(&test_coins[0], &helper.pair_addr); let astro_pool = helper.coin_balance(&test_coins[1], &helper.pair_addr); - assert_eq!(inj_pool, 497543_148893233248565365); - assert_eq!(astro_pool, 995084_822997); + assert_eq!(inj_pool, 497542_933893233248565365); + assert_eq!(astro_pool, 995085_325039); // total liquidity is close to initial provided liquidity let total_inj = inj_deposit + inj_pool; @@ -1116,16 +977,8 @@ fn check_last_withdraw() { let test_coins = vec![TestCoin::native("inj"), TestCoin::native("astro")]; let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), price_scale: f64_to_dec(0.5), - ma_half_time: 600, - track_asset_balances: None, + ..common_pcl_params() }; let mut helper = Helper::new(&owner, test_coins.clone(), params, true).unwrap(); @@ -1181,16 +1034,8 @@ fn check_deactivate_orderbook() { let test_coins = vec![TestCoin::native("inj"), TestCoin::native("astro")]; let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), price_scale: f64_to_dec(0.5), - ma_half_time: 600, - track_asset_balances: None, + ..common_pcl_params() }; let mut helper = Helper::new(&owner, test_coins.clone(), params, true).unwrap(); @@ -1580,16 +1425,8 @@ fn test_migrate_cl_to_orderbook_cl() { let test_coins = vec![TestCoin::native("inj"), TestCoin::native("astro")]; let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), price_scale: f64_to_dec(0.5), - ma_half_time: 600, - track_asset_balances: None, + ..common_pcl_params() }; let mut app = mock_inj_app(|_, _, _| {}); let market_id = app @@ -1624,6 +1461,7 @@ fn test_migrate_cl_to_orderbook_cl() { None, ) .unwrap(); + helper.next_block(true).unwrap(); } let migrate_msg = MigrateMsg::MigrateToOrderbook { @@ -1701,6 +1539,7 @@ fn test_migrate_cl_to_orderbook_cl() { None, ) .unwrap(); + helper.next_block(true).unwrap(); } // Check that orders have been created @@ -1720,8 +1559,8 @@ fn test_migrate_cl_to_orderbook_cl() { .deposits .total_balance .into(); - assert_eq!(inj_deposit, 2489_761000000000000000); - assert_eq!(astro_deposit, 4979_563465); + assert_eq!(inj_deposit, 2489_976000000000000000); + assert_eq!(astro_deposit, 4979_061419); let inj_pool = helper.coin_balance(&test_coins[0], &helper.pair_addr); let astro_pool = helper.coin_balance(&test_coins[1], &helper.pair_addr); @@ -1740,16 +1579,8 @@ fn test_wrong_assets_order() { let test_coins = vec![TestCoin::native("inj"), TestCoin::native("astro")]; let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), price_scale: f64_to_dec(0.5), - ma_half_time: 600, - track_asset_balances: None, + ..common_pcl_params() }; let mut app = mock_inj_app(|_, _, _| {}); let market_id = app @@ -1808,16 +1639,8 @@ fn test_feegrant_mode() { let test_coins = vec![TestCoin::native("inj"), TestCoin::native("astro")]; let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), price_scale: f64_to_dec(0.5), - ma_half_time: 600, - track_asset_balances: None, + ..common_pcl_params() }; let mut app = mock_inj_app(|_, _, _| {}); app.create_market( @@ -1991,15 +1814,8 @@ fn provide_withdraw_provide() { let params = ConcentratedPoolParams { amp: f64_to_dec(10f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), price_scale: Decimal::from_ratio(10u8, 1u8), - ma_half_time: 600, - track_asset_balances: None, + ..common_pcl_params() }; let mut helper = Helper::new(&owner, test_coins.clone(), params, true).unwrap(); @@ -2037,15 +1853,8 @@ fn provide_withdraw_slippage() { let params = ConcentratedPoolParams { amp: f64_to_dec(10f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), price_scale: Decimal::from_ratio(10u8, 1u8), - ma_half_time: 600, - track_asset_balances: None, + ..common_pcl_params() }; let mut helper = Helper::new(&owner, test_coins.clone(), params, true).unwrap(); @@ -2068,7 +1877,7 @@ fn provide_withdraw_slippage() { .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.02))) .unwrap_err(); assert_eq!( - ContractError::MaxSpreadAssertion {}, + ContractError::PclError(PclError::MaxSpreadAssertion {}), err.downcast().unwrap() ); // With 3% slippage it should work @@ -2085,10 +1894,87 @@ fn provide_withdraw_slippage() { .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.02))) .unwrap_err(); assert_eq!( - ContractError::MaxSpreadAssertion {}, + ContractError::PclError(PclError::MaxSpreadAssertion {}), err.downcast().unwrap(), ); helper .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.5))) .unwrap(); } + +#[test] +fn check_small_trades() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![TestCoin::native("uusd"), TestCoin::native("uluna")]; + + let params = ConcentratedPoolParams { + price_scale: f64_to_dec(4.360000915600192), + ..common_pcl_params() + }; + + let mut helper = Helper::new(&owner, test_coins.clone(), params, true).unwrap(); + + // Fully balanced but small provide + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(8_000000u128), + helper.assets[&test_coins[1]].with_balance(1_834862u128), + ]; + helper.provide_liquidity(&owner, &assets).unwrap(); + + // Trying to mess the last price with lowest possible swap + for _ in 0..1000 { + helper.app.next_block(30); + let offer_asset = helper.assets[&test_coins[1]].with_balance(1u8); + helper + .swap_full_params(&owner, &offer_asset, None, Some(Decimal::MAX)) + .unwrap(); + } + + // Check that after price scale adjustments (even they are small) internal value is still nearly balanced + let config = helper.query_config().unwrap(); + let pool = helper + .query_pool() + .unwrap() + .assets + .into_iter() + .map(|asset| asset.amount.to_decimal256(6u8).unwrap()) + .collect_vec(); + + let ixs = [pool[0], pool[1] * config.pool_state.price_state.price_scale]; + let relative_diff = ixs[0].abs_diff(ixs[1]) / max(&ixs).unwrap(); + + assert!( + relative_diff < Decimal256::percent(3), + "Internal PCL value is off. Relative_diff: {}", + relative_diff + ); + + // Trying to mess the last price with lowest possible provide + for _ in 0..1000 { + helper.app.next_block(30); + let assets = vec![helper.assets[&test_coins[1]].with_balance(1u8)]; + helper + .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.5))) + .unwrap(); + } + + // Check that after price scale adjustments (even they are small) internal value is still nearly balanced + let config = helper.query_config().unwrap(); + let pool = helper + .query_pool() + .unwrap() + .assets + .into_iter() + .map(|asset| asset.amount.to_decimal256(6u8).unwrap()) + .collect_vec(); + + let ixs = [pool[0], pool[1] * config.pool_state.price_state.price_scale]; + let relative_diff = ixs[0].abs_diff(ixs[1]) / max(&ixs).unwrap(); + + assert!( + relative_diff < Decimal256::percent(3), + "Internal PCL value is off. Relative_diff: {}", + relative_diff + ); +} diff --git a/contracts/pair_concentrated_inj/tests/pair_inj_concentrated_simulation.rs b/contracts/pair_concentrated_inj/tests/pair_inj_concentrated_simulation.rs index 8f3bd3698..0817ede31 100644 --- a/contracts/pair_concentrated_inj/tests/pair_inj_concentrated_simulation.rs +++ b/contracts/pair_concentrated_inj/tests/pair_inj_concentrated_simulation.rs @@ -2,16 +2,19 @@ extern crate core; -mod helper; +use std::collections::HashMap; + +use cosmwasm_std::{Addr, Decimal, Decimal256}; +use proptest::prelude::*; -use crate::helper::{dec_to_f64, f64_to_dec, AppExtension, Helper, TestCoin}; use astroport::asset::AssetInfoExt; use astroport::cosmwasm_ext::AbsDiff; -use astroport::pair_concentrated::ConcentratedPoolParams; use astroport_pair_concentrated_injective::error::ContractError; -use cosmwasm_std::{Addr, Decimal, Decimal256}; -use proptest::prelude::*; -use std::collections::HashMap; +use astroport_pcl_common::error::PclError; + +use crate::helper::{common_pcl_params, dec_to_f64, AppExtension, Helper, TestCoin}; + +mod helper; const MAX_EVENTS: usize = 100; @@ -21,22 +24,9 @@ fn simulate_case(case: Vec<(usize, u128, u64)>) { let test_coins = vec![TestCoin::native("uluna"), TestCoin::native("USDC")]; - let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: Decimal::one(), - ma_half_time: 600, - track_asset_balances: None, - }; - let balances = vec![100_000_000_000000u128, 100_000_000_000000u128]; - let mut helper = Helper::new(&owner, test_coins.clone(), params, true).unwrap(); + let mut helper = Helper::new(&owner, test_coins.clone(), common_pcl_params(), true).unwrap(); let assets = vec![ helper.assets[&test_coins[0]].with_balance(balances[0]), @@ -55,7 +45,7 @@ fn simulate_case(case: Vec<(usize, u128, u64)>) { if let Err(err) = helper.swap(&user, &offer_asset, None) { let err: ContractError = err.downcast().unwrap(); match err { - ContractError::MaxSpreadAssertion {} => { + ContractError::PclError(PclError::MaxSpreadAssertion {}) => { // if swap fails because of spread then skip this case println!("exceeds spread limit"); } @@ -82,20 +72,8 @@ fn simulate_provide_case(case: Vec<(impl Into, u128, u128, u64)>) { let test_coins = vec![TestCoin::native("uluna"), TestCoin::native("USDC")]; let initial_price_scale = Decimal::one(); - let params = ConcentratedPoolParams { - amp: f64_to_dec(40f64), - gamma: f64_to_dec(0.000145), - mid_fee: f64_to_dec(0.0026), - out_fee: f64_to_dec(0.0045), - fee_gamma: f64_to_dec(0.00023), - repeg_profit_threshold: f64_to_dec(0.000002), - min_price_scale_delta: f64_to_dec(0.000146), - price_scale: initial_price_scale, - ma_half_time: 600, - track_asset_balances: None, - }; - - let mut helper = Helper::new(&owner, test_coins.clone(), params, true).unwrap(); + + let mut helper = Helper::new(&owner, test_coins.clone(), common_pcl_params(), true).unwrap(); // owner makes the first provide cuz the pool charges small amount of fees let assets = vec![ @@ -117,7 +95,7 @@ fn simulate_provide_case(case: Vec<(impl Into, u128, u128, u64)>) { if let Err(err) = helper.provide_liquidity(&user, &assets) { let err: ContractError = err.downcast().unwrap(); match err { - ContractError::MaxSpreadAssertion {} => { + ContractError::PclError(PclError::MaxSpreadAssertion {}) => { // if swap fails because of spread then skip this case println!("spread limit exceeded"); } diff --git a/contracts/pair_stable/Cargo.toml b/contracts/pair_stable/Cargo.toml index 3846e5860..25a0af206 100644 --- a/contracts/pair_stable/Cargo.toml +++ b/contracts/pair_stable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-pair-stable" -version = "3.1.1" +version = "3.3.0" authors = ["Astroport"] edition = "2021" description = "The Astroport stableswap pair contract implementation" @@ -24,7 +24,7 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -astroport = { path = "../../packages/astroport", default-features = false } +astroport = { path = "../../packages/astroport", version = "3" } cw2 = { version = "0.15" } cw20 = { version = "0.15" } cosmwasm-std = { version = "1.1" } @@ -33,7 +33,7 @@ thiserror = { version = "1.0" } itertools = "0.10" cosmwasm-schema = "1.1" cw-utils = "1.0.1" -astroport-circular-buffer = { path = "../../packages/circular_buffer" } +astroport-circular-buffer = { path = "../../packages/circular_buffer", version = "0.1" } [dev-dependencies] anyhow = "1.0" diff --git a/contracts/pair_stable/src/contract.rs b/contracts/pair_stable/src/contract.rs index 980664041..d179b95e2 100644 --- a/contracts/pair_stable/src/contract.rs +++ b/contracts/pair_stable/src/contract.rs @@ -23,12 +23,12 @@ use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_ow use astroport::cosmwasm_ext::IntegerToDecimal; use astroport::factory::PairType; use astroport::pair::{ - ConfigResponse, InstantiateMsg, StablePoolParams, StablePoolUpdateParams, DEFAULT_SLIPPAGE, - MAX_ALLOWED_SLIPPAGE, + ConfigResponse, FeeShareConfig, InstantiateMsg, StablePoolParams, StablePoolUpdateParams, + DEFAULT_SLIPPAGE, MAX_ALLOWED_SLIPPAGE, MAX_FEE_SHARE_BPS, MIN_TRADE_SIZE, }; use crate::migration::{migrate_config_from_v21, migrate_config_to_v210}; -use astroport::observation::{query_observation, MIN_TRADE_SIZE, OBSERVATIONS_SIZE}; +use astroport::observation::{query_observation, PrecommitObservation, OBSERVATIONS_SIZE}; use astroport::pair::{ Cw20HookMsg, ExecuteMsg, MigrateMsg, PoolResponse, QueryMsg, ReverseSimulationResponse, SimulationResponse, StablePoolConfig, @@ -104,6 +104,7 @@ pub fn instantiate( next_amp: params.amp * AMP_PRECISION, next_amp_time: env.block.time.seconds(), greatest_precision, + fee_share: None, }; CONFIG.save(deps.storage, &config)?; @@ -658,19 +659,50 @@ pub fn swap( messages.push(return_asset.into_msg(receiver.clone())?) } + // If this pool is configured to share fees, calculate the amount to send + // to the receiver and add the transfer message + // The calculation works as follows: We take the share percentage first, + // and the remainder is then split between LPs and maker + let mut fees_commission_amount = commission_amount; + let mut fee_share_amount = Uint128::zero(); + if let Some(fee_share) = config.fee_share { + // Calculate the fee share amount from the full commission amount + let share_fee_rate = Decimal::from_ratio(fee_share.bps, 10000u16); + fee_share_amount = fees_commission_amount * share_fee_rate; + + if !fee_share_amount.is_zero() { + // Subtract the fee share amount from the commission + fees_commission_amount = fees_commission_amount.saturating_sub(fee_share_amount); + + // Build send message for the shared amount + let fee_share_msg = Asset { + info: ask_pool.info.clone(), + amount: fee_share_amount, + } + .into_msg(fee_share.recipient)?; + messages.push(fee_share_msg); + } + } + // Compute the Maker fee let mut maker_fee_amount = Uint128::zero(); if let Some(fee_address) = fee_info.fee_address { - if let Some(f) = - calculate_maker_fee(&ask_pool.info, commission_amount, fee_info.maker_fee_rate) - { + if let Some(f) = calculate_maker_fee( + &ask_pool.info, + fees_commission_amount, + fee_info.maker_fee_rate, + ) { maker_fee_amount = f.amount; messages.push(f.into_msg(fee_address)?); } } - // Store time series data. - // Skipping small unsafe values which can seriously mess oracle price due to rounding errors + // Store observation from precommit data + accumulate_swap_sizes(deps.storage, &env)?; + + // Store time series data in precommit observation. + // Skipping small unsafe values which can seriously mess oracle price due to rounding errors. + // This data will be reflected in observations in the next action. let ask_precision = get_precision(deps.storage, &ask_pool.info)?; if offer_asset_dec.amount >= MIN_TRADE_SIZE && return_amount.to_decimal256(ask_precision)? >= MIN_TRADE_SIZE @@ -678,7 +710,7 @@ pub fn swap( // Store time series data let (base_amount, quote_amount) = determine_base_quote_amount(&pools, &offer_asset, return_amount)?; - accumulate_swap_sizes(deps.storage, &env, base_amount, quote_amount)?; + PrecommitObservation::save(deps.storage, &env, base_amount, quote_amount)?; } Ok(Response::new() @@ -698,6 +730,7 @@ pub fn swap( attr("spread_amount", spread_amount), attr("commission_amount", commission_amount), attr("maker_fee_amount", maker_fee_amount), + attr("fee_share_amount", fee_share_amount), ])) } @@ -964,6 +997,7 @@ pub fn query_config(deps: Deps, env: Env) -> StdResult { block_time_last: config.block_time_last, params: Some(to_binary(&StablePoolConfig { amp: Decimal::from_ratio(compute_current_amp(&config, &env)?, AMP_PRECISION), + fee_share: config.fee_share, })?), owner: config.owner.unwrap_or(factory_config.owner), factory_addr: config.factory_addr, @@ -1065,7 +1099,7 @@ pub fn update_config( info: MessageInfo, params: Binary, ) -> Result { - let config = CONFIG.load(deps.storage)?; + let mut config = CONFIG.load(deps.storage)?; let factory_config = query_factory_config(&deps.querier, &config.factory_addr)?; if info.sender @@ -1078,15 +1112,55 @@ pub fn update_config( return Err(ContractError::Unauthorized {}); } + let mut response = Response::default(); + match from_binary::(¶ms)? { StablePoolUpdateParams::StartChangingAmp { next_amp, next_amp_time, } => start_changing_amp(config, deps, env, next_amp, next_amp_time)?, StablePoolUpdateParams::StopChangingAmp {} => stop_changing_amp(config, deps, env)?, + StablePoolUpdateParams::EnableFeeShare { + fee_share_bps, + fee_share_address, + } => { + // Enable fee sharing for this contract + // If fee sharing is already enabled, we should be able to overwrite + // the values currently set + + // Ensure the fee share isn't 0 and doesn't exceed the maximum allowed value + if fee_share_bps == 0 || fee_share_bps > MAX_FEE_SHARE_BPS { + return Err(ContractError::FeeShareOutOfBounds {}); + } + + // Set sharing config + config.fee_share = Some(FeeShareConfig { + bps: fee_share_bps, + recipient: deps.api.addr_validate(&fee_share_address)?, + }); + + CONFIG.save(deps.storage, &config)?; + + response.attributes.push(attr("action", "enable_fee_share")); + response + .attributes + .push(attr("fee_share_bps", fee_share_bps.to_string())); + response + .attributes + .push(attr("fee_share_address", fee_share_address)); + } + StablePoolUpdateParams::DisableFeeShare => { + // Disable fee sharing for this contract by setting bps and + // address back to None + config.fee_share = None; + CONFIG.save(deps.storage, &config)?; + response + .attributes + .push(attr("action", "disable_fee_share")); + } } - Ok(Response::default()) + Ok(response) } /// Start changing the AMP value. diff --git a/contracts/pair_stable/src/error.rs b/contracts/pair_stable/src/error.rs index 66109f573..ed0ae3941 100644 --- a/contracts/pair_stable/src/error.rs +++ b/contracts/pair_stable/src/error.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{CheckedMultiplyRatioError, ConversionOverflowError, OverflowError, StdError}; use thiserror::Error; -use astroport::asset::MINIMUM_LIQUIDITY_AMOUNT; +use astroport::{asset::MINIMUM_LIQUIDITY_AMOUNT, pair::MAX_FEE_SHARE_BPS}; use astroport_circular_buffer::error::BufferError; use crate::math::{MAX_AMP, MAX_AMP_CHANGE, MIN_AMP_CHANGING_TIME}; @@ -89,6 +89,12 @@ pub enum ContractError { #[error("Failed to parse or process reply message")] FailedToParseReply {}, + + #[error( + "Fee share is 0 or exceeds maximum allowed value of {} bps", + MAX_FEE_SHARE_BPS + )] + FeeShareOutOfBounds {}, } impl From for ContractError { diff --git a/contracts/pair_stable/src/migration.rs b/contracts/pair_stable/src/migration.rs index bae384963..096b917e0 100644 --- a/contracts/pair_stable/src/migration.rs +++ b/contracts/pair_stable/src/migration.rs @@ -74,6 +74,7 @@ pub fn migrate_config_to_v210(mut deps: DepsMut) -> StdResult { next_amp: cfg_v100.next_amp, next_amp_time: cfg_v100.next_amp_time, greatest_precision, + fee_share: None, }; CONFIG.save(deps.storage, &cfg)?; @@ -123,6 +124,7 @@ pub fn migrate_config_from_v21(deps: DepsMut) -> StdResult<()> { next_amp: cfg_v212.next_amp, next_amp_time: cfg_v212.next_amp_time, greatest_precision: cfg_v212.greatest_precision, + fee_share: None, }; CONFIG.save(deps.storage, &cfg)?; diff --git a/contracts/pair_stable/src/state.rs b/contracts/pair_stable/src/state.rs index f9c62beb5..76bd09a0c 100644 --- a/contracts/pair_stable/src/state.rs +++ b/contracts/pair_stable/src/state.rs @@ -1,6 +1,7 @@ use astroport::asset::{AssetInfo, PairInfo}; use astroport::common::OwnershipProposal; use astroport::observation::Observation; +use astroport::pair::FeeShareConfig; use astroport_circular_buffer::CircularBuffer; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, DepsMut, StdResult, Storage}; @@ -27,6 +28,8 @@ pub struct Config { pub next_amp_time: u64, /// The greatest precision of assets in the pool pub greatest_precision: u8, + // The config for swap fee sharing + pub fee_share: Option, } /// Circular buffer to store trade size observations diff --git a/contracts/pair_stable/src/testing.rs b/contracts/pair_stable/src/testing.rs index 3a967c060..a7c1cca03 100644 --- a/contracts/pair_stable/src/testing.rs +++ b/contracts/pair_stable/src/testing.rs @@ -775,6 +775,7 @@ fn try_native_to_token() { attr("spread_amount", 7593888.to_string()), attr("commission_amount", expected_commission_amount.to_string()), attr("maker_fee_amount", expected_maker_fee_amount.to_string()), + attr("fee_share_amount", "0"), ] ); @@ -946,6 +947,7 @@ fn try_token_to_native() { attr("spread_amount", expected_spread_amount.to_string()), attr("commission_amount", expected_commission_amount.to_string()), attr("maker_fee_amount", expected_maker_fee_amount.to_string()), + attr("fee_share_amount", "0"), ] ); diff --git a/contracts/pair_stable/src/utils.rs b/contracts/pair_stable/src/utils.rs index f9ac5896f..eb6a0f2e7 100644 --- a/contracts/pair_stable/src/utils.rs +++ b/contracts/pair_stable/src/utils.rs @@ -1,13 +1,14 @@ +use std::cmp::Ordering; + use cosmwasm_std::{ - to_binary, wasm_execute, Addr, Api, CosmosMsg, Decimal, Env, QuerierWrapper, StdError, - StdResult, Storage, Uint128, Uint256, Uint64, + to_binary, wasm_execute, Addr, Api, CosmosMsg, Decimal, Env, QuerierWrapper, StdResult, + Storage, Uint128, Uint64, }; use cw20::Cw20ExecuteMsg; use itertools::Itertools; -use std::cmp::Ordering; use astroport::asset::{Asset, AssetInfo, Decimal256Ext, DecimalAsset}; -use astroport::observation::Observation; +use astroport::observation::{Observation, PrecommitObservation}; use astroport::querier::query_factory_config; use astroport_circular_buffer::error::BufferResult; use astroport_circular_buffer::BufferManager; @@ -291,77 +292,45 @@ pub(crate) fn compute_swap( } /// Calculate and save moving averages of swap sizes. -pub fn accumulate_swap_sizes( - storage: &mut dyn Storage, - env: &Env, - base_amount: Uint128, - quote_amount: Uint128, -) -> BufferResult<()> { - let mut buffer = BufferManager::new(storage, OBSERVATIONS)?; - - let new_observation; - if let Some(last_obs) = buffer.read_last(storage)? { - // Since this is circular buffer the next index contains the oldest value - let count = buffer.capacity(); - if let Some(oldest_obs) = buffer.read_single(storage, buffer.head() + 1)? { - let new_base_sma = safe_sma_calculation( - last_obs.base_sma, - oldest_obs.base_amount, - count, - base_amount, - )?; - let new_quote_sma = safe_sma_calculation( - last_obs.quote_sma, - oldest_obs.quote_amount, - count, - quote_amount, - )?; - new_observation = Observation { - base_amount, - quote_amount, - base_sma: new_base_sma, - quote_sma: new_quote_sma, - timestamp: env.block.time.seconds(), - }; +pub fn accumulate_swap_sizes(storage: &mut dyn Storage, env: &Env) -> BufferResult<()> { + if let Some(PrecommitObservation { + base_amount, + quote_amount, + precommit_ts, + }) = PrecommitObservation::may_load(storage)? + { + let mut buffer = BufferManager::new(storage, OBSERVATIONS)?; + + if let Some(last_obs) = buffer.read_last(storage)? { + // Skip saving observation if it has been already saved + if last_obs.timestamp < precommit_ts { + buffer.instant_push( + storage, + &Observation { + base_amount, + quote_amount, + timestamp: precommit_ts, + ..Default::default() + }, + )? + } } else { - // Buffer is not full yet - let count = Uint128::from(buffer.head()); - let new_base_sma = (last_obs.base_sma * count + base_amount) / (count + Uint128::one()); - let new_quote_sma = - (last_obs.quote_sma * count + quote_amount) / (count + Uint128::one()); - new_observation = Observation { - base_amount, - quote_amount, - base_sma: new_base_sma, - quote_sma: new_quote_sma, - timestamp: env.block.time.seconds(), - }; + // Buffer is empty + if env.block.time.seconds() > precommit_ts { + buffer.instant_push( + storage, + &Observation { + timestamp: precommit_ts, + base_amount, + quote_amount, + ..Default::default() + }, + )? + } } - } else { - // Buffer is empty - new_observation = Observation { - timestamp: env.block.time.seconds(), - base_sma: base_amount, - base_amount, - quote_sma: quote_amount, - quote_amount, - }; } - buffer.instant_push(storage, &new_observation) -} - -/// Internal function to calculate new moving average using Uint256. -/// Overflow is possible only if new average order size is greater than 2^128 - 1 which is unlikely. -fn safe_sma_calculation( - sma: Uint128, - oldest_amount: Uint128, - count: u32, - new_amount: Uint128, -) -> StdResult { - let res = (sma.full_mul(count) + Uint256::from(new_amount) - Uint256::from(oldest_amount)) - .checked_div(count.into())?; - res.try_into().map_err(StdError::from) + Ok(()) } /// Internal function to determine which asset is base one, which is quote one diff --git a/contracts/pair_stable/tests/integration.rs b/contracts/pair_stable/tests/integration.rs index 6f92086a2..572bb5dea 100644 --- a/contracts/pair_stable/tests/integration.rs +++ b/contracts/pair_stable/tests/integration.rs @@ -6,9 +6,10 @@ use astroport::factory::{ QueryMsg as FactoryQueryMsg, }; use astroport::pair::{ - ConfigResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, QueryMsg, StablePoolConfig, - StablePoolParams, StablePoolUpdateParams, + ConfigResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, PoolResponse, QueryMsg, + StablePoolConfig, StablePoolParams, StablePoolUpdateParams, MAX_FEE_SHARE_BPS, }; +use astroport_pair_stable::error::ContractError; use std::cell::RefCell; use std::rc::Rc; use std::str::FromStr; @@ -1264,6 +1265,176 @@ fn update_pair_config() { assert_eq!(params.amp, Decimal::from_ratio(150u32, 1u32)); } +#[test] +fn enable_disable_fee_sharing() { + let owner = Addr::unchecked(OWNER); + let mut router = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ], + ); + + let coin_registry_address = instantiate_coin_registry( + &mut router, + Some(vec![("uusd".to_string(), 6), ("uluna".to_string(), 6)]), + ); + + let token_contract_code_id = store_token_code(&mut router); + let pair_contract_code_id = store_pair_code(&mut router); + + let factory_code_id = store_factory_code(&mut router); + + let init_msg = FactoryInstantiateMsg { + fee_address: None, + pair_configs: vec![], + token_code_id: token_contract_code_id, + generator_address: Some(String::from("generator")), + owner: owner.to_string(), + whitelist_code_id: 234u64, + coin_registry_address: coin_registry_address.to_string(), + }; + + let factory_instance = router + .instantiate_contract( + factory_code_id, + owner.clone(), + &init_msg, + &[], + "FACTORY", + None, + ) + .unwrap(); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ], + token_code_id: token_contract_code_id, + factory_addr: factory_instance.to_string(), + init_params: Some( + to_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + let pair = router + .instantiate_contract( + pair_contract_code_id, + owner.clone(), + &msg, + &[], + String::from("PAIR"), + None, + ) + .unwrap(); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Config {}) + .unwrap(); + + let params: StablePoolConfig = from_binary(&res.params.unwrap()).unwrap(); + + assert_eq!(params.amp, Decimal::from_ratio(100u32, 1u32)); + assert_eq!(params.fee_share, None); + + // Attemt to set fee sharing higher than maximum + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&StablePoolUpdateParams::EnableFeeShare { + fee_share_bps: MAX_FEE_SHARE_BPS + 1, + fee_share_address: "contract".to_string(), + }) + .unwrap(), + }; + + assert_eq!( + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap_err() + .downcast_ref::() + .unwrap(), + &ContractError::FeeShareOutOfBounds {} + ); + + // Attemt to set fee sharing to 0 + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&StablePoolUpdateParams::EnableFeeShare { + fee_share_bps: 0, + fee_share_address: "contract".to_string(), + }) + .unwrap(), + }; + + assert_eq!( + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap_err() + .downcast_ref::() + .unwrap(), + &ContractError::FeeShareOutOfBounds {} + ); + + let fee_share_bps = 500; // 5% + let fee_share_address = "contract".to_string(); + + // Set valid fee share + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&StablePoolUpdateParams::EnableFeeShare { + fee_share_bps, + fee_share_address: fee_share_address.clone(), + }) + .unwrap(), + }; + + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap(); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Config {}) + .unwrap(); + + let params: StablePoolConfig = from_binary(&res.params.unwrap()).unwrap(); + + let set_fee_share = params.fee_share.unwrap(); + assert_eq!(set_fee_share.bps, fee_share_bps); + assert_eq!(set_fee_share.recipient, fee_share_address); + + // Disable fee share + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&StablePoolUpdateParams::DisableFeeShare {}).unwrap(), + }; + + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap(); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Config {}) + .unwrap(); + + let params: StablePoolConfig = from_binary(&res.params.unwrap()).unwrap(); + assert!(params.fee_share.is_none()); +} + #[test] fn check_observe_queries() { let owner = Addr::unchecked("owner"); @@ -1509,3 +1680,323 @@ fn test_imbalance_withdraw_is_disabled() { "Generic error: Imbalanced withdraw is currently disabled" ); } + +#[test] +fn check_correct_fee_share() { + // Validate the resulting values + // We swapped 1_000000 of token X + // Fee is set to 0.05% of the swap amount resulting in 1000000 * 0.0005 = 500 + // User receives with 1000000 - 500 = 999500 + // Of the 500 fee, 10% is sent to the fee sharing contract resulting in 50 + + // Test with 10% fee share, 0.05% total fee and 50% maker fee + test_fee_share( + 5000u16, + 5u16, + 1000u16, + Uint128::from(50u64), + Uint128::from(225u64), + ); + + // Test with 5% fee share, 0.05% total fee and 50% maker fee + test_fee_share( + 5000u16, + 5u16, + 500u16, + Uint128::from(25u64), + Uint128::from(237u64), + ); + + // // Test with 5% fee share, 0.1% total fee and 33.33% maker fee + test_fee_share( + 3333u16, + 10u16, + 500u16, + Uint128::from(50u64), + Uint128::from(316u64), + ); +} + +fn test_fee_share( + maker_fee_bps: u16, + total_fee_bps: u16, + fee_share_bps: u16, + expected_fee_share: Uint128, + expected_maker_fee: Uint128, +) { + let owner = Addr::unchecked(OWNER); + let mut app = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ], + ); + + let token_code_id = store_token_code(&mut app); + + let x_amount = Uint128::new(1_000_000_000000); + let y_amount = Uint128::new(1_000_000_000000); + let x_offer = Uint128::new(1_000000); + + let token_name = "Xtoken"; + + let init_msg = TokenInstantiateMsg { + name: token_name.to_string(), + symbol: token_name.to_string(), + decimals: 6, + initial_balances: vec![Cw20Coin { + address: OWNER.to_string(), + amount: x_amount + x_offer, + }], + mint: Some(MinterResponse { + minter: String::from(OWNER), + cap: None, + }), + marketing: None, + }; + + let token_x_instance = app + .instantiate_contract( + token_code_id, + owner.clone(), + &init_msg, + &[], + token_name, + None, + ) + .unwrap(); + + let token_name = "Ytoken"; + + let init_msg = TokenInstantiateMsg { + name: token_name.to_string(), + symbol: token_name.to_string(), + decimals: 6, + initial_balances: vec![Cw20Coin { + address: OWNER.to_string(), + amount: y_amount, + }], + mint: Some(MinterResponse { + minter: String::from(OWNER), + cap: None, + }), + marketing: None, + }; + + let token_y_instance = app + .instantiate_contract( + token_code_id, + owner.clone(), + &init_msg, + &[], + token_name, + None, + ) + .unwrap(); + + let pair_code_id = store_pair_code(&mut app); + let factory_code_id = store_factory_code(&mut app); + + let maker_address = "maker".to_string(); + + let init_msg = FactoryInstantiateMsg { + fee_address: Some(maker_address.clone()), + pair_configs: vec![PairConfig { + code_id: pair_code_id, + maker_fee_bps, + total_fee_bps, + pair_type: PairType::Stable {}, + is_disabled: false, + is_generator_disabled: false, + }], + token_code_id, + generator_address: Some(String::from("generator")), + owner: String::from("owner0000"), + whitelist_code_id: 234u64, + coin_registry_address: "coin_registry".to_string(), + }; + + let factory_instance = app + .instantiate_contract( + factory_code_id, + owner.clone(), + &init_msg, + &[], + "FACTORY", + None, + ) + .unwrap(); + + let msg = FactoryExecuteMsg::CreatePair { + pair_type: PairType::Stable {}, + asset_infos: vec![ + AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + ], + init_params: Some( + to_binary(&StablePoolParams { + amp: 100, + owner: Some(owner.to_string()), + }) + .unwrap(), + ), + }; + + app.execute_contract(owner.clone(), factory_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = FactoryQueryMsg::Pair { + asset_infos: vec![ + AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + ], + }; + + let res: PairInfo = app + .wrap() + .query_wasm_smart(&factory_instance, &msg) + .unwrap(); + + let pair_instance = res.contract_addr; + + let msg = Cw20ExecuteMsg::IncreaseAllowance { + spender: pair_instance.to_string(), + expires: None, + amount: x_amount + x_offer, + }; + + app.execute_contract(owner.clone(), token_x_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = Cw20ExecuteMsg::IncreaseAllowance { + spender: pair_instance.to_string(), + expires: None, + amount: y_amount, + }; + + app.execute_contract(owner.clone(), token_y_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + amount: x_amount, + }, + Asset { + info: AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + amount: y_amount, + }, + ], + slippage_tolerance: None, + auto_stake: None, + receiver: None, + }; + + app.execute_contract(owner.clone(), pair_instance.clone(), &msg, &[]) + .unwrap(); + + let d: u128 = app + .wrap() + .query_wasm_smart(&pair_instance, &QueryMsg::QueryComputeD {}) + .unwrap(); + assert_eq!(d, 2000000000000); + + // Set up 10% fee sharing + let fee_share_address = "contract_receiver".to_string(); + + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&StablePoolUpdateParams::EnableFeeShare { + fee_share_bps, + fee_share_address: fee_share_address.clone(), + }) + .unwrap(), + }; + + app.execute_contract(owner.clone(), pair_instance.clone(), &msg, &[]) + .unwrap(); + + let user = Addr::unchecked("user"); + + let msg = Cw20ExecuteMsg::Send { + contract: pair_instance.to_string(), + msg: to_binary(&Cw20HookMsg::Swap { + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: Some(user.to_string()), + }) + .unwrap(), + amount: x_offer, + }; + + app.execute_contract(owner.clone(), token_x_instance.clone(), &msg, &[]) + .unwrap(); + + let y_expected_return = + x_offer - Uint128::from((x_offer * Decimal::from_ratio(total_fee_bps, 10000u64)).u128()); + + let msg = Cw20QueryMsg::Balance { + address: user.to_string(), + }; + + let res: BalanceResponse = app + .wrap() + .query_wasm_smart(&token_y_instance, &msg) + .unwrap(); + + assert_eq!(res.balance, y_expected_return); + + let msg = Cw20QueryMsg::Balance { + address: fee_share_address.to_string(), + }; + + let res: BalanceResponse = app + .wrap() + .query_wasm_smart(&token_y_instance, &msg) + .unwrap(); + + assert_eq!(res.balance, expected_fee_share); + + let msg = Cw20QueryMsg::Balance { + address: maker_address.to_string(), + }; + + let res: BalanceResponse = app + .wrap() + .query_wasm_smart(&token_y_instance, &msg) + .unwrap(); + + assert_eq!(res.balance, expected_maker_fee); + + app.update_block(|b| b.height += 1); + + // Assert LP balances are correct + let msg = QueryMsg::Pool {}; + let res: PoolResponse = app.wrap().query_wasm_smart(&pair_instance, &msg).unwrap(); + + assert_eq!(res.assets[0].amount, x_amount + x_offer); + assert_eq!( + res.assets[1].amount, + y_amount - y_expected_return - expected_maker_fee - expected_fee_share + ); +} diff --git a/contracts/pair_stable/tests/stablepool_tests.rs b/contracts/pair_stable/tests/stablepool_tests.rs index 91288f7d2..80fc662ad 100644 --- a/contracts/pair_stable/tests/stablepool_tests.rs +++ b/contracts/pair_stable/tests/stablepool_tests.rs @@ -464,7 +464,18 @@ fn check_pool_prices() { Some(helper.assets[&test_coins[0]].clone()), ) .unwrap(); + + // One more swap to trigger price update in the next step + helper + .swap( + &owner, + &offer_asset, + Some(helper.assets[&test_coins[0]].clone()), + ) + .unwrap(); + helper.app.next_block(86400); + assert_eq!( helper.query_observe(0).unwrap(), OracleObservation { diff --git a/contracts/periphery/fee_granter/Cargo.toml b/contracts/periphery/fee_granter/Cargo.toml index 962291483..81cbe0aa8 100644 --- a/contracts/periphery/fee_granter/Cargo.toml +++ b/contracts/periphery/fee_granter/Cargo.toml @@ -10,7 +10,7 @@ crate-type = ["cdylib", "rlib"] library = [] [dependencies] -astroport = { path = "../../../packages/astroport" } +astroport = { path = "../../../packages/astroport", version = "3" } cosmos-sdk-proto = { version = "0.19.0", default-features = false } cosmwasm-std = { version = "1.1", features = ["stargate"] } cw-storage-plus = "0.15" diff --git a/contracts/periphery/liquidity_manager/Cargo.toml b/contracts/periphery/liquidity_manager/Cargo.toml index f9285da2c..ed4188fd1 100644 --- a/contracts/periphery/liquidity_manager/Cargo.toml +++ b/contracts/periphery/liquidity_manager/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-liquidity-manager" -version = "1.0.1" +version = "1.0.2" edition = "2021" [features] @@ -15,11 +15,11 @@ cosmwasm-schema = "1.1" cw-storage-plus = "1.0" cw20 = "0.15" thiserror = "1.0" -astroport = { path = "../../../packages/astroport" } +astroport = { path = "../../../packages/astroport", version = "3" } cw20-base = { version = "0.15", features = ["library"] } -astroport-pair = { path = "../../pair", features = ["library"] } -astroport-pair-stable = { path = "../../pair_stable", features = ["library"] } -astroport-factory = { path = "../../factory", features = ["library"] } +astroport-pair = { path = "../../pair", features = ["library"], version = "1" } +astroport-pair-stable = { path = "../../pair_stable", features = ["library"], version = "3" } +astroport-factory = { path = "../../factory", features = ["library"], version = "1" } [dev-dependencies] cw-multi-test = "0.16.4" diff --git a/contracts/periphery/liquidity_manager/src/contract.rs b/contracts/periphery/liquidity_manager/src/contract.rs index 8f05ffa16..6ade1a26c 100644 --- a/contracts/periphery/liquidity_manager/src/contract.rs +++ b/contracts/periphery/liquidity_manager/src/contract.rs @@ -259,7 +259,7 @@ fn provide_liquidity( let allowance_submessages = assets .iter() .filter_map(|asset| match &asset.info { - AssetInfo::Token { contract_addr } => { + AssetInfo::Token { contract_addr } if !asset.amount.is_zero() => { let transfer_from_msg = wasm_execute( contract_addr, &Cw20ExecuteMsg::TransferFrom { diff --git a/contracts/periphery/liquidity_manager/src/query.rs b/contracts/periphery/liquidity_manager/src/query.rs index 672647d51..c3979e00f 100644 --- a/contracts/periphery/liquidity_manager/src/query.rs +++ b/contracts/periphery/liquidity_manager/src/query.rs @@ -10,7 +10,7 @@ use astroport::querier::query_supply; use astroport_pair::contract::get_share_in_assets; use crate::error::ContractError; -use crate::utils::{stableswap_provide_simulation, xyk_provide_simulation}; +use crate::utils::{convert_config, stableswap_provide_simulation, xyk_provide_simulation}; #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { @@ -95,8 +95,11 @@ fn simulate_provide( to_binary(&predicted_lp_amount) } PairType::Stable {} => { - let pair_config = - astroport_pair_stable::state::CONFIG.query(&deps.querier, pair_addr)?; + let pair_config_data = deps + .querier + .query_wasm_raw(pair_addr, b"config")? + .ok_or_else(|| StdError::generic_err("pair stable config not found"))?; + let pair_config = convert_config(deps.querier, pair_config_data)?; to_binary( &stableswap_provide_simulation( deps.querier, diff --git a/contracts/periphery/liquidity_manager/src/utils.rs b/contracts/periphery/liquidity_manager/src/utils.rs index 2b474cca8..d68cef6b9 100644 --- a/contracts/periphery/liquidity_manager/src/utils.rs +++ b/contracts/periphery/liquidity_manager/src/utils.rs @@ -1,9 +1,12 @@ use std::collections::HashMap; -use cosmwasm_std::{Addr, Decimal, Decimal256, Env, QuerierWrapper, StdError, StdResult, Uint128}; +use cosmwasm_std::{ + from_slice, Addr, Decimal, Decimal256, Env, QuerierWrapper, StdError, StdResult, Uint128, +}; use astroport::asset::{Asset, Decimal256Ext, DecimalAsset, PairInfo, MINIMUM_LIQUIDITY_AMOUNT}; use astroport::generator::QueryMsg as GeneratorQueryMsg; +use astroport::liquidity_manager::CompatPairStableConfig; use astroport::querier::{query_supply, query_token_balance}; use astroport::U256; use astroport_pair::{ @@ -185,7 +188,10 @@ pub fn stableswap_provide_simulation( config.pair_info.contract_addr.clone(), asset.info.to_string(), )? - .unwrap(); + .or_else(|| asset.info.decimals(&querier, &config.factory_addr).ok()) + .ok_or_else(|| { + StdError::generic_err(format!("Asset {asset} precision not found")) + })?; Ok(( asset.to_decimal_asset(coin_precision)?, Decimal256::with_precision(pool, coin_precision)?, @@ -236,3 +242,34 @@ pub fn stableswap_provide_simulation( Ok(share) } + +pub fn convert_config( + querier: QuerierWrapper, + config_data: Vec, +) -> StdResult { + let compat_config: CompatPairStableConfig = from_slice(&config_data)?; + + let greatest_precision = if let Some(prec) = compat_config.greatest_precision { + prec + } else { + let mut greatest_precision = 0u8; + for asset_info in &compat_config.pair_info.asset_infos { + let precision = asset_info.decimals(&querier, &compat_config.factory_addr)?; + greatest_precision = greatest_precision.max(precision); + } + greatest_precision + }; + + Ok(PairStableConfig { + owner: compat_config.owner, + pair_info: compat_config.pair_info, + factory_addr: compat_config.factory_addr, + block_time_last: compat_config.block_time_last, + init_amp: compat_config.init_amp, + init_amp_time: compat_config.init_amp_time, + next_amp: compat_config.next_amp, + next_amp_time: compat_config.next_amp_time, + greatest_precision, + fee_share: None, + }) +} diff --git a/contracts/periphery/liquidity_manager/tests/liquidity_manager_integration.rs b/contracts/periphery/liquidity_manager/tests/liquidity_manager_integration.rs index 2a0af4d9a..57b934b8d 100644 --- a/contracts/periphery/liquidity_manager/tests/liquidity_manager_integration.rs +++ b/contracts/periphery/liquidity_manager/tests/liquidity_manager_integration.rs @@ -475,3 +475,42 @@ fn test_withdraw() { err.downcast().unwrap() ); } + +#[test] +fn test_onesided_provide_stable() { + let owner = Addr::unchecked("owner"); + let test_coins = vec![TestCoin::native("uusd"), TestCoin::cw20("UST")]; + let mut helper = Helper::new( + &owner, + test_coins.clone(), + PoolParams::Stable(StablePoolParams { + amp: 40, + owner: None, + }), + ) + .unwrap(); + + // initial provide must be double-sided + helper + .provide_liquidity( + &owner, + &[ + helper.assets[&test_coins[0]].with_balance(100_000_000000_u128), + helper.assets[&test_coins[1]].with_balance(100_000_000000_u128), + ], + None, + ) + .unwrap(); + + // one-sided provide + helper + .provide_liquidity( + &owner, + &[ + helper.assets[&test_coins[0]].with_balance(100_000_000000_u128), + helper.assets[&test_coins[1]].with_balance(0u8), + ], + None, + ) + .unwrap(); +} diff --git a/contracts/periphery/native-coin-wrapper/Cargo.toml b/contracts/periphery/native-coin-wrapper/Cargo.toml index 831a35272..147c0f627 100644 --- a/contracts/periphery/native-coin-wrapper/Cargo.toml +++ b/contracts/periphery/native-coin-wrapper/Cargo.toml @@ -31,7 +31,7 @@ cw2 = "0.15" cw20 = "0.15" cw-utils = "0.15" thiserror = { version = "1.0" } -astroport = { path = "../../../packages/astroport" } +astroport = { path = "../../../packages/astroport", version = "3" } [dev-dependencies] cw-multi-test = "0.15" diff --git a/contracts/periphery/native_coin_registry/Cargo.toml b/contracts/periphery/native_coin_registry/Cargo.toml index 40619888d..ea748fef5 100644 --- a/contracts/periphery/native_coin_registry/Cargo.toml +++ b/contracts/periphery/native_coin_registry/Cargo.toml @@ -28,7 +28,7 @@ cosmwasm-storage = "1.1" cw-storage-plus = "0.15" cw2 = "0.15" thiserror = { version = "1.0" } -astroport = { path = "../../../packages/astroport" } +astroport = { path = "../../../packages/astroport", version = "3" } [dev-dependencies] cw-multi-test = "0.15" diff --git a/contracts/periphery/oracle/Cargo.toml b/contracts/periphery/oracle/Cargo.toml index 6e0968a72..0b524102d 100644 --- a/contracts/periphery/oracle/Cargo.toml +++ b/contracts/periphery/oracle/Cargo.toml @@ -25,7 +25,7 @@ cw-storage-plus = "0.15" thiserror = { version = "1.0" } cw2 = "0.15" cw20 = "0.15" -astroport = { path = "../../../packages/astroport", default-features = false } +astroport = { path = "../../../packages/astroport", version = "3" } cosmwasm-schema = { version = "1.1" } [dev-dependencies] diff --git a/contracts/periphery/shared_multisig/Cargo.toml b/contracts/periphery/shared_multisig/Cargo.toml index 0d96e3963..24358defa 100644 --- a/contracts/periphery/shared_multisig/Cargo.toml +++ b/contracts/periphery/shared_multisig/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-shared-multisig" -version = "0.1.0" +version = "1.0.0" authors = ["Astroport, Ethan Frey "] edition = "2021" @@ -17,10 +17,15 @@ cosmwasm-schema = "1.1" cw-utils = "1.0" cw2 = "1.0" cw3 = "1.0" +cw20 = "0.15" cw-storage-plus = "0.15" cosmwasm-std = "1.1" thiserror = "1.0" -astroport = { path = "../../../packages/astroport" } +itertools = "0.10" +astroport = { path = "../../../packages/astroport", version = "3" } [dev-dependencies] -cw-multi-test = "0.15" +astroport-mocks = { path = "../../../packages/astroport_mocks"} +astroport-pair = { path = "../../pair" } +astroport-pair-concentrated = { path = "../../pair_concentrated" } +astroport-generator = { path = "../../tokenomics/generator" } \ No newline at end of file diff --git a/contracts/periphery/shared_multisig/README.md b/contracts/periphery/shared_multisig/README.md index 4e357080e..7bebfb00c 100644 --- a/contracts/periphery/shared_multisig/README.md +++ b/contracts/periphery/shared_multisig/README.md @@ -1,20 +1,24 @@ # Astroport Shared Multisig -It is a multisig with two addresses created upon instantiation. Each address has its own role (dao or manager), however, +It is a multisig with two addresses created upon instantiation. Each address has its own role (manager1 or manager2), however, both have exactly the same permissions. Each role can propose a new address which can then claim that role. ## Instantiation To create the multisig, you must pass in a set of address for each one to pass a proposal. To create a 2 multisig, -pass 2 voters (DAO and Manager). +pass 2 voters (manager1 and manager2). ```json { + "factory_addr": "wasm...", "max_voting_period": { "height": 123 }, - "manager": "wasm...", - "dao": "wasm..." + "manager1": "wasm...", + "manager2": "wasm...", + "denom1": "wasm...", + "denom2": "wasm...", + "target_pool": "wasm..." } ``` @@ -81,83 +85,243 @@ Closes a proposal by ID } ``` -### `update_config` +### `setup_max_voting_period` Updates contract parameters ```json { - "update_config": { + "setup_max_voting_period": { "max_voting_period": 123 } } ``` -### `propose_new_manager` +### `start_rage_quit` + +Locks the contract and starts the migration from the target pool. + +```json +{ + "start_rage_quit": {} +} +``` + +### `complete_target_pool_migration` + +Completes the migration from the target pool. + +```json +{ + "complete_target_pool_migration": {} +} +``` + +### `update_config` + +Update configuration + +```json +{ + "update_config": { + "factory": "wasm...", + "generator": "wasm..." + } +} +``` + +### `transfer` + +Transfer coins + +```json +{ + "transfer": { + "asset": { + "native_token": { + "denom": "uusd" + } + }, + "recipient": "wasm..." + } +} +``` + +### `provide_liquidity` + +Providing Liquidity With Slippage Tolerance + +```json +{ + "provide_liquidity": { + "pool": { + "target": {} + }, + "assets": [ + { + "info": { + "token": { + "contract_addr": "wasm..." + } + }, + "amount": "1000000" + }, + { + "info": { + "native_token": { + "denom": "uusd" + } + }, + "amount": "1000000" + } + ], + "slippage_tolerance": "0.01", + "receiver": "wasm..." + } +} +``` + +### `setup_pools` + +```json +{ + "setup_pools": { + "target_pool": "wasm...", + "migration_pool": "wasm..." + } +} +``` + +### `withdraw_target_pool_lp` + +Withdraws LP tokens from the target pool. If `provide_params` is specified, liquidity will be introduced +into the migration pool in the same transaction. + +```json +{ + "withdraw_target_pool_lp": { + "withdraw_amount": "1234", + "provide_params": { + "slippage_tolerance": "0.01" + } + } +} +``` + +### `withdraw_rage_quit_lp` + +Withdraws the LP tokens from the specified pool. + +```json +{ + "withdraw_rage_quit_lp": { + "pool": { + "target": {} + }, + "withdraw_amount": "1234" + } +} +``` + +### `deposit_generator` + +Stakes the target LP tokens in the Generator contract + +```json +{ + "deposit_generator": { + "amount": "1234" + } +} +``` + +### `withdraw_generator` + +Withdraw LP tokens from the Astroport generator. + +```json +{ + "withdraw_generator": { + "amount": "1234" + } +} +``` + +### `claim_generator_rewards` + +Update generator rewards and returns them to the Multisig. + +```json +{ + "claim_generator_rewards": {} +} +``` + +### `propose_new_manager_1` Creates an offer to change the contract manager. The validity period of the offer is set in the `expires_in` variable. After `expires_in` seconds pass, the proposal expires and cannot be accepted anymore. ```json { - "propose_new_manager": { - "manager": "wasm...", + "propose_new_manager_1": { + "new_manager": "wasm...", "expires_in": 1234567 } } ``` -### `drop_manager_proposal` +### `drop_manager_1_proposal` Removes an existing offer to change the contract manager. ```json { - "drop_manager_proposal": {} + "drop_manager_1_proposal": {} } ``` -### `claim_manager` +### `claim_manager_1` Used to claim contract manager. ```json { - "claim_manager": {} + "claim_manager_1": {} } ``` -### `propose_new_dao` +### `propose_new_manager_2` -Creates an offer to change the contract DAO. The validity period of the offer is set in the `expires_in` variable. +Creates an offer to change the contract Manager2. The validity period of the offer is set in the `expires_in` variable. After `expires_in` seconds pass, the proposal expires and cannot be accepted anymore. ```json { - "propose_new_dao": { - "dao": "wasm...", + "propose_new_manager_2": { + "new_manager": "wasm...", "expires_in": 1234567 } } ``` -### `drop_dao_proposal` +### `drop_manager_2_proposal` -Removes an existing offer to change the contract DAO. +Removes an existing offer to change the contract Manager2. ```json { - "drop_dao_proposal": {} + "drop_manager_2_proposal": {} } ``` -### `claim_dao` +### `claim_manager_2` -Used to claim contract DAO. +Used to claim contract Manager2. ```json { - "claim_dao": {} + "claim_manager_2": {} } ``` diff --git a/contracts/periphery/shared_multisig/src/contract.rs b/contracts/periphery/shared_multisig/src/contract.rs index fbe3c1434..372cf180e 100644 --- a/contracts/periphery/shared_multisig/src/contract.rs +++ b/contracts/periphery/shared_multisig/src/contract.rs @@ -3,15 +3,24 @@ use std::cmp::Ordering; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Binary, BlockInfo, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Order, - Response, StdError, StdResult, + attr, to_binary, BankMsg, Binary, BlockInfo, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, + MessageInfo, Order, Response, StdError, StdResult, Uint128, WasmMsg, }; +use cw20::Cw20ExecuteMsg; +use astroport::asset::{addr_opt_validate, validate_native_denom, Asset, AssetInfo}; use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; + use astroport::shared_multisig::{ - Config, ConfigResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, MultisigRole, QueryMsg, - DEFAULT_WEIGHT, TOTAL_WEIGHT, + Config, ConfigResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, MultisigRole, PoolType, + ProvideParams, QueryMsg, DEFAULT_WEIGHT, TOTAL_WEIGHT, +}; + +use astroport::generator::{ + Cw20HookMsg, ExecuteMsg as GeneratorExecuteMsg, QueryMsg as GeneratorQueryMsg, }; + +use astroport::querier::{query_balance, query_token_balance}; use cw2::set_contract_version; use cw3::{ Proposal, ProposalListResponse, ProposalResponse, Status, Vote, VoteInfo, VoteListResponse, @@ -22,8 +31,12 @@ use cw_utils::{Duration, Expiration, Threshold}; use crate::error::ContractError; use crate::state::{ - load_vote, next_id, BALLOTS, CONFIG, DAO_PROPOSAL, DEFAULT_LIMIT, MANAGER_PROPOSAL, MAX_LIMIT, - PROPOSALS, + load_vote, next_id, update_distributed_rewards, BALLOTS, CONFIG, DEFAULT_LIMIT, + MANAGER1_PROPOSAL, MANAGER2_PROPOSAL, MAX_LIMIT, PROPOSALS, +}; +use crate::utils::{ + check_generator_deposit, check_pool, check_provide_assets, get_pool_info, + prepare_provide_after_withdraw_msg, prepare_provide_msg, prepare_withdraw_msg, }; // version info for migration info @@ -38,6 +51,8 @@ pub fn instantiate( msg: InstantiateMsg, ) -> Result { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + validate_native_denom(msg.denom1.as_str())?; + validate_native_denom(msg.denom2.as_str())?; let cfg = Config { threshold: Threshold::AbsoluteCount { @@ -45,9 +60,21 @@ pub fn instantiate( }, total_weight: TOTAL_WEIGHT, max_voting_period: msg.max_voting_period, - dao: deps.api.addr_validate(&msg.dao)?, - manager: deps.api.addr_validate(&msg.manager)?, + factory_addr: deps.api.addr_validate(&msg.factory_addr)?, + generator_addr: deps.api.addr_validate(&msg.generator_addr)?, + manager1: deps.api.addr_validate(&msg.manager1)?, + manager2: deps.api.addr_validate(&msg.manager2)?, + target_pool: addr_opt_validate(deps.api, &msg.target_pool)?, + migration_pool: None, + rage_quit_started: false, + denom1: msg.denom1, + denom2: msg.denom2, }; + + if let Some(target_pool) = &cfg.target_pool { + check_pool(&deps.querier, target_pool, &cfg)?; + } + CONFIG.save(deps.storage, &cfg)?; Ok(Response::default()) @@ -59,8 +86,47 @@ pub fn execute( env: Env, info: MessageInfo, msg: ExecuteMsg, -) -> Result, ContractError> { +) -> Result { match msg { + ExecuteMsg::UpdateConfig { factory, generator } => { + update_config(deps, env, info, factory, generator) + } + ExecuteMsg::DepositGenerator { amount } => deposit_generator(deps, env, info, amount), + ExecuteMsg::ClaimGeneratorRewards {} => claim_generator_rewards(deps), + ExecuteMsg::WithdrawGenerator { amount } => withdraw_generator(deps, env, info, amount), + ExecuteMsg::SetupMaxVotingPeriod { max_voting_period } => { + setup_max_voting_period(deps, info, env, max_voting_period) + } + ExecuteMsg::SetupPools { + target_pool, + migration_pool, + } => setup_pools(deps, env, info, target_pool, migration_pool), + ExecuteMsg::WithdrawTargetPoolLP { + withdraw_amount, + provide_params, + } => withdraw_target_pool(deps, env, info, withdraw_amount, provide_params), + ExecuteMsg::WithdrawRageQuitLP { + pool_type, + withdraw_amount, + } => withdraw_ragequit(deps, env, info, pool_type, withdraw_amount), + ExecuteMsg::Transfer { asset, recipient } => transfer(deps, info, env, &asset, recipient), + ExecuteMsg::ProvideLiquidity { + pool_type, + assets, + slippage_tolerance, + auto_stake, + .. + } => provide( + deps, + env, + info, + pool_type, + assets, + slippage_tolerance, + auto_stake, + ), + ExecuteMsg::StartRageQuit {} => start_rage_quit(deps, info), + ExecuteMsg::CompleteTargetPoolMigration {} => end_target_pool_migration(deps, info, env), ExecuteMsg::Propose { title, description, @@ -68,13 +134,10 @@ pub fn execute( latest, } => execute_propose(deps, env, info, title, description, msgs, latest), ExecuteMsg::Vote { proposal_id, vote } => execute_vote(deps, env, info, proposal_id, vote), - ExecuteMsg::Execute { proposal_id } => execute_execute(deps, env, info, proposal_id), - ExecuteMsg::Close { proposal_id } => execute_close(deps, env, info, proposal_id), - ExecuteMsg::UpdateConfig { max_voting_period } => { - update_config(deps, env, info, max_voting_period) - } - ExecuteMsg::ProposeNewManager { - manager, + ExecuteMsg::Execute { proposal_id } => execute_execute(deps, env, proposal_id), + ExecuteMsg::Close { proposal_id } => execute_close(deps, env, proposal_id), + ExecuteMsg::ProposeNewManager2 { + new_manager, expires_in, } => { let config = CONFIG.load(deps.storage)?; @@ -82,45 +145,57 @@ pub fn execute( deps, info, env, - manager, + new_manager, expires_in, - config.manager, - MANAGER_PROPOSAL, + config.manager2, + MANAGER2_PROPOSAL, ) .map_err(Into::into) } - ExecuteMsg::DropManagerProposal {} => { + ExecuteMsg::DropManager2Proposal {} => { let config = CONFIG.load(deps.storage)?; - drop_ownership_proposal(deps, info, config.manager, MANAGER_PROPOSAL) + drop_ownership_proposal(deps, info, config.manager2, MANAGER2_PROPOSAL) .map_err(Into::into) } - ExecuteMsg::ClaimManager {} => { - claim_ownership(deps, info, env, MANAGER_PROPOSAL, |deps, new_manager| { + ExecuteMsg::ClaimManager2 {} => { + claim_ownership(deps, info, env, MANAGER2_PROPOSAL, |deps, new_manager| { CONFIG .update::<_, StdError>(deps.storage, |mut v| { - v.manager = new_manager; + v.manager2 = new_manager; Ok(v) }) .map(|_| ()) }) .map_err(Into::into) } - ExecuteMsg::ProposeNewDao { dao, expires_in } => { + ExecuteMsg::ProposeNewManager1 { + new_manager, + expires_in, + } => { let config = CONFIG.load(deps.storage)?; - propose_new_owner(deps, info, env, dao, expires_in, config.dao, DAO_PROPOSAL) - .map_err(Into::into) + propose_new_owner( + deps, + info, + env, + new_manager, + expires_in, + config.manager1, + MANAGER1_PROPOSAL, + ) + .map_err(Into::into) } - ExecuteMsg::DropDaoProposal {} => { + ExecuteMsg::DropManager1Proposal {} => { let config = CONFIG.load(deps.storage)?; - drop_ownership_proposal(deps, info, config.dao, DAO_PROPOSAL).map_err(Into::into) + drop_ownership_proposal(deps, info, config.manager1, MANAGER1_PROPOSAL) + .map_err(Into::into) } - ExecuteMsg::ClaimDao {} => { - claim_ownership(deps, info, env, DAO_PROPOSAL, |deps, new_dao| { + ExecuteMsg::ClaimManager1 {} => { + claim_ownership(deps, info, env, MANAGER1_PROPOSAL, |deps, new_manager| { CONFIG .update::<_, StdError>(deps.storage, |mut v| { - v.dao = new_dao; + v.manager1 = new_manager; Ok(v) }) .map(|_| ()) @@ -130,6 +205,410 @@ pub fn execute( } } +pub fn update_config( + deps: DepsMut, + env: Env, + info: MessageInfo, + factory: Option, + generator: Option, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + let mut attributes = vec![attr("action", "update_config")]; + + // we need to approve from both managers + if info.sender != env.contract.address { + return Err(ContractError::Unauthorized {}); + } + + if config.rage_quit_started { + return Err(ContractError::RageQuitStarted {}); + } + + if let Some(factory) = factory { + config.factory_addr = deps.api.addr_validate(&factory)?; + attributes.push(attr("factory", factory)); + } + + if let Some(new_generator) = generator { + let (_, lp_token) = get_pool_info(&deps.querier, &config, PoolType::Target)?; + + // checks if all LP tokens have been withdrawn from the generator for the target pool + check_generator_deposit( + &deps.querier, + &config.generator_addr, + &lp_token, + &env.contract.address, + )?; + + if config.migration_pool.is_some() { + let (_, lp_token) = get_pool_info(&deps.querier, &config, PoolType::Migration)?; + + // checks if all LP tokens have been withdrawn from the generator for the migration pool + check_generator_deposit( + &deps.querier, + &config.generator_addr, + &lp_token, + &env.contract.address, + )?; + } + + config.generator_addr = deps.api.addr_validate(&new_generator)?; + attributes.push(attr("generator", new_generator)); + } + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new().add_attributes(attributes)) +} + +/// Stakes the target LP tokens in the Generator contract. +pub fn deposit_generator( + deps: DepsMut, + env: Env, + info: MessageInfo, + amount: Option, +) -> Result { + let cfg = CONFIG.load(deps.storage)?; + + if cfg.rage_quit_started { + return Err(ContractError::RageQuitStarted {}); + } + + if cfg.migration_pool.is_some() { + return Err(ContractError::MigrationNotCompleted {}); + } + + if info.sender != cfg.manager2 && info.sender != cfg.manager1 { + return Err(ContractError::Unauthorized {}); + } + + let (_, lp_token) = get_pool_info(&deps.querier, &cfg, PoolType::Target)?; + + let total_lp_amount = query_token_balance(&deps.querier, &lp_token, &env.contract.address)?; + let deposit_amount = amount.unwrap_or(total_lp_amount); + + if deposit_amount.is_zero() { + return Err(ContractError::InvalidZeroAmount {}); + } + + if deposit_amount > total_lp_amount { + return Err(ContractError::BalanceToSmall( + env.contract.address.to_string(), + total_lp_amount.to_string(), + )); + } + + Ok(Response::new() + .add_message(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: lp_token.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Send { + contract: cfg.generator_addr.to_string(), + amount: deposit_amount, + msg: to_binary(&Cw20HookMsg::Deposit {})?, + })?, + funds: vec![], + })) + .add_attributes([attr("action", "deposit_generator")])) +} + +/// Updates generator rewards and return it to Multisig +pub fn claim_generator_rewards(deps: DepsMut) -> Result { + let cfg = CONFIG.load(deps.storage)?; + + let (_, lp_token) = get_pool_info(&deps.querier, &cfg, PoolType::Target)?; + + Ok(Response::new() + .add_message(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cfg.generator_addr.to_string(), + msg: to_binary(&GeneratorExecuteMsg::ClaimRewards { + lp_tokens: vec![lp_token.to_string()], + })?, + funds: vec![], + })) + .add_attributes([attr("action", "claim_generator_rewards")])) +} + +/// Withdraws the LP tokens from the specified pool +pub fn withdraw_generator( + deps: DepsMut, + env: Env, + info: MessageInfo, + amount: Option, +) -> Result { + let cfg = CONFIG.load(deps.storage)?; + + if cfg.rage_quit_started { + return Err(ContractError::RageQuitStarted {}); + } + + // We should complete the migration from the target pool + if cfg.migration_pool.is_some() { + return Err(ContractError::MigrationNotCompleted {}); + } + + if info.sender != cfg.manager2 && info.sender != cfg.manager1 { + return Err(ContractError::Unauthorized {}); + } + + let (_, lp_token) = get_pool_info(&deps.querier, &cfg, PoolType::Target)?; + + let total_amount: Uint128 = deps.querier.query_wasm_smart( + &cfg.generator_addr, + &GeneratorQueryMsg::Deposit { + lp_token: lp_token.to_string(), + user: env.contract.address.to_string(), + }, + )?; + + let burn_amount = amount.unwrap_or(total_amount); + if burn_amount > total_amount { + return Err(ContractError::BalanceToSmall( + env.contract.address.to_string(), + total_amount.to_string(), + )); + } + + Ok(Response::new() + .add_message(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cfg.generator_addr.to_string(), + msg: to_binary(&GeneratorExecuteMsg::Withdraw { + lp_token: lp_token.to_string(), + amount: burn_amount, + })?, + funds: vec![], + })) + .add_attributes([attr("action", "withdraw_generator")])) +} + +/// Withdraw liquidity from the pool. +/// * **withdraw_amount** is the amount of LP tokens to burn. +/// +/// * **provide_params** is the parameters to LP tokens in the same transaction to migration_pool +pub fn withdraw_target_pool( + deps: DepsMut, + env: Env, + info: MessageInfo, + amount: Option, + provide_params: Option, +) -> Result { + let cfg = CONFIG.load(deps.storage)?; + + if cfg.rage_quit_started { + return Err(ContractError::RageQuitStarted {}); + } + + if cfg.migration_pool.is_none() { + return Err(ContractError::MigrationPoolError {}); + } + + if info.sender != cfg.manager2 && info.sender != cfg.manager1 { + return Err(ContractError::Unauthorized {}); + } + + let (pair, lp_token) = get_pool_info(&deps.querier, &cfg, PoolType::Target)?; + + let mut attributes = vec![attr("action", "withdraw_target_pool")]; + let mut messages = vec![]; + + let (withdraw_msg, burn_amount) = prepare_withdraw_msg( + &deps.querier, + &env.contract.address, + &pair, + &lp_token, + amount, + )?; + + messages.push(withdraw_msg); + + if let Some(provide_params) = provide_params { + messages.push(prepare_provide_after_withdraw_msg( + &deps.querier, + &cfg, + burn_amount, + &pair, + provide_params, + &mut attributes, + )?); + } + + Ok(Response::new() + .add_messages(messages) + .add_attributes(attributes)) +} + +/// Withdraws the LP tokens from the specified pool +pub fn withdraw_ragequit( + deps: DepsMut, + env: Env, + info: MessageInfo, + pool_type: PoolType, + amount: Option, +) -> Result { + let cfg = CONFIG.load(deps.storage)?; + + if !cfg.rage_quit_started { + return Err(ContractError::RageQuitIsNotStarted {}); + } + + if info.sender != cfg.manager2 && info.sender != cfg.manager1 { + return Err(ContractError::Unauthorized {}); + } + + let (pair, lp_token) = get_pool_info(&deps.querier, &cfg, pool_type)?; + let (withdraw_msg, _) = prepare_withdraw_msg( + &deps.querier, + &env.contract.address, + &pair, + &lp_token, + amount, + )?; + + Ok(Response::new() + .add_message(withdraw_msg) + .add_attributes([attr("action", "withdraw_ragequit")])) +} + +pub fn provide( + deps: DepsMut, + env: Env, + info: MessageInfo, + pool_type: PoolType, + assets: Vec, + slippage_tolerance: Option, + auto_stake: Option, +) -> Result { + let cfg = CONFIG.load(deps.storage)?; + + if info.sender != cfg.manager2 && info.sender != cfg.manager1 { + return Err(ContractError::Unauthorized {}); + } + + if cfg.rage_quit_started { + return Err(ContractError::RageQuitStarted {}); + } + + if pool_type == PoolType::Target { + // we cannot provide to the target pool if migration pool is set + if cfg.migration_pool.is_some() { + return Err(ContractError::MigrationPoolIsAlreadySet {}); + } + } + + check_provide_assets(&deps.querier, &env.contract.address, &assets, &cfg)?; + + let (pair, _) = get_pool_info(&deps.querier, &cfg, pool_type)?; + let message = prepare_provide_msg(&pair, assets, slippage_tolerance, auto_stake)?; + + Ok(Response::new() + .add_message(message) + .add_attribute("action", "shared_multisig_provide")) +} + +fn transfer( + deps: DepsMut, + info: MessageInfo, + env: Env, + asset: &Asset, + recipient: Option, +) -> Result { + if asset.amount.is_zero() { + return Err(StdError::generic_err("Can't send 0 amount").into()); + } + + let config = CONFIG.load(deps.storage)?; + if info.sender != config.manager1 && info.sender != config.manager2 { + return Err(ContractError::Unauthorized {}); + } + + let recipient = recipient.unwrap_or(info.sender.to_string()); + + let message = match &asset.info { + AssetInfo::Token { contract_addr } => { + let (_, lp_token) = get_pool_info(&deps.querier, &config, PoolType::Target)?; + if lp_token == *contract_addr { + return Err(ContractError::UnauthorizedTransfer( + info.sender.to_string(), + lp_token.to_string(), + )); + } + + if config.migration_pool.is_some() { + let (_, lp_token) = get_pool_info(&deps.querier, &config, PoolType::Migration)?; + if lp_token == *contract_addr { + return Err(ContractError::UnauthorizedTransfer( + info.sender.to_string(), + lp_token.to_string(), + )); + } + } + + let total_amount = + query_token_balance(&deps.querier, contract_addr, &env.contract.address)?; + update_distributed_rewards( + deps.storage, + &contract_addr.to_string(), + asset.amount, + total_amount, + &info.sender, + &config, + )?; + + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract_addr.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: recipient.clone(), + amount: asset.amount, + })?, + funds: vec![], + }) + } + AssetInfo::NativeToken { denom } => { + // Either manager cannot transfer his coin specified in the config before rage quit is not started + if (*denom == config.denom1 || *denom == config.denom2) && !config.rage_quit_started { + return Err(ContractError::RageQuitIsNotStarted {}); + } + + // Either manager can transfer only his coin specified in the config. Also, either manager can + // transfer any coins that aren't set in the config + if (*denom == config.denom1 && info.sender != config.manager1) + || (*denom == config.denom2 && info.sender != config.manager2) + { + return Err(ContractError::UnauthorizedTransfer( + info.sender.to_string(), + denom.clone(), + )); + } + + let total_amount = query_balance(&deps.querier, &env.contract.address, denom)?; + if *denom != config.denom1 && *denom != config.denom2 { + update_distributed_rewards( + deps.storage, + denom, + asset.amount, + total_amount, + &info.sender, + &config, + )?; + } + + CosmosMsg::Bank(BankMsg::Send { + to_address: recipient.clone(), + amount: vec![Coin { + denom: denom.to_string(), + amount: asset.amount, + }], + }) + } + }; + + Ok(Response::default().add_message(message).add_attributes([ + attr("action", "transfer"), + attr("recipient", recipient), + attr("amount", asset.amount), + attr("denom", asset.info.to_string()), + ])) +} + pub fn execute_propose( deps: DepsMut, env: Env, @@ -139,10 +618,10 @@ pub fn execute_propose( msgs: Vec, // we ignore earliest latest: Option, -) -> Result, ContractError> { +) -> Result { let cfg = CONFIG.load(deps.storage)?; - if info.sender != cfg.dao && info.sender != cfg.manager { + if info.sender != cfg.manager2 && info.sender != cfg.manager1 { return Err(ContractError::Unauthorized {}); } @@ -150,7 +629,6 @@ pub fn execute_propose( let max_expires = cfg.max_voting_period.after(&env.block); let mut expires = latest.unwrap_or(max_expires); let comp = expires.partial_cmp(&max_expires); - if let Some(Ordering::Greater) = comp { expires = max_expires; } else if comp.is_none() { @@ -175,15 +653,14 @@ pub fn execute_propose( PROPOSALS.save(deps.storage, id, &prop)?; // add the first yes vote from voter - if info.sender == cfg.dao { - BALLOTS.save(deps.storage, (id, &MultisigRole::Dao), &Vote::Yes)?; + if info.sender == cfg.manager1 { + BALLOTS.save(deps.storage, (id, &MultisigRole::Manager1), &Vote::Yes)?; } else { - BALLOTS.save(deps.storage, (id, &MultisigRole::Manager), &Vote::Yes)?; + BALLOTS.save(deps.storage, (id, &MultisigRole::Manager2), &Vote::Yes)?; } Ok(Response::new() .add_attribute("action", "propose") - .add_attribute("sender", info.sender) .add_attribute("proposal_id", id.to_string()) .add_attribute("status", format!("{:?}", prop.status))) } @@ -194,10 +671,10 @@ pub fn execute_vote( info: MessageInfo, proposal_id: u64, vote: Vote, -) -> Result, ContractError> { +) -> Result { let config = CONFIG.load(deps.storage)?; - if info.sender != config.dao && info.sender != config.manager { + if info.sender != config.manager1 && info.sender != config.manager2 { return Err(ContractError::Unauthorized {}); } @@ -214,10 +691,10 @@ pub fn execute_vote( } // store sender vote - if info.sender == config.dao { + if info.sender == config.manager1 { BALLOTS.update( deps.storage, - (proposal_id, &MultisigRole::Dao), + (proposal_id, &MultisigRole::Manager1), |bal| match bal { Some(_) => Err(ContractError::AlreadyVoted {}), None => Ok(vote), @@ -226,7 +703,7 @@ pub fn execute_vote( } else { BALLOTS.update( deps.storage, - (proposal_id, &MultisigRole::Manager), + (proposal_id, &MultisigRole::Manager2), |bal| match bal { Some(_) => Err(ContractError::AlreadyVoted {}), None => Ok(vote), @@ -241,7 +718,6 @@ pub fn execute_vote( Ok(Response::new() .add_attribute("action", "vote") - .add_attribute("sender", info.sender) .add_attribute("proposal_id", proposal_id.to_string()) .add_attribute("status", format!("{:?}", prop.status))) } @@ -249,7 +725,6 @@ pub fn execute_vote( pub fn execute_execute( deps: DepsMut, env: Env, - info: MessageInfo, proposal_id: u64, ) -> Result { // anyone can trigger this if the vote passed @@ -270,16 +745,10 @@ pub fn execute_execute( Ok(Response::new() .add_messages(prop.msgs) .add_attribute("action", "execute") - .add_attribute("sender", info.sender) .add_attribute("proposal_id", proposal_id.to_string())) } -pub fn execute_close( - deps: DepsMut, - env: Env, - info: MessageInfo, - proposal_id: u64, -) -> Result, ContractError> { +pub fn execute_close(deps: DepsMut, env: Env, proposal_id: u64) -> Result { // anyone can trigger this if the vote passed let mut prop = PROPOSALS.load(deps.storage, proposal_id)?; @@ -302,29 +771,162 @@ pub fn execute_close( Ok(Response::new() .add_attribute("action", "close") - .add_attribute("sender", info.sender) .add_attribute("proposal_id", proposal_id.to_string())) } -pub fn update_config( +pub fn setup_pools( deps: DepsMut, env: Env, info: MessageInfo, + target_pool: Option, + migration_pool: Option, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + let mut attributes = vec![attr("action", "setup_pools")]; + + // if we change target or migration pool, we need to approve from both managers + if info.sender != env.contract.address { + return Err(ContractError::Unauthorized {}); + } + + if config.rage_quit_started { + return Err(ContractError::RageQuitStarted {}); + } + + // change migration pool + if let Some(migration_pool) = migration_pool { + // we can change the migration pool if rage quit is not started and the migration pool is None + if config.migration_pool.is_some() { + return Err(ContractError::MigrationPoolIsAlreadySet {}); + } + + let migration_pool_addr = deps.api.addr_validate(&migration_pool)?; + check_pool(&deps.querier, &migration_pool_addr, &config)?; + + config.migration_pool = Some(migration_pool_addr); + attributes.push(attr("migration_pool", migration_pool)); + } + + // change target pool + if let Some(target_pool) = target_pool { + if config.target_pool.is_some() { + return Err(ContractError::TargetPoolIsAlreadySet {}); + } + + let target_pool_addr = deps.api.addr_validate(&target_pool)?; + check_pool(&deps.querier, &target_pool_addr, &config)?; + + config.target_pool = Some(target_pool_addr); + attributes.push(attr("target_pool", target_pool)); + } + + if config.target_pool.eq(&config.migration_pool) { + return Err(ContractError::PoolsError {}); + } + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new().add_attributes(attributes)) +} + +pub fn setup_max_voting_period( + deps: DepsMut, + info: MessageInfo, + env: Env, max_voting_period: Duration, -) -> Result, ContractError> { +) -> Result { let mut config = CONFIG.load(deps.storage)?; + let mut attributes = vec![attr("action", "update_config")]; + + if config.rage_quit_started { + return Err(ContractError::RageQuitStarted {}); + } + // we need to approve from both managers if info.sender != env.contract.address { return Err(ContractError::Unauthorized {}); } config.max_voting_period = max_voting_period; + attributes.push(attr("max_voting_period", max_voting_period.to_string())); + CONFIG.save(deps.storage, &config)?; - Ok(Response::new() - .add_attribute("action", "update_config") - .add_attribute("sender", info.sender) - .add_attribute("max_voting_period", max_voting_period.to_string())) + Ok(Response::new().add_attributes(attributes)) +} + +pub fn start_rage_quit(deps: DepsMut, info: MessageInfo) -> Result { + let mut config = CONFIG.load(deps.storage)?; + + if info.sender != config.manager1 && info.sender != config.manager2 { + return Err(ContractError::Unauthorized {}); + } + + if config.rage_quit_started { + return Err(ContractError::RageQuitStarted {}); + } + + config.rage_quit_started = true; + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new().add_attributes(vec![attr("action", "start_rage_quit")])) +} + +pub fn end_target_pool_migration( + deps: DepsMut, + info: MessageInfo, + env: Env, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + let mut attributes = vec![attr("action", "end_target_pool_migration")]; + + // the other options either manager can change alone + if info.sender != config.manager1 && info.sender != config.manager2 { + return Err(ContractError::Unauthorized {}); + } + + if config.rage_quit_started { + return Err(ContractError::RageQuitStarted {}); + } + + let (target_pool, lp_token) = get_pool_info(&deps.querier, &config, PoolType::Target)?; + + // checks if all LP tokens have been withdrawn from the generator + check_generator_deposit( + &deps.querier, + &config.generator_addr, + &lp_token, + &env.contract.address, + )?; + + // we cannot set the target pool to None + if config.migration_pool.is_none() { + return Err(ContractError::MigrationPoolError {}); + } + + // checks if all LP tokens have been withdrawn from the target pool + let total_amount = query_token_balance(&deps.querier, lp_token, env.contract.address)?; + if !total_amount.is_zero() { + return Err(ContractError::TargetPoolAmountError {}); + } + + attributes.push(attr("old_target_pool", target_pool.as_str())); + attributes.push(attr( + "old_migration_pool", + config.migration_pool.clone().unwrap().as_str(), + )); + config.target_pool = config.migration_pool.clone(); + config.migration_pool = None; + + attributes.push(attr( + "new_target_pool", + config.target_pool.clone().unwrap().as_str(), + )); + attributes.push(attr("new_migration_pool", "None")); + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new().add_attributes(attributes)) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -354,8 +956,15 @@ fn query_config(deps: Deps) -> StdResult { Ok(ConfigResponse { threshold: cfg.threshold.to_response(cfg.total_weight), max_voting_period: cfg.max_voting_period, - dao: cfg.dao, - manager: cfg.manager, + manager1: cfg.manager1.into(), + manager2: cfg.manager2.into(), + target_pool: cfg.target_pool, + migration_pool: cfg.migration_pool, + rage_quit_started: cfg.rage_quit_started, + denom1: cfg.denom1, + denom2: cfg.denom2, + factory: cfg.factory_addr.into(), + generator: cfg.generator_addr.to_string(), }) } @@ -439,10 +1048,10 @@ fn query_vote(deps: Deps, proposal_id: u64, voter: String) -> StdResult StdResult StdResult { let mut votes = vec![]; - if let Some(vote_info) = load_vote(deps, (proposal_id, &MultisigRole::Dao))? { + if let Some(vote_info) = load_vote(deps, (proposal_id, &MultisigRole::Manager1))? { votes.push(vote_info); } - if let Some(vote_info) = load_vote(deps, (proposal_id, &MultisigRole::Manager))? { + if let Some(vote_info) = load_vote(deps, (proposal_id, &MultisigRole::Manager2))? { votes.push(vote_info); } diff --git a/contracts/periphery/shared_multisig/src/error.rs b/contracts/periphery/shared_multisig/src/error.rs index 28d192b29..ace0b252f 100644 --- a/contracts/periphery/shared_multisig/src/error.rs +++ b/contracts/periphery/shared_multisig/src/error.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::StdError; +use cosmwasm_std::{DivideByZeroError, OverflowError, StdError}; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -32,4 +32,73 @@ pub enum ContractError { #[error("Contract can't be migrated!")] MigrationError {}, + + #[error("Target pool is not set")] + TargetPoolError {}, + + #[error("Target pool is already set")] + TargetPoolIsAlreadySet {}, + + #[error("Target pool is not empty")] + TargetPoolAmountError {}, + + #[error("Withdraw all LP tokens from the generator before migrating the target pool")] + GeneratorAmountError {}, + + #[error("Migration pool is not set")] + MigrationPoolError {}, + + #[error("Migration pool is already set")] + MigrationPoolIsAlreadySet {}, + + #[error("Complete migration from the target pool")] + MigrationNotCompleted {}, + + #[error("Target and migration pools cannot be the same")] + PoolsError {}, + + #[error("Unsupported pair type. Allowed pair types are: xyk, concentrated")] + PairTypeError {}, + + #[error("Operation is unavailable. Rage quit has already started")] + RageQuitStarted {}, + + #[error("Operation is unavailable. Rage quit is not started")] + RageQuitIsNotStarted {}, + + #[error("Unauthorized: {0} cannot transfer {1}")] + UnauthorizedTransfer(String, String), + + #[error("The asset {0} does not belong to the target pool")] + InvalidAsset(String), + + #[error("CW20 tokens unsupported in the target pool. Use native token instead")] + UnsupportedCw20 {}, + + #[error( + "Asset balance mismatch between the argument and the Multisig balance. \ + Available Multisig balance for {0}: {1}" + )] + AssetBalanceMismatch(String, String), + + #[error("Insufficient balance for: {0}. Available balance: {1}")] + BalanceToSmall(String, String), + + #[error("Invalid zero amount")] + InvalidZeroAmount {}, + + #[error("Claim all rewards from the generator before migrating the target pool")] + ClaimAmountError {}, +} + +impl From for ContractError { + fn from(o: OverflowError) -> Self { + StdError::from(o).into() + } +} + +impl From for ContractError { + fn from(err: DivideByZeroError) -> Self { + StdError::from(err).into() + } } diff --git a/contracts/periphery/shared_multisig/src/lib.rs b/contracts/periphery/shared_multisig/src/lib.rs index 6c1df8978..9f406168c 100644 --- a/contracts/periphery/shared_multisig/src/lib.rs +++ b/contracts/periphery/shared_multisig/src/lib.rs @@ -1,5 +1,6 @@ pub mod contract; mod error; pub mod state; +mod utils; pub use crate::error::ContractError; diff --git a/contracts/periphery/shared_multisig/src/state.rs b/contracts/periphery/shared_multisig/src/state.rs index 1dcc48b18..a5a1219b6 100644 --- a/contracts/periphery/shared_multisig/src/state.rs +++ b/contracts/periphery/shared_multisig/src/state.rs @@ -1,5 +1,7 @@ -use cosmwasm_std::{Deps, StdResult, Storage}; +use cosmwasm_std::{Addr, Deps, StdResult, Storage, Uint128}; +use std::ops::Deref; +use crate::ContractError; use astroport::common::OwnershipProposal; use astroport::shared_multisig::{Config, MultisigRole, DEFAULT_WEIGHT}; use cw3::{Proposal, Vote, VoteInfo}; @@ -11,11 +13,16 @@ pub const PROPOSAL_COUNT: Item = Item::new("proposal_count"); pub const BALLOTS: Map<(u64, &MultisigRole), Vote> = Map::new("votes"); pub const PROPOSALS: Map = Map::new("proposals"); -/// Contains a proposal to change contract Manager. -pub const MANAGER_PROPOSAL: Item = Item::new("manager_proposal"); +/// Contains a proposal to change contract Manager One. +pub const MANAGER1_PROPOSAL: Item = Item::new("manager1_proposal"); -/// Contains a proposal to change contract DAO. -pub const DAO_PROPOSAL: Item = Item::new("dao_proposal"); +/// Contains a proposal to change contract Manager Two. +pub const MANAGER2_PROPOSAL: Item = Item::new("manager2_proposal"); + +/// Key is reward token + manager +/// Values is amount of distributed rewards +pub const DISTRIBUTED_REWARDS: Map<(String, &MultisigRole), Uint128> = + Map::new("distributed_rewards"); // settings for pagination pub const MAX_LIMIT: u32 = 30; @@ -31,7 +38,7 @@ pub fn load_vote(deps: Deps, key: (u64, &MultisigRole)) -> StdResult StdResult Result { + Ok( + if let Some(amount) = DISTRIBUTED_REWARDS.may_load(store, (denom.to_string(), role))? { + amount + } else { + Uint128::zero() + }, + ) +} + +pub(crate) fn update_distributed_rewards( + store: &mut dyn Storage, + denom: &String, + amount: Uint128, + total_amount: Uint128, + sender: &Addr, + cfg: &Config, +) -> Result<(), ContractError> { + let released_manager1 = released_rewards(store.deref(), denom, &MultisigRole::Manager1)?; + let released_manager2 = released_rewards(store.deref(), denom, &MultisigRole::Manager2)?; + + let sender_released = if sender == cfg.manager1 { + released_manager1 + } else { + released_manager2 + }; + + let allowed_amount = (total_amount + released_manager1 + released_manager2) + .checked_div(Uint128::new(2))? + .checked_sub(sender_released)?; + + if amount > allowed_amount { + return Err(ContractError::BalanceToSmall( + sender.to_string(), + allowed_amount.to_string(), + )); + } + + if sender == cfg.manager1 { + DISTRIBUTED_REWARDS.save( + store, + (denom.to_string(), &MultisigRole::Manager1), + &(sender_released + amount), + )?; + } else { + DISTRIBUTED_REWARDS.save( + store, + (denom.to_string(), &MultisigRole::Manager2), + &(sender_released + amount), + )?; + } + + Ok(()) +} diff --git a/contracts/periphery/shared_multisig/src/utils.rs b/contracts/periphery/shared_multisig/src/utils.rs new file mode 100644 index 000000000..213187b5b --- /dev/null +++ b/contracts/periphery/shared_multisig/src/utils.rs @@ -0,0 +1,208 @@ +use crate::ContractError; +use astroport::asset::{Asset, AssetInfo, PairInfo}; +use astroport::pair::ExecuteMsg as PairExecuteMsg; +use astroport::pair::{Cw20HookMsg as PairCw20HookMsg, QueryMsg as PairQueryMsg}; + +use astroport::factory::PairType; +use astroport::generator::QueryMsg as GeneratorQueryMsg; +use astroport::querier::{query_balance, query_pair_info, query_token_balance}; +use astroport::shared_multisig::{Config, PoolType, ProvideParams}; +use cosmwasm_std::{ + attr, to_binary, Addr, Attribute, CosmosMsg, Decimal, QuerierWrapper, StdError, StdResult, + Uint128, WasmMsg, +}; +use cw20::Cw20ExecuteMsg; +use itertools::Itertools; + +pub(crate) fn prepare_provide_after_withdraw_msg( + querier: &QuerierWrapper, + cfg: &Config, + burn_amount: Uint128, + burn_pool: &Addr, + provide_params: ProvideParams, + attributes: &mut Vec, +) -> Result { + // we should check if migration pool exists and than provide + let (migration_pool, _) = get_pool_info(querier, cfg, PoolType::Migration)?; + + let assets: Vec = querier.query_wasm_smart( + burn_pool, + &PairQueryMsg::Share { + amount: burn_amount, + }, + )?; + + attributes.push(attr("second_action", "provide")); + attributes.push(attr("provide_pool", migration_pool.to_string().as_str())); + attributes.push(attr("provide_assets", assets.iter().join(", "))); + + Ok(prepare_provide_msg( + &migration_pool, + assets, + provide_params.slippage_tolerance, + provide_params.auto_stake, + )?) +} + +pub(crate) fn prepare_withdraw_msg( + querier: &QuerierWrapper, + account_addr: &Addr, + pair: &Addr, + lp_token: &Addr, + amount: Option, +) -> Result<(CosmosMsg, Uint128), ContractError> { + let total_amount = query_token_balance(querier, lp_token, account_addr)?; + let burn_amount = amount.unwrap_or(total_amount); + if burn_amount > total_amount { + return Err(ContractError::BalanceToSmall( + account_addr.to_string(), + total_amount.to_string(), + )); + } + + Ok(( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: lp_token.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Send { + contract: pair.to_string(), + msg: to_binary(&PairCw20HookMsg::WithdrawLiquidity { assets: vec![] })?, + amount: burn_amount, + })?, + funds: vec![], + }), + burn_amount, + )) +} + +pub(crate) fn prepare_provide_msg( + contract_addr: &Addr, + assets: Vec, + slippage_tolerance: Option, + auto_stake: Option, +) -> StdResult { + Ok(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract_addr.to_string(), + funds: assets + .iter() + .map(|asset| asset.as_coin()) + .collect::>()?, + msg: to_binary(&PairExecuteMsg::ProvideLiquidity { + assets, + slippage_tolerance, + auto_stake, + receiver: None, + })?, + })) +} + +pub(crate) fn check_provide_assets( + querier: &QuerierWrapper, + account: &Addr, + assets: &[Asset], + cfg: &Config, +) -> Result<(), ContractError> { + for asset in assets { + let denom = check_denom(&asset.info, cfg)?; + + let balance = query_balance(querier, account, denom)?; + if asset.amount > balance { + return Err(ContractError::AssetBalanceMismatch( + asset.info.to_string(), + balance.to_string(), + )); + } + } + + Ok(()) +} + +pub(crate) fn check_denom(asset_info: &AssetInfo, cfg: &Config) -> Result { + let denom = match &asset_info { + AssetInfo::NativeToken { denom } => &**denom, + AssetInfo::Token { .. } => return Err(ContractError::UnsupportedCw20 {}), + }; + + if cfg.denom1 != denom && cfg.denom2 != denom { + return Err(ContractError::InvalidAsset(denom.to_string())); + } + + Ok(denom.to_string()) +} + +pub(crate) fn check_pool( + querier: &QuerierWrapper, + contract_addr: &Addr, + cfg: &Config, +) -> Result<(), ContractError> { + // check pair assets + let pair: PairInfo = querier.query_wasm_smart(contract_addr, &PairQueryMsg::Pair {})?; + for asset_info in &pair.asset_infos { + check_denom(asset_info, cfg)?; + } + + // check if pair is registered in the factory + let pair_info: PairInfo = query_pair_info(querier, &cfg.factory_addr, &pair.asset_infos) + .map_err(|_| { + ContractError::Std(StdError::generic_err(format!( + "The pair is not registered: {}-{}", + cfg.denom1, cfg.denom2 + ))) + })?; + + // check if pool type is either xyk or PCL + if !pair_info.pair_type.eq(&PairType::Xyk {}) + && !pair_info + .pair_type + .eq(&PairType::Custom("concentrated".to_string())) + { + return Err(ContractError::PairTypeError {}); + } + + Ok(()) +} + +pub(crate) fn get_pool_info( + querier: &QuerierWrapper, + cfg: &Config, + pool_type: PoolType, +) -> Result<(Addr, Addr), ContractError> { + match pool_type { + PoolType::Target => match &cfg.target_pool { + Some(target_pool) => { + let pair_info: PairInfo = + querier.query_wasm_smart(target_pool, &PairQueryMsg::Pair {})?; + Ok((target_pool.clone(), pair_info.liquidity_token)) + } + None => Err(ContractError::TargetPoolError {}), + }, + PoolType::Migration => match &cfg.migration_pool { + Some(migration_pool) => { + let pair_info: PairInfo = + querier.query_wasm_smart(migration_pool, &PairQueryMsg::Pair {})?; + Ok((migration_pool.clone(), pair_info.liquidity_token)) + } + None => Err(ContractError::MigrationPoolError {}), + }, + } +} + +pub(crate) fn check_generator_deposit( + querier: &QuerierWrapper, + generator_addr: &Addr, + lp_token: &Addr, + user: &Addr, +) -> Result<(), ContractError> { + let generator_total_amount: Uint128 = querier.query_wasm_smart( + generator_addr, + &GeneratorQueryMsg::Deposit { + lp_token: lp_token.to_string(), + user: user.to_string(), + }, + )?; + + if !generator_total_amount.is_zero() { + return Err(ContractError::GeneratorAmountError {}); + } + + Ok(()) +} diff --git a/contracts/periphery/shared_multisig/tests/integration.rs b/contracts/periphery/shared_multisig/tests/integration.rs index f385d9102..c64f7e132 100644 --- a/contracts/periphery/shared_multisig/tests/integration.rs +++ b/contracts/periphery/shared_multisig/tests/integration.rs @@ -1,13 +1,18 @@ #![cfg(not(tarpaulin_include))] -use cosmwasm_std::{Addr, BankMsg, Coin, StdError, Uint128}; -use cw3::{ - ProposalListResponse, ProposalResponse, Status, Vote, VoteInfo, VoteListResponse, VoteResponse, -}; -use cw_multi_test::{App, ContractWrapper, Executor}; -use cw_utils::{Duration, Expiration, ThresholdResponse}; +use astroport::asset::{Asset, AssetInfo}; +use astroport::generator::PendingTokenResponse; +use cosmwasm_std::{to_binary, Addr, Coin, CosmosMsg, Decimal, Uint128, WasmMsg}; +use cw20::Cw20ExecuteMsg; +use cw3::{Status, Vote, VoteInfo, VoteListResponse, VoteResponse}; +use cw_utils::{Duration, ThresholdResponse}; +use std::{cell::RefCell, rc::Rc}; -use astroport::shared_multisig::{ConfigResponse, ExecuteMsg, InstantiateMsg, QueryMsg}; +use astroport::shared_multisig::{ExecuteMsg, PoolType, ProvideParams}; + +use astroport_mocks::cw_multi_test::{App, Executor}; +use astroport_mocks::shared_multisig::MockSharedMultisigBuilder; +use astroport_mocks::{astroport_address, MockFactoryBuilder, MockGeneratorBuilder}; fn mock_app(owner: &Addr, coins: Option>) -> App { let app = App::new(|router, _, storage| { @@ -21,56 +26,26 @@ fn mock_app(owner: &Addr, coins: Option>) -> App { app } -fn store_shared_multisig_code(app: &mut App) -> u64 { - let contract = Box::new(ContractWrapper::new_with_empty( - astroport_shared_multisig::contract::execute, - astroport_shared_multisig::contract::instantiate, - astroport_shared_multisig::contract::query, - )); - - app.store_code(contract) -} - -fn shared_multisig_instance(app: &mut App, owner: Addr, dao: String, manager: String) -> Addr { - let shared_multisig_code_id = store_shared_multisig_code(app); - - app.instantiate_contract( - shared_multisig_code_id, - owner, - &InstantiateMsg { - max_voting_period: Duration::Height(3), - dao, - manager, - }, - &[], - "Astroport shared multisig", - None, - ) - .unwrap() -} - const OWNER: &str = "owner"; -const DAO: &str = "dao"; -const MANAGER: &str = "manager"; +const MANAGER1: &str = "manager1"; +const MANAGER2: &str = "manager2"; const CHEATER: &str = "cheater"; #[test] fn proper_initialization() { - let owner = Addr::unchecked("owner"); - let manager = Addr::unchecked("manager"); - let dao = Addr::unchecked("dao"); - let mut app = mock_app(&owner, None); + let manager2 = Addr::unchecked("manager2"); + let manager1 = Addr::unchecked("manager1"); - let shared_addr = - shared_multisig_instance(&mut app, owner, DAO.to_string(), MANAGER.to_string()); + let router = Rc::new(RefCell::new(App::default())); - let config_res: ConfigResponse = app - .wrap() - .query_wasm_smart(&shared_addr, &QueryMsg::Config {}) - .unwrap(); + let factory = MockFactoryBuilder::new(&router).instantiate(); + let shared_multisig = + MockSharedMultisigBuilder::new(&router).instantiate(&factory.address, None, None); - assert_eq!(manager, config_res.manager); - assert_eq!(dao, config_res.dao); + let config_res = shared_multisig.query_config().unwrap(); + + assert_eq!(manager2, config_res.manager2); + assert_eq!(manager1, config_res.manager1); assert_eq!(Duration::Height(3), config_res.max_voting_period); assert_eq!( ThresholdResponse::AbsoluteCount { @@ -82,34 +57,35 @@ fn proper_initialization() { } #[test] -fn check_update_manager() { - let owner = Addr::unchecked("owner"); - let manager = Addr::unchecked("manager"); - let dao = Addr::unchecked("dao"); +fn check_update_manager2() { + let manager1 = Addr::unchecked("manager1"); + let manager2 = Addr::unchecked("manager2"); let new_manager = Addr::unchecked("new_manager"); - let recipient = Addr::unchecked("recipient"); - let mut app = mock_app(&owner, None); - let shared_addr = - shared_multisig_instance(&mut app, owner, DAO.to_string(), MANAGER.to_string()); + let router = Rc::new(RefCell::new(App::default())); + let factory = MockFactoryBuilder::new(&router).instantiate(); + let shared_multisig = + MockSharedMultisigBuilder::new(&router).instantiate(&factory.address, None, None); // New manager - let msg = ExecuteMsg::ProposeNewManager { - manager: new_manager.to_string(), + let msg = ExecuteMsg::ProposeNewManager2 { + new_manager: "new_manager".to_string(), expires_in: 100, // seconds }; - let err = app - .execute_contract(dao.clone(), shared_addr.clone(), &msg, &[]) + let err = router + .borrow_mut() + .execute_contract(manager1.clone(), shared_multisig.address.clone(), &msg, &[]) .unwrap_err(); assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); // Claim before proposal - let err = app + let err = router + .borrow_mut() .execute_contract( new_manager.clone(), - shared_addr.clone(), - &ExecuteMsg::ClaimManager {}, + shared_multisig.address.clone(), + &ExecuteMsg::ClaimManager1 {}, &[], ) .unwrap_err(); @@ -118,65 +94,55 @@ fn check_update_manager() { "Generic error: Ownership proposal not found" ); - let propose_msg = ExecuteMsg::Propose { - title: "Transfer 100 tokens".to_string(), - description: "Need to transfer tokens".to_string(), - msgs: vec![BankMsg::Send { - to_address: recipient.to_string(), - amount: vec![Coin { - denom: "utrn".to_string(), - amount: Uint128::new(100_000_000), - }], - } - .into()], - latest: None, - }; - - // try to propose from manager - app.execute_contract(manager.clone(), shared_addr.clone(), &propose_msg, &[]) - .unwrap(); - - // Try to propose new manager - app.execute_contract(manager.clone(), shared_addr.clone(), &msg, &[]) + // Try to propose new manager2 + router + .borrow_mut() + .execute_contract(manager2.clone(), shared_multisig.address.clone(), &msg, &[]) .unwrap(); - // Claim from DAO - let err = app + // Claim from manager1 + let err = router + .borrow_mut() .execute_contract( - dao.clone(), - shared_addr.clone(), - &ExecuteMsg::ClaimManager {}, + manager1.clone(), + shared_multisig.address.clone(), + &ExecuteMsg::ClaimManager2 {}, &[], ) .unwrap_err(); assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); - // Drop manager proposal - let err = app + // Drop manager1 proposal + let err = router + .borrow_mut() .execute_contract( new_manager.clone(), - shared_addr.clone(), - &ExecuteMsg::DropManagerProposal {}, + shared_multisig.address.clone(), + &ExecuteMsg::DropManager1Proposal {}, &[], ) .unwrap_err(); - // new_manager is not an manager yet + + // new_manager is not an manager1 yet assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); - app.execute_contract( - manager.clone(), - shared_addr.clone(), - &ExecuteMsg::DropManagerProposal {}, - &[], - ) - .unwrap(); + router + .borrow_mut() + .execute_contract( + manager2.clone(), + shared_multisig.address.clone(), + &ExecuteMsg::DropManager2Proposal {}, + &[], + ) + .unwrap(); - // Try to claim manager - let err = app + // Try to claim manager2 + let err = router + .borrow_mut() .execute_contract( new_manager.clone(), - shared_addr.clone(), - &ExecuteMsg::ClaimManager {}, + shared_multisig.address.clone(), + &ExecuteMsg::ClaimManager2 {}, &[], ) .unwrap_err(); @@ -186,57 +152,59 @@ fn check_update_manager() { ); // Propose new manager again - app.execute_contract(manager.clone(), shared_addr.clone(), &msg, &[]) + router + .borrow_mut() + .execute_contract(manager2.clone(), shared_multisig.address.clone(), &msg, &[]) .unwrap(); - // Claim manager - app.execute_contract( - new_manager.clone(), - shared_addr.clone(), - &ExecuteMsg::ClaimManager {}, - &[], - ) - .unwrap(); + // Claim manager2 + router + .borrow_mut() + .execute_contract( + new_manager.clone(), + shared_multisig.address.clone(), + &ExecuteMsg::ClaimManager2 {}, + &[], + ) + .unwrap(); // Let's query the contract state - let res: ConfigResponse = app - .wrap() - .query_wasm_smart(&shared_addr, &QueryMsg::Config {}) - .unwrap(); + let res = shared_multisig.query_config().unwrap(); - assert_eq!(res.manager, new_manager); - assert_eq!(res.dao, dao); + assert_eq!(res.manager2, new_manager); + assert_eq!(res.manager1, manager1); } #[test] -fn check_update_dao() { - let owner = Addr::unchecked("owner"); - let manager = Addr::unchecked("manager"); - let dao = Addr::unchecked("dao"); - let new_dao = Addr::unchecked("new_dao"); - let recipient = Addr::unchecked("recipient"); - let mut app = mock_app(&owner, None); - - let shared_addr = - shared_multisig_instance(&mut app, owner, DAO.to_string(), MANAGER.to_string()); - - // New DAO - let msg = ExecuteMsg::ProposeNewDao { - dao: new_dao.to_string(), +fn check_update_manager1() { + let manager2 = Addr::unchecked("manager2"); + let manager1 = Addr::unchecked("manager1"); + let new_manager1 = Addr::unchecked("new_manager1"); + + let router = Rc::new(RefCell::new(App::default())); + let factory = MockFactoryBuilder::new(&router).instantiate(); + let shared_multisig = + MockSharedMultisigBuilder::new(&router).instantiate(&factory.address, None, None); + + // New manager1 + let msg = ExecuteMsg::ProposeNewManager1 { + new_manager: new_manager1.to_string(), expires_in: 100, // seconds }; - let err = app - .execute_contract(manager.clone(), shared_addr.clone(), &msg, &[]) + let err = router + .borrow_mut() + .execute_contract(manager2.clone(), shared_multisig.address.clone(), &msg, &[]) .unwrap_err(); assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); // Claim before proposal - let err = app + let err = router + .borrow_mut() .execute_contract( - new_dao.clone(), - shared_addr.clone(), - &ExecuteMsg::ClaimDao {}, + new_manager1.clone(), + shared_multisig.address.clone(), + &ExecuteMsg::ClaimManager1 {}, &[], ) .unwrap_err(); @@ -245,66 +213,55 @@ fn check_update_dao() { "Generic error: Ownership proposal not found" ); - let propose_msg = ExecuteMsg::Propose { - title: "Transfer 100 tokens".to_string(), - description: "Need to transfer tokens".to_string(), - msgs: vec![BankMsg::Send { - to_address: recipient.to_string(), - amount: vec![Coin { - denom: "utrn".to_string(), - amount: Uint128::new(100_000_000), - }], - } - .into()], - latest: None, - }; - - // try to propose from DAO - app.execute_contract(dao.clone(), shared_addr.clone(), &propose_msg, &[]) - .unwrap(); - - // Try to propose new DAO - app.execute_contract(dao.clone(), shared_addr.clone(), &msg, &[]) + // Try to propose new manager1 + router + .borrow_mut() + .execute_contract(manager1.clone(), shared_multisig.address.clone(), &msg, &[]) .unwrap(); - // Claim from manager - let err = app + // Claim from manager2 + let err = router + .borrow_mut() .execute_contract( - manager.clone(), - shared_addr.clone(), - &ExecuteMsg::ClaimDao {}, + manager2.clone(), + shared_multisig.address.clone(), + &ExecuteMsg::ClaimManager1 {}, &[], ) .unwrap_err(); assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); - // Drop DAO proposal - let err = app + // Drop manager1 proposal + let err = router + .borrow_mut() .execute_contract( - new_dao.clone(), - shared_addr.clone(), - &ExecuteMsg::DropDaoProposal {}, + new_manager1.clone(), + shared_multisig.address.clone(), + &ExecuteMsg::DropManager1Proposal {}, &[], ) .unwrap_err(); - // new_dao is not an DAO yet + // new_manager1 is not an manager1 yet assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); - app.execute_contract( - dao.clone(), - shared_addr.clone(), - &ExecuteMsg::DropDaoProposal {}, - &[], - ) - .unwrap(); + router + .borrow_mut() + .execute_contract( + manager1.clone(), + shared_multisig.address.clone(), + &ExecuteMsg::DropManager1Proposal {}, + &[], + ) + .unwrap(); - // Try to claim DAO - let err = app + // Try to claim manager1 + let err = router + .borrow_mut() .execute_contract( - new_dao.clone(), - shared_addr.clone(), - &ExecuteMsg::ClaimDao {}, + new_manager1.clone(), + shared_multisig.address.clone(), + &ExecuteMsg::ClaimManager1 {}, &[], ) .unwrap_err(); @@ -313,665 +270,2022 @@ fn check_update_dao() { "Generic error: Ownership proposal not found" ); - // Propose new DAO again - app.execute_contract(dao.clone(), shared_addr.clone(), &msg, &[]) + // Propose new manager1 again + router + .borrow_mut() + .execute_contract(manager1.clone(), shared_multisig.address.clone(), &msg, &[]) .unwrap(); - // Claim DAO - app.execute_contract( - new_dao.clone(), - shared_addr.clone(), - &ExecuteMsg::ClaimDao {}, - &[], - ) - .unwrap(); - - // Let's query the contract state - let res: ConfigResponse = app - .wrap() - .query_wasm_smart(&shared_addr, &QueryMsg::Config {}) + // Claim manager1 + router + .borrow_mut() + .execute_contract( + new_manager1.clone(), + shared_multisig.address.clone(), + &ExecuteMsg::ClaimManager1 {}, + &[], + ) .unwrap(); - assert_eq!(res.manager, manager); - assert_eq!(res.dao, new_dao); + // Let's query the contract state + let res = shared_multisig.query_config().unwrap(); + assert_eq!(res.manager2, manager2); + assert_eq!(res.manager1, new_manager1); } #[test] -fn shared_multisig_controls() { - let dao = Addr::unchecked(DAO); - let manager = Addr::unchecked(MANAGER); - let owner = Addr::unchecked(OWNER); +fn test_proposal() { + let manager1 = Addr::unchecked(MANAGER1); + let manager2 = Addr::unchecked(MANAGER2); let cheater = Addr::unchecked(CHEATER); - let recipient = Addr::unchecked("recipient"); + let astroport = astroport_address(); + + let denom1 = String::from("untrn"); + let denom2 = String::from("ibc/astro"); + let denom3 = String::from("usdt"); + + let router = Rc::new(RefCell::new(mock_app( + &astroport, + Some(vec![ + Coin { + denom: denom1.clone(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: denom2.clone(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: denom3, + amount: Uint128::new(100_000_000_000u128), + }, + ]), + ))); - let mut router = mock_app( - &owner, - Some(vec![Coin { - denom: "utrn".to_string(), - amount: Uint128::new(100_000_000_000u128), - }]), - ); + let factory = MockFactoryBuilder::new(&router).instantiate(); + let coin_registry = factory.coin_registry(); + coin_registry.add(vec![(denom1.to_owned(), 6), (denom2.to_owned(), 6)]); - let shared_addr = shared_multisig_instance( - &mut router, - owner.clone(), - DAO.to_string(), - MANAGER.to_string(), + let pcl = factory.instantiate_concentrated_pair( + &[ + AssetInfo::NativeToken { + denom: denom1.clone(), + }, + AssetInfo::NativeToken { + denom: denom2.clone(), + }, + ], + None, ); - // Sends tokens to the multisig - router - .send_tokens( - owner.clone(), - shared_addr.clone(), - &[Coin { - denom: "utrn".to_string(), - amount: Uint128::new(200_000_000u128), - }], - ) - .unwrap(); - - // Check the recipient's balance - let res = router - .wrap() - .query_balance(recipient.to_string(), "utrn") - .unwrap(); - assert_eq!(res.amount, Uint128::zero()); - assert_eq!(res.denom, "utrn"); - - // Check the holder's balance - let res = router - .wrap() - .query_balance(shared_addr.to_string(), "utrn") - .unwrap(); - assert_eq!(res.amount, Uint128::new(200_000_000)); - assert_eq!(res.denom, "utrn"); - - let transfer_msg = BankMsg::Send { - to_address: recipient.to_string(), - amount: vec![Coin { - denom: "utrn".to_string(), - amount: Uint128::new(100_000_000), - }], - }; + let shared_multisig = + MockSharedMultisigBuilder::new(&router).instantiate(&factory.address, None, None); - let propose_msg = ExecuteMsg::Propose { - title: "Transfer 100 tokens".to_string(), - description: "Need to transfer tokens".to_string(), - msgs: vec![transfer_msg.into()], - latest: None, - }; + let setup_pools_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: shared_multisig.address.to_string(), + msg: to_binary(&ExecuteMsg::SetupPools { + target_pool: None, + migration_pool: Some(pcl.address.to_string()), + }) + .unwrap(), + funds: vec![], + }); // try to propose from cheater - let err = router - .execute_contract(cheater.clone(), shared_addr.clone(), &propose_msg, &[]) + let err = shared_multisig + .propose(&cheater, vec![setup_pools_msg.clone()]) .unwrap_err(); assert_eq!("Unauthorized", err.root_cause().to_string()); - // try to propose from DAO - router - .execute_contract(dao.clone(), shared_addr.clone(), &propose_msg, &[]) + // try to propose from manager1 + shared_multisig + .propose(&manager1, vec![setup_pools_msg.clone()]) .unwrap(); // Try to vote from cheater - let err = router - .execute_contract( - cheater.clone(), - shared_addr.clone(), - &ExecuteMsg::Vote { - proposal_id: 1, - vote: Vote::Yes, - }, - &[], - ) - .unwrap_err(); + let err = shared_multisig.vote(&cheater, 1, Vote::Yes).unwrap_err(); assert_eq!("Unauthorized", err.root_cause().to_string()); - // Try to execute with only 1 vote - let err = router - .execute_contract( - dao.clone(), - shared_addr.clone(), - &ExecuteMsg::Execute { proposal_id: 1 }, - &[], - ) - .unwrap_err(); + // Try to execute from cheater + let err = shared_multisig.execute(&cheater, 1).unwrap_err(); assert_eq!( "Proposal must have passed and not yet been executed", err.root_cause().to_string() ); - // Check DAO vote - let res: VoteResponse = router - .wrap() - .query_wasm_smart( - &shared_addr, - &QueryMsg::Vote { - proposal_id: 1, - voter: dao.to_string(), - }, - ) - .unwrap(); + // Try to execute from manager1 + let err = shared_multisig.execute(&manager1, 1).unwrap_err(); + assert_eq!( + "Proposal must have passed and not yet been executed", + err.root_cause().to_string() + ); + + // Check manager1 vote + let res = shared_multisig.query_vote(1, &manager1).unwrap(); assert_eq!( res, VoteResponse { vote: Some(VoteInfo { proposal_id: 1, - voter: dao.to_string(), + voter: manager1.to_string(), vote: Vote::Yes, weight: 1 }), } ); - // Check Manager vote - let res: VoteResponse = router - .wrap() - .query_wasm_smart( - &shared_addr, - &QueryMsg::Vote { - proposal_id: 1, - voter: manager.to_string(), - }, - ) - .unwrap(); + // Check manager2 vote + let res = shared_multisig.query_vote(1, &manager2).unwrap(); assert_eq!(res.vote, None); - // Try to vote from Manager - router - .execute_contract( - manager.clone(), - shared_addr.clone(), - &ExecuteMsg::Vote { - proposal_id: 1, - vote: Vote::No, - }, - &[], - ) - .unwrap(); + // Try to vote from manager2 + shared_multisig.vote(&manager2, 1, Vote::No).unwrap(); - // Check Manager vote - let res: VoteResponse = router - .wrap() - .query_wasm_smart( - &shared_addr, - &QueryMsg::Vote { - proposal_id: 1, - voter: manager.to_string(), - }, - ) - .unwrap(); + // Check manager2 vote + let res = shared_multisig.query_vote(1, &manager2).unwrap(); assert_eq!( res, VoteResponse { vote: Some(VoteInfo { proposal_id: 1, - voter: manager.to_string(), + voter: manager2.to_string(), vote: Vote::No, weight: 1 }) } ); - let err = router - .execute_contract( - cheater.clone(), - shared_addr.clone(), - &ExecuteMsg::Execute { proposal_id: 1 }, - &[], - ) - .unwrap_err(); + // Check manager2 vote + let res = shared_multisig.query_votes(1).unwrap(); assert_eq!( - "Proposal must have passed and not yet been executed", - err.root_cause().to_string() + res, + VoteListResponse { + votes: vec![ + VoteInfo { + proposal_id: 1, + voter: "manager1".to_string(), + vote: Vote::Yes, + weight: 1 + }, + VoteInfo { + proposal_id: 1, + voter: "manager2".to_string(), + vote: Vote::No, + weight: 1 + } + ] + } ); - // Try to vote from Manager - let err = router - .execute_contract( - manager.clone(), - shared_addr.clone(), - &ExecuteMsg::Vote { - proposal_id: 1, - vote: Vote::Yes, - }, - &[], - ) - .unwrap_err(); + // Try to vote from Manager2 + let err = shared_multisig.vote(&manager2, 1, Vote::Yes).unwrap_err(); assert_eq!( "Already voted on this proposal", err.root_cause().to_string() ); - // Check the recipient's balance - let res = router - .wrap() - .query_balance(recipient.to_string(), "utrn") - .unwrap(); - assert_eq!(res.amount, Uint128::zero()); - assert_eq!(res.denom, "utrn"); - - // Check the holder's balance - let res = router - .wrap() - .query_balance(shared_addr.to_string(), "utrn") + // try to propose the second proposal from manager1 + shared_multisig + .propose(&manager1, vec![setup_pools_msg.clone()]) .unwrap(); - assert_eq!(res.amount, Uint128::new(200_000_000)); - assert_eq!(res.denom, "utrn"); - // try to propose from DAO - router - .execute_contract(dao.clone(), shared_addr.clone(), &propose_msg, &[]) - .unwrap(); + router.borrow_mut().update_block(|b| { + b.height += 4; + }); - router.update_block(|b| b.height += 4); + // check that the first proposal is rejected + let res = shared_multisig.query_proposal(1).unwrap(); + assert_eq!(res.status, Status::Rejected); - // Try to vote from Manager - let err = router - .execute_contract( - manager.clone(), - shared_addr.clone(), - &ExecuteMsg::Vote { - proposal_id: 2, - vote: Vote::Yes, - }, - &[], - ) - .unwrap_err(); + // Try to vote from Manager2 + let err = shared_multisig.vote(&manager2, 2, Vote::Yes).unwrap_err(); assert_eq!( "Proposal voting period has expired", err.root_cause().to_string() ); - let err = router - .execute_contract( - cheater.clone(), - shared_addr.clone(), - &ExecuteMsg::Execute { proposal_id: 2 }, - &[], - ) - .unwrap_err(); + // try to execute the second proposal from the cheater + let err = shared_multisig.execute(&cheater, 2).unwrap_err(); assert_eq!( "Proposal must have passed and not yet been executed", err.root_cause().to_string() ); - // Check votes status - let res: VoteResponse = router - .wrap() - .query_wasm_smart( - &shared_addr, - &QueryMsg::Vote { - proposal_id: 1, - voter: manager.to_string(), - }, - ) - .unwrap(); - assert_eq!( - res, - VoteResponse { - vote: Some(VoteInfo { - proposal_id: 1, - voter: manager.to_string(), - vote: Vote::No, - weight: 1 - }) - } - ); - - let res: ProposalResponse = router - .wrap() - .query_wasm_smart(&shared_addr, &QueryMsg::Proposal { proposal_id: 1 }) - .unwrap(); - assert_eq!(res.status, Status::Rejected); - - let res: ProposalResponse = router - .wrap() - .query_wasm_smart(&shared_addr, &QueryMsg::Proposal { proposal_id: 2 }) - .unwrap(); + // check that the second proposal is rejected + let res = shared_multisig.query_proposal(2).unwrap(); assert_eq!(res.status, Status::Rejected); - // Try to update config from Manager - let err = router - .execute_contract( - manager.clone(), - shared_addr.clone(), - &ExecuteMsg::UpdateConfig { - max_voting_period: Duration::Height(10), - }, - &[], - ) + // Try to setup max voting period config from Manager2 + let err = shared_multisig + .setup_max_voting_period(&manager2, Duration::Height(10)) .unwrap_err(); assert_eq!(err.root_cause().to_string(), "Unauthorized"); - // Try to update config from multisig contract - router - .execute_contract( - shared_addr.clone(), - shared_addr.clone(), - &ExecuteMsg::UpdateConfig { - max_voting_period: Duration::Height(10), - }, - &[], - ) + // Try to setup max voting period config direct from multisig + shared_multisig + .setup_max_voting_period(&shared_multisig.address, Duration::Height(10)) .unwrap(); - let res: ConfigResponse = router - .wrap() - .query_wasm_smart(&shared_addr, &QueryMsg::Config {}) - .unwrap(); + // check configuration + let res = shared_multisig.query_config().unwrap(); assert_eq!(res.max_voting_period, Duration::Height(10)); - // try to propose from DAO - router - .execute_contract(dao.clone(), shared_addr.clone(), &propose_msg, &[]) + // try to propose from manager1 + shared_multisig + .propose(&manager1, vec![setup_pools_msg.clone()]) .unwrap(); - // Try to vote from Manager - router - .execute_contract( - manager.clone(), - shared_addr.clone(), - &ExecuteMsg::Vote { - proposal_id: 3, - vote: Vote::Yes, + // Try to vote from Manager2 + shared_multisig.vote(&manager2, 3, Vote::Yes).unwrap(); + + // Try to execute the third proposal + shared_multisig.execute(&manager2, 3).unwrap(); + + // check configuration + let res = shared_multisig.query_config().unwrap(); + assert_eq!(res.target_pool, None); + assert_eq!(res.migration_pool, Some(pcl.address)); +} + +#[test] +fn test_transfer() { + let manager1 = Addr::unchecked(MANAGER1); + let manager2 = Addr::unchecked(MANAGER2); + let owner = Addr::unchecked(OWNER); + let recipient = Addr::unchecked("recipient"); + + let denom1 = String::from("untrn"); + let denom2 = String::from("ibc/astro"); + let denom3 = String::from("usdt"); + + let router = Rc::new(RefCell::new(mock_app( + &owner, + Some(vec![ + Coin { + denom: denom1.clone(), + amount: Uint128::new(100_000_000_000u128), }, - &[], + Coin { + denom: denom2.clone(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: denom3.clone(), + amount: Uint128::new(100_000_000_000u128), + }, + ]), + ))); + + let factory = MockFactoryBuilder::new(&router).instantiate(); + let shared_multisig = + MockSharedMultisigBuilder::new(&router).instantiate(&factory.address, None, None); + + // Sends tokens to the multisig + shared_multisig + .send_tokens( + &owner, + Some(vec![ + Coin { + denom: denom1.clone(), + amount: Uint128::new(200_000_000u128), + }, + Coin { + denom: denom2.clone(), + amount: Uint128::new(200_000_000u128), + }, + Coin { + denom: denom3.clone(), + amount: Uint128::new(300_000_000u128), + }, + ]), + None, ) .unwrap(); - // Try to execute with only 1 vote - router - .execute_contract( - recipient.clone(), - shared_addr.clone(), - &ExecuteMsg::Execute { proposal_id: 3 }, - &[], - ) + // Check the recipient's balance utrn + let res = shared_multisig + .query_native_balance(Some(recipient.as_str()), denom1.as_str()) .unwrap(); + assert_eq!(res.amount, Uint128::zero()); + assert_eq!(res.denom, denom1.clone()); // Check the recipient's balance - let res = router - .wrap() - .query_balance(recipient.to_string(), "utrn") + let res = shared_multisig + .query_native_balance(Some(recipient.as_str()), denom2.as_str()) .unwrap(); - assert_eq!(res.amount, Uint128::new(100_000_000)); - assert_eq!(res.denom, "utrn"); + assert_eq!(res.amount, Uint128::zero()); + assert_eq!(res.denom, denom2.clone()); - // Check the holder's balance - let res = router - .wrap() - .query_balance(shared_addr.to_string(), "utrn") + // Check the recipient's balance + let res = shared_multisig + .query_native_balance(Some(recipient.as_str()), denom3.as_str()) .unwrap(); - assert_eq!(res.amount, Uint128::new(100_000_000)); - assert_eq!(res.denom, "utrn"); + assert_eq!(res.amount, Uint128::zero()); + assert_eq!(res.denom, denom3); - // try to propose from DAO - router - .execute_contract(dao.clone(), shared_addr.clone(), &propose_msg, &[]) + // Check the holder's balance + let res = shared_multisig + .query_native_balance(None, denom1.as_str()) .unwrap(); + assert_eq!(res.amount, Uint128::new(200_000_000)); + assert_eq!(res.denom, denom1); - router.update_block(|b| b.height += 100); - - // Try to close expired proposal - router - .execute_contract( - recipient.clone(), - shared_addr.clone(), - &ExecuteMsg::Close { proposal_id: 4 }, - &[], - ) + // Check the holder's balance + let res = shared_multisig + .query_native_balance(None, denom2.as_str()) .unwrap(); -} - -#[test] -fn query_proposal() { - let owner = Addr::unchecked("owner"); - let dao = Addr::unchecked("dao"); - let manager = Addr::unchecked("manager"); - - let mut app = mock_app(&owner, None); - let shared_addr = - shared_multisig_instance(&mut app, owner, "dao".to_string(), "manager".to_string()); + assert_eq!(res.amount, Uint128::new(200_000_000)); + assert_eq!(res.denom, denom2); - let err = app - .wrap() - .query_wasm_smart::(&shared_addr, &QueryMsg::Proposal { proposal_id: 0 }) - .unwrap_err(); - assert_eq!( - StdError::generic_err("Querier contract error: cw3::proposal::Proposal not found"), - err - ); + // Check the holder's balance + let res = shared_multisig + .query_native_balance(None, denom3.as_str()) + .unwrap(); + assert_eq!(res.amount, Uint128::new(300_000_000)); + assert_eq!(res.denom, denom3); + + // try to transfer when rage quit is not started yet + let err = shared_multisig + .transfer( + &manager2, + Asset { + info: AssetInfo::NativeToken { + denom: denom1.to_string(), + }, + amount: Uint128::new(100_000_000), + }, + Some(recipient.to_string()), + ) + .unwrap_err(); + assert_eq!( + "Operation is unavailable. Rage quit is not started", + err.root_cause().to_string() + ); - let propose_msg = ExecuteMsg::Propose { - title: "Empty proposal".to_string(), - description: "Empty proposal".to_string(), - msgs: vec![], - latest: None, - }; + // try to transfer when rage quit is not started yet + let err = shared_multisig + .transfer( + &manager2, + Asset { + info: AssetInfo::NativeToken { + denom: denom2.to_string(), + }, + amount: Uint128::new(100_000_000), + }, + Some(recipient.to_string()), + ) + .unwrap_err(); + assert_eq!( + "Operation is unavailable. Rage quit is not started", + err.root_cause().to_string() + ); - // try to propose from DAO - app.execute_contract(dao.clone(), shared_addr.clone(), &propose_msg, &[]) + // try to transfer denom3 when rage quit is not started yet + shared_multisig + .transfer( + &manager2, + Asset { + info: AssetInfo::NativeToken { + denom: denom3.to_string(), + }, + amount: Uint128::new(100_000_000), + }, + Some(recipient.to_string()), + ) .unwrap(); - let res: ProposalResponse = app - .wrap() - .query_wasm_smart(&shared_addr, &QueryMsg::Proposal { proposal_id: 1 }) - .unwrap(); + // try to update config from manager1 + shared_multisig.start_rage_quit(&manager2).unwrap(); + // try to transfer denom1 from manager2 + let err = shared_multisig + .transfer( + &manager2, + Asset { + info: AssetInfo::NativeToken { + denom: denom1.to_string(), + }, + amount: Uint128::new(100_000_000), + }, + Some(recipient.to_string()), + ) + .unwrap_err(); assert_eq!( - res, - ProposalResponse { - id: 1, - title: "Empty proposal".to_string(), - description: "Empty proposal".to_string(), - msgs: vec![], - status: Status::Open, - expires: Expiration::AtHeight(app.block_info().height + 3), - threshold: ThresholdResponse::AbsoluteCount { - weight: 2, - total_weight: 2 - }, - proposer: dao.clone(), - deposit: None - } + "Unauthorized: manager2 cannot transfer untrn", + err.root_cause().to_string() ); - let propose_msg = ExecuteMsg::Propose { - title: "The second empty proposal".to_string(), - description: "The second empty proposal".to_string(), - msgs: vec![], - latest: None, - }; + // try to transfer denom1 from manager1 + shared_multisig + .transfer( + &manager1, + Asset { + info: AssetInfo::NativeToken { + denom: denom1.to_string(), + }, + amount: Uint128::new(100_000_000), + }, + Some(recipient.to_string()), + ) + .unwrap(); + + // try to transfer denom2 from manager1 + let err = shared_multisig + .transfer( + &manager1, + Asset { + info: AssetInfo::NativeToken { + denom: denom2.to_string(), + }, + amount: Uint128::new(100_000_000), + }, + Some(recipient.to_string()), + ) + .unwrap_err(); + assert_eq!( + "Unauthorized: manager1 cannot transfer ibc/astro", + err.root_cause().to_string() + ); - // try to propose from DAO - app.execute_contract(manager.clone(), shared_addr.clone(), &propose_msg, &[]) + // try to transfer denom2 from manager2 + shared_multisig + .transfer( + &manager2, + Asset { + info: AssetInfo::NativeToken { + denom: denom2.to_string(), + }, + amount: Uint128::new(100_000_000), + }, + Some(recipient.to_string()), + ) .unwrap(); - let res: ProposalListResponse = app - .wrap() - .query_wasm_smart( - &shared_addr, - &QueryMsg::ListProposals { - start_after: None, - limit: None, + // try to transfer usdt from manager1 + shared_multisig + .transfer( + &manager1, + Asset { + info: AssetInfo::NativeToken { + denom: denom3.to_string(), + }, + amount: Uint128::new(100_000_000), }, + Some(recipient.to_string()), ) .unwrap(); + // try to transfer usdt from manager2 + let err = shared_multisig + .transfer( + &manager2, + Asset { + info: AssetInfo::NativeToken { + denom: denom3.to_string(), + }, + amount: Uint128::new(100_000_000), + }, + Some(recipient.to_string()), + ) + .unwrap_err(); assert_eq!( - res.proposals, - vec![ - ProposalResponse { - id: 1, - title: "Empty proposal".to_string(), - description: "Empty proposal".to_string(), - msgs: vec![], - status: Status::Open, - expires: Expiration::AtHeight(app.block_info().height + 3), - threshold: ThresholdResponse::AbsoluteCount { - weight: 2, - total_weight: 2 + err.root_cause().to_string(), + "Insufficient balance for: manager2. Available balance: 50000000" + ); + + // try to transfer usdt from manager2 + let err = shared_multisig + .transfer( + &manager1, + Asset { + info: AssetInfo::NativeToken { + denom: denom3.to_string(), + }, + amount: Uint128::new(100_000_000), + }, + Some(recipient.to_string()), + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Insufficient balance for: manager1. Available balance: 50000000" + ); + + // try to transfer usdt from manager2 + shared_multisig + .transfer( + &manager2, + Asset { + info: AssetInfo::NativeToken { + denom: denom3.to_string(), }, - proposer: dao.clone(), - deposit: None - }, - ProposalResponse { - id: 2, - title: "The second empty proposal".to_string(), - description: "The second empty proposal".to_string(), - msgs: vec![], - status: Status::Open, - expires: Expiration::AtHeight(app.block_info().height + 3), - threshold: ThresholdResponse::AbsoluteCount { - weight: 2, - total_weight: 2 + amount: Uint128::new(50_000_000), + }, + Some(recipient.to_string()), + ) + .unwrap(); + + // try to transfer usdt from manager2 + let err = shared_multisig + .transfer( + &manager2, + Asset { + info: AssetInfo::NativeToken { + denom: denom3.to_string(), }, - proposer: manager.clone(), - deposit: None - } - ] + amount: Uint128::new(50_000_000), + }, + Some(recipient.to_string()), + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Insufficient balance for: manager2. Available balance: 0" ); - let res: ProposalListResponse = app - .wrap() - .query_wasm_smart( - &shared_addr, - &QueryMsg::ReverseProposals { - start_before: None, - limit: None, + shared_multisig + .transfer( + &manager1, + Asset { + info: AssetInfo::NativeToken { + denom: denom3.to_string(), + }, + amount: Uint128::new(50_000_000), }, + Some(recipient.to_string()), ) .unwrap(); + // Check the recipient's balance denom1 + let res = shared_multisig + .query_native_balance(Some(recipient.as_str()), &denom1) + .unwrap(); + assert_eq!(res.amount, Uint128::new(100_000_000)); + assert_eq!(res.denom, denom1); + + // Check the recipient's balance denom2 + let res = shared_multisig + .query_native_balance(Some(recipient.as_str()), &denom2) + .unwrap(); + assert_eq!(res.amount, Uint128::new(100_000_000)); + assert_eq!(res.denom, denom2); + + // Check the recipient's balance denom3 + let res = shared_multisig + .query_native_balance(Some(recipient.as_str()), &denom3) + .unwrap(); + assert_eq!(res.amount, Uint128::new(300_000_000)); + assert_eq!(res.denom, denom3); + + // Check the holder's balance + let res = shared_multisig.query_native_balance(None, &denom1).unwrap(); + assert_eq!(res.amount, Uint128::new(100_000_000)); + assert_eq!(res.denom, denom1); + + // Check the holder's balance + let res = shared_multisig.query_native_balance(None, &denom2).unwrap(); + assert_eq!(res.amount, Uint128::new(100_000_000)); + assert_eq!(res.denom, denom2); + + // Check the holder's balance + let res = shared_multisig.query_native_balance(None, &denom3).unwrap(); + assert_eq!(res.amount, Uint128::zero()); + assert_eq!(res.denom, denom3); +} + +#[test] +fn test_target_pool() { + let manager1 = Addr::unchecked(MANAGER1); + let manager2 = Addr::unchecked(MANAGER2); + let owner = Addr::unchecked(OWNER); + let denom1 = String::from("untrn"); + let denom2 = String::from("ibc/astro"); + let denom3 = String::from("usdt"); + + let router = Rc::new(RefCell::new(mock_app( + &owner, + Some(vec![ + Coin { + denom: denom1.clone(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: denom2.clone(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: denom3, + amount: Uint128::new(100_000_000_000u128), + }, + ]), + ))); + + let factory = MockFactoryBuilder::new(&router).instantiate(); + let coin_registry = factory.coin_registry(); + coin_registry.add(vec![(denom1.to_owned(), 6), (denom2.to_owned(), 6)]); + + let pcl = factory.instantiate_concentrated_pair( + &[ + AssetInfo::NativeToken { + denom: denom1.clone(), + }, + AssetInfo::NativeToken { + denom: denom2.clone(), + }, + ], + None, + ); + + let pcl_pair_info = pcl.pair_info(); assert_eq!( - res.proposals, + pcl_pair_info.asset_infos, vec![ - ProposalResponse { - id: 2, - title: "The second empty proposal".to_string(), - description: "The second empty proposal".to_string(), - msgs: vec![], - status: Status::Open, - expires: Expiration::AtHeight(app.block_info().height + 3), - threshold: ThresholdResponse::AbsoluteCount { - weight: 2, - total_weight: 2 - }, - proposer: manager, - deposit: None - }, - ProposalResponse { - id: 1, - title: "Empty proposal".to_string(), - description: "Empty proposal".to_string(), - msgs: vec![], - status: Status::Open, - expires: Expiration::AtHeight(app.block_info().height + 3), - threshold: ThresholdResponse::AbsoluteCount { - weight: 2, - total_weight: 2 - }, - proposer: dao, - deposit: None - } + AssetInfo::NativeToken { + denom: denom1.clone(), + }, + AssetInfo::NativeToken { + denom: denom2.clone(), + }, ] ); -} -#[test] -fn query_list_votes() { - let owner = Addr::unchecked("owner"); - let dao = Addr::unchecked("dao"); - let manager = Addr::unchecked("manager"); - - let mut app = mock_app(&owner, None); - let shared_addr = - shared_multisig_instance(&mut app, owner, "dao".to_string(), "manager".to_string()); - - let propose_msg = ExecuteMsg::Propose { - title: "Empty proposal".to_string(), - description: "Empty proposal".to_string(), - msgs: vec![], - latest: None, - }; + let shared_multisig = + MockSharedMultisigBuilder::new(&router).instantiate(&factory.address, None, None); + + // Sends tokens to the multisig + shared_multisig.send_tokens(&owner, None, None).unwrap(); - // try to propose from DAO - app.execute_contract(dao.clone(), shared_addr.clone(), &propose_msg, &[]) + // try to provide from manager1 + let err = shared_multisig + .provide(&manager1, PoolType::Target, None, None, None, None) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Target pool is not set"); + + // Direct set up target pool without proposal + shared_multisig + .setup_pools( + &shared_multisig.address, + Some(pcl.address.to_string()), + None, + ) .unwrap(); - // DAO vote - app.wrap() - .query_wasm_smart::( - &shared_addr, - &QueryMsg::Vote { - proposal_id: 1, - voter: dao.to_string(), + let config = shared_multisig.query_config().unwrap(); + assert_eq!(config.target_pool, Some(pcl.address)); + + // try to provide from manager1 + shared_multisig + .provide(&manager1, PoolType::Target, None, None, None, None) + .unwrap(); + + // try to withdraw from target + let err = shared_multisig.withdraw(&manager1, None, None).unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Migration pool is not set"); + + // Check the holder's balance for denom1 + let denom1_before = shared_multisig.query_native_balance(None, &denom1).unwrap(); + assert_eq!(denom1_before.amount, Uint128::new(800_000_000)); + assert_eq!(denom1_before.denom, denom1.clone()); + + // Check the holder's balance for denom2 + let denom1_before = shared_multisig.query_native_balance(None, &denom2).unwrap(); + assert_eq!(denom1_before.amount, Uint128::new(800_000_000)); + assert_eq!(denom1_before.denom, denom2.clone()); + + // Check the holder's LP balance + let res = shared_multisig + .query_cw20_balance(&pcl_pair_info.liquidity_token, None) + .unwrap(); + assert_eq!(res.balance, Uint128::new(99_999_000)); + + // deregister the target pool + factory + .deregister_pair(&[ + AssetInfo::NativeToken { + denom: denom1.clone(), }, - ) + AssetInfo::NativeToken { + denom: denom2.clone(), + }, + ]) .unwrap(); - let res: VoteResponse = app - .wrap() - .query_wasm_smart( - &shared_addr, - &QueryMsg::Vote { - proposal_id: 1, - voter: "dao".to_string(), + // create the migration pool + let pcl_2 = factory.instantiate_concentrated_pair( + &[ + AssetInfo::NativeToken { + denom: denom1.clone(), + }, + AssetInfo::NativeToken { + denom: denom2.clone(), }, + ], + None, + ); + + // Direct set up migration pool without proposal + shared_multisig + .setup_pools( + &shared_multisig.address, + None, + Some(pcl_2.address.to_string()), ) .unwrap(); + // try to provide from manager1 + let err = shared_multisig + .provide(&manager1, PoolType::Target, None, None, None, None) + .unwrap_err(); assert_eq!( - res.vote, - Some(VoteInfo { - proposal_id: 1, - voter: "dao".to_string(), - vote: Vote::Yes, - weight: 1 - }) + err.root_cause().to_string(), + "Migration pool is already set" ); - let res: VoteListResponse = app - .wrap() - .query_wasm_smart(&shared_addr, &QueryMsg::ListVotes { proposal_id: 1 }) + // try to withdraw from target pool + shared_multisig.withdraw(&manager2, None, None).unwrap(); + + // Check the holder's balance for denom1 + let denom1_before = shared_multisig.query_native_balance(None, &denom1).unwrap(); + assert_eq!(denom1_before.amount, Uint128::new(899_998_999)); + assert_eq!(denom1_before.denom, denom1.clone()); + + // Check the holder's balance for denom2 + let denom1_before = shared_multisig.query_native_balance(None, &denom2).unwrap(); + assert_eq!(denom1_before.amount, Uint128::new(899_998_999)); + assert_eq!(denom1_before.denom, denom2.clone()); + + // Check the holder's LP balance + let res = shared_multisig + .query_cw20_balance(&pcl_pair_info.liquidity_token, None) .unwrap(); + assert_eq!(res.balance, Uint128::zero()); + + // try to update config from manager1 + shared_multisig.start_rage_quit(&manager2).unwrap(); + + // check if rage quit started + let res = shared_multisig.query_config().unwrap(); + assert_eq!(res.rage_quit_started, true); + + // check if rage quit cannot be set back to false + let err = shared_multisig.start_rage_quit(&manager2).unwrap_err(); assert_eq!( - res, - VoteListResponse { - votes: vec![VoteInfo { - proposal_id: 1, - voter: "dao".to_string(), - vote: Vote::Yes, - weight: 1 - }] - } + err.root_cause().to_string(), + "Operation is unavailable. Rage quit has already started" ); - // DAO vote - app.wrap() - .query_wasm_smart::( - &shared_addr, - &QueryMsg::Vote { - proposal_id: 1, - voter: manager.to_string(), + // try to provide after rage quit started + let err = shared_multisig + .provide(&manager2, PoolType::Target, None, None, None, None) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Operation is unavailable. Rage quit has already started" + ); +} + +#[test] +fn test_provide_withdraw_pcl() { + let astroport = astroport_address(); + let manager1 = Addr::unchecked(MANAGER1); + let manager2 = Addr::unchecked(MANAGER2); + let recipient = Addr::unchecked("recipient"); + + let denom1 = String::from("untrn"); + let denom2 = String::from("ibc/astro"); + let denom3 = String::from("usdt"); + + let router = Rc::new(RefCell::new(mock_app( + &astroport, + Some(vec![ + Coin { + denom: denom1.clone(), + amount: Uint128::new(100_000_000_000u128), }, + Coin { + denom: denom2.clone(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: denom3, + amount: Uint128::new(100_000_000_000u128), + }, + ]), + ))); + + let factory = MockFactoryBuilder::new(&router).instantiate(); + let coin_registry = factory.coin_registry(); + coin_registry.add(vec![(denom1.to_owned(), 6), (denom2.to_owned(), 6)]); + + let pcl = factory.instantiate_concentrated_pair( + &[ + AssetInfo::NativeToken { + denom: denom1.clone(), + }, + AssetInfo::NativeToken { + denom: denom2.clone(), + }, + ], + None, + ); + + let pcl_pair_info = pcl.pair_info(); + let shared_multisig = + MockSharedMultisigBuilder::new(&router).instantiate(&factory.address, None, None); + + // Direct set up target pool without proposal + shared_multisig + .setup_pools( + &shared_multisig.address, + Some(pcl.address.to_string()), + None, + ) + .unwrap(); + + let config = shared_multisig.query_config().unwrap(); + assert_eq!(config.target_pool, Some(pcl.address)); + + // try to provide from recipient + let err = shared_multisig + .provide(&recipient, PoolType::Target, None, None, None, None) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Unauthorized"); + + // try to provide without funds on multisig from manager1 + let err = shared_multisig + .provide(&manager1, PoolType::Target, None, None, None, None) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Asset balance mismatch between the argument and the \ + Multisig balance. Available Multisig balance for untrn: 0" + ); + + // Sends tokens to the multisig + shared_multisig.send_tokens(&astroport, None, None).unwrap(); + + // Check the holder's balance for denom1 + let res = shared_multisig.query_native_balance(None, &denom1).unwrap(); + assert_eq!(res.amount, Uint128::new(900_000_000)); + assert_eq!(res.denom, denom1.clone()); + + // Check the holder's balance for denom2 + let res = shared_multisig.query_native_balance(None, &denom2).unwrap(); + assert_eq!(res.amount, Uint128::new(900_000_000)); + assert_eq!(res.denom, denom2.clone()); + + // try to provide from manager1 + shared_multisig + .provide(&manager1, PoolType::Target, None, None, None, None) + .unwrap(); + + // send tokens to the recipient + shared_multisig + .send_tokens(&astroport, None, Some(recipient.clone())) + .unwrap(); + + // try to swap tokens + for _ in 0..10 { + shared_multisig + .swap( + &recipient, + &pcl_pair_info.contract_addr, + &denom1, + 10_000_000, + None, + None, + Some(Decimal::from_ratio(5u128, 10u128)), + None, + ) + .unwrap(); + + router.borrow_mut().update_block(|b| { + b.height += 1200; + b.time = b.time.plus_seconds(3600); + }); + + shared_multisig + .swap( + &recipient, + &pcl_pair_info.contract_addr, + &denom2, + 15_000_000, + None, + None, + Some(Decimal::from_ratio(5u128, 10u128)), + None, + ) + .unwrap(); + + router.borrow_mut().update_block(|b| { + b.height += 100; + }); + + // try to provide from manager2 + shared_multisig + .provide( + &manager2, + PoolType::Target, + Some(vec![ + Asset { + info: AssetInfo::NativeToken { + denom: denom1.clone(), + }, + amount: Uint128::new(10_000_000), + }, + Asset { + info: AssetInfo::NativeToken { + denom: denom2.clone(), + }, + amount: Uint128::new(10_000_000), + }, + ]), + Some(Decimal::from_ratio(5u128, 10u128)), + None, + None, + ) + .unwrap(); + } + + router.borrow_mut().update_block(|b| { + b.time = b.time.plus_seconds(86400 * 7); + }); + + // Check the holder's balance for denom1 + let res = shared_multisig.query_native_balance(None, &denom1).unwrap(); + assert_eq!(res.amount, Uint128::new(700_000_000)); + assert_eq!(res.denom, denom1.clone()); + + // Check the holder's balance for denom2 + let res = shared_multisig.query_native_balance(None, &denom2).unwrap(); + assert_eq!(res.amount, Uint128::new(700_000_000)); + assert_eq!(res.denom, denom2.clone()); + + // try to provide from manager2 + shared_multisig + .provide( + &manager2, + PoolType::Target, + None, + Some(Decimal::from_ratio(5u128, 10u128)), + None, + None, ) .unwrap(); - let res: VoteListResponse = app - .wrap() - .query_wasm_smart(&shared_addr, &QueryMsg::ListVotes { proposal_id: 1 }) + // Check the holder's balance for denom1 + let res = shared_multisig.query_native_balance(None, &denom1).unwrap(); + assert_eq!(res.amount, Uint128::new(600_000_000)); + assert_eq!(res.denom, denom1.clone()); + + // Check the holder's balance for denom2 + let res = shared_multisig.query_native_balance(None, &denom2).unwrap(); + assert_eq!(res.amount, Uint128::new(600_000_000)); + assert_eq!(res.denom, denom2.clone()); + + // Check the holder's LP balance + let res = shared_multisig + .query_cw20_balance(&pcl_pair_info.liquidity_token, None) + .unwrap(); + assert_eq!(res.balance, Uint128::new(301_118_256)); +} + +#[test] +fn test_provide_withdraw_xyk() { + let astroport = astroport_address(); + let manager1 = Addr::unchecked(MANAGER1); + let manager2 = Addr::unchecked(MANAGER2); + let recipient = Addr::unchecked("recipient"); + + let denom1 = String::from("untrn"); + let denom2 = String::from("ibc/astro"); + let denom3 = String::from("usdt"); + + let router = Rc::new(RefCell::new(mock_app( + &astroport, + Some(vec![ + Coin { + denom: denom1.clone(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: denom2.clone(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: denom3, + amount: Uint128::new(100_000_000_000u128), + }, + ]), + ))); + + let factory = MockFactoryBuilder::new(&router).instantiate(); + let coin_registry = factory.coin_registry(); + coin_registry.add(vec![(denom1.to_owned(), 6), (denom2.to_owned(), 6)]); + + let xyk = factory.instantiate_xyk_pair(&[ + AssetInfo::NativeToken { + denom: denom1.clone(), + }, + AssetInfo::NativeToken { + denom: denom2.clone(), + }, + ]); + + let xyk_pair_info = xyk.pair_info().unwrap(); + let shared_multisig = + MockSharedMultisigBuilder::new(&router).instantiate(&factory.address, None, None); + + // Direct set up target pool without proposal + shared_multisig + .setup_pools( + &shared_multisig.address, + Some(xyk.address.to_string()), + None, + ) .unwrap(); + + let config = shared_multisig.query_config().unwrap(); + assert_eq!(config.target_pool, Some(xyk.address)); + + // try to provide from recipient + let err = shared_multisig + .provide(&recipient, PoolType::Target, None, None, None, None) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Unauthorized"); + + // try to provide without funds on multisig from manager1 + let err = shared_multisig + .provide(&manager1, PoolType::Target, None, None, None, None) + .unwrap_err(); assert_eq!( - res, - VoteListResponse { - votes: vec![VoteInfo { - proposal_id: 1, - voter: "dao".to_string(), - vote: Vote::Yes, - weight: 1 - }] - } + err.root_cause().to_string(), + "Asset balance mismatch between the argument and the \ + Multisig balance. Available Multisig balance for untrn: 0" + ); + + // Sends tokens to the multisig + shared_multisig.send_tokens(&astroport, None, None).unwrap(); + + // Check the holder's balance for denom1 + let res = shared_multisig.query_native_balance(None, &denom1).unwrap(); + assert_eq!(res.amount, Uint128::new(900_000_000)); + assert_eq!(res.denom, denom1.clone()); + + // Check the holder's balance for denom2 + let res = shared_multisig.query_native_balance(None, &denom2).unwrap(); + assert_eq!(res.amount, Uint128::new(900_000_000)); + assert_eq!(res.denom, denom2.clone()); + + // try to provide from manager1 + shared_multisig + .provide(&manager1, PoolType::Target, None, None, None, None) + .unwrap(); + + // send tokens to the recipient + shared_multisig + .send_tokens(&astroport, None, Some(recipient.clone())) + .unwrap(); + + // try to swap tokens + for _ in 0..10 { + shared_multisig + .swap( + &recipient, + &xyk_pair_info.contract_addr, + &denom1, + 10_000_000, + None, + None, + Some(Decimal::from_ratio(5u128, 10u128)), + None, + ) + .unwrap(); + + router.borrow_mut().update_block(|b| { + b.height += 1400; + }); + + shared_multisig + .swap( + &recipient, + &xyk_pair_info.contract_addr, + &denom2, + 15_000_000, + None, + None, + Some(Decimal::from_ratio(5u128, 10u128)), + None, + ) + .unwrap(); + + router.borrow_mut().update_block(|b| { + b.height += 100; + }); + + // try to provide from manager2 + shared_multisig + .provide( + &manager2, + PoolType::Target, + Some(vec![ + Asset { + info: AssetInfo::NativeToken { + denom: denom1.clone(), + }, + amount: Uint128::new(10_000_000), + }, + Asset { + info: AssetInfo::NativeToken { + denom: denom2.clone(), + }, + amount: Uint128::new(10_000_000), + }, + ]), + Some(Decimal::from_ratio(5u128, 10u128)), + None, + None, + ) + .unwrap(); + } + + router.borrow_mut().update_block(|b| { + b.height += 500; + b.time = b.time.plus_seconds(86400); + }); + + // Check the holder's balance for denom1 + let res = shared_multisig.query_native_balance(None, &denom1).unwrap(); + assert_eq!(res.amount, Uint128::new(700_000_000)); + assert_eq!(res.denom, denom1.clone()); + + // Check the holder's balance for denom2 + let res = shared_multisig.query_native_balance(None, &denom2).unwrap(); + assert_eq!(res.amount, Uint128::new(700_000_000)); + assert_eq!(res.denom, denom2.clone()); + + // try to provide from manager2 + shared_multisig + .provide( + &manager2, + PoolType::Target, + None, + Some(Decimal::from_ratio(5u128, 10u128)), + None, + None, + ) + .unwrap(); + + // Check the holder's balance for denom1 + let res = shared_multisig.query_native_balance(None, &denom1).unwrap(); + assert_eq!(res.amount, Uint128::new(600_000_000)); + assert_eq!(res.denom, denom1.clone()); + + // Check the holder's balance for denom2 + let res = shared_multisig.query_native_balance(None, &denom2).unwrap(); + assert_eq!(res.amount, Uint128::new(600_000_000)); + assert_eq!(res.denom, denom2.clone()); + + // Check the holder's LP balance + let res = shared_multisig + .query_cw20_balance(&xyk_pair_info.liquidity_token, None) + .unwrap(); + assert_eq!(res.balance, Uint128::new(263_078_132)); +} + +#[test] +fn test_provide_to_both_pools() { + let manager1 = Addr::unchecked(MANAGER1); + let manager2 = Addr::unchecked(MANAGER2); + let owner = Addr::unchecked(OWNER); + let denom1 = String::from("untrn"); + let denom2 = String::from("ibc/astro"); + let denom3 = String::from("usdt"); + + let router = Rc::new(RefCell::new(mock_app( + &owner, + Some(vec![ + Coin { + denom: denom1.clone(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: denom2.clone(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: denom3.clone(), + amount: Uint128::new(100_000_000_000u128), + }, + ]), + ))); + + let generator = MockGeneratorBuilder::new(&router).instantiate(); + let factory = generator.factory(); + let coin_registry = factory.coin_registry(); + coin_registry.add(vec![ + (denom1.to_owned(), 6), + (denom2.to_owned(), 6), + (denom3.to_owned(), 6), + ]); + + let pcl_target = factory.instantiate_concentrated_pair( + &[ + AssetInfo::NativeToken { + denom: denom1.clone(), + }, + AssetInfo::NativeToken { + denom: denom2.clone(), + }, + ], + None, + ); + + let shared_multisig = MockSharedMultisigBuilder::new(&router).instantiate( + &factory.address, + Some(generator.address), + None, + ); + + // Sends tokens to the multisig + shared_multisig.send_tokens(&owner, None, None).unwrap(); + + // try to provide from manager1 + let err = shared_multisig + .provide(&manager1, PoolType::Target, None, None, None, None) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Target pool is not set"); + + // try to provide from manager1 + let err = shared_multisig + .provide(&manager1, PoolType::Migration, None, None, None, None) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Migration pool is not set"); + + // Direct set up target pool without proposal + shared_multisig + .setup_pools( + &shared_multisig.address, + Some(pcl_target.address.to_string()), + None, + ) + .unwrap(); + + let config = shared_multisig.query_config().unwrap(); + assert_eq!(config.target_pool, Some(pcl_target.address.clone())); + assert_eq!(config.migration_pool, None); + + // try to provide from manager1 + shared_multisig + .provide(&manager1, PoolType::Target, None, None, None, None) + .unwrap(); + + // try to withdraw from target + let err = shared_multisig.withdraw(&manager1, None, None).unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Migration pool is not set"); + + // try to withdraw from migration + let err = shared_multisig.withdraw(&manager2, None, None).unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Migration pool is not set"); + + // try to update config from manager1 + let err = shared_multisig + .complete_target_pool_migration(&manager2) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Migration pool is not set"); + + // try to update config from manager1 + shared_multisig.start_rage_quit(&manager2).unwrap(); + + // check if rage quit started + let res = shared_multisig.query_config().unwrap(); + assert_eq!(res.rage_quit_started, true); + + // check if rage quit cannot be set back to false + let err = shared_multisig.start_rage_quit(&manager2).unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Operation is unavailable. Rage quit has already started" + ); + + // try to provide after rage quit started in target pool + let err = shared_multisig + .provide(&manager2, PoolType::Target, None, None, None, None) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Operation is unavailable. Rage quit has already started" + ); +} + +#[test] +fn test_transfer_lp_tokens() { + let astroport = astroport_address(); + let manager1 = Addr::unchecked(MANAGER1); + let manager2 = Addr::unchecked(MANAGER2); + let cheater = Addr::unchecked(CHEATER); + let recipient = Addr::unchecked("recipient"); + + let denom1 = String::from("untrn"); + let denom2 = String::from("ibc/astro"); + let denom3 = String::from("usdt"); + + let router = Rc::new(RefCell::new(mock_app( + &astroport, + Some(vec![ + Coin { + denom: denom1.clone(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: denom2.clone(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: denom3, + amount: Uint128::new(100_000_000_000u128), + }, + ]), + ))); + + let factory = MockFactoryBuilder::new(&router).instantiate(); + let coin_registry = factory.coin_registry(); + coin_registry.add(vec![(denom1.to_owned(), 6), (denom2.to_owned(), 6)]); + + let pcl = factory.instantiate_concentrated_pair( + &[ + AssetInfo::NativeToken { + denom: denom1.clone(), + }, + AssetInfo::NativeToken { + denom: denom2.clone(), + }, + ], + None, + ); + + let pcl_pair_info = pcl.pair_info(); + let shared_multisig = + MockSharedMultisigBuilder::new(&router).instantiate(&factory.address, None, None); + + // Sends tokens to the multisig + shared_multisig.send_tokens(&astroport, None, None).unwrap(); + + // Direct set up target pool without proposal + shared_multisig + .setup_pools( + &shared_multisig.address, + Some(pcl.address.to_string()), + None, + ) + .unwrap(); + + // try to provide from recipient + let err = shared_multisig + .provide(&recipient, PoolType::Target, None, None, None, None) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Unauthorized"); + + // try to provide from manager1 + shared_multisig + .provide(&manager1, PoolType::Target, None, None, None, None) + .unwrap(); + + // Check the holder's LP balance + let res = shared_multisig + .query_cw20_balance(&pcl_pair_info.liquidity_token, None) + .unwrap(); + assert_eq!(res.balance, Uint128::new(99_999_000)); + + // Check the recipient's LP balance + let res = shared_multisig + .query_cw20_balance(&pcl_pair_info.liquidity_token, Some(recipient.clone())) + .unwrap(); + assert_eq!(res.balance, Uint128::zero()); + + // try to transfer LP tokens through transfer endpoint + let err = shared_multisig + .transfer( + &manager2, + Asset { + info: AssetInfo::Token { + contract_addr: pcl_pair_info.liquidity_token.clone(), + }, + amount: Uint128::new(100_000_000), + }, + Some(recipient.to_string()), + ) + .unwrap_err(); + assert_eq!( + "Unauthorized: manager2 cannot transfer contract3", + err.root_cause().to_string() + ); + + // create proposal message for transfer LP tokens to the recipient + let lp_transfer_amount = Uint128::new(10_000_000); + let transfer_lp_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: pcl_pair_info.liquidity_token.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: recipient.to_string(), + amount: lp_transfer_amount, + }) + .unwrap(), + funds: vec![], + }); + + // try to propose from cheater + let err = shared_multisig + .propose(&cheater, vec![transfer_lp_msg.clone()]) + .unwrap_err(); + assert_eq!("Unauthorized", err.root_cause().to_string()); + + // try to propose from manager1 + shared_multisig + .propose(&manager1, vec![transfer_lp_msg.clone()]) + .unwrap(); + + // Try to vote from manager2 + shared_multisig.vote(&manager2, 1, Vote::Yes).unwrap(); + + // Try to execute the third proposal + shared_multisig.execute(&manager2, 1).unwrap(); + + // Check the holder's LP balance + let res = shared_multisig + .query_cw20_balance(&pcl_pair_info.liquidity_token, None) + .unwrap(); + assert_eq!(res.balance, Uint128::new(89_999_000)); + + // Check the recipient's LP balance + let res = shared_multisig + .query_cw20_balance(&pcl_pair_info.liquidity_token, Some(recipient)) + .unwrap(); + assert_eq!(res.balance, Uint128::new(10_000_000)); +} + +#[test] +fn test_end_migrate_from_target_to_migration_pool() { + let manager1 = Addr::unchecked(MANAGER1); + let manager2 = Addr::unchecked(MANAGER2); + let owner = Addr::unchecked(OWNER); + let denom1 = String::from("untrn"); + let denom2 = String::from("ibc/astro"); + let denom3 = String::from("usdt"); + + let router = Rc::new(RefCell::new(mock_app( + &owner, + Some(vec![ + Coin { + denom: denom1.clone(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: denom2.clone(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: denom3, + amount: Uint128::new(100_000_000_000u128), + }, + ]), + ))); + + let generator = MockGeneratorBuilder::new(&router).instantiate(); + let factory = generator.factory(); + let coin_registry = factory.coin_registry(); + coin_registry.add(vec![(denom1.to_owned(), 6), (denom2.to_owned(), 6)]); + + let xyk_pool = factory.instantiate_xyk_pair(&[ + AssetInfo::NativeToken { + denom: denom1.clone(), + }, + AssetInfo::NativeToken { + denom: denom2.clone(), + }, + ]); + + let xyk_pair_info = xyk_pool.pair_info().unwrap(); + assert_eq!( + xyk_pair_info.asset_infos, + vec![ + AssetInfo::NativeToken { + denom: denom1.clone(), + }, + AssetInfo::NativeToken { + denom: denom2.clone(), + }, + ] + ); + + let shared_multisig = MockSharedMultisigBuilder::new(&router).instantiate( + &factory.address, + Some(generator.address), + None, + ); + + // Sends tokens to the multisig + shared_multisig.send_tokens(&owner, None, None).unwrap(); + + // Direct set up target pool without proposal + shared_multisig + .setup_pools( + &shared_multisig.address, + Some(xyk_pool.address.to_string()), + None, + ) + .unwrap(); + + let config = shared_multisig.query_config().unwrap(); + assert_eq!(config.target_pool, Some(xyk_pool.address.clone())); + + // try to provide from manager1 + shared_multisig + .provide(&manager1, PoolType::Target, None, None, None, None) + .unwrap(); + + // try to withdraw from target + let err = shared_multisig.withdraw(&manager1, None, None).unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Migration pool is not set"); + + // Check the holder's LP balance + let res = shared_multisig + .query_cw20_balance(&xyk_pair_info.liquidity_token, None) + .unwrap(); + assert_eq!(res.balance, Uint128::new(99_999_000)); + + // deregister the target pool + factory + .deregister_pair(&[ + AssetInfo::NativeToken { + denom: denom1.clone(), + }, + AssetInfo::NativeToken { + denom: denom2.clone(), + }, + ]) + .unwrap(); + + // create the migration pool + let pcl_pool = factory.instantiate_concentrated_pair( + &[ + AssetInfo::NativeToken { + denom: denom1.clone(), + }, + AssetInfo::NativeToken { + denom: denom2.clone(), + }, + ], + None, + ); + let pcl_pair_info = pcl_pool.pair_info(); + // Direct set up migration pool without proposal + shared_multisig + .setup_pools( + &shared_multisig.address, + None, + Some(pcl_pool.address.to_string()), + ) + .unwrap(); + + // try to withdraw from target pool and provide to migration pool in the same transaction + shared_multisig + .withdraw( + &manager2, + None, + Some(ProvideParams { + slippage_tolerance: None, + auto_stake: None, + }), + ) + .unwrap(); + + // Check the holder's balance for denom1 + let denom1_before = shared_multisig.query_native_balance(None, &denom1).unwrap(); + assert_eq!(denom1_before.amount, Uint128::new(800_000_000)); + assert_eq!(denom1_before.denom, denom1.clone()); + + // Check the holder's balance for denom2 + let denom1_before = shared_multisig.query_native_balance(None, &denom2).unwrap(); + assert_eq!(denom1_before.amount, Uint128::new(800_000_000)); + assert_eq!(denom1_before.denom, denom2.clone()); + + // Check the holder's LP balance + let res = shared_multisig + .query_cw20_balance(&xyk_pair_info.liquidity_token, None) + .unwrap(); + assert_eq!(res.balance, Uint128::zero()); + + // Check the holder's LP balance + let res = shared_multisig + .query_cw20_balance(&pcl_pair_info.liquidity_token, None) + .unwrap(); + assert_eq!(res.balance, Uint128::new(99_998_000)); + + // try to update config from manager1 + shared_multisig + .complete_target_pool_migration(&manager2) + .unwrap(); + + // check if migration is successful + let res = shared_multisig.query_config().unwrap(); + assert_eq!(res.migration_pool, None); + assert_eq!(res.target_pool, Some(pcl_pool.address)); +} + +#[test] +fn test_withdraw_raqe_quit() { + let manager1 = Addr::unchecked(MANAGER1); + let manager2 = Addr::unchecked(MANAGER2); + let owner = Addr::unchecked(OWNER); + let denom1 = String::from("untrn"); + let denom2 = String::from("ibc/astro"); + let denom3 = String::from("usdt"); + + let router = Rc::new(RefCell::new(mock_app( + &owner, + Some(vec![ + Coin { + denom: denom1.clone(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: denom2.clone(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: denom3, + amount: Uint128::new(100_000_000_000u128), + }, + ]), + ))); + + let factory = MockFactoryBuilder::new(&router).instantiate(); + let coin_registry = factory.coin_registry(); + coin_registry.add(vec![(denom1.to_owned(), 6), (denom2.to_owned(), 6)]); + + let xyk_pool = factory.instantiate_xyk_pair(&[ + AssetInfo::NativeToken { + denom: denom1.clone(), + }, + AssetInfo::NativeToken { + denom: denom2.clone(), + }, + ]); + + let xyk_pair_info = xyk_pool.pair_info().unwrap(); + assert_eq!( + xyk_pair_info.asset_infos, + vec![ + AssetInfo::NativeToken { + denom: denom1.clone(), + }, + AssetInfo::NativeToken { + denom: denom2.clone(), + }, + ] + ); + + let shared_multisig = MockSharedMultisigBuilder::new(&router).instantiate( + &factory.address, + None, + Some(xyk_pool.address.to_string()), + ); + + // Sends tokens to the multisig + shared_multisig.send_tokens(&owner, None, None).unwrap(); + + let config = shared_multisig.query_config().unwrap(); + assert_eq!(config.target_pool, Some(xyk_pool.address.clone())); + + // try to provide from manager1 + shared_multisig + .provide(&manager1, PoolType::Target, None, None, None, None) + .unwrap(); + + // Check the holder's balance for denom1 + let denom1_before = shared_multisig.query_native_balance(None, &denom1).unwrap(); + assert_eq!(denom1_before.amount, Uint128::new(800_000_000)); + assert_eq!(denom1_before.denom, denom1.clone()); + + // Check the holder's balance for denom2 + let denom1_before = shared_multisig.query_native_balance(None, &denom2).unwrap(); + assert_eq!(denom1_before.amount, Uint128::new(800_000_000)); + assert_eq!(denom1_before.denom, denom2.clone()); + + // Check the holder's LP balance + let res = shared_multisig + .query_cw20_balance(&xyk_pair_info.liquidity_token, None) + .unwrap(); + assert_eq!(res.balance, Uint128::new(99999000)); + + // try to update config from manager1 + shared_multisig.start_rage_quit(&manager2).unwrap(); + + // check if rage quit has already started + let res = shared_multisig.query_config().unwrap(); + assert_eq!(res.rage_quit_started, true); + + // try to provide from manager1 + let err = shared_multisig + .provide(&manager1, PoolType::Target, None, None, None, None) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Operation is unavailable. Rage quit has already started" + ); + + // try to update config from manager1 + let err = shared_multisig + .complete_target_pool_migration(&manager2) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Operation is unavailable. Rage quit has already started" + ); + + // try to withdraw from target pool and provide to migration pool in the same transaction + let err = shared_multisig + .withdraw( + &manager2, + None, + Some(ProvideParams { + slippage_tolerance: None, + auto_stake: None, + }), + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Operation is unavailable. Rage quit has already started" + ); +} + +#[test] +fn test_autostake_and_withdraw() { + let astroport = astroport_address(); + let manager1 = Addr::unchecked(MANAGER1); + let manager2 = Addr::unchecked(MANAGER2); + + let denom1 = String::from("untrn"); + let denom2 = String::from("ibc/astro"); + let denom3 = String::from("usdt"); + + let router = Rc::new(RefCell::new(mock_app( + &astroport, + Some(vec![ + Coin { + denom: denom1.clone(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: denom2.clone(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: denom3, + amount: Uint128::new(100_000_000_000u128), + }, + ]), + ))); + + let mut generator = MockGeneratorBuilder::new(&router).instantiate(); + let factory = generator.factory(); + let astro_token = generator.astro_token_info(); + let coin_registry = factory.coin_registry(); + coin_registry.add(vec![(denom1.to_owned(), 6), (denom2.to_owned(), 6)]); + + let xyk = factory.instantiate_xyk_pair(&[ + AssetInfo::NativeToken { + denom: denom1.clone(), + }, + AssetInfo::NativeToken { + denom: denom2.clone(), + }, + ]); + + let xyk_pair_info = xyk.pair_info().unwrap(); + let shared_multisig = MockSharedMultisigBuilder::new(&router).instantiate( + &factory.address, + Some(generator.address.clone()), + None, + ); + + // Direct set up target pool without proposal + shared_multisig + .setup_pools( + &shared_multisig.address, + Some(xyk.address.to_string()), + None, + ) + .unwrap(); + + let config = shared_multisig.query_config().unwrap(); + assert_eq!(config.target_pool, Some(xyk.address.clone())); + + // Sends tokens to the multisig + shared_multisig.send_tokens(&astroport, None, None).unwrap(); + + // Check the holder's balance for denom1 + let res = shared_multisig.query_native_balance(None, &denom1).unwrap(); + assert_eq!(res.amount, Uint128::new(900_000_000)); + assert_eq!(res.denom, denom1.clone()); + + // Check the holder's balance for denom2 + let res = shared_multisig.query_native_balance(None, &denom2).unwrap(); + assert_eq!(res.amount, Uint128::new(900_000_000)); + assert_eq!(res.denom, denom2.clone()); + + // try to provide from manager1 + shared_multisig + .provide(&manager1, PoolType::Target, None, None, None, None) + .unwrap(); + + // try to provide from manager2 + shared_multisig + .provide( + &manager2, + PoolType::Target, + None, + Some(Decimal::from_ratio(5u128, 10u128)), + None, + None, + ) + .unwrap(); + + // Check the holder's balance for denom1 + let res = shared_multisig.query_native_balance(None, &denom1).unwrap(); + assert_eq!(res.amount, Uint128::new(700_000_000)); + assert_eq!(res.denom, denom1.clone()); + + // Check the holder's balance for denom2 + let res = shared_multisig.query_native_balance(None, &denom2).unwrap(); + assert_eq!(res.amount, Uint128::new(700_000_000)); + assert_eq!(res.denom, denom2.clone()); + + // Check the holder's LP balance + let res = shared_multisig + .query_cw20_balance(&xyk_pair_info.liquidity_token, None) + .unwrap(); + assert_eq!(res.balance, Uint128::new(199999000)); + + // Try to unstake from generator + let err = shared_multisig + .withdraw_generator(&manager2, Some(Uint128::new(10))) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Insufficient balance for: contract8. Available balance: 0" + ); + + // try to provide from manager2 + shared_multisig + .provide(&manager2, PoolType::Target, None, None, Some(true), None) + .unwrap(); + + assert_eq!( + generator.query_deposit(&xyk.lp_token(), &shared_multisig.address), + Uint128::new(100_000_000), + ); + + assert_eq!( + generator.pending_token(&xyk.lp_token().address, &shared_multisig.address), + PendingTokenResponse { + pending: Default::default(), + pending_on_proxy: None + }, + ); + + generator.setup_pools(&[(xyk.lp_token().address.to_string(), Uint128::one())]); + + router.borrow_mut().update_block(|b| { + b.height += 100; + }); + + assert_eq!( + generator.pending_token(&xyk.lp_token().address, &shared_multisig.address), + PendingTokenResponse { + pending: Uint128::new(100_000_000), + pending_on_proxy: None + }, + ); + + // try to claim from manager2 + shared_multisig.claim_generator_rewards(&manager2).unwrap(); + + assert_eq!( + generator.pending_token(&xyk.lp_token().address, &shared_multisig.address), + PendingTokenResponse { + pending: Uint128::zero(), + pending_on_proxy: None + }, + ); + + // Check the holder's ASTRO balance + let res = shared_multisig + .query_cw20_balance(&Addr::unchecked(astro_token.to_string()), None) + .unwrap(); + assert_eq!(res.balance, Uint128::new(100_000_000)); + + // check the holder's deposit + assert_eq!( + generator.query_deposit(&xyk.lp_token(), &shared_multisig.address), + Uint128::new(100_000_000), + ); + + // Try to unstake from generator + shared_multisig.withdraw_generator(&manager2, None).unwrap(); + + // Check the holder's LP balance + let res = shared_multisig + .query_cw20_balance(&xyk_pair_info.liquidity_token, None) + .unwrap(); + assert_eq!(res.balance, Uint128::new(299_999_000)); + + router.borrow_mut().update_block(|b| { + b.height += 100; + }); + + // check the holder's deposit + assert_eq!( + generator.query_deposit(&xyk.lp_token(), &shared_multisig.address), + Uint128::zero(), + ); + + assert_eq!( + generator.pending_token(&xyk.lp_token().address, &shared_multisig.address), + PendingTokenResponse { + pending: Uint128::zero(), + pending_on_proxy: None + }, + ); + + // check the holder's deposit + assert_eq!( + generator.query_deposit(&xyk.lp_token(), &shared_multisig.address), + Uint128::zero(), + ); + + // Try to deposit to generator + shared_multisig + .deposit_generator(&manager2, Some(Uint128::new(10))) + .unwrap(); + + // check the holder's deposit + assert_eq!( + generator.query_deposit(&xyk.lp_token(), &shared_multisig.address), + Uint128::new(10), + ); + + // Try to deposit zero LP tokens to generator + let err = shared_multisig + .deposit_generator(&manager2, Some(Uint128::zero())) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Invalid zero amount"); + + // Try to deposit more LP tokens to generator then we have + let err = shared_multisig + .deposit_generator(&manager2, Some(Uint128::new(1000000000000))) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Insufficient balance for: contract8. Available balance: 299998990" + ); + + // Try to deposit all LP tokens to generator + shared_multisig.deposit_generator(&manager2, None).unwrap(); + + // check the holder's deposit + assert_eq!( + generator.query_deposit(&xyk.lp_token(), &shared_multisig.address), + Uint128::new(299999000), + ); + + assert_eq!( + generator.pending_token(&xyk.lp_token().address, &shared_multisig.address), + PendingTokenResponse { + pending: Uint128::zero(), + pending_on_proxy: None + }, + ); + + router.borrow_mut().update_block(|b| { + b.height += 100; + }); + + assert_eq!( + generator.pending_token(&xyk.lp_token().address, &shared_multisig.address), + PendingTokenResponse { + pending: Uint128::new(99_999_999), + pending_on_proxy: None + }, + ); + + // Try to unstake from generator + shared_multisig.withdraw_generator(&manager2, None).unwrap(); + + // check the holder's deposit + assert_eq!( + generator.query_deposit(&xyk.lp_token(), &shared_multisig.address), + Uint128::zero(), ); } diff --git a/contracts/router/Cargo.toml b/contracts/router/Cargo.toml index 68da90e63..074f1276f 100644 --- a/contracts/router/Cargo.toml +++ b/contracts/router/Cargo.toml @@ -28,7 +28,7 @@ cw20 = "0.15" cosmwasm-std = "1.1" cw-storage-plus = "0.15" integer-sqrt = "0.1" -astroport = { path = "../../packages/astroport", default-features = false } +astroport = { path = "../../packages/astroport", version = "3" } thiserror = { version = "1.0" } cosmwasm-schema = "1.1" diff --git a/contracts/router/src/testing/mock_querier.rs b/contracts/router/src/testing/mock_querier.rs index 35fda74fb..6d8764d95 100644 --- a/contracts/router/src/testing/mock_querier.rs +++ b/contracts/router/src/testing/mock_querier.rs @@ -225,7 +225,7 @@ impl WasmMockQuerier { pub fn with_balance(&mut self, balances: &[(&String, &[Coin])]) { for (addr, balance) in balances { - self.base.update_balance(addr.clone(), balance.to_vec()); + self.base.update_balance(addr.as_str(), balance.to_vec()); } } diff --git a/contracts/token/Cargo.toml b/contracts/token/Cargo.toml index a132d9da9..73ff66921 100644 --- a/contracts/token/Cargo.toml +++ b/contracts/token/Cargo.toml @@ -18,7 +18,7 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -astroport = { path = "../../packages/astroport", default-features = false } +astroport = { path = "../../packages/astroport", version = "3" } cw2 = "0.15" cw20 = "0.15" cw20-base = { version = "0.15", features = ["library"] } diff --git a/contracts/tokenomics/generator/Cargo.toml b/contracts/tokenomics/generator/Cargo.toml index 7c2de8dc4..adef6cbbc 100644 --- a/contracts/tokenomics/generator/Cargo.toml +++ b/contracts/tokenomics/generator/Cargo.toml @@ -5,9 +5,9 @@ authors = ["Astroport"] edition = "2021" exclude = [ - # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. - "contract.wasm", - "hash.txt", + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", ] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -20,23 +20,21 @@ crate-type = ["cdylib", "rlib"] backtraces = ["cosmwasm-std/backtraces"] [dependencies] - cw-storage-plus = "0.15" cw1-whitelist = { version = "0.15", features = ["library"] } thiserror = { version = "1.0" } -astroport-governance = { git = "https://github.com/astroport-fi/astroport-governance" } +astroport-governance = { git = "https://github.com/astroport-fi/astroport-governance", version = "1" } protobuf = { version = "2", features = ["with-bytes"] } cosmwasm-std = "1.1" cw2 = "0.15" cw20 = "0.15" -astroport = { path = "../../../packages/astroport" } +astroport = { path = "../../../packages/astroport", version = "3" } cosmwasm-schema = "1.1" cw-utils = "1.0.1" [dev-dependencies] generator-controller = { git = "https://github.com/astroport-fi/astroport-governance" } -astroport-mocks = { path = "../../../packages/astroport_mocks/" } -cw-multi-test = "0.15" +astroport-mocks = { path = "../../../packages/astroport_mocks" } astroport-token = { path = "../../token" } astroport-vesting = { path = "../vesting" } astroport-staking = { path = "../staking" } @@ -49,8 +47,6 @@ voting-escrow = { git = "https://github.com/astroport-fi/astroport-governance" } voting-escrow-delegation = { git = "https://github.com/astroport-fi/astroport-governance" } astroport-nft = { git = "https://github.com/astroport-fi/astroport-governance" } cw721-base = { version = "0.15", features = ["library"] } - - generator-proxy-to-vkr = { git = "https://github.com/astroport-fi/astro-generator-proxy-contracts", branch = "main" } valkyrie = { git = "https://github.com/astroport-fi/valkyrieprotocol", rev = "b5fcb666f17d7e291f40365756e50fc0d7b9bf54" } valkyrie-lp-staking = { git = "https://github.com/astroport-fi/valkyrieprotocol", rev = "b5fcb666f17d7e291f40365756e50fc0d7b9bf54" } diff --git a/contracts/tokenomics/maker/Cargo.toml b/contracts/tokenomics/maker/Cargo.toml index 07f917e5a..90f8ed854 100644 --- a/contracts/tokenomics/maker/Cargo.toml +++ b/contracts/tokenomics/maker/Cargo.toml @@ -25,10 +25,10 @@ cosmwasm-std = "1.1" cw2 = "0.15" cw20 = "0.15" cw-storage-plus = "0.15" -astroport = { path = "../../../packages/astroport", default-features = false } +astroport = { path = "../../../packages/astroport", version = "3" } thiserror = { version = "1.0" } cosmwasm-schema = "1.1" -astro-satellite-package = { git = "https://github.com/astroport-fi/astroport_ibc" } +astro-satellite-package = { git = "https://github.com/astroport-fi/astroport_ibc", version = "0.1" } [dev-dependencies] astroport-token = { path = "../../token" } diff --git a/contracts/tokenomics/maker/src/utils.rs b/contracts/tokenomics/maker/src/utils.rs index 364e52ede..74160f883 100644 --- a/contracts/tokenomics/maker/src/utils.rs +++ b/contracts/tokenomics/maker/src/utils.rs @@ -8,8 +8,8 @@ use astroport::pair::Cw20HookMsg; use astroport::querier::query_pair_info; use cosmwasm_std::{ - coins, to_binary, wasm_execute, Addr, Binary, CosmosMsg, Decimal, Deps, Env, QuerierWrapper, - StdError, StdResult, SubMsg, Uint128, WasmMsg, + coins, to_binary, wasm_execute, Addr, Binary, CosmosMsg, Decimal, Deps, Empty, Env, + QuerierWrapper, StdError, StdResult, SubMsg, Uint128, WasmMsg, }; use cw20::Cw20ExecuteMsg; @@ -219,7 +219,9 @@ pub fn build_send_msg( })), AssetInfo::NativeToken { denom } => Ok(CosmosMsg::Wasm(wasm_execute( recipient, - &astro_satellite_package::ExecuteMsg::TransferAstro {}, + // Satellite type parameter is only needed for CheckMessages endpoint which is not used in Maker contract. + // So it's safe to pass Empty as CustomMsg + &astro_satellite_package::ExecuteMsg::::TransferAstro {}, coins(asset.amount.u128(), denom), )?)), } diff --git a/contracts/tokenomics/staking/Cargo.toml b/contracts/tokenomics/staking/Cargo.toml index eef76663d..402c3f0fa 100644 --- a/contracts/tokenomics/staking/Cargo.toml +++ b/contracts/tokenomics/staking/Cargo.toml @@ -25,7 +25,7 @@ cw-storage-plus = "0.15" thiserror = { version = "1.0" } cw2 = "0.15" cw20 = "0.15" -astroport = { path = "../../../packages/astroport", default-features = false } +astroport = { path = "../../../packages/astroport", version = "3" } protobuf = { version = "2", features = ["with-bytes"] } cosmwasm-schema = { version = "1.1" } cw-utils = "1.0.1" diff --git a/contracts/tokenomics/vesting/Cargo.toml b/contracts/tokenomics/vesting/Cargo.toml index b00fa55b9..89358b6f9 100644 --- a/contracts/tokenomics/vesting/Cargo.toml +++ b/contracts/tokenomics/vesting/Cargo.toml @@ -17,7 +17,7 @@ cw2 = { version = "0.15" } cw20 = { version = "0.15" } cosmwasm-std = { version = "1.1" } cw-storage-plus = "0.15" -astroport = { path = "../../../packages/astroport", default-features = false } +astroport = { path = "../../../packages/astroport", version = "3" } thiserror = { version = "1.0" } cw-utils = "0.15" cosmwasm-schema = { version = "1.1", default-features = false } diff --git a/contracts/tokenomics/xastro_outpost_token/Cargo.toml b/contracts/tokenomics/xastro_outpost_token/Cargo.toml index 44c38af8f..cfb7cc735 100644 --- a/contracts/tokenomics/xastro_outpost_token/Cargo.toml +++ b/contracts/tokenomics/xastro_outpost_token/Cargo.toml @@ -18,7 +18,7 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -astroport = { path = "../../../packages/astroport", default-features = false } +astroport = { path = "../../../packages/astroport", version = "3" } cw2 = "0.15" cw20 = "0.15" cw20-base = { version = "0.15", features = ["library"] } diff --git a/contracts/tokenomics/xastro_token/Cargo.toml b/contracts/tokenomics/xastro_token/Cargo.toml index f6cf05e62..746133ea2 100644 --- a/contracts/tokenomics/xastro_token/Cargo.toml +++ b/contracts/tokenomics/xastro_token/Cargo.toml @@ -18,7 +18,7 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -astroport = { path = "../../../packages/astroport", default-features = false } +astroport = { path = "../../../packages/astroport", version = "3" } cw2 = "0.15" cw20 = "0.15" cw20-base = { version = "0.15", features = ["library"] } diff --git a/contracts/whitelist/Cargo.toml b/contracts/whitelist/Cargo.toml index c5b86898d..817511b77 100644 --- a/contracts/whitelist/Cargo.toml +++ b/contracts/whitelist/Cargo.toml @@ -18,7 +18,7 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -astroport = { path = "../../packages/astroport", default-features = false } +astroport = { path = "../../packages/astroport", version = "3" } cw1-whitelist = { version = "0.15", features = ["library"] } cw2 = "0.15" cosmwasm-std = "1.1" diff --git a/packages/astroport/Cargo.toml b/packages/astroport/Cargo.toml index 191242bc4..810b180ce 100644 --- a/packages/astroport/Cargo.toml +++ b/packages/astroport/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport" -version = "3.3.2" +version = "3.6.0" authors = ["Astroport"] edition = "2021" description = "Common Astroport types, queriers and other utils" @@ -23,13 +23,13 @@ uint = "0.9" cw-storage-plus = "0.15" itertools = "0.10" cosmwasm-schema = "1.1" -astroport-circular-buffer = { path = "../circular_buffer" } +astroport-circular-buffer = { version = "0.1", path = "../circular_buffer" } cw-utils = "1.0" cw3 = "1.0" # optional injective-math = { version = "0.1", optional = true } -thiserror = { version="1.0", optional = true } +thiserror = { version = "1.0", optional = true } [dev-dependencies] test-case = "3.1.0" diff --git a/packages/astroport/src/cw20_ics20.rs b/packages/astroport/src/cw20_ics20.rs new file mode 100644 index 000000000..9409488a6 --- /dev/null +++ b/packages/astroport/src/cw20_ics20.rs @@ -0,0 +1,16 @@ +use cosmwasm_schema::cw_serde; + +/// This is the message we accept via Receive +#[cw_serde] +pub struct TransferMsg { + /// The local channel to send the packets on + pub channel: String, + /// The remote address to send to. + /// Don't use HumanAddress as this will likely have a different Bech32 prefix than we use + /// and cannot be validated locally + pub remote_address: String, + /// How long the packet lives in seconds. If not specified, use default_timeout + pub timeout: Option, + /// An optional memo to add to the IBC transfer + pub memo: Option, +} diff --git a/packages/astroport/src/factory.rs b/packages/astroport/src/factory.rs index d8091b44f..059c53ee0 100644 --- a/packages/astroport/src/factory.rs +++ b/packages/astroport/src/factory.rs @@ -32,6 +32,7 @@ pub struct Config { /// Stable {}; /// Custom(String::from("Custom")); /// ``` +#[derive(Eq)] #[cw_serde] pub enum PairType { /// XYK pair type diff --git a/packages/astroport/src/lib.rs b/packages/astroport/src/lib.rs index 774861eec..461927f14 100644 --- a/packages/astroport/src/lib.rs +++ b/packages/astroport/src/lib.rs @@ -1,10 +1,13 @@ pub mod asset; pub mod common; pub mod cosmwasm_ext; +pub mod cw20_ics20; pub mod factory; pub mod fee_granter; pub mod generator; pub mod generator_proxy; +#[cfg(feature = "injective")] +pub mod injective_ext; pub mod maker; pub mod native_coin_registry; pub mod native_coin_wrapper; @@ -25,9 +28,6 @@ pub mod vesting; pub mod xastro_outpost_token; pub mod xastro_token; -#[cfg(feature = "injective")] -pub mod injective_ext; - #[cfg(test)] mod mock_querier; diff --git a/packages/astroport/src/liquidity_manager.rs b/packages/astroport/src/liquidity_manager.rs index 7b9f7facb..e2f78f6bc 100644 --- a/packages/astroport/src/liquidity_manager.rs +++ b/packages/astroport/src/liquidity_manager.rs @@ -1,9 +1,9 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::Uint128; +use cosmwasm_std::{Addr, Uint128}; use cw20::Cw20ReceiveMsg; -use crate::asset::Asset; -use crate::pair::{Cw20HookMsg as PairCw20HookMsg, ExecuteMsg as PairExecuteMsg}; +use crate::asset::{Asset, AssetInfo, PairInfo}; +use crate::pair::{Cw20HookMsg as PairCw20HookMsg, ExecuteMsg as PairExecuteMsg, FeeShareConfig}; #[cw_serde] pub struct InstantiateMsg { @@ -44,3 +44,37 @@ pub enum QueryMsg { lp_tokens: Uint128, }, } + +/// Stable swap config which is used in raw queries. It's compatible with v1, v2 and v3 stable pair contract. +#[cw_serde] +pub struct CompatPairStableConfig { + /// The contract owner + pub owner: Option, + /// The pair information stored in a [`PairInfo`] struct + pub pair_info: PairInfo, + /// The factory contract address + pub factory_addr: Addr, + /// The last timestamp when the pair contract update the asset cumulative prices + pub block_time_last: u64, + /// This is the current amplification used in the pool + pub init_amp: u64, + /// This is the start time when amplification starts to scale up or down + pub init_amp_time: u64, + /// This is the target amplification to reach at `next_amp_time` + pub next_amp: u64, + /// This is the timestamp when the current pool amplification should be `next_amp` + pub next_amp_time: u64, + + // Fields below are added for compatability with v1 and v2 + /// The greatest precision of assets in the pool + pub greatest_precision: Option, + /// The vector contains cumulative prices for each pair of assets in the pool + #[serde(default)] + pub cumulative_prices: Vec<(AssetInfo, AssetInfo, Uint128)>, + /// The last cumulative price 0 asset in pool + pub price0_cumulative_last: Option, + /// The last cumulative price 1 asset in pool + pub price1_cumulative_last: Option, + // Fee sharing configuration + pub fee_share: Option, +} diff --git a/packages/astroport/src/observation.rs b/packages/astroport/src/observation.rs index c723cd721..052bb570f 100644 --- a/packages/astroport/src/observation.rs +++ b/packages/astroport/src/observation.rs @@ -1,20 +1,18 @@ -use crate::cosmwasm_ext::AbsDiff; -use astroport_circular_buffer::{BufferManager, CircularBuffer}; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{ - CustomQuery, Decimal, Decimal256, Deps, Env, StdError, StdResult, Storage, Uint128, -}; +use cosmwasm_std::{CustomQuery, Decimal, Deps, Env, StdError, StdResult, Storage, Uint128}; +use cw_storage_plus::Item; + +use astroport_circular_buffer::{BufferManager, CircularBuffer}; + +use crate::cosmwasm_ext::AbsDiff; /// Circular buffer size which stores observations pub const OBSERVATIONS_SIZE: u32 = 3000; -/// Min safe trading size (0.001) to calculate oracle price in observation. This value considers -/// amount in decimal form with respective token precision. -pub const MIN_TRADE_SIZE: Decimal256 = Decimal256::raw(1000000000000000); /// Stores trade size observations. We use it in orderbook integration /// and derive prices for external contracts/users. #[cw_serde] -#[derive(Copy)] +#[derive(Copy, Default)] pub struct Observation { pub timestamp: u64, /// Base asset simple moving average (mean) @@ -54,7 +52,18 @@ where oldest_ind = 0; newest_ind %= buffer.capacity(); } else { - return Err(StdError::generic_err("Buffer is empty")); + return match PrecommitObservation::may_load(deps.storage)? { + // First observation after pool initialization could be captured but not committed yet + Some(obs) if obs.precommit_ts <= target => Ok(OracleObservation { + timestamp: target, + price: Decimal::from_ratio(obs.base_amount, obs.quote_amount), + }), + Some(_) => Err(StdError::generic_err(format!( + "Requested observation is too old. Last known observation is at {}", + target + ))), + None => Err(StdError::generic_err("Buffer is empty")), + }; } } @@ -143,6 +152,47 @@ fn binary_search( } } +#[cw_serde] +pub struct PrecommitObservation { + pub base_amount: Uint128, + pub quote_amount: Uint128, + pub precommit_ts: u64, +} + +impl<'a> PrecommitObservation { + /// Temporal storage for observation which should be committed in the next block + const PRECOMMIT_OBSERVATION: Item<'a, PrecommitObservation> = + Item::new("precommit_observation"); + + pub fn save( + storage: &mut dyn Storage, + env: &Env, + base_amount: Uint128, + quote_amount: Uint128, + ) -> StdResult<()> { + let next_obs = match Self::may_load(storage)? { + // Accumulating observations at the same block + Some(mut prev_obs) if env.block.time.seconds() == prev_obs.precommit_ts => { + prev_obs.base_amount += base_amount; + prev_obs.quote_amount += quote_amount; + prev_obs + } + _ => PrecommitObservation { + base_amount, + quote_amount, + precommit_ts: env.block.time.seconds(), + }, + }; + + Self::PRECOMMIT_OBSERVATION.save(storage, &next_obs) + } + + #[inline] + pub fn may_load(storage: &dyn Storage) -> StdResult> { + Self::PRECOMMIT_OBSERVATION.may_load(storage) + } +} + #[cfg(test)] mod test { use crate::observation::Observation; diff --git a/packages/astroport/src/pair.rs b/packages/astroport/src/pair.rs index 174ed4590..53f861c38 100644 --- a/packages/astroport/src/pair.rs +++ b/packages/astroport/src/pair.rs @@ -3,17 +3,23 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use crate::asset::{Asset, AssetInfo, PairInfo}; -use cosmwasm_std::{Addr, Binary, Decimal, Uint128, Uint64}; +use cosmwasm_std::{Addr, Binary, Decimal, Decimal256, Uint128, Uint64}; use cw20::Cw20ReceiveMsg; /// The default swap slippage pub const DEFAULT_SLIPPAGE: &str = "0.005"; /// The maximum allowed swap slippage pub const MAX_ALLOWED_SLIPPAGE: &str = "0.5"; +/// The maximum fee share allowed, 10% +pub const MAX_FEE_SHARE_BPS: u16 = 1000; /// Decimal precision for TWAP results pub const TWAP_PRECISION: u8 = 6; +/// Min safe trading size (0.00001) to calculate a price. This value considers +/// amount in decimal form with respective token precision. +pub const MIN_TRADE_SIZE: Decimal256 = Decimal256::raw(10000000000000); + /// This structure describes the parameters used for creating a contract. #[cw_serde] pub struct InstantiateMsg { @@ -151,6 +157,15 @@ pub struct ConfigResponse { pub factory_addr: Addr, } +/// Holds the configuration for fee sharing +#[cw_serde] +pub struct FeeShareConfig { + /// The fee shared with the address + pub bps: u16, + /// The share is sent to this address on every swap + pub recipient: Addr, +} + /// This structure holds the parameters that are returned from a swap simulation response #[cw_serde] pub struct SimulationResponse { @@ -203,6 +218,8 @@ pub struct XYKPoolParams { pub struct XYKPoolConfig { /// Whether asset balances are tracked over blocks or not. pub track_asset_balances: bool, + // The config for swap fee sharing + pub fee_share: Option, } /// This enum stores the option available to enable asset balances tracking over blocks. @@ -210,6 +227,14 @@ pub struct XYKPoolConfig { pub enum XYKPoolUpdateParams { /// Enables asset balances tracking over blocks. EnableAssetBalancesTracking, + /// Enables the sharing of swap fees with an external party. + EnableFeeShare { + /// The fee shared with the fee_share_address + fee_share_bps: u16, + /// The fee_share_bps is sent to this address on every swap + fee_share_address: String, + }, + DisableFeeShare, } /// This structure holds stableswap pool parameters. @@ -226,13 +251,26 @@ pub struct StablePoolParams { pub struct StablePoolConfig { /// The stableswap pool amplification pub amp: Decimal, + // The config for swap fee sharing + pub fee_share: Option, } /// This enum stores the options available to start and stop changing a stableswap pool's amplification. #[cw_serde] pub enum StablePoolUpdateParams { - StartChangingAmp { next_amp: u64, next_amp_time: u64 }, + StartChangingAmp { + next_amp: u64, + next_amp_time: u64, + }, StopChangingAmp {}, + /// Enables the sharing of swap fees with an external party. + EnableFeeShare { + /// The fee shared with the fee_share_address + fee_share_bps: u16, + /// The fee_share_bps is sent to this address on every swap + fee_share_address: String, + }, + DisableFeeShare, } #[cfg(test)] @@ -281,6 +319,7 @@ mod tests { params: Some( to_binary(&StablePoolConfig { amp: Decimal::one(), + fee_share: None, }) .unwrap(), ), diff --git a/packages/astroport/src/pair_concentrated.rs b/packages/astroport/src/pair_concentrated.rs index 6211f6812..24e4d864e 100644 --- a/packages/astroport/src/pair_concentrated.rs +++ b/packages/astroport/src/pair_concentrated.rs @@ -5,8 +5,8 @@ use crate::asset::PairInfo; use crate::asset::{Asset, AssetInfo}; use crate::observation::OracleObservation; use crate::pair::{ - ConfigResponse, CumulativePricesResponse, PoolResponse, ReverseSimulationResponse, - SimulationResponse, + ConfigResponse, CumulativePricesResponse, FeeShareConfig, PoolResponse, + ReverseSimulationResponse, SimulationResponse, }; /// This structure holds concentrated pool parameters. @@ -36,6 +36,8 @@ pub struct ConcentratedPoolParams { /// They will not be tracked if the parameter is ignored. /// It can not be disabled later once enabled. pub track_asset_balances: Option, + /// The config for swap fee sharing + pub fee_share: Option, } /// This structure holds concentrated pool parameters which can be changed immediately. @@ -68,6 +70,14 @@ pub enum ConcentratedPoolUpdateParams { StopChangingAmpGamma {}, /// Enable asset balances tracking EnableAssetBalancesTracking {}, + /// Enables the sharing of swap fees with an external party. + EnableFeeShare { + /// The fee shared with the fee_share_address + fee_share_bps: u16, + /// The fee_share_bps is sent to this address on every swap + fee_share_address: String, + }, + DisableFeeShare, } /// This structure stores a CL pool's configuration. @@ -95,6 +105,8 @@ pub struct ConcentratedPoolConfig { pub ma_half_time: u64, /// Whether asset balances are tracked over blocks or not. pub track_asset_balances: bool, + /// The config for swap fee sharing + pub fee_share: Option, } /// This structure describes the query messages available in the contract. diff --git a/packages/astroport/src/shared_multisig.rs b/packages/astroport/src/shared_multisig.rs index e04bf924a..1a78885a4 100644 --- a/packages/astroport/src/shared_multisig.rs +++ b/packages/astroport/src/shared_multisig.rs @@ -1,5 +1,6 @@ +use crate::asset::Asset; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{from_slice, Addr, CosmosMsg, Empty, StdResult}; +use cosmwasm_std::{from_slice, Addr, CosmosMsg, Decimal, Empty, StdResult, Uint128}; use cw3::Vote; use cw_storage_plus::{Key, KeyDeserialize, Prefixer, PrimaryKey}; use cw_utils::{Duration, Expiration, Threshold, ThresholdResponse}; @@ -13,41 +14,86 @@ pub struct Config { pub threshold: Threshold, pub total_weight: u64, pub max_voting_period: Duration, + /// The factory contract address + pub factory_addr: Addr, + /// The generator contract address + pub generator_addr: Addr, /// Address allowed to change contract parameters - pub dao: Addr, + pub manager1: Addr, /// Address allowed to change contract parameters - pub manager: Addr, + pub manager2: Addr, + /// The target pool is the one where the contract can LP NTRN and ASTRO at the current pool price + pub target_pool: Option, + /// This is the pool into which liquidity will be migrated from the target pool. + pub migration_pool: Option, + /// Allows to withdraw funds for both managers + pub rage_quit_started: bool, + /// This is the denom that the first manager will manage. + pub denom1: String, + /// This is the denom that the second manager will manage. + pub denom2: String, } #[cw_serde] pub struct ConfigResponse { pub threshold: ThresholdResponse, pub max_voting_period: Duration, - pub dao: Addr, - pub manager: Addr, + pub manager1: String, + pub manager2: String, + pub target_pool: Option, + pub migration_pool: Option, + pub rage_quit_started: bool, + pub denom1: String, + pub denom2: String, + pub factory: String, + pub generator: String, } #[cw_serde] pub struct InstantiateMsg { + pub factory_addr: String, + pub generator_addr: String, pub max_voting_period: Duration, /// Address allowed to change contract parameters - pub dao: String, + pub manager1: String, /// Address allowed to change contract parameters - pub manager: String, + pub manager2: String, + /// This is the denom that the first manager will manage. + pub denom1: String, + /// This is the denom that the second manager will manage. + pub denom2: String, + /// The target pool is the one where the contract can LP NTRN and ASTRO at the current pool price + pub target_pool: Option, +} + +#[cw_serde] +#[derive(Hash, Eq)] +pub enum PoolType { + Target, + Migration, +} + +impl Display for PoolType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + PoolType::Target => f.write_str("target"), + PoolType::Migration => f.write_str("migration"), + } + } } #[cw_serde] #[derive(Hash, Eq)] pub enum MultisigRole { - Manager, - Dao, + Manager1, + Manager2, } impl Display for MultisigRole { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - MultisigRole::Manager => f.write_str("manager"), - MultisigRole::Dao => f.write_str("dao"), + MultisigRole::Manager1 => f.write_str("manager1"), + MultisigRole::Manager2 => f.write_str("manager2"), } } } @@ -55,8 +101,8 @@ impl Display for MultisigRole { impl MultisigRole { pub fn as_bytes(&self) -> &[u8] { match self { - MultisigRole::Manager => "manager".as_bytes(), - MultisigRole::Dao => "dao".as_bytes(), + MultisigRole::Manager1 => "manager1".as_bytes(), + MultisigRole::Manager2 => "manager2".as_bytes(), } } } @@ -90,8 +136,73 @@ impl KeyDeserialize for &MultisigRole { } } +#[cw_serde] +pub struct ProvideParams { + /// The slippage tolerance that allows liquidity provision only if the price in the pool + /// doesn't move too much + pub slippage_tolerance: Option, + /// Determines whether the LP tokens minted for the user is auto_staked in the Generator contract + pub auto_stake: Option, +} + #[cw_serde] pub enum ExecuteMsg { + UpdateConfig { + generator: Option, + factory: Option, + }, + SetupPools { + /// The target pool is the one where the contract can LP NTRN and ASTRO at the current pool price + target_pool: Option, + /// This is the pool into which liquidity will be migrated from the target pool. + migration_pool: Option, + }, + SetupMaxVotingPeriod { + max_voting_period: Duration, + }, + /// Withdraws coins from the target pool. Also it is possible to withdraw LP from the target + /// pool and make provide LP to the migration pool in the same transaction. + WithdrawTargetPoolLP { + withdraw_amount: Option, + provide_params: Option, + }, + /// Withdraws coins from the specified pool. + WithdrawRageQuitLP { + pool_type: PoolType, + withdraw_amount: Option, + }, + /// Withdraw LP tokens from the Generator + WithdrawGenerator { + /// The amount to withdraw + amount: Option, + }, + /// Update generator rewards and returns them to the Multisig. + ClaimGeneratorRewards {}, + /// Deposit the target LP tokens to the generator + DepositGenerator { + /// The amount to deposit + amount: Option, + }, + /// Provides liquidity to the specified pool + ProvideLiquidity { + pool_type: PoolType, + /// The assets available in the pool + assets: Vec, + /// The slippage tolerance that allows liquidity provision only if the price in the pool doesn't move too much + slippage_tolerance: Option, + /// Determines whether the LP tokens minted for the user is auto_staked in the Generator contract + auto_stake: Option, + /// The receiver of LP tokens + receiver: Option, + }, + /// Transfers manager coins and other coins from the shared_multisig. + /// Executor: manager1 or manager2. + Transfer { + asset: Asset, + recipient: Option, + }, + CompleteTargetPoolMigration {}, + StartRageQuit {}, Propose { title: String, description: String, @@ -106,36 +217,33 @@ pub enum ExecuteMsg { Execute { proposal_id: u64, }, - UpdateConfig { - max_voting_period: Duration, - }, Close { proposal_id: u64, }, - /// Creates a proposal to change contract manager. The validity period for the proposal is set + /// Creates a proposal to change contract manager1. The validity period for the proposal is set /// in the `expires_in` variable. - ProposeNewManager { + ProposeNewManager1 { /// Newly proposed contract manager - manager: String, + new_manager: String, /// The date after which this proposal expires expires_in: u64, }, - /// Removes the existing offer to change contract manager. - DropManagerProposal {}, - /// Used to claim a new contract manager. - ClaimManager {}, - /// Creates a proposal to change contract DAO. The validity period for the proposal is set + /// Removes the existing offer to change contract manager1. + DropManager1Proposal {}, + /// Used to claim a new contract manager1. + ClaimManager1 {}, + /// Creates a proposal to change contract manager2. The validity period for the proposal is set /// in the `expires_in` variable. - ProposeNewDao { - /// Newly proposed contract DAO - dao: String, + ProposeNewManager2 { + /// Newly proposed contract + new_manager: String, /// The date after which this proposal expires expires_in: u64, }, - /// Removes the existing offer to change contract manager. - DropDaoProposal {}, - /// Used to claim a new contract DAO. - ClaimDao {}, + /// Removes the existing offer to change contract manager2. + DropManager2Proposal {}, + /// Used to claim a new second manager of contract. + ClaimManager2 {}, } #[cw_serde] @@ -172,13 +280,13 @@ mod tests { #[test] fn test_multisig_role() { - assert_eq!(MultisigRole::Manager.as_bytes(), "manager".as_bytes()); - assert_eq!(MultisigRole::Dao.as_bytes(), "dao".as_bytes()); + assert_eq!(MultisigRole::Manager1.as_bytes(), "manager1".as_bytes()); + assert_eq!(MultisigRole::Manager2.as_bytes(), "manager2".as_bytes()); } #[test] fn test_multisig_role_display() { - assert_eq!(MultisigRole::Manager.to_string(), "manager"); - assert_eq!(MultisigRole::Dao.to_string(), "dao"); + assert_eq!(MultisigRole::Manager1.to_string(), "manager1"); + assert_eq!(MultisigRole::Manager2.to_string(), "manager2"); } } diff --git a/packages/astroport_mocks/Cargo.toml b/packages/astroport_mocks/Cargo.toml index f94a37125..28cf91f3c 100644 --- a/packages/astroport_mocks/Cargo.toml +++ b/packages/astroport_mocks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-mocks" -version = "0.1.0" +version = "0.2.0" authors = ["Astroport"] edition = "2021" description = "Mock Astroport contracts used for integration testing" @@ -15,6 +15,7 @@ astroport = { path = "../astroport" } astroport-factory = { path = "../../contracts/factory" } astroport-generator = { path = "../../contracts/tokenomics/generator" } astroport-native-coin-registry = { path = "../../contracts/periphery/native_coin_registry" } +astroport-shared-multisig = { path = "../../contracts/periphery/shared_multisig" } astroport-pair = { path = "../../contracts/pair" } astroport-pair-stable = { path = "../../contracts/pair_stable" } astroport-pair-concentrated = { path = "../../contracts/pair_concentrated" } @@ -30,3 +31,7 @@ cw-multi-test = { git = "https://github.com/astroport-fi/cw-multi-test.git", rev injective-cosmwasm = "0.2" schemars = "0.8.1" serde = "1.0" +cw-utils = "1.0" +cw20 = "0.15" +anyhow = "1.0" +cw3 = "1.0" diff --git a/packages/astroport_mocks/src/coin_registry.rs b/packages/astroport_mocks/src/coin_registry.rs index f35b1bc9c..c1d8d2b04 100644 --- a/packages/astroport_mocks/src/coin_registry.rs +++ b/packages/astroport_mocks/src/coin_registry.rs @@ -2,7 +2,9 @@ use std::fmt::Debug; use astroport::native_coin_registry::{ExecuteMsg, InstantiateMsg}; use cosmwasm_std::{Addr, Api, CustomQuery, Storage}; -use cw_multi_test::{Bank, ContractWrapper, Distribution, Executor, Gov, Ibc, Module, Staking}; +use cw_multi_test::{ + AppResponse, Bank, ContractWrapper, Distribution, Executor, Gov, Ibc, Module, Staking, +}; use schemars::JsonSchema; use serde::de::DeserializeOwned; @@ -93,3 +95,33 @@ pub struct MockCoinRegistry { pub app: WKApp, pub address: Addr, } + +impl MockCoinRegistry +where + B: Bank, + A: Api, + S: Storage, + C: Module, + X: Staking, + D: Distribution, + I: Ibc, + G: Gov, + C::ExecT: Clone + Debug + PartialEq + JsonSchema + DeserializeOwned + 'static, + C::QueryT: CustomQuery + DeserializeOwned + 'static, +{ + pub fn add(&self, coins: Vec<(String, u8)>) -> AppResponse { + let astroport = astroport_address(); + + self.app + .borrow_mut() + .execute_contract( + astroport, + self.address.clone(), + &ExecuteMsg::Add { + native_coins: coins, + }, + &[], + ) + .unwrap() + } +} diff --git a/packages/astroport_mocks/src/factory.rs b/packages/astroport_mocks/src/factory.rs index 528ce678d..0fd8a8583 100644 --- a/packages/astroport_mocks/src/factory.rs +++ b/packages/astroport_mocks/src/factory.rs @@ -1,3 +1,4 @@ +use anyhow::Result as AnyResult; use std::fmt::Debug; use astroport::{ @@ -7,7 +8,9 @@ use astroport::{ pair_concentrated::ConcentratedPoolParams, }; use cosmwasm_std::{to_binary, Addr, Api, CustomQuery, Decimal, Storage}; -use cw_multi_test::{Bank, ContractWrapper, Distribution, Executor, Gov, Ibc, Module, Staking}; +use cw_multi_test::{ + AppResponse, Bank, ContractWrapper, Distribution, Executor, Gov, Ibc, Module, Staking, +}; use schemars::JsonSchema; use serde::de::DeserializeOwned; @@ -62,6 +65,7 @@ where pub fn new(app: &WKApp) -> Self { Self { app: app.clone() } } + pub fn instantiate(self) -> MockFactory { let code_id = store_code(&self.app); let astroport = astroport_address(); @@ -288,6 +292,7 @@ where price_scale: Decimal::one(), ma_half_time: 600, track_asset_balances: None, + fee_share: None, }; self.app @@ -322,6 +327,19 @@ where } } + pub fn deregister_pair(&self, asset_infos: &[AssetInfo]) -> AnyResult { + let astroport = astroport_address(); + + self.app.borrow_mut().execute_contract( + astroport, + self.address.clone(), + &ExecuteMsg::Deregister { + asset_infos: asset_infos.to_vec(), + }, + &[], + ) + } + pub fn config(&self) -> ConfigResponse { let config: ConfigResponse = self .app diff --git a/packages/astroport_mocks/src/lib.rs b/packages/astroport_mocks/src/lib.rs index 6d6502d0c..0774c30a8 100644 --- a/packages/astroport_mocks/src/lib.rs +++ b/packages/astroport_mocks/src/lib.rs @@ -11,6 +11,7 @@ pub mod pair; pub mod pair_concentrated; pub mod pair_concentrated_inj; pub mod pair_stable; +pub mod shared_multisig; pub mod staking; pub mod token; pub mod vesting; diff --git a/packages/astroport_mocks/src/pair.rs b/packages/astroport_mocks/src/pair.rs index 1af4f663c..0226c2267 100644 --- a/packages/astroport_mocks/src/pair.rs +++ b/packages/astroport_mocks/src/pair.rs @@ -4,7 +4,7 @@ use astroport::{ asset::{Asset, AssetInfo, PairInfo}, pair::{ExecuteMsg, QueryMsg}, }; -use cosmwasm_std::{Addr, Api, Coin, CustomQuery, Decimal, Storage}; +use cosmwasm_std::{Addr, Api, Coin, CustomQuery, Decimal, StdResult, Storage}; use cw_multi_test::{Bank, ContractWrapper, Distribution, Executor, Gov, Ibc, Module, Staking}; use schemars::JsonSchema; use serde::de::DeserializeOwned; @@ -168,4 +168,11 @@ where } self.provide(sender, assets, None, true, None) } + + pub fn pair_info(&self) -> StdResult { + self.app + .borrow() + .wrap() + .query_wasm_smart(self.address.clone(), &QueryMsg::Pair {}) + } } diff --git a/packages/astroport_mocks/src/pair_concentrated.rs b/packages/astroport_mocks/src/pair_concentrated.rs index a6336c5f1..3e7935f6d 100644 --- a/packages/astroport_mocks/src/pair_concentrated.rs +++ b/packages/astroport_mocks/src/pair_concentrated.rs @@ -147,4 +147,15 @@ where }; xyk.mint_allow_provide_and_stake(sender, assets); } + + pub fn pair_info(&self) -> PairInfo { + let pair_info: PairInfo = self + .app + .borrow() + .wrap() + .query_wasm_smart(self.address.clone(), &QueryMsg::Pair {}) + .unwrap(); + + pair_info + } } diff --git a/packages/astroport_mocks/src/pair_concentrated_inj.rs b/packages/astroport_mocks/src/pair_concentrated_inj.rs index 695a274ec..6963adb8b 100644 --- a/packages/astroport_mocks/src/pair_concentrated_inj.rs +++ b/packages/astroport_mocks/src/pair_concentrated_inj.rs @@ -139,6 +139,7 @@ where price_scale: Decimal::one(), ma_half_time: 600, track_asset_balances: None, + fee_share: None, }, orderbook_config: OrderbookConfig { market_id, diff --git a/packages/astroport_mocks/src/shared_multisig.rs b/packages/astroport_mocks/src/shared_multisig.rs new file mode 100644 index 000000000..6a60ca3a0 --- /dev/null +++ b/packages/astroport_mocks/src/shared_multisig.rs @@ -0,0 +1,441 @@ +use anyhow::Result as AnyResult; +use cw_utils::Duration; +use std::fmt::Debug; + +use crate::{astroport_address, WKApp, ASTROPORT}; +use astroport::asset::{Asset, AssetInfo}; +use astroport::pair::ExecuteMsg as PairExecuteMsg; +use astroport::shared_multisig::{ + ConfigResponse, ExecuteMsg, InstantiateMsg, PoolType, ProvideParams, QueryMsg, +}; + +use cosmwasm_std::{Addr, Api, Coin, CosmosMsg, CustomQuery, Decimal, StdResult, Storage, Uint128}; +use cw20::{BalanceResponse, Cw20QueryMsg}; +use cw3::{ProposalResponse, Vote, VoteListResponse, VoteResponse}; +use cw_multi_test::{ + AppResponse, Bank, ContractWrapper, Distribution, Executor, Gov, Ibc, Module, Staking, +}; +use schemars::JsonSchema; +use serde::de::DeserializeOwned; + +pub fn store_code(app: &WKApp) -> u64 +where + B: Bank, + A: Api, + S: Storage, + C: Module, + X: Staking, + D: Distribution, + I: Ibc, + G: Gov, + C::ExecT: Clone + Debug + PartialEq + JsonSchema + DeserializeOwned + 'static, + C::QueryT: CustomQuery + DeserializeOwned + 'static, +{ + let contract = Box::new(ContractWrapper::new_with_empty( + astroport_shared_multisig::contract::execute, + astroport_shared_multisig::contract::instantiate, + astroport_shared_multisig::contract::query, + )); + + app.borrow_mut().store_code(contract) +} + +pub struct MockSharedMultisigBuilder { + pub app: WKApp, +} + +impl MockSharedMultisigBuilder +where + B: Bank, + A: Api, + S: Storage, + C: Module, + X: Staking, + D: Distribution, + I: Ibc, + G: Gov, + C::ExecT: Clone + Debug + PartialEq + JsonSchema + DeserializeOwned + 'static, + C::QueryT: CustomQuery + DeserializeOwned + 'static, +{ + pub fn new(app: &WKApp) -> Self { + Self { app: app.clone() } + } + + pub fn instantiate( + self, + factory_addr: &Addr, + generator_addr: Option, + target_pool: Option, + ) -> MockSharedMultisig { + let code_id = store_code(&self.app); + let astroport = astroport_address(); + + let address = self + .app + .borrow_mut() + .instantiate_contract( + code_id, + astroport, + &InstantiateMsg { + factory_addr: factory_addr.to_string(), + generator_addr: generator_addr + .unwrap_or(Addr::unchecked("generator_addr")) + .to_string(), + max_voting_period: Duration::Height(3), + manager1: "manager1".to_string(), + manager2: "manager2".to_string(), + denom1: "untrn".to_string(), + denom2: "ibc/astro".to_string(), + target_pool, + }, + &[], + "Astroport Shared Multisig", + Some(ASTROPORT.to_owned()), + ) + .unwrap(); + + MockSharedMultisig { + app: self.app, + address, + } + } +} + +pub struct MockSharedMultisig { + pub app: WKApp, + pub address: Addr, +} + +impl MockSharedMultisig +where + B: Bank, + A: Api, + S: Storage, + C: Module, + X: Staking, + D: Distribution, + I: Ibc, + G: Gov, + C::ExecT: Clone + Debug + PartialEq + JsonSchema + DeserializeOwned + 'static, + C::QueryT: CustomQuery + DeserializeOwned + 'static, +{ + pub fn propose(&self, sender: &Addr, msgs: Vec) -> AnyResult { + self.app.borrow_mut().execute_contract( + sender.clone(), + self.address.clone(), + &ExecuteMsg::Propose { + title: "Create a new proposal".to_string(), + description: "Create a new proposal".to_string(), + msgs, + latest: None, + }, + &[], + ) + } + + pub fn vote(&self, sender: &Addr, proposal_id: u64, vote: Vote) -> AnyResult { + self.app.borrow_mut().execute_contract( + sender.clone(), + self.address.clone(), + &ExecuteMsg::Vote { proposal_id, vote }, + &[], + ) + } + + pub fn execute(&self, sender: &Addr, proposal_id: u64) -> AnyResult { + self.app.borrow_mut().execute_contract( + sender.clone(), + self.address.clone(), + &ExecuteMsg::Execute { proposal_id }, + &[], + ) + } + + pub fn transfer( + &self, + sender: &Addr, + asset: Asset, + recipient: Option, + ) -> AnyResult { + self.app.borrow_mut().execute_contract( + sender.clone(), + self.address.clone(), + &ExecuteMsg::Transfer { asset, recipient }, + &[], + ) + } + + pub fn setup_max_voting_period( + &self, + sender: &Addr, + max_voting_period: Duration, + ) -> AnyResult { + self.app.borrow_mut().execute_contract( + sender.clone(), + self.address.clone(), + &ExecuteMsg::SetupMaxVotingPeriod { max_voting_period }, + &[], + ) + } + + pub fn start_rage_quit(&self, sender: &Addr) -> AnyResult { + self.app.borrow_mut().execute_contract( + sender.clone(), + self.address.clone(), + &ExecuteMsg::StartRageQuit {}, + &[], + ) + } + + pub fn complete_target_pool_migration(&self, sender: &Addr) -> AnyResult { + self.app.borrow_mut().execute_contract( + sender.clone(), + self.address.clone(), + &ExecuteMsg::CompleteTargetPoolMigration {}, + &[], + ) + } + + pub fn setup_pools( + &self, + sender: &Addr, + target_pool: Option, + migration_pool: Option, + ) -> AnyResult { + self.app.borrow_mut().execute_contract( + sender.clone(), + self.address.clone(), + &ExecuteMsg::SetupPools { + target_pool, + migration_pool, + }, + &[], + ) + } + + pub fn provide( + &self, + sender: &Addr, + pool_type: PoolType, + assets: Option>, + slippage_tolerance: Option, + auto_stake: Option, + receiver: Option, + ) -> AnyResult { + let assets = if let Some(assets) = assets { + assets + } else { + vec![ + Asset { + info: AssetInfo::NativeToken { + denom: "untrn".to_string(), + }, + amount: Uint128::new(100_000_000), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "ibc/astro".to_string(), + }, + amount: Uint128::new(100_000_000), + }, + ] + }; + + self.app.borrow_mut().execute_contract( + sender.clone(), + self.address.clone(), + &ExecuteMsg::ProvideLiquidity { + pool_type, + assets, + slippage_tolerance, + auto_stake, + receiver, + }, + &[], + ) + } + + pub fn withdraw( + &self, + sender: &Addr, + withdraw_amount: Option, + provide_params: Option, + ) -> AnyResult { + self.app.borrow_mut().execute_contract( + sender.clone(), + self.address.clone(), + &ExecuteMsg::WithdrawTargetPoolLP { + withdraw_amount, + provide_params, + }, + &[], + ) + } + + pub fn withdraw_ragequit( + &self, + sender: &Addr, + pool_type: PoolType, + withdraw_amount: Option, + ) -> AnyResult { + self.app.borrow_mut().execute_contract( + sender.clone(), + self.address.clone(), + &ExecuteMsg::WithdrawRageQuitLP { + pool_type, + withdraw_amount, + }, + &[], + ) + } + + pub fn deposit_generator( + &self, + sender: &Addr, + amount: Option, + ) -> AnyResult { + self.app.borrow_mut().execute_contract( + sender.clone(), + self.address.clone(), + &ExecuteMsg::DepositGenerator { amount }, + &[], + ) + } + + pub fn withdraw_generator( + &self, + sender: &Addr, + amount: Option, + ) -> AnyResult { + self.app.borrow_mut().execute_contract( + sender.clone(), + self.address.clone(), + &ExecuteMsg::WithdrawGenerator { amount }, + &[], + ) + } + + pub fn claim_generator_rewards(&self, sender: &Addr) -> AnyResult { + self.app.borrow_mut().execute_contract( + sender.clone(), + self.address.clone(), + &ExecuteMsg::ClaimGeneratorRewards {}, + &[], + ) + } + + #[allow(clippy::too_many_arguments)] + pub fn swap( + &self, + sender: &Addr, + pair: &Addr, + denom: &String, + amount: u64, + ask_asset_info: Option, + belief_price: Option, + max_spread: Option, + to: Option, + ) -> AnyResult { + let msg = PairExecuteMsg::Swap { + offer_asset: Asset { + info: AssetInfo::NativeToken { + denom: denom.clone(), + }, + amount: Uint128::from(amount), + }, + ask_asset_info, + belief_price, + max_spread, + to, + }; + + let send_funds = vec![Coin { + denom: denom.to_owned(), + amount: Uint128::from(amount), + }]; + + self.app + .borrow_mut() + .execute_contract(sender.clone(), pair.clone(), &msg, &send_funds) + } + + pub fn query_config(&self) -> StdResult { + self.app + .borrow() + .wrap() + .query_wasm_smart(self.address.clone(), &QueryMsg::Config {}) + } + + pub fn query_vote(&self, proposal_id: u64, voter: &Addr) -> StdResult { + self.app.borrow().wrap().query_wasm_smart( + self.address.clone(), + &QueryMsg::Vote { + proposal_id, + voter: voter.to_string(), + }, + ) + } + + pub fn query_votes(&self, proposal_id: u64) -> StdResult { + self.app + .borrow() + .wrap() + .query_wasm_smart(self.address.clone(), &QueryMsg::ListVotes { proposal_id }) + } + + pub fn query_proposal(&self, proposal_id: u64) -> StdResult { + self.app + .borrow() + .wrap() + .query_wasm_smart(self.address.clone(), &QueryMsg::Proposal { proposal_id }) + } + + pub fn query_native_balance(&self, account: Option<&str>, denom: &str) -> StdResult { + self.app + .borrow() + .wrap() + .query_balance(account.unwrap_or(self.address.as_str()), denom.to_owned()) + } + + pub fn query_cw20_balance( + &self, + lp_token: &Addr, + account: Option, + ) -> StdResult { + self.app + .borrow() + .wrap() + .query_wasm_smart::( + lp_token.as_str(), + &Cw20QueryMsg::Balance { + address: account.unwrap_or(self.address.clone()).to_string(), + }, + ) + } + + pub fn send_tokens( + &self, + owner: &Addr, + denoms: Option>, + recipient: Option, + ) -> AnyResult { + self.app.borrow_mut().send_tokens( + owner.clone(), + recipient.unwrap_or(self.address.clone()), + &denoms.unwrap_or(vec![ + Coin { + denom: String::from("untrn"), + amount: Uint128::new(900_000_000u128), + }, + Coin { + denom: String::from("ibc/astro"), + amount: Uint128::new(900_000_000u128), + }, + Coin { + denom: String::from("usdt"), + amount: Uint128::new(900_000_000u128), + }, + ]), + ) + } +} diff --git a/packages/astroport_pcl_common/Cargo.toml b/packages/astroport_pcl_common/Cargo.toml new file mode 100644 index 000000000..ade4d457f --- /dev/null +++ b/packages/astroport_pcl_common/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "astroport-pcl-common" +version = "1.0.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cosmwasm-std = "1" +cosmwasm-schema = "1" +cw-storage-plus = "1" +cw20 = "1" +thiserror = "1" +astroport = { path = "../astroport", version = "3" } +astroport-factory = { path = "../../contracts/factory", version = "1.6", features = ["library"] } +itertools = "0.11" + +[dev-dependencies] +anyhow = "1" diff --git a/contracts/pair_concentrated/src/consts.rs b/packages/astroport_pcl_common/src/consts.rs similarity index 100% rename from contracts/pair_concentrated/src/consts.rs rename to packages/astroport_pcl_common/src/consts.rs diff --git a/packages/astroport_pcl_common/src/error.rs b/packages/astroport_pcl_common/src/error.rs new file mode 100644 index 000000000..fcea9b470 --- /dev/null +++ b/packages/astroport_pcl_common/src/error.rs @@ -0,0 +1,43 @@ +use cosmwasm_std::{Decimal, StdError}; +use thiserror::Error; + +use crate::consts::MIN_AMP_CHANGING_TIME; + +/// This enum describes pair contract errors +#[derive(Error, Debug, PartialEq)] +pub enum PclError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0} parameter must be greater than {1} and less than or equal to {2}")] + IncorrectPoolParam(String, String, String), + + #[error( + "{0} error: The difference between the old and new amp or gamma values must not exceed {1} percent", + )] + MaxChangeAssertion(String, Decimal), + + #[error( + "Amp and gamma coefficients cannot be changed more often than once per {} seconds", + MIN_AMP_CHANGING_TIME + )] + MinChangingTimeAssertion {}, + + #[error("Doubling assets in asset infos")] + DoublingAssets {}, + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Generator address is not set in factory. Cannot auto-stake")] + AutoStakeError {}, + + #[error("Operation exceeds max spread limit")] + MaxSpreadAssertion {}, + + #[error("Provided spread amount exceeds allowed limit")] + AllowedSpreadAssertion {}, + + #[error("The asset {0} does not belong to the pair")] + InvalidAsset(String), +} diff --git a/packages/astroport_pcl_common/src/lib.rs b/packages/astroport_pcl_common/src/lib.rs new file mode 100644 index 000000000..36a5f38f4 --- /dev/null +++ b/packages/astroport_pcl_common/src/lib.rs @@ -0,0 +1,7 @@ +pub use math::*; + +pub mod consts; +pub mod error; +mod math; +pub mod state; +pub mod utils; diff --git a/contracts/pair_concentrated/src/math/math_decimal.rs b/packages/astroport_pcl_common/src/math/math_decimal.rs similarity index 98% rename from contracts/pair_concentrated/src/math/math_decimal.rs rename to packages/astroport_pcl_common/src/math/math_decimal.rs index 9e665547c..e778e8846 100644 --- a/contracts/pair_concentrated/src/math/math_decimal.rs +++ b/packages/astroport_pcl_common/src/math/math_decimal.rs @@ -1,10 +1,8 @@ use cosmwasm_std::{Decimal256, Fraction, StdError, StdResult, Uint128}; -use itertools::Itertools; - -use astroport::cosmwasm_ext::AbsDiff; use crate::consts::{HALFPOW_TOL, MAX_ITER, N, N_POW2, TOL}; use crate::math::signed_decimal::SignedDecimal256; +use itertools::Itertools; /// Internal constant to increase calculation accuracy. (1000.0) const PADDING: Decimal256 = Decimal256::raw(1000000000000000000000); @@ -138,7 +136,7 @@ pub fn half_float_pow(power: Decimal256) -> StdResult { let k = Decimal256::from_atomics(i, 0).unwrap(); let mut c = k - Decimal256::one(); - c = frac_pow.diff(c); + c = frac_pow.abs_diff(c); term = term * c * half / k; sum -= term; @@ -292,7 +290,10 @@ mod tests { fn test_calculations() { let gamma = 0.000145; - let x_range: Vec = (1000u128..=100_000).step_by(10000).into_iter().collect(); + let x_range = (1000u128..=100_000) + .step_by(10000) + .into_iter() + .collect_vec(); let mut a_range = (100u128..=10000u128).step_by(1000).collect_vec(); a_range.push(1); diff --git a/contracts/pair_concentrated/src/math/math_f64.rs b/packages/astroport_pcl_common/src/math/math_f64.rs similarity index 100% rename from contracts/pair_concentrated/src/math/math_f64.rs rename to packages/astroport_pcl_common/src/math/math_f64.rs diff --git a/contracts/pair_concentrated/src/math/mod.rs b/packages/astroport_pcl_common/src/math/mod.rs similarity index 100% rename from contracts/pair_concentrated/src/math/mod.rs rename to packages/astroport_pcl_common/src/math/mod.rs diff --git a/contracts/pair_concentrated/src/math/signed_decimal.rs b/packages/astroport_pcl_common/src/math/signed_decimal.rs similarity index 99% rename from contracts/pair_concentrated/src/math/signed_decimal.rs rename to packages/astroport_pcl_common/src/math/signed_decimal.rs index fab68ad1f..7ed1ddb4e 100644 --- a/contracts/pair_concentrated/src/math/signed_decimal.rs +++ b/packages/astroport_pcl_common/src/math/signed_decimal.rs @@ -1,4 +1,3 @@ -use astroport::cosmwasm_ext::AbsDiff; use cosmwasm_std::{Decimal256, StdError}; use std::fmt::{Display, Formatter}; use std::ops; @@ -34,7 +33,7 @@ impl SignedDecimal256 { } pub fn diff(self, other: SignedDecimal256) -> Decimal256 { if self.neg == other.neg { - self.val.diff(other.val) + self.val.abs_diff(other.val) } else { self.val + other.val } diff --git a/packages/astroport_pcl_common/src/state.rs b/packages/astroport_pcl_common/src/state.rs new file mode 100644 index 000000000..0417887ac --- /dev/null +++ b/packages/astroport_pcl_common/src/state.rs @@ -0,0 +1,843 @@ +use std::fmt::Display; + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{ + attr, Addr, Attribute, CustomQuery, Decimal, Decimal256, DepsMut, Env, Order, StdError, + StdResult, Storage, +}; +use cw_storage_plus::Map; + +use astroport::asset::{AssetInfo, PairInfo}; +use astroport::cosmwasm_ext::{AbsDiff, IntegerToDecimal}; +use astroport::pair::FeeShareConfig; +use astroport::pair_concentrated::{PromoteParams, UpdatePoolParams}; + +use crate::consts::{ + AMP_MAX, AMP_MIN, FEE_GAMMA_MAX, FEE_GAMMA_MIN, FEE_TOL, GAMMA_MAX, GAMMA_MIN, MAX_CHANGE, + MAX_FEE, MA_HALF_TIME_LIMITS, MIN_AMP_CHANGING_TIME, MIN_FEE, N_POW2, PRICE_SCALE_DELTA_MAX, + PRICE_SCALE_DELTA_MIN, REPEG_PROFIT_THRESHOLD_MAX, REPEG_PROFIT_THRESHOLD_MIN, TWO, +}; +use crate::error::PclError; +use crate::math::{calc_d, get_xcp, half_float_pow}; + +/// This structure stores the concentrated pair parameters. +#[cw_serde] +pub struct Config { + /// The pair information stored in a [`PairInfo`] struct + pub pair_info: PairInfo, + /// The factory contract address + pub factory_addr: Addr, + /// Pool parameters + pub pool_params: PoolParams, + /// Pool state + pub pool_state: PoolState, + /// Pool's owner + pub owner: Option, + /// Whether asset balances are tracked over blocks or not. + pub track_asset_balances: bool, + /// The config for swap fee sharing + pub fee_share: Option, +} + +/// This structure stores the pool parameters which may be adjusted via the `update_pool_params`. +#[cw_serde] +#[derive(Default)] +pub struct PoolParams { + /// The minimum fee, charged when pool is fully balanced + pub mid_fee: Decimal, + /// The maximum fee, charged when pool is imbalanced + pub out_fee: Decimal, + /// Parameter that defines how gradual the fee changes from fee_mid to fee_out based on + /// distance from price_scale + pub fee_gamma: Decimal, + /// Minimum profit before initiating a new repeg + pub repeg_profit_threshold: Decimal, + /// Minimum amount to change price_scale when repegging + pub min_price_scale_delta: Decimal, + /// Half-time used for calculating the price oracle + pub ma_half_time: u64, +} + +/// Validates input value against its limits. +fn validate_param(name: &str, val: T, min: T, max: T) -> Result<(), PclError> +where + T: PartialOrd + Display, +{ + if val >= min && val <= max { + Ok(()) + } else { + Err(PclError::IncorrectPoolParam( + name.to_string(), + min.to_string(), + max.to_string(), + )) + } +} + +impl PoolParams { + /// Intended to update current pool parameters. Performs validation of the new parameters. + /// Returns a vector of attributes with updated parameters. + /// + /// * `update_params` - an object which contains new pool parameters. Any of the parameters may be omitted. + pub fn update_params( + &mut self, + update_params: UpdatePoolParams, + ) -> Result, PclError> { + let mut attributes = vec![]; + if let Some(mid_fee) = update_params.mid_fee { + validate_param("mid_fee", mid_fee, MIN_FEE, MAX_FEE)?; + self.mid_fee = mid_fee; + attributes.push(attr("mid_fee", mid_fee.to_string())); + } + + if let Some(out_fee) = update_params.out_fee { + validate_param("out_fee", out_fee, MIN_FEE, MAX_FEE)?; + if out_fee <= self.mid_fee { + return Err(StdError::generic_err(format!( + "out_fee {out_fee} must be more than mid_fee {}", + self.mid_fee + )) + .into()); + } + self.out_fee = out_fee; + attributes.push(attr("out_fee", out_fee.to_string())); + } + + if let Some(fee_gamma) = update_params.fee_gamma { + validate_param("fee_gamma", fee_gamma, FEE_GAMMA_MIN, FEE_GAMMA_MAX)?; + self.fee_gamma = fee_gamma; + attributes.push(attr("fee_gamma", fee_gamma.to_string())); + } + + if let Some(repeg_profit_threshold) = update_params.repeg_profit_threshold { + validate_param( + "repeg_profit_threshold", + repeg_profit_threshold, + REPEG_PROFIT_THRESHOLD_MIN, + REPEG_PROFIT_THRESHOLD_MAX, + )?; + self.repeg_profit_threshold = repeg_profit_threshold; + attributes.push(attr( + "repeg_profit_threshold", + repeg_profit_threshold.to_string(), + )); + } + + if let Some(min_price_scale_delta) = update_params.min_price_scale_delta { + validate_param( + "min_price_scale_delta", + min_price_scale_delta, + PRICE_SCALE_DELTA_MIN, + PRICE_SCALE_DELTA_MAX, + )?; + self.min_price_scale_delta = min_price_scale_delta; + attributes.push(attr( + "min_price_scale_delta", + min_price_scale_delta.to_string(), + )); + } + + if let Some(ma_half_time) = update_params.ma_half_time { + validate_param( + "ma_half_time", + ma_half_time, + *MA_HALF_TIME_LIMITS.start(), + *MA_HALF_TIME_LIMITS.end(), + )?; + self.ma_half_time = ma_half_time; + attributes.push(attr("ma_half_time", ma_half_time.to_string())); + } + + Ok(attributes) + } + + pub fn fee(&self, xp: &[Decimal256]) -> Decimal256 { + let fee_gamma: Decimal256 = self.fee_gamma.into(); + let sum = xp[0] + xp[1]; + let mut k = xp[0] * xp[1] * N_POW2 / sum.pow(2); + k = fee_gamma / (fee_gamma + Decimal256::one() - k); + + if k <= FEE_TOL { + k = Decimal256::zero() + } + + k * Decimal256::from(self.mid_fee) + + (Decimal256::one() - k) * Decimal256::from(self.out_fee) + } +} + +/// Structure which stores Amp and Gamma. +#[cw_serde] +#[derive(Default, Copy)] +pub struct AmpGamma { + pub amp: Decimal, + pub gamma: Decimal, +} + +impl AmpGamma { + /// Validates the parameters and creates a new object of the [`AmpGamma`] structure. + pub fn new(amp: Decimal, gamma: Decimal) -> Result { + validate_param("amp", amp, AMP_MIN, AMP_MAX)?; + validate_param("gamma", gamma, GAMMA_MIN, GAMMA_MAX)?; + + Ok(AmpGamma { amp, gamma }) + } +} + +/// Internal structure which stores the price state. +/// This structure cannot be updated via update_config. +#[cw_serde] +#[derive(Default)] +pub struct PriceState { + /// Internal oracle price + pub oracle_price: Decimal256, + /// The last saved price + pub last_price: Decimal256, + /// Current price scale between 1st and 2nd assets. + /// I.e. such C that x = C * y where x - 1st asset, y - 2nd asset. + pub price_scale: Decimal256, + /// Last timestamp when the price_oracle was updated. + pub last_price_update: u64, + /// Keeps track of positive change in xcp due to fees accruing + pub xcp_profit: Decimal256, + /// Profits due to fees inclusive of realized losses from rebalancing + pub xcp_profit_real: Decimal256, +} + +/// Internal structure which stores the pool's state. +#[cw_serde] +pub struct PoolState { + /// Initial Amp and Gamma + pub initial: AmpGamma, + /// Future Amp and Gamma + pub future: AmpGamma, + /// Timestamp when Amp and Gamma should become equal to self.future + pub future_time: u64, + /// Timestamp when Amp and Gamma started being changed + pub initial_time: u64, + /// Current price state + pub price_state: PriceState, +} + +impl PoolState { + /// Validates Amp and Gamma promotion parameters. + /// Saves current values in self.initial and setups self.future. + /// If amp and gamma are being changed then current values will be used as initial values. + pub fn promote_params(&mut self, env: &Env, params: PromoteParams) -> Result<(), PclError> { + let block_time = env.block.time.seconds(); + + // Validate time interval + if block_time < self.initial_time + MIN_AMP_CHANGING_TIME + || params.future_time < block_time + MIN_AMP_CHANGING_TIME + { + return Err(PclError::MinChangingTimeAssertion {}); + } + + // Validate amp and gamma + let next_amp_gamma = AmpGamma::new(params.next_amp, params.next_gamma)?; + + // Calculate current amp and gamma + let cur_amp_gamma = self.get_amp_gamma(env); + + // Validate amp and gamma values are being changed by <= 10% + let one = Decimal::one(); + if (next_amp_gamma.amp / cur_amp_gamma.amp).diff(one) > MAX_CHANGE { + return Err(PclError::MaxChangeAssertion("Amp".to_string(), MAX_CHANGE)); + } + if (next_amp_gamma.gamma / cur_amp_gamma.gamma).diff(one) > MAX_CHANGE { + return Err(PclError::MaxChangeAssertion( + "Gamma".to_string(), + MAX_CHANGE, + )); + } + + self.initial = cur_amp_gamma; + self.initial_time = block_time; + + self.future = next_amp_gamma; + self.future_time = params.future_time; + + Ok(()) + } + + /// Stops amp and gamma promotion. Saves current values in self.future. + pub fn stop_promotion(&mut self, env: &Env) { + self.future = self.get_amp_gamma(env); + self.future_time = env.block.time.seconds(); + } + + /// Calculates current amp and gamma. + /// This function handles parameters upgrade as well as downgrade. + /// If block time >= self.future_time then it returns self.future parameters. + pub fn get_amp_gamma(&self, env: &Env) -> AmpGamma { + let block_time = env.block.time.seconds(); + if block_time < self.future_time { + let total = (self.future_time - self.initial_time).to_decimal(); + let passed = (block_time - self.initial_time).to_decimal(); + let left = total - passed; + + // A1 = A0 + (A1 - A0) * (block_time - t_init) / (t_end - t_init) -> simplified to: + // A1 = ( A0 * (t_end - block_time) + A1 * (block_time - t_init) ) / (t_end - t_init) + let amp = (self.initial.amp * left + self.future.amp * passed) / total; + let gamma = (self.initial.gamma * left + self.future.gamma * passed) / total; + + AmpGamma { amp, gamma } + } else { + AmpGamma { + amp: self.future.amp, + gamma: self.future.gamma, + } + } + } + + /// The function is responsible for repegging mechanism. + /// It updates internal oracle price and adjusts price scale. + /// + /// * **total_lp** total LP tokens were minted + /// * **cur_xs** - internal representation of pool volumes + /// * **cur_price** - last price happened in the previous action (swap, provide or withdraw) + pub fn update_price( + &mut self, + pool_params: &PoolParams, + env: &Env, + total_lp: Decimal256, + cur_xs: &[Decimal256], + cur_price: Decimal256, + ) -> StdResult<()> { + let amp_gamma = self.get_amp_gamma(env); + let block_time = env.block.time.seconds(); + let price_state = &mut self.price_state; + + if price_state.last_price_update < block_time { + let arg = Decimal256::from_ratio( + block_time - price_state.last_price_update, + pool_params.ma_half_time, + ); + let alpha = half_float_pow(arg)?; + price_state.oracle_price = price_state.last_price * (Decimal256::one() - alpha) + + price_state.oracle_price * alpha; + price_state.last_price_update = block_time; + } + price_state.last_price = cur_price; + + let cur_d = calc_d(cur_xs, &_gamma)?; + let xcp = get_xcp(cur_d, price_state.price_scale); + + if !price_state.xcp_profit_real.is_zero() { + let xcp_profit_real = xcp / total_lp; + + // If xcp dropped and no ramping happens then this swap makes loss + if xcp_profit_real < price_state.xcp_profit_real && block_time >= self.future_time { + return Err(StdError::generic_err( + "XCP profit real value dropped. This action makes loss", + )); + } + + price_state.xcp_profit = + price_state.xcp_profit * xcp_profit_real / price_state.xcp_profit_real; + price_state.xcp_profit_real = xcp_profit_real; + } + + let xcp_profit = price_state.xcp_profit; + + let norm = (price_state.oracle_price / price_state.price_scale).diff(Decimal256::one()); + let scale_delta = Decimal256::from(pool_params.min_price_scale_delta) + .max(norm * Decimal256::from_ratio(1u8, 10u8)); + + if norm >= scale_delta + && price_state.xcp_profit_real - Decimal256::one() + > (xcp_profit - Decimal256::one()) / TWO + + Decimal256::from(pool_params.repeg_profit_threshold) + { + let numerator = price_state.price_scale * (norm - scale_delta) + + scale_delta * price_state.oracle_price; + let price_scale_new = numerator / norm; + + let xs = [ + cur_xs[0], + cur_xs[1] * price_scale_new / price_state.price_scale, + ]; + let new_d = calc_d(&xs, &_gamma)?; + + let new_xcp = get_xcp(new_d, price_scale_new); + let new_xcp_profit_real = new_xcp / total_lp; + + if TWO * new_xcp_profit_real > xcp_profit + Decimal256::one() { + price_state.price_scale = price_scale_new; + price_state.xcp_profit_real = new_xcp_profit_real; + }; + } + + Ok(()) + } +} + +pub struct Precisions(Vec<(String, u8)>); + +impl<'a> Precisions { + /// Stores map of AssetInfo (as String) -> precision + const PRECISIONS: Map<'a, String, u8> = Map::new("precisions"); + pub fn new(storage: &dyn Storage) -> StdResult { + let items = Self::PRECISIONS + .range(storage, None, None, Order::Ascending) + .collect::>>()?; + + Ok(Self(items)) + } + + /// Store all token precisions + pub fn store_precisions( + deps: DepsMut, + asset_infos: &[AssetInfo], + factory_addr: &Addr, + ) -> StdResult<()> { + for asset_info in asset_infos { + let precision = asset_info.decimals(&deps.querier, factory_addr)?; + Self::PRECISIONS.save(deps.storage, asset_info.to_string(), &precision)?; + } + + Ok(()) + } + + pub fn get_precision(&self, asset_info: &AssetInfo) -> Result { + self.0 + .iter() + .find_map(|(info, prec)| { + if info == &asset_info.to_string() { + Some(*prec) + } else { + None + } + }) + .ok_or_else(|| PclError::InvalidAsset(asset_info.to_string())) + } +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use cosmwasm_std::testing::mock_env; + use cosmwasm_std::Timestamp; + + use crate::math::calc_y; + + use super::*; + + fn f64_to_dec(val: f64) -> Decimal { + Decimal::from_str(&val.to_string()).unwrap() + } + fn f64_to_dec256(val: f64) -> Decimal256 { + Decimal256::from_str(&val.to_string()).unwrap() + } + fn dec_to_f64(val: Decimal256) -> f64 { + f64::from_str(&val.to_string()).unwrap() + } + + #[test] + #[should_panic(expected = "attempt to subtract with overflow")] + fn test_validator_odd_behaviour() { + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(86400); + + let mut state = PoolState { + initial: AmpGamma { + amp: Decimal::zero(), + gamma: Decimal::zero(), + }, + future: AmpGamma { + amp: f64_to_dec(100_f64), + gamma: f64_to_dec(0.0000001_f64), + }, + future_time: 0, + initial_time: 0, + price_state: Default::default(), + }; + + // Increase values + let promote_params = PromoteParams { + next_amp: f64_to_dec(110_f64), + next_gamma: f64_to_dec(0.00000011_f64), + future_time: env.block.time.seconds() + 100_000, + }; + state.promote_params(&env, promote_params).unwrap(); + + let AmpGamma { amp, gamma } = state.get_amp_gamma(&env); + assert_eq!(amp, f64_to_dec(100_f64)); + assert_eq!(gamma, f64_to_dec(0.0000001_f64)); + + // Simulating validator odd behavior + env.block.time = env.block.time.minus_seconds(1000); + state.get_amp_gamma(&env); + } + + #[test] + fn test_pool_state() { + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(86400); + + let mut state = PoolState { + initial: AmpGamma { + amp: Decimal::zero(), + gamma: Decimal::zero(), + }, + future: AmpGamma { + amp: f64_to_dec(100_f64), + gamma: f64_to_dec(0.0000001_f64), + }, + future_time: 0, + initial_time: 0, + price_state: Default::default(), + }; + + // Trying to promote params with future time in the past + let promote_params = PromoteParams { + next_amp: f64_to_dec(110_f64), + next_gamma: f64_to_dec(0.00000011_f64), + future_time: env.block.time.seconds() - 10000, + }; + let err = state.promote_params(&env, promote_params).unwrap_err(); + assert_eq!(err, PclError::MinChangingTimeAssertion {}); + + // Increase values + let promote_params = PromoteParams { + next_amp: f64_to_dec(110_f64), + next_gamma: f64_to_dec(0.00000011_f64), + future_time: env.block.time.seconds() + 100_000, + }; + state.promote_params(&env, promote_params).unwrap(); + + let AmpGamma { amp, gamma } = state.get_amp_gamma(&env); + assert_eq!(amp, f64_to_dec(100_f64)); + assert_eq!(gamma, f64_to_dec(0.0000001_f64)); + + env.block.time = env.block.time.plus_seconds(50_000); + + let AmpGamma { amp, gamma } = state.get_amp_gamma(&env); + assert_eq!(amp, f64_to_dec(105_f64)); + assert_eq!(gamma, f64_to_dec(0.000000105_f64)); + + env.block.time = env.block.time.plus_seconds(100_001); + let AmpGamma { amp, gamma } = state.get_amp_gamma(&env); + assert_eq!(amp, f64_to_dec(110_f64)); + assert_eq!(gamma, f64_to_dec(0.00000011_f64)); + + // Decrease values + let promote_params = PromoteParams { + next_amp: f64_to_dec(108_f64), + next_gamma: f64_to_dec(0.000000106_f64), + future_time: env.block.time.seconds() + 100_000, + }; + state.promote_params(&env, promote_params).unwrap(); + + env.block.time = env.block.time.plus_seconds(50_000); + let AmpGamma { amp, gamma } = state.get_amp_gamma(&env); + assert_eq!(amp, f64_to_dec(109_f64)); + assert_eq!(gamma, f64_to_dec(0.000000108_f64)); + + env.block.time = env.block.time.plus_seconds(50_001); + let AmpGamma { amp, gamma } = state.get_amp_gamma(&env); + assert_eq!(amp, f64_to_dec(108_f64)); + assert_eq!(gamma, f64_to_dec(0.000000106_f64)); + + // Increase amp only + let promote_params = PromoteParams { + next_amp: f64_to_dec(118_f64), + next_gamma: f64_to_dec(0.000000106_f64), + future_time: env.block.time.seconds() + 100_000, + }; + state.promote_params(&env, promote_params).unwrap(); + + env.block.time = env.block.time.plus_seconds(50_000); + let AmpGamma { amp, gamma } = state.get_amp_gamma(&env); + assert_eq!(amp, f64_to_dec(113_f64)); + assert_eq!(gamma, f64_to_dec(0.000000106_f64)); + + env.block.time = env.block.time.plus_seconds(50_001); + let AmpGamma { amp, gamma } = state.get_amp_gamma(&env); + assert_eq!(amp, f64_to_dec(118_f64)); + assert_eq!(gamma, f64_to_dec(0.000000106_f64)); + } + + #[test] + fn check_fee_update() { + let mid_fee = 0.25f64; + let out_fee = 0.46f64; + let fee_gamma = 0.0002f64; + + let params = PoolParams { + mid_fee: f64_to_dec(mid_fee), + out_fee: f64_to_dec(out_fee), + fee_gamma: f64_to_dec(fee_gamma), + repeg_profit_threshold: Default::default(), + min_price_scale_delta: Default::default(), + ma_half_time: 0, + }; + + let xp = vec![f64_to_dec256(1_000_000f64), f64_to_dec256(1_000_000f64)]; + let result = params.fee(&xp); + assert_eq!(dec_to_f64(result), mid_fee); + + let xp = vec![f64_to_dec256(990_000f64), f64_to_dec256(1_000_000f64)]; + let result = params.fee(&xp); + assert_eq!(dec_to_f64(result), 0.2735420730476899); + + let xp = vec![f64_to_dec256(100_000f64), f64_to_dec256(1_000_000_f64)]; + let result = params.fee(&xp); + assert_eq!(dec_to_f64(result), out_fee); + } + + /// (cur_d, total_lp, new_price) + fn swap( + ext_xs: &mut [Decimal256], + offer_amount: Decimal256, + price_scale: Decimal256, + ask_ind: usize, + amp_gamma: &AmpGamma, + pool_params: &PoolParams, + ) -> Decimal256 { + let offer_ind = 1 - ask_ind; + + let mut xs = ext_xs.to_vec(); + println!("Before swap: {} {}", xs[0], xs[1]); + + // internal repr + xs[1] *= price_scale; + println!("Before swap (internal): {} {}", xs[0], xs[1]); + + let cur_d = calc_d(&xs, amp_gamma).unwrap(); + + let mut offer_amount_internal = offer_amount; + // internal repr + if offer_ind == 1 { + offer_amount_internal *= price_scale; + } + + xs[offer_ind] += offer_amount_internal; + let mut ask_amount = xs[ask_ind] - calc_y(&xs, cur_d, amp_gamma, ask_ind).unwrap(); + xs[ask_ind] -= ask_amount; + let fee = ask_amount * pool_params.fee(&xs); + println!("fee {fee} ({}%)", pool_params.fee(&xs)); + xs[ask_ind] += fee; + ask_amount -= fee; + + println!( + "Internal Swap {} x[{}] for {} x[{}] by {} price", + offer_amount_internal, + offer_ind, + ask_amount, + ask_ind, + ask_amount / offer_amount_internal + ); + + // external repr + let new_price = if ask_ind == 1 { + ask_amount /= price_scale; + offer_amount / ask_amount + } else { + ask_amount / offer_amount + }; + + println!( + "Swap {} x[{}] for {} x[{}] by {new_price} price", + offer_amount, offer_ind, ask_amount, ask_ind + ); + + ext_xs[offer_ind] += offer_amount; + ext_xs[ask_ind] -= ask_amount; + + let ext_d = calc_d(ext_xs, amp_gamma).unwrap(); + let cur_d = calc_d(&xs, amp_gamma).unwrap(); + + println!("Internal: d {cur_d}",); + println!("External: d {ext_d}",); + + println!("After swap: {} {}", ext_xs[0], ext_xs[1]); + println!( + "After swap (internal): {} {}", + ext_xs[0], + ext_xs[1] * price_scale + ); + + new_price + } + + fn to_future(env: &mut Env, by_secs: u64) { + env.block.time = env.block.time.plus_seconds(by_secs) + } + + fn to_internal_repr(xs: &[Decimal256], price_scale: Decimal256) -> Vec { + vec![xs[0], xs[1] * price_scale] + } + + #[test] + fn check_repeg() { + let (amp, gamma) = (40f64, 0.000145); + let amp_gamma = AmpGamma { + amp: f64_to_dec(amp), + gamma: f64_to_dec(gamma), + }; + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(0); + + let pool_params = PoolParams { + mid_fee: f64_to_dec(0.0026), + out_fee: f64_to_dec(0.0045), + fee_gamma: f64_to_dec(0.00023), + repeg_profit_threshold: f64_to_dec(0.000002), + min_price_scale_delta: f64_to_dec(0.000146), + ma_half_time: 600, + }; + + let mut pool_state = PoolState { + initial: AmpGamma::default(), + future: amp_gamma, + future_time: 0, + initial_time: 0, + price_state: PriceState { + oracle_price: f64_to_dec256(2f64), + last_price: f64_to_dec256(2f64), + price_scale: f64_to_dec256(2f64), + last_price_update: env.block.time.seconds(), + xcp_profit: Decimal256::one(), + xcp_profit_real: Decimal256::one(), + }, + }; + + to_future(&mut env, 1); + + // external repr + let mut ext_xs = [f64_to_dec256(1_000_000f64), f64_to_dec256(500_000f64)]; + let mut xs = ext_xs.to_vec(); + xs[1] *= pool_state.price_state.price_scale; + let cur_d = calc_d(&xs, &_gamma).unwrap(); + let total_lp = get_xcp(cur_d, pool_state.price_state.price_scale); + + let offer_amount = f64_to_dec256(1000_f64); + let price = swap( + &mut ext_xs, + offer_amount, + pool_state.price_state.price_scale, + 0, + &_gamma, + &pool_params, + ); + pool_state + .update_price( + &pool_params, + &env, + total_lp, + &to_internal_repr(&ext_xs, pool_state.price_state.price_scale), + price, + ) + .unwrap(); + + to_future(&mut env, 600); + + let offer_amount = f64_to_dec256(10000_f64); + let price = swap( + &mut ext_xs, + offer_amount, + pool_state.price_state.price_scale, + 0, + &_gamma, + &pool_params, + ); + pool_state + .update_price( + &pool_params, + &env, + total_lp, + &to_internal_repr(&ext_xs, pool_state.price_state.price_scale), + price, + ) + .unwrap(); + + to_future(&mut env, 600); + + let offer_amount = f64_to_dec256(200_000_f64); + let price = swap( + &mut ext_xs, + offer_amount, + pool_state.price_state.price_scale, + 0, + &_gamma, + &pool_params, + ); + pool_state + .update_price( + &pool_params, + &env, + total_lp, + &to_internal_repr(&ext_xs, pool_state.price_state.price_scale), + price, + ) + .unwrap(); + + to_future(&mut env, 12000); + + let offer_amount = f64_to_dec256(1_000_f64); + let price = swap( + &mut ext_xs, + offer_amount, + pool_state.price_state.price_scale, + 0, + &_gamma, + &pool_params, + ); + + pool_state + .update_price( + &pool_params, + &env, + total_lp, + &to_internal_repr(&ext_xs, pool_state.price_state.price_scale), + price, + ) + .unwrap(); + + to_future(&mut env, 600); + + let offer_amount = f64_to_dec256(200_000_f64); + let price = swap( + &mut ext_xs, + offer_amount, + pool_state.price_state.price_scale, + 1, + &_gamma, + &pool_params, + ); + + pool_state + .update_price( + &pool_params, + &env, + total_lp, + &to_internal_repr(&ext_xs, pool_state.price_state.price_scale), + price, + ) + .unwrap(); + + to_future(&mut env, 60); + + let offer_amount = f64_to_dec256(2_000_f64); + let price = swap( + &mut ext_xs, + offer_amount, + pool_state.price_state.price_scale, + 1, + &_gamma, + &pool_params, + ); + + pool_state + .update_price( + &pool_params, + &env, + total_lp, + &to_internal_repr(&ext_xs, pool_state.price_state.price_scale), + price, + ) + .unwrap(); + } +} diff --git a/packages/astroport_pcl_common/src/utils.rs b/packages/astroport_pcl_common/src/utils.rs new file mode 100644 index 000000000..4ef08c77a --- /dev/null +++ b/packages/astroport_pcl_common/src/utils.rs @@ -0,0 +1,470 @@ +use cosmwasm_std::{ + to_binary, wasm_execute, Addr, Api, CosmosMsg, CustomMsg, CustomQuery, Decimal, Decimal256, + Env, Fraction, QuerierWrapper, StdError, StdResult, Uint128, Uint256, +}; +use cw20::Cw20ExecuteMsg; +use itertools::Itertools; + +use astroport::asset::{Asset, AssetInfo, DecimalAsset}; +use astroport::cosmwasm_ext::AbsDiff; +use astroport::querier::query_factory_config; +use astroport_factory::state::pair_key; + +use crate::consts::{DEFAULT_SLIPPAGE, MAX_ALLOWED_SLIPPAGE, N, OFFER_PERCENT, TWO}; +use crate::error::PclError; +use crate::state::{Config, PoolParams, PriceState}; +use crate::{calc_d, calc_y}; + +/// Helper function to check the given asset infos are valid. +pub fn check_asset_infos(api: &dyn Api, asset_infos: &[AssetInfo]) -> Result<(), PclError> { + if !asset_infos.iter().all_unique() { + return Err(PclError::DoublingAssets {}); + } + + asset_infos + .iter() + .try_for_each(|asset_info| asset_info.check(api)) + .map_err(Into::into) +} + +/// Helper function to check that the assets in a given array are valid. +pub fn check_assets(api: &dyn Api, assets: &[Asset]) -> Result<(), PclError> { + let asset_infos = assets.iter().map(|asset| asset.info.clone()).collect_vec(); + check_asset_infos(api, &asset_infos) +} + +/// Checks that cw20 token is part of the pool. +/// +/// * **cw20_sender** is cw20 token address which is being checked. +pub fn check_cw20_in_pool(config: &Config, cw20_sender: &Addr) -> Result<(), PclError> { + for asset_info in &config.pair_info.asset_infos { + match asset_info { + AssetInfo::Token { contract_addr } if contract_addr == cw20_sender => return Ok(()), + _ => {} + } + } + + Err(PclError::Unauthorized {}) +} + +/// Mint LP tokens for a beneficiary and auto stake the tokens in the Generator contract (if auto staking is specified). +/// +/// * **recipient** LP token recipient. +/// +/// * **amount** amount of LP tokens that will be minted for the recipient. +/// +/// * **auto_stake** determines whether the newly minted LP tokens will +/// be automatically staked in the Generator on behalf of the recipient. +pub fn mint_liquidity_token_message( + querier: QuerierWrapper, + config: &Config, + contract_address: &Addr, + recipient: &Addr, + amount: Uint128, + auto_stake: bool, +) -> Result>, PclError> +where + C: CustomQuery, + T: CustomMsg, +{ + let lp_token = &config.pair_info.liquidity_token; + + // If no auto-stake - just mint to recipient + if !auto_stake { + return Ok(vec![wasm_execute( + lp_token, + &Cw20ExecuteMsg::Mint { + recipient: recipient.to_string(), + amount, + }, + vec![], + )? + .into()]); + } + + // Mint for the pair contract and stake into the Generator contract + let generator = query_factory_config(&querier, &config.factory_addr)?.generator_address; + + if let Some(generator) = generator { + Ok(vec![ + wasm_execute( + lp_token, + &Cw20ExecuteMsg::Mint { + recipient: contract_address.to_string(), + amount, + }, + vec![], + )? + .into(), + wasm_execute( + lp_token, + &Cw20ExecuteMsg::Send { + contract: generator.to_string(), + amount, + msg: to_binary(&astroport::generator::Cw20HookMsg::DepositFor( + recipient.to_string(), + ))?, + }, + vec![], + )? + .into(), + ]) + } else { + Err(PclError::AutoStakeError {}) + } +} + +/// Return the amount of tokens that a specific amount of LP tokens would withdraw. +/// +/// * **pools** assets available in the pool. +/// +/// * **amount** amount of LP tokens to calculate underlying amounts for. +/// +/// * **total_share** total amount of LP tokens currently issued by the pool. +pub fn get_share_in_assets( + pools: &[DecimalAsset], + amount: Uint128, + total_share: Uint128, +) -> Vec { + let share_ratio = if !total_share.is_zero() { + Decimal256::from_ratio(amount, total_share) + } else { + Decimal256::zero() + }; + + pools + .iter() + .map(|pool| DecimalAsset { + info: pool.info.clone(), + amount: pool.amount * share_ratio, + }) + .collect() +} + +/// If `belief_price` and `max_spread` are both specified, we compute a new spread, +/// otherwise we just use the swap spread to check `max_spread`. +/// +/// * **belief_price** belief price used in the swap. +/// +/// * **max_spread** max spread allowed so that the swap can be executed successfuly. +/// +/// * **offer_amount** amount of assets to swap. +/// +/// * **return_amount** amount of assets a user wants to receive from the swap. +/// +/// * **spread_amount** spread used in the swap. +pub fn assert_max_spread( + belief_price: Option, + max_spread: Option, + offer_amount: Uint128, + return_amount: Uint128, + spread_amount: Uint128, +) -> Result<(), PclError> { + let max_spread = max_spread.map(Decimal256::from).unwrap_or(DEFAULT_SLIPPAGE); + if max_spread > MAX_ALLOWED_SLIPPAGE { + return Err(PclError::AllowedSpreadAssertion {}); + } + + if let Some(belief_price) = belief_price { + let expected_return = offer_amount + * belief_price.inv().ok_or_else(|| { + StdError::generic_err("Invalid belief_price. Check the input values.") + })?; + + let spread_amount = expected_return.saturating_sub(return_amount); + + if return_amount < expected_return + && Decimal256::from_ratio(spread_amount, expected_return) > max_spread + { + return Err(PclError::MaxSpreadAssertion {}); + } + } else if Decimal256::from_ratio(spread_amount, return_amount + spread_amount) > max_spread { + return Err(PclError::MaxSpreadAssertion {}); + } + + Ok(()) +} + +/// Checks whether it possible to make a swap or not. +pub fn before_swap_check(pools: &[DecimalAsset], offer_amount: Decimal256) -> StdResult<()> { + if offer_amount.is_zero() { + return Err(StdError::generic_err("Swap amount must not be zero")); + } + if pools.iter().any(|a| a.amount.is_zero()) { + return Err(StdError::generic_err("One of the pools is empty")); + } + + Ok(()) +} + +/// This structure is for internal use only. Represents swap's result. +pub struct SwapResult { + pub dy: Decimal256, + pub spread_fee: Decimal256, + pub maker_fee: Decimal256, + pub share_fee: Decimal256, + pub total_fee: Decimal256, +} + +impl SwapResult { + /// Calculates **last price** and **last real price**. + /// Returns (last_price, last_real_price) where: + /// - last_price is a price for repeg algo, + pub fn calc_last_price(&self, offer_amount: Decimal256, offer_ind: usize) -> Decimal256 { + if offer_ind == 0 { + offer_amount / (self.dy + self.maker_fee) + } else { + (self.dy + self.maker_fee) / offer_amount + } + } +} + +/// Performs swap simulation to calculate a price. +pub fn calc_last_prices(xs: &[Decimal256], config: &Config, env: &Env) -> StdResult { + let mut offer_amount = Decimal256::one().min(xs[0] * OFFER_PERCENT); + if offer_amount.is_zero() { + offer_amount = Decimal256::raw(1u128); + } + + let last_price = compute_swap( + xs, + offer_amount, + 1, + config, + env, + Decimal256::zero(), + Decimal256::zero(), + )? + .calc_last_price(offer_amount, 0); + + Ok(last_price) +} + +/// Calculate swap result. +pub fn compute_swap( + xs: &[Decimal256], + offer_amount: Decimal256, + ask_ind: usize, + config: &Config, + env: &Env, + maker_fee_share: Decimal256, + share_fee_share: Decimal256, +) -> StdResult { + let offer_ind = 1 ^ ask_ind; + + let mut ixs = xs.to_vec(); + ixs[1] *= config.pool_state.price_state.price_scale; + + let amp_gamma = config.pool_state.get_amp_gamma(env); + let d = calc_d(&ixs, &_gamma)?; + + let offer_amount = if offer_ind == 1 { + offer_amount * config.pool_state.price_state.price_scale + } else { + offer_amount + }; + + ixs[offer_ind] += offer_amount; + + let new_y = calc_y(&ixs, d, &_gamma, ask_ind)?; + let mut dy = ixs[ask_ind] - new_y; + ixs[ask_ind] = new_y; + + let price = if ask_ind == 1 { + dy /= config.pool_state.price_state.price_scale; + config.pool_state.price_state.price_scale.inv().unwrap() + } else { + config.pool_state.price_state.price_scale + }; + + // Since price_scale moves slower than real price spread fee may become negative + let spread_fee = (offer_amount * price).saturating_sub(dy); + + let fee_rate = config.pool_params.fee(&ixs); + let total_fee = fee_rate * dy; + dy -= total_fee; + + let share_fee = total_fee * share_fee_share; + + Ok(SwapResult { + dy, + spread_fee, + maker_fee: (total_fee - share_fee) * maker_fee_share, + share_fee, + total_fee, + }) +} + +/// Returns an amount of offer assets for a specified amount of ask assets. +pub fn compute_offer_amount( + xs: &[Decimal256], + mut want_amount: Decimal256, + ask_ind: usize, + config: &Config, + env: &Env, +) -> StdResult<(Decimal256, Decimal256, Decimal256)> { + let offer_ind = 1 ^ ask_ind; + + if ask_ind == 1 { + want_amount *= config.pool_state.price_state.price_scale + } + + let mut ixs = xs.to_vec(); + ixs[1] *= config.pool_state.price_state.price_scale; + + let amp_gamma = config.pool_state.get_amp_gamma(env); + let d = calc_d(&ixs, &_gamma)?; + + // It's hard to predict fee rate thus we use maximum possible fee rate + let before_fee = want_amount + * (Decimal256::one() - Decimal256::from(config.pool_params.out_fee)) + .inv() + .unwrap(); + let mut fee = before_fee - want_amount; + + ixs[ask_ind] -= before_fee; + + let new_y = calc_y(&ixs, d, &_gamma, offer_ind)?; + let mut dy = new_y - ixs[offer_ind]; + + let mut spread_fee = dy.saturating_sub(before_fee); + if offer_ind == 1 { + dy /= config.pool_state.price_state.price_scale; + spread_fee /= config.pool_state.price_state.price_scale; + fee /= config.pool_state.price_state.price_scale; + } + + Ok((dy, spread_fee, fee)) +} + +/// Calculate provide fee applied on the amount of LP tokens. Only charged for imbalanced provide. +/// * `deposits` - internal repr of deposit +/// * `xp` - internal repr of pools +pub fn calc_provide_fee( + deposits: &[Decimal256], + xp: &[Decimal256], + params: &PoolParams, +) -> Decimal256 { + let sum = deposits[0] + deposits[1]; + let avg = sum / N; + + deposits[0].diff(avg) * params.fee(xp) / sum +} + +/// This is an internal function that enforces slippage tolerance for provides. Returns actual slippage. +pub fn assert_slippage_tolerance( + deposits: &[Decimal256], + actual_share: Decimal256, + price_state: &PriceState, + slippage_tolerance: Option, +) -> Result { + let slippage_tolerance = slippage_tolerance + .map(Into::into) + .unwrap_or(DEFAULT_SLIPPAGE); + if slippage_tolerance > MAX_ALLOWED_SLIPPAGE { + return Err(PclError::AllowedSpreadAssertion {}); + } + + let deposit_value = deposits[0] + deposits[1] * price_state.price_scale; + let lp_expected = (deposit_value / TWO * deposit_value / (TWO * price_state.price_scale)) + .sqrt() + / price_state.xcp_profit_real; + let slippage = lp_expected.saturating_sub(actual_share) / lp_expected; + + if slippage > slippage_tolerance { + return Err(PclError::MaxSpreadAssertion {}); + } + + Ok(slippage) +} + +/// Checks whether the pair is registered in the factory or not. +pub fn check_pair_registered( + querier: QuerierWrapper, + factory: &Addr, + asset_infos: &[AssetInfo], +) -> StdResult +where + C: CustomQuery, +{ + astroport_factory::state::PAIRS + .query(&querier, factory.clone(), &pair_key(asset_infos)) + .map(|inner| inner.is_some()) +} + +/// Internal function to calculate new moving average using Uint256. +/// Overflow is possible only if new average order size is greater than 2^128 - 1 which is unlikely. +pub fn safe_sma_calculation( + sma: Uint128, + oldest_amount: Uint128, + count: u32, + new_amount: Uint128, +) -> StdResult { + let res = (sma.full_mul(count) + Uint256::from(new_amount) - Uint256::from(oldest_amount)) + .checked_div(count.into())?; + res.try_into().map_err(StdError::from) +} + +/// Same as [`safe_sma_calculation`] but is being used when buffer is not full yet. +pub fn safe_sma_buffer_not_full( + sma: Uint128, + count: u32, + new_amount: Uint128, +) -> StdResult { + let res = (sma.full_mul(count) + Uint256::from(new_amount)).checked_div((count + 1).into())?; + res.try_into().map_err(StdError::from) +} + +#[cfg(test)] +mod tests { + use std::error::Error; + use std::fmt::Display; + use std::str::FromStr; + + use crate::state::PoolParams; + + use super::*; + + pub fn f64_to_dec(val: f64) -> T + where + T: FromStr, + T::Err: Error, + { + T::from_str(&val.to_string()).unwrap() + } + + pub fn dec_to_f64(val: impl Display) -> f64 { + f64::from_str(&val.to_string()).unwrap() + } + + #[test] + fn test_provide_fees() { + let params = PoolParams { + mid_fee: f64_to_dec(0.0026), + out_fee: f64_to_dec(0.0045), + fee_gamma: f64_to_dec(0.00023), + ..PoolParams::default() + }; + + let fee_rate = calc_provide_fee( + &[f64_to_dec(50_000f64), f64_to_dec(50_000f64)], + &[f64_to_dec(100_000f64), f64_to_dec(100_000f64)], + ¶ms, + ); + assert_eq!(dec_to_f64(fee_rate), 0.0); + + let fee_rate = calc_provide_fee( + &[f64_to_dec(99_000f64), f64_to_dec(1_000f64)], + &[f64_to_dec(100_000f64), f64_to_dec(100_000f64)], + ¶ms, + ); + assert_eq!(dec_to_f64(fee_rate), 0.001274); + + let fee_rate = calc_provide_fee( + &[f64_to_dec(99_000f64), f64_to_dec(1_000f64)], + &[f64_to_dec(1_000f64), f64_to_dec(99_000f64)], + ¶ms, + ); + assert_eq!(dec_to_f64(fee_rate), 0.002205); + } +} diff --git a/scripts/publish_crates.sh b/scripts/publish_crates.sh new file mode 100755 index 000000000..6c560d9cd --- /dev/null +++ b/scripts/publish_crates.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash + +set -eu +set -o pipefail + +declare CONTRACTS +declare ROOT_DIR +declare FIRST_CRATES +declare SKIP_CRATES +declare DRY_FLAGS + +# NOTE: astroport-governance and astro-satellite-package should be published first + +if [ -z "${1:-}" ]; then + echo "Usage: $0 [optional: --publish]" + echo "If flag --publish is not set, only dry-run will be performed." + echo "NOTE: astroport-governance and astro-satellite-package should be published first." + exit 1 +fi + +DRY_FLAGS="--dry-run --allow-dirty" +if [ -z "${2:-}" ]; then + echo "Dry run mode" +else + echo "Publishing mode" + DRY_FLAGS="" +fi + +publish() { + local cargo_error temp_err_file ret_code=0 + local crate="$1" + + echo "Publishing $crate ..." + + set +e + + # Run 'cargo publish' and redirect stderr to a temporary file + temp_err_file="/tmp/cargo-publish-error-$crate.$$" + # shellcheck disable=SC2086 + cargo publish -p "$crate" --locked $DRY_FLAGS 2> >(tee "$temp_err_file") + ret_code=$? + cargo_error="$(<"$temp_err_file")" + rm "$temp_err_file" + + set -e + + # Sleep for 60 seconds if the crate was published successfully + [ $ret_code -eq 0 ] && [ -z "$DRY_FLAGS" ] && sleep 60 + + # Check if the error is related to the crate version already being uploaded + if [[ $cargo_error =~ "the remote server responded with an error: crate version" && $cargo_error =~ "is already uploaded" ]]; then + ret_code=0 + fi + + # Skip if the error is related to the crate version not being found in the registry and + # the script is running in dry-run mode + if [[ $cargo_error =~ "no matching package named" || $cargo_error =~ "failed to select a version for the requirement" ]] && \ + [[ -n "$DRY_FLAGS" ]]; then + ret_code=0 + fi + + # Return the original exit code from 'cargo publish' + return $ret_code +} + +ROOT_DIR="$(realpath "$1")" + +FIRST_CRATES="astroport-circular-buffer astroport astroport-factory" +SKIP_CRATES="astroport-pair-astro-xastro" + +for contract in $FIRST_CRATES; do + publish "$contract" +done + +CONTRACTS="$(cargo metadata --no-deps --locked --manifest-path "$ROOT_DIR/Cargo.toml" --format-version 1 | + jq -r --arg contracts "$ROOT_DIR/contracts" \ + '.packages[] + | select(.manifest_path | startswith($contracts)) + | .name')" + +echo -e "Publishing crates:\n$CONTRACTS" + +for contract in $CONTRACTS; do + if [[ "$FIRST_CRATES $SKIP_CRATES" == *"$contract"* ]]; then + continue + fi + + publish "$contract" +done + +echo "ALL DONE"