diff --git a/.cargo/audit.toml b/.cargo/audit.toml new file mode 100644 index 000000000..65c70990d --- /dev/null +++ b/.cargo/audit.toml @@ -0,0 +1,6 @@ +# Reference: https://github.com/rustsec/rustsec/blob/main/cargo-audit/audit.toml.example + +[advisories] +# Ignore the following advisory IDs. +# RUSTSEC-2022-0093 is reported for test-tube which is only used for testing. +ignore = ["RUSTSEC-2022-0093", "RUSTSEC-2023-0052"] \ No newline at end of file diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml deleted file mode 100644 index db63209d2..000000000 --- a/.github/workflows/artifacts.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Artifacts - -on: - push: - branches: - - master - - main - pull_request: - -env: - RUST_BACKTRACE: 1 - CARGO_TERM_COLOR: always - -jobs: - artifacts: - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v3 - - - name: Install cargo make - uses: davidB/rust-cargo-make@v1 - - - name: Compile contracts to wasm - run: cargo make rust-optimizer diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 67bf0d443..e876778b7 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -25,23 +25,15 @@ jobs: - name: Install stable Rust run: cargo make install-stable - # selecting a toolchain should happen before the plugin, as the cache uses the current rustc version as its cache key - # - name: Cache dependencies - # uses: Swatinem/rust-cache@v2 - # Artifacts used by tests - name: Compile workspace run: cargo make build - - name: Run test - run: cargo make test - - # disabled because of "no space left" error. - # - name: Run test coverage - # run: cargo make coverage-lcov + - name: Run test coverage + run: cargo make coverage-lcov - # - name: Upload coverage to Codecov - # uses: codecov/codecov-action@v3 - # with: - # token: ${{ secrets.CODECOV_TOKEN }} - # files: target/coverage/lcov.info + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: target/coverage/lcov.info diff --git a/CHANGELOG.md b/CHANGELOG.md index 5504e2fb4..62424d67d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. +## v1.2.0 + +- Allow Credit account to lend/reclaim to the Red Bank (calls Deposit/Withdraw in Red Bank), claim incentive rewards from lending to the Red Bank (pass account_id to track Credit Manager users in `red-bank` and `incentives` contract). +- Pyth oracle price sourcing support for EMA, confidence. Different price query for liquidation and other actions (Deposit / Borrow / Repay etc.). +- New liquidation mechanism on Red Bank and Credit Manager to allow variable liquidation bonus and close factors (similar to Euler model). +- Previously Red Bank and Credit Manager had separate deposit caps to protect against holding to many assets in relation to chain liquidity - consolidated these deposit caps into one `params` contract. +- Common `swapper` contract for Red Bank and Credit Manager. +- Credit Manager and Red Bank use `params` contract for common asset params (see new [types](/contracts/params/src/types)). Previous `Market` / `Markets` queries has changed - part of the params are moved to `params` contract (see [diff](./files/types_diff_v1_0_0__mars_v2.txt)). + ## v1.0.0-rc0 This section documents the API changes compared to the Terra Classic deployment, found in the [`mars-core`](https://github.com/mars-protocol/mars-core) repository. This section is **not comprehensive**, as the changes are numerous. Changelog for later version start here should be made comprehensive. diff --git a/Cargo.lock b/Cargo.lock index 01b560ac4..af441b202 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -293,7 +293,7 @@ checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -342,9 +342,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.20.0" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" [[package]] name = "base64ct" @@ -527,9 +527,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.79" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +dependencies = [ + "libc", +] [[package]] name = "cexpr" @@ -736,7 +739,7 @@ dependencies = [ "hex", "schemars", "serde", - "serde-json-wasm 0.5.1", + "serde-json-wasm", "sha2 0.10.7", "thiserror", ] @@ -812,9 +815,9 @@ dependencies = [ [[package]] name = "cw-it" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e3120d46b30b900c4b9ab0f996ce527c1187095c75ffa61d9ea5aa6d1e93b41" +checksum = "6c7ad91046afef49ea3b922c36395298254802844cd360911cf2e1b96825c9df" dependencies = [ "anyhow", "apollo-cw-multi-test", @@ -842,7 +845,7 @@ dependencies = [ "regex", "serde", "serde_json", - "strum", + "strum 0.24.1", "test-tube", "thiserror", ] @@ -1164,9 +1167,9 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" dependencies = [ "errno-dragonfly", "libc", @@ -1296,7 +1299,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -1711,9 +1714,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" [[package]] name = "log" @@ -1723,42 +1726,44 @@ checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" [[package]] name = "mars-address-provider" -version = "1.2.0" +version = "2.0.0" dependencies = [ "bech32", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.1.0", "cw2 1.1.0", - "mars-owner", - "mars-red-bank-types", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", "serde", "thiserror", ] [[package]] name = "mars-health" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-std", - "mars-red-bank-types", + "mars-params", + "mars-red-bank-types 2.0.0", "mars-testing", "thiserror", ] [[package]] name = "mars-incentives" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.1.0", "cw2 1.1.0", - "mars-owner", + "mars-owner 2.0.0", "mars-red-bank", - "mars-red-bank-types", + "mars-red-bank-types 1.0.0", + "mars-red-bank-types 2.0.0", "mars-testing", - "mars-utils", + "mars-utils 2.0.0", "osmosis-std", "test-case", "thiserror", @@ -1766,35 +1771,67 @@ dependencies = [ [[package]] name = "mars-integration-tests" -version = "1.2.0" +version = "2.0.0" dependencies = [ "anyhow", "cosmwasm-std", "cw-it", "cw-multi-test", + "mars-incentives", "mars-oracle-base", "mars-oracle-osmosis", "mars-osmosis", + "mars-params", "mars-red-bank", - "mars-red-bank-types", + "mars-red-bank-types 2.0.0", "mars-rewards-collector-osmosis", "mars-swapper-osmosis", "mars-testing", - "mars-utils", + "mars-utils 2.0.0", "osmosis-std", "osmosis-test-tube", "serde", ] +[[package]] +name = "mars-interest-rate" +version = "2.0.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "mars-red-bank-types 2.0.0", + "mars-utils 2.0.0", +] + +[[package]] +name = "mars-liquidation" +version = "1.0.0" +dependencies = [ + "cosmwasm-std", + "mars-health", + "mars-params", + "thiserror", +] + +[[package]] +name = "mars-mock-pyth" +version = "2.0.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "pyth-sdk-cw", +] + [[package]] name = "mars-oracle-base" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-std", "cw-storage-plus 1.1.0", "cw2 1.1.0", - "mars-owner", - "mars-red-bank-types", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", + "mars-utils 2.0.0", "pyth-sdk-cw", "schemars", "serde", @@ -1803,7 +1840,7 @@ dependencies = [ [[package]] name = "mars-oracle-osmosis" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1811,10 +1848,10 @@ dependencies = [ "cw2 1.1.0", "mars-oracle-base", "mars-osmosis", - "mars-owner", - "mars-red-bank-types", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", "mars-testing", - "mars-utils", + "mars-utils 2.0.0", "osmosis-std", "pyth-sdk-cw", "schemars", @@ -1823,7 +1860,7 @@ dependencies = [ [[package]] name = "mars-oracle-wasm" -version = "1.2.0" +version = "2.0.0" dependencies = [ "astroport", "cosmwasm-schema", @@ -1832,8 +1869,8 @@ dependencies = [ "cw-storage-plus 1.1.0", "cw2 1.1.0", "mars-oracle-base", - "mars-owner", - "mars-red-bank-types", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", "mars-testing", "proptest", "pyth-sdk-cw", @@ -1842,10 +1879,11 @@ dependencies = [ [[package]] name = "mars-osmosis" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-std", "osmosis-std", + "prost 0.11.9", "serde", ] @@ -1862,48 +1900,96 @@ dependencies = [ "thiserror", ] +[[package]] +name = "mars-owner" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab46e0b2f81a8a98036b46730fbe33a337e98e87cb3d34553b45a5ae87c5828c" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.1.0", + "schemars", + "thiserror", +] + +[[package]] +name = "mars-params" +version = "2.0.0" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test", + "cw-storage-plus 1.1.0", + "cw2 1.1.0", + "mars-interest-rate", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", + "mars-testing", + "mars-utils 2.0.0", + "test-case", + "thiserror", +] + [[package]] name = "mars-red-bank" -version = "1.2.0" +version = "2.0.0" dependencies = [ + "anyhow", "cosmwasm-schema", "cosmwasm-std", + "cw-multi-test", "cw-storage-plus 1.1.0", "cw-utils 1.0.1", "cw2 1.1.0", "mars-health", - "mars-owner", - "mars-red-bank-types", + "mars-interest-rate", + "mars-liquidation", + "mars-owner 2.0.0", + "mars-params", + "mars-red-bank-types 2.0.0", "mars-testing", - "mars-utils", + "mars-utils 2.0.0", + "pyth-sdk-cw", + "test-case", "thiserror", ] [[package]] name = "mars-red-bank-types" -version = "1.2.0" +version = "1.0.0" +source = "git+https://github.com/mars-protocol/red-bank?tag=v1.0.0#13fcc446fe687dfb8f08ffacd57b1ab6ad6cfcc9" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "mars-owner", - "mars-utils", - "strum", + "mars-owner 1.2.0", + "mars-utils 1.0.0", + "thiserror", +] + +[[package]] +name = "mars-red-bank-types" +version = "2.0.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "mars-owner 2.0.0", + "mars-utils 2.0.0", + "strum 0.25.0", "thiserror", ] [[package]] name = "mars-rewards-collector-base" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.1.0", - "mars-osmosis", - "mars-owner", - "mars-red-bank-types", - "mars-testing", - "mars-utils", - "osmosis-std", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", + "mars-utils 2.0.0", "schemars", "serde", "thiserror", @@ -1911,48 +1997,34 @@ dependencies = [ [[package]] name = "mars-rewards-collector-neutron" -version = "1.2.0" +version = "2.0.0" dependencies = [ - "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 1.1.0", "cw2 1.1.0", - "mars-osmosis", - "mars-owner", - "mars-red-bank-types", + "mars-red-bank-types 2.0.0", "mars-rewards-collector-base", - "mars-testing", - "mars-utils", "neutron-sdk", - "osmosis-std", - "schemars", - "serde", - "thiserror", ] [[package]] name = "mars-rewards-collector-osmosis" -version = "1.2.0" +version = "2.0.0" dependencies = [ - "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 1.1.0", "cw2 1.1.0", "mars-osmosis", - "mars-owner", - "mars-red-bank-types", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", "mars-rewards-collector-base", "mars-testing", - "mars-utils", + "mars-utils 2.0.0", "osmosis-std", - "schemars", "serde", - "thiserror", ] [[package]] name = "mars-swapper-astroport" -version = "1.2.0" +version = "2.0.0" dependencies = [ "anyhow", "astroport", @@ -1961,7 +2033,7 @@ dependencies = [ "cw-it", "cw2 1.1.0", "mars-oracle-wasm", - "mars-red-bank-types", + "mars-red-bank-types 2.0.0", "mars-swapper-base", "mars-testing", "test-case", @@ -1969,14 +2041,14 @@ dependencies = [ [[package]] name = "mars-swapper-base" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-paginate", "cw-storage-plus 1.1.0", - "mars-owner", - "mars-red-bank-types", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", "schemars", "serde", "thiserror", @@ -1984,17 +2056,15 @@ dependencies = [ [[package]] name = "mars-swapper-mock" -version = "1.2.0" +version = "2.0.0" dependencies = [ - "anyhow", "cosmwasm-std", - "cw-multi-test", - "mars-red-bank-types", + "mars-red-bank-types 2.0.0", ] [[package]] name = "mars-swapper-osmosis" -version = "1.2.0" +version = "2.0.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -2002,29 +2072,32 @@ dependencies = [ "cw-it", "cw2 1.1.0", "mars-osmosis", - "mars-owner", - "mars-red-bank-types", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", "mars-swapper-base", "osmosis-std", ] [[package]] name = "mars-testing" -version = "1.2.0" +version = "2.0.0" dependencies = [ "anyhow", "astroport", + "cosmwasm-schema", "cosmwasm-std", "cw-it", "cw-multi-test", "mars-address-provider", "mars-incentives", + "mars-mock-pyth", "mars-oracle-osmosis", "mars-oracle-wasm", "mars-osmosis", - "mars-owner", + "mars-owner 2.0.0", + "mars-params", "mars-red-bank", - "mars-red-bank-types", + "mars-red-bank-types 2.0.0", "mars-rewards-collector-osmosis", "mars-swapper-astroport", "osmosis-std", @@ -2034,7 +2107,16 @@ dependencies = [ [[package]] name = "mars-utils" -version = "1.2.0" +version = "1.0.0" +source = "git+https://github.com/mars-protocol/red-bank?tag=v1.0.0#13fcc446fe687dfb8f08ffacd57b1ab6ad6cfcc9" +dependencies = [ + "cosmwasm-std", + "thiserror", +] + +[[package]] +name = "mars-utils" +version = "2.0.0" dependencies = [ "cosmwasm-std", "thiserror", @@ -2080,11 +2162,11 @@ dependencies = [ [[package]] name = "neutron-sdk" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7cc760801f3ed881155431c6a0102c1a8df178430af341c9f8008951ceb0721" +checksum = "adfc6f92cae61b5af9014c09b7bac25ac95b7442be38441a7103377a8edfd37c" dependencies = [ - "base64 0.20.0", + "base64 0.21.2", "bech32", "cosmos-sdk-proto 0.16.0", "cosmwasm-schema", @@ -2094,7 +2176,7 @@ dependencies = [ "protobuf 3.2.0", "schemars", "serde", - "serde-json-wasm 0.4.1", + "serde-json-wasm", "serde_json", "thiserror", ] @@ -2194,9 +2276,9 @@ checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" [[package]] name = "osmosis-std" -version = "0.16.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fa46d2ad5ae738572887974e000934374ce3546b820505c0ee19ca708e49622" +checksum = "75895e4db1a81ca29118e366365744f64314938327e4eedba8e6e462fb15e94f" dependencies = [ "chrono", "cosmwasm-std", @@ -2210,9 +2292,9 @@ dependencies = [ [[package]] name = "osmosis-std-derive" -version = "0.16.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11c2ba5535743617d6f44ae8d572d064fabab6d06ffcf403512f89c58954dbe9" +checksum = "f47f0b2f22adb341bb59e5a3a1b464dde033181954bd055b9ae86d6511ba465b" dependencies = [ "itertools", "proc-macro2", @@ -2223,9 +2305,9 @@ dependencies = [ [[package]] name = "osmosis-test-tube" -version = "16.0.1" +version = "16.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527375c01396e7e4de4ccc18a0141aeb6b342dc089d30c57282025f3a8753e72" +checksum = "4929047d1dcec5d7d02fd0a00ecdfca78918d3a33bffc193bf57b3eeb4d407ab" dependencies = [ "base64 0.13.1", "bindgen", @@ -2301,9 +2383,9 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pest" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d2d1d55045829d65aad9d389139882ad623b33b904e7c9f1b10c5b8927298e5" +checksum = "1acb4a4365a13f749a93f1a094a7805e5cfa0955373a9de860d962eaa3a5fe5a" dependencies = [ "thiserror", "ucd-trie", @@ -2311,9 +2393,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f94bca7e7a599d89dea5dfa309e217e7906c3c007fb9c3299c40b10d6a315d3" +checksum = "666d00490d4ac815001da55838c500eafb0320019bbaa44444137c48b443a853" dependencies = [ "pest", "pest_generator", @@ -2321,22 +2403,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d490fe7e8556575ff6911e45567ab95e71617f43781e5c05490dc8d75c965c" +checksum = "68ca01446f50dbda87c1786af8770d535423fa8a53aec03b8f4e3d7eb10e0929" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] name = "pest_meta" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2674c66ebb4b4d9036012091b537aae5878970d6999f81a265034d85b136b341" +checksum = "56af0a30af74d0445c0bf6d9d051c979b516a1a5af790d251daee76005420a48" dependencies = [ "once_cell", "pest", @@ -2345,29 +2427,29 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] name = "pin-project-lite" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" +checksum = "2c516611246607d0c04186886dbb3a754368ef82c79e9827a802c6d836dd111c" [[package]] name = "pin-utils" @@ -2634,9 +2716,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" dependencies = [ "aho-corasick", "memchr", @@ -2646,9 +2728,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.3" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" dependencies = [ "aho-corasick", "memchr", @@ -2748,9 +2830,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.4" +version = "0.38.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +checksum = "172891ebdceb05aa0005f533a6cbfca599ddd7d966f6f5d4d9b2e70478e70399" dependencies = [ "bitflags 2.3.3", "errno", @@ -2905,9 +2987,9 @@ checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" [[package]] name = "serde" -version = "1.0.176" +version = "1.0.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76dc28c9523c5d70816e393136b86d48909cfb27cecaa902d338c19ed47164dc" +checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" dependencies = [ "serde_derive", ] @@ -2921,15 +3003,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde-json-wasm" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479b4dbc401ca13ee8ce902851b834893251404c4f3c65370a49e047a6be09a5" -dependencies = [ - "serde", -] - [[package]] name = "serde-json-wasm" version = "0.5.1" @@ -2950,13 +3023,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.176" +version = "1.0.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e7b8c5dc823e3b90651ff1d3808419cd14e5ad76de04feaf37da114e7a306f" +checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -2983,13 +3056,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e168eaaf71e8f9bd6037feb05190485708e019f4fd87d161b3c0a0d37daf85e5" +checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -3127,7 +3200,16 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" dependencies = [ - "strum_macros", + "strum_macros 0.24.3", +] + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros 0.25.2", ] [[package]] @@ -3143,6 +3225,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "strum_macros" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.28", +] + [[package]] name = "subtle" version = "2.5.0" @@ -3171,9 +3266,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.27" +version = "2.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" dependencies = [ "proc-macro2", "quote", @@ -3182,9 +3277,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.7.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" +checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651" dependencies = [ "cfg-if", "fastrand", @@ -3390,7 +3485,7 @@ checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -3451,7 +3546,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -3655,7 +3750,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", "wasm-bindgen-shared", ] @@ -3677,7 +3772,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3851,5 +3946,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] diff --git a/Cargo.toml b/Cargo.toml index 6663546fd..71ea9291f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,12 +2,16 @@ members = [ "contracts/address-provider", "contracts/incentives", + "contracts/mock-pyth", "contracts/oracle/*", + "contracts/params", "contracts/swapper/*", "contracts/red-bank", "contracts/rewards-collector/*", "packages/chains/*", "packages/health", + "packages/interest-rate", + "packages/liquidation", "packages/testing", "packages/types", "packages/utils", @@ -16,7 +20,7 @@ members = [ resolver = "2" [workspace.package] -version = "1.2.0" +version = "2.0.0" authors = [ "Gabe R. ", "Larry Engineer ", @@ -26,62 +30,63 @@ authors = [ "Ahmad Kaouk", "Harry Scholes", ] -edition = "2021" -license = "GPL-3.0-or-later" -repository = "https://github.com/mars-protocol/red-bank" -homepage = "https://marsprotocol.io" +edition = "2021" +license = "GPL-3.0-or-later" +repository = "https://github.com/mars-protocol/red-bank" +homepage = "https://marsprotocol.io" documentation = "https://docs.marsprotocol.io/" -keywords = [ - "mars", - "cosmos", - "cosmwasm", -] +keywords = ["mars", "cosmos", "cosmwasm"] [workspace.dependencies] -anyhow = "1.0.71" +anyhow = "1.0.72" +astroport = "2.8.0" bech32 = "0.9.1" -cosmwasm-schema = "1.2.6" -cosmwasm-std = "1.2.6" +cosmwasm-schema = "1.3.1" +cosmwasm-std = "1.3.1" cw2 = "1.1.0" -cw-storage-plus = "1.0.1" +cw-paginate = "0.2.1" +cw-storage-plus = "1.1.0" cw-utils = "1.0.1" -mars-owner = { version = "1.2.0", features = ["emergency-owner"] } -osmosis-std = "0.16.1" -prost = { version = "0.11.5", default-features = false, features = ["prost-derive"] } +mars-owner = { version = "2.0.0", features = ["emergency-owner"] } +neutron-sdk = "0.6.1" +osmosis-std = "0.16.2" +prost = { version = "0.11.9", default-features = false } +pyth-sdk-cw = "1.2.0" schemars = "0.8.12" -serde = { version = "1.0.163", default-features = false, features = ["derive"] } -thiserror = "1.0.40" -pyth-sdk-cw = "1.2.0" -cw-paginate = "0.2.1" -astroport = "2.8.0" -strum = "0.24.1" -neutron-sdk = "0.6.0" +serde = { version = "1.0.181", default-features = false } +strum = "0.25.0" +thiserror = "1.0.44" # dev-dependencies cw-multi-test = "0.16.5" -cw-it = "0.1.0" -osmosis-test-tube = "16.0.0" -test-case = "3.0.0" -proptest = "1.1.0" +cw-it = "0.1.1" +osmosis-test-tube = "=16.1.1" +proptest = "1.2.0" +test-case = "3.1.0" # packages -mars-health = { path = "./packages/health" } -mars-osmosis = { path = "./packages/chains/osmosis" } -mars-red-bank-types = { path = "./packages/types" } -mars-testing = { path = "./packages/testing" } -mars-utils = { path = "./packages/utils" } +mars-health = { path = "./packages/health" } +mars-interest-rate = { path = "./packages/interest-rate" } +mars-liquidation = { path = "./packages/liquidation" } +mars-osmosis = { path = "./packages/chains/osmosis" } +mars-red-bank-types = { path = "./packages/types" } +mars-testing = { path = "./packages/testing" } +mars-utils = { path = "./packages/utils" } # contracts mars-address-provider = { path = "./contracts/address-provider" } mars-incentives = { path = "./contracts/incentives" } +mars-mock-pyth = { path = "./contracts/mock-pyth" } mars-oracle-base = { path = "./contracts/oracle/base" } mars-oracle-osmosis = { path = "./contracts/oracle/osmosis" } mars-oracle-wasm = { path = "./contracts/oracle/wasm" } +mars-params = { path = "./contracts/params" } mars-red-bank = { path = "./contracts/red-bank" } mars-rewards-collector-base = { path = "./contracts/rewards-collector/base" } +mars-rewards-collector-neutron = { path = "./contracts/rewards-collector/neutron" } mars-rewards-collector-osmosis = { path = "./contracts/rewards-collector/osmosis" } -mars-swapper-base = { path = "./contracts/swapper/base" } mars-swapper-astroport = { path = "./contracts/swapper/astroport" } +mars-swapper-base = { path = "./contracts/swapper/base" } mars-swapper-osmosis = { path = "./contracts/swapper/osmosis" } [profile.release] diff --git a/Makefile.toml b/Makefile.toml index 0f985ef21..a27c7d1ae 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -10,7 +10,7 @@ default_to_workspace = false # Directory with wasm files used by integration tests (another directory can be used instead, for example 'artifacts' from rust-optimizer) ARTIFACTS_DIR_PATH = "target/wasm32-unknown-unknown/release" # If you bump this version, verify RUST_VERSION correctness -RUST_OPTIMIZER_VERSION = "0.12.13" +RUST_OPTIMIZER_VERSION = "0.13.0" # Use rust version from rust-optimizer Dockerfile (see https://github.com/CosmWasm/rust-optimizer/blob/main/Dockerfile#L1) # to be sure that we compile / test against the same version RUST_VERSION = "1.69.0" @@ -44,7 +44,7 @@ else image="cosmwasm/workspace-optimizer:${RUST_OPTIMIZER_VERSION}" fi docker run --rm -v "$(pwd)":/code \ - --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ + --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \ --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ ${image} """ diff --git a/contracts/address-provider/Cargo.toml b/contracts/address-provider/Cargo.toml index cc8b186cd..373b90e19 100644 --- a/contracts/address-provider/Cargo.toml +++ b/contracts/address-provider/Cargo.toml @@ -11,22 +11,13 @@ documentation = { workspace = true } keywords = { workspace = true } [lib] -crate-type = [ - "cdylib", - "rlib", -] -doctest = false - -[profile.release] -overflow-checks = true +crate-type = ["cdylib", "rlib"] +doctest = false [features] # for more explicit tests, cargo test --features=backtraces -backtraces = [ - "cosmwasm-std/backtraces", -] -library = [ -] +backtraces = ["cosmwasm-std/backtraces"] +library = [] [dependencies] bech32 = { workspace = true } diff --git a/contracts/address-provider/tests/all_tests.rs b/contracts/address-provider/tests/all_tests.rs new file mode 100644 index 000000000..14f00389d --- /dev/null +++ b/contracts/address-provider/tests/all_tests.rs @@ -0,0 +1 @@ +mod tests; diff --git a/contracts/address-provider/tests/helpers.rs b/contracts/address-provider/tests/tests/helpers/mod.rs similarity index 100% rename from contracts/address-provider/tests/helpers.rs rename to contracts/address-provider/tests/tests/helpers/mod.rs diff --git a/contracts/address-provider/tests/tests/mod.rs b/contracts/address-provider/tests/tests/mod.rs new file mode 100644 index 000000000..d89823fb5 --- /dev/null +++ b/contracts/address-provider/tests/tests/mod.rs @@ -0,0 +1,5 @@ +mod helpers; + +mod test_addresses; +mod test_instantiate; +mod test_update_owner; diff --git a/contracts/address-provider/tests/test_addresses.rs b/contracts/address-provider/tests/tests/test_addresses.rs similarity index 98% rename from contracts/address-provider/tests/test_addresses.rs rename to contracts/address-provider/tests/tests/test_addresses.rs index be3d1b1ae..ced40337a 100644 --- a/contracts/address-provider/tests/test_addresses.rs +++ b/contracts/address-provider/tests/tests/test_addresses.rs @@ -5,9 +5,7 @@ use mars_red_bank_types::address_provider::{ AddressResponseItem, ExecuteMsg, MarsAddressType, QueryMsg, }; -use crate::helpers::{th_query, th_setup}; - -mod helpers; +use super::helpers::{th_query, th_setup}; #[test] fn setting_address_if_unauthorized() { diff --git a/contracts/address-provider/tests/test_instantiate.rs b/contracts/address-provider/tests/tests/test_instantiate.rs similarity index 96% rename from contracts/address-provider/tests/test_instantiate.rs rename to contracts/address-provider/tests/tests/test_instantiate.rs index d075a9ce8..71c36766c 100644 --- a/contracts/address-provider/tests/test_instantiate.rs +++ b/contracts/address-provider/tests/tests/test_instantiate.rs @@ -2,9 +2,7 @@ use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use mars_address_provider::{contract::instantiate, error::ContractError}; use mars_red_bank_types::address_provider::{ConfigResponse, InstantiateMsg, QueryMsg}; -use crate::helpers::th_query; - -mod helpers; +use super::helpers::th_query; #[test] fn invalid_chain_prefix() { diff --git a/contracts/address-provider/tests/test_update_owner.rs b/contracts/address-provider/tests/tests/test_update_owner.rs similarity index 97% rename from contracts/address-provider/tests/test_update_owner.rs rename to contracts/address-provider/tests/tests/test_update_owner.rs index 511aefdca..a61716795 100644 --- a/contracts/address-provider/tests/test_update_owner.rs +++ b/contracts/address-provider/tests/tests/test_update_owner.rs @@ -3,9 +3,7 @@ use mars_address_provider::{contract::execute, error::ContractError}; use mars_owner::{OwnerError::NotOwner, OwnerUpdate}; use mars_red_bank_types::address_provider::{ConfigResponse, ExecuteMsg, QueryMsg}; -use crate::helpers::{th_query, th_setup}; - -mod helpers; +use super::helpers::{th_query, th_setup}; #[test] fn initialized_state() { diff --git a/contracts/incentives/Cargo.toml b/contracts/incentives/Cargo.toml index 9dae3dccd..37ec5391e 100644 --- a/contracts/incentives/Cargo.toml +++ b/contracts/incentives/Cargo.toml @@ -12,26 +12,26 @@ keywords = { workspace = true } [lib] crate-type = ["cdylib", "rlib"] -doctest = false - -[profile.release] -overflow-checks = true +doctest = false [features] # for more explicit tests, cargo test --features=backtraces backtraces = ["cosmwasm-std/backtraces", "mars-testing/backtraces", "mars-utils/backtraces", "mars-red-bank/backtraces"] [dependencies] -cosmwasm-std = { workspace = true } -cw2 = { workspace = true } -cw-storage-plus = { workspace = true } -mars-owner = { workspace = true } -mars-red-bank-types = { workspace = true } -mars-utils = { workspace = true } -thiserror = { workspace = true } +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw2 = { workspace = true } +cw-storage-plus = { workspace = true } +mars-owner = { workspace = true } +mars-red-bank-types = { workspace = true } +mars-utils = { workspace = true } +thiserror = { workspace = true } + +# Old red-bank types used for migration. +mars-red-bank-types-old = { package = "mars-red-bank-types", git = "https://github.com/mars-protocol/red-bank", tag = "v1.0.0" } [dev-dependencies] -cosmwasm-schema = { workspace = true } mars-testing = { workspace = true } osmosis-std = { workspace = true } mars-red-bank = { workspace = true } diff --git a/contracts/incentives/src/contract.rs b/contracts/incentives/src/contract.rs index ec48353fb..849a0055b 100644 --- a/contracts/incentives/src/contract.rs +++ b/contracts/incentives/src/contract.rs @@ -22,6 +22,7 @@ use crate::{ helpers::{ self, compute_user_accrued_rewards, compute_user_unclaimed_rewards, update_incentive_index, }, + migrations, state::{ self, CONFIG, DEFAULT_LIMIT, EMISSIONS, EPOCH_DURATION, INCENTIVE_STATES, MAX_LIMIT, OWNER, USER_ASSET_INDICES, USER_UNCLAIMED_REWARDS, WHITELIST, WHITELIST_COUNT, @@ -103,6 +104,7 @@ pub fn execute( ), ExecuteMsg::BalanceChange { user_addr, + account_id, denom, user_amount_scaled_before, total_amount_scaled_before, @@ -111,11 +113,13 @@ pub fn execute( env, info, user_addr, + account_id, denom, user_amount_scaled_before, total_amount_scaled_before, ), ExecuteMsg::ClaimRewards { + account_id, start_after_collateral_denom, start_after_incentive_denom, limit, @@ -123,6 +127,7 @@ pub fn execute( deps, env, info, + account_id, start_after_collateral_denom, start_after_incentive_denom, limit, @@ -338,6 +343,7 @@ pub fn execute_balance_change( env: Env, info: MessageInfo, user_addr: Addr, + account_id: Option, collateral_denom: String, user_amount_scaled_before: Uint128, total_amount_scaled_before: Uint128, @@ -348,10 +354,17 @@ pub fn execute_balance_change( return Err(MarsError::Unauthorized {}.into()); } + let acc_id = account_id.clone().unwrap_or("".to_string()); + let base_event = Event::new("mars/incentives/balance_change") .add_attribute("action", "balance_change") .add_attribute("denom", collateral_denom.clone()) .add_attribute("user", user_addr.to_string()); + let base_event = if account_id.is_some() { + base_event.add_attribute("account_id", &acc_id) + } else { + base_event + }; let mut events = vec![base_event]; let incentive_states = INCENTIVE_STATES @@ -370,7 +383,7 @@ pub fn execute_balance_change( // Check if user has accumulated uncomputed rewards (which means index is not up to date) let user_asset_index_key = - USER_ASSET_INDICES.key((&user_addr, &collateral_denom, &incentive_denom)); + USER_ASSET_INDICES.key(((&user_addr, &acc_id), &collateral_denom, &incentive_denom)); let user_asset_index = user_asset_index_key.may_load(deps.storage)?.unwrap_or_else(Decimal::zero); @@ -390,6 +403,7 @@ pub fn execute_balance_change( state::increase_unclaimed_rewards( deps.storage, &user_addr, + &acc_id, &collateral_denom, &incentive_denom, accrued_rewards, @@ -414,18 +428,26 @@ pub fn execute_claim_rewards( mut deps: DepsMut, env: Env, info: MessageInfo, + account_id: Option, start_after_collateral_denom: Option, start_after_incentive_denom: Option, limit: Option, ) -> Result { - let red_bank_addr = query_red_bank_address(deps.as_ref())?; let user_addr = info.sender; + let acc_id = account_id.clone().unwrap_or("".to_string()); - let mut response = Response::new().add_event( - Event::new("mars/incentives/claim_rewards") - .add_attribute("action", "claim_rewards") - .add_attribute("user", user_addr.to_string()), - ); + let red_bank_addr = query_red_bank_address(deps.as_ref())?; + + let mut response = Response::new(); + let base_event = Event::new("mars/incentives/claim_rewards") + .add_attribute("action", "claim_rewards") + .add_attribute("user", user_addr.to_string()); + let base_event = if account_id.is_some() { + base_event.add_attribute("account_id", &acc_id) + } else { + base_event + }; + response = response.add_event(base_event); let asset_incentives = state::paginate_incentive_states( deps.storage, @@ -444,6 +466,7 @@ pub fn execute_claim_rewards( &env.block, &red_bank_addr, &user_addr, + &account_id, &collateral_denom, &incentive_denom, )?; @@ -451,7 +474,7 @@ pub fn execute_claim_rewards( // clear unclaimed rewards USER_UNCLAIMED_REWARDS.save( deps.storage, - (&user_addr, &collateral_denom, &incentive_denom), + ((&user_addr, &acc_id), &collateral_denom, &incentive_denom), &Uint128::zero(), )?; @@ -531,6 +554,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { )?), QueryMsg::UserUnclaimedRewards { user, + account_id, start_after_collateral_denom, start_after_incentive_denom, limit, @@ -538,6 +562,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { deps, env, user, + account_id, start_after_collateral_denom, start_after_incentive_denom, limit, @@ -596,6 +621,7 @@ pub fn query_config(deps: Deps) -> StdResult { address_provider: config.address_provider, max_whitelisted_denoms: config.max_whitelisted_denoms, epoch_duration: EPOCH_DURATION.load(deps.storage)?, + whitelist_count: WHITELIST_COUNT.may_load(deps.storage)?.unwrap_or_default(), }) } @@ -634,13 +660,15 @@ pub fn query_user_unclaimed_rewards( deps: Deps, env: Env, user: String, + account_id: Option, start_after_collateral_denom: Option, start_after_incentive_denom: Option, limit: Option, -) -> StdResult> { - let red_bank_addr = query_red_bank_address(deps)?; +) -> Result, ContractError> { let user_addr = deps.api.addr_validate(&user)?; + let red_bank_addr = query_red_bank_address(deps)?; + let incentive_states = state::paginate_incentive_states( deps.storage, start_after_collateral_denom, @@ -657,6 +685,7 @@ pub fn query_user_unclaimed_rewards( &env.block, &red_bank_addr, &user_addr, + &account_id, &collateral_denom, &incentive_denom, )?; @@ -737,6 +766,8 @@ pub fn query_emissions( /// MIGRATION #[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { - Ok(Response::default()) +pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Result { + match msg { + MigrateMsg::V1_0_0ToV2_0_0(updates) => migrations::v2_0_0::migrate(deps, env, updates), + } } diff --git a/contracts/incentives/src/error.rs b/contracts/incentives/src/error.rs index 7a77a92f8..167efdfb0 100644 --- a/contracts/incentives/src/error.rs +++ b/contracts/incentives/src/error.rs @@ -66,6 +66,9 @@ pub enum ContractError { DuplicateDenom { denom: String, }, + + #[error("{0}")] + Version(#[from] cw2::VersionError), } impl From for StdError { diff --git a/contracts/incentives/src/helpers.rs b/contracts/incentives/src/helpers.rs index 7183496ea..5201bd946 100644 --- a/contracts/incentives/src/helpers.rs +++ b/contracts/incentives/src/helpers.rs @@ -254,11 +254,14 @@ pub fn compute_user_unclaimed_rewards( block: &BlockInfo, red_bank_addr: &Addr, user_addr: &Addr, + account_id: &Option, collateral_denom: &str, incentive_denom: &str, ) -> StdResult { + let acc_id = account_id.clone().unwrap_or("".to_string()); + let mut unclaimed_rewards = USER_UNCLAIMED_REWARDS - .may_load(storage.to_storage(), (user_addr, collateral_denom, incentive_denom))? + .may_load(storage.to_storage(), ((user_addr, &acc_id), collateral_denom, incentive_denom))? .unwrap_or_else(Uint128::zero); // Get asset user balances and total supply @@ -266,6 +269,7 @@ pub fn compute_user_unclaimed_rewards( red_bank_addr, &red_bank::QueryMsg::UserCollateral { user: user_addr.to_string(), + account_id: account_id.clone(), denom: collateral_denom.to_string(), }, )?; @@ -292,7 +296,7 @@ pub fn compute_user_unclaimed_rewards( )?; let user_asset_index = USER_ASSET_INDICES - .may_load(storage.to_storage(), (user_addr, collateral_denom, incentive_denom))? + .may_load(storage.to_storage(), ((user_addr, &acc_id), collateral_denom, incentive_denom))? .unwrap_or_else(Decimal::zero); if user_asset_index != incentive_state.index { @@ -310,7 +314,7 @@ pub fn compute_user_unclaimed_rewards( if user_asset_index != incentive_state.index { USER_ASSET_INDICES.save( *storage, - (user_addr, collateral_denom, incentive_denom), + ((user_addr, &acc_id), collateral_denom, incentive_denom), &incentive_state.index, )? } diff --git a/contracts/incentives/src/lib.rs b/contracts/incentives/src/lib.rs index 627a6e152..88d83aeaf 100644 --- a/contracts/incentives/src/lib.rs +++ b/contracts/incentives/src/lib.rs @@ -1,6 +1,7 @@ pub mod contract; mod error; pub mod helpers; +pub mod migrations; pub mod state; pub use error::ContractError; diff --git a/contracts/incentives/src/migrations/mod.rs b/contracts/incentives/src/migrations/mod.rs new file mode 100644 index 000000000..7592b6f12 --- /dev/null +++ b/contracts/incentives/src/migrations/mod.rs @@ -0,0 +1 @@ +pub mod v2_0_0; diff --git a/contracts/incentives/src/migrations/v2_0_0.rs b/contracts/incentives/src/migrations/v2_0_0.rs new file mode 100644 index 000000000..4e3da098a --- /dev/null +++ b/contracts/incentives/src/migrations/v2_0_0.rs @@ -0,0 +1,286 @@ +use std::collections::HashMap; + +use cosmwasm_std::{DepsMut, Env, Order, Response, StdResult, Uint128}; +use cw2::{assert_contract_version, set_contract_version}; +use mars_owner::OwnerInit; +use mars_red_bank_types::incentives::{Config, IncentiveState, V2Updates}; + +use crate::{ + contract::{CONTRACT_NAME, CONTRACT_VERSION, MIN_EPOCH_DURATION}, + error::ContractError, + state::{ + CONFIG, EPOCH_DURATION, INCENTIVE_STATES, OWNER, USER_ASSET_INDICES, + USER_UNCLAIMED_REWARDS, WHITELIST, WHITELIST_COUNT, + }, +}; + +const FROM_VERSION: &str = "1.0.0"; + +pub mod v1_state { + use cosmwasm_schema::cw_serde; + use cosmwasm_std::{Addr, Decimal, Uint128}; + use cw_storage_plus::{Item, Map}; + use mars_red_bank_types_old::incentives::{AssetIncentive, Config}; + + pub const OWNER: Item = Item::new("owner"); + pub const CONFIG: Item = Item::new("config"); + + pub const ASSET_INCENTIVES: Map<&str, AssetIncentive> = Map::new("incentives"); + pub const USER_ASSET_INDICES: Map<(&Addr, &str), Decimal> = Map::new("indices"); + pub const USER_UNCLAIMED_REWARDS: Map<&Addr, Uint128> = Map::new("unclaimed_rewards"); + + #[cw_serde] + pub enum OwnerState { + B(OwnerSetNoneProposed), + } + + #[cw_serde] + pub struct OwnerSetNoneProposed { + pub owner: Addr, + } + + pub fn current_owner(state: OwnerState) -> Addr { + match state { + OwnerState::B(b) => b.owner, + } + } + + // Copy of helpers from v1.0.0 tag: + // https://github.com/mars-protocol/red-bank/blob/v1.0.0/contracts/incentives/src/helpers.rs + // Included as dependency coudn't generate proper schema for mars-incentive, even with specified + // version. + pub mod helpers { + use std::cmp::{max, min}; + + use cosmwasm_std::{ + Decimal, OverflowError, OverflowOperation, StdError, StdResult, Uint128, + }; + use mars_red_bank_types_old::incentives::AssetIncentive; + + /// Updates asset incentive index and last updated timestamp by computing + /// how many rewards were accrued since last time updated given incentive's + /// emission per second. + /// Total supply is the total (liquidity) token supply during the period being computed. + /// Note that this method does not commit updates to state as that should be executed by the + /// caller + pub fn update_asset_incentive_index( + asset_incentive: &mut AssetIncentive, + total_amount_scaled: Uint128, + current_block_time: u64, + ) -> StdResult<()> { + let end_time_sec = asset_incentive.start_time + asset_incentive.duration; + if (current_block_time != asset_incentive.last_updated) + && current_block_time > asset_incentive.start_time + && asset_incentive.last_updated < end_time_sec + && !total_amount_scaled.is_zero() + && !asset_incentive.emission_per_second.is_zero() + { + let time_start = max(asset_incentive.start_time, asset_incentive.last_updated); + let time_end = min(current_block_time, end_time_sec); + asset_incentive.index = compute_asset_incentive_index( + asset_incentive.index, + asset_incentive.emission_per_second, + total_amount_scaled, + time_start, + time_end, + )?; + } + asset_incentive.last_updated = current_block_time; + Ok(()) + } + + pub fn compute_asset_incentive_index( + previous_index: Decimal, + emission_per_second: Uint128, + total_amount_scaled: Uint128, + time_start: u64, + time_end: u64, + ) -> StdResult { + if time_start > time_end { + return Err(StdError::overflow(OverflowError::new( + OverflowOperation::Sub, + time_start, + time_end, + ))); + } + let seconds_elapsed = time_end - time_start; + let emission_for_elapsed_seconds = + emission_per_second.checked_mul(Uint128::from(seconds_elapsed))?; + let new_index = previous_index + + Decimal::from_ratio(emission_for_elapsed_seconds, total_amount_scaled); + Ok(new_index) + } + + /// Computes user accrued rewards using the difference between asset_incentive index and + /// user current index + /// asset_incentives index should be up to date. + pub fn compute_user_accrued_rewards( + user_amount_scaled: Uint128, + user_asset_index: Decimal, + asset_incentive_index: Decimal, + ) -> StdResult { + let result = (user_amount_scaled * asset_incentive_index) + .checked_sub(user_amount_scaled * user_asset_index)?; + Ok(result) + } + } +} + +pub fn migrate(mut deps: DepsMut, env: Env, updates: V2Updates) -> Result { + // make sure we're migrating the correct contract and from the correct version + assert_contract_version(deps.storage, &format!("crates.io:{CONTRACT_NAME}"), FROM_VERSION)?; + + // Owner package updated, re-initializing + let old_owner_state = v1_state::OWNER.load(deps.storage)?; + let old_owner = v1_state::current_owner(old_owner_state); + v1_state::OWNER.remove(deps.storage); + OWNER.initialize( + deps.storage, + deps.api, + OwnerInit::SetInitialOwner { + owner: old_owner.to_string(), + }, + )?; + + // CONFIG updated, re-initializing + let old_config_state = v1_state::CONFIG.load(deps.storage)?; + v1_state::CONFIG.remove(deps.storage); + CONFIG.save( + deps.storage, + &Config { + address_provider: old_config_state.address_provider, + max_whitelisted_denoms: updates.max_whitelisted_denoms, + }, + )?; + + // WHITELIST not existent in v1, initializing + WHITELIST.save(deps.storage, &old_config_state.mars_denom, &Uint128::one())?; + WHITELIST_COUNT.save(deps.storage, &1)?; + + // EPOCH_DURATION not existent in v1, initializing + if updates.epoch_duration < MIN_EPOCH_DURATION { + return Err(ContractError::EpochDurationTooShort { + min_epoch_duration: MIN_EPOCH_DURATION, + }); + } + EPOCH_DURATION.save(deps.storage, &updates.epoch_duration)?; + + migrate_indices_and_unclaimed_rewards(&mut deps, env, &old_config_state.mars_denom)?; + + set_contract_version(deps.storage, format!("crates.io:{CONTRACT_NAME}"), CONTRACT_VERSION)?; + + Ok(Response::new() + .add_attribute("action", "migrate") + .add_attribute("from_version", FROM_VERSION) + .add_attribute("to_version", CONTRACT_VERSION)) +} + +// Migrate indices and unclaimed rewards from v1 to v2 with helpers from v1.0.0 tag: +// https://github.com/mars-protocol/red-bank/blob/v1.0.0/contracts/incentives/src/helpers.rs +// +// This is done by querying the Red Bank contract for the collateral total supply and +// user collateral amount for each collateral denom. +fn migrate_indices_and_unclaimed_rewards( + deps: &mut DepsMut, + env: Env, + mars_denom: &str, +) -> Result<(), ContractError> { + let current_block_time = env.block.time.seconds(); + + let config = CONFIG.load(deps.storage)?; + + let red_bank_addr = mars_red_bank_types::address_provider::helpers::query_contract_addr( + deps.as_ref(), + &config.address_provider, + mars_red_bank_types::address_provider::MarsAddressType::RedBank, + )?; + + let mut asset_incentives = v1_state::ASSET_INCENTIVES + .range(deps.storage, None, None, Order::Ascending) + .collect::>>()?; + v1_state::ASSET_INCENTIVES.clear(deps.storage); + + for (denom, asset_incentive) in asset_incentives.iter_mut() { + let market: mars_red_bank_types::red_bank::Market = deps.querier.query_wasm_smart( + red_bank_addr.clone(), + &mars_red_bank_types::red_bank::QueryMsg::Market { + denom: denom.clone(), + }, + )?; + + v1_state::helpers::update_asset_incentive_index( + asset_incentive, + market.collateral_total_scaled, + current_block_time, + )?; + + // Update incentive state for collateral and incentive denom (Mars) + INCENTIVE_STATES.save( + deps.storage, + (denom, mars_denom), + &IncentiveState { + index: asset_incentive.index, + last_updated: current_block_time, + }, + )?; + } + + let user_asset_indices = v1_state::USER_ASSET_INDICES + .range(deps.storage, None, None, Order::Ascending) + .collect::>>()?; + v1_state::USER_ASSET_INDICES.clear(deps.storage); + + let mut user_unclaimed_rewards = v1_state::USER_UNCLAIMED_REWARDS + .range(deps.storage, None, None, Order::Ascending) + .collect::>>()?; + v1_state::USER_UNCLAIMED_REWARDS.clear(deps.storage); + + for ((user, denom), user_asset_index) in user_asset_indices { + let collateral: mars_red_bank_types::red_bank::UserCollateralResponse = + deps.querier.query_wasm_smart( + red_bank_addr.clone(), + &mars_red_bank_types::red_bank::QueryMsg::UserCollateral { + user: user.to_string(), + account_id: None, + denom: denom.clone(), + }, + )?; + + // Get asset incentive for a denom. It should be available but just in case we don't unwrap + let denom_idx = asset_incentives.get(&denom); + if let Some(asset_incentive) = denom_idx { + // Since we didn't track unclaimed rewards per collateral denom in v1 we add them + // to the user unclaimed rewards for the first user collateral denom. + let mut unclaimed_rewards = user_unclaimed_rewards.remove(&user).unwrap_or_default(); + + if user_asset_index != asset_incentive.index { + // Compute user accrued rewards + let asset_accrued_rewards = v1_state::helpers::compute_user_accrued_rewards( + collateral.amount_scaled, + user_asset_index, + asset_incentive.index, + )?; + + unclaimed_rewards += asset_accrued_rewards; + } + + if !unclaimed_rewards.is_zero() { + // Update user unclaimed rewards + USER_UNCLAIMED_REWARDS.save( + deps.storage, + ((&user, ""), &denom, mars_denom), + &unclaimed_rewards, + )?; + } + + // Update user asset index + USER_ASSET_INDICES.save( + deps.storage, + ((&user, ""), &denom, mars_denom), + &asset_incentive.index, + )?; + } + } + + Ok(()) +} diff --git a/contracts/incentives/src/state.rs b/contracts/incentives/src/state.rs index b8f9ede94..16d2342f9 100644 --- a/contracts/incentives/src/state.rs +++ b/contracts/incentives/src/state.rs @@ -32,12 +32,13 @@ pub const INCENTIVE_STATES: Map<(&str, &str), IncentiveState> = Map::new("incent pub const EMISSIONS: Map<(&str, &str, u64), Uint128> = Map::new("emissions"); /// A map containing the incentive index for a given user, collateral denom and incentive denom. -/// The key is (user address, collateral denom, incentive denom). -pub const USER_ASSET_INDICES: Map<(&Addr, &str, &str), Decimal> = Map::new("indices"); +/// The key is (user address with optional account id, collateral denom, incentive denom). +pub const USER_ASSET_INDICES: Map<((&Addr, &str), &str, &str), Decimal> = Map::new("indices"); /// A map containing the amount of unclaimed incentives for a given user and incentive denom. -/// The key is (user address, collateral denom, incentive denom). -pub const USER_UNCLAIMED_REWARDS: Map<(&Addr, &str, &str), Uint128> = Map::new("unclaimed_rewards"); +/// The key is (user address with optional account id, collateral denom, incentive denom). +pub const USER_UNCLAIMED_REWARDS: Map<((&Addr, &str), &str, &str), Uint128> = + Map::new("unclaimed_rewards"); /// The default limit for pagination pub const DEFAULT_LIMIT: u32 = 5; @@ -50,13 +51,14 @@ pub const MAX_LIMIT: u32 = 10; pub fn increase_unclaimed_rewards( storage: &mut dyn Storage, user_addr: &Addr, + acc_id: &str, collateral_denom: &str, incentive_denom: &str, accrued_rewards: Uint128, ) -> StdResult<()> { USER_UNCLAIMED_REWARDS.update( storage, - (user_addr, collateral_denom, incentive_denom), + ((user_addr, acc_id), collateral_denom, incentive_denom), |ur: Option| -> StdResult { Ok(ur.map_or_else(|| accrued_rewards, |r| r + accrued_rewards)) }, diff --git a/contracts/incentives/tests/all_tests.rs b/contracts/incentives/tests/all_tests.rs new file mode 100644 index 000000000..14f00389d --- /dev/null +++ b/contracts/incentives/tests/all_tests.rs @@ -0,0 +1 @@ +mod tests; diff --git a/contracts/incentives/tests/helpers.rs b/contracts/incentives/tests/tests/helpers/mod.rs similarity index 100% rename from contracts/incentives/tests/helpers.rs rename to contracts/incentives/tests/tests/helpers/mod.rs diff --git a/contracts/incentives/tests/tests/mod.rs b/contracts/incentives/tests/tests/mod.rs new file mode 100644 index 000000000..e95d9f695 --- /dev/null +++ b/contracts/incentives/tests/tests/mod.rs @@ -0,0 +1,11 @@ +mod helpers; + +mod test_admin; +mod test_balance_change; +mod test_claim_rewards; +mod test_indices_usage; +mod test_migration_v2; +mod test_quering; +mod test_set_asset_incentive; +mod test_update_owner; +mod test_whitelist; diff --git a/contracts/incentives/tests/test_admin.rs b/contracts/incentives/tests/tests/test_admin.rs similarity index 90% rename from contracts/incentives/tests/test_admin.rs rename to contracts/incentives/tests/tests/test_admin.rs index fc0807433..864f3da72 100644 --- a/contracts/incentives/tests/test_admin.rs +++ b/contracts/incentives/tests/tests/test_admin.rs @@ -10,9 +10,7 @@ use mars_owner::OwnerError::NotOwner; use mars_red_bank_types::incentives::{ConfigResponse, ExecuteMsg, InstantiateMsg, QueryMsg}; use mars_testing::mock_dependencies; -use crate::helpers::{th_query, th_setup}; - -mod helpers; +use super::helpers::{th_query, th_setup}; #[test] fn proper_initialization() { @@ -34,6 +32,9 @@ fn proper_initialization() { assert_eq!(config.owner, Some("owner".to_string())); assert_eq!(config.proposed_new_owner, None); assert_eq!(config.address_provider, "address_provider".to_string()); + assert_eq!(config.epoch_duration, 604800); + assert_eq!(config.max_whitelisted_denoms, 10); + assert_eq!(config.whitelist_count, 0); } #[test] @@ -89,5 +90,7 @@ fn update_config() { assert_eq!(new_config.owner, Some("owner".to_string())); assert_eq!(new_config.proposed_new_owner, None); assert_eq!(new_config.address_provider, Addr::unchecked("new_addr_provider")); + assert_eq!(new_config.epoch_duration, 604800); + assert_eq!(new_config.whitelist_count, 0); assert_eq!(new_config.max_whitelisted_denoms, 20); } diff --git a/contracts/incentives/tests/test_balance_change.rs b/contracts/incentives/tests/tests/test_balance_change.rs similarity index 77% rename from contracts/incentives/tests/test_balance_change.rs rename to contracts/incentives/tests/tests/test_balance_change.rs index 96216f174..e68a68458 100644 --- a/contracts/incentives/tests/test_balance_change.rs +++ b/contracts/incentives/tests/tests/test_balance_change.rs @@ -15,9 +15,7 @@ use mars_red_bank_types::{ }; use mars_testing::MockEnvParams; -use crate::helpers::{th_setup, ths_setup_with_epoch_duration}; - -mod helpers; +use super::helpers::{th_setup, ths_setup_with_epoch_duration}; #[test] fn balance_change_unauthorized() { @@ -30,6 +28,7 @@ fn balance_change_unauthorized() { mock_info("jake", &[]), // not Red Bank ExecuteMsg::BalanceChange { user_addr: Addr::unchecked("user"), + account_id: None, denom: "uosmo".to_string(), user_amount_scaled_before: Uint128::new(100000), total_amount_scaled_before: Uint128::new(100000), @@ -47,6 +46,7 @@ fn execute_balance_change_noops() { let info = mock_info("red_bank", &[]); let msg = ExecuteMsg::BalanceChange { user_addr: Addr::unchecked("user"), + account_id: None, denom: "uosmo".to_string(), user_amount_scaled_before: Uint128::new(100000), total_amount_scaled_before: Uint128::new(100000), @@ -93,6 +93,7 @@ fn balance_change_zero_emission() { }); let msg = ExecuteMsg::BalanceChange { user_addr: Addr::unchecked("user"), + account_id: None, denom: "uosmo".to_string(), user_amount_scaled_before: Uint128::new(100_000), total_amount_scaled_before: Uint128::new(100_000), @@ -106,7 +107,7 @@ fn balance_change_zero_emission() { assert_eq!( res.events[0].attributes, - vec![attr("action", "balance_change"), attr("denom", denom), attr("user", "user"),] + vec![attr("action", "balance_change"), attr("denom", denom), attr("user", "user")] ); assert_eq!( res.events[1].attributes, @@ -124,12 +125,13 @@ fn balance_change_zero_emission() { // user index is set to asset's index let user_asset_index = - USER_ASSET_INDICES.load(deps.as_ref().storage, (&user_addr, denom, "umars")).unwrap(); + USER_ASSET_INDICES.load(deps.as_ref().storage, ((&user_addr, ""), denom, "umars")).unwrap(); assert_eq!(user_asset_index, asset_incentive_index); // rewards get updated - let user_unclaimed_rewards = - USER_UNCLAIMED_REWARDS.load(deps.as_ref().storage, (&user_addr, denom, "umars")).unwrap(); + let user_unclaimed_rewards = USER_UNCLAIMED_REWARDS + .load(deps.as_ref().storage, ((&user_addr, ""), denom, "umars")) + .unwrap(); assert_eq!(user_unclaimed_rewards, expected_accrued_rewards) } @@ -167,6 +169,7 @@ fn balance_change_user_with_zero_balance() { }); let msg = ExecuteMsg::BalanceChange { user_addr: user_addr.clone(), + account_id: None, denom: "uosmo".to_string(), user_amount_scaled_before: Uint128::zero(), total_amount_scaled_before: total_supply, @@ -185,7 +188,7 @@ fn balance_change_user_with_zero_balance() { assert_eq!( res.events[0].attributes, - vec![attr("action", "balance_change"), attr("denom", denom), attr("user", "user"),] + vec![attr("action", "balance_change"), attr("denom", denom), attr("user", "user")] ); assert_eq!( res.events[1].attributes, @@ -203,12 +206,12 @@ fn balance_change_user_with_zero_balance() { // user index is set to asset's index let user_asset_index = - USER_ASSET_INDICES.load(deps.as_ref().storage, (&user_addr, denom, "umars")).unwrap(); + USER_ASSET_INDICES.load(deps.as_ref().storage, ((&user_addr, ""), denom, "umars")).unwrap(); assert_eq!(user_asset_index, expected_index); // no new rewards let user_unclaimed_rewards = USER_UNCLAIMED_REWARDS - .may_load(deps.as_ref().storage, (&user_addr, denom, "umars")) + .may_load(deps.as_ref().storage, ((&user_addr, ""), denom, "umars")) .unwrap(); assert_eq!(user_unclaimed_rewards, None) } @@ -247,6 +250,7 @@ fn with_zero_previous_balance_and_asset_with_zero_index_accumulates_rewards() { }); let msg = ExecuteMsg::BalanceChange { user_addr: user_addr.clone(), + account_id: None, denom: "uosmo".to_string(), user_amount_scaled_before: Uint128::zero(), total_amount_scaled_before: Uint128::zero(), @@ -278,9 +282,16 @@ fn with_zero_previous_balance_and_asset_with_zero_index_accumulates_rewards() { block_time: Timestamp::from_seconds(time_contract_call + 1000), ..Default::default() }); - let rewards_query = - query_user_unclaimed_rewards(deps.as_ref(), env, "user".to_string(), None, None, None) - .unwrap(); + let rewards_query = query_user_unclaimed_rewards( + deps.as_ref(), + env, + "user".to_string(), + None, + None, + None, + None, + ) + .unwrap(); // Rewards that are accrued when no one had deposit in Red Bank are distributed to the first depositor assert_eq!( vec![coin( @@ -351,9 +362,16 @@ fn set_new_asset_incentive_user_non_zero_balance() { ..Default::default() }); - let unclaimed_rewards = - query_user_unclaimed_rewards(deps.as_ref(), env, "user".to_string(), None, None, None) - .unwrap(); + let unclaimed_rewards = query_user_unclaimed_rewards( + deps.as_ref(), + env, + "user".to_string(), + None, + None, + None, + None, + ) + .unwrap(); // 100_000 s * 100 MARS/s * 1/10th of total deposit let expected_unclaimed_rewards = vec![coin(1_000_000, "umars")]; assert_eq!(unclaimed_rewards, expected_unclaimed_rewards); @@ -386,6 +404,7 @@ fn set_new_asset_incentive_user_non_zero_balance() { env, info, user_addr, + None, denom.to_string(), Uint128::new(10_000), total_supply, @@ -402,9 +421,16 @@ fn set_new_asset_incentive_user_non_zero_balance() { ..Default::default() }); - let unclaimed_rewards = - query_user_unclaimed_rewards(deps.as_ref(), env, "user".to_string(), None, None, None) - .unwrap(); + let unclaimed_rewards = query_user_unclaimed_rewards( + deps.as_ref(), + env, + "user".to_string(), + None, + None, + None, + None, + ) + .unwrap(); let expected_unclaimed_rewards = vec![coin( // 200_000 s * 100 MARS/s * 1/10th of total deposit + 2_000_000 + @@ -461,6 +487,7 @@ fn balance_change_user_non_zero_balance() { }); let msg = ExecuteMsg::BalanceChange { user_addr: user_addr.clone(), + account_id: None, denom: "uosmo".to_string(), user_amount_scaled_before: user_balance, total_amount_scaled_before: total_supply, @@ -484,7 +511,7 @@ fn balance_change_user_non_zero_balance() { .unwrap(); assert_eq!( res.events[0].attributes, - vec![attr("action", "balance_change"), attr("denom", denom), attr("user", "user"),] + vec![attr("action", "balance_change"), attr("denom", denom), attr("user", "user")] ); assert_eq!( res.events[1].attributes, @@ -504,13 +531,14 @@ fn balance_change_user_non_zero_balance() { assert_eq!(asset_incentive.last_updated, expected_time_last_updated); // user index is set to asset's index - let user_asset_index = - USER_ASSET_INDICES.load(deps.as_ref().storage, (&user_addr, denom, "umars")).unwrap(); + let user_asset_index = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_addr, ""), denom, "umars")) + .unwrap(); assert_eq!(user_asset_index, expected_asset_incentive_index); // user gets new rewards let user_unclaimed_rewards = USER_UNCLAIMED_REWARDS - .load(deps.as_ref().storage, (&user_addr, denom, "umars")) + .load(deps.as_ref().storage, ((&user_addr, ""), denom, "umars")) .unwrap(); expected_accumulated_rewards += expected_accrued_rewards; assert_eq!(user_unclaimed_rewards, expected_accumulated_rewards) @@ -527,6 +555,7 @@ fn balance_change_user_non_zero_balance() { }); let msg = ExecuteMsg::BalanceChange { user_addr: user_addr.clone(), + account_id: None, denom: "uosmo".to_string(), user_amount_scaled_before: user_balance, total_amount_scaled_before: total_supply, @@ -551,7 +580,7 @@ fn balance_change_user_non_zero_balance() { .unwrap(); assert_eq!( res.events[0].attributes, - vec![attr("action", "balance_change"), attr("denom", denom), attr("user", "user"),] + vec![attr("action", "balance_change"), attr("denom", denom), attr("user", "user")] ); assert_eq!( res.events[1].attributes, @@ -571,13 +600,14 @@ fn balance_change_user_non_zero_balance() { assert_eq!(asset_incentive.last_updated, expected_time_last_updated); // user index is set to asset's index - let user_asset_index = - USER_ASSET_INDICES.load(deps.as_ref().storage, (&user_addr, denom, "umars")).unwrap(); + let user_asset_index = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_addr, ""), denom, "umars")) + .unwrap(); assert_eq!(user_asset_index, expected_asset_incentive_index); // user gets new rewards let user_unclaimed_rewards = USER_UNCLAIMED_REWARDS - .load(deps.as_ref().storage, (&user_addr, denom, "umars")) + .load(deps.as_ref().storage, ((&user_addr, ""), denom, "umars")) .unwrap(); expected_accumulated_rewards += expected_accrued_rewards; assert_eq!(user_unclaimed_rewards, expected_accumulated_rewards) @@ -594,6 +624,7 @@ fn balance_change_user_non_zero_balance() { }); let msg = ExecuteMsg::BalanceChange { user_addr: user_addr.clone(), + account_id: None, denom: "uosmo".to_string(), user_amount_scaled_before: user_balance, total_amount_scaled_before: total_supply, @@ -602,7 +633,7 @@ fn balance_change_user_non_zero_balance() { assert_eq!( res.events[0].attributes, - vec![attr("action", "balance_change"), attr("denom", denom), attr("user", "user"),] + vec![attr("action", "balance_change"), attr("denom", denom), attr("user", "user")] ); assert_eq!( res.events[1].attributes, @@ -620,14 +651,117 @@ fn balance_change_user_non_zero_balance() { assert_eq!(asset_incentive.last_updated, expected_time_last_updated); // user index is still the same - let user_asset_index = - USER_ASSET_INDICES.load(deps.as_ref().storage, (&user_addr, denom, "umars")).unwrap(); + let user_asset_index = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_addr, ""), denom, "umars")) + .unwrap(); assert_eq!(user_asset_index, expected_asset_incentive_index); // user gets no new rewards let user_unclaimed_rewards = USER_UNCLAIMED_REWARDS - .load(deps.as_ref().storage, (&user_addr, denom, "umars")) + .load(deps.as_ref().storage, ((&user_addr, ""), denom, "umars")) .unwrap(); assert_eq!(user_unclaimed_rewards, expected_accumulated_rewards) } } + +#[test] +fn balance_change_for_credit_account_id_with_non_zero_balance() { + let env = mock_env(); + let mut deps = ths_setup_with_epoch_duration(env, 8640000); + let denom = "uosmo"; + let user_addr = Addr::unchecked("credit_manager"); + let account_id = "random_account_id"; + + let emission_per_second = Uint128::new(100); + let total_supply = Uint128::new(100_000); + + let mut expected_asset_incentive_index = Decimal::from_ratio(1_u128, 2_u128); + let mut expected_time_last_updated = 500_000_u64; + let mut expected_accumulated_rewards = Uint128::zero(); + + INCENTIVE_STATES + .save( + deps.as_mut().storage, + (denom, "umars"), + &IncentiveState { + index: expected_asset_incentive_index, + last_updated: expected_time_last_updated, + }, + ) + .unwrap(); + EMISSIONS + .save( + deps.as_mut().storage, + (denom, "umars", expected_time_last_updated), + &emission_per_second, + ) + .unwrap(); + + let info = mock_info("red_bank", &[]); + + let time_contract_call = 600_000_u64; + let user_balance = Uint128::new(10_000); + + let env = mars_testing::mock_env(MockEnvParams { + block_time: Timestamp::from_seconds(time_contract_call), + ..Default::default() + }); + let msg = ExecuteMsg::BalanceChange { + user_addr: user_addr.clone(), + account_id: Some(account_id.to_string()), + denom: "uosmo".to_string(), + user_amount_scaled_before: user_balance, + total_amount_scaled_before: total_supply, + }; + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + + expected_asset_incentive_index = compute_incentive_index( + expected_asset_incentive_index, + emission_per_second, + total_supply, + expected_time_last_updated, + time_contract_call, + ) + .unwrap(); + + let expected_accrued_rewards = + compute_user_accrued_rewards(user_balance, Decimal::zero(), expected_asset_incentive_index) + .unwrap(); + assert_eq!( + res.events[0].attributes, + vec![ + attr("action", "balance_change"), + attr("denom", denom), + attr("user", "credit_manager"), + attr("account_id", account_id) + ] + ); + assert_eq!( + res.events[1].attributes, + vec![ + attr("incentive_denom", "umars"), + attr("rewards_accrued", expected_accrued_rewards), + attr("asset_index", expected_asset_incentive_index.to_string()) + ] + ); + + // asset incentive gets updated + expected_time_last_updated = time_contract_call; + + let asset_incentive = INCENTIVE_STATES.load(deps.as_ref().storage, (denom, "umars")).unwrap(); + assert_eq!(asset_incentive.index, expected_asset_incentive_index); + assert_eq!(asset_incentive.last_updated, expected_time_last_updated); + + // user index is set to asset's index + let user_asset_index = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_addr, account_id), denom, "umars")) + .unwrap(); + assert_eq!(user_asset_index, expected_asset_incentive_index); + + // user gets new rewards + let user_unclaimed_rewards = USER_UNCLAIMED_REWARDS + .load(deps.as_ref().storage, ((&user_addr, account_id), denom, "umars")) + .unwrap(); + expected_accumulated_rewards += expected_accrued_rewards; + assert_eq!(user_unclaimed_rewards, expected_accumulated_rewards) +} diff --git a/contracts/incentives/tests/test_claim_rewards.rs b/contracts/incentives/tests/tests/test_claim_rewards.rs similarity index 90% rename from contracts/incentives/tests/test_claim_rewards.rs rename to contracts/incentives/tests/tests/test_claim_rewards.rs index e2162dfa6..bb090bde1 100644 --- a/contracts/incentives/tests/test_claim_rewards.rs +++ b/contracts/incentives/tests/tests/test_claim_rewards.rs @@ -14,9 +14,7 @@ use mars_red_bank_types::{ }; use mars_testing::MockEnvParams; -use crate::helpers::{th_setup, ths_setup_with_epoch_duration}; - -mod helpers; +use super::helpers::{th_setup, ths_setup_with_epoch_duration}; #[test] fn execute_claim_rewards() { @@ -133,13 +131,13 @@ fn execute_claim_rewards() { // user indices USER_ASSET_INDICES - .save(deps.as_mut().storage, (&user_addr, asset_denom, "umars"), &Decimal::one()) + .save(deps.as_mut().storage, ((&user_addr, ""), asset_denom, "umars"), &Decimal::one()) .unwrap(); USER_ASSET_INDICES .save( deps.as_mut().storage, - (&user_addr, zero_denom, "umars"), + ((&user_addr, ""), zero_denom, "umars"), &Decimal::from_ratio(1_u128, 2_u128), ) .unwrap(); @@ -148,7 +146,7 @@ fn execute_claim_rewards() { USER_UNCLAIMED_REWARDS .save( deps.as_mut().storage, - (&user_addr, asset_denom, "umars"), + ((&user_addr, ""), asset_denom, "umars"), &previous_unclaimed_rewards, ) .unwrap(); @@ -186,6 +184,7 @@ fn execute_claim_rewards() { ..Default::default() }); let msg = ExecuteMsg::ClaimRewards { + account_id: None, start_after_collateral_denom: None, start_after_incentive_denom: None, limit: None, @@ -203,6 +202,7 @@ fn execute_claim_rewards() { None, None, None, + None, ) .unwrap(); assert!(rewards_query_before.len() == 1); @@ -216,6 +216,7 @@ fn execute_claim_rewards() { None, None, None, + None, ) .unwrap(); assert_eq!(rewards_query[0].amount, expected_accrued_rewards); @@ -227,9 +228,16 @@ fn execute_claim_rewards() { // NOTE: the query should return an empty array, instead of a non-empty array // with a zero-amount coin! the latter is considered an invalid coins array // and will result in error. - let rewards_query_after = - query_user_unclaimed_rewards(deps.as_ref(), env, String::from("user"), None, None, None) - .unwrap(); + let rewards_query_after = query_user_unclaimed_rewards( + deps.as_ref(), + env, + String::from("user"), + None, + None, + None, + None, + ) + .unwrap(); assert!(rewards_query_after.is_empty()); // ASSERT @@ -267,23 +275,25 @@ fn execute_claim_rewards() { assert_eq!(no_user_incentive.last_updated, time_start); // user's asset and zero indices are updated - let user_asset_index = - USER_ASSET_INDICES.load(deps.as_ref().storage, (&user_addr, asset_denom, "umars")).unwrap(); + let user_asset_index = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_addr, ""), asset_denom, "umars")) + .unwrap(); assert_eq!(user_asset_index, expected_asset_incentive_index); - let user_zero_index = - USER_ASSET_INDICES.load(deps.as_ref().storage, (&user_addr, zero_denom, "umars")).unwrap(); + let user_zero_index = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_addr, ""), zero_denom, "umars")) + .unwrap(); assert_eq!(user_zero_index, Decimal::one()); // user's no_user does not get updated let user_no_user_index = USER_ASSET_INDICES - .may_load(deps.as_ref().storage, (&user_addr, no_user_denom, "umars")) + .may_load(deps.as_ref().storage, ((&user_addr, ""), no_user_denom, "umars")) .unwrap(); assert_eq!(user_no_user_index, None); // user rewards are cleared let user_unclaimed_rewards = USER_UNCLAIMED_REWARDS - .load(deps.as_ref().storage, (&user_addr, asset_denom, "umars")) + .load(deps.as_ref().storage, ((&user_addr, ""), asset_denom, "umars")) .unwrap(); assert_eq!(user_unclaimed_rewards, Uint128::zero()) } @@ -295,6 +305,7 @@ fn claim_zero_rewards() { let info = mock_info("user", &[]); let msg = ExecuteMsg::ClaimRewards { + account_id: None, start_after_collateral_denom: None, start_after_incentive_denom: None, limit: None, @@ -304,6 +315,6 @@ fn claim_zero_rewards() { assert_eq!(res.messages.len(), 0); assert_eq!( res.events[0].attributes, - vec![attr("action", "claim_rewards"), attr("user", "user"),] + vec![attr("action", "claim_rewards"), attr("user", "user")] ); } diff --git a/contracts/incentives/tests/test_indices_usage.rs b/contracts/incentives/tests/tests/test_indices_usage.rs similarity index 99% rename from contracts/incentives/tests/test_indices_usage.rs rename to contracts/incentives/tests/tests/test_indices_usage.rs index 78a2ce8e0..e57ae7026 100644 --- a/contracts/incentives/tests/test_indices_usage.rs +++ b/contracts/incentives/tests/tests/test_indices_usage.rs @@ -11,8 +11,6 @@ use mars_incentives::{ }; use mars_red_bank_types::incentives::{Config, IncentiveState}; -mod helpers; - fn store_config_with_epoch_duration(storage: &mut dyn Storage, epoch_duration: u64) { CONFIG .save( diff --git a/contracts/incentives/tests/tests/test_migration_v2.rs b/contracts/incentives/tests/tests/test_migration_v2.rs new file mode 100644 index 000000000..463ca2529 --- /dev/null +++ b/contracts/incentives/tests/tests/test_migration_v2.rs @@ -0,0 +1,401 @@ +use std::collections::HashMap; + +use cosmwasm_std::{ + attr, testing::mock_env, Addr, Decimal, Event, Order, StdResult, Timestamp, Uint128, +}; +use cw2::VersionError; +use mars_incentives::{ + contract::migrate, + migrations::v2_0_0::v1_state::{self, OwnerSetNoneProposed}, + state::{ + CONFIG, INCENTIVE_STATES, OWNER, USER_ASSET_INDICES, USER_UNCLAIMED_REWARDS, WHITELIST, + WHITELIST_COUNT, + }, + ContractError, +}; +use mars_red_bank_types::{ + incentives::{Config, IncentiveState, MigrateMsg, V2Updates}, + red_bank::{Market, UserCollateralResponse}, +}; +use mars_testing::{mock_dependencies, MockEnvParams}; + +#[test] +fn wrong_contract_name() { + let mut deps = mock_dependencies(&[]); + cw2::set_contract_version(deps.as_mut().storage, "contract_xyz", "1.0.0").unwrap(); + + let err = migrate( + deps.as_mut(), + mock_env(), + MigrateMsg::V1_0_0ToV2_0_0(V2Updates { + epoch_duration: 604800, + max_whitelisted_denoms: 10, + }), + ) + .unwrap_err(); + + assert_eq!( + err, + ContractError::Version(VersionError::WrongContract { + expected: "crates.io:mars-incentives".to_string(), + found: "contract_xyz".to_string() + }) + ); +} + +#[test] +fn wrong_contract_version() { + let mut deps = mock_dependencies(&[]); + cw2::set_contract_version(deps.as_mut().storage, "crates.io:mars-incentives", "4.1.0").unwrap(); + + let err = migrate( + deps.as_mut(), + mock_env(), + MigrateMsg::V1_0_0ToV2_0_0(V2Updates { + epoch_duration: 604800, + max_whitelisted_denoms: 10, + }), + ) + .unwrap_err(); + + assert_eq!( + err, + ContractError::Version(VersionError::WrongVersion { + expected: "1.0.0".to_string(), + found: "4.1.0".to_string() + }) + ); +} + +#[test] +fn successful_migration() { + let mut deps = mock_dependencies(&[]); + cw2::set_contract_version(deps.as_mut().storage, "crates.io:mars-incentives", "1.0.0").unwrap(); + + let old_owner = "spiderman_246"; + v1_state::OWNER + .save( + deps.as_mut().storage, + &v1_state::OwnerState::B(OwnerSetNoneProposed { + owner: Addr::unchecked(old_owner), + }), + ) + .unwrap(); + + let mars_denom = "umars"; + let old_config = mars_red_bank_types_old::incentives::Config { + address_provider: Addr::unchecked("address_provider"), + mars_denom: mars_denom.to_string(), + }; + v1_state::CONFIG.save(deps.as_mut().storage, &old_config).unwrap(); + + let atom_denom = "uatom"; + let usdc_denom = "uusdc"; + let osmo_denom = "uosmo"; + + let incentive_start_time = 500_000u64; + let duration = 864_000u64; // 10 days + let migration_time = incentive_start_time + duration + 100u64; + + // The incentive will have to be recalculated for the entire duration + let atom_incentive = mars_red_bank_types_old::incentives::AssetIncentive { + emission_per_second: Uint128::new(100), + start_time: incentive_start_time, + duration, + index: Decimal::one(), + last_updated: incentive_start_time, + }; + v1_state::ASSET_INCENTIVES.save(deps.as_mut().storage, atom_denom, &atom_incentive).unwrap(); + + // The incentive will have to be recalculated for the part of the duration + let usdc_incentive = mars_red_bank_types_old::incentives::AssetIncentive { + emission_per_second: Uint128::new(50), + start_time: incentive_start_time, + duration, + index: Decimal::from_ratio(12u128, 10u128), + last_updated: incentive_start_time + 86400u64, // + 1 day + }; + v1_state::ASSET_INCENTIVES.save(deps.as_mut().storage, usdc_denom, &usdc_incentive).unwrap(); + + // The incentive won't be recalculated because it finished before migration time + let osmo_incentive = mars_red_bank_types_old::incentives::AssetIncentive { + emission_per_second: Uint128::new(50), + start_time: incentive_start_time, + duration, + index: Decimal::from_ratio(15u128, 10u128), + last_updated: migration_time - 10u64, + }; + v1_state::ASSET_INCENTIVES.save(deps.as_mut().storage, osmo_denom, &osmo_incentive).unwrap(); + + // Set user asset indices for all incentive assets + let user_1 = Addr::unchecked("user_1"); + let user_1_atom_idx_old = Decimal::one(); + v1_state::USER_ASSET_INDICES + .save(deps.as_mut().storage, (&user_1, atom_denom), &user_1_atom_idx_old) + .unwrap(); + let user_1_usdc_idx_old = Decimal::one(); + v1_state::USER_ASSET_INDICES + .save(deps.as_mut().storage, (&user_1, usdc_denom), &user_1_usdc_idx_old) + .unwrap(); + let user_1_osmo_idx_old = Decimal::one(); + v1_state::USER_ASSET_INDICES + .save(deps.as_mut().storage, (&user_1, osmo_denom), &user_1_osmo_idx_old) + .unwrap(); + + // Set user asset indices only for osmo. Index is up to date with asset incentive index. No rewards accured. + let user_2 = Addr::unchecked("user_2"); + let user_2_osmo_idx_old = osmo_incentive.index; + v1_state::USER_ASSET_INDICES + .save(deps.as_mut().storage, (&user_2, osmo_denom), &user_2_osmo_idx_old) + .unwrap(); + + // Set user asset indices only for atom + let user_3 = Addr::unchecked("user_3"); + let user_3_atom_idx_old = Decimal::one(); + v1_state::USER_ASSET_INDICES + .save(deps.as_mut().storage, (&user_3, atom_denom), &user_3_atom_idx_old) + .unwrap(); + + // Set unclaimed rewards only for user_1. + // user_2 doesn't accrue any new rewards because osmo incentive finished before migration time. + // user_3 not set in order to check if new state creation works for him. + let user_1_unclaimed_rewards = Uint128::new(1000); + v1_state::USER_UNCLAIMED_REWARDS + .save(deps.as_mut().storage, &user_1, &user_1_unclaimed_rewards) + .unwrap(); + + // Setup markets + let atom_collateral_total_scaled = Uint128::new(100_000_000); + deps.querier.set_redbank_market(create_market(atom_denom, atom_collateral_total_scaled)); + let usdc_collateral_total_scaled = Uint128::new(1_250_000_000); + deps.querier.set_redbank_market(create_market(usdc_denom, usdc_collateral_total_scaled)); + let osmo_collateral_total_scaled = Uint128::new(520_000_000); + deps.querier.set_redbank_market(create_market(osmo_denom, osmo_collateral_total_scaled)); + + // Setup atom collaterals. Sum of all positions should be equal to atom_collateral_total_scaled. + let user_1_atom_amount_scaled = Uint128::zero(); // Setting zero to check if user_1 index is updated correctly + deps.querier.set_red_bank_user_collateral( + &user_1, + create_user_collateral(atom_denom, user_1_atom_amount_scaled), + ); + let user_3_atom_amount_scaled = atom_collateral_total_scaled; + deps.querier.set_red_bank_user_collateral( + &user_3, + create_user_collateral(atom_denom, user_3_atom_amount_scaled), + ); + + // Setup usdc collaterals. Sum of all positions should be equal to usdc_collateral_total_scaled + let user_1_usdc_amount_scaled = usdc_collateral_total_scaled; + deps.querier.set_red_bank_user_collateral( + &user_1, + create_user_collateral(usdc_denom, user_1_usdc_amount_scaled), + ); + + // Setup osmo collaterals. Sum of all positions should be equal to osmo_collateral_total_scaled + let user_1_osmo_amount_scaled = Uint128::new(120_000_000); + deps.querier.set_red_bank_user_collateral( + &user_1, + create_user_collateral(osmo_denom, user_1_osmo_amount_scaled), + ); + let user_2_osmo_amount_scaled = Uint128::new(400_000_000); + deps.querier.set_red_bank_user_collateral( + &user_2, + create_user_collateral(osmo_denom, user_2_osmo_amount_scaled), + ); + + let env = mars_testing::mock_env(MockEnvParams { + block_time: Timestamp::from_seconds(migration_time), + ..Default::default() + }); + + let epoch_duration = 604800; + let max_whitelisted_denoms = 12; + let res = migrate( + deps.as_mut(), + env, + MigrateMsg::V1_0_0ToV2_0_0(V2Updates { + epoch_duration, + max_whitelisted_denoms, + }), + ) + .unwrap(); + + assert_eq!(res.messages, vec![]); + assert_eq!(res.events, vec![] as Vec); + assert!(res.data.is_none()); + assert_eq!( + res.attributes, + vec![attr("action", "migrate"), attr("from_version", "1.0.0"), attr("to_version", "2.0.0")] + ); + + let o = OWNER.query(deps.as_ref().storage).unwrap(); + assert_eq!(old_owner.to_string(), o.owner.unwrap()); + assert!(o.proposed.is_none()); + assert!(o.initialized); + assert!(!o.abolished); + assert!(o.emergency_owner.is_none()); + + let new_config = CONFIG.load(deps.as_ref().storage).unwrap(); + assert_eq!( + new_config, + Config { + address_provider: old_config.address_provider, + max_whitelisted_denoms + } + ); + + let whitelist_count = WHITELIST_COUNT.load(deps.as_ref().storage).unwrap(); + assert_eq!(whitelist_count, 1); + let whitelist = WHITELIST + .range(deps.as_ref().storage, None, None, Order::Ascending) + .collect::>>() + .unwrap(); + assert_eq!(whitelist.len(), 1); + assert_eq!(whitelist.get("umars").unwrap(), &Uint128::one()); + + // Update asset incentive indices and check if indices changed + let mut new_atom_incentive = atom_incentive.clone(); + v1_state::helpers::update_asset_incentive_index( + &mut new_atom_incentive, + atom_collateral_total_scaled, + migration_time, + ) + .unwrap(); + assert_ne!(atom_incentive.index, new_atom_incentive.index); + let mut new_usdc_incentive = usdc_incentive.clone(); + v1_state::helpers::update_asset_incentive_index( + &mut new_usdc_incentive, + usdc_collateral_total_scaled, + migration_time, + ) + .unwrap(); + assert_ne!(usdc_incentive.index, new_usdc_incentive.index); + let mut new_osmo_incentive = osmo_incentive.clone(); + v1_state::helpers::update_asset_incentive_index( + &mut new_osmo_incentive, + osmo_collateral_total_scaled, + migration_time, + ) + .unwrap(); + assert_eq!(osmo_incentive.index, new_osmo_incentive.index); // should be equal because last_updated is after incentive end time + + // Check if incentive states are updated correctly + let incentive_states = INCENTIVE_STATES + .range(deps.as_ref().storage, None, None, Order::Ascending) + .collect::>>() + .unwrap(); + assert_eq!(incentive_states.len(), 3); + assert_eq!( + incentive_states.get(&(atom_denom.to_string(), mars_denom.to_string())).unwrap(), + &IncentiveState { + index: new_atom_incentive.index, + last_updated: migration_time + } + ); + assert_eq!( + incentive_states.get(&(usdc_denom.to_string(), mars_denom.to_string())).unwrap(), + &IncentiveState { + index: new_usdc_incentive.index, + last_updated: migration_time + } + ); + assert_eq!( + incentive_states.get(&(osmo_denom.to_string(), mars_denom.to_string())).unwrap(), + &IncentiveState { + index: new_osmo_incentive.index, + last_updated: migration_time + } + ); + + // Check if user asset indices are updated correctly + let user_1_atom_idx = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_1, ""), atom_denom, mars_denom)) + .unwrap(); + assert_eq!(user_1_atom_idx, new_atom_incentive.index); + let user_1_usdc_idx = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_1, ""), usdc_denom, mars_denom)) + .unwrap(); + assert_eq!(user_1_usdc_idx, new_usdc_incentive.index); + let user_1_osmo_idx = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_1, ""), osmo_denom, mars_denom)) + .unwrap(); + assert_eq!(user_1_osmo_idx, new_osmo_incentive.index); + + let user_2_osmo_idx = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_2, ""), osmo_denom, mars_denom)) + .unwrap(); + assert_eq!(user_2_osmo_idx, new_osmo_incentive.index); + + let user_3_atom_idx = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_3, ""), atom_denom, mars_denom)) + .unwrap(); + assert_eq!(user_3_atom_idx, new_atom_incentive.index); + + // Check if user unclaimed rewards are migrated correctly + let user_1_atom_rewards = v1_state::helpers::compute_user_accrued_rewards( + user_1_atom_amount_scaled, + user_1_atom_idx_old, + new_atom_incentive.index, + ) + .unwrap(); + let user_1_atom_rewards_migrated = USER_UNCLAIMED_REWARDS + .load(deps.as_ref().storage, ((&user_1, ""), atom_denom, mars_denom)) + .unwrap(); + assert_eq!(user_1_atom_rewards_migrated, user_1_unclaimed_rewards + user_1_atom_rewards); + let user_1_usdc_rewards = v1_state::helpers::compute_user_accrued_rewards( + user_1_usdc_amount_scaled, + user_1_usdc_idx_old, + new_usdc_incentive.index, + ) + .unwrap(); + let user_1_usdc_rewards_migrated = USER_UNCLAIMED_REWARDS + .load(deps.as_ref().storage, ((&user_1, ""), usdc_denom, mars_denom)) + .unwrap(); + assert_eq!(user_1_usdc_rewards_migrated, user_1_usdc_rewards); + let user_1_osmo_rewards = v1_state::helpers::compute_user_accrued_rewards( + user_1_osmo_amount_scaled, + user_1_osmo_idx_old, + new_osmo_incentive.index, + ) + .unwrap(); + let user_1_osmo_rewards_migrated = USER_UNCLAIMED_REWARDS + .load(deps.as_ref().storage, ((&user_1, ""), osmo_denom, mars_denom)) + .unwrap(); + assert_eq!(user_1_osmo_rewards_migrated, user_1_osmo_rewards); + + let user_2_osmo_rewards = v1_state::helpers::compute_user_accrued_rewards( + user_2_osmo_amount_scaled, + user_2_osmo_idx_old, + new_osmo_incentive.index, + ) + .unwrap(); + assert_eq!(user_2_osmo_rewards, Uint128::zero()); + + let user_3_atom_rewards = v1_state::helpers::compute_user_accrued_rewards( + user_3_atom_amount_scaled, + user_3_atom_idx_old, + new_atom_incentive.index, + ) + .unwrap(); + let user_3_atom_rewards_migrated = USER_UNCLAIMED_REWARDS + .load(deps.as_ref().storage, ((&user_3, ""), atom_denom, mars_denom)) + .unwrap(); + assert_eq!(user_3_atom_rewards_migrated, user_3_atom_rewards); +} + +fn create_market(denom: &str, scaled_amt: Uint128) -> Market { + Market { + denom: denom.to_string(), + collateral_total_scaled: scaled_amt, + ..Default::default() + } +} + +fn create_user_collateral(denom: &str, scaled_amt: Uint128) -> UserCollateralResponse { + UserCollateralResponse { + denom: denom.to_string(), + amount_scaled: scaled_amt, + amount: Uint128::zero(), // doesn't matter for this test + enabled: true, + } +} diff --git a/contracts/incentives/tests/test_quering.rs b/contracts/incentives/tests/tests/test_quering.rs similarity index 92% rename from contracts/incentives/tests/test_quering.rs rename to contracts/incentives/tests/tests/test_quering.rs index c041f5a21..ba4a12f5c 100644 --- a/contracts/incentives/tests/test_quering.rs +++ b/contracts/incentives/tests/tests/test_quering.rs @@ -6,9 +6,7 @@ use mars_red_bank_types::incentives::{ use mars_testing::{mock_env, MockEnvParams}; use test_case::test_case; -use crate::helpers::th_setup; - -mod helpers; +use super::helpers::{th_query, th_query_with_env, th_setup}; #[test] fn query_incentive_state() { @@ -31,7 +29,7 @@ fn query_incentive_state() { }; INCENTIVE_STATES.save(deps.as_mut().storage, ("uusdc", "umars"), &uusdc_incentive).unwrap(); - let res: IncentiveStateResponse = helpers::th_query( + let res: IncentiveStateResponse = th_query( deps.as_ref(), QueryMsg::IncentiveState { collateral_denom: "uatom".to_string(), @@ -66,7 +64,7 @@ fn query_incentive_states() { INCENTIVE_STATES.save(deps.as_mut().storage, ("uusdc", "umars"), &uusdc_incentive).unwrap(); // NOTE: responses are ordered alphabetically by denom - let res: Vec = helpers::th_query( + let res: Vec = th_query( deps.as_ref(), QueryMsg::IncentiveStates { start_after_collateral_denom: None, @@ -88,7 +86,7 @@ fn query_incentive_states() { ); // NOTE: responses are ordered alphabetically by denom - let res: Vec = helpers::th_query( + let res: Vec = th_query( deps.as_ref(), QueryMsg::IncentiveStates { start_after_collateral_denom: Some("uatom".to_string()), @@ -116,7 +114,7 @@ fn query_emission() { .unwrap(); // Query before emission start - let res: Uint128 = helpers::th_query( + let res: Uint128 = th_query( deps.as_ref(), QueryMsg::Emission { collateral_denom: "uosmo".to_string(), @@ -127,7 +125,7 @@ fn query_emission() { assert_eq!(res, Uint128::zero()); // Query at timestamp of first emission start - let res: Uint128 = helpers::th_query( + let res: Uint128 = th_query( deps.as_ref(), QueryMsg::Emission { collateral_denom: "uosmo".to_string(), @@ -138,7 +136,7 @@ fn query_emission() { assert_eq!(res, Uint128::new(100)); // Query at timestamp of second emission start - let res: Uint128 = helpers::th_query( + let res: Uint128 = th_query( deps.as_ref(), QueryMsg::Emission { collateral_denom: "uosmo".to_string(), @@ -149,7 +147,7 @@ fn query_emission() { assert_eq!(res, Uint128::new(50)); // Query one second before second emission start - let res: Uint128 = helpers::th_query( + let res: Uint128 = th_query( deps.as_ref(), QueryMsg::Emission { collateral_denom: "uosmo".to_string(), @@ -160,7 +158,7 @@ fn query_emission() { assert_eq!(res, Uint128::new(100)); // Query at timestamp some time into second emission start - let res: Uint128 = helpers::th_query( + let res: Uint128 = th_query( deps.as_ref(), QueryMsg::Emission { collateral_denom: "uosmo".to_string(), @@ -171,7 +169,7 @@ fn query_emission() { assert_eq!(res, Uint128::new(50)); // Query the second before emission end - let res: Uint128 = helpers::th_query( + let res: Uint128 = th_query( deps.as_ref(), QueryMsg::Emission { collateral_denom: "uosmo".to_string(), @@ -182,7 +180,7 @@ fn query_emission() { assert_eq!(res, Uint128::new(50)); // Query the second after emission end - let res: Uint128 = helpers::th_query( + let res: Uint128 = th_query( deps.as_ref(), QueryMsg::Emission { collateral_denom: "uosmo".to_string(), @@ -203,7 +201,7 @@ fn query_emissions() { .save(deps.as_mut().storage, ("uusdc", "umars", 604800 * 2), &Uint128::new(50)) .unwrap(); - let res: Vec = helpers::th_query( + let res: Vec = th_query( deps.as_ref(), QueryMsg::Emissions { collateral_denom: "uusdc".to_string(), @@ -221,7 +219,7 @@ fn query_emissions() { ] ); - let res: Vec = helpers::th_query( + let res: Vec = th_query( deps.as_ref(), QueryMsg::Emissions { collateral_denom: "uusdc".to_string(), @@ -238,7 +236,7 @@ fn query_emissions() { ] ); - let res: Vec = helpers::th_query( + let res: Vec = th_query( deps.as_ref(), QueryMsg::Emissions { collateral_denom: "uusdc".to_string(), @@ -297,7 +295,7 @@ fn query_active_emissions(query_at_time: u64) -> Vec<(String, Uint128)> { .save(deps.as_mut().storage, ("uusdc", "uosmo", 604800 * 2), &Uint128::new(100)) .unwrap(); - helpers::th_query_with_env::>( + th_query_with_env::>( deps.as_ref(), mock_env(MockEnvParams { block_time: Timestamp::from_seconds(query_at_time), diff --git a/contracts/incentives/tests/test_set_asset_incentive.rs b/contracts/incentives/tests/tests/test_set_asset_incentive.rs similarity index 99% rename from contracts/incentives/tests/test_set_asset_incentive.rs rename to contracts/incentives/tests/tests/test_set_asset_incentive.rs index a508b1706..bf24c9d07 100644 --- a/contracts/incentives/tests/test_set_asset_incentive.rs +++ b/contracts/incentives/tests/tests/test_set_asset_incentive.rs @@ -12,12 +12,10 @@ use mars_red_bank_types::{incentives::ExecuteMsg, red_bank::Market}; use mars_testing::MockEnvParams; use mars_utils::error::ValidationError; -use crate::helpers::{ +use super::helpers::{ th_setup, th_setup_with_env, th_whitelist_denom, ths_setup_with_epoch_duration, }; -mod helpers; - const ONE_WEEK_IN_SECS: u64 = 604800; #[test] diff --git a/contracts/incentives/tests/test_update_owner.rs b/contracts/incentives/tests/tests/test_update_owner.rs similarity index 97% rename from contracts/incentives/tests/test_update_owner.rs rename to contracts/incentives/tests/tests/test_update_owner.rs index 00b7dc4ba..3989240ad 100644 --- a/contracts/incentives/tests/test_update_owner.rs +++ b/contracts/incentives/tests/tests/test_update_owner.rs @@ -3,9 +3,7 @@ use mars_incentives::{contract::execute, ContractError}; use mars_owner::{OwnerError::NotOwner, OwnerUpdate}; use mars_red_bank_types::incentives::{ConfigResponse, ExecuteMsg, QueryMsg}; -use crate::helpers::{th_query, th_setup}; - -mod helpers; +use super::helpers::{th_query, th_setup}; #[test] fn initialized_state() { diff --git a/contracts/incentives/tests/test_whitelist.rs b/contracts/incentives/tests/tests/test_whitelist.rs similarity index 98% rename from contracts/incentives/tests/test_whitelist.rs rename to contracts/incentives/tests/tests/test_whitelist.rs index 6424c5ab6..0873e14b6 100644 --- a/contracts/incentives/tests/test_whitelist.rs +++ b/contracts/incentives/tests/tests/test_whitelist.rs @@ -10,18 +10,16 @@ use mars_incentives::{ }; use mars_owner::OwnerError::NotOwner; use mars_red_bank_types::{ - incentives::{ExecuteMsg, QueryMsg, WhitelistEntry}, + incentives::{ConfigResponse, ExecuteMsg, QueryMsg, WhitelistEntry}, red_bank::{Market, UserCollateralResponse}, }; use mars_testing::MockEnvParams; use mars_utils::error::ValidationError; -use crate::helpers::{ +use super::helpers::{ th_query, th_query_with_env, th_setup, th_setup_with_env, ths_setup_with_epoch_duration, }; -mod helpers; - #[test] fn initialized_state() { let deps = th_setup(); @@ -210,6 +208,7 @@ fn incentives_updated_and_removed_when_removing_from_whitelist() { env.clone(), mock_info("red_bank", &[]), user_addr.clone(), + None, "uosmo".to_string(), Uint128::zero(), Uint128::zero(), @@ -236,6 +235,7 @@ fn incentives_updated_and_removed_when_removing_from_whitelist() { env.clone(), QueryMsg::UserUnclaimedRewards { user: user_addr.to_string(), + account_id: None, start_after_collateral_denom: None, start_after_incentive_denom: None, limit: None, @@ -255,6 +255,7 @@ fn incentives_updated_and_removed_when_removing_from_whitelist() { env, QueryMsg::UserUnclaimedRewards { user: user_addr.to_string(), + account_id: None, start_after_collateral_denom: None, start_after_incentive_denom: None, limit: None, @@ -355,8 +356,8 @@ fn cannot_whitelist_more_than_max_limit() { execute(deps.as_mut(), mock_env(), mock_info(owner, &[]), add_whitelist_msg).unwrap(); // Check whitelist count. Should still be 10. - let whitelist_count = WHITELIST_COUNT.load(&deps.storage).unwrap(); - assert_eq!(whitelist_count, 10); + let config: ConfigResponse = th_query(deps.as_ref(), QueryMsg::Config {}); + assert_eq!(config.max_whitelisted_denoms, 10); } #[test] diff --git a/contracts/mock-pyth/Cargo.toml b/contracts/mock-pyth/Cargo.toml new file mode 100644 index 000000000..0c4a6fd25 --- /dev/null +++ b/contracts/mock-pyth/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "mars-mock-pyth" +description = "Mocked version of the Pyth oracle contract" +version = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +keywords = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] +doctest = false + +[features] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +library = [] + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +pyth-sdk-cw = { workspace = true } diff --git a/contracts/mock-pyth/src/contract.rs b/contracts/mock-pyth/src/contract.rs new file mode 100644 index 000000000..2817dbc60 --- /dev/null +++ b/contracts/mock-pyth/src/contract.rs @@ -0,0 +1,51 @@ +use cosmwasm_std::{ + entry_point, to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult, +}; +use pyth_sdk_cw::{Price, PriceFeed, PriceFeedResponse, PriceIdentifier, QueryMsg}; + +#[entry_point] +pub fn instantiate( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: Empty, +) -> StdResult { + Ok(Response::default()) +} + +#[entry_point] +pub fn execute(_deps: DepsMut, _env: Env, _info: MessageInfo, _msg: Empty) -> StdResult { + Ok(Response::default()) +} + +#[entry_point] +pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::PriceFeed { + id, + } => to_binary(&mocked_price_feed(deps, id)?), + _ => panic!("Unsupported query!"), + } +} + +fn mocked_price_feed(_deps: Deps, id: PriceIdentifier) -> StdResult { + let price_feed_response = PriceFeedResponse { + price_feed: PriceFeed::new( + id, + Price { + price: 680000, + conf: 510000, + expo: -5, + publish_time: 1571797419, + }, + Price { + price: 681000, + conf: 400000, + expo: -5, + publish_time: 1571797419, + }, + ), + }; + + Ok(price_feed_response) +} diff --git a/contracts/mock-pyth/src/lib.rs b/contracts/mock-pyth/src/lib.rs new file mode 100644 index 000000000..2943dbb50 --- /dev/null +++ b/contracts/mock-pyth/src/lib.rs @@ -0,0 +1 @@ +pub mod contract; diff --git a/contracts/oracle/base/Cargo.toml b/contracts/oracle/base/Cargo.toml index eea829f89..535dcc096 100644 --- a/contracts/oracle/base/Cargo.toml +++ b/contracts/oracle/base/Cargo.toml @@ -12,11 +12,7 @@ keywords = { workspace = true } [lib] doctest = false -[profile.release] -overflow-checks = true - [features] -pyth = ["pyth-sdk-cw"] # for more explicit tests, cargo test --features=backtraces backtraces = ["cosmwasm-std/backtraces"] @@ -25,8 +21,9 @@ cosmwasm-std = { workspace = true } cw2 = { workspace = true } cw-storage-plus = { workspace = true } mars-owner = { workspace = true } +mars-utils = { workspace = true } mars-red-bank-types = { workspace = true } +pyth-sdk-cw = { workspace = true } schemars = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } -pyth-sdk-cw = { workspace = true, optional = true } diff --git a/contracts/oracle/base/src/contract.rs b/contracts/oracle/base/src/contract.rs index 0d6669ecc..b402c913b 100644 --- a/contracts/oracle/base/src/contract.rs +++ b/contracts/oracle/base/src/contract.rs @@ -7,14 +7,12 @@ use cosmwasm_std::{ use cw_storage_plus::{Bound, Item, Map}; use mars_owner::{Owner, OwnerInit::SetInitialOwner, OwnerUpdate}; use mars_red_bank_types::oracle::msg::{ - Config, ConfigResponse, ExecuteMsg, InstantiateMsg, PriceResponse, PriceSourceResponse, - QueryMsg, + ActionKind, Config, ConfigResponse, ExecuteMsg, InstantiateMsg, PriceResponse, + PriceSourceResponse, QueryMsg, }; +use mars_utils::helpers::validate_native_denom; -use crate::{ - error::ContractResult, utils::validate_native_denom, ContractError, PriceSourceChecked, - PriceSourceUnchecked, -}; +use crate::{error::ContractResult, ContractError, PriceSourceChecked, PriceSourceUnchecked}; const DEFAULT_LIMIT: u32 = 10; const MAX_LIMIT: u32 = 30; @@ -126,11 +124,24 @@ where } => to_binary(&self.query_price_sources(deps, start_after, limit)?), QueryMsg::Price { denom, - } => to_binary(&self.query_price(deps, env, denom)?), + kind, + } => to_binary(&self.query_price( + deps, + env, + denom, + kind.unwrap_or(ActionKind::Default), + )?), QueryMsg::Prices { start_after, limit, - } => to_binary(&self.query_prices(deps, env, start_after, limit)?), + kind, + } => to_binary(&self.query_prices( + deps, + env, + start_after, + limit, + kind.unwrap_or(ActionKind::Default), + )?), }; res.map_err(Into::into) } @@ -251,13 +262,26 @@ where .collect() } - fn query_price(&self, deps: Deps, env: Env, denom: String) -> ContractResult { + fn query_price( + &self, + deps: Deps, + env: Env, + denom: String, + kind: ActionKind, + ) -> ContractResult { let cfg = self.config.load(deps.storage)?; let price_source = self.query_price_source(deps, denom.clone())?.price_source; Ok(PriceResponse { - price: price_source.query_price(&deps, &env, &denom, &cfg, &self.price_sources)?, + price: price_source.query_price( + &deps, + &env, + &denom, + &cfg, + &self.price_sources, + kind, + )?, denom, }) } @@ -268,6 +292,7 @@ where env: Env, start_after: Option, limit: Option, + kind: ActionKind, ) -> ContractResult> { let cfg = self.config.load(deps.storage)?; @@ -280,7 +305,14 @@ where .map(|item| { let (k, v) = item?; Ok(PriceResponse { - price: v.query_price(&deps, &env, &k, &cfg, &self.price_sources)?, + price: v.query_price( + &deps, + &env, + &k, + &cfg, + &self.price_sources, + kind.clone(), + )?, denom: k, }) }) diff --git a/contracts/oracle/base/src/error.rs b/contracts/oracle/base/src/error.rs index 8b71779a8..06cf2c50e 100644 --- a/contracts/oracle/base/src/error.rs +++ b/contracts/oracle/base/src/error.rs @@ -3,6 +3,7 @@ use cosmwasm_std::{ DecimalRangeExceeded, DivideByZeroError, OverflowError, StdError, }; use mars_owner::OwnerError; +use mars_utils::error::ValidationError; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -10,12 +11,8 @@ pub enum ContractError { #[error("{0}")] Std(#[from] StdError), - // #[error("{0}")] - // Mars(#[from] MarsError), - #[error("Invalid denom: {reason}")] - InvalidDenom { - reason: String, - }, + #[error("{0}")] + Validation(#[from] ValidationError), #[error("{0}")] Version(#[from] cw2::VersionError), diff --git a/contracts/oracle/base/src/lib.rs b/contracts/oracle/base/src/lib.rs index 48deeef95..450ac1ac1 100644 --- a/contracts/oracle/base/src/lib.rs +++ b/contracts/oracle/base/src/lib.rs @@ -1,12 +1,9 @@ mod contract; mod error; mod traits; -mod utils; -#[cfg(feature = "pyth")] pub mod pyth; pub use contract::*; pub use error::*; pub use traits::*; -pub use utils::*; diff --git a/contracts/oracle/base/src/pyth.rs b/contracts/oracle/base/src/pyth.rs index be99a5b5f..f0d25ed86 100644 --- a/contracts/oracle/base/src/pyth.rs +++ b/contracts/oracle/base/src/pyth.rs @@ -1,44 +1,110 @@ use cosmwasm_std::{Addr, Decimal, Deps, Empty, Env, StdError, Uint128}; use cw_storage_plus::Map; -use mars_red_bank_types::oracle::Config; -use pyth_sdk_cw::query_price_feed; -pub use pyth_sdk_cw::PriceIdentifier; +use mars_red_bank_types::oracle::{ActionKind, Config}; +use pyth_sdk_cw::{query_price_feed, Price, PriceFeed, PriceFeedResponse, PriceIdentifier}; use super::*; use crate::error::ContractError::InvalidPrice; +// We don't support any denom with more than 18 decimals +const MAX_DENOM_DECIMALS: u8 = 18; + +/// We want to discriminate which actions should trigger a circuit breaker check. +/// The objective is to allow liquidations to happen without requiring too many checks (always be open for liquidations) +/// while not allowing other actions to be taken in cases of extreme volatility (which could indicate price manipulation attacks). +#[allow(clippy::too_many_arguments)] pub fn query_pyth_price>( deps: &Deps, env: &Env, contract_addr: Addr, price_feed_id: PriceIdentifier, max_staleness: u64, + max_confidence: Decimal, + max_deviation: Decimal, denom_decimals: u8, config: &Config, price_sources: &Map<&str, P>, + kind: ActionKind, ) -> ContractResult { // Use current price source for USD to check how much 1 USD is worth in base_denom let usd_price = price_sources .load(deps.storage, "usd") .map_err(|_| StdError::generic_err("Price source not found for denom 'usd'"))? - .query_price(deps, env, "usd", config, price_sources)?; - - let current_time = env.block.time.seconds(); + .query_price(deps, env, "usd", config, price_sources, kind.clone())?; let price_feed_response = query_price_feed(&deps.querier, contract_addr, price_feed_id)?; + + match kind { + ActionKind::Default => query_pyth_price_for_default( + env, + max_staleness, + max_confidence, + max_deviation, + denom_decimals, + usd_price, + price_feed_response, + ), + ActionKind::Liquidation => query_pyth_price_for_liquidation( + env, + max_staleness, + denom_decimals, + usd_price, + price_feed_response, + ), + } +} + +fn query_pyth_price_for_default( + env: &Env, + max_staleness: u64, + max_confidence: Decimal, + max_deviation: Decimal, + denom_decimals: u8, + usd_price: Decimal, + price_feed_response: PriceFeedResponse, +) -> ContractResult { let price_feed = price_feed_response.price_feed; - // Check if the current price is not too old - let current_price_opt = price_feed.get_price_no_older_than(current_time as i64, max_staleness); - let Some(current_price) = current_price_opt else { + let current_time = env.block.time.seconds(); + let current_price = + assert_pyth_current_price_not_too_old(price_feed, current_time, max_staleness)?; + let ema_price = assert_pyth_ema_price_not_too_old(price_feed, current_time, max_staleness)?; + + // Check if the current and EMA price is > 0 + if current_price.price <= 0 || ema_price.price <= 0 { return Err(InvalidPrice { - reason: format!( - "current price publish time is too old/stale. published: {}, now: {}", - price_feed.get_price_unchecked().publish_time, - current_time - ), + reason: "price can't be <= 0".to_string(), }); - }; + } + + let current_price_dec = scale_to_exponent(current_price.price as u128, current_price.expo)?; + let ema_price_dec = scale_to_exponent(ema_price.price as u128, ema_price.expo)?; + + assert_pyth_price_confidence(current_price, ema_price_dec, max_confidence)?; + assert_pyth_price_deviation(current_price_dec, ema_price_dec, max_deviation)?; + + let current_price_dec = scale_pyth_price( + current_price.price as u128, + current_price.expo, + denom_decimals, + usd_price, + )?; + + Ok(current_price_dec) +} + +fn query_pyth_price_for_liquidation( + env: &Env, + max_staleness: u64, + denom_decimals: u8, + usd_price: Decimal, + price_feed_response: PriceFeedResponse, +) -> ContractResult { + let price_feed = price_feed_response.price_feed; + + let current_time = env.block.time.seconds(); + let current_price = + assert_pyth_current_price_not_too_old(price_feed, current_time, max_staleness)?; // Check if the current price is > 0 if current_price.price <= 0 { @@ -57,17 +123,106 @@ pub fn query_pyth_price>( Ok(current_price_dec) } -/// Price feeds represent numbers in a fixed-point format. -/// The same exponent is used for both the price and confidence interval. -/// The integer representation of these values can be computed by multiplying by 10^exponent. -/// -/// As an example, imagine Pyth reported the following values for ATOM/USD: -/// expo: -8 -/// conf: 574566 -/// price: 1365133270 -/// The confidence interval is 574566 * 10^(-8) = $0.00574566, and the price is 1365133270 * 10^(-8) = $13.6513327. -/// -/// Moreover, we have to represent the price for utoken in base_denom. +/// Assert Pyth configuration +pub fn assert_pyth( + max_confidence: Decimal, + max_deviation: Decimal, + denom_decimals: u8, +) -> ContractResult<()> { + if !max_confidence.le(&Decimal::percent(20u64)) { + return Err(ContractError::InvalidPriceSource { + reason: "max_confidence must be in the range of <0;0.2>".to_string(), + }); + } + + if !max_deviation.le(&Decimal::percent(20u64)) { + return Err(ContractError::InvalidPriceSource { + reason: "max_deviation must be in the range of <0;0.2>".to_string(), + }); + } + + if denom_decimals > MAX_DENOM_DECIMALS { + return Err(ContractError::InvalidPriceSource { + reason: format!("denom_decimals must be <= {}", MAX_DENOM_DECIMALS), + }); + } + + Ok(()) +} + +/// Check if the current price is not too old +pub fn assert_pyth_current_price_not_too_old( + price_feed: PriceFeed, + current_time: u64, + max_staleness: u64, +) -> ContractResult { + let current_price_opt = price_feed.get_price_no_older_than(current_time as i64, max_staleness); + let Some(current_price) = current_price_opt else { + return Err(InvalidPrice { + reason: format!( + "current price publish time is too old/stale. published: {}, now: {}", + price_feed.get_price_unchecked().publish_time, + current_time + ), + }); + }; + Ok(current_price) +} + +/// Check if the ema price is not too old +pub fn assert_pyth_ema_price_not_too_old( + price_feed: PriceFeed, + current_time: u64, + max_staleness: u64, +) -> ContractResult { + let ema_price_opt = price_feed.get_ema_price_no_older_than(current_time as i64, max_staleness); + let Some(ema_price) = ema_price_opt else { + return Err(InvalidPrice { + reason: format!( + "EMA price publish time is too old/stale. published: {}, now: {}", + price_feed.get_ema_price_unchecked().publish_time, + current_time + ), + }); + }; + Ok(ema_price) +} + +/// Check price confidence +pub fn assert_pyth_price_confidence( + current_price: Price, + ema_price_dec: Decimal, + max_confidence: Decimal, +) -> ContractResult<()> { + let confidence = scale_to_exponent(current_price.conf as u128, current_price.expo)?; + let price_confidence = confidence.checked_div(ema_price_dec)?; + if price_confidence > max_confidence { + return Err(InvalidPrice { + reason: format!("price confidence deviation {price_confidence} exceeds max allowed {max_confidence}") + }); + } + Ok(()) +} + +/// Check price deviation +pub fn assert_pyth_price_deviation( + current_price_dec: Decimal, + ema_price_dec: Decimal, + max_deviation: Decimal, +) -> ContractResult<()> { + let delta = current_price_dec.abs_diff(ema_price_dec); + let price_deviation = delta.checked_div(ema_price_dec)?; + if price_deviation > max_deviation { + return Err(InvalidPrice { + reason: format!( + "price deviation {price_deviation} exceeds max allowed {max_deviation}" + ), + }); + } + Ok(()) +} + +/// We have to represent the price for utoken in base_denom. /// Pyth price should be normalized with token decimals. /// /// Let's try to convert ATOM/USD reported by Pyth to uatom/base_denom: @@ -92,13 +247,7 @@ pub fn scale_pyth_price( denom_decimals: u8, usd_price: Decimal, ) -> ContractResult { - let target_expo = Uint128::from(10u8).checked_pow(expo.unsigned_abs())?; - let pyth_price = if expo < 0 { - Decimal::checked_from_ratio(value, target_expo)? - } else { - let res = Uint128::from(value).checked_mul(target_expo)?; - Decimal::from_ratio(res, 1u128) - }; + let pyth_price = scale_to_exponent(value, expo)?; let denom_scaled = Decimal::from_atomics(1u128, denom_decimals as u32)?; @@ -119,9 +268,34 @@ pub fn scale_pyth_price( // 26 decimals used (overflow) !!! let price = usd_price.checked_mul(denom_scaled)?.checked_mul(pyth_price)?; + if price.is_zero() { + return Err(InvalidPrice { + reason: "price is zero".to_string(), + }); + } + Ok(price) } +/// Price feeds represent numbers in a fixed-point format. +/// The same exponent is used for both the price and confidence interval. +/// The integer representation of these values can be computed by multiplying by 10^exponent. +/// +/// As an example, imagine Pyth reported the following values for ATOM/USD: +/// expo: -8 +/// conf: 574566 +/// price: 1365133270 +/// The confidence interval is 574566 * 10^(-8) = $0.00574566, and the price is 1365133270 * 10^(-8) = $13.6513327. +pub fn scale_to_exponent(value: u128, expo: i32) -> ContractResult { + let target_expo = Uint128::from(10u8).checked_pow(expo.unsigned_abs())?; + if expo < 0 { + Ok(Decimal::checked_from_ratio(value, target_expo)?) + } else { + let res = Uint128::from(value).checked_mul(target_expo)?; + Ok(Decimal::from_ratio(res, 1u128)) + } +} + /// Assert availability of usd price source pub fn assert_usd_price_source>( deps: &Deps, @@ -172,4 +346,16 @@ mod tests { .unwrap(); assert_eq!(ueth_price_in_uusd, Decimal::from_atomics(100000098000001u128, 20u32).unwrap()); } + + #[test] + fn return_error_if_scaled_pyth_price_is_zero() { + let price_err = + scale_pyth_price(1u128, -18, 18u8, Decimal::from_str("1000000").unwrap()).unwrap_err(); + assert_eq!( + price_err, + ContractError::InvalidPrice { + reason: "price is zero".to_string() + } + ); + } } diff --git a/contracts/oracle/base/src/traits.rs b/contracts/oracle/base/src/traits.rs index 5821bb49f..2577bbc15 100644 --- a/contracts/oracle/base/src/traits.rs +++ b/contracts/oracle/base/src/traits.rs @@ -2,7 +2,7 @@ use std::fmt::{Debug, Display}; use cosmwasm_std::{CustomQuery, Decimal, Deps, Env}; use cw_storage_plus::Map; -use mars_red_bank_types::oracle::Config; +use mars_red_bank_types::oracle::{ActionKind, Config}; use schemars::JsonSchema; use serde::{de::DeserializeOwned, Serialize}; @@ -49,5 +49,6 @@ where denom: &str, config: &Config, price_sources: &Map<&str, Self>, + kind: ActionKind, ) -> ContractResult; } diff --git a/contracts/oracle/base/src/utils.rs b/contracts/oracle/base/src/utils.rs deleted file mode 100644 index 8b5cb83b5..000000000 --- a/contracts/oracle/base/src/utils.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::ContractError; - -/// follows cosmos SDK validation logic where denoms can be 3 - 128 characters long -/// and starts with a letter, followed but either a letter, number, or separator ( ‘/' , ‘:' , ‘.’ , ‘_’ , or '-') -/// reference: https://github.com/cosmos/cosmos-sdk/blob/7728516abfab950dc7a9120caad4870f1f962df5/types/coin.go#L865-L867 -pub fn validate_native_denom(denom: &str) -> Result<(), ContractError> { - if denom.len() < 3 || denom.len() > 128 { - return Err(ContractError::InvalidDenom { - reason: "Invalid denom length".to_string(), - }); - } - - let mut chars = denom.chars(); - let first = chars.next().unwrap(); - if !first.is_ascii_alphabetic() { - return Err(ContractError::InvalidDenom { - reason: "First character is not ASCII alphabetic".to_string(), - }); - } - - let set = ['/', ':', '.', '_', '-']; - for c in chars { - if !(c.is_ascii_alphanumeric() || set.contains(&c)) { - return Err(ContractError::InvalidDenom { - reason: "Not all characters are ASCII alphanumeric or one of: / : . _ -" - .to_string(), - }); - } - } - - Ok(()) -} diff --git a/contracts/oracle/osmosis/Cargo.toml b/contracts/oracle/osmosis/Cargo.toml index bcabeb060..57e951f7b 100644 --- a/contracts/oracle/osmosis/Cargo.toml +++ b/contracts/oracle/osmosis/Cargo.toml @@ -12,10 +12,7 @@ keywords = { workspace = true } [lib] crate-type = ["cdylib", "rlib"] -doctest = false - -[profile.release] -overflow-checks = true +doctest = false [features] # for more explicit tests, cargo test --features=backtraces @@ -26,9 +23,10 @@ cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw2 = { workspace = true } cw-storage-plus = { workspace = true } -mars-owner = { workspace = true } -mars-oracle-base = { workspace = true, features = ["pyth"] } +mars-oracle-base = { workspace = true } mars-osmosis = { workspace = true } +mars-owner = { workspace = true } +mars-utils = { workspace = true } mars-red-bank-types = { workspace = true } osmosis-std = { workspace = true } pyth-sdk-cw = { workspace = true } @@ -37,7 +35,6 @@ serde = { workspace = true } [dev-dependencies] cosmwasm-schema = { workspace = true } -mars-testing = { workspace = true } mars-owner = { workspace = true } +mars-testing = { workspace = true } mars-utils = { workspace = true } -pyth-sdk-cw = { workspace = true } diff --git a/contracts/oracle/osmosis/src/helpers.rs b/contracts/oracle/osmosis/src/helpers.rs index 342c9e2ce..e3b612ca2 100644 --- a/contracts/oracle/osmosis/src/helpers.rs +++ b/contracts/oracle/osmosis/src/helpers.rs @@ -1,46 +1,100 @@ use mars_oracle_base::{ContractError, ContractResult}; -use mars_osmosis::helpers::{has_denom, Pool}; +use mars_osmosis::{ + helpers::{CommonPoolData, Pool}, + BalancerPool, +}; use crate::DowntimeDetector; /// 48 hours in seconds const TWO_DAYS_IN_SECONDS: u64 = 172800u64; -/// Assert the Osmosis pool indicated by `pool_id` is of XYK type and assets are OSMO and `denom` +/// Assert the Osmosis pool indicated by `pool_id` is of Balancer XYK, StableSwap or ConcentratedLiquidity and assets are OSMO and `denom` pub fn assert_osmosis_pool_assets( pool: &Pool, denom: &str, base_denom: &str, ) -> ContractResult<()> { - assert_osmosis_xyk_pool(pool)?; + assert_pool_has_two_assets(pool)?; + assert_pool_contains_assets(pool, denom, base_denom)?; - if !has_denom(base_denom, &pool.pool_assets) { + match pool { + Pool::Balancer(balancer_pool) => { + assert_equal_asset_weights(balancer_pool)?; + } + Pool::StableSwap(_) => {} + Pool::ConcentratedLiquidity(_) => {} + }; + + Ok(()) +} + +/// Assert the Osmosis pool indicated by `pool_id` is Balancer XYK type +pub fn assert_osmosis_xyk_lp_pool(pool: &Pool) -> ContractResult<()> { + assert_pool_has_two_assets(pool)?; + + match pool { + Pool::Balancer(balancer_pool) => assert_equal_asset_weights(balancer_pool)?, + Pool::StableSwap(stable_swap_pool) => { + return Err(ContractError::InvalidPriceSource { + reason: format!("StableSwap pool not supported. Pool id {}", stable_swap_pool.id), + }); + } + Pool::ConcentratedLiquidity(cl_pool) => { + return Err(ContractError::InvalidPriceSource { + reason: format!("ConcentratedLiquidity pool not supported. Pool id {}", cl_pool.id), + }); + } + }; + + Ok(()) +} + +/// Assert the Osmosis pool has exactly two assets +fn assert_pool_has_two_assets(pool: &Pool) -> ContractResult<()> { + let pool_id = pool.get_pool_id(); + let pool_denoms = pool.get_pool_denoms(); + if pool_denoms.len() != 2 { return Err(ContractError::InvalidPriceSource { - reason: format!("pool {} does not contain the base denom {}", pool.id, base_denom), + reason: format!( + "expecting pool {} to contain exactly two coins; found {}", + pool_id, + pool_denoms.len() + ), }); } - if !has_denom(denom, &pool.pool_assets) { + Ok(()) +} + +/// Assert the Osmosis pool contains both `denom` and `base_denom`, and they are not the same +fn assert_pool_contains_assets(pool: &Pool, denom: &str, base_denom: &str) -> ContractResult<()> { + let pool_id = pool.get_pool_id(); + let pool_denoms = pool.get_pool_denoms(); + + if denom == base_denom { return Err(ContractError::InvalidPriceSource { - reason: format!("pool {} does not contain {}", pool.id, denom), + reason: "denom and base denom can't be the same".to_string(), }); } - Ok(()) -} + if !pool_denoms.contains(&base_denom.to_string()) { + return Err(ContractError::InvalidPriceSource { + reason: format!("pool {} does not contain the base denom {}", pool_id, base_denom), + }); + } -/// Assert the Osmosis pool indicated by `pool_id` is of XYK type -pub fn assert_osmosis_xyk_pool(pool: &Pool) -> ContractResult<()> { - if pool.pool_assets.len() != 2 { + if !pool_denoms.contains(&denom.to_string()) { return Err(ContractError::InvalidPriceSource { - reason: format!( - "expecting pool {} to contain exactly two coins; found {}", - pool.id, - pool.pool_assets.len() - ), + reason: format!("pool {} does not contain {}", pool_id, denom), }); } + Ok(()) +} + +/// Assert the Osmosis pool has assets with equal weights (for XYK pools) +fn assert_equal_asset_weights(pool: &BalancerPool) -> ContractResult<()> { if pool.pool_assets[0].weight != pool.pool_assets[1].weight { return Err(ContractError::InvalidPriceSource { reason: format!("assets in pool {} do not have equal weights", pool.id), diff --git a/contracts/oracle/osmosis/src/lib.rs b/contracts/oracle/osmosis/src/lib.rs index 31089de17..0e8835960 100644 --- a/contracts/oracle/osmosis/src/lib.rs +++ b/contracts/oracle/osmosis/src/lib.rs @@ -6,6 +6,6 @@ mod price_source; pub mod stride; pub use price_source::{ - scale_pyth_price, Downtime, DowntimeDetector, GeometricTwap, OsmosisPriceSourceChecked, + Downtime, DowntimeDetector, GeometricTwap, OsmosisPriceSourceChecked, OsmosisPriceSourceUnchecked, RedemptionRate, }; diff --git a/contracts/oracle/osmosis/src/migrations.rs b/contracts/oracle/osmosis/src/migrations.rs index a10f865b3..a74db63d1 100644 --- a/contracts/oracle/osmosis/src/migrations.rs +++ b/contracts/oracle/osmosis/src/migrations.rs @@ -10,7 +10,11 @@ pub mod v1_0_1 { pub fn migrate(deps: DepsMut) -> ContractResult { // make sure we're migrating the correct contract and from the correct version - cw2::assert_contract_version(deps.as_ref().storage, CONTRACT_NAME, FROM_VERSION)?; + cw2::assert_contract_version( + deps.as_ref().storage, + &format!("crates.io:{CONTRACT_NAME}"), + FROM_VERSION, + )?; // map old owner struct to new one let old_owner = old_state::OWNER.load(deps.storage)?; @@ -32,7 +36,11 @@ pub mod v1_0_1 { )?; // update contract version - cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + cw2::set_contract_version( + deps.storage, + format!("crates.io:{CONTRACT_NAME}"), + CONTRACT_VERSION, + )?; Ok(Response::new() .add_attribute("action", "migrate") @@ -83,7 +91,12 @@ pub mod v1_0_1 { fn migration_owner_from_state_b() { let mut deps = mock_dependencies(); - cw2::set_contract_version(deps.as_mut().storage, CONTRACT_NAME, FROM_VERSION).unwrap(); + cw2::set_contract_version( + deps.as_mut().storage, + format!("crates.io:{CONTRACT_NAME}"), + FROM_VERSION, + ) + .unwrap(); old_state::OWNER .save( @@ -101,7 +114,7 @@ pub mod v1_0_1 { vec![ attr("action", "migrate"), attr("from_version", "1.0.1"), - attr("to_version", "1.2.0") + attr("to_version", "2.0.0") ] ); @@ -113,7 +126,12 @@ pub mod v1_0_1 { fn migration_owner_from_state_c() { let mut deps = mock_dependencies(); - cw2::set_contract_version(deps.as_mut().storage, CONTRACT_NAME, FROM_VERSION).unwrap(); + cw2::set_contract_version( + deps.as_mut().storage, + format!("crates.io:{CONTRACT_NAME}"), + FROM_VERSION, + ) + .unwrap(); old_state::OWNER .save( @@ -131,7 +149,7 @@ pub mod v1_0_1 { vec![ attr("action", "migrate"), attr("from_version", "1.0.1"), - attr("to_version", "1.2.0") + attr("to_version", "2.0.0") ] ); diff --git a/contracts/oracle/osmosis/src/price_source.rs b/contracts/oracle/osmosis/src/price_source.rs index e6ac09707..1a0e9f95e 100644 --- a/contracts/oracle/osmosis/src/price_source.rs +++ b/contracts/oracle/osmosis/src/price_source.rs @@ -3,14 +3,16 @@ use std::{cmp::min, fmt}; use cosmwasm_std::{Addr, Decimal, Decimal256, Deps, Empty, Env, Isqrt, Uint128, Uint256}; use cw_storage_plus::Map; use mars_oracle_base::{ - pyth::PriceIdentifier, ContractError::InvalidPrice, ContractResult, PriceSourceChecked, - PriceSourceUnchecked, + ContractError::{self, InvalidPrice}, + ContractResult, PriceSourceChecked, PriceSourceUnchecked, }; use mars_osmosis::helpers::{ query_arithmetic_twap_price, query_geometric_twap_price, query_pool, query_spot_price, recovered_since_downtime_of_length, Pool, }; -use mars_red_bank_types::oracle::Config; +use mars_red_bank_types::oracle::{ActionKind, Config}; +use mars_utils::helpers::validate_native_denom; +use pyth_sdk_cw::PriceIdentifier; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -164,6 +166,16 @@ pub enum OsmosisPriceSource { /// rejecting the price as too stale max_staleness: u64, + /// The maximum confidence deviation allowed for an oracle price. + /// + /// The confidence is measured as the percent of the confidence interval + /// value provided by the oracle as compared to the weighted average value + /// of the price. + max_confidence: Decimal, + + /// The maximum deviation (percentage) between current and EMA price + max_deviation: Decimal, + /// Assets are represented in their smallest unit and every asset can have different decimals (e.g. OSMO - 6 decimals, WETH - 18 decimals). /// /// Pyth prices are denominated in USD so basically it means how much 1 USDC, 1 ATOM, 1 OSMO is worth in USD (NOT 1 uusdc, 1 uatom, 1 uosmo). @@ -276,9 +288,11 @@ impl fmt::Display for OsmosisPriceSourceChecked { contract_addr, price_feed_id, max_staleness, + max_confidence, + max_deviation, denom_decimals, } => { - format!("pyth:{contract_addr}:{price_feed_id}:{max_staleness}:{denom_decimals}") + format!("pyth:{contract_addr}:{price_feed_id}:{max_staleness}:{max_confidence}:{max_deviation}:{denom_decimals}") } OsmosisPriceSource::Lsd { transitive_denom, @@ -357,7 +371,7 @@ impl PriceSourceUnchecked for OsmosisPriceSour pool_id, } => { let pool = query_pool(&deps.querier, *pool_id)?; - helpers::assert_osmosis_xyk_pool(&pool)?; + helpers::assert_osmosis_xyk_lp_pool(&pool)?; Ok(OsmosisPriceSourceChecked::XykLiquidityToken { pool_id: *pool_id, }) @@ -368,9 +382,12 @@ impl PriceSourceUnchecked for OsmosisPriceSour window_size, downtime_detector, } => { + validate_native_denom(transitive_denom)?; + let pool = query_pool(&deps.querier, *pool_id)?; helpers::assert_osmosis_pool_assets(&pool, denom, transitive_denom)?; helpers::assert_osmosis_twap(*window_size, downtime_detector)?; + Ok(OsmosisPriceSourceChecked::StakedGeometricTwap { transitive_denom: transitive_denom.to_string(), pool_id: *pool_id, @@ -382,14 +399,22 @@ impl PriceSourceUnchecked for OsmosisPriceSour contract_addr, price_feed_id, max_staleness, + max_confidence, + max_deviation, denom_decimals, } => { + mars_oracle_base::pyth::assert_pyth( + *max_confidence, + *max_deviation, + *denom_decimals, + )?; mars_oracle_base::pyth::assert_usd_price_source(deps, price_sources)?; - Ok(OsmosisPriceSourceChecked::Pyth { contract_addr: deps.api.addr_validate(contract_addr)?, price_feed_id: *price_feed_id, max_staleness: *max_staleness, + max_confidence: *max_confidence, + max_deviation: *max_deviation, denom_decimals: *denom_decimals, }) } @@ -398,12 +423,15 @@ impl PriceSourceUnchecked for OsmosisPriceSour geometric_twap, redemption_rate, } => { + validate_native_denom(transitive_denom)?; + let pool = query_pool(&deps.querier, geometric_twap.pool_id)?; helpers::assert_osmosis_pool_assets(&pool, denom, transitive_denom)?; helpers::assert_osmosis_twap( geometric_twap.window_size, &geometric_twap.downtime_detector, )?; + Ok(OsmosisPriceSourceChecked::Lsd { transitive_denom: transitive_denom.to_string(), geometric_twap: geometric_twap.clone(), @@ -425,6 +453,7 @@ impl PriceSourceChecked for OsmosisPriceSourceChecked { denom: &str, config: &Config, price_sources: &Map<&str, Self>, + kind: ActionKind, ) -> ContractResult { match self { OsmosisPriceSourceChecked::Fixed { @@ -470,7 +499,14 @@ impl PriceSourceChecked for OsmosisPriceSourceChecked { } OsmosisPriceSourceChecked::XykLiquidityToken { pool_id, - } => Self::query_xyk_liquidity_token_price(deps, env, *pool_id, config, price_sources), + } => Self::query_xyk_liquidity_token_price( + deps, + env, + *pool_id, + config, + price_sources, + kind, + ), OsmosisPriceSourceChecked::StakedGeometricTwap { transitive_denom, pool_id, @@ -488,12 +524,15 @@ impl PriceSourceChecked for OsmosisPriceSourceChecked { *window_size, config, price_sources, + kind, ) } OsmosisPriceSourceChecked::Pyth { contract_addr, price_feed_id, max_staleness, + max_confidence, + max_deviation, denom_decimals, } => Ok(mars_oracle_base::pyth::query_pyth_price( deps, @@ -501,9 +540,12 @@ impl PriceSourceChecked for OsmosisPriceSourceChecked { contract_addr.to_owned(), *price_feed_id, *max_staleness, + *max_confidence, + *max_deviation, *denom_decimals, config, price_sources, + kind, )?), OsmosisPriceSourceChecked::Lsd { transitive_denom, @@ -521,6 +563,7 @@ impl PriceSourceChecked for OsmosisPriceSourceChecked { redemption_rate.clone(), config, price_sources, + kind, ) } } @@ -558,9 +601,26 @@ impl OsmosisPriceSourceChecked { pool_id: u64, config: &Config, price_sources: &Map<&str, Self>, + kind: ActionKind, ) -> ContractResult { // XYK pool asserted during price source creation let pool = query_pool(&deps.querier, pool_id)?; + let pool = match pool { + Pool::Balancer(pool) => pool, + Pool::StableSwap(pool) => { + return Err(ContractError::InvalidPrice { + reason: format!("StableSwap pool not supported. Pool id {}", pool.id), + }) + } + Pool::ConcentratedLiquidity(pool) => { + return Err(ContractError::InvalidPrice { + reason: format!( + "ConcentratedLiquidity pool not supported. Pool id {}", + pool.id + ), + }) + } + }; let coin0 = Pool::unwrap_coin(&pool.pool_assets[0].token)?; let coin1 = Pool::unwrap_coin(&pool.pool_assets[1].token)?; @@ -571,6 +631,7 @@ impl OsmosisPriceSourceChecked { &coin0.denom, config, price_sources, + kind.clone(), )?; let coin1_price = price_sources.load(deps.storage, &coin1.denom)?.query_price( deps, @@ -578,6 +639,7 @@ impl OsmosisPriceSourceChecked { &coin1.denom, config, price_sources, + kind, )?; let coin0_value = Uint256::from_uint128(coin0.amount) * Decimal256::from(coin0_price); @@ -608,6 +670,7 @@ impl OsmosisPriceSourceChecked { window_size: u64, config: &Config, price_sources: &Map<&str, OsmosisPriceSourceChecked>, + kind: ActionKind, ) -> ContractResult { let start_time = env.block.time.seconds() - window_size; let staked_price = query_geometric_twap_price( @@ -625,6 +688,7 @@ impl OsmosisPriceSourceChecked { transitive_denom, config, price_sources, + kind, )?; staked_price.checked_mul(transitive_price).map_err(Into::into) @@ -645,6 +709,7 @@ impl OsmosisPriceSourceChecked { redemption_rate: RedemptionRate, config: &Config, price_sources: &Map<&str, OsmosisPriceSourceChecked>, + kind: ActionKind, ) -> ContractResult { let current_time = env.block.time.seconds(); let start_time = current_time - geometric_twap.window_size; @@ -683,166 +748,9 @@ impl OsmosisPriceSourceChecked { transitive_denom, config, price_sources, + kind, )?; min_price.checked_mul(transitive_price).map_err(Into::into) } } - -/// Price feeds represent numbers in a fixed-point format. -/// The same exponent is used for both the price and confidence interval. -/// The integer representation of these values can be computed by multiplying by 10^exponent. -/// -/// As an example, imagine Pyth reported the following values for ATOM/USD: -/// expo: -8 -/// conf: 574566 -/// price: 1365133270 -/// The confidence interval is 574566 * 10^(-8) = $0.00574566, and the price is 1365133270 * 10^(-8) = $13.6513327. -/// -/// Moreover, we have to represent the price for utoken in base_denom. -/// Pyth price should be normalized with token decimals. -/// -/// Let's try to convert ATOM/USD reported by Pyth to uatom/base_denom: -/// - base_denom = uusd -/// - price source set for usd (e.g. FIXED price source where 1 usd = 1000000 uusd) -/// - denom_decimals (ATOM) = 6 -/// -/// 1 ATOM = 10^6 uatom -/// -/// 1 ATOM = price * 10^expo USD -/// 10^6 uatom = price * 10^expo * 1000000 uusd -/// uatom = price * 10^expo * 1000000 / 10^6 uusd -/// uatom = price * 10^expo * 1000000 * 10^(-6) uusd -/// uatom/uusd = 1365133270 * 10^(-8) * 1000000 * 10^(-6) -/// uatom/uusd = 1365133270 * 10^(-8) = 13.6513327 -/// -/// Generalized formula: -/// utoken/uusd = price * 10^expo * usd_price_in_base_denom * 10^(-denom_decimals) -pub fn scale_pyth_price( - value: u128, - expo: i32, - denom_decimals: u8, - usd_price: Decimal, -) -> ContractResult { - let target_expo = Uint128::from(10u8).checked_pow(expo.unsigned_abs())?; - let pyth_price = if expo < 0 { - Decimal::checked_from_ratio(value, target_expo)? - } else { - let res = Uint128::from(value).checked_mul(target_expo)?; - Decimal::from_ratio(res, 1u128) - }; - - let denom_scaled = Decimal::from_atomics(1u128, denom_decimals as u32)?; - - // Multiplication order matters !!! It can overflow doing different ways. - // usd_price is represented in smallest unit so it can be quite big number and can be used to reduce number of decimals. - // - // Let's assume that: - // - usd_price = 1000000 = 10^6 - // - expo = -8 - // - denom_decimals = 18 - // - // If we multiply usd_price by denom_scaled firstly we will decrease number of decimals used in next multiplication by pyth_price: - // 10^6 * 10^(-18) = 10^(-12) - // 12 decimals used. - // - // BUT if we multiply pyth_price by denom_scaled: - // 10^(-8) * 10^(-18) = 10^(-26) - // 26 decimals used (overflow) !!! - let price = usd_price.checked_mul(denom_scaled)?.checked_mul(pyth_price)?; - - Ok(price) -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use super::*; - - #[test] - fn display_pyth_price_source() { - let ps = OsmosisPriceSourceChecked::Pyth { - contract_addr: Addr::unchecked("osmo12j43nf2f0qumnt2zrrmpvnsqgzndxefujlvr08"), - price_feed_id: PriceIdentifier::from_hex( - "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", - ) - .unwrap(), - max_staleness: 60, - denom_decimals: 6, - }; - assert_eq!( - ps.to_string(), - "pyth:osmo12j43nf2f0qumnt2zrrmpvnsqgzndxefujlvr08:0x61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3:60:6" - ) - } - - #[test] - fn display_lsd_price_source() { - let ps = OsmosisPriceSourceChecked::Lsd { - transitive_denom: "transitive".to_string(), - geometric_twap: GeometricTwap { - pool_id: 456, - window_size: 380, - downtime_detector: None, - }, - redemption_rate: RedemptionRate { - contract_addr: Addr::unchecked( - "osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc", - ), - max_staleness: 1234, - }, - }; - assert_eq!(ps.to_string(), "lsd:transitive:456:380:None:osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc:1234"); - - let ps = OsmosisPriceSourceChecked::Lsd { - transitive_denom: "transitive".to_string(), - geometric_twap: GeometricTwap { - pool_id: 456, - window_size: 380, - downtime_detector: Some(DowntimeDetector { - downtime: Downtime::Duration30m, - recovery: 552, - }), - }, - redemption_rate: RedemptionRate { - contract_addr: Addr::unchecked( - "osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc", - ), - max_staleness: 1234, - }, - }; - assert_eq!(ps.to_string(), "lsd:transitive:456:380:Some(Duration30m:552):osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc:1234"); - } - - #[test] - fn scale_real_pyth_price() { - // ATOM - let uatom_price_in_uusd = - scale_pyth_price(1035200881u128, -8, 6u8, Decimal::from_str("1000000").unwrap()) - .unwrap(); - assert_eq!(uatom_price_in_uusd, Decimal::from_str("10.35200881").unwrap()); - - // ETH - let ueth_price_in_uusd = - scale_pyth_price(181598000001u128, -8, 18u8, Decimal::from_str("1000000").unwrap()) - .unwrap(); - assert_eq!(ueth_price_in_uusd, Decimal::from_str("0.00000000181598").unwrap()); - } - - #[test] - fn scale_pyth_price_if_expo_above_zero() { - let ueth_price_in_uusd = - scale_pyth_price(181598000001u128, 8, 18u8, Decimal::from_str("1000000").unwrap()) - .unwrap(); - assert_eq!(ueth_price_in_uusd, Decimal::from_atomics(181598000001u128, 4u32).unwrap()); - } - - #[test] - fn scale_big_eth_pyth_price() { - let ueth_price_in_uusd = - scale_pyth_price(100000098000001u128, -8, 18u8, Decimal::from_str("1000000").unwrap()) - .unwrap(); - assert_eq!(ueth_price_in_uusd, Decimal::from_atomics(100000098000001u128, 20u32).unwrap()); - } -} diff --git a/contracts/oracle/osmosis/tests/all_tests.rs b/contracts/oracle/osmosis/tests/all_tests.rs new file mode 100644 index 000000000..14f00389d --- /dev/null +++ b/contracts/oracle/osmosis/tests/all_tests.rs @@ -0,0 +1 @@ +mod tests; diff --git a/contracts/oracle/osmosis/tests/helpers.rs b/contracts/oracle/osmosis/tests/tests/helpers/mod.rs similarity index 51% rename from contracts/oracle/osmosis/tests/helpers.rs rename to contracts/oracle/osmosis/tests/tests/helpers/mod.rs index 91f43b3e3..ba18c9670 100644 --- a/contracts/oracle/osmosis/tests/helpers.rs +++ b/contracts/oracle/osmosis/tests/tests/helpers/mod.rs @@ -1,18 +1,18 @@ #![allow(dead_code)] -use std::marker::PhantomData; +use std::{marker::PhantomData, str::FromStr}; use cosmwasm_std::{ coin, from_binary, testing::{mock_env, MockApi, MockQuerier, MockStorage}, - Coin, Deps, DepsMut, OwnedDeps, + Coin, Decimal, Deps, DepsMut, OwnedDeps, }; use mars_oracle_base::ContractError; use mars_oracle_osmosis::{contract::entry, msg::ExecuteMsg, OsmosisPriceSourceUnchecked}; -use mars_osmosis::helpers::{Pool, QueryPoolResponse}; +use mars_osmosis::{BalancerPool, ConcentratedLiquidityPool, StableSwapPool}; use mars_red_bank_types::oracle::msg::{InstantiateMsg, QueryMsg}; use mars_testing::{mock_info, MarsMockQuerier}; -use osmosis_std::types::osmosis::gamm::v1beta1::PoolAsset; +use osmosis_std::types::osmosis::{gamm::v1beta1::PoolAsset, poolmanager::v1beta1::PoolResponse}; use pyth_sdk_cw::PriceIdentifier; pub fn setup_test_with_pools() -> OwnedDeps { @@ -22,25 +22,40 @@ pub fn setup_test_with_pools() -> OwnedDeps OwnedDeps OwnedDeps OwnedDeps OwnedDeps { + let mut deps = setup_test(); + + // price source used to convert USD to base_denom + set_price_source( + deps.as_mut(), + "usd", + OsmosisPriceSourceUnchecked::Fixed { + price: Decimal::from_str("1000000").unwrap(), + }, + ); + deps } @@ -98,15 +142,15 @@ pub fn setup_test() -> OwnedDeps { deps } -pub fn prepare_query_pool_response( +pub fn prepare_query_balancer_pool_response( pool_id: u64, assets: &[Coin], weights: &[u64], shares: &Coin, -) -> QueryPoolResponse { - let pool = Pool { +) -> PoolResponse { + let pool = BalancerPool { address: "address".to_string(), - id: pool_id.to_string(), + id: pool_id, pool_params: None, future_pool_governor: "future_pool_governor".to_string(), total_shares: Some(osmosis_std::types::cosmos::base::v1beta1::Coin { @@ -116,8 +160,8 @@ pub fn prepare_query_pool_response( pool_assets: prepare_pool_assets(assets, weights), total_weight: "".to_string(), }; - QueryPoolResponse { - pool, + PoolResponse { + pool: Some(pool.to_any()), } } @@ -140,6 +184,56 @@ fn prepare_pool_assets(coins: &[Coin], weights: &[u64]) -> Vec { .collect() } +pub fn prepare_query_stable_swap_pool_response(pool_id: u64, assets: &[Coin]) -> PoolResponse { + let pool_liquidity: Vec<_> = assets + .iter() + .map(|coin| osmosis_std::types::cosmos::base::v1beta1::Coin { + denom: coin.denom.clone(), + amount: coin.amount.to_string(), + }) + .collect(); + + let pool = StableSwapPool { + address: "osmo15v4mn84s9flhzpstkf9ql2mu0rnxh42pm8zhq47kh2fzs5zlwjsqaterkr".to_string(), + id: pool_id, + pool_params: None, + future_pool_governor: "".to_string(), + total_shares: Some(osmosis_std::types::cosmos::base::v1beta1::Coin { + denom: format!("gamm/pool/{pool_id}"), + amount: 4497913440357232330148u128.to_string(), + }), + pool_liquidity, + scaling_factors: vec![100000u64, 113890u64], + scaling_factor_controller: "osmo1k8c2m5cn322akk5wy8lpt87dd2f4yh9afcd7af".to_string(), + }; + PoolResponse { + pool: Some(pool.to_any()), + } +} + +pub fn prepare_query_cl_pool_response(pool_id: u64, token0: &str, token1: &str) -> PoolResponse { + let pool = ConcentratedLiquidityPool { + address: "osmo126pr9qp44aft4juw7x4ev4s2qdtnwe38jzwunec9pxt5cpzaaphqyagqpu".to_string(), + incentives_address: "osmo1h2mhtj3wmsdt3uacev9pgpg38hkcxhsmyyn9ums0ya6eddrsafjsxs9j03" + .to_string(), + spread_rewards_address: "osmo16j5sssw32xuk8a0kjj8n54g25ye6kr339nz5axf8lzyeajk0k22stsm36c" + .to_string(), + id: pool_id, + current_tick_liquidity: "3820025893854099618.699762490947860933".to_string(), + token0: token0.to_string(), + token1: token1.to_string(), + current_sqrt_price: "656651.537483144215151633465586753226461989".to_string(), + current_tick: 102311912, + tick_spacing: 100, + exponent_at_price_one: -6, + spread_factor: "0.002000000000000000".to_string(), + last_liquidity_update: None, + }; + PoolResponse { + pool: Some(pool.to_any()), + } +} + pub fn set_pyth_price_source(deps: DepsMut, denom: &str, price_id: PriceIdentifier) { set_price_source( deps, @@ -148,6 +242,8 @@ pub fn set_pyth_price_source(deps: DepsMut, denom: &str, price_id: PriceIdentifi contract_addr: "pyth_contract".to_string(), price_feed_id: price_id, max_staleness: 30, + max_confidence: Decimal::percent(10u64), + max_deviation: Decimal::percent(15u64), denom_decimals: 6, }, ) diff --git a/contracts/oracle/osmosis/tests/tests/mod.rs b/contracts/oracle/osmosis/tests/tests/mod.rs new file mode 100644 index 000000000..13eec4284 --- /dev/null +++ b/contracts/oracle/osmosis/tests/tests/mod.rs @@ -0,0 +1,10 @@ +mod helpers; + +mod test_admin; +mod test_custom_execute; +mod test_price_source_fmt; +mod test_query_price; +mod test_query_price_for_pyth; +mod test_remove_price_source; +mod test_set_price_source; +mod test_update_owner; diff --git a/contracts/oracle/osmosis/tests/test_admin.rs b/contracts/oracle/osmosis/tests/tests/test_admin.rs similarity index 90% rename from contracts/oracle/osmosis/tests/test_admin.rs rename to contracts/oracle/osmosis/tests/tests/test_admin.rs index b996215d3..06758b60c 100644 --- a/contracts/oracle/osmosis/tests/test_admin.rs +++ b/contracts/oracle/osmosis/tests/tests/test_admin.rs @@ -4,8 +4,9 @@ use mars_oracle_osmosis::{contract::entry, msg::ExecuteMsg}; use mars_owner::OwnerError::NotOwner; use mars_red_bank_types::oracle::{ConfigResponse, InstantiateMsg, QueryMsg}; use mars_testing::{mock_dependencies, mock_info}; +use mars_utils::error::ValidationError; -mod helpers; +use super::helpers; #[test] fn instantiating() { @@ -35,9 +36,9 @@ fn instantiating_incorrect_denom() { ); assert_eq!( res, - Err(ContractError::InvalidDenom { + Err(ContractError::Validation(ValidationError::InvalidDenom { reason: "First character is not ASCII alphabetic".to_string() - }) + })) ); let res = entry::instantiate( @@ -52,10 +53,10 @@ fn instantiating_incorrect_denom() { ); assert_eq!( res, - Err(ContractError::InvalidDenom { + Err(ContractError::Validation(ValidationError::InvalidDenom { reason: "Not all characters are ASCII alphanumeric or one of: / : . _ -" .to_string() - }) + })) ); let res = entry::instantiate( @@ -70,9 +71,9 @@ fn instantiating_incorrect_denom() { ); assert_eq!( res, - Err(ContractError::InvalidDenom { + Err(ContractError::Validation(ValidationError::InvalidDenom { reason: "Invalid denom length".to_string() - }) + })) ); } @@ -99,9 +100,9 @@ fn update_config_with_invalid_base_denom() { let res_err = entry::execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); assert_eq!( res_err, - ContractError::InvalidDenom { + ContractError::Validation(ValidationError::InvalidDenom { reason: "First character is not ASCII alphabetic".to_string() - } + }) ); } diff --git a/contracts/oracle/osmosis/tests/test_custom_execute.rs b/contracts/oracle/osmosis/tests/tests/test_custom_execute.rs similarity index 96% rename from contracts/oracle/osmosis/tests/test_custom_execute.rs rename to contracts/oracle/osmosis/tests/tests/test_custom_execute.rs index e8405a703..a0b7e8fcc 100644 --- a/contracts/oracle/osmosis/tests/test_custom_execute.rs +++ b/contracts/oracle/osmosis/tests/tests/test_custom_execute.rs @@ -4,7 +4,7 @@ use mars_oracle_osmosis::contract::entry; use mars_red_bank_types::oracle::ExecuteMsg; use mars_testing::mock_info; -mod helpers; +use super::helpers; #[test] fn custom_execute() { diff --git a/contracts/oracle/osmosis/tests/test_price_source_fmt.rs b/contracts/oracle/osmosis/tests/tests/test_price_source_fmt.rs similarity index 97% rename from contracts/oracle/osmosis/tests/test_price_source_fmt.rs rename to contracts/oracle/osmosis/tests/tests/test_price_source_fmt.rs index de4085329..0ece290c8 100644 --- a/contracts/oracle/osmosis/tests/test_price_source_fmt.rs +++ b/contracts/oracle/osmosis/tests/tests/test_price_source_fmt.rs @@ -4,8 +4,6 @@ use mars_oracle_osmosis::{ }; use pyth_sdk_cw::PriceIdentifier; -mod helpers; - #[test] fn display_downtime_detector() { let dd = DowntimeDetector { @@ -110,11 +108,13 @@ fn display_pyth_price_source() { ) .unwrap(), max_staleness: 60, + max_confidence: Decimal::percent(10u64), + max_deviation: Decimal::percent(15u64), denom_decimals: 18, }; assert_eq!( ps.to_string(), - "pyth:osmo12j43nf2f0qumnt2zrrmpvnsqgzndxefujlvr08:0x61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3:60:18" + "pyth:osmo12j43nf2f0qumnt2zrrmpvnsqgzndxefujlvr08:0x61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3:60:0.1:0.15:18" ) } diff --git a/contracts/oracle/osmosis/tests/test_query_price.rs b/contracts/oracle/osmosis/tests/tests/test_query_price.rs similarity index 79% rename from contracts/oracle/osmosis/tests/test_query_price.rs rename to contracts/oracle/osmosis/tests/tests/test_query_price.rs index e36c73703..c16f18281 100644 --- a/contracts/oracle/osmosis/tests/test_query_price.rs +++ b/contracts/oracle/osmosis/tests/tests/test_query_price.rs @@ -5,6 +5,7 @@ use cosmwasm_std::{ testing::{mock_env, MockApi, MockStorage}, Decimal, OwnedDeps, StdError, }; +use helpers::prepare_query_balancer_pool_response; use mars_oracle_base::{pyth::scale_pyth_price, ContractError}; use mars_oracle_osmosis::{ contract::entry, stride::RedemptionRateResponse, Downtime, DowntimeDetector, GeometricTwap, @@ -18,9 +19,7 @@ use osmosis_std::types::osmosis::{ }; use pyth_sdk_cw::{Price, PriceFeed, PriceFeedResponse, PriceIdentifier}; -use crate::helpers::prepare_query_pool_response; - -mod helpers; +use super::helpers; #[test] fn querying_fixed_price() { @@ -38,6 +37,7 @@ fn querying_fixed_price() { deps.as_ref(), QueryMsg::Price { denom: "uosmo".to_string(), + kind: None, }, ); assert_eq!(res.price, Decimal::one()); @@ -52,6 +52,7 @@ fn querying_fixed_price_price_source_not_set() { mock_env(), QueryMsg::Price { denom: "uosmo".to_string(), + kind: None, }, ) .unwrap_err(); @@ -87,6 +88,7 @@ fn querying_spot_price() { deps.as_ref(), QueryMsg::Price { denom: "umars".to_string(), + kind: None, }, ); assert_eq!(res.price, Decimal::from_ratio(88888u128, 12345u128)); @@ -119,6 +121,7 @@ fn querying_arithmetic_twap_price() { deps.as_ref(), QueryMsg::Price { denom: "umars".to_string(), + kind: None, }, ); assert_eq!(res.price, Decimal::from_ratio(77777u128, 12345u128)); @@ -147,6 +150,7 @@ fn querying_arithmetic_twap_price_with_downtime_detector() { deps.as_ref(), QueryMsg::Price { denom: "umars".to_string(), + kind: None, }, ); assert_eq!( @@ -169,6 +173,7 @@ fn querying_arithmetic_twap_price_with_downtime_detector() { deps.as_ref(), QueryMsg::Price { denom: "umars".to_string(), + kind: None, }, ); assert_eq!(res.price, Decimal::from_ratio(77777u128, 12345u128)); @@ -201,6 +206,7 @@ fn querying_geometric_twap_price() { deps.as_ref(), QueryMsg::Price { denom: "umars".to_string(), + kind: None, }, ); assert_eq!(res.price, Decimal::from_ratio(66666u128, 12345u128)); @@ -229,6 +235,7 @@ fn querying_geometric_twap_price_with_downtime_detector() { deps.as_ref(), QueryMsg::Price { denom: "umars".to_string(), + kind: None, }, ); assert_eq!( @@ -251,6 +258,7 @@ fn querying_geometric_twap_price_with_downtime_detector() { deps.as_ref(), QueryMsg::Price { denom: "umars".to_string(), + kind: None, }, ); assert_eq!(res.price, Decimal::from_ratio(77777u128, 12345u128)); @@ -303,6 +311,7 @@ fn querying_staked_geometric_twap_price() { deps.as_ref(), QueryMsg::Price { denom: "ustatom".to_string(), + kind: None, }, ); let expected_price = ustatom_uatom_price * uatom_uosmo_price; @@ -338,6 +347,7 @@ fn querying_staked_geometric_twap_price_if_no_transitive_denom_price_source() { deps.as_ref(), QueryMsg::Price { denom: "ustatom".to_string(), + kind: None, }, ); assert_eq!( @@ -381,6 +391,7 @@ fn querying_staked_geometric_twap_price_with_downtime_detector() { deps.as_ref(), QueryMsg::Price { denom: "ustatom".to_string(), + kind: None, }, ); assert_eq!( @@ -415,6 +426,7 @@ fn querying_staked_geometric_twap_price_with_downtime_detector() { deps.as_ref(), QueryMsg::Price { denom: "ustatom".to_string(), + kind: None, }, ); let expected_price = ustatom_uatom_price * uatom_uosmo_price; @@ -470,6 +482,7 @@ fn querying_lsd_price() { mock_env_at_block_time(publish_time), QueryMsg::Price { denom: "ustatom".to_string(), + kind: None, }, ) .unwrap(); @@ -510,6 +523,7 @@ fn querying_lsd_price() { mock_env_at_block_time(publish_time), QueryMsg::Price { denom: "ustatom".to_string(), + kind: None, }, ) .unwrap(); @@ -535,6 +549,8 @@ fn setup_pyth_and_geometric_twap_for_lsd( contract_addr: "pyth_contract_addr".to_string(), price_feed_id: price_id, max_staleness: 1800u64, + max_confidence: Decimal::percent(10u64), + max_deviation: Decimal::percent(15u64), denom_decimals: 6u8, }, ); @@ -622,6 +638,7 @@ fn querying_lsd_price_if_no_transitive_denom_price_source() { mock_env_at_block_time(publish_time), QueryMsg::Price { denom: "ustatom".to_string(), + kind: None, }, ) .unwrap_err(); @@ -685,6 +702,7 @@ fn querying_lsd_price_if_redemption_rate_too_old() { mock_env_at_block_time(publish_time), QueryMsg::Price { denom: "ustatom".to_string(), + kind: None, }, ) .unwrap_err(); @@ -752,6 +770,7 @@ fn querying_lsd_price_with_downtime_detector() { mock_env_at_block_time(publish_time), QueryMsg::Price { denom: "ustatom".to_string(), + kind: None, }, ) .unwrap_err(); @@ -768,6 +787,7 @@ fn querying_lsd_price_with_downtime_detector() { mock_env_at_block_time(publish_time), QueryMsg::Price { denom: "ustatom".to_string(), + kind: None, }, ) .unwrap(); @@ -783,7 +803,7 @@ fn querying_xyk_lp_price() { let assets = vec![coin(1, "uatom"), coin(1, "uosmo")]; deps.querier.set_query_pool_response( 10001, - prepare_query_pool_response( + prepare_query_balancer_pool_response( 10001, &assets, &[5000u64, 5000u64], @@ -794,7 +814,7 @@ fn querying_xyk_lp_price() { let assets = vec![coin(1, "umars"), coin(1, "uosmo")]; deps.querier.set_query_pool_response( 10002, - prepare_query_pool_response( + prepare_query_balancer_pool_response( 10002, &assets, &[5000u64, 5000u64], @@ -805,7 +825,7 @@ fn querying_xyk_lp_price() { let assets = vec![coin(10000, "uatom"), coin(885000, "umars")]; deps.querier.set_query_pool_response( 10003, - prepare_query_pool_response( + prepare_query_balancer_pool_response( 10003, &assets, &[5000u64, 5000u64], @@ -854,6 +874,7 @@ fn querying_xyk_lp_price() { deps.as_ref(), QueryMsg::Price { denom: "uatom_umars_lp".to_string(), + kind: None, }, ); assert_eq!(res.price, Decimal::from_ratio(1770000_u128, 10000_u128)); @@ -867,7 +888,7 @@ fn querying_xyk_lp_price() { let assets = vec![coin(6389, "uatom"), coin(1385000, "umars")]; deps.querier.set_query_pool_response( 10003, - prepare_query_pool_response( + prepare_query_balancer_pool_response( 10003, &assets, &[5000u64, 5000u64], @@ -879,6 +900,7 @@ fn querying_xyk_lp_price() { deps.as_ref(), QueryMsg::Price { denom: "uatom_umars_lp".to_string(), + kind: None, }, ); // Atom price: 88.5 @@ -889,244 +911,6 @@ fn querying_xyk_lp_price() { assert_eq!(res.price, Decimal::from_ratio(1769874_u128, 10000_u128)); } -#[test] -fn querying_pyth_price_if_publish_price_too_old() { - let mut deps = helpers::setup_test(); - - // price source used to convert USD to base_denom - helpers::set_price_source( - deps.as_mut(), - "usd", - OsmosisPriceSourceUnchecked::Fixed { - price: Decimal::from_str("1000000").unwrap(), - }, - ); - - let price_id = PriceIdentifier::from_hex( - "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", - ) - .unwrap(); - - let max_staleness = 30u64; - helpers::set_price_source( - deps.as_mut(), - "uatom", - OsmosisPriceSourceUnchecked::Pyth { - contract_addr: "pyth_contract_addr".to_string(), - price_feed_id: price_id, - max_staleness, - denom_decimals: 6u8, - }, - ); - - let price_publish_time = 1677157333u64; - let ema_price_publish_time = price_publish_time + max_staleness; - deps.querier.set_pyth_price( - price_id, - PriceFeedResponse { - price_feed: PriceFeed::new( - price_id, - Price { - price: 1371155677, - conf: 646723, - expo: -8, - publish_time: price_publish_time as i64, - }, - Price { - price: 1365133270, - conf: 574566, - expo: -8, - publish_time: ema_price_publish_time as i64, - }, - ), - }, - ); - - let res_err = entry::query( - deps.as_ref(), - mock_env_at_block_time(price_publish_time + max_staleness + 1u64), - QueryMsg::Price { - denom: "uatom".to_string(), - }, - ) - .unwrap_err(); - assert_eq!( - res_err, - ContractError::InvalidPrice { - reason: - "current price publish time is too old/stale. published: 1677157333, now: 1677157364" - .to_string() - } - ); -} - -#[test] -fn querying_pyth_price_if_signed() { - let mut deps = helpers::setup_test(); - - // price source used to convert USD to base_denom - helpers::set_price_source( - deps.as_mut(), - "usd", - OsmosisPriceSourceUnchecked::Fixed { - price: Decimal::from_str("1000000").unwrap(), - }, - ); - - let price_id = PriceIdentifier::from_hex( - "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", - ) - .unwrap(); - - let max_staleness = 30u64; - helpers::set_price_source( - deps.as_mut(), - "uatom", - OsmosisPriceSourceUnchecked::Pyth { - contract_addr: "pyth_contract_addr".to_string(), - price_feed_id: price_id, - max_staleness, - denom_decimals: 6u8, - }, - ); - - let publish_time = 1677157333u64; - deps.querier.set_pyth_price( - price_id, - PriceFeedResponse { - price_feed: PriceFeed::new( - price_id, - Price { - price: -1371155677, - conf: 646723, - expo: -8, - publish_time: publish_time as i64, - }, - Price { - price: -1365133270, - conf: 574566, - expo: -8, - publish_time: publish_time as i64, - }, - ), - }, - ); - - let res_err = entry::query( - deps.as_ref(), - mock_env_at_block_time(publish_time), - QueryMsg::Price { - denom: "uatom".to_string(), - }, - ) - .unwrap_err(); - assert_eq!( - res_err, - ContractError::InvalidPrice { - reason: "price can't be <= 0".to_string() - } - ); -} - -#[test] -fn querying_pyth_price_successfully() { - let mut deps = helpers::setup_test(); - - // price source used to convert USD to base_denom - helpers::set_price_source( - deps.as_mut(), - "usd", - OsmosisPriceSourceUnchecked::Fixed { - price: Decimal::from_str("1000000").unwrap(), - }, - ); - - let price_id = PriceIdentifier::from_hex( - "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", - ) - .unwrap(); - - let max_staleness = 30u64; - helpers::set_price_source( - deps.as_mut(), - "uatom", - OsmosisPriceSourceUnchecked::Pyth { - contract_addr: "pyth_contract_addr".to_string(), - price_feed_id: price_id, - max_staleness, - denom_decimals: 6u8, - }, - ); - - let publish_time = 1677157333u64; - - // exp < 0 - deps.querier.set_pyth_price( - price_id, - PriceFeedResponse { - price_feed: PriceFeed::new( - price_id, - Price { - price: 1021000, - conf: 50000, - expo: -4, - publish_time: publish_time as i64, - }, - Price { - price: 1000000, - conf: 40000, - expo: -4, - publish_time: publish_time as i64, - }, - ), - }, - ); - - let res = entry::query( - deps.as_ref(), - mock_env_at_block_time(publish_time), - QueryMsg::Price { - denom: "uatom".to_string(), - }, - ) - .unwrap(); - let res: PriceResponse = from_binary(&res).unwrap(); - assert_eq!(res.price, Decimal::from_ratio(1021000u128, 10000u128)); - - // exp > 0 - deps.querier.set_pyth_price( - price_id, - PriceFeedResponse { - price_feed: PriceFeed::new( - price_id, - Price { - price: 102, - conf: 5, - expo: 3, - publish_time: publish_time as i64, - }, - Price { - price: 100, - conf: 4, - expo: 3, - publish_time: publish_time as i64, - }, - ), - }, - ); - - let res = entry::query( - deps.as_ref(), - mock_env_at_block_time(publish_time), - QueryMsg::Price { - denom: "uatom".to_string(), - }, - ) - .unwrap(); - let res: PriceResponse = from_binary(&res).unwrap(); - assert_eq!(res.price, Decimal::from_ratio(102000u128, 1u128)); -} - #[test] fn querying_all_prices() { let mut deps = helpers::setup_test_with_pools(); @@ -1176,6 +960,7 @@ fn querying_all_prices() { QueryMsg::Prices { start_after: None, limit: None, + kind: None, }, ); assert_eq!( diff --git a/contracts/oracle/osmosis/tests/tests/test_query_price_for_pyth.rs b/contracts/oracle/osmosis/tests/tests/test_query_price_for_pyth.rs new file mode 100644 index 000000000..be8145645 --- /dev/null +++ b/contracts/oracle/osmosis/tests/tests/test_query_price_for_pyth.rs @@ -0,0 +1,616 @@ +use cosmwasm_std::{from_binary, Decimal}; +use mars_oracle_base::ContractError; +use mars_oracle_osmosis::{contract::entry, OsmosisPriceSourceUnchecked}; +use mars_red_bank_types::oracle::{ActionKind, PriceResponse, QueryMsg}; +use mars_testing::mock_env_at_block_time; +use pyth_sdk_cw::{Price, PriceFeed, PriceFeedResponse, PriceIdentifier}; + +use super::helpers; + +#[test] +fn querying_default_pyth_price_if_publish_price_too_old() { + let mut deps = helpers::setup_test_for_pyth(); + + let price_id = PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(); + + let max_staleness = 30u64; + helpers::set_price_source( + deps.as_mut(), + "uatom", + OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), + price_feed_id: price_id, + max_staleness, + max_confidence: Decimal::percent(10u64), + max_deviation: Decimal::percent(15u64), + denom_decimals: 6u8, + }, + ); + + let price_publish_time = 1677157333u64; + let ema_price_publish_time = price_publish_time + max_staleness; + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: 1371155677, + conf: 646723, + expo: -8, + publish_time: price_publish_time as i64, + }, + Price { + price: 1365133270, + conf: 574566, + expo: -8, + publish_time: ema_price_publish_time as i64, + }, + ), + }, + ); + + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(price_publish_time + max_staleness + 1u64), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Default), + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::InvalidPrice { + reason: + "current price publish time is too old/stale. published: 1677157333, now: 1677157364" + .to_string() + } + ); + + let ema_price_publish_time = 1677157333u64; + let price_publish_time = ema_price_publish_time + max_staleness; + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: 1371155677, + conf: 646723, + expo: -8, + publish_time: price_publish_time as i64, + }, + Price { + price: 1365133270, + conf: 574566, + expo: -8, + publish_time: ema_price_publish_time as i64, + }, + ), + }, + ); + + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(ema_price_publish_time + max_staleness + 1u64), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Default), + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::InvalidPrice { + reason: + "EMA price publish time is too old/stale. published: 1677157333, now: 1677157364" + .to_string() + } + ); +} + +#[test] +fn querying_liquidation_pyth_price_if_publish_price_too_old() { + let mut deps = helpers::setup_test_for_pyth(); + + let price_id = PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(); + + let max_staleness = 30u64; + helpers::set_price_source( + deps.as_mut(), + "uatom", + OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), + price_feed_id: price_id, + max_staleness, + max_confidence: Decimal::percent(10u64), + max_deviation: Decimal::percent(15u64), + denom_decimals: 6u8, + }, + ); + + let price_publish_time = 1677157333u64; + let ema_price_publish_time = price_publish_time + max_staleness; + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: 1371155677, + conf: 646723, + expo: -8, + publish_time: price_publish_time as i64, + }, + Price { + price: 1365133270, + conf: 574566, + expo: -8, + publish_time: ema_price_publish_time as i64, + }, + ), + }, + ); + + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(price_publish_time + max_staleness + 1u64), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Liquidation), + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::InvalidPrice { + reason: + "current price publish time is too old/stale. published: 1677157333, now: 1677157364" + .to_string() + } + ); +} + +#[test] +fn querying_default_pyth_price_if_signed() { + let mut deps = helpers::setup_test_for_pyth(); + + let price_id = PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(); + + let max_staleness = 30u64; + helpers::set_price_source( + deps.as_mut(), + "uatom", + OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), + price_feed_id: price_id, + max_staleness, + max_confidence: Decimal::percent(10u64), + max_deviation: Decimal::percent(15u64), + denom_decimals: 6u8, + }, + ); + + let publish_time = 1677157333u64; + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: -1371155677, + conf: 646723, + expo: -8, + publish_time: publish_time as i64, + }, + Price { + price: -1365133270, + conf: 574566, + expo: -8, + publish_time: publish_time as i64, + }, + ), + }, + ); + + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Default), + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::InvalidPrice { + reason: "price can't be <= 0".to_string() + } + ); +} + +#[test] +fn querying_liquidation_pyth_price_if_signed() { + let mut deps = helpers::setup_test_for_pyth(); + + let price_id = PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(); + + let max_staleness = 30u64; + helpers::set_price_source( + deps.as_mut(), + "uatom", + OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), + price_feed_id: price_id, + max_staleness, + max_confidence: Decimal::percent(10u64), + max_deviation: Decimal::percent(15u64), + denom_decimals: 6u8, + }, + ); + + let publish_time = 1677157333u64; + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: -1371155677, + conf: 646723, + expo: -8, + publish_time: publish_time as i64, + }, + Price { + price: -1365133270, + conf: 574566, + expo: -8, + publish_time: publish_time as i64, + }, + ), + }, + ); + + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Liquidation), + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::InvalidPrice { + reason: "price can't be <= 0".to_string() + } + ); +} + +#[test] +fn querying_pyth_price_if_confidence_exceeded() { + let mut deps = helpers::setup_test_for_pyth(); + + let price_id = PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(); + + let max_staleness = 30u64; + helpers::set_price_source( + deps.as_mut(), + "uatom", + OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), + price_feed_id: price_id, + max_staleness, + max_confidence: Decimal::percent(5u64), + max_deviation: Decimal::percent(6u64), + denom_decimals: 6u8, + }, + ); + + let publish_time = 1677157333u64; + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: 1010000, + conf: 51000, + expo: -4, + publish_time: publish_time as i64, + }, + Price { + price: 1000000, + conf: 40000, + expo: -4, + publish_time: publish_time as i64, + }, + ), + }, + ); + + // should fail for Default pricing + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Default), + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::InvalidPrice { + reason: "price confidence deviation 0.051 exceeds max allowed 0.05".to_string() + } + ); + + // should succeed for Liquidation pricing + let res = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Liquidation), + }, + ) + .unwrap(); + let res: PriceResponse = from_binary(&res).unwrap(); + assert_eq!(res.price, Decimal::from_ratio(101u128, 1u128)); +} + +#[test] +fn querying_pyth_price_if_deviation_exceeded() { + let mut deps = helpers::setup_test_for_pyth(); + + let price_id = PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(); + + let max_staleness = 30u64; + helpers::set_price_source( + deps.as_mut(), + "uatom", + OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), + price_feed_id: price_id, + max_staleness, + max_confidence: Decimal::percent(5u64), + max_deviation: Decimal::percent(6u64), + denom_decimals: 6u8, + }, + ); + + let publish_time = 1677157333u64; + + // price > ema_price + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: 1061000, + conf: 50000, + expo: -4, + publish_time: publish_time as i64, + }, + Price { + price: 1000000, + conf: 40000, + expo: -4, + publish_time: publish_time as i64, + }, + ), + }, + ); + + // should fail for Default pricing + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Default), + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::InvalidPrice { + reason: "price deviation 0.061 exceeds max allowed 0.06".to_string() + } + ); + + // should succeed for Liquidation pricing + let res = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Liquidation), + }, + ) + .unwrap(); + let res: PriceResponse = from_binary(&res).unwrap(); + assert_eq!(res.price, Decimal::from_ratio(1061u128, 10u128)); + + // ema_price > price + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: 939999, + conf: 50000, + expo: -4, + publish_time: publish_time as i64, + }, + Price { + price: 1000000, + conf: 40000, + expo: -4, + publish_time: publish_time as i64, + }, + ), + }, + ); + + // should fail for Default pricing + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Default), + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::InvalidPrice { + reason: "price deviation 0.060001 exceeds max allowed 0.06".to_string() + } + ); + + // should succeed for Liquidation pricing + let res = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Liquidation), + }, + ) + .unwrap(); + let res: PriceResponse = from_binary(&res).unwrap(); + assert_eq!(res.price, Decimal::from_ratio(939999u128, 10000u128)); +} + +#[test] +fn querying_pyth_price_successfully() { + let mut deps = helpers::setup_test_for_pyth(); + + let price_id = PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(); + + let max_staleness = 30u64; + helpers::set_price_source( + deps.as_mut(), + "uatom", + OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), + price_feed_id: price_id, + max_staleness, + max_confidence: Decimal::percent(10u64), + max_deviation: Decimal::percent(15u64), + denom_decimals: 6u8, + }, + ); + + let publish_time = 1677157333u64; + + // exp < 0 + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: 1021000, + conf: 50000, + expo: -4, + publish_time: publish_time as i64, + }, + Price { + price: 1000000, + conf: 40000, + expo: -4, + publish_time: publish_time as i64, + }, + ), + }, + ); + + let res = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: None, + }, + ) + .unwrap(); + let res: PriceResponse = from_binary(&res).unwrap(); + assert_eq!(res.price, Decimal::from_ratio(1021000u128, 10000u128)); + + // exp > 0 + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: 102, + conf: 5, + expo: 3, + publish_time: publish_time as i64, + }, + Price { + price: 100, + conf: 4, + expo: 3, + publish_time: publish_time as i64, + }, + ), + }, + ); + + let default_res = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Default), + }, + ) + .unwrap(); + let default_res: PriceResponse = from_binary(&default_res).unwrap(); + assert_eq!(default_res.price, Decimal::from_ratio(102000u128, 1u128)); + + let liq_res = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Liquidation), + }, + ) + .unwrap(); + let liq_res: PriceResponse = from_binary(&liq_res).unwrap(); + // Price for default and liquidation actions should be the same + assert_eq!(liq_res.price, default_res.price); +} diff --git a/contracts/oracle/osmosis/tests/test_remove_price_source.rs b/contracts/oracle/osmosis/tests/tests/test_remove_price_source.rs similarity index 99% rename from contracts/oracle/osmosis/tests/test_remove_price_source.rs rename to contracts/oracle/osmosis/tests/tests/test_remove_price_source.rs index 7e41ffc42..330622199 100644 --- a/contracts/oracle/osmosis/tests/test_remove_price_source.rs +++ b/contracts/oracle/osmosis/tests/tests/test_remove_price_source.rs @@ -9,7 +9,7 @@ use mars_owner::OwnerError::NotOwner; use mars_red_bank_types::oracle::msg::QueryMsg; use mars_testing::mock_info; -mod helpers; +use super::helpers; #[test] fn remove_price_source_by_non_owner() { diff --git a/contracts/oracle/osmosis/tests/test_set_price_source.rs b/contracts/oracle/osmosis/tests/tests/test_set_price_source.rs similarity index 81% rename from contracts/oracle/osmosis/tests/test_set_price_source.rs rename to contracts/oracle/osmosis/tests/tests/test_set_price_source.rs index 6712264eb..11613758d 100644 --- a/contracts/oracle/osmosis/tests/test_set_price_source.rs +++ b/contracts/oracle/osmosis/tests/tests/test_set_price_source.rs @@ -11,9 +11,10 @@ use mars_oracle_osmosis::{ use mars_owner::OwnerError::NotOwner; use mars_red_bank_types::oracle::msg::QueryMsg; use mars_testing::mock_info; +use mars_utils::error::ValidationError; use pyth_sdk_cw::PriceIdentifier; -mod helpers; +use super::helpers; #[test] fn setting_price_source_by_non_owner() { @@ -83,9 +84,9 @@ fn setting_price_source_incorrect_denom() { ); assert_eq!( res, - Err(ContractError::InvalidDenom { + Err(ContractError::Validation(ValidationError::InvalidDenom { reason: "First character is not ASCII alphabetic".to_string() - }) + })) ); let res_two = execute( @@ -101,10 +102,10 @@ fn setting_price_source_incorrect_denom() { ); assert_eq!( res_two, - Err(ContractError::InvalidDenom { + Err(ContractError::Validation(ValidationError::InvalidDenom { reason: "Not all characters are ASCII alphanumeric or one of: / : . _ -" .to_string() - }) + })) ); let res_three = execute( @@ -120,9 +121,9 @@ fn setting_price_source_incorrect_denom() { ); assert_eq!( res_three, - Err(ContractError::InvalidDenom { + Err(ContractError::Validation(ValidationError::InvalidDenom { reason: "Invalid denom length".to_string() - }) + })) ); } @@ -144,6 +145,15 @@ fn setting_price_source_spot() { ) }; + // attempting to set price source for base denom; should fail + let err = set_price_source_spot("uosmo", 1).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "denom and base denom can't be the same".to_string() + } + ); + // attempting to use a pool that does not contain the denom of interest; should fail let err = set_price_source_spot("umars", 1).unwrap_err(); assert_eq!( @@ -180,6 +190,15 @@ fn setting_price_source_spot() { } ); + // attempting to use a StableSwap pool that contains more than two assets; should fail + let err = set_price_source_spot("uatom", 6666).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "expecting pool 6666 to contain exactly two coins; found 3".to_string() + } + ); + // properly set spot price source let res = set_price_source_spot("umars", 89).unwrap(); assert_eq!(res.messages.len(), 0); @@ -222,6 +241,15 @@ fn setting_price_source_arithmetic_twap_with_invalid_params() { ) }; + // attempting to set price source for base denom; should fail + let err = set_price_source_twap("uosmo", 1, 86400, None).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "denom and base denom can't be the same".to_string() + } + ); + // attempting to use a pool that does not contain the denom of interest; should fail let err = set_price_source_twap("umars", 1, 86400, None).unwrap_err(); assert_eq!( @@ -258,6 +286,15 @@ fn setting_price_source_arithmetic_twap_with_invalid_params() { } ); + // attempting to use a StableSwap pool that contains more than two assets; should fail + let err = set_price_source_twap("uatom", 6666, 86400, None).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "expecting pool 6666 to contain exactly two coins; found 3".to_string() + } + ); + // attempting to set window_size bigger than 172800 sec (48h) let err = set_price_source_twap("umars", 89, 172801, None).unwrap_err(); assert_eq!( @@ -385,6 +422,15 @@ fn setting_price_source_geometric_twap_with_invalid_params() { ) }; + // attempting to set price source for base denom; should fail + let err = set_price_source_twap("uosmo", 1, 86400, None).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "denom and base denom can't be the same".to_string() + } + ); + // attempting to use a pool that does not contain the denom of interest; should fail let err = set_price_source_twap("umars", 1, 86400, None).unwrap_err(); assert_eq!( @@ -421,6 +467,15 @@ fn setting_price_source_geometric_twap_with_invalid_params() { } ); + // attempting to use a StableSwap pool that contains more than two assets; should fail + let err = set_price_source_twap("uatom", 6666, 86400, None).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "expecting pool 6666 to contain exactly two coins; found 3".to_string() + } + ); + // attempting to set window_size bigger than 172800 sec (48h) let err = set_price_source_twap("umars", 89, 172801, None).unwrap_err(); assert_eq!( @@ -550,6 +605,24 @@ fn setting_price_source_staked_geometric_twap_with_invalid_params() { ) }; + // attempting to set price source for base denom; should fail + let err = set_price_source_twap("uosmo", "uosmo", 1, 86400, None).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "denom and base denom can't be the same".to_string() + } + ); + + // attempting to set price source with invalid transitive denom; should fail + let err = set_price_source_twap("ustatom", "!*jadfaefc", 803, 86400, None).unwrap_err(); + assert_eq!( + err, + ContractError::Validation(ValidationError::InvalidDenom { + reason: "First character is not ASCII alphabetic".to_string() + }) + ); + // attempting to use a pool that does not contain the denom of interest; should fail let err = set_price_source_twap("ustatom", "umars", 803, 86400, None).unwrap_err(); assert_eq!( @@ -576,7 +649,7 @@ fn setting_price_source_staked_geometric_twap_with_invalid_params() { ); // attempting to use not XYK pool - let err = set_price_source_twap("ustatom", "uatom", 4444, 86400, None).unwrap_err(); + let err = set_price_source_twap("uion", "uosmo", 4444, 86400, None).unwrap_err(); assert_eq!( err, ContractError::InvalidPriceSource { @@ -584,6 +657,15 @@ fn setting_price_source_staked_geometric_twap_with_invalid_params() { } ); + // attempting to use a StableSwap pool that contains more than two assets; should fail + let err = set_price_source_twap("uatom", "uusdc", 6666, 86400, None).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "expecting pool 6666 to contain exactly two coins; found 3".to_string() + } + ); + // attempting to set window_size bigger than 172800 sec (48h) let err = set_price_source_twap("ustatom", "uatom", 803, 172801, None).unwrap_err(); assert_eq!( @@ -724,6 +806,24 @@ fn setting_price_source_lsd_with_invalid_params() { ) }; + // attempting to set price source for base denom; should fail + let err = set_price_source_twap("uosmo", "uosmo", 1, 86400, None).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "denom and base denom can't be the same".to_string() + } + ); + + // attempting to set price source with invalid transitive denom; should fail + let err = set_price_source_twap("ustatom", "!*jadfaefc", 3333, 86400, None).unwrap_err(); + assert_eq!( + err, + ContractError::Validation(ValidationError::InvalidDenom { + reason: "First character is not ASCII alphabetic".to_string() + }) + ); + // attempting to use a pool that does not contain the denom of interest; should fail let err = set_price_source_twap("ustatom", "umars", 803, 86400, None).unwrap_err(); assert_eq!( @@ -750,7 +850,7 @@ fn setting_price_source_lsd_with_invalid_params() { ); // attempting to use not XYK pool - let err = set_price_source_twap("ustatom", "uatom", 4444, 86400, None).unwrap_err(); + let err = set_price_source_twap("uion", "uosmo", 4444, 86400, None).unwrap_err(); assert_eq!( err, ContractError::InvalidPriceSource { @@ -758,6 +858,15 @@ fn setting_price_source_lsd_with_invalid_params() { } ); + // attempting to use a StableSwap pool that contains more than two assets; should fail + let err = set_price_source_twap("uatom", "uusdc", 6666, 86400, None).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "expecting pool 6666 to contain exactly two coins; found 3".to_string() + } + ); + // attempting to set window_size bigger than 172800 sec (48h) let err = set_price_source_twap("ustatom", "uatom", 803, 172801, None).unwrap_err(); assert_eq!( @@ -926,6 +1035,24 @@ fn setting_price_source_xyk_lp() { } ); + // attempting to use StableSwap pool + let err = set_price_source_xyk_lp("atom_uosmo_lp", 5555).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "StableSwap pool not supported. Pool id 5555".to_string() + } + ); + + // attempting to use ConcentratedLiquid pool + let err = set_price_source_xyk_lp("ujuno_uosmo_lp", 7777).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "ConcentratedLiquidity pool not supported. Pool id 7777".to_string() + } + ); + // properly set xyk lp price source let res = set_price_source_xyk_lp("uosmo_umars_lp", 89).unwrap(); assert_eq!(res.messages.len(), 0); @@ -944,6 +1071,61 @@ fn setting_price_source_xyk_lp() { ); } +#[test] +fn setting_price_source_pyth_with_invalid_params() { + let mut deps = helpers::setup_test(); + + let mut set_price_source_pyth = + |max_confidence: Decimal, max_deviation: Decimal, denom_decimals: u8| { + execute( + deps.as_mut(), + mock_env(), + mock_info("owner"), + ExecuteMsg::SetPriceSource { + denom: "uatom".to_string(), + price_source: OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), + price_feed_id: PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(), + max_staleness: 30, + max_confidence, + max_deviation, + denom_decimals, + }, + }, + ) + }; + + // attempting to set max_confidence > 20%; should fail + let err = set_price_source_pyth(Decimal::percent(21), Decimal::percent(6), 6).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "max_confidence must be in the range of <0;0.2>".to_string() + } + ); + + // attempting to set max_deviation > 20%; should fail + let err = set_price_source_pyth(Decimal::percent(5), Decimal::percent(21), 18).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "max_deviation must be in the range of <0;0.2>".to_string() + } + ); + + // attempting to set denom_decimals > 18; should fail + let err = set_price_source_pyth(Decimal::percent(5), Decimal::percent(20), 19).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "denom_decimals must be <= 18".to_string() + } + ); +} + #[test] fn setting_price_source_pyth_if_missing_usd() { let mut deps = helpers::setup_test(); @@ -961,6 +1143,8 @@ fn setting_price_source_pyth_if_missing_usd() { ) .unwrap(), max_staleness: 30, + max_confidence: Decimal::percent(10), + max_deviation: Decimal::percent(10), denom_decimals: 8, }, }, @@ -1000,6 +1184,8 @@ fn setting_price_source_pyth_successfully() { ) .unwrap(), max_staleness: 30, + max_confidence: Decimal::percent(12), + max_deviation: Decimal::percent(14), denom_decimals: 8, }, }, @@ -1022,7 +1208,9 @@ fn setting_price_source_pyth_successfully() { ) .unwrap(), max_staleness: 30, - denom_decimals: 8 + max_confidence: Decimal::percent(12), + max_deviation: Decimal::percent(14), + denom_decimals: 8, }, ); } diff --git a/contracts/oracle/osmosis/tests/test_update_owner.rs b/contracts/oracle/osmosis/tests/tests/test_update_owner.rs similarity index 97% rename from contracts/oracle/osmosis/tests/test_update_owner.rs rename to contracts/oracle/osmosis/tests/tests/test_update_owner.rs index 21130aa84..c56f49c64 100644 --- a/contracts/oracle/osmosis/tests/test_update_owner.rs +++ b/contracts/oracle/osmosis/tests/tests/test_update_owner.rs @@ -1,12 +1,11 @@ use cosmwasm_std::testing::{mock_env, mock_info}; +use helpers::{query, setup_test_with_pools}; use mars_oracle_base::ContractError; use mars_oracle_osmosis::contract::entry::execute; use mars_owner::{OwnerError::NotOwner, OwnerUpdate}; use mars_red_bank_types::oracle::msg::{ConfigResponse, ExecuteMsg, QueryMsg}; -use crate::helpers::{query, setup_test_with_pools}; - -mod helpers; +use super::helpers; #[test] fn initialized_state() { diff --git a/contracts/oracle/wasm/Cargo.toml b/contracts/oracle/wasm/Cargo.toml index 0a7147f40..502618919 100644 --- a/contracts/oracle/wasm/Cargo.toml +++ b/contracts/oracle/wasm/Cargo.toml @@ -14,29 +14,26 @@ keywords = { workspace = true } crate-type = ["cdylib", "rlib"] doctest = false -[profile.release] -overflow-checks = true - [features] # for more explicit tests, cargo test --features=backtraces -backtraces = ["cosmwasm-std/backtraces"] -library = [] +backtraces = ["cosmwasm-std/backtraces"] +library = [] osmosis-test-app = ["cw-it/osmosis-test-tube", "mars-testing/osmosis-test-tube"] [dependencies] +astroport = { workspace = true } +cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw2 = { workspace = true } cw-storage-plus = { workspace = true } -mars-oracle-base = { workspace = true, features = ["pyth"] } +mars-oracle-base = { workspace = true } mars-red-bank-types = { workspace = true } -cosmwasm-schema = { workspace = true } -astroport = { workspace = true } +pyth-sdk-cw = { workspace = true } [dev-dependencies] cosmwasm-schema = { workspace = true } -mars-testing = { workspace = true, features = ["astroport"] } -mars-owner = { workspace = true } cw-it = { workspace = true, features = ["astroport", "astroport-multi-test"] } -test-case = { workspace = true } +mars-owner = { workspace = true } +mars-testing = { workspace = true, features = ["astroport"] } proptest = { workspace = true } -pyth-sdk-cw = { workspace = true } +test-case = { workspace = true } diff --git a/contracts/oracle/wasm/src/helpers.rs b/contracts/oracle/wasm/src/helpers.rs index 07fbfbd70..1305acb4c 100644 --- a/contracts/oracle/wasm/src/helpers.rs +++ b/contracts/oracle/wasm/src/helpers.rs @@ -10,7 +10,7 @@ use cosmwasm_std::{ }; use cw_storage_plus::Map; use mars_oracle_base::{ContractError, ContractResult, PriceSourceChecked}; -use mars_red_bank_types::oracle::{AstroportTwapSnapshot, Config}; +use mars_red_bank_types::oracle::{ActionKind, AstroportTwapSnapshot, Config}; use crate::WasmPriceSourceChecked; @@ -168,6 +168,7 @@ pub fn normalize_price( pair_info: &PairInfo, denom: &str, price: Decimal, + kind: ActionKind, ) -> ContractResult { let pair_denoms = get_astroport_pair_denoms(pair_info)?; @@ -177,8 +178,14 @@ pub fn normalize_price( let other_pair_denom = get_other_astroport_pair_denom(&pair_denoms, denom)?; let other_price_source = price_sources.load(deps.storage, &other_pair_denom)?; - let other_price = - other_price_source.query_price(deps, env, &other_pair_denom, config, price_sources)?; + let other_price = other_price_source.query_price( + deps, + env, + &other_pair_denom, + config, + price_sources, + kind, + )?; Ok(price.checked_mul(other_price)?) } diff --git a/contracts/oracle/wasm/src/price_source.rs b/contracts/oracle/wasm/src/price_source.rs index b90917470..4df7140a7 100644 --- a/contracts/oracle/wasm/src/price_source.rs +++ b/contracts/oracle/wasm/src/price_source.rs @@ -5,11 +5,11 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Decimal, Deps, Empty, Env, Uint128}; use cw_storage_plus::Map; use mars_oracle_base::{ - pyth::PriceIdentifier, ContractError::{self}, ContractResult, PriceSourceChecked, PriceSourceUnchecked, }; -use mars_red_bank_types::oracle::{AstroportTwapSnapshot, Config}; +use mars_red_bank_types::oracle::{ActionKind, AstroportTwapSnapshot, Config}; +use pyth_sdk_cw::PriceIdentifier; use crate::{ helpers::{ @@ -54,12 +54,24 @@ pub enum WasmPriceSource { contract_addr: A, /// Price feed id of an asset from the list: https://pyth.network/developers/price-feed-ids + /// We can't verify what denoms consist of the price feed. + /// Be very careful when adding it !!! price_feed_id: PriceIdentifier, /// The maximum number of seconds since the last price was by an oracle, before /// rejecting the price as too stale max_staleness: u64, + /// The maximum confidence deviation allowed for an oracle price. + /// + /// The confidence is measured as the percent of the confidence interval + /// value provided by the oracle as compared to the weighted average value + /// of the price. + max_confidence: Decimal, + + /// The maximum deviation (percentage) between current and EMA price + max_deviation: Decimal, + /// Assets are represented in their smallest unit and every asset can have different decimals (e.g. OSMO - 6 decimals, WETH - 18 decimals). /// /// Pyth prices are denominated in USD so basically it means how much 1 USDC, 1 ATOM, 1 OSMO is worth in USD (NOT 1 uusdc, 1 uatom, 1 uosmo). @@ -104,8 +116,10 @@ impl fmt::Display for WasmPriceSourceChecked { contract_addr, price_feed_id, max_staleness, - denom_decimals, - } => format!("pyth:{contract_addr}:{price_feed_id}:{max_staleness}:{denom_decimals}"), + max_confidence, + max_deviation, + denom_decimals + } => format!("pyth:{contract_addr}:{price_feed_id}:{max_staleness}:{max_confidence}:{max_deviation}:{denom_decimals}"), }; write!(f, "{label}") } @@ -183,14 +197,18 @@ impl PriceSourceUnchecked for WasmPriceSourceUnch contract_addr, price_feed_id, max_staleness, + max_confidence, + max_deviation, denom_decimals, } => { + mars_oracle_base::pyth::assert_pyth(max_confidence, max_deviation, denom_decimals)?; mars_oracle_base::pyth::assert_usd_price_source(deps, price_sources)?; - Ok(WasmPriceSourceChecked::Pyth { contract_addr: deps.api.addr_validate(&contract_addr)?, price_feed_id, max_staleness, + max_confidence, + max_deviation, denom_decimals, }) } @@ -207,6 +225,7 @@ impl PriceSourceChecked for WasmPriceSourceChecked { denom: &str, config: &Config, price_sources: &Map<&str, Self>, + kind: ActionKind, ) -> ContractResult { match self { WasmPriceSource::Fixed { @@ -214,7 +233,15 @@ impl PriceSourceChecked for WasmPriceSourceChecked { } => Ok(*price), WasmPriceSource::AstroportSpot { pair_address, - } => query_astroport_spot_price(deps, env, denom, config, price_sources, pair_address), + } => query_astroport_spot_price( + deps, + env, + denom, + config, + price_sources, + pair_address, + kind, + ), WasmPriceSource::AstroportTwap { pair_address, window_size, @@ -228,11 +255,14 @@ impl PriceSourceChecked for WasmPriceSourceChecked { pair_address, *window_size, *tolerance, + kind, ), WasmPriceSource::Pyth { contract_addr, price_feed_id, max_staleness, + max_confidence, + max_deviation, denom_decimals, } => mars_oracle_base::pyth::query_pyth_price( deps, @@ -240,9 +270,12 @@ impl PriceSourceChecked for WasmPriceSourceChecked { contract_addr.clone(), *price_feed_id, *max_staleness, + *max_confidence, + *max_deviation, *denom_decimals, config, price_sources, + kind, ), } } @@ -256,6 +289,7 @@ fn query_astroport_spot_price( config: &Config, price_sources: &Map<&str, WasmPriceSourceChecked>, pair_address: &Addr, + kind: ActionKind, ) -> ContractResult { let astroport_factory = ASTROPORT_FACTORY.load(deps.storage)?; let pair_info = query_astroport_pair_info(&deps.querier, pair_address)?; @@ -270,7 +304,7 @@ fn query_astroport_spot_price( let price = Decimal::from_ratio(sim_res.return_amount, one); - normalize_price(deps, env, config, price_sources, &pair_info, denom, price) + normalize_price(deps, env, config, price_sources, &pair_info, denom, price, kind) } /// Queries the TWAP price of `denom` denominated in `base_denom` from the Astroport pair at `pair_address`. @@ -284,6 +318,7 @@ fn query_astroport_twap_price( pair_address: &Addr, window_size: u64, tolerance: u64, + kind: ActionKind, ) -> ContractResult { let snapshots = ASTROPORT_TWAP_SNAPSHOTS .may_load(deps.storage, denom)? @@ -382,7 +417,7 @@ fn query_astroport_twap_price( PairType::Custom(_) => return Err(ContractError::InvalidPairType {}), }; - normalize_price(deps, env, config, price_sources, &pair_info, denom, price) + normalize_price(deps, env, config, price_sources, &pair_info, denom, price, kind) } #[cfg(test)] @@ -398,11 +433,13 @@ mod tests { ) .unwrap(), max_staleness: 60, - denom_decimals: 6, + max_confidence: Decimal::percent(10u64), + max_deviation: Decimal::percent(15u64), + denom_decimals: 18, }; assert_eq!( - ps.to_string(), - "pyth:osmo12j43nf2f0qumnt2zrrmpvnsqgzndxefujlvr08:0x61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3:60:6" - ) + ps.to_string(), + "pyth:osmo12j43nf2f0qumnt2zrrmpvnsqgzndxefujlvr08:0x61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3:60:0.1:0.15:18" + ) } } diff --git a/contracts/oracle/wasm/tests/all_tests.rs b/contracts/oracle/wasm/tests/all_tests.rs new file mode 100644 index 000000000..14f00389d --- /dev/null +++ b/contracts/oracle/wasm/tests/all_tests.rs @@ -0,0 +1 @@ +mod tests; diff --git a/contracts/oracle/wasm/tests/helpers.rs b/contracts/oracle/wasm/tests/tests/helpers/mod.rs similarity index 100% rename from contracts/oracle/wasm/tests/helpers.rs rename to contracts/oracle/wasm/tests/tests/helpers/mod.rs diff --git a/contracts/oracle/wasm/tests/tests/mod.rs b/contracts/oracle/wasm/tests/tests/mod.rs new file mode 100644 index 000000000..2d3303374 --- /dev/null +++ b/contracts/oracle/wasm/tests/tests/mod.rs @@ -0,0 +1,6 @@ +mod helpers; + +mod prop_tests; +mod test_migrate; +mod test_price_source; +mod test_update_admin; diff --git a/contracts/oracle/wasm/tests/prop_tests.proptest-regressions b/contracts/oracle/wasm/tests/tests/prop_tests.proptest-regressions similarity index 100% rename from contracts/oracle/wasm/tests/prop_tests.proptest-regressions rename to contracts/oracle/wasm/tests/tests/prop_tests.proptest-regressions diff --git a/contracts/oracle/wasm/tests/prop_tests.rs b/contracts/oracle/wasm/tests/tests/prop_tests.rs similarity index 100% rename from contracts/oracle/wasm/tests/prop_tests.rs rename to contracts/oracle/wasm/tests/tests/prop_tests.rs diff --git a/contracts/oracle/wasm/tests/test_migrate.rs b/contracts/oracle/wasm/tests/tests/test_migrate.rs similarity index 100% rename from contracts/oracle/wasm/tests/test_migrate.rs rename to contracts/oracle/wasm/tests/tests/test_migrate.rs diff --git a/contracts/oracle/wasm/tests/test_price_source.rs b/contracts/oracle/wasm/tests/tests/test_price_source.rs similarity index 88% rename from contracts/oracle/wasm/tests/test_price_source.rs rename to contracts/oracle/wasm/tests/tests/test_price_source.rs index b8e445fa4..18e0353ee 100644 --- a/contracts/oracle/wasm/tests/test_price_source.rs +++ b/contracts/oracle/wasm/tests/tests/test_price_source.rs @@ -16,12 +16,13 @@ use cw_it::{ traits::CwItRunner, }; use cw_storage_plus::Map; -use mars_oracle_base::{pyth::PriceIdentifier, ContractError, PriceSourceUnchecked}; +use mars_oracle_base::{ContractError, PriceSourceUnchecked}; use mars_oracle_wasm::{ contract::entry::{self, execute}, WasmPriceSource, WasmPriceSourceChecked, WasmPriceSourceUnchecked, }; use mars_red_bank_types::oracle::{ExecuteMsg, PriceResponse, QueryMsg}; +use pyth_sdk_cw::PriceIdentifier; const ONE: Decimal = Decimal::one(); const TWO: Decimal = Decimal::new(Uint128::new(2_000_000_000_000_000_000u128)); @@ -39,7 +40,7 @@ use mars_testing::{ use pyth_sdk_cw::{Price, PriceFeed, PriceFeedResponse}; use test_case::test_case; -mod helpers; +use super::helpers; #[test] fn test_contract_initialization() { @@ -285,6 +286,7 @@ fn test_query_astroport_twap_price_with_only_one_snapshot() { &robot.mars_oracle_contract_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap_err(); @@ -372,6 +374,8 @@ fn querying_pyth_price_if_publish_price_too_old() { contract_addr: "pyth_contract_addr".to_string(), price_feed_id: price_id, max_staleness, + max_confidence: Decimal::percent(12), + max_deviation: Decimal::percent(14), denom_decimals: 6, }, ); @@ -404,6 +408,7 @@ fn querying_pyth_price_if_publish_price_too_old() { mock_env_at_block_time(price_publish_time + max_staleness + 1u64), QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap_err(); @@ -451,6 +456,8 @@ fn querying_pyth_price_if_signed() { contract_addr: "pyth_contract_addr".to_string(), price_feed_id: price_id, max_staleness, + max_confidence: Decimal::percent(12), + max_deviation: Decimal::percent(14), denom_decimals: 6, }, ); @@ -482,6 +489,7 @@ fn querying_pyth_price_if_signed() { mock_env_at_block_time(publish_time), QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap_err(); @@ -527,6 +535,8 @@ fn querying_pyth_price_successfully() { contract_addr: "pyth_contract_addr".to_string(), price_feed_id: price_id, max_staleness, + max_confidence: Decimal::percent(12), + max_deviation: Decimal::percent(14), denom_decimals: 6, }, ); @@ -560,6 +570,7 @@ fn querying_pyth_price_successfully() { mock_env_at_block_time(publish_time), QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -593,6 +604,7 @@ fn querying_pyth_price_successfully() { mock_env_at_block_time(publish_time), QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -627,6 +639,8 @@ fn setting_price_source_pyth_if_missing_usd() { contract_addr: "new_pyth_contract_addr".to_string(), price_feed_id: price_id, max_staleness: 30, + max_confidence: Decimal::percent(10), + max_deviation: Decimal::percent(10), denom_decimals: 8, }, }, @@ -640,6 +654,69 @@ fn setting_price_source_pyth_if_missing_usd() { ); } +#[test] +fn setting_price_source_pyth_with_invalid_params() { + let runner = get_test_runner(); + let robot = WasmOracleTestRobot::new( + &runner, + get_contracts(&get_test_runner()), + &get_test_runner().init_default_accounts().unwrap()[0], + None, + ); + + let mut deps = helpers::setup_test(&robot.astroport_contracts.factory.address); + + let mut set_price_source_pyth = + |max_confidence: Decimal, max_deviation: Decimal, denom_decimals: u8| { + execute( + deps.as_mut(), + mock_env(), + mock_info("owner"), + ExecuteMsg::SetPriceSource { + denom: "uatom".to_string(), + price_source: WasmPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), + price_feed_id: PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(), + max_staleness: 30, + max_confidence, + max_deviation, + denom_decimals, + }, + }, + ) + }; + + // attempting to set max_confidence > 20%; should fail + let err = set_price_source_pyth(Decimal::percent(21), Decimal::percent(6), 6).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "max_confidence must be in the range of <0;0.2>".to_string() + } + ); + + // attempting to set max_deviation > 20%; should fail + let err = set_price_source_pyth(Decimal::percent(5), Decimal::percent(21), 18).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "max_deviation must be in the range of <0;0.2>".to_string() + } + ); + + // attempting to set denom_decimals > 18; should fail + let err = set_price_source_pyth(Decimal::percent(5), Decimal::percent(20), 19).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "denom_decimals must be <= 18".to_string() + } + ); +} + #[test] fn twap_window_size_not_gt_tolerance() { let runner = get_test_runner(); diff --git a/contracts/oracle/wasm/tests/test_update_admin.rs b/contracts/oracle/wasm/tests/tests/test_update_admin.rs similarity index 100% rename from contracts/oracle/wasm/tests/test_update_admin.rs rename to contracts/oracle/wasm/tests/tests/test_update_admin.rs diff --git a/contracts/params/Cargo.toml b/contracts/params/Cargo.toml new file mode 100644 index 000000000..78ac63bee --- /dev/null +++ b/contracts/params/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "mars-params" +description = "Contract storing the asset params for Credit Manager and Red Bank." +version = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +keywords = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] +doctest = false + +[features] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +library = [] + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw2 = { workspace = true } +cw-storage-plus = { workspace = true } +mars-interest-rate = { workspace = true } +mars-owner = { workspace = true } +mars-red-bank-types = { workspace = true } +mars-utils = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +anyhow = { workspace = true } +cw-multi-test = { workspace = true } +mars-testing = { workspace = true } +test-case = { workspace = true } diff --git a/contracts/params/README.md b/contracts/params/README.md new file mode 100644 index 000000000..00c8264b3 --- /dev/null +++ b/contracts/params/README.md @@ -0,0 +1,34 @@ +# Mars Params Contract + +The Mars Params Contract is published to [Crates.io](https://crates.io/crates/mars-params) + +This contract holds the following values for all the assets in Mars Protocol: + +- **Max Loan To Value:** Max percentage of collateral that can be borrowed +- **Liquidation Threshold:** LTV at which the loan is defined as under collateralized and can be liquidated +- **Liquidation Bonus:** Percentage of extra collateral the liquidator gets as a bonus +- **Deposit Enabled:** Is the asset able to be deposited into the Red Bank +- **Borrow Enabled:** Is the asset able to be borrowed from the Red Bank +- **Deposit Cap:** Max amount that can be deposited into the Red Bank +- **Asset Settings:** Credit Manager and Red Bank Permission Settings + +Note: Credit Manager Vaults only utilize max loan to value, liquidation threshold, and deposit cap parameters, while Red Bank Markets utilize all of the above parameters. + +## High Levered Strategies (HLS) + +An HLS is a position where the borrowed asset is highly correlated to the collateral asset (e.g. atom debt -> stAtom collateral). +This has a low risk of liquidation. For this reason, Credit Manager grants higher MaxLTV & LiqThreshold parameters, +granting higher leverage. An asset's HLS parameters are stored in this contract and are applied to credit accounts +of the HLS type during a health check. + +### De-listing an HLS asset + +There are a few scenarios depending on what denom is being de-listed. Always communicate each step to the users! +- **De-listing a collateral denom**: + - Set the MaxLTV of the denom to zero. + - Gradually reduce the HLS Liquidation Threshold to zero. + - _Do not_ set HLS parameters to None or remove it from correlations list for debt denom. This would result in freezing the HLS accounts that have that collateral. +- **De-listing a debt denom**: + - Set the MaxLTV of all denoms in the debt denom's correlations list to zero. + - Gradually reduce the HLS Liquidation Threshold to zero. + - _Do not_ set HLS parameters to None. This would result in freezing the HLS accounts that have that debt denom. diff --git a/contracts/params/examples/schema.rs b/contracts/params/examples/schema.rs new file mode 100644 index 000000000..29e8f6f7d --- /dev/null +++ b/contracts/params/examples/schema.rs @@ -0,0 +1,10 @@ +use cosmwasm_schema::write_api; +use mars_params::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg, + } +} diff --git a/contracts/params/src/contract.rs b/contracts/params/src/contract.rs new file mode 100644 index 000000000..ab9b04e64 --- /dev/null +++ b/contracts/params/src/contract.rs @@ -0,0 +1,103 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response}; +use cw2::set_contract_version; +use mars_owner::OwnerInit::SetInitialOwner; + +use crate::{ + emergency_powers::{disable_borrowing, disallow_coin, set_zero_deposit_cap, set_zero_max_ltv}, + error::ContractResult, + execute::{assert_thf, update_asset_params, update_target_health_factor, update_vault_config}, + msg::{ + CmEmergencyUpdate, EmergencyUpdate, ExecuteMsg, InstantiateMsg, QueryMsg, + RedBankEmergencyUpdate, + }, + query::{ + query_all_asset_params, query_all_vault_configs, query_total_deposit, query_vault_config, + }, + state::{ADDRESS_PROVIDER, ASSET_PARAMS, OWNER, TARGET_HEALTH_FACTOR}, +}; + +const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _: Env, + _: MessageInfo, + msg: InstantiateMsg, +) -> ContractResult { + set_contract_version(deps.storage, format!("crates.io:{CONTRACT_NAME}"), CONTRACT_VERSION)?; + + OWNER.initialize( + deps.storage, + deps.api, + SetInitialOwner { + owner: msg.owner, + }, + )?; + + let address_provider_addr = deps.api.addr_validate(&msg.address_provider)?; + ADDRESS_PROVIDER.save(deps.storage, &address_provider_addr)?; + + assert_thf(msg.target_health_factor)?; + TARGET_HEALTH_FACTOR.save(deps.storage, &msg.target_health_factor)?; + + Ok(Response::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + _: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> ContractResult { + match msg { + ExecuteMsg::UpdateOwner(update) => Ok(OWNER.update(deps, info, update)?), + ExecuteMsg::UpdateAssetParams(update) => update_asset_params(deps, info, update), + ExecuteMsg::UpdateTargetHealthFactor(mcf) => update_target_health_factor(deps, info, mcf), + ExecuteMsg::UpdateVaultConfig(update) => update_vault_config(deps, info, update), + ExecuteMsg::EmergencyUpdate(update) => match update { + EmergencyUpdate::RedBank(rb_u) => match rb_u { + RedBankEmergencyUpdate::DisableBorrowing(denom) => { + disable_borrowing(deps, info, &denom) + } + }, + EmergencyUpdate::CreditManager(rv_u) => match rv_u { + CmEmergencyUpdate::DisallowCoin(denom) => disallow_coin(deps, info, &denom), + CmEmergencyUpdate::SetZeroMaxLtvOnVault(v) => set_zero_max_ltv(deps, info, &v), + CmEmergencyUpdate::SetZeroDepositCapOnVault(v) => { + set_zero_deposit_cap(deps, info, &v) + } + }, + }, + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> ContractResult { + let res = match msg { + QueryMsg::Owner {} => to_binary(&OWNER.query(deps.storage)?), + QueryMsg::AssetParams { + denom, + } => to_binary(&ASSET_PARAMS.load(deps.storage, &denom)?), + QueryMsg::AllAssetParams { + start_after, + limit, + } => to_binary(&query_all_asset_params(deps, start_after, limit)?), + QueryMsg::VaultConfig { + address, + } => to_binary(&query_vault_config(deps, &address)?), + QueryMsg::AllVaultConfigs { + start_after, + limit, + } => to_binary(&query_all_vault_configs(deps, start_after, limit)?), + QueryMsg::TargetHealthFactor {} => to_binary(&TARGET_HEALTH_FACTOR.load(deps.storage)?), + QueryMsg::TotalDeposit { + denom, + } => to_binary(&query_total_deposit(deps, &env, denom)?), + }; + res.map_err(Into::into) +} diff --git a/contracts/params/src/emergency_powers.rs b/contracts/params/src/emergency_powers.rs new file mode 100644 index 000000000..982b6954b --- /dev/null +++ b/contracts/params/src/emergency_powers.rs @@ -0,0 +1,82 @@ +use cosmwasm_std::{Decimal, DepsMut, MessageInfo, Response, Uint128}; + +use crate::{ + error::ContractError, + state::{ASSET_PARAMS, OWNER, VAULT_CONFIGS}, +}; + +pub fn disable_borrowing( + deps: DepsMut, + info: MessageInfo, + denom: &str, +) -> Result { + OWNER.assert_emergency_owner(deps.storage, &info.sender)?; + + let mut params = ASSET_PARAMS.load(deps.storage, denom)?; + params.red_bank.borrow_enabled = false; + ASSET_PARAMS.save(deps.storage, denom, ¶ms)?; + + let response = Response::new() + .add_attribute("action", "emergency_disable_borrowing") + .add_attribute("denom", denom.to_string()); + + Ok(response) +} + +pub fn disallow_coin( + deps: DepsMut, + info: MessageInfo, + denom: &str, +) -> Result { + OWNER.assert_emergency_owner(deps.storage, &info.sender)?; + + let mut params = ASSET_PARAMS.load(deps.storage, denom)?; + params.credit_manager.whitelisted = false; + ASSET_PARAMS.save(deps.storage, denom, ¶ms)?; + + let response = Response::new() + .add_attribute("action", "emergency_disallow_coin") + .add_attribute("denom", denom.to_string()); + + Ok(response) +} + +pub fn set_zero_max_ltv( + deps: DepsMut, + info: MessageInfo, + vault: &str, +) -> Result { + OWNER.assert_emergency_owner(deps.storage, &info.sender)?; + + let vault_addr = deps.api.addr_validate(vault)?; + + let mut config = VAULT_CONFIGS.load(deps.storage, &vault_addr)?; + config.max_loan_to_value = Decimal::zero(); + VAULT_CONFIGS.save(deps.storage, &vault_addr, &config)?; + + let response = Response::new() + .add_attribute("action", "emergency_set_zero_max_ltv") + .add_attribute("vault", vault.to_string()); + + Ok(response) +} + +pub fn set_zero_deposit_cap( + deps: DepsMut, + info: MessageInfo, + vault: &str, +) -> Result { + OWNER.assert_emergency_owner(deps.storage, &info.sender)?; + + let vault_addr = deps.api.addr_validate(vault)?; + + let mut config = VAULT_CONFIGS.load(deps.storage, &vault_addr)?; + config.deposit_cap.amount = Uint128::zero(); + VAULT_CONFIGS.save(deps.storage, &vault_addr, &config)?; + + let response = Response::new() + .add_attribute("action", "emergency_set_zero_deposit_cap") + .add_attribute("vault", vault.to_string()); + + Ok(response) +} diff --git a/contracts/params/src/error.rs b/contracts/params/src/error.rs new file mode 100644 index 000000000..3d6497e70 --- /dev/null +++ b/contracts/params/src/error.rs @@ -0,0 +1,21 @@ +use cosmwasm_std::{DecimalRangeExceeded, StdError}; +use mars_owner::OwnerError; +pub use mars_utils::error::ValidationError; +use thiserror::Error; + +pub type ContractResult = Result; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + DecimalRangeExceeded(#[from] DecimalRangeExceeded), + + #[error("{0}")] + Owner(#[from] OwnerError), + + #[error("{0}")] + Validation(#[from] ValidationError), +} diff --git a/contracts/params/src/execute.rs b/contracts/params/src/execute.rs new file mode 100644 index 000000000..7d03967d0 --- /dev/null +++ b/contracts/params/src/execute.rs @@ -0,0 +1,118 @@ +use cosmwasm_std::{Decimal, DepsMut, MessageInfo, Response}; +use mars_utils::error::ValidationError; + +use crate::{ + error::{ContractError, ContractResult}, + msg::{AssetParamsUpdate, VaultConfigUpdate}, + state::{ASSET_PARAMS, OWNER, TARGET_HEALTH_FACTOR, VAULT_CONFIGS}, +}; + +pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub fn update_target_health_factor( + deps: DepsMut, + info: MessageInfo, + target_health_factor: Decimal, +) -> ContractResult { + OWNER.assert_owner(deps.storage, &info.sender)?; + + assert_thf(target_health_factor)?; + TARGET_HEALTH_FACTOR.save(deps.storage, &target_health_factor)?; + + let response = Response::new() + .add_attribute("action", "update_target_health_factor") + .add_attribute("value", target_health_factor.to_string()); + + Ok(response) +} + +pub fn update_asset_params( + deps: DepsMut, + info: MessageInfo, + update: AssetParamsUpdate, +) -> ContractResult { + OWNER.assert_owner(deps.storage, &info.sender)?; + + let mut response = Response::new().add_attribute("action", "update_asset_param"); + + match update { + AssetParamsUpdate::AddOrUpdate { + params: unchecked, + } => { + let params = unchecked.check(deps.api)?; + + ASSET_PARAMS.save(deps.storage, ¶ms.denom, ¶ms)?; + response = response + .add_attribute("action_type", "add_or_update") + .add_attribute("denom", params.denom); + } + } + + Ok(response) +} + +pub fn update_vault_config( + deps: DepsMut, + info: MessageInfo, + update: VaultConfigUpdate, +) -> ContractResult { + OWNER.assert_owner(deps.storage, &info.sender)?; + + let mut response = Response::new().add_attribute("action", "update_vault_config"); + + match update { + VaultConfigUpdate::AddOrUpdate { + config, + } => { + let checked = config.check(deps.api)?; + VAULT_CONFIGS.save(deps.storage, &checked.addr, &checked)?; + response = response + .add_attribute("action_type", "add_or_update") + .add_attribute("addr", checked.addr); + } + } + + Ok(response) +} + +pub fn assert_thf(thf: Decimal) -> Result<(), ContractError> { + if thf < Decimal::one() || thf > Decimal::from_atomics(2u128, 0u32)? { + return Err(ValidationError::InvalidParam { + param_name: "target_health_factor".to_string(), + invalid_value: thf.to_string(), + predicate: "[1, 2]".to_string(), + } + .into()); + } + Ok(()) +} + +/// liquidation_threshold should be greater than or equal to max_loan_to_value +pub fn assert_lqt_gt_max_ltv( + max_ltv: Decimal, + liq_threshold: Decimal, +) -> Result<(), ValidationError> { + if liq_threshold <= max_ltv { + return Err(ValidationError::InvalidParam { + param_name: "liquidation_threshold".to_string(), + invalid_value: liq_threshold.to_string(), + predicate: format!("> {} (max LTV)", max_ltv), + }); + } + Ok(()) +} + +pub fn assert_hls_lqt_gt_max_ltv( + max_ltv: Decimal, + liq_threshold: Decimal, +) -> Result<(), ValidationError> { + if liq_threshold <= max_ltv { + return Err(ValidationError::InvalidParam { + param_name: "hls_liquidation_threshold".to_string(), + invalid_value: liq_threshold.to_string(), + predicate: format!("> {} (hls max LTV)", max_ltv), + }); + } + Ok(()) +} diff --git a/contracts/params/src/lib.rs b/contracts/params/src/lib.rs new file mode 100644 index 000000000..2f486c2cc --- /dev/null +++ b/contracts/params/src/lib.rs @@ -0,0 +1,8 @@ +pub mod contract; +pub mod emergency_powers; +pub mod error; +pub mod execute; +pub mod msg; +pub mod query; +pub mod state; +pub mod types; diff --git a/contracts/params/src/msg.rs b/contracts/params/src/msg.rs new file mode 100644 index 000000000..d4fbd3177 --- /dev/null +++ b/contracts/params/src/msg.rs @@ -0,0 +1,103 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Decimal, Uint128}; +use mars_owner::OwnerUpdate; + +use crate::types::{asset::AssetParamsUnchecked, vault::VaultConfigUnchecked}; + +#[cw_serde] +pub struct InstantiateMsg { + /// Contract's owner + pub owner: String, + /// Address of the address provider contract + pub address_provider: String, + /// Determines the ideal HF a position should be left at immediately after the position has been liquidated. + pub target_health_factor: Decimal, +} + +#[cw_serde] +pub enum ExecuteMsg { + UpdateOwner(OwnerUpdate), + UpdateTargetHealthFactor(Decimal), + UpdateAssetParams(AssetParamsUpdate), + UpdateVaultConfig(VaultConfigUpdate), + EmergencyUpdate(EmergencyUpdate), +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(mars_owner::OwnerResponse)] + Owner {}, + + #[returns(crate::types::asset::AssetParams)] + AssetParams { + denom: String, + }, + + #[returns(Vec)] + AllAssetParams { + start_after: Option, + limit: Option, + }, + + #[returns(crate::types::vault::VaultConfig)] + VaultConfig { + /// Address of vault + address: String, + }, + + #[returns(Vec)] + AllVaultConfigs { + start_after: Option, + limit: Option, + }, + + #[returns(Decimal)] + TargetHealthFactor {}, + + /// Compute the total amount deposited of the given asset across Red Bank + /// and Credit Manager. + #[returns(TotalDepositResponse)] + TotalDeposit { + denom: String, + }, +} + +#[cw_serde] +pub struct TotalDepositResponse { + pub denom: String, + pub cap: Uint128, + pub amount: Uint128, +} + +#[cw_serde] +pub enum AssetParamsUpdate { + AddOrUpdate { + params: AssetParamsUnchecked, + }, +} + +#[cw_serde] +pub enum VaultConfigUpdate { + AddOrUpdate { + config: VaultConfigUnchecked, + }, +} + +#[cw_serde] +pub enum CmEmergencyUpdate { + SetZeroMaxLtvOnVault(String), + SetZeroDepositCapOnVault(String), + DisallowCoin(String), +} + +#[cw_serde] +pub enum RedBankEmergencyUpdate { + DisableBorrowing(String), +} + +#[cw_serde] +pub enum EmergencyUpdate { + CreditManager(CmEmergencyUpdate), + RedBank(RedBankEmergencyUpdate), +} diff --git a/contracts/params/src/query.rs b/contracts/params/src/query.rs new file mode 100644 index 000000000..71e512e3f --- /dev/null +++ b/contracts/params/src/query.rs @@ -0,0 +1,144 @@ +use cosmwasm_std::{Addr, Deps, Env, Order, StdResult, Uint128}; +use cw_storage_plus::Bound; +use mars_interest_rate::get_underlying_liquidity_amount; +use mars_red_bank_types::{ + address_provider::{self, MarsAddressType}, + red_bank::{self, Market, UserDebtResponse}, +}; + +use crate::{ + msg::TotalDepositResponse, + state::{ADDRESS_PROVIDER, ASSET_PARAMS, VAULT_CONFIGS}, + types::{asset::AssetParams, vault::VaultConfig}, +}; + +pub const DEFAULT_LIMIT: u32 = 10; +pub const MAX_LIMIT: u32 = 30; + +pub fn query_all_asset_params( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult> { + let start = start_after.as_ref().map(|denom| Bound::exclusive(denom.as_str())); + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + ASSET_PARAMS + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| Ok(res?.1)) + .collect() +} + +pub fn query_vault_config(deps: Deps, unchecked: &str) -> StdResult { + let addr = deps.api.addr_validate(unchecked)?; + VAULT_CONFIGS.load(deps.storage, &addr) +} + +pub fn query_all_vault_configs( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult> { + let vault_addr: Addr; + let start = match &start_after { + Some(unchecked) => { + vault_addr = deps.api.addr_validate(unchecked)?; + Some(Bound::exclusive(&vault_addr)) + } + None => None, + }; + + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + + VAULT_CONFIGS + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| Ok(res?.1)) + .collect() +} + +/// Query and compute the total deposited amount of the given asset across Red +/// Bank (RB) and Credit Manager (CM). +/// +/// Specifically, the amount is defined as: +/// rb_deposit + cm_deposit - cm_debt_owed_to_rb +/// +/// Note: +/// +/// 1. We subtract the amount of debt that CM owes to RB to avoid double- +/// counting. +/// +/// 2. We only consider spot asset holdings, meaning we don't unwrap DEX LP +/// tokens or vault tokens to the underlying assets. After some discussions +/// we have concluded the latter is not feasible. +/// +/// For example, when computing the deposited amount of ATOM, we only include +/// ATOM deposited in RB and CM; we don't include the ATOM-OSMO LP token, or +/// the ATOM-OSMO farming vault. +pub fn query_total_deposit( + deps: Deps, + env: &Env, + denom: String, +) -> StdResult { + let current_timestamp = env.block.time.seconds(); + + // query contract addresses + let address_provider_addr = ADDRESS_PROVIDER.load(deps.storage)?; + let addresses = address_provider::helpers::query_contract_addrs( + deps, + &address_provider_addr, + vec![MarsAddressType::RedBank, MarsAddressType::CreditManager], + )?; + let credit_manager_addr = &addresses[&MarsAddressType::CreditManager]; + let red_bank_addr = &addresses[&MarsAddressType::RedBank]; + + // amount of this asset deposited into Red Bank + // if the market doesn't exist on RB, we default to zero + let rb_deposit = deps + .querier + .query_wasm_smart::>( + red_bank_addr, + &red_bank::QueryMsg::Market { + denom: denom.clone(), + }, + )? + .map(|market| { + get_underlying_liquidity_amount( + market.collateral_total_scaled, + &market, + current_timestamp, + ) + }) + .transpose()? + .unwrap_or_else(Uint128::zero); + + // amount of debt in this asset the Credit Manager owes to Red Bank + // this query returns zero if no debt is owed + let cm_debt = deps + .querier + .query_wasm_smart::( + red_bank_addr, + &red_bank::QueryMsg::UserDebt { + user: credit_manager_addr.into(), + denom: denom.clone(), + }, + )? + .amount; + + // amount of this asset deposited into Credit Manager + // this is simply the coin balance of the CM contract + // note that this way, we don't include LP tokens or vault positions + let cm_deposit = deps.querier.query_balance(credit_manager_addr, &denom)?.amount; + + // total deposited amount + let amount = rb_deposit.checked_add(cm_deposit)?.checked_sub(cm_debt)?; + + // additionally, we include the deposit cap in the response + let asset_params = ASSET_PARAMS.load(deps.storage, &denom)?; + + Ok(TotalDepositResponse { + denom, + amount, + cap: asset_params.deposit_cap, + }) +} diff --git a/contracts/params/src/state.rs b/contracts/params/src/state.rs new file mode 100644 index 000000000..0259a0377 --- /dev/null +++ b/contracts/params/src/state.rs @@ -0,0 +1,11 @@ +use cosmwasm_std::{Addr, Decimal}; +use cw_storage_plus::{Item, Map}; +use mars_owner::Owner; + +use crate::types::{asset::AssetParams, vault::VaultConfig}; + +pub const OWNER: Owner = Owner::new("owner"); +pub const ADDRESS_PROVIDER: Item = Item::new("address_provider"); +pub const ASSET_PARAMS: Map<&str, AssetParams> = Map::new("asset_params"); +pub const VAULT_CONFIGS: Map<&Addr, VaultConfig> = Map::new("vault_configs"); +pub const TARGET_HEALTH_FACTOR: Item = Item::new("target_health_factor"); diff --git a/contracts/params/src/types/asset.rs b/contracts/params/src/types/asset.rs new file mode 100644 index 000000000..2021c317e --- /dev/null +++ b/contracts/params/src/types/asset.rs @@ -0,0 +1,181 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Api, Decimal, Uint128}; +use mars_utils::{ + error::ValidationError, + helpers::{decimal_param_le_one, decimal_param_lt_one, validate_native_denom}, +}; + +use crate::{ + error::ContractResult, + execute::{assert_hls_lqt_gt_max_ltv, assert_lqt_gt_max_ltv}, + types::hls::HlsParamsBase, +}; + +#[cw_serde] +pub struct CmSettings { + pub whitelisted: bool, + pub hls: Option>, +} + +#[cw_serde] +pub struct RedBankSettings { + pub deposit_enabled: bool, + pub borrow_enabled: bool, +} + +/// The LB will depend on the Health Factor and a couple other parameters as follows: +/// Liquidation Bonus = min( +/// b + (slope * (1 - HF)), +/// max( +/// min(CR - 1, max_lb), +/// min_lb +/// ) +/// ) +#[cw_serde] +pub struct LiquidationBonus { + /// Marks the level at which the LB starts when HF drops marginally below 1. + /// If set at 1%, at HF = 0.999 the LB will be 1%. If set at 0%, the LB starts increasing from 0% as the HF drops below 1. + pub starting_lb: Decimal, + /// Defines the slope at which the LB increases as the HF decreases. + /// The higher the slope, the faster the LB increases as the HF decreases. + pub slope: Decimal, + /// Minimum LB that will be granted to liquidators even when the position is undercollateralized. + pub min_lb: Decimal, + /// Maximum LB that can be granted to a liquidator; in other words, the maxLB establishes a ceiling to the LB. + /// This is a precautionary parameter to mitigate liquidated users being over-punished. + pub max_lb: Decimal, +} + +impl LiquidationBonus { + pub fn validate(&self) -> Result<(), ValidationError> { + assert_starting_lb_within_range(self.starting_lb)?; + assert_lb_slope_within_range(self.slope)?; + assert_min_lb_within_range(self.min_lb)?; + assert_max_lb_within_range(self.max_lb)?; + assert_max_lb_gt_min_lb(self.min_lb, self.max_lb)?; + Ok(()) + } +} + +fn assert_starting_lb_within_range(b: Decimal) -> Result<(), ValidationError> { + if b > Decimal::percent(10) { + return Err(ValidationError::InvalidParam { + param_name: "starting_lb".to_string(), + invalid_value: b.to_string(), + predicate: "[0, 0.1]".to_string(), + }); + } + Ok(()) +} + +fn assert_lb_slope_within_range(slope: Decimal) -> Result<(), ValidationError> { + if slope < Decimal::one() || slope > Decimal::from_ratio(5u8, 1u8) { + return Err(ValidationError::InvalidParam { + param_name: "slope".to_string(), + invalid_value: slope.to_string(), + predicate: "[1, 5]".to_string(), + }); + } + Ok(()) +} + +fn assert_min_lb_within_range(min_lb: Decimal) -> Result<(), ValidationError> { + if min_lb > Decimal::percent(10) { + return Err(ValidationError::InvalidParam { + param_name: "min_lb".to_string(), + invalid_value: min_lb.to_string(), + predicate: "[0, 0.1]".to_string(), + }); + } + Ok(()) +} + +fn assert_max_lb_within_range(max_lb: Decimal) -> Result<(), ValidationError> { + if max_lb < Decimal::percent(5) || max_lb > Decimal::percent(30) { + return Err(ValidationError::InvalidParam { + param_name: "max_lb".to_string(), + invalid_value: max_lb.to_string(), + predicate: "[0.05, 0.3]".to_string(), + }); + } + Ok(()) +} + +fn assert_max_lb_gt_min_lb(min_lb: Decimal, max_lb: Decimal) -> Result<(), ValidationError> { + if min_lb > max_lb { + return Err(ValidationError::InvalidParam { + param_name: "max_lb".to_string(), + invalid_value: max_lb.to_string(), + predicate: format!("> {} (min LB)", min_lb), + }); + } + Ok(()) +} + +#[cw_serde] +pub struct AssetParamsBase { + pub denom: String, + pub credit_manager: CmSettings, + pub red_bank: RedBankSettings, + pub max_loan_to_value: Decimal, + pub liquidation_threshold: Decimal, + pub liquidation_bonus: LiquidationBonus, + pub protocol_liquidation_fee: Decimal, + pub deposit_cap: Uint128, +} + +pub type AssetParams = AssetParamsBase; +pub type AssetParamsUnchecked = AssetParamsBase; + +impl From for AssetParamsUnchecked { + fn from(p: AssetParams) -> Self { + Self { + denom: p.denom, + credit_manager: CmSettings { + whitelisted: p.credit_manager.whitelisted, + hls: p.credit_manager.hls.map(Into::into), + }, + red_bank: p.red_bank, + max_loan_to_value: p.max_loan_to_value, + liquidation_threshold: p.liquidation_threshold, + liquidation_bonus: p.liquidation_bonus, + protocol_liquidation_fee: p.protocol_liquidation_fee, + deposit_cap: p.deposit_cap, + } + } +} + +impl AssetParamsUnchecked { + pub fn check(&self, api: &dyn Api) -> ContractResult { + validate_native_denom(&self.denom)?; + + decimal_param_lt_one(self.max_loan_to_value, "max_loan_to_value")?; + decimal_param_le_one(self.liquidation_threshold, "liquidation_threshold")?; + assert_lqt_gt_max_ltv(self.max_loan_to_value, self.liquidation_threshold)?; + + self.liquidation_bonus.validate()?; + decimal_param_lt_one(self.protocol_liquidation_fee, "protocol_liquidation_fee")?; + + if let Some(hls) = self.credit_manager.hls.as_ref() { + decimal_param_lt_one(hls.max_loan_to_value, "hls_max_loan_to_value")?; + decimal_param_le_one(hls.liquidation_threshold, "hls_liquidation_threshold")?; + assert_hls_lqt_gt_max_ltv(hls.max_loan_to_value, hls.liquidation_threshold)?; + } + + let hls = self.credit_manager.hls.as_ref().map(|hls| hls.check(api)).transpose()?; + + Ok(AssetParams { + denom: self.denom.clone(), + credit_manager: CmSettings { + whitelisted: self.credit_manager.whitelisted, + hls, + }, + red_bank: self.red_bank.clone(), + max_loan_to_value: self.max_loan_to_value, + liquidation_threshold: self.liquidation_threshold, + liquidation_bonus: self.liquidation_bonus.clone(), + protocol_liquidation_fee: self.protocol_liquidation_fee, + deposit_cap: self.deposit_cap, + }) + } +} diff --git a/contracts/params/src/types/hls.rs b/contracts/params/src/types/hls.rs new file mode 100644 index 000000000..d6cd96536 --- /dev/null +++ b/contracts/params/src/types/hls.rs @@ -0,0 +1,82 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Api, Decimal}; +use mars_utils::helpers::validate_native_denom; + +use crate::error::ContractResult; + +#[cw_serde] +pub enum HlsAssetType { + Coin { + denom: String, + }, + Vault { + addr: T, + }, +} + +impl From> for HlsAssetType { + fn from(t: HlsAssetType) -> Self { + match t { + HlsAssetType::Coin { + denom, + } => HlsAssetType::Coin { + denom, + }, + HlsAssetType::Vault { + addr, + } => HlsAssetType::Vault { + addr: addr.to_string(), + }, + } + } +} + +#[cw_serde] +pub struct HlsParamsBase { + pub max_loan_to_value: Decimal, + pub liquidation_threshold: Decimal, + /// Given this asset is debt, correlations are the only allowed collateral + /// which are permitted to fulfill the HLS strategy + pub correlations: Vec>, +} + +pub type HlsParams = HlsParamsBase; +pub type HlsParamsUnchecked = HlsParamsBase; + +impl From for HlsParamsUnchecked { + fn from(hls: HlsParams) -> Self { + Self { + max_loan_to_value: hls.max_loan_to_value, + liquidation_threshold: hls.liquidation_threshold, + correlations: hls.correlations.into_iter().map(Into::into).collect(), + } + } +} + +impl HlsParamsUnchecked { + pub fn check(&self, api: &dyn Api) -> ContractResult { + Ok(HlsParamsBase { + max_loan_to_value: self.max_loan_to_value, + liquidation_threshold: self.liquidation_threshold, + correlations: self + .correlations + .iter() + .map(|c| match c { + HlsAssetType::Coin { + denom, + } => { + validate_native_denom(denom)?; + Ok(HlsAssetType::Coin { + denom: denom.clone(), + }) + } + HlsAssetType::Vault { + addr, + } => Ok(HlsAssetType::Vault { + addr: api.addr_validate(addr)?, + }), + }) + .collect::>>()?, + }) + } +} diff --git a/contracts/params/src/types/mod.rs b/contracts/params/src/types/mod.rs new file mode 100644 index 000000000..76b6ca7a0 --- /dev/null +++ b/contracts/params/src/types/mod.rs @@ -0,0 +1,3 @@ +pub mod asset; +pub mod hls; +pub mod vault; diff --git a/contracts/params/src/types/vault.rs b/contracts/params/src/types/vault.rs new file mode 100644 index 000000000..e7b8f6fcf --- /dev/null +++ b/contracts/params/src/types/vault.rs @@ -0,0 +1,59 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Api, Coin, Decimal}; +use mars_utils::helpers::decimal_param_le_one; + +use crate::{ + error::ContractResult, + execute::{assert_hls_lqt_gt_max_ltv, assert_lqt_gt_max_ltv}, + types::hls::HlsParamsBase, +}; + +#[cw_serde] +pub struct VaultConfigBase { + pub addr: T, + pub deposit_cap: Coin, + pub max_loan_to_value: Decimal, + pub liquidation_threshold: Decimal, + pub whitelisted: bool, + pub hls: Option>, +} + +pub type VaultConfigUnchecked = VaultConfigBase; +pub type VaultConfig = VaultConfigBase; + +impl From for VaultConfigUnchecked { + fn from(v: VaultConfig) -> Self { + VaultConfigUnchecked { + addr: v.addr.to_string(), + deposit_cap: v.deposit_cap, + max_loan_to_value: v.max_loan_to_value, + liquidation_threshold: v.liquidation_threshold, + whitelisted: v.whitelisted, + hls: v.hls.map(Into::into), + } + } +} + +impl VaultConfigUnchecked { + pub fn check(&self, api: &dyn Api) -> ContractResult { + decimal_param_le_one(self.max_loan_to_value, "max_loan_to_value")?; + decimal_param_le_one(self.liquidation_threshold, "liquidation_threshold")?; + assert_lqt_gt_max_ltv(self.max_loan_to_value, self.liquidation_threshold)?; + + // High levered strategies + if let Some(hls) = self.hls.as_ref() { + decimal_param_le_one(hls.max_loan_to_value, "hls_max_loan_to_value")?; + decimal_param_le_one(hls.liquidation_threshold, "hls_liquidation_threshold")?; + assert_hls_lqt_gt_max_ltv(hls.max_loan_to_value, hls.liquidation_threshold)?; + } + + Ok(VaultConfig { + addr: api.addr_validate(&self.addr)?, + deposit_cap: self.deposit_cap.clone(), + max_loan_to_value: self.max_loan_to_value, + liquidation_threshold: self.liquidation_threshold, + whitelisted: self.whitelisted, + hls: self.hls.as_ref().map(|hls| hls.check(api)).transpose()?, + }) + } +} diff --git a/contracts/params/tests/all_tests.rs b/contracts/params/tests/all_tests.rs new file mode 100644 index 000000000..14f00389d --- /dev/null +++ b/contracts/params/tests/all_tests.rs @@ -0,0 +1 @@ +mod tests; diff --git a/contracts/params/tests/tests/helpers/assertions.rs b/contracts/params/tests/tests/helpers/assertions.rs new file mode 100644 index 000000000..857060fd9 --- /dev/null +++ b/contracts/params/tests/tests/helpers/assertions.rs @@ -0,0 +1,25 @@ +use std::{collections::HashSet, hash::Hash}; + +use anyhow::Result as AnyResult; +use cw_multi_test::AppResponse; +use mars_params::error::ContractError; + +pub fn assert_err(res: AnyResult, err: ContractError) { + match res { + Ok(_) => panic!("Result was not an error"), + Err(generic_err) => { + let contract_err: ContractError = generic_err.downcast().unwrap(); + assert_eq!(contract_err, err); + } + } +} + +pub fn assert_contents_equal(vec_a: &[T], vec_b: &[T]) +where + T: Eq + Hash, +{ + let set_a: HashSet<_> = vec_a.iter().collect(); + let set_b: HashSet<_> = vec_b.iter().collect(); + + assert!(set_a == set_b) +} diff --git a/contracts/params/tests/tests/helpers/contracts.rs b/contracts/params/tests/tests/helpers/contracts.rs new file mode 100644 index 000000000..52c2cfef9 --- /dev/null +++ b/contracts/params/tests/tests/helpers/contracts.rs @@ -0,0 +1,11 @@ +use cosmwasm_std::Empty; +use cw_multi_test::{Contract, ContractWrapper}; + +pub fn mock_params_contract() -> Box> { + let contract = ContractWrapper::new( + mars_params::contract::execute, + mars_params::contract::instantiate, + mars_params::contract::query, + ); + Box::new(contract) +} diff --git a/contracts/params/tests/tests/helpers/generator.rs b/contracts/params/tests/tests/helpers/generator.rs new file mode 100644 index 000000000..267b0ab36 --- /dev/null +++ b/contracts/params/tests/tests/helpers/generator.rs @@ -0,0 +1,42 @@ +use std::str::FromStr; + +use cosmwasm_std::{coin, Decimal, Uint128}; +use mars_params::types::{ + asset::{AssetParamsUnchecked, CmSettings, LiquidationBonus, RedBankSettings}, + vault::VaultConfigUnchecked, +}; + +pub fn default_asset_params(denom: &str) -> AssetParamsUnchecked { + AssetParamsUnchecked { + denom: denom.to_string(), + credit_manager: CmSettings { + whitelisted: false, + hls: None, + }, + red_bank: RedBankSettings { + deposit_enabled: true, + borrow_enabled: false, + }, + max_loan_to_value: Decimal::from_str("0.6").unwrap(), + liquidation_threshold: Decimal::from_str("0.7").unwrap(), + liquidation_bonus: LiquidationBonus { + starting_lb: Decimal::percent(4), + slope: Decimal::from_str("2.0").unwrap(), + min_lb: Decimal::percent(1), + max_lb: Decimal::percent(8), + }, + protocol_liquidation_fee: Decimal::percent(2), + deposit_cap: Uint128::new(1_000_000_000), + } +} + +pub fn default_vault_config(addr: &str) -> VaultConfigUnchecked { + VaultConfigUnchecked { + addr: addr.to_string(), + deposit_cap: coin(100_000_000_000, "uusdc"), + max_loan_to_value: Decimal::from_str("0.47").unwrap(), + liquidation_threshold: Decimal::from_str("0.5").unwrap(), + whitelisted: true, + hls: None, + } +} diff --git a/contracts/params/tests/tests/helpers/mock_env.rs b/contracts/params/tests/tests/helpers/mock_env.rs new file mode 100644 index 000000000..f812739ba --- /dev/null +++ b/contracts/params/tests/tests/helpers/mock_env.rs @@ -0,0 +1,241 @@ +use std::{mem::take, str::FromStr}; + +use anyhow::Result as AnyResult; +use cosmwasm_std::{Addr, Decimal}; +use cw_multi_test::{App, AppResponse, BasicApp, Executor}; +use mars_owner::{OwnerResponse, OwnerUpdate}; +use mars_params::{ + msg::{ + AssetParamsUpdate, EmergencyUpdate, ExecuteMsg, InstantiateMsg, QueryMsg, VaultConfigUpdate, + }, + types::{asset::AssetParams, vault::VaultConfig}, +}; + +use super::contracts::mock_params_contract; + +pub struct MockEnv { + pub app: BasicApp, + pub params_contract: Addr, +} + +pub struct MockEnvBuilder { + pub app: BasicApp, + pub target_health_factor: Option, + pub emergency_owner: Option, +} + +#[allow(clippy::new_ret_no_self)] +impl MockEnv { + pub fn new() -> MockEnvBuilder { + MockEnvBuilder { + app: App::default(), + target_health_factor: None, + emergency_owner: None, + } + } + + //-------------------------------------------------------------------------------------------------- + // Execute Msgs + //-------------------------------------------------------------------------------------------------- + + pub fn update_asset_params( + &mut self, + sender: &Addr, + update: AssetParamsUpdate, + ) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.params_contract.clone(), + &ExecuteMsg::UpdateAssetParams(update), + &[], + ) + } + + pub fn update_vault_config( + &mut self, + sender: &Addr, + update: VaultConfigUpdate, + ) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.params_contract.clone(), + &ExecuteMsg::UpdateVaultConfig(update), + &[], + ) + } + + pub fn update_owner(&mut self, sender: &Addr, update: OwnerUpdate) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.params_contract.clone(), + &ExecuteMsg::UpdateOwner(update), + &[], + ) + } + + pub fn update_target_health_factor( + &mut self, + sender: &Addr, + thf: Decimal, + ) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.params_contract.clone(), + &ExecuteMsg::UpdateTargetHealthFactor(thf), + &[], + ) + } + + pub fn emergency_update( + &mut self, + sender: &Addr, + update: EmergencyUpdate, + ) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.params_contract.clone(), + &ExecuteMsg::EmergencyUpdate(update), + &[], + ) + } + + //-------------------------------------------------------------------------------------------------- + // Queries + //-------------------------------------------------------------------------------------------------- + + pub fn query_owner(&self) -> Addr { + let res = self.query_ownership(); + Addr::unchecked(res.owner.unwrap()) + } + + pub fn query_ownership(&self) -> OwnerResponse { + self.app.wrap().query_wasm_smart(self.params_contract.clone(), &QueryMsg::Owner {}).unwrap() + } + + pub fn query_asset_params(&self, denom: &str) -> AssetParams { + self.app + .wrap() + .query_wasm_smart( + self.params_contract.clone(), + &QueryMsg::AssetParams { + denom: denom.to_string(), + }, + ) + .unwrap() + } + + pub fn query_all_asset_params( + &self, + start_after: Option, + limit: Option, + ) -> Vec { + self.app + .wrap() + .query_wasm_smart( + self.params_contract.clone(), + &QueryMsg::AllAssetParams { + start_after, + limit, + }, + ) + .unwrap() + } + + pub fn query_vault_config(&self, addr: &str) -> VaultConfig { + self.app + .wrap() + .query_wasm_smart( + self.params_contract.clone(), + &QueryMsg::VaultConfig { + address: addr.to_string(), + }, + ) + .unwrap() + } + + pub fn query_all_vault_configs( + &self, + start_after: Option, + limit: Option, + ) -> Vec { + self.app + .wrap() + .query_wasm_smart( + self.params_contract.clone(), + &QueryMsg::AllVaultConfigs { + start_after, + limit, + }, + ) + .unwrap() + } + + pub fn query_target_health_factor(&self) -> Decimal { + self.app + .wrap() + .query_wasm_smart(self.params_contract.clone(), &QueryMsg::TargetHealthFactor {}) + .unwrap() + } +} + +impl MockEnvBuilder { + pub fn build(&mut self) -> AnyResult { + let code_id = self.app.store_code(mock_params_contract()); + + let params_contract = self.app.instantiate_contract( + code_id, + Addr::unchecked("owner"), + &InstantiateMsg { + owner: "owner".to_string(), + address_provider: "address_provider".to_string(), + target_health_factor: self.get_target_health_factor(), + }, + &[], + "mock-params-contract", + None, + )?; + + if self.emergency_owner.is_some() { + self.set_emergency_owner(¶ms_contract, &self.emergency_owner.clone().unwrap()); + } + + Ok(MockEnv { + app: take(&mut self.app), + params_contract, + }) + } + + fn set_emergency_owner(&mut self, params_contract: &Addr, eo: &str) { + self.app + .execute_contract( + Addr::unchecked("owner"), + params_contract.clone(), + &ExecuteMsg::UpdateOwner(OwnerUpdate::SetEmergencyOwner { + emergency_owner: eo.to_string(), + }), + &[], + ) + .unwrap(); + } + + //-------------------------------------------------------------------------------------------------- + // Get or defaults + //-------------------------------------------------------------------------------------------------- + + pub fn get_target_health_factor(&self) -> Decimal { + self.target_health_factor.unwrap_or(Decimal::from_str("1.05").unwrap()) + } + + //-------------------------------------------------------------------------------------------------- + // Setter functions + //-------------------------------------------------------------------------------------------------- + pub fn target_health_factor(&mut self, thf: Decimal) -> &mut Self { + self.target_health_factor = Some(thf); + self + } + + pub fn emergency_owner(&mut self, eo: &str) -> &mut Self { + self.emergency_owner = Some(eo.to_string()); + self + } +} diff --git a/contracts/params/tests/tests/helpers/mod.rs b/contracts/params/tests/tests/helpers/mod.rs new file mode 100644 index 000000000..2c580c0be --- /dev/null +++ b/contracts/params/tests/tests/helpers/mod.rs @@ -0,0 +1,6 @@ +pub use self::{assertions::*, contracts::*, generator::*, mock_env::*}; + +mod assertions; +mod contracts; +mod generator; +mod mock_env; diff --git a/contracts/params/tests/tests/mod.rs b/contracts/params/tests/tests/mod.rs new file mode 100644 index 000000000..bdfca4340 --- /dev/null +++ b/contracts/params/tests/tests/mod.rs @@ -0,0 +1,10 @@ +mod helpers; + +mod test_asset_validation; +mod test_deposit_cap; +mod test_emergency_powers; +mod test_owner; +mod test_target_health_factor; +mod test_update_asset_params; +mod test_vault_validation; +mod test_vaults; diff --git a/contracts/params/tests/tests/test_asset_validation.rs b/contracts/params/tests/tests/test_asset_validation.rs new file mode 100644 index 000000000..5dd216883 --- /dev/null +++ b/contracts/params/tests/tests/test_asset_validation.rs @@ -0,0 +1,366 @@ +use std::str::FromStr; + +use cosmwasm_std::Decimal; +use mars_params::{ + error::ContractError::Validation, + msg::AssetParamsUpdate, + types::hls::{HlsAssetType, HlsParamsUnchecked}, +}; +use mars_utils::error::ValidationError::{InvalidDenom, InvalidParam}; + +use super::helpers::{assert_err, default_asset_params, MockEnv}; + +#[test] +fn denom_must_be_native() { + let mut mock = MockEnv::new().build().unwrap(); + let denom = "AA".to_string(); // Invalid native denom length + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom), + }, + ); + assert_err( + res, + Validation(InvalidDenom { + reason: "Invalid denom length".to_string(), + }), + ); +} + +#[test] +fn max_ltv_less_than_one() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + params.max_loan_to_value = Decimal::from_str("1.1235").unwrap(); + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "max_loan_to_value".to_string(), + invalid_value: "1.1235".to_string(), + predicate: "< 1".to_string(), + }), + ); +} + +#[test] +fn liquidation_threshold_less_than_or_equal_to_one() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + params.liquidation_threshold = Decimal::from_str("1.1235").unwrap(); + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "liquidation_threshold".to_string(), + invalid_value: "1.1235".to_string(), + predicate: "<= 1".to_string(), + }), + ); +} + +#[test] +fn liq_threshold_gt_max_ltv() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + params.liquidation_threshold = Decimal::from_str("0.5").unwrap(); + params.max_loan_to_value = Decimal::from_str("0.6").unwrap(); + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "liquidation_threshold".to_string(), + invalid_value: "0.5".to_string(), + predicate: "> 0.6 (max LTV)".to_string(), + }), + ); +} + +#[test] +fn hls_max_ltv_less_than_one() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + params.credit_manager.hls = Some(HlsParamsUnchecked { + max_loan_to_value: Decimal::from_str("1.1235").unwrap(), + liquidation_threshold: Decimal::from_str("0.5").unwrap(), + correlations: vec![], + }); + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "hls_max_loan_to_value".to_string(), + invalid_value: "1.1235".to_string(), + predicate: "< 1".to_string(), + }), + ); +} + +#[test] +fn hls_liquidation_threshold_less_than_or_equal_to_one() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + params.credit_manager.hls = Some(HlsParamsUnchecked { + max_loan_to_value: Decimal::from_str("0.6").unwrap(), + liquidation_threshold: Decimal::from_str("1.1235").unwrap(), + correlations: vec![], + }); + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "hls_liquidation_threshold".to_string(), + invalid_value: "1.1235".to_string(), + predicate: "<= 1".to_string(), + }), + ); +} + +#[test] +fn hls_liq_threshold_gt_hls_max_ltv() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + params.credit_manager.hls = Some(HlsParamsUnchecked { + max_loan_to_value: Decimal::from_str("0.6").unwrap(), + liquidation_threshold: Decimal::from_str("0.5").unwrap(), + correlations: vec![], + }); + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "hls_liquidation_threshold".to_string(), + invalid_value: "0.5".to_string(), + predicate: "> 0.6 (hls max LTV)".to_string(), + }), + ); +} + +#[test] +fn correlations_must_be_valid_denoms() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + params.credit_manager.hls = Some(HlsParamsUnchecked { + max_loan_to_value: Decimal::from_str("0.5").unwrap(), + liquidation_threshold: Decimal::from_str("0.7").unwrap(), + correlations: vec![HlsAssetType::Coin { + denom: "AA".to_string(), + }], + }); + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidDenom { + reason: "Invalid denom length".to_string(), + }), + ); +} + +#[test] +fn protocol_liquidation_fee_less_than_one() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + params.protocol_liquidation_fee = Decimal::from_str("1").unwrap(); + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "protocol_liquidation_fee".to_string(), + invalid_value: "1".to_string(), + predicate: "< 1".to_string(), + }), + ); +} + +#[test] +fn liquidation_bonus_param_b_out_of_range() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + params.liquidation_bonus.starting_lb = Decimal::from_str("0.101").unwrap(); + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "starting_lb".to_string(), + invalid_value: "0.101".to_string(), + predicate: "[0, 0.1]".to_string(), + }), + ); +} + +#[test] +fn liquidation_bonus_param_slope_out_of_range() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + + params.liquidation_bonus.slope = Decimal::from_str("0.99").unwrap(); + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params: params.clone(), + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "slope".to_string(), + invalid_value: "0.99".to_string(), + predicate: "[1, 5]".to_string(), + }), + ); + + params.liquidation_bonus.slope = Decimal::from_str("5.01").unwrap(); + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "slope".to_string(), + invalid_value: "5.01".to_string(), + predicate: "[1, 5]".to_string(), + }), + ); +} + +#[test] +fn liquidation_bonus_param_min_lb_out_of_range() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + params.liquidation_bonus.min_lb = Decimal::from_str("0.101").unwrap(); + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "min_lb".to_string(), + invalid_value: "0.101".to_string(), + predicate: "[0, 0.1]".to_string(), + }), + ); +} + +#[test] +fn liquidation_bonus_param_max_lb_out_of_range() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + + params.liquidation_bonus.max_lb = Decimal::from_str("0.0499").unwrap(); + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params: params.clone(), + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "max_lb".to_string(), + invalid_value: "0.0499".to_string(), + predicate: "[0.05, 0.3]".to_string(), + }), + ); + + params.liquidation_bonus.max_lb = Decimal::from_str("0.31").unwrap(); + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "max_lb".to_string(), + invalid_value: "0.31".to_string(), + predicate: "[0.05, 0.3]".to_string(), + }), + ); +} + +#[test] +fn liquidation_bonus_param_max_lb_gt_min_lb() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + params.liquidation_bonus.min_lb = Decimal::from_str("0.08").unwrap(); + params.liquidation_bonus.max_lb = Decimal::from_str("0.07").unwrap(); + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "max_lb".to_string(), + invalid_value: "0.07".to_string(), + predicate: "> 0.08 (min LB)".to_string(), + }), + ); +} diff --git a/contracts/params/tests/tests/test_deposit_cap.rs b/contracts/params/tests/tests/test_deposit_cap.rs new file mode 100644 index 000000000..63a51a60c --- /dev/null +++ b/contracts/params/tests/tests/test_deposit_cap.rs @@ -0,0 +1,84 @@ +use std::str::FromStr; + +use cosmwasm_std::{coins, Addr, Decimal, Uint128}; +use mars_interest_rate::get_underlying_liquidity_amount; +use mars_params::{ + msg::TotalDepositResponse, + query::query_total_deposit, + state::{ADDRESS_PROVIDER, ASSET_PARAMS}, +}; +use mars_red_bank_types::red_bank::{Market, UserDebtResponse}; +use mars_testing::{mock_dependencies, mock_env_at_block_time}; +use test_case::test_case; + +use super::helpers::default_asset_params; + +const CREDIT_MANAGER: &str = "credit_manager"; +const MOCK_DENOM: &str = "utoken"; +const TIMESTAMP: u64 = 1690573960; + +#[test_case( + Market { + denom: MOCK_DENOM.into(), + collateral_total_scaled: Uint128::zero(), + liquidity_index: Decimal::one(), + indexes_last_updated: TIMESTAMP, + ..Default::default() + }, + UserDebtResponse { + denom: MOCK_DENOM.into(), + amount_scaled: Uint128::zero(), + amount: Uint128::zero(), + uncollateralized: true, + }, + Uint128::zero(); + "zero liquidity, zero debt, zero balance" +)] +#[test_case( + Market { + denom: MOCK_DENOM.into(), + collateral_total_scaled: Uint128::new(6023580722925709342), + liquidity_index: Decimal::from_str("1.010435027113017045").unwrap(), + indexes_last_updated: 1690573862, + ..Default::default() + }, + UserDebtResponse { + denom: MOCK_DENOM.into(), + amount_scaled: Uint128::new(442125932248737808), + amount: Uint128::new(459180188271), + uncollateralized: true, + }, + Uint128::new(1751191642); + "real data queried from mainnet" +)] +fn querying_total_deposit(rb_market: Market, rb_debt: UserDebtResponse, cm_balance: Uint128) { + let mut deps = mock_dependencies(&[]); + let env = mock_env_at_block_time(TIMESTAMP); + + let params_unchecked = default_asset_params(MOCK_DENOM); + let params = params_unchecked.check(deps.as_ref().api).unwrap(); + + // setup + deps.querier.set_redbank_market(rb_market.clone()); + deps.querier.set_red_bank_user_debt(CREDIT_MANAGER, rb_debt.clone()); + deps.querier.update_balances(CREDIT_MANAGER, coins(cm_balance.u128(), MOCK_DENOM)); + ADDRESS_PROVIDER.save(deps.as_mut().storage, &Addr::unchecked("address_provider")).unwrap(); + ASSET_PARAMS.save(deps.as_mut().storage, MOCK_DENOM, ¶ms).unwrap(); + + // compute the correct, expected total deposit + let rb_deposit = + get_underlying_liquidity_amount(rb_market.collateral_total_scaled, &rb_market, TIMESTAMP) + .unwrap(); + let exp_total_deposit = rb_deposit + cm_balance - rb_debt.amount; + + // query total deposit + let res = query_total_deposit(deps.as_ref(), &env, MOCK_DENOM.into()).unwrap(); + assert_eq!( + res, + TotalDepositResponse { + denom: MOCK_DENOM.into(), + amount: exp_total_deposit, + cap: params.deposit_cap, + } + ); +} diff --git a/contracts/params/tests/tests/test_emergency_powers.rs b/contracts/params/tests/tests/test_emergency_powers.rs new file mode 100644 index 000000000..5e4c08ac8 --- /dev/null +++ b/contracts/params/tests/tests/test_emergency_powers.rs @@ -0,0 +1,156 @@ +use cosmwasm_std::Addr; +use mars_owner::OwnerError; +use mars_params::{ + error::ContractError::Owner, + msg::{ + AssetParamsUpdate, CmEmergencyUpdate, EmergencyUpdate, RedBankEmergencyUpdate, + VaultConfigUpdate, + }, +}; + +use super::helpers::{assert_err, default_asset_params, default_vault_config, MockEnv}; + +#[test] +fn only_owner_can_invoke_emergency_powers() { + let mut mock = MockEnv::new().build().unwrap(); + let bad_guy = Addr::unchecked("doctor_otto_983"); + let res = mock.emergency_update( + &bad_guy, + EmergencyUpdate::RedBank(RedBankEmergencyUpdate::DisableBorrowing("xyz".to_string())), + ); + assert_err(res, Owner(OwnerError::NotEmergencyOwner {})); + + let res = mock.emergency_update( + &bad_guy, + EmergencyUpdate::CreditManager(CmEmergencyUpdate::DisallowCoin("xyz".to_string())), + ); + assert_err(res, Owner(OwnerError::NotEmergencyOwner {})); + + let res = mock.emergency_update( + &bad_guy, + EmergencyUpdate::CreditManager(CmEmergencyUpdate::SetZeroDepositCapOnVault( + "xyz".to_string(), + )), + ); + assert_err(res, Owner(OwnerError::NotEmergencyOwner {})); + + let res = mock.emergency_update( + &bad_guy, + EmergencyUpdate::CreditManager(CmEmergencyUpdate::SetZeroMaxLtvOnVault("xyz".to_string())), + ); + assert_err(res, Owner(OwnerError::NotEmergencyOwner {})); +} + +#[test] +fn disabling_borrowing() { + let emergency_owner = Addr::unchecked("miles_morales"); + let mut mock = MockEnv::new().emergency_owner(emergency_owner.as_str()).build().unwrap(); + let denom = "atom".to_string(); + + let mut params = default_asset_params(&denom); + params.red_bank.borrow_enabled = true; + + mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ) + .unwrap(); + + let params = mock.query_asset_params(&denom); + assert!(params.red_bank.borrow_enabled); + + mock.emergency_update( + &emergency_owner, + EmergencyUpdate::RedBank(RedBankEmergencyUpdate::DisableBorrowing(denom.clone())), + ) + .unwrap(); + + let params = mock.query_asset_params(&denom); + assert!(!params.red_bank.borrow_enabled); +} + +#[test] +fn disallow_coin() { + let emergency_owner = Addr::unchecked("miles_morales"); + let mut mock = MockEnv::new().emergency_owner(emergency_owner.as_str()).build().unwrap(); + let denom = "atom".to_string(); + + let mut params = default_asset_params(&denom); + params.credit_manager.whitelisted = true; + + mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ) + .unwrap(); + + let params = mock.query_asset_params(&denom); + assert!(params.credit_manager.whitelisted); + + mock.emergency_update( + &emergency_owner, + EmergencyUpdate::CreditManager(CmEmergencyUpdate::DisallowCoin(denom.clone())), + ) + .unwrap(); + + let params = mock.query_asset_params(&denom); + assert!(!params.credit_manager.whitelisted); +} + +#[test] +fn set_zero_max_ltv() { + let emergency_owner = Addr::unchecked("miles_morales"); + let mut mock = MockEnv::new().emergency_owner(emergency_owner.as_str()).build().unwrap(); + let vault = "vault_addr_123".to_string(); + + mock.update_vault_config( + &mock.query_owner(), + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault), + }, + ) + .unwrap(); + + let params = mock.query_vault_config(&vault); + assert!(!params.max_loan_to_value.is_zero()); + + mock.emergency_update( + &emergency_owner, + EmergencyUpdate::CreditManager(CmEmergencyUpdate::SetZeroMaxLtvOnVault(vault.clone())), + ) + .unwrap(); + + let params = mock.query_vault_config(&vault); + assert!(params.max_loan_to_value.is_zero()); +} + +#[test] +fn set_zero_deposit_cap() { + let emergency_owner = Addr::unchecked("miles_morales"); + let mut mock = MockEnv::new().emergency_owner(emergency_owner.as_str()).build().unwrap(); + let vault = "vault_addr_123".to_string(); + + mock.update_vault_config( + &mock.query_owner(), + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault), + }, + ) + .unwrap(); + + let params = mock.query_vault_config(&vault); + assert!(!params.deposit_cap.amount.is_zero()); + + mock.emergency_update( + &emergency_owner, + EmergencyUpdate::CreditManager(CmEmergencyUpdate::SetZeroDepositCapOnVault(vault.clone())), + ) + .unwrap(); + + let params = mock.query_vault_config(&vault); + assert!(params.deposit_cap.amount.is_zero()); +} diff --git a/contracts/params/tests/tests/test_owner.rs b/contracts/params/tests/tests/test_owner.rs new file mode 100644 index 000000000..79ca8aaa6 --- /dev/null +++ b/contracts/params/tests/tests/test_owner.rs @@ -0,0 +1,45 @@ +use cosmwasm_std::Addr; +use mars_owner::{OwnerError, OwnerUpdate}; +use mars_params::error::ContractError::Owner; + +use super::helpers::{assert_err, MockEnv}; + +#[test] +fn owner_set_on_init() { + let mock = MockEnv::new().build().unwrap(); + let owner = mock.query_owner(); + assert_eq!("owner", &owner.to_string()) +} + +#[test] +fn only_owner_can_execute_updates() { + let mut mock = MockEnv::new().build().unwrap(); + let bad_guy = Addr::unchecked("doctor_otto_983"); + let res = mock.update_owner( + &bad_guy, + OwnerUpdate::ProposeNewOwner { + proposed: bad_guy.to_string(), + }, + ); + assert_err(res, Owner(OwnerError::NotOwner {})); +} + +#[test] +fn owner_can_execute_updates() { + let mut mock = MockEnv::new().build().unwrap(); + + let ownership = mock.query_ownership(); + assert_eq!(ownership.emergency_owner, None); + + let em_owner = "miles_morales".to_string(); + mock.update_owner( + &mock.query_owner(), + OwnerUpdate::SetEmergencyOwner { + emergency_owner: em_owner.clone(), + }, + ) + .unwrap(); + + let ownership = mock.query_ownership(); + assert_eq!(ownership.emergency_owner, Some(em_owner)); +} diff --git a/contracts/params/tests/tests/test_target_health_factor.rs b/contracts/params/tests/tests/test_target_health_factor.rs new file mode 100644 index 000000000..9eaa6f1ee --- /dev/null +++ b/contracts/params/tests/tests/test_target_health_factor.rs @@ -0,0 +1,72 @@ +use std::str::FromStr; + +use cosmwasm_std::{Addr, Decimal}; +use mars_owner::OwnerError; +use mars_params::error::ContractError::{Owner, Validation}; +use mars_utils::error::ValidationError::InvalidParam; + +use super::helpers::{assert_err, MockEnv}; + +#[test] +fn thf_set_on_init() { + let mock = MockEnv::new().build().unwrap(); + let thf = mock.query_target_health_factor(); + assert_eq!(thf, Decimal::from_str("1.05").unwrap()) +} + +#[test] +fn thf_validated_on_init() { + let res = MockEnv::new().target_health_factor(Decimal::from_str("0.99").unwrap()).build(); + if res.is_ok() { + panic!("Should have thrown an instantiate error"); + } +} + +#[test] +fn only_owner_can_update_thf() { + let mut mock = MockEnv::new().build().unwrap(); + let bad_guy = Addr::unchecked("doctor_otto_983"); + let res = mock.update_target_health_factor(&bad_guy, Decimal::from_str("1.1").unwrap()); + assert_err(res, Owner(OwnerError::NotOwner {})); +} + +#[test] +fn validated_updates() { + let mut mock = MockEnv::new().build().unwrap(); + + let res = + mock.update_target_health_factor(&mock.query_owner(), Decimal::from_str("0.99").unwrap()); + assert_err( + res, + Validation(InvalidParam { + param_name: "target_health_factor".to_string(), + invalid_value: "0.99".to_string(), + predicate: "[1, 2]".to_string(), + }), + ); + + let res = + mock.update_target_health_factor(&mock.query_owner(), Decimal::from_str("2.01").unwrap()); + assert_err( + res, + Validation(InvalidParam { + param_name: "target_health_factor".to_string(), + invalid_value: "2.01".to_string(), + predicate: "[1, 2]".to_string(), + }), + ); +} + +#[test] +fn update_thf() { + let mut mock = MockEnv::new().build().unwrap(); + let target_health_factor = Decimal::from_str("1.08").unwrap(); + let current_thf = mock.query_target_health_factor(); + assert_ne!(current_thf, target_health_factor); + + mock.update_target_health_factor(&mock.query_owner(), Decimal::from_str("1.08").unwrap()) + .unwrap(); + + let current_thf = mock.query_target_health_factor(); + assert_eq!(current_thf, target_health_factor); +} diff --git a/contracts/params/tests/tests/test_update_asset_params.rs b/contracts/params/tests/tests/test_update_asset_params.rs new file mode 100644 index 000000000..ee0601045 --- /dev/null +++ b/contracts/params/tests/tests/test_update_asset_params.rs @@ -0,0 +1,249 @@ +use cosmwasm_std::Addr; +use mars_owner::OwnerError; +use mars_params::{error::ContractError::Owner, msg::AssetParamsUpdate}; + +use super::helpers::{assert_contents_equal, assert_err, default_asset_params, MockEnv}; + +#[test] +fn initial_state_of_params() { + let mock = MockEnv::new().build().unwrap(); + let params = mock.query_all_asset_params(None, None); + assert!(params.is_empty()); +} + +#[test] +fn only_owner_can_update_asset_params() { + let mut mock = MockEnv::new().build().unwrap(); + let bad_guy = Addr::unchecked("doctor_otto_983"); + let res = mock.update_asset_params( + &bad_guy, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params("xyz"), + }, + ); + assert_err(res, Owner(OwnerError::NotOwner {})); +} + +#[test] +fn initializing_asset_param() { + let mut mock = MockEnv::new().build().unwrap(); + let owner = mock.query_owner(); + let denom0 = "atom".to_string(); + let denom1 = "osmo".to_string(); + + let params = default_asset_params(&denom0); + + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: params.clone(), + }, + ) + .unwrap(); + + let all_asset_params = mock.query_all_asset_params(None, None); + assert_eq!(1, all_asset_params.len()); + let res = all_asset_params.first().unwrap(); + assert_eq!(&denom0, &res.denom); + + // Validate config set correctly + assert_eq!(params, res.clone().into()); + + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom1), + }, + ) + .unwrap(); + + let asset_params = mock.query_all_asset_params(None, None); + assert_eq!(2, asset_params.len()); + assert_eq!(&denom1, &asset_params.get(1).unwrap().denom); +} + +#[test] +fn add_same_denom_multiple_times() { + let mut mock = MockEnv::new().build().unwrap(); + let owner = mock.query_owner(); + let denom0 = "atom".to_string(); + + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom0), + }, + ) + .unwrap(); + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom0), + }, + ) + .unwrap(); + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom0), + }, + ) + .unwrap(); + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom0), + }, + ) + .unwrap(); + + let asset_params = mock.query_all_asset_params(None, None); + assert_eq!(1, asset_params.len()); + assert_eq!(denom0, asset_params.first().unwrap().denom); +} + +#[test] +fn update_existing_asset_params() { + let mut mock = MockEnv::new().build().unwrap(); + let owner = mock.query_owner(); + let denom0 = "atom".to_string(); + + let mut params = default_asset_params(&denom0); + + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: params.clone(), + }, + ) + .unwrap(); + + let asset_params = mock.query_asset_params(&denom0); + assert!(!asset_params.credit_manager.whitelisted); + assert!(asset_params.red_bank.deposit_enabled); + + params.credit_manager.whitelisted = true; + params.red_bank.deposit_enabled = false; + + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params, + }, + ) + .unwrap(); + + let all_asset_params = mock.query_all_asset_params(None, None); + assert_eq!(1, all_asset_params.len()); + + let asset_params = mock.query_asset_params(&denom0); + assert!(asset_params.credit_manager.whitelisted); + assert!(!asset_params.red_bank.deposit_enabled); +} + +#[test] +fn removing_from_asset_params() { + let mut mock = MockEnv::new().build().unwrap(); + let owner = mock.query_owner(); + let denom0 = "atom".to_string(); + let denom1 = "osmo".to_string(); + let denom2 = "juno".to_string(); + + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom0), + }, + ) + .unwrap(); + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom1), + }, + ) + .unwrap(); + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom2), + }, + ) + .unwrap(); + + let asset_params = mock.query_all_asset_params(None, None); + assert_eq!(3, asset_params.len()); +} + +#[test] +fn pagination_query() { + let mut mock = MockEnv::new().build().unwrap(); + let owner = mock.query_owner(); + let denom0 = "atom".to_string(); + let denom1 = "osmo".to_string(); + let denom2 = "juno".to_string(); + let denom3 = "mars".to_string(); + let denom4 = "ion".to_string(); + let denom5 = "usdc".to_string(); + + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom0), + }, + ) + .unwrap(); + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom1), + }, + ) + .unwrap(); + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom2), + }, + ) + .unwrap(); + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom3), + }, + ) + .unwrap(); + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom4), + }, + ) + .unwrap(); + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom5), + }, + ) + .unwrap(); + + let asset_params_a = mock.query_all_asset_params(None, Some(2)); + let asset_params_b = + mock.query_all_asset_params(asset_params_a.last().map(|r| r.denom.clone()), Some(2)); + let asset_params_c = + mock.query_all_asset_params(asset_params_b.last().map(|r| r.denom.clone()), None); + + let combined = asset_params_a + .iter() + .cloned() + .chain(asset_params_b.iter().cloned()) + .chain(asset_params_c.iter().cloned()) + .map(|r| r.denom) + .collect::>(); + + assert_eq!(6, combined.len()); + + assert_contents_equal(&[denom0, denom1, denom2, denom3, denom4, denom5], &combined) +} diff --git a/contracts/params/tests/tests/test_vault_validation.rs b/contracts/params/tests/tests/test_vault_validation.rs new file mode 100644 index 000000000..ecf4021bf --- /dev/null +++ b/contracts/params/tests/tests/test_vault_validation.rs @@ -0,0 +1,172 @@ +use std::str::FromStr; + +use cosmwasm_std::{Decimal, StdError::GenericErr}; +use mars_params::{ + error::ContractError::{Std, Validation}, + msg::VaultConfigUpdate, + types::hls::HlsParamsUnchecked, +}; +use mars_utils::error::ValidationError::InvalidParam; + +use super::helpers::{assert_err, default_vault_config, MockEnv}; + +#[test] +fn vault_addr_must_be_valid() { + let mut mock = MockEnv::new().build().unwrap(); + + let res = mock.update_vault_config( + &mock.query_owner(), + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config("%"), + }, + ); + assert_err( + res, + Std(GenericErr { msg: "Invalid input: human address too short for this mock implementation (must be >= 3).".to_string() }), + ); +} + +#[test] +fn vault_max_ltv_less_than_or_equal_to_one() { + let mut mock = MockEnv::new().build().unwrap(); + let mut config = default_vault_config("vault_xyz"); + config.max_loan_to_value = Decimal::from_str("1.1235").unwrap(); + + let res = mock.update_vault_config( + &mock.query_owner(), + VaultConfigUpdate::AddOrUpdate { + config, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "max_loan_to_value".to_string(), + invalid_value: "1.1235".to_string(), + predicate: "<= 1".to_string(), + }), + ); +} + +#[test] +fn vault_liquidation_threshold_less_than_or_equal_to_one() { + let mut mock = MockEnv::new().build().unwrap(); + let mut config = default_vault_config("vault_xyz"); + config.liquidation_threshold = Decimal::from_str("1.1235").unwrap(); + + let res = mock.update_vault_config( + &mock.query_owner(), + VaultConfigUpdate::AddOrUpdate { + config, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "liquidation_threshold".to_string(), + invalid_value: "1.1235".to_string(), + predicate: "<= 1".to_string(), + }), + ); +} + +#[test] +fn vault_liq_threshold_gt_max_ltv() { + let mut mock = MockEnv::new().build().unwrap(); + let mut config = default_vault_config("vault_xyz"); + config.liquidation_threshold = Decimal::from_str("0.5").unwrap(); + config.max_loan_to_value = Decimal::from_str("0.6").unwrap(); + + let res = mock.update_vault_config( + &mock.query_owner(), + VaultConfigUpdate::AddOrUpdate { + config, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "liquidation_threshold".to_string(), + invalid_value: "0.5".to_string(), + predicate: "> 0.6 (max LTV)".to_string(), + }), + ); +} + +#[test] +fn vault_hls_max_ltv_less_than_or_equal_to_one() { + let mut mock = MockEnv::new().build().unwrap(); + let mut config = default_vault_config("vault_xyz"); + config.hls = Some(HlsParamsUnchecked { + max_loan_to_value: Decimal::from_str("1.1235").unwrap(), + liquidation_threshold: Decimal::from_str("2.1235").unwrap(), + correlations: vec![], + }); + + let res = mock.update_vault_config( + &mock.query_owner(), + VaultConfigUpdate::AddOrUpdate { + config, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "hls_max_loan_to_value".to_string(), + invalid_value: "1.1235".to_string(), + predicate: "<= 1".to_string(), + }), + ); +} + +#[test] +fn vault_hls_liquidation_threshold_less_than_or_equal_to_one() { + let mut mock = MockEnv::new().build().unwrap(); + let mut config = default_vault_config("vault_xyz"); + config.hls = Some(HlsParamsUnchecked { + max_loan_to_value: Decimal::from_str("0.8").unwrap(), + liquidation_threshold: Decimal::from_str("1.1235").unwrap(), + correlations: vec![], + }); + + let res = mock.update_vault_config( + &mock.query_owner(), + VaultConfigUpdate::AddOrUpdate { + config, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "hls_liquidation_threshold".to_string(), + invalid_value: "1.1235".to_string(), + predicate: "<= 1".to_string(), + }), + ); +} + +#[test] +fn vault_hls_liq_threshold_gt_max_ltv() { + let mut mock = MockEnv::new().build().unwrap(); + let mut config = default_vault_config("vault_xyz"); + config.hls = Some(HlsParamsUnchecked { + max_loan_to_value: Decimal::from_str("0.6").unwrap(), + liquidation_threshold: Decimal::from_str("0.5").unwrap(), + correlations: vec![], + }); + + let res = mock.update_vault_config( + &mock.query_owner(), + VaultConfigUpdate::AddOrUpdate { + config, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "hls_liquidation_threshold".to_string(), + invalid_value: "0.5".to_string(), + predicate: "> 0.6 (hls max LTV)".to_string(), + }), + ); +} diff --git a/contracts/params/tests/tests/test_vaults.rs b/contracts/params/tests/tests/test_vaults.rs new file mode 100644 index 000000000..2f87735ae --- /dev/null +++ b/contracts/params/tests/tests/test_vaults.rs @@ -0,0 +1,225 @@ +use std::str::FromStr; + +use cosmwasm_std::{Addr, Decimal}; +use mars_owner::OwnerError; +use mars_params::{ + error::ContractError::Owner, msg::VaultConfigUpdate, types::vault::VaultConfigUnchecked, +}; + +use super::helpers::{assert_contents_equal, assert_err, default_vault_config, MockEnv}; + +#[test] +fn initial_state_of_vault_configs() { + let mock = MockEnv::new().build().unwrap(); + let configs = mock.query_all_vault_configs(None, None); + assert!(configs.is_empty()); +} + +#[test] +fn only_owner_can_update_vault_configs() { + let mut mock = MockEnv::new().build().unwrap(); + let bad_guy = Addr::unchecked("doctor_otto_983"); + let res = mock.update_vault_config( + &bad_guy, + VaultConfigUpdate::AddOrUpdate { + config: VaultConfigUnchecked { + addr: "xyz".to_string(), + deposit_cap: Default::default(), + max_loan_to_value: Default::default(), + liquidation_threshold: Default::default(), + whitelisted: false, + hls: None, + }, + }, + ); + assert_err(res, Owner(OwnerError::NotOwner {})); +} + +#[test] +fn initializing_asset_param() { + let mut mock = MockEnv::new().build().unwrap(); + let owner = mock.query_owner(); + let vault0 = "vault_addr_0".to_string(); + let vault1 = "vault_addr_1".to_string(); + + let starting_vault_config = default_vault_config(&vault0); + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: starting_vault_config.clone(), + }, + ) + .unwrap(); + + let all_vault_configs = mock.query_all_vault_configs(None, None); + assert_eq!(1, all_vault_configs.len()); + + // Validate config set correctly + let config = all_vault_configs.first().unwrap(); + assert_eq!(starting_vault_config, config.clone().into()); + + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault1), + }, + ) + .unwrap(); + + let vault_configs = mock.query_all_vault_configs(None, None); + assert_eq!(2, vault_configs.len()); + assert_eq!(&vault1, &vault_configs.get(1).unwrap().addr); +} + +#[test] +fn add_same_vault_multiple_times() { + let mut mock = MockEnv::new().build().unwrap(); + let owner = mock.query_owner(); + let vault0 = "vault_addr_0".to_string(); + + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault0), + }, + ) + .unwrap(); + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault0), + }, + ) + .unwrap(); + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault0), + }, + ) + .unwrap(); + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault0), + }, + ) + .unwrap(); + + let vault_configs = mock.query_all_vault_configs(None, None); + assert_eq!(1, vault_configs.len()); + assert_eq!(vault0, vault_configs.first().unwrap().addr); +} + +#[test] +fn update_existing_vault_configs() { + let mut mock = MockEnv::new().build().unwrap(); + let owner = mock.query_owner(); + let vault0 = "vault_addr_0".to_string(); + + let mut config = default_vault_config(&vault0); + + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: config.clone(), + }, + ) + .unwrap(); + + let vault_config = mock.query_vault_config(&vault0); + assert!(vault_config.whitelisted); + assert_eq!(vault_config.max_loan_to_value, Decimal::from_str("0.47").unwrap()); + + let new_max_ltv = Decimal::from_str("0.39").unwrap(); + config.whitelisted = false; + config.max_loan_to_value = new_max_ltv; + + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config, + }, + ) + .unwrap(); + + let all_vault_configs = mock.query_all_vault_configs(None, None); + assert_eq!(1, all_vault_configs.len()); + + let vault_config = mock.query_vault_config(&vault0); + assert!(!vault_config.whitelisted); + assert_eq!(vault_config.max_loan_to_value, new_max_ltv); +} + +#[test] +fn pagination_query() { + let mut mock = MockEnv::new().build().unwrap(); + let owner = mock.query_owner(); + let vault0 = "vault_addr_0".to_string(); + let vault1 = "vault_addr_1".to_string(); + let vault2 = "vault_addr_2".to_string(); + let vault3 = "vault_addr_3".to_string(); + let vault4 = "vault_addr_4".to_string(); + let vault5 = "vault_addr_5".to_string(); + + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault0), + }, + ) + .unwrap(); + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault1), + }, + ) + .unwrap(); + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault2), + }, + ) + .unwrap(); + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault3), + }, + ) + .unwrap(); + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault4), + }, + ) + .unwrap(); + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault5), + }, + ) + .unwrap(); + + let vault_configs_a = mock.query_all_vault_configs(None, Some(2)); + let vault_configs_b = + mock.query_all_vault_configs(vault_configs_a.last().map(|r| r.addr.to_string()), Some(2)); + let vault_configs_c = + mock.query_all_vault_configs(vault_configs_b.last().map(|r| r.addr.to_string()), None); + + let combined = vault_configs_a + .iter() + .cloned() + .chain(vault_configs_b.iter().cloned()) + .chain(vault_configs_c.iter().cloned()) + .map(|r| r.addr.to_string()) + .collect::>(); + + assert_eq!(6, combined.len()); + + assert_contents_equal(&[vault0, vault1, vault2, vault3, vault4, vault5], &combined) +} diff --git a/contracts/red-bank/Cargo.toml b/contracts/red-bank/Cargo.toml index 1d56efd11..9f0f9963a 100644 --- a/contracts/red-bank/Cargo.toml +++ b/contracts/red-bank/Cargo.toml @@ -12,10 +12,7 @@ keywords = { workspace = true } [lib] crate-type = ["cdylib", "rlib"] -doctest = false - -[profile.release] -overflow-checks = true +doctest = false [features] # for more explicit tests, cargo test --features=backtraces @@ -27,11 +24,18 @@ cw2 = { workspace = true } cw-storage-plus = { workspace = true } cw-utils = { workspace = true } mars-health = { workspace = true } +mars-interest-rate = { workspace = true } +mars-liquidation = { workspace = true } mars-owner = { workspace = true } +mars-params = { workspace = true } mars-red-bank-types = { workspace = true } mars-utils = { workspace = true } +pyth-sdk-cw = { workspace = true } thiserror = { workspace = true } [dev-dependencies] +anyhow = { workspace = true } cosmwasm-schema = { workspace = true } +cw-multi-test = { workspace = true } mars-testing = { workspace = true } +test-case = { workspace = true } diff --git a/contracts/red-bank/src/asset.rs b/contracts/red-bank/src/asset.rs new file mode 100644 index 000000000..587c44011 --- /dev/null +++ b/contracts/red-bank/src/asset.rs @@ -0,0 +1,144 @@ +use cosmwasm_std::{Decimal, DepsMut, Env, MessageInfo, Response, Uint128}; +use mars_red_bank_types::{ + address_provider, + address_provider::MarsAddressType, + error::MarsError, + red_bank::{InitOrUpdateAssetParams, Market}, +}; +use mars_utils::helpers::validate_native_denom; + +use crate::{ + error::ContractError, + interest_rates::{apply_accumulated_interests, update_interest_rates}, + state::{CONFIG, MARKETS, OWNER}, +}; + +/// Initialize asset if not exist. +/// Initialization requires that all params are provided and there is no asset in state. +pub fn init_asset( + deps: DepsMut, + env: Env, + info: MessageInfo, + denom: String, + params: InitOrUpdateAssetParams, +) -> Result { + OWNER.assert_owner(deps.storage, &info.sender)?; + + validate_native_denom(&denom)?; + + if MARKETS.may_load(deps.storage, &denom)?.is_some() { + return Err(ContractError::AssetAlreadyInitialized {}); + } + + let new_market = create_market(env.block.time.seconds(), &denom, params)?; + MARKETS.save(deps.storage, &denom, &new_market)?; + + Ok(Response::new().add_attribute("action", "init_asset").add_attribute("denom", denom)) +} + +/// Initialize new market +pub fn create_market( + block_time: u64, + denom: &str, + params: InitOrUpdateAssetParams, +) -> Result { + // Destructuring a struct’s fields into separate variables in order to force + // compile error if we add more params + let InitOrUpdateAssetParams { + reserve_factor, + interest_rate_model, + } = params; + + // All fields should be available + let available = reserve_factor.is_some() && interest_rate_model.is_some(); + + if !available { + return Err(MarsError::InstantiateParamsUnavailable {}.into()); + } + + let new_market = Market { + denom: denom.to_string(), + borrow_index: Decimal::one(), + liquidity_index: Decimal::one(), + borrow_rate: Decimal::zero(), + liquidity_rate: Decimal::zero(), + reserve_factor: reserve_factor.unwrap(), + indexes_last_updated: block_time, + collateral_total_scaled: Uint128::zero(), + debt_total_scaled: Uint128::zero(), + interest_rate_model: interest_rate_model.unwrap(), + }; + + new_market.validate()?; + + Ok(new_market) +} + +/// Update asset with new params. +pub fn update_asset( + deps: DepsMut, + env: Env, + info: MessageInfo, + denom: String, + params: InitOrUpdateAssetParams, +) -> Result { + OWNER.assert_owner(deps.storage, &info.sender)?; + + let market_option = MARKETS.may_load(deps.storage, &denom)?; + match market_option { + None => Err(ContractError::AssetNotInitialized {}), + Some(mut market) => { + // Destructuring a struct’s fields into separate variables in order to force + // compile error if we add more params + let InitOrUpdateAssetParams { + reserve_factor, + interest_rate_model, + } = params; + + // If reserve factor or interest rates are updated we update indexes with + // current values before applying the change to prevent applying this + // new params to a period where they were not valid yet. Interests rates are + // recalculated after changes are applied. + let should_update_interest_rates = (reserve_factor.is_some() + && reserve_factor.unwrap() != market.reserve_factor) + || interest_rate_model.is_some(); + + let mut response = Response::new(); + + if should_update_interest_rates { + let config = CONFIG.load(deps.storage)?; + let addresses = address_provider::helpers::query_contract_addrs( + deps.as_ref(), + &config.address_provider, + vec![MarsAddressType::Incentives, MarsAddressType::RewardsCollector], + )?; + let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; + let incentives_addr = &addresses[&MarsAddressType::Incentives]; + + response = apply_accumulated_interests( + deps.storage, + &env, + &mut market, + rewards_collector_addr, + incentives_addr, + response, + )?; + } + + let mut updated_market = Market { + reserve_factor: reserve_factor.unwrap_or(market.reserve_factor), + interest_rate_model: interest_rate_model.unwrap_or(market.interest_rate_model), + ..market + }; + + updated_market.validate()?; + + if should_update_interest_rates { + response = update_interest_rates(&env, &mut updated_market, response)?; + } + MARKETS.save(deps.storage, &denom, &updated_market)?; + + Ok(response.add_attribute("action", "update_asset").add_attribute("denom", denom)) + } + } +} diff --git a/contracts/red-bank/src/borrow.rs b/contracts/red-bank/src/borrow.rs new file mode 100644 index 000000000..ad57b1a14 --- /dev/null +++ b/contracts/red-bank/src/borrow.rs @@ -0,0 +1,141 @@ +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Uint128}; +use mars_interest_rate::{ + get_scaled_debt_amount, get_underlying_debt_amount, get_underlying_liquidity_amount, +}; +use mars_red_bank_types::{address_provider, address_provider::MarsAddressType}; +use mars_utils::helpers::build_send_asset_msg; + +use crate::{ + error::ContractError, + health::assert_below_max_ltv_after_borrow, + helpers::query_asset_params, + interest_rates::{apply_accumulated_interests, update_interest_rates}, + state::{CONFIG, MARKETS}, + user::User, +}; + +/// Add debt for the borrower and send the borrowed funds +pub fn borrow( + deps: DepsMut, + env: Env, + info: MessageInfo, + denom: String, + borrow_amount: Uint128, + recipient: Option, +) -> Result { + let borrower = User(&info.sender); + + let config = CONFIG.load(deps.storage)?; + + let addresses = address_provider::helpers::query_contract_addrs( + deps.as_ref(), + &config.address_provider, + vec![ + MarsAddressType::Oracle, + MarsAddressType::Incentives, + MarsAddressType::RewardsCollector, + MarsAddressType::Params, + ], + )?; + let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; + let incentives_addr = &addresses[&MarsAddressType::Incentives]; + let oracle_addr = &addresses[&MarsAddressType::Oracle]; + let params_addr = &addresses[&MarsAddressType::Params]; + + let asset_params = query_asset_params(&deps.querier, params_addr, &denom)?; + + if !asset_params.red_bank.borrow_enabled { + return Err(ContractError::BorrowNotEnabled { + denom, + }); + } + + // Load market and user state + let mut borrow_market = MARKETS.load(deps.storage, &denom)?; + + let collateral_balance_before = get_underlying_liquidity_amount( + borrow_market.collateral_total_scaled, + &borrow_market, + env.block.time.seconds(), + )?; + + // Cannot borrow zero amount or more than available collateral + if borrow_amount.is_zero() || borrow_amount > collateral_balance_before { + return Err(ContractError::InvalidBorrowAmount { + denom, + }); + } + + let uncollateralized_loan_limit = borrower.uncollateralized_loan_limit(deps.storage, &denom)?; + + // Check if user can borrow specified amount + let mut uncollateralized_debt = false; + if uncollateralized_loan_limit.is_zero() { + if !assert_below_max_ltv_after_borrow( + &deps.as_ref(), + &env, + borrower.address(), + "", + oracle_addr, + params_addr, + &denom, + borrow_amount, + )? { + return Err(ContractError::BorrowAmountExceedsGivenCollateral {}); + } + } else { + // Uncollateralized loan: check borrow amount plus debt does not exceed uncollateralized loan limit + uncollateralized_debt = true; + + let debt_amount_scaled = borrower.debt_amount_scaled(deps.storage, &denom)?; + + let asset_market = MARKETS.load(deps.storage, &denom)?; + let debt_amount = get_underlying_debt_amount( + debt_amount_scaled, + &asset_market, + env.block.time.seconds(), + )?; + + let debt_after_borrow = debt_amount.checked_add(borrow_amount)?; + if debt_after_borrow > uncollateralized_loan_limit { + return Err(ContractError::BorrowAmountExceedsUncollateralizedLoanLimit {}); + } + } + + let mut response = Response::new(); + + response = apply_accumulated_interests( + deps.storage, + &env, + &mut borrow_market, + rewards_collector_addr, + incentives_addr, + response, + )?; + + // Set new debt + let borrow_amount_scaled = + get_scaled_debt_amount(borrow_amount, &borrow_market, env.block.time.seconds())?; + + borrow_market.increase_debt(borrow_amount_scaled)?; + borrower.increase_debt(deps.storage, &denom, borrow_amount_scaled, uncollateralized_debt)?; + + response = update_interest_rates(&env, &mut borrow_market, response)?; + MARKETS.save(deps.storage, &denom, &borrow_market)?; + + // Send borrow amount to borrower or another recipient + let recipient_addr = if let Some(recipient) = recipient { + deps.api.addr_validate(&recipient)? + } else { + borrower.address().clone() + }; + + Ok(response + .add_message(build_send_asset_msg(&recipient_addr, &denom, borrow_amount)) + .add_attribute("action", "borrow") + .add_attribute("sender", borrower) + .add_attribute("recipient", recipient_addr) + .add_attribute("denom", denom) + .add_attribute("amount", borrow_amount) + .add_attribute("amount_scaled", borrow_amount_scaled)) +} diff --git a/contracts/red-bank/src/collateral.rs b/contracts/red-bank/src/collateral.rs new file mode 100644 index 000000000..9e6fc65ea --- /dev/null +++ b/contracts/red-bank/src/collateral.rs @@ -0,0 +1,69 @@ +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; +use mars_red_bank_types::{ + self, + address_provider::{self, MarsAddressType}, +}; + +use crate::{ + error::ContractError, + health::get_health_and_positions, + state::{COLLATERALS, CONFIG}, + user::User, +}; + +/// Update (enable / disable) collateral asset for specific user +pub fn update_asset_collateral_status( + deps: DepsMut, + env: Env, + info: MessageInfo, + denom: String, + enable: bool, +) -> Result { + let user = User(&info.sender); + + let mut collateral = COLLATERALS + .may_load(deps.storage, (user.address(), "", &denom))? + .ok_or_else(|| ContractError::UserNoCollateralBalance { + user: user.into(), + denom: denom.clone(), + })?; + + let previously_enabled = collateral.enabled; + + collateral.enabled = enable; + COLLATERALS.save(deps.storage, (user.address(), "", &denom), &collateral)?; + + // if the collateral was previously enabled, but is not disabled, it is necessary to ensure the + // user is not liquidatable after disabling + if previously_enabled && !enable { + let config = CONFIG.load(deps.storage)?; + + let addresses = address_provider::helpers::query_contract_addrs( + deps.as_ref(), + &config.address_provider, + vec![MarsAddressType::Oracle, MarsAddressType::Params], + )?; + let oracle_addr = &addresses[&MarsAddressType::Oracle]; + let params_addr = &addresses[&MarsAddressType::Params]; + + let (health, _) = get_health_and_positions( + &deps.as_ref(), + &env, + user.address(), + "", + oracle_addr, + params_addr, + false, + )?; + + if health.is_liquidatable() { + return Err(ContractError::InvalidHealthFactorAfterDisablingCollateral {}); + } + } + + Ok(Response::new() + .add_attribute("action", "update_asset_collateral_status") + .add_attribute("user", user) + .add_attribute("denom", denom) + .add_attribute("enable", enable.to_string())) +} diff --git a/contracts/red-bank/src/config.rs b/contracts/red-bank/src/config.rs new file mode 100644 index 000000000..eede3e1c2 --- /dev/null +++ b/contracts/red-bank/src/config.rs @@ -0,0 +1,42 @@ +use cosmwasm_std::{DepsMut, MessageInfo, Response}; +use mars_owner::OwnerUpdate; +use mars_red_bank_types::red_bank::CreateOrUpdateConfig; +use mars_utils::helpers::option_string_to_addr; + +use crate::{ + error::ContractError, + state::{CONFIG, OWNER}, +}; + +pub fn update_owner( + deps: DepsMut, + info: MessageInfo, + update: OwnerUpdate, +) -> Result { + Ok(OWNER.update(deps, info, update)?) +} + +/// Update config +pub fn update_config( + deps: DepsMut, + info: MessageInfo, + new_config: CreateOrUpdateConfig, +) -> Result { + OWNER.assert_owner(deps.storage, &info.sender)?; + + let mut config = CONFIG.load(deps.storage)?; + + // Destructuring a struct’s fields into separate variables in order to force + // compile error if we add more params + let CreateOrUpdateConfig { + address_provider, + } = new_config; + + // Update config + config.address_provider = + option_string_to_addr(deps.api, address_provider, config.address_provider)?; + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new().add_attribute("action", "update_config")) +} diff --git a/contracts/red-bank/src/contract.rs b/contracts/red-bank/src/contract.rs index cbbb0687b..409729cc6 100644 --- a/contracts/red-bank/src/contract.rs +++ b/contracts/red-bank/src/contract.rs @@ -1,7 +1,12 @@ -use cosmwasm_std::{entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response}; +use cosmwasm_std::{ + entry_point, to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult, +}; use mars_red_bank_types::red_bank::{ExecuteMsg, InstantiateMsg, QueryMsg}; -use crate::{error::ContractError, execute, query}; +use crate::{ + asset, borrow, collateral, config, deposit, error::ContractError, instantiate, liquidate, + query, repay, uncollateralized_loan, withdraw, +}; #[entry_point] pub fn instantiate( @@ -10,7 +15,7 @@ pub fn instantiate( _info: MessageInfo, msg: InstantiateMsg, ) -> Result { - execute::instantiate(deps, msg) + instantiate::instantiate(deps, msg) } #[entry_point] @@ -21,39 +26,61 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { - ExecuteMsg::UpdateOwner(update) => execute::update_owner(deps, info, update), + ExecuteMsg::UpdateOwner(update) => config::update_owner(deps, info, update), ExecuteMsg::UpdateConfig { config, - } => execute::update_config(deps, info, config), + } => config::update_config(deps, info, config), ExecuteMsg::InitAsset { denom, params, - } => execute::init_asset(deps, env, info, denom, params), + } => asset::init_asset(deps, env, info, denom, params), ExecuteMsg::UpdateAsset { denom, params, - } => execute::update_asset(deps, env, info, denom, params), + } => asset::update_asset(deps, env, info, denom, params), ExecuteMsg::UpdateUncollateralizedLoanLimit { user, denom, new_limit, } => { let user_addr = deps.api.addr_validate(&user)?; - execute::update_uncollateralized_loan_limit(deps, info, user_addr, denom, new_limit) + uncollateralized_loan::update_uncollateralized_loan_limit( + deps, info, user_addr, denom, new_limit, + ) } ExecuteMsg::Deposit { + account_id, on_behalf_of, } => { let sent_coin = cw_utils::one_coin(&info)?; - execute::deposit(deps, env, info, on_behalf_of, sent_coin.denom, sent_coin.amount) + deposit::deposit( + deps, + env, + info, + on_behalf_of, + sent_coin.denom, + sent_coin.amount, + account_id, + ) } ExecuteMsg::Withdraw { denom, amount, recipient, + account_id, + liquidation_related, } => { cw_utils::nonpayable(&info)?; - execute::withdraw(deps, env, info, denom, amount, recipient) + withdraw::withdraw( + deps, + env, + info, + denom, + amount, + recipient, + account_id, + liquidation_related.unwrap_or(false), + ) } ExecuteMsg::Borrow { denom, @@ -61,13 +88,13 @@ pub fn execute( recipient, } => { cw_utils::nonpayable(&info)?; - execute::borrow(deps, env, info, denom, amount, recipient) + borrow::borrow(deps, env, info, denom, amount, recipient) } ExecuteMsg::Repay { on_behalf_of, } => { let sent_coin = cw_utils::one_coin(&info)?; - execute::repay(deps, env, info, on_behalf_of, sent_coin.denom, sent_coin.amount) + repay::repay(deps, env, info, on_behalf_of, sent_coin.denom, sent_coin.amount) } ExecuteMsg::Liquidate { user, @@ -76,7 +103,7 @@ pub fn execute( } => { let user_addr = deps.api.addr_validate(&user)?; let sent_coin = cw_utils::one_coin(&info)?; - execute::liquidate( + liquidate::liquidate( deps, env, info, @@ -92,7 +119,7 @@ pub fn execute( enable, } => { cw_utils::nonpayable(&info)?; - execute::update_asset_collateral_status(deps, env, info, denom, enable) + collateral::update_asset_collateral_status(deps, env, info, denom, enable) } } } @@ -145,13 +172,17 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { let user_addr = deps.api.addr_validate(&user)?; - to_binary(&query::query_user_collateral(deps, &env.block, user_addr, denom)?) + to_binary(&query::query_user_collateral( + deps, &env.block, user_addr, account_id, denom, + )?) } QueryMsg::UserCollaterals { user, + account_id, start_after, limit, } => { @@ -160,15 +191,40 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { + let user_addr = deps.api.addr_validate(&user)?; + to_binary(&query::query_user_collaterals_v2( + deps, + &env.block, + user_addr, + account_id, start_after, limit, )?) } QueryMsg::UserPosition { user, + account_id, + } => { + let user_addr = deps.api.addr_validate(&user)?; + to_binary(&query::query_user_position(deps, env, user_addr, account_id, false)?) + } + QueryMsg::UserPositionLiquidationPricing { + user, + account_id, } => { let user_addr = deps.api.addr_validate(&user)?; - to_binary(&query::query_user_position(deps, env, user_addr)?) + to_binary(&query::query_user_position(deps, env, user_addr, account_id, true)?) } QueryMsg::ScaledLiquidityAmount { denom, @@ -189,3 +245,8 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result StdResult { + Ok(Response::default()) +} diff --git a/contracts/red-bank/src/deposit.rs b/contracts/red-bank/src/deposit.rs new file mode 100644 index 000000000..e3fe60668 --- /dev/null +++ b/contracts/red-bank/src/deposit.rs @@ -0,0 +1,123 @@ +use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response, Uint128}; +use mars_interest_rate::get_scaled_liquidity_amount; +use mars_red_bank_types::{ + address_provider::{self, MarsAddressType}, + error::MarsError, +}; + +use crate::{ + error::ContractError, + helpers::{query_asset_params, query_total_deposit}, + interest_rates::{apply_accumulated_interests, update_interest_rates}, + state::{CONFIG, MARKETS}, + user::User, +}; + +pub fn deposit( + deps: DepsMut, + env: Env, + info: MessageInfo, + on_behalf_of: Option, + denom: String, + deposit_amount: Uint128, + account_id: Option, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + let addresses = address_provider::helpers::query_contract_addrs( + deps.as_ref(), + &config.address_provider, + vec![ + MarsAddressType::Incentives, + MarsAddressType::RewardsCollector, + MarsAddressType::Params, + MarsAddressType::CreditManager, + ], + )?; + let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; + let incentives_addr = &addresses[&MarsAddressType::Incentives]; + let params_addr = &addresses[&MarsAddressType::Params]; + let credit_manager_addr = &addresses[&MarsAddressType::CreditManager]; + + // Don't allow red-bank users to create alternative account ids. + // Only allow credit-manager contract to create them. + // Even if account_id contains empty string we won't allow it. + if account_id.is_some() && info.sender != credit_manager_addr { + return Err(ContractError::Mars(MarsError::Unauthorized {})); + } + + let user_addr: Addr; + let user = match on_behalf_of.as_ref() { + // A malicious user can permanently disable the lend action in credit-manager contract by performing the following steps: + // 1.) Wait for a new asset XXX to be listed and makes sure there is no coin lent out for XXX from the credit-manager to red-bank. + // 2.) Calls deposit on red-bank and sends 1 XXX and deposits on behalf of credit-manager. + // 3.) A user wants to lend out XXX from credit-manager but the call fails because TOTAL_LENT_SHARES is never initialized + // because this query red_bank.query_lent(&deps.querier, &env.contract.address, &coin.denom)? returns one. + Some(address) if address == credit_manager_addr.as_str() => { + return Err(ContractError::Mars(MarsError::Unauthorized {})); + } + Some(address) => { + user_addr = deps.api.addr_validate(address)?; + User(&user_addr) + } + None => User(&info.sender), + }; + + let mut market = MARKETS.load(deps.storage, &denom)?; + + let asset_params = query_asset_params(&deps.querier, params_addr, &denom)?; + + if !asset_params.red_bank.deposit_enabled { + return Err(ContractError::DepositNotEnabled { + denom, + }); + } + + let total_deposits = query_total_deposit(&deps.querier, params_addr, &denom)?; + if total_deposits.amount.checked_add(deposit_amount)? > asset_params.deposit_cap { + return Err(ContractError::DepositCapExceeded { + denom, + }); + } + + let mut response = Response::new(); + + // update indexes and interest rates + response = apply_accumulated_interests( + deps.storage, + &env, + &mut market, + rewards_collector_addr, + incentives_addr, + response, + )?; + + if market.liquidity_index.is_zero() { + return Err(ContractError::InvalidLiquidityIndex {}); + } + let deposit_amount_scaled = + get_scaled_liquidity_amount(deposit_amount, &market, env.block.time.seconds())?; + + response = user.increase_collateral( + deps.storage, + &market, + deposit_amount_scaled, + incentives_addr, + response, + account_id, + )?; + + market.increase_collateral(deposit_amount_scaled)?; + + response = update_interest_rates(&env, &mut market, response)?; + + MARKETS.save(deps.storage, &denom, &market)?; + + Ok(response + .add_attribute("action", "deposit") + .add_attribute("sender", &info.sender) + .add_attribute("on_behalf_of", user) + .add_attribute("denom", denom) + .add_attribute("amount", deposit_amount) + .add_attribute("amount_scaled", deposit_amount_scaled)) +} diff --git a/contracts/red-bank/src/error.rs b/contracts/red-bank/src/error.rs index a65dd2592..54b1972ac 100644 --- a/contracts/red-bank/src/error.rs +++ b/contracts/red-bank/src/error.rs @@ -1,6 +1,9 @@ -use cosmwasm_std::{OverflowError, StdError}; +use cosmwasm_std::{ + CheckedFromRatioError, CheckedMultiplyFractionError, DivideByZeroError, OverflowError, StdError, +}; use cw_utils::PaymentError; use mars_health::error::HealthError; +use mars_liquidation::error::LiquidationError; use mars_owner::OwnerError; use mars_red_bank_types::error::MarsError; use mars_utils::error::ValidationError; @@ -26,9 +29,21 @@ pub enum ContractError { #[error("{0}")] Overflow(#[from] OverflowError), + #[error("{0}")] + CheckedFromRatio(#[from] CheckedFromRatioError), + + #[error("{0}")] + CheckedMultiplyFraction(#[from] CheckedMultiplyFractionError), + + #[error("{0}")] + DivideByZero(#[from] DivideByZeroError), + #[error("{0}")] Health(#[from] HealthError), + #[error("{0}")] + Liquidation(#[from] LiquidationError), + #[error("Price not found for asset: {denom:?}")] PriceNotFound { denom: String, @@ -81,6 +96,9 @@ pub enum ContractError { #[error("Amount to repay is greater than total debt")] CannotRepayMoreThanDebt {}, + #[error("User cannot issue liquidation of own account")] + CannotLiquidateSelf {}, + #[error("User has a positive uncollateralized loan limit and thus cannot be liquidated")] CannotLiquidateWhenPositiveUncollateralizedLoanLimit {}, diff --git a/contracts/red-bank/src/execute.rs b/contracts/red-bank/src/execute.rs deleted file mode 100644 index 9507d5ec5..000000000 --- a/contracts/red-bank/src/execute.rs +++ /dev/null @@ -1,1075 +0,0 @@ -use std::{cmp::min, str}; - -use cosmwasm_std::{ - Addr, Decimal, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Uint128, -}; -use cw2::set_contract_version; -use mars_owner::{OwnerError, OwnerInit::SetInitialOwner, OwnerUpdate}; -use mars_red_bank_types::{ - address_provider::{self, MarsAddressType}, - error::MarsError, - red_bank::{ - Config, CreateOrUpdateConfig, Debt, InitOrUpdateAssetParams, InstantiateMsg, Market, - }, -}; -use mars_utils::{ - helpers::{build_send_asset_msg, option_string_to_addr, validate_native_denom, zero_address}, - math, -}; - -use crate::{ - error::ContractError, - health::{ - assert_below_liq_threshold_after_withdraw, assert_below_max_ltv_after_borrow, - assert_liquidatable, - }, - interest_rates::{ - apply_accumulated_interests, get_scaled_debt_amount, get_scaled_liquidity_amount, - get_underlying_debt_amount, get_underlying_liquidity_amount, update_interest_rates, - }, - state::{COLLATERALS, CONFIG, DEBTS, MARKETS, OWNER, UNCOLLATERALIZED_LOAN_LIMITS}, - user::User, -}; - -pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); -pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); - -pub fn instantiate(deps: DepsMut, msg: InstantiateMsg) -> Result { - set_contract_version(deps.storage, format!("crates.io:{CONTRACT_NAME}"), CONTRACT_VERSION)?; - - // Destructuring a struct’s fields into separate variables in order to force - // compile error if we add more params - let CreateOrUpdateConfig { - address_provider, - close_factor, - } = msg.config; - - // All fields should be available - let available = address_provider.is_some() && close_factor.is_some(); - - if !available { - return Err(MarsError::InstantiateParamsUnavailable {}.into()); - }; - - let config = Config { - address_provider: option_string_to_addr(deps.api, address_provider, zero_address())?, - close_factor: close_factor.unwrap(), - }; - - config.validate()?; - - CONFIG.save(deps.storage, &config)?; - - OWNER.initialize( - deps.storage, - deps.api, - SetInitialOwner { - owner: msg.owner, - }, - )?; - - Ok(Response::default()) -} - -pub fn update_owner( - deps: DepsMut, - info: MessageInfo, - update: OwnerUpdate, -) -> Result { - Ok(OWNER.update(deps, info, update)?) -} - -/// Update config -pub fn update_config( - deps: DepsMut, - info: MessageInfo, - new_config: CreateOrUpdateConfig, -) -> Result { - OWNER.assert_owner(deps.storage, &info.sender)?; - - let mut config = CONFIG.load(deps.storage)?; - - // Destructuring a struct’s fields into separate variables in order to force - // compile error if we add more params - let CreateOrUpdateConfig { - address_provider, - close_factor, - } = new_config; - - // Update config - config.address_provider = - option_string_to_addr(deps.api, address_provider, config.address_provider)?; - config.close_factor = close_factor.unwrap_or(config.close_factor); - - // Validate config - config.validate()?; - - CONFIG.save(deps.storage, &config)?; - - Ok(Response::new().add_attribute("action", "update_config")) -} - -/// Initialize asset if not exist. -/// Initialization requires that all params are provided and there is no asset in state. -pub fn init_asset( - deps: DepsMut, - env: Env, - info: MessageInfo, - denom: String, - params: InitOrUpdateAssetParams, -) -> Result { - OWNER.assert_owner(deps.storage, &info.sender)?; - - validate_native_denom(&denom)?; - - if MARKETS.may_load(deps.storage, &denom)?.is_some() { - return Err(ContractError::AssetAlreadyInitialized {}); - } - - let new_market = create_market(env.block.time.seconds(), &denom, params)?; - MARKETS.save(deps.storage, &denom, &new_market)?; - - Ok(Response::new().add_attribute("action", "init_asset").add_attribute("denom", denom)) -} - -/// Initialize new market -pub fn create_market( - block_time: u64, - denom: &str, - params: InitOrUpdateAssetParams, -) -> Result { - // Destructuring a struct’s fields into separate variables in order to force - // compile error if we add more params - let InitOrUpdateAssetParams { - max_loan_to_value, - reserve_factor, - liquidation_threshold, - liquidation_bonus, - interest_rate_model, - deposit_enabled, - borrow_enabled, - deposit_cap, - } = params; - - // All fields should be available - let available = max_loan_to_value.is_some() - && reserve_factor.is_some() - && liquidation_threshold.is_some() - && liquidation_bonus.is_some() - && interest_rate_model.is_some() - && deposit_enabled.is_some() - && borrow_enabled.is_some(); - - if !available { - return Err(MarsError::InstantiateParamsUnavailable {}.into()); - } - - let new_market = Market { - denom: denom.to_string(), - borrow_index: Decimal::one(), - liquidity_index: Decimal::one(), - borrow_rate: Decimal::zero(), - liquidity_rate: Decimal::zero(), - max_loan_to_value: max_loan_to_value.unwrap(), - reserve_factor: reserve_factor.unwrap(), - indexes_last_updated: block_time, - collateral_total_scaled: Uint128::zero(), - debt_total_scaled: Uint128::zero(), - liquidation_threshold: liquidation_threshold.unwrap(), - liquidation_bonus: liquidation_bonus.unwrap(), - interest_rate_model: interest_rate_model.unwrap(), - deposit_enabled: deposit_enabled.unwrap(), - borrow_enabled: borrow_enabled.unwrap(), - // if not specified, deposit cap is set to unlimited - deposit_cap: deposit_cap.unwrap_or(Uint128::MAX), - }; - - new_market.validate()?; - - Ok(new_market) -} - -/// Update asset with new params. -pub fn update_asset( - deps: DepsMut, - env: Env, - info: MessageInfo, - denom: String, - params: InitOrUpdateAssetParams, -) -> Result { - if OWNER.is_owner(deps.storage, &info.sender)? { - update_asset_by_owner(deps, &env, &denom, params) - } else if OWNER.is_emergency_owner(deps.storage, &info.sender)? { - update_asset_by_emergency_owner(deps, &denom, params) - } else { - Err(OwnerError::NotOwner {}.into()) - } -} - -fn update_asset_by_owner( - deps: DepsMut, - env: &Env, - denom: &str, - params: InitOrUpdateAssetParams, -) -> Result { - let market_option = MARKETS.may_load(deps.storage, denom)?; - match market_option { - None => Err(ContractError::AssetNotInitialized {}), - Some(mut market) => { - // Destructuring a struct’s fields into separate variables in order to force - // compile error if we add more params - let InitOrUpdateAssetParams { - max_loan_to_value, - reserve_factor, - liquidation_threshold, - liquidation_bonus, - interest_rate_model, - deposit_enabled, - borrow_enabled, - deposit_cap, - } = params; - - // If reserve factor or interest rates are updated we update indexes with - // current values before applying the change to prevent applying this - // new params to a period where they were not valid yet. Interests rates are - // recalculated after changes are applied. - let should_update_interest_rates = (reserve_factor.is_some() - && reserve_factor.unwrap() != market.reserve_factor) - || interest_rate_model.is_some(); - - let mut response = Response::new(); - - if should_update_interest_rates { - let config = CONFIG.load(deps.storage)?; - let addresses = address_provider::helpers::query_contract_addrs( - deps.as_ref(), - &config.address_provider, - vec![MarsAddressType::Incentives, MarsAddressType::RewardsCollector], - )?; - let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; - let incentives_addr = &addresses[&MarsAddressType::Incentives]; - - response = apply_accumulated_interests( - deps.storage, - env, - &mut market, - rewards_collector_addr, - incentives_addr, - response, - )?; - } - - let mut updated_market = Market { - max_loan_to_value: max_loan_to_value.unwrap_or(market.max_loan_to_value), - reserve_factor: reserve_factor.unwrap_or(market.reserve_factor), - liquidation_threshold: liquidation_threshold - .unwrap_or(market.liquidation_threshold), - liquidation_bonus: liquidation_bonus.unwrap_or(market.liquidation_bonus), - interest_rate_model: interest_rate_model.unwrap_or(market.interest_rate_model), - deposit_enabled: deposit_enabled.unwrap_or(market.deposit_enabled), - borrow_enabled: borrow_enabled.unwrap_or(market.borrow_enabled), - deposit_cap: deposit_cap.unwrap_or(market.deposit_cap), - ..market - }; - - updated_market.validate()?; - - if should_update_interest_rates { - response = update_interest_rates(env, &mut updated_market, response)?; - } - MARKETS.save(deps.storage, denom, &updated_market)?; - - Ok(response.add_attribute("action", "update_asset").add_attribute("denom", denom)) - } - } -} - -/// Emergency owner can only DISABLE BORROWING. -fn update_asset_by_emergency_owner( - deps: DepsMut, - denom: &str, - params: InitOrUpdateAssetParams, -) -> Result { - if let Some(mut market) = MARKETS.may_load(deps.storage, denom)? { - match params.borrow_enabled { - Some(borrow_enabled) if !borrow_enabled => { - market.borrow_enabled = borrow_enabled; - MARKETS.save(deps.storage, denom, &market)?; - - Ok(Response::new() - .add_attribute("action", "emergency_update_asset") - .add_attribute("denom", denom)) - } - _ => Err(MarsError::Unauthorized {}.into()), - } - } else { - Err(ContractError::AssetNotInitialized {}) - } -} - -/// Update uncollateralized loan limit by a given amount in base asset -pub fn update_uncollateralized_loan_limit( - deps: DepsMut, - info: MessageInfo, - user_addr: Addr, - denom: String, - new_limit: Uint128, -) -> Result { - OWNER.assert_owner(deps.storage, &info.sender)?; - - // Check that the user has no collateralized debt - let current_limit = UNCOLLATERALIZED_LOAN_LIMITS - .may_load(deps.storage, (&user_addr, &denom))? - .unwrap_or_else(Uint128::zero); - let current_debt = DEBTS - .may_load(deps.storage, (&user_addr, &denom))? - .map(|debt| debt.amount_scaled) - .unwrap_or_else(Uint128::zero); - if current_limit.is_zero() && !current_debt.is_zero() { - return Err(ContractError::UserHasCollateralizedDebt {}); - } - if !current_limit.is_zero() && new_limit.is_zero() && !current_debt.is_zero() { - return Err(ContractError::UserHasUncollateralizedDebt {}); - } - - UNCOLLATERALIZED_LOAN_LIMITS.save(deps.storage, (&user_addr, &denom), &new_limit)?; - - DEBTS.update(deps.storage, (&user_addr, &denom), |debt_opt: Option| -> StdResult<_> { - let mut debt = debt_opt.unwrap_or(Debt { - amount_scaled: Uint128::zero(), - uncollateralized: false, - }); - // if limit == 0 then uncollateralized = false, otherwise uncollateralized = true - debt.uncollateralized = !new_limit.is_zero(); - Ok(debt) - })?; - - Ok(Response::new() - .add_attribute("action", "update_uncollateralized_loan_limit") - .add_attribute("user", user_addr) - .add_attribute("denom", denom) - .add_attribute("new_allowance", new_limit)) -} - -/// Execute deposits -pub fn deposit( - deps: DepsMut, - env: Env, - info: MessageInfo, - on_behalf_of: Option, - denom: String, - deposit_amount: Uint128, -) -> Result { - let user_addr: Addr; - let user = if let Some(address) = on_behalf_of { - user_addr = deps.api.addr_validate(&address)?; - User(&user_addr) - } else { - User(&info.sender) - }; - - let mut market = MARKETS.load(deps.storage, &denom)?; - if !market.deposit_enabled { - return Err(ContractError::DepositNotEnabled { - denom, - }); - } - - let total_scaled_deposits = market.collateral_total_scaled; - let total_deposits = - get_underlying_liquidity_amount(total_scaled_deposits, &market, env.block.time.seconds())?; - if total_deposits.checked_add(deposit_amount)? > market.deposit_cap { - return Err(ContractError::DepositCapExceeded { - denom, - }); - } - - let mut response = Response::new(); - - let config = CONFIG.load(deps.storage)?; - - // update indexes and interest rates - let addresses = address_provider::helpers::query_contract_addrs( - deps.as_ref(), - &config.address_provider, - vec![MarsAddressType::Incentives, MarsAddressType::RewardsCollector], - )?; - let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; - let incentives_addr = &addresses[&MarsAddressType::Incentives]; - - response = apply_accumulated_interests( - deps.storage, - &env, - &mut market, - rewards_collector_addr, - incentives_addr, - response, - )?; - - if market.liquidity_index.is_zero() { - return Err(ContractError::InvalidLiquidityIndex {}); - } - let deposit_amount_scaled = - get_scaled_liquidity_amount(deposit_amount, &market, env.block.time.seconds())?; - - response = user.increase_collateral( - deps.storage, - &market, - deposit_amount_scaled, - incentives_addr, - response, - )?; - - market.increase_collateral(deposit_amount_scaled)?; - - response = update_interest_rates(&env, &mut market, response)?; - - MARKETS.save(deps.storage, &denom, &market)?; - - Ok(response - .add_attribute("action", "deposit") - .add_attribute("sender", &info.sender) - .add_attribute("on_behalf_of", user) - .add_attribute("denom", denom) - .add_attribute("amount", deposit_amount) - .add_attribute("amount_scaled", deposit_amount_scaled)) -} - -/// Burns sent maAsset in exchange of underlying asset -pub fn withdraw( - deps: DepsMut, - env: Env, - info: MessageInfo, - denom: String, - amount: Option, - recipient: Option, -) -> Result { - let withdrawer = User(&info.sender); - - let mut market = MARKETS.load(deps.storage, &denom)?; - - let collateral = withdrawer.collateral(deps.storage, &denom)?; - let withdrawer_balance_scaled_before = collateral.amount_scaled; - - if withdrawer_balance_scaled_before.is_zero() { - return Err(ContractError::UserNoCollateralBalance { - user: withdrawer.into(), - denom, - }); - } - - let withdrawer_balance_before = get_underlying_liquidity_amount( - withdrawer_balance_scaled_before, - &market, - env.block.time.seconds(), - )?; - - let withdraw_amount = match amount { - // Check user has sufficient balance to send back - Some(amount) if amount.is_zero() || amount > withdrawer_balance_before => { - return Err(ContractError::InvalidWithdrawAmount { - denom, - }); - } - Some(amount) => amount, - // If no amount is specified, the full balance is withdrawn - None => withdrawer_balance_before, - }; - - let config = CONFIG.load(deps.storage)?; - - let addresses = address_provider::helpers::query_contract_addrs( - deps.as_ref(), - &config.address_provider, - vec![ - MarsAddressType::Oracle, - MarsAddressType::Incentives, - MarsAddressType::RewardsCollector, - ], - )?; - let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; - let incentives_addr = &addresses[&MarsAddressType::Incentives]; - let oracle_addr = &addresses[&MarsAddressType::Oracle]; - - // if asset is used as collateral and user is borrowing we need to validate health factor after withdraw, - // otherwise no reasons to block the withdraw - if collateral.enabled - && withdrawer.is_borrowing(deps.storage) - && !assert_below_liq_threshold_after_withdraw( - &deps.as_ref(), - &env, - withdrawer.address(), - oracle_addr, - &denom, - withdraw_amount, - )? - { - return Err(ContractError::InvalidHealthFactorAfterWithdraw {}); - } - - let mut response = Response::new(); - - // update indexes and interest rates - response = apply_accumulated_interests( - deps.storage, - &env, - &mut market, - rewards_collector_addr, - incentives_addr, - response, - )?; - - // reduce the withdrawer's scaled collateral amount - let withdrawer_balance_after = withdrawer_balance_before.checked_sub(withdraw_amount)?; - let withdrawer_balance_scaled_after = - get_scaled_liquidity_amount(withdrawer_balance_after, &market, env.block.time.seconds())?; - - let withdraw_amount_scaled = - withdrawer_balance_scaled_before.checked_sub(withdrawer_balance_scaled_after)?; - - response = withdrawer.decrease_collateral( - deps.storage, - &market, - withdraw_amount_scaled, - incentives_addr, - response, - )?; - - market.decrease_collateral(withdraw_amount_scaled)?; - - response = update_interest_rates(&env, &mut market, response)?; - - MARKETS.save(deps.storage, &denom, &market)?; - - // send underlying asset to user or another recipient - let recipient_addr = if let Some(recipient) = recipient { - deps.api.addr_validate(&recipient)? - } else { - withdrawer.address().clone() - }; - - Ok(response - .add_message(build_send_asset_msg(&recipient_addr, &denom, withdraw_amount)) - .add_attribute("action", "withdraw") - .add_attribute("sender", withdrawer) - .add_attribute("recipient", recipient_addr) - .add_attribute("denom", denom) - .add_attribute("amount", withdraw_amount) - .add_attribute("amount_scaled", withdraw_amount_scaled)) -} - -/// Add debt for the borrower and send the borrowed funds -pub fn borrow( - deps: DepsMut, - env: Env, - info: MessageInfo, - denom: String, - borrow_amount: Uint128, - recipient: Option, -) -> Result { - let borrower = User(&info.sender); - - // Load market and user state - let mut borrow_market = MARKETS.load(deps.storage, &denom)?; - - if !borrow_market.borrow_enabled { - return Err(ContractError::BorrowNotEnabled { - denom, - }); - } - - let collateral_balance_before = get_underlying_liquidity_amount( - borrow_market.collateral_total_scaled, - &borrow_market, - env.block.time.seconds(), - )?; - - // Cannot borrow zero amount or more than available collateral - if borrow_amount.is_zero() || borrow_amount > collateral_balance_before { - return Err(ContractError::InvalidBorrowAmount { - denom, - }); - } - - let uncollateralized_loan_limit = borrower.uncollateralized_loan_limit(deps.storage, &denom)?; - - let config = CONFIG.load(deps.storage)?; - - let addresses = address_provider::helpers::query_contract_addrs( - deps.as_ref(), - &config.address_provider, - vec![ - MarsAddressType::Oracle, - MarsAddressType::Incentives, - MarsAddressType::RewardsCollector, - ], - )?; - let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; - let incentives_addr = &addresses[&MarsAddressType::Incentives]; - let oracle_addr = &addresses[&MarsAddressType::Oracle]; - - // Check if user can borrow specified amount - let mut uncollateralized_debt = false; - if uncollateralized_loan_limit.is_zero() { - if !assert_below_max_ltv_after_borrow( - &deps.as_ref(), - &env, - borrower.address(), - oracle_addr, - &denom, - borrow_amount, - )? { - return Err(ContractError::BorrowAmountExceedsGivenCollateral {}); - } - } else { - // Uncollateralized loan: check borrow amount plus debt does not exceed uncollateralized loan limit - uncollateralized_debt = true; - - let debt_amount_scaled = borrower.debt_amount_scaled(deps.storage, &denom)?; - - let asset_market = MARKETS.load(deps.storage, &denom)?; - let debt_amount = get_underlying_debt_amount( - debt_amount_scaled, - &asset_market, - env.block.time.seconds(), - )?; - - let debt_after_borrow = debt_amount.checked_add(borrow_amount)?; - if debt_after_borrow > uncollateralized_loan_limit { - return Err(ContractError::BorrowAmountExceedsUncollateralizedLoanLimit {}); - } - } - - let mut response = Response::new(); - - response = apply_accumulated_interests( - deps.storage, - &env, - &mut borrow_market, - rewards_collector_addr, - incentives_addr, - response, - )?; - - // Set new debt - let borrow_amount_scaled = - get_scaled_debt_amount(borrow_amount, &borrow_market, env.block.time.seconds())?; - - borrow_market.increase_debt(borrow_amount_scaled)?; - borrower.increase_debt(deps.storage, &denom, borrow_amount_scaled, uncollateralized_debt)?; - - response = update_interest_rates(&env, &mut borrow_market, response)?; - MARKETS.save(deps.storage, &denom, &borrow_market)?; - - // Send borrow amount to borrower or another recipient - let recipient_addr = if let Some(recipient) = recipient { - deps.api.addr_validate(&recipient)? - } else { - borrower.address().clone() - }; - - Ok(response - .add_message(build_send_asset_msg(&recipient_addr, &denom, borrow_amount)) - .add_attribute("action", "borrow") - .add_attribute("sender", borrower) - .add_attribute("recipient", recipient_addr) - .add_attribute("denom", denom) - .add_attribute("amount", borrow_amount) - .add_attribute("amount_scaled", borrow_amount_scaled)) -} - -/// Handle the repay of native tokens. Refund extra funds if they exist -pub fn repay( - deps: DepsMut, - env: Env, - info: MessageInfo, - on_behalf_of: Option, - denom: String, - repay_amount: Uint128, -) -> Result { - let user_addr: Addr; - let user = if let Some(address) = on_behalf_of { - user_addr = deps.api.addr_validate(&address)?; - let user = User(&user_addr); - // Uncollateralized loans should not have 'on behalf of' because it creates accounting complexity for them - if !user.uncollateralized_loan_limit(deps.storage, &denom)?.is_zero() { - return Err(ContractError::CannotRepayUncollateralizedLoanOnBehalfOf {}); - } - user - } else { - User(&info.sender) - }; - - // Check new debt - let debt = DEBTS - .may_load(deps.storage, (user.address(), &denom))? - .ok_or(ContractError::CannotRepayZeroDebt {})?; - - let config = CONFIG.load(deps.storage)?; - - let addresses = address_provider::helpers::query_contract_addrs( - deps.as_ref(), - &config.address_provider, - vec![MarsAddressType::Incentives, MarsAddressType::RewardsCollector], - )?; - let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; - let incentives_addr = &addresses[&MarsAddressType::Incentives]; - - let mut market = MARKETS.load(deps.storage, &denom)?; - - let mut response = Response::new(); - - response = apply_accumulated_interests( - deps.storage, - &env, - &mut market, - rewards_collector_addr, - incentives_addr, - response, - )?; - - let debt_amount_scaled_before = debt.amount_scaled; - let debt_amount_before = - get_underlying_debt_amount(debt.amount_scaled, &market, env.block.time.seconds())?; - - // If repay amount exceeds debt, refund any excess amounts - let mut refund_amount = Uint128::zero(); - let mut debt_amount_after = Uint128::zero(); - if repay_amount > debt_amount_before { - refund_amount = repay_amount - debt_amount_before; - let refund_msg = build_send_asset_msg(&info.sender, &denom, refund_amount); - response = response.add_message(refund_msg); - } else { - debt_amount_after = debt_amount_before - repay_amount; - } - - let debt_amount_scaled_after = - get_scaled_debt_amount(debt_amount_after, &market, env.block.time.seconds())?; - - let debt_amount_scaled_delta = - debt_amount_scaled_before.checked_sub(debt_amount_scaled_after)?; - - market.decrease_debt(debt_amount_scaled_delta)?; - user.decrease_debt(deps.storage, &denom, debt_amount_scaled_delta)?; - - response = update_interest_rates(&env, &mut market, response)?; - MARKETS.save(deps.storage, &denom, &market)?; - - Ok(response - .add_attribute("action", "repay") - .add_attribute("sender", &info.sender) - .add_attribute("on_behalf_of", user) - .add_attribute("denom", denom) - .add_attribute("amount", repay_amount.checked_sub(refund_amount)?) - .add_attribute("amount_scaled", debt_amount_scaled_delta)) -} - -/// Execute loan liquidations on under-collateralized loans -pub fn liquidate( - deps: DepsMut, - env: Env, - info: MessageInfo, - collateral_denom: String, - debt_denom: String, - user_addr: Addr, - sent_debt_amount: Uint128, - recipient: Option, -) -> Result { - let block_time = env.block.time.seconds(); - let user = User(&user_addr); - // The recipient address for receiving underlying collateral - let recipient_addr = option_string_to_addr(deps.api, recipient, info.sender.clone())?; - let recipient = User(&recipient_addr); - - // 1. Validate liquidation - // If user (contract) has a positive uncollateralized limit then the user - // cannot be liquidated - if !user.uncollateralized_loan_limit(deps.storage, &debt_denom)?.is_zero() { - return Err(ContractError::CannotLiquidateWhenPositiveUncollateralizedLoanLimit {}); - }; - - // check if the user has enabled the collateral asset as collateral - let user_collateral = COLLATERALS - .may_load(deps.storage, (&user_addr, &collateral_denom))? - .ok_or(ContractError::CannotLiquidateWhenNoCollateralBalance {})?; - if !user_collateral.enabled { - return Err(ContractError::CannotLiquidateWhenCollateralUnset { - denom: collateral_denom, - }); - } - - // check if user has available collateral in specified collateral asset to be liquidated - let collateral_market = MARKETS.load(deps.storage, &collateral_denom)?; - - // check if user has outstanding debt in the deposited asset that needs to be repayed - let user_debt = DEBTS - .may_load(deps.storage, (&user_addr, &debt_denom))? - .ok_or(ContractError::CannotLiquidateWhenNoDebtBalance {})?; - - // 2. Compute health factor - let config = CONFIG.load(deps.storage)?; - - let addresses = address_provider::helpers::query_contract_addrs( - deps.as_ref(), - &config.address_provider, - vec![ - MarsAddressType::Oracle, - MarsAddressType::Incentives, - MarsAddressType::RewardsCollector, - ], - )?; - let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; - let incentives_addr = &addresses[&MarsAddressType::Incentives]; - let oracle_addr = &addresses[&MarsAddressType::Oracle]; - - let (liquidatable, assets_positions) = - assert_liquidatable(&deps.as_ref(), &env, &user_addr, oracle_addr)?; - - if !liquidatable { - return Err(ContractError::CannotLiquidateHealthyPosition {}); - } - - let collateral_and_debt_are_the_same_asset = debt_denom == collateral_denom; - - let debt_market = if !collateral_and_debt_are_the_same_asset { - MARKETS.load(deps.storage, &debt_denom)? - } else { - collateral_market.clone() - }; - - // 3. Compute debt to repay and collateral to liquidate - let collateral_price = assets_positions - .get(&collateral_denom) - .ok_or(ContractError::CannotLiquidateWhenNoCollateralBalance {})? - .asset_price; - let debt_price = assets_positions - .get(&debt_denom) - .ok_or(ContractError::CannotLiquidateWhenNoDebtBalance {})? - .asset_price; - - let mut response = Response::new(); - - let user_debt_amount = - get_underlying_debt_amount(user_debt.amount_scaled, &debt_market, block_time)?; - - let ( - debt_amount_to_repay, - collateral_amount_to_liquidate, - collateral_amount_to_liquidate_scaled, - refund_amount, - ) = liquidation_compute_amounts( - user_collateral.amount_scaled, - user_debt_amount, - sent_debt_amount, - &collateral_market, - collateral_price, - debt_price, - block_time, - config.close_factor, - )?; - - // 4. Transfer collateral shares from the user to the liquidator - response = user.decrease_collateral( - deps.storage, - &collateral_market, - collateral_amount_to_liquidate_scaled, - incentives_addr, - response, - )?; - response = recipient.increase_collateral( - deps.storage, - &collateral_market, - collateral_amount_to_liquidate_scaled, - incentives_addr, - response, - )?; - - // 5. Reduce the user's debt shares - let user_debt_amount_after = user_debt_amount.checked_sub(debt_amount_to_repay)?; - let user_debt_amount_scaled_after = - get_scaled_debt_amount(user_debt_amount_after, &debt_market, block_time)?; - - // Compute delta so it can be substracted to total debt - let debt_amount_scaled_delta = - user_debt.amount_scaled.checked_sub(user_debt_amount_scaled_after)?; - - user.decrease_debt(deps.storage, &debt_denom, debt_amount_scaled_delta)?; - - let debt_market_debt_total_scaled_after = - debt_market.debt_total_scaled.checked_sub(debt_amount_scaled_delta)?; - - // 6. Update markets depending on whether the collateral and debt markets are the same - // and whether the liquidator receives coins (no change in liquidity) or underlying asset - // (changes liquidity) - if collateral_and_debt_are_the_same_asset { - // NOTE: for the sake of clarity copy attributes from collateral market and - // give generic naming. Debt market could have been used as well - let mut asset_market_after = collateral_market; - let denom = &collateral_denom; - - response = apply_accumulated_interests( - deps.storage, - &env, - &mut asset_market_after, - rewards_collector_addr, - incentives_addr, - response, - )?; - - asset_market_after.debt_total_scaled = debt_market_debt_total_scaled_after; - - response = update_interest_rates(&env, &mut asset_market_after, response)?; - - MARKETS.save(deps.storage, denom, &asset_market_after)?; - } else { - let mut debt_market_after = debt_market; - - response = apply_accumulated_interests( - deps.storage, - &env, - &mut debt_market_after, - rewards_collector_addr, - incentives_addr, - response, - )?; - - debt_market_after.debt_total_scaled = debt_market_debt_total_scaled_after; - - response = update_interest_rates(&env, &mut debt_market_after, response)?; - - MARKETS.save(deps.storage, &debt_denom, &debt_market_after)?; - } - - // 7. Build response - // refund sent amount in excess of actual debt amount to liquidate - if !refund_amount.is_zero() { - response = - response.add_message(build_send_asset_msg(&info.sender, &debt_denom, refund_amount)); - } - - Ok(response - .add_attribute("action", "liquidate") - .add_attribute("user", user) - .add_attribute("liquidator", info.sender.to_string()) - .add_attribute("recipient", recipient) - .add_attribute("collateral_denom", collateral_denom) - .add_attribute("collateral_amount", collateral_amount_to_liquidate) - .add_attribute("collateral_amount_scaled", collateral_amount_to_liquidate_scaled) - .add_attribute("debt_denom", debt_denom) - .add_attribute("debt_amount", debt_amount_to_repay) - .add_attribute("debt_amount_scaled", debt_amount_scaled_delta)) -} - -/// Computes debt to repay (in debt asset), -/// collateral to liquidate (in collateral asset) and -/// amount to refund the liquidator (in debt asset) -pub fn liquidation_compute_amounts( - user_collateral_amount_scaled: Uint128, - user_debt_amount: Uint128, - sent_debt_amount: Uint128, - collateral_market: &Market, - collateral_price: Decimal, - debt_price: Decimal, - block_time: u64, - close_factor: Decimal, -) -> StdResult<(Uint128, Uint128, Uint128, Uint128)> { - // Debt: Only up to a fraction of the total debt (determined by the close factor) can be - // repayed. - let mut debt_amount_to_repay = min(sent_debt_amount, close_factor * user_debt_amount); - - // Collateral: debt to repay in base asset times the liquidation bonus - let mut collateral_amount_to_liquidate = math::divide_uint128_by_decimal( - debt_amount_to_repay * debt_price * (Decimal::one() + collateral_market.liquidation_bonus), - collateral_price, - )?; - let mut collateral_amount_to_liquidate_scaled = - get_scaled_liquidity_amount(collateral_amount_to_liquidate, collateral_market, block_time)?; - - // If collateral amount to liquidate is higher than user_collateral_balance, - // liquidate the full balance and adjust the debt amount to repay accordingly - if collateral_amount_to_liquidate_scaled > user_collateral_amount_scaled { - collateral_amount_to_liquidate_scaled = user_collateral_amount_scaled; - collateral_amount_to_liquidate = get_underlying_liquidity_amount( - collateral_amount_to_liquidate_scaled, - collateral_market, - block_time, - )?; - debt_amount_to_repay = math::divide_uint128_by_decimal( - math::divide_uint128_by_decimal( - collateral_amount_to_liquidate * collateral_price, - debt_price, - )?, - Decimal::one() + collateral_market.liquidation_bonus, - )?; - } - - // In some edges scenarios: - // - if debt_amount_to_repay = 0, some liquidators could drain collaterals and all their coins - // would be refunded, i.e.: without spending coins. - // - if collateral_amount_to_liquidate is 0, some users could liquidate without receiving collaterals - // in return. - if (!collateral_amount_to_liquidate.is_zero() && debt_amount_to_repay.is_zero()) - || (collateral_amount_to_liquidate.is_zero() && !debt_amount_to_repay.is_zero()) - { - return Err(StdError::generic_err( - format!("Can't process liquidation. Invalid collateral_amount_to_liquidate ({collateral_amount_to_liquidate}) and debt_amount_to_repay ({debt_amount_to_repay})") - )); - } - - let refund_amount = sent_debt_amount - debt_amount_to_repay; - - Ok(( - debt_amount_to_repay, - collateral_amount_to_liquidate, - collateral_amount_to_liquidate_scaled, - refund_amount, - )) -} - -/// Update (enable / disable) collateral asset for specific user -pub fn update_asset_collateral_status( - deps: DepsMut, - env: Env, - info: MessageInfo, - denom: String, - enable: bool, -) -> Result { - let user = User(&info.sender); - - let mut collateral = - COLLATERALS.may_load(deps.storage, (user.address(), &denom))?.ok_or_else(|| { - ContractError::UserNoCollateralBalance { - user: user.into(), - denom: denom.clone(), - } - })?; - - let previously_enabled = collateral.enabled; - - collateral.enabled = enable; - COLLATERALS.save(deps.storage, (user.address(), &denom), &collateral)?; - - // if the collateral was previously enabled, but is not disabled, it is necessary to ensure the - // user is not liquidatable after disabling - if previously_enabled && !enable { - let config = CONFIG.load(deps.storage)?; - let oracle_addr = address_provider::helpers::query_contract_addr( - deps.as_ref(), - &config.address_provider, - MarsAddressType::Oracle, - )?; - - let (liquidatable, _) = - assert_liquidatable(&deps.as_ref(), &env, user.address(), &oracle_addr)?; - - if liquidatable { - return Err(ContractError::InvalidHealthFactorAfterDisablingCollateral {}); - } - } - - Ok(Response::new() - .add_attribute("action", "update_asset_collateral_status") - .add_attribute("user", user) - .add_attribute("denom", denom) - .add_attribute("enable", enable.to_string())) -} diff --git a/contracts/red-bank/src/health.rs b/contracts/red-bank/src/health.rs index 5be29e812..280fa1c7b 100644 --- a/contracts/red-bank/src/health.rs +++ b/contracts/red-bank/src/health.rs @@ -2,25 +2,37 @@ use std::collections::{HashMap, HashSet}; use cosmwasm_std::{Addr, Deps, Env, Order, StdError, StdResult, Uint128}; use mars_health::health::{Health, Position as HealthPosition}; +use mars_interest_rate::{get_underlying_debt_amount, get_underlying_liquidity_amount}; use mars_red_bank_types::{oracle, red_bank::Position}; use crate::{ error::ContractError, - interest_rates::{get_underlying_debt_amount, get_underlying_liquidity_amount}, + helpers::query_asset_params, state::{COLLATERALS, DEBTS, MARKETS}, }; -/// Check the Health Factor for a given user -pub fn assert_liquidatable( +/// Get health and positions for a given user +pub fn get_health_and_positions( deps: &Deps, env: &Env, user_addr: &Addr, + account_id: &str, oracle_addr: &Addr, -) -> Result<(bool, HashMap), ContractError> { - let positions = get_user_positions_map(deps, env, user_addr, oracle_addr)?; + params_addr: &Addr, + is_liquidation: bool, +) -> Result<(Health, HashMap), ContractError> { + let positions = get_user_positions_map( + deps, + env, + user_addr, + account_id, + oracle_addr, + params_addr, + is_liquidation, + )?; let health = compute_position_health(&positions)?; - Ok((health.is_liquidatable(), positions)) + Ok((health, positions)) } /// Check the Health Factor for a given user after a withdraw @@ -28,12 +40,22 @@ pub fn assert_below_liq_threshold_after_withdraw( deps: &Deps, env: &Env, user_addr: &Addr, + account_id: &str, oracle_addr: &Addr, + params_addr: &Addr, denom: &str, withdraw_amount: Uint128, + is_liquidation: bool, ) -> Result { - let mut positions = get_user_positions_map(deps, env, user_addr, oracle_addr)?; - + let mut positions = get_user_positions_map( + deps, + env, + user_addr, + account_id, + oracle_addr, + params_addr, + is_liquidation, + )?; // Update position to compute health factor after withdraw match positions.get_mut(denom) { Some(p) => { @@ -51,11 +73,14 @@ pub fn assert_below_max_ltv_after_borrow( deps: &Deps, env: &Env, user_addr: &Addr, + account_id: &str, oracle_addr: &Addr, + params_addr: &Addr, denom: &str, borrow_amount: Uint128, ) -> Result { - let mut positions = get_user_positions_map(deps, env, user_addr, oracle_addr)?; + let mut positions = + get_user_positions_map(deps, env, user_addr, account_id, oracle_addr, params_addr, false)?; // Update position to compute health factor after borrow positions @@ -106,13 +131,16 @@ pub fn get_user_positions_map( deps: &Deps, env: &Env, user_addr: &Addr, + account_id: &str, oracle_addr: &Addr, -) -> StdResult> { + params_addr: &Addr, + is_liquidation: bool, +) -> Result, ContractError> { let block_time = env.block.time.seconds(); // Find all denoms that the user has a collateral or debt position in let collateral_denoms = COLLATERALS - .prefix(user_addr) + .prefix((user_addr, account_id)) .keys(deps.storage, None, None, Order::Ascending) .collect::>>()?; let debt_denoms = DEBTS @@ -131,14 +159,16 @@ pub fn get_user_positions_map( .into_iter() .map(|denom| { let market = MARKETS.load(deps.storage, &denom)?; + let params = query_asset_params(&deps.querier, params_addr, &denom)?; - let collateral_amount = match COLLATERALS.may_load(deps.storage, (user_addr, &denom))? { - Some(collateral) if collateral.enabled => { - let amount_scaled = collateral.amount_scaled; - get_underlying_liquidity_amount(amount_scaled, &market, block_time)? - } - _ => Uint128::zero(), - }; + let collateral_amount = + match COLLATERALS.may_load(deps.storage, (user_addr, account_id, &denom))? { + Some(collateral) if collateral.enabled => { + let amount_scaled = collateral.amount_scaled; + get_underlying_liquidity_amount(amount_scaled, &market, block_time)? + } + _ => Uint128::zero(), + }; let (debt_amount, uncollateralized_debt) = match DEBTS.may_load(deps.storage, (user_addr, &denom))? { @@ -150,15 +180,19 @@ pub fn get_user_positions_map( None => (Uint128::zero(), false), }; - let asset_price = oracle::helpers::query_price(&deps.querier, oracle_addr, &denom)?; + let asset_price = if is_liquidation { + oracle::helpers::query_price_for_liquidate(&deps.querier, oracle_addr, &denom)? + } else { + oracle::helpers::query_price(&deps.querier, oracle_addr, &denom)? + }; let position = Position { denom: denom.clone(), collateral_amount, debt_amount, uncollateralized_debt, - max_ltv: market.max_loan_to_value, - liquidation_threshold: market.liquidation_threshold, + max_ltv: params.max_loan_to_value, + liquidation_threshold: params.liquidation_threshold, asset_price, }; diff --git a/contracts/red-bank/src/helpers.rs b/contracts/red-bank/src/helpers.rs new file mode 100644 index 000000000..79fa686ff --- /dev/null +++ b/contracts/red-bank/src/helpers.rs @@ -0,0 +1,35 @@ +use cosmwasm_std::{Coin, Decimal, QuerierWrapper, StdResult}; +use mars_params::{msg::QueryMsg, types::asset::AssetParams}; + +pub fn query_asset_params( + querier: &QuerierWrapper, + params: impl Into, + denom: impl Into, +) -> StdResult { + querier.query_wasm_smart( + params.into(), + &QueryMsg::AssetParams { + denom: denom.into(), + }, + ) +} + +pub fn query_target_health_factor( + querier: &QuerierWrapper, + params: impl Into, +) -> StdResult { + querier.query_wasm_smart(params.into(), &QueryMsg::TargetHealthFactor {}) +} + +pub fn query_total_deposit( + querier: &QuerierWrapper, + params: impl Into, + denom: impl Into, +) -> StdResult { + querier.query_wasm_smart( + params.into(), + &QueryMsg::TotalDeposit { + denom: denom.into(), + }, + ) +} diff --git a/contracts/red-bank/src/instantiate.rs b/contracts/red-bank/src/instantiate.rs new file mode 100644 index 000000000..5b084a649 --- /dev/null +++ b/contracts/red-bank/src/instantiate.rs @@ -0,0 +1,46 @@ +use cosmwasm_std::{DepsMut, Response}; +use cw2::set_contract_version; +use mars_owner::OwnerInit::SetInitialOwner; +use mars_red_bank_types::{ + error::MarsError, + red_bank::{Config, CreateOrUpdateConfig, InstantiateMsg}, +}; +use mars_utils::helpers::{option_string_to_addr, zero_address}; + +use crate::{ + error::ContractError, + state::{CONFIG, OWNER}, +}; + +pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub fn instantiate(deps: DepsMut, msg: InstantiateMsg) -> Result { + set_contract_version(deps.storage, format!("crates.io:{CONTRACT_NAME}"), CONTRACT_VERSION)?; + + // Destructuring a struct’s fields into separate variables in order to force + // compile error if we add more params + let CreateOrUpdateConfig { + address_provider, + } = msg.config; + + if address_provider.is_none() { + return Err(MarsError::InstantiateParamsUnavailable {}.into()); + }; + + let config = Config { + address_provider: option_string_to_addr(deps.api, address_provider, zero_address())?, + }; + + CONFIG.save(deps.storage, &config)?; + + OWNER.initialize( + deps.storage, + deps.api, + SetInitialOwner { + owner: msg.owner, + }, + )?; + + Ok(Response::default()) +} diff --git a/contracts/red-bank/src/interest_rates.rs b/contracts/red-bank/src/interest_rates.rs index ce01a6a4f..d0efc4c80 100644 --- a/contracts/red-bank/src/interest_rates.rs +++ b/contracts/red-bank/src/interest_rates.rs @@ -1,16 +1,14 @@ use std::str; -use cosmwasm_std::{Addr, Decimal, Env, Event, Response, StdError, StdResult, Storage, Uint128}; +use cosmwasm_std::{Addr, Decimal, Env, Event, Response, Storage, Uint128}; +use mars_interest_rate::{ + calculate_applied_linear_interest_rate, compute_scaled_amount, compute_underlying_amount, + get_underlying_debt_amount, get_underlying_liquidity_amount, ScalingOperation, +}; use mars_red_bank_types::red_bank::Market; -use mars_utils::math; use crate::{error::ContractError, user::User}; -/// Scaling factor used to keep more precision during division / multiplication by index. -pub const SCALING_FACTOR: Uint128 = Uint128::new(1_000_000); - -const SECONDS_PER_YEAR: u64 = 31536000u64; - /// Calculates accumulated interest for the time between last time market index was updated /// and current block. /// Applies desired side effects: @@ -28,7 +26,7 @@ pub fn apply_accumulated_interests( rewards_collector_addr: &Addr, incentives_addr: &Addr, mut response: Response, -) -> StdResult { +) -> Result { let current_timestamp = env.block.time.seconds(); let previous_borrow_index = market.borrow_index; @@ -88,6 +86,7 @@ pub fn apply_accumulated_interests( reward_amount_scaled, incentives_addr, response, + None, )?; market.increase_collateral(reward_amount_scaled)?; } @@ -95,181 +94,6 @@ pub fn apply_accumulated_interests( Ok(response) } -pub fn calculate_applied_linear_interest_rate( - index: Decimal, - rate: Decimal, - time_elapsed: u64, -) -> StdResult { - let rate_factor = rate.checked_mul(Decimal::from_ratio( - Uint128::from(time_elapsed), - Uint128::from(SECONDS_PER_YEAR), - ))?; - index.checked_mul(Decimal::one() + rate_factor).map_err(StdError::from) -} - -/// Get scaled liquidity amount from an underlying amount, a Market and timestamp in seconds -/// Liquidity amounts are always truncated to make sure rounding errors accumulate in favor of -/// the protocol -/// NOTE: Calling this function when interests for the market are up to date with the current block -/// and index is not, will use the wrong interest rate to update the index. -/// NOTE: This function should not be used when calculating how much scaled amount is getting -/// burned from given underlying withdraw amount. In that case, all math should be done in underlying -/// amounts then get scaled back again -pub fn get_scaled_liquidity_amount( - amount: Uint128, - market: &Market, - timestamp: u64, -) -> StdResult { - compute_scaled_amount( - amount, - get_updated_liquidity_index(market, timestamp)?, - ScalingOperation::Truncate, - ) -} - -/// Get underlying liquidity amount from a scaled amount, a Market and timestamp in seconds -/// Liquidity amounts are always truncated to make sure rounding errors accumulate in favor of -/// the protocol -/// NOTE: Calling this function when interests for the market are up to date with the current block -/// and index is not, will use the wrong interest rate to update the index. -pub fn get_underlying_liquidity_amount( - amount_scaled: Uint128, - market: &Market, - timestamp: u64, -) -> StdResult { - compute_underlying_amount( - amount_scaled, - get_updated_liquidity_index(market, timestamp)?, - ScalingOperation::Truncate, - ) -} - -/// Get scaled borrow amount from an underlying amount, a Market and timestamp in seconds -/// Debt amounts are always ceiled to make sure rounding errors accumulate in favor of -/// the protocol -/// NOTE: Calling this function when interests for the market are up to date with the current block -/// and index is not, will use the wrong interest rate to update the index. -/// NOTE: This function should not be used when calculating how much scaled amount is getting -/// repaid from a sent underlying amount. In that case, all math should be done in underlying -/// amounts then get scaled back again -pub fn get_scaled_debt_amount( - amount: Uint128, - market: &Market, - timestamp: u64, -) -> StdResult { - compute_scaled_amount( - amount, - get_updated_borrow_index(market, timestamp)?, - ScalingOperation::Ceil, - ) -} - -/// Get underlying borrow amount from a scaled amount, a Market and timestamp in seconds -/// Debt amounts are always ceiled so as for rounding errors to accumulate in favor of -/// the protocol -/// NOTE: Calling this function when interests for the market are up to date with the current block -/// and index is not, will use the wrong interest rate to update the index. -pub fn get_underlying_debt_amount( - amount_scaled: Uint128, - market: &Market, - timestamp: u64, -) -> StdResult { - compute_underlying_amount( - amount_scaled, - get_updated_borrow_index(market, timestamp)?, - ScalingOperation::Ceil, - ) -} - -pub enum ScalingOperation { - Truncate, - Ceil, -} - -/// Scales the amount dividing by an index in order to compute interest rates. Before dividing, -/// the value is multiplied by SCALING_FACTOR for greater precision. -/// Example: -/// Current index is 10. We deposit 6.123456 OSMO (6123456 uosmo). Scaled amount will be -/// 6123456 / 10 = 612345 so we loose some precision. In order to avoid this situation -/// we scale the amount by SCALING_FACTOR. -pub fn compute_scaled_amount( - amount: Uint128, - index: Decimal, - scaling_operation: ScalingOperation, -) -> StdResult { - // Scale by SCALING_FACTOR to have better precision - let scaled_amount = amount.checked_mul(SCALING_FACTOR)?; - match scaling_operation { - ScalingOperation::Truncate => math::divide_uint128_by_decimal(scaled_amount, index), - ScalingOperation::Ceil => math::divide_uint128_by_decimal_and_ceil(scaled_amount, index), - } -} - -/// Descales the amount introduced by `get_scaled_amount`, returning the underlying amount. -/// As interest rate is accumulated the index used to descale the amount should be bigger than the one used to scale it. -pub fn compute_underlying_amount( - scaled_amount: Uint128, - index: Decimal, - scaling_operation: ScalingOperation, -) -> StdResult { - // Multiply scaled amount by decimal (index) - let before_scaling_factor = scaled_amount * index; - - // Descale by SCALING_FACTOR which is introduced when scaling the amount - match scaling_operation { - ScalingOperation::Truncate => Ok(before_scaling_factor.checked_div(SCALING_FACTOR)?), - ScalingOperation::Ceil => { - math::uint128_checked_div_with_ceil(before_scaling_factor, SCALING_FACTOR) - } - } -} - -/// Return applied interest rate for borrow index according to passed blocks -/// NOTE: Calling this function when interests for the market are up to date with the current block -/// and index is not, will use the wrong interest rate to update the index. -pub fn get_updated_borrow_index(market: &Market, timestamp: u64) -> StdResult { - if market.indexes_last_updated < timestamp { - let time_elapsed = timestamp - market.indexes_last_updated; - - if !market.borrow_rate.is_zero() { - let updated_index = calculate_applied_linear_interest_rate( - market.borrow_index, - market.borrow_rate, - time_elapsed, - ); - return updated_index; - } - } - - Ok(market.borrow_index) -} - -/// Return applied interest rate for liquidity index according to passed blocks -/// NOTE: Calling this function when interests for the market are up to date with the current block -/// and index is not, will use the wrong interest rate to update the index. -pub fn get_updated_liquidity_index(market: &Market, timestamp: u64) -> StdResult { - if market.indexes_last_updated > timestamp { - return Err(StdError::generic_err( - format!("Cannot compute updated liquidity index for a timestamp: {} smaller than last updated timestamp for market: {}", timestamp, market.indexes_last_updated) - )); - } - - if market.indexes_last_updated < timestamp { - let time_elapsed = timestamp - market.indexes_last_updated; - - if !market.liquidity_rate.is_zero() { - let updated_index = calculate_applied_linear_interest_rate( - market.liquidity_index, - market.liquidity_rate, - time_elapsed, - ); - return updated_index; - } - } - - Ok(market.liquidity_index) -} - /// Update interest rates for current liquidity and debt levels /// Note it does not save the market to the store (that is left to the caller) /// Returns response with appended interest rates updated event @@ -306,48 +130,3 @@ pub fn build_interests_updated_event(denom: &str, market: &Market) -> Event { .add_attribute("borrow_rate", market.borrow_rate.to_string()) .add_attribute("liquidity_rate", market.liquidity_rate.to_string()) } - -#[cfg(test)] -mod tests { - use cosmwasm_std::{Decimal, Uint128}; - use mars_red_bank_types::red_bank::Market; - - use crate::interest_rates::{ - calculate_applied_linear_interest_rate, get_scaled_debt_amount, - get_scaled_liquidity_amount, get_underlying_debt_amount, get_underlying_liquidity_amount, - }; - - #[test] - fn accumulated_index_calculation() { - let index = Decimal::from_ratio(1u128, 10u128); - let rate = Decimal::from_ratio(2u128, 10u128); - let time_elapsed = 15768000; // half a year - let accumulated = - calculate_applied_linear_interest_rate(index, rate, time_elapsed).unwrap(); - - assert_eq!(accumulated, Decimal::from_ratio(11u128, 100u128)); - } - - #[test] - fn liquidity_and_debt_rounding() { - let start = Uint128::from(100_000_000_000_u128); - let market = Market { - liquidity_index: Decimal::from_ratio(3_u128, 1_u128), - borrow_index: Decimal::from_ratio(3_u128, 1_u128), - indexes_last_updated: 1, - ..Default::default() - }; - - let scaled_amount_liquidity = get_scaled_liquidity_amount(start, &market, 1).unwrap(); - let scaled_amount_debt = get_scaled_debt_amount(start, &market, 1).unwrap(); - assert_eq!(Uint128::from(33_333_333_333_333_333_u128), scaled_amount_liquidity); - assert_eq!(Uint128::from(33_333_333_333_333_334_u128), scaled_amount_debt); - - let back_to_underlying_liquidity = - get_underlying_liquidity_amount(scaled_amount_liquidity, &market, 1).unwrap(); - let back_to_underlying_debt = - get_underlying_debt_amount(scaled_amount_debt, &market, 1).unwrap(); - assert_eq!(Uint128::from(99_999_999_999_u128), back_to_underlying_liquidity); - assert_eq!(Uint128::from(100_000_000_001_u128), back_to_underlying_debt); - } -} diff --git a/contracts/red-bank/src/lib.rs b/contracts/red-bank/src/lib.rs index d3d5c1ef0..c3e17f007 100644 --- a/contracts/red-bank/src/lib.rs +++ b/contracts/red-bank/src/lib.rs @@ -1,9 +1,20 @@ +pub mod asset; +pub mod borrow; +pub mod collateral; +pub mod config; #[cfg(not(feature = "library"))] pub mod contract; +pub mod deposit; pub mod error; -pub mod execute; pub mod health; +pub mod instantiate; pub mod interest_rates; +pub mod liquidate; pub mod query; +pub mod repay; pub mod state; +pub mod uncollateralized_loan; pub mod user; +pub mod withdraw; + +pub mod helpers; diff --git a/contracts/red-bank/src/liquidate.rs b/contracts/red-bank/src/liquidate.rs new file mode 100644 index 000000000..ed1dc1e44 --- /dev/null +++ b/contracts/red-bank/src/liquidate.rs @@ -0,0 +1,237 @@ +use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response, Uint128}; +use mars_interest_rate::{ + get_scaled_debt_amount, get_scaled_liquidity_amount, get_underlying_debt_amount, + get_underlying_liquidity_amount, +}; +use mars_liquidation::liquidation::calculate_liquidation_amounts; +use mars_red_bank_types::address_provider::{self, MarsAddressType}; +use mars_utils::helpers::{build_send_asset_msg, option_string_to_addr}; + +use crate::{ + error::ContractError, + health::get_health_and_positions, + helpers::{query_asset_params, query_target_health_factor}, + interest_rates::{apply_accumulated_interests, update_interest_rates}, + state::{COLLATERALS, CONFIG, DEBTS, MARKETS}, + user::User, +}; + +/// Execute loan liquidations on under-collateralized loans +pub fn liquidate( + deps: DepsMut, + env: Env, + info: MessageInfo, + collateral_denom: String, + debt_denom: String, + liquidatee_addr: Addr, + sent_debt_amount: Uint128, + recipient: Option, +) -> Result { + let block_time = env.block.time.seconds(); + + let liquidatee = User(&liquidatee_addr); + + // The recipient address for receiving collateral + let recipient_addr = option_string_to_addr(deps.api, recipient, info.sender.clone())?; + let recipient = User(&recipient_addr); + + // 1. Validate liquidation + + // User cannot liquidate themselves + if info.sender == liquidatee_addr { + return Err(ContractError::CannotLiquidateSelf {}); + } + + // If user (contract) has a positive uncollateralized limit then the user + // cannot be liquidated + if !liquidatee.uncollateralized_loan_limit(deps.storage, &debt_denom)?.is_zero() { + return Err(ContractError::CannotLiquidateWhenPositiveUncollateralizedLoanLimit {}); + }; + + // check if the user has enabled the collateral asset as collateral + let user_collateral = COLLATERALS + .may_load(deps.storage, (&liquidatee_addr, "", &collateral_denom))? + .ok_or(ContractError::CannotLiquidateWhenNoCollateralBalance {})?; + if !user_collateral.enabled { + return Err(ContractError::CannotLiquidateWhenCollateralUnset { + denom: collateral_denom, + }); + } + + // check if user has outstanding debt in the deposited asset that needs to be repayed + let user_debt = DEBTS + .may_load(deps.storage, (&liquidatee_addr, &debt_denom))? + .ok_or(ContractError::CannotLiquidateWhenNoDebtBalance {})?; + + // check if user has available collateral in specified collateral asset to be liquidated + let collateral_market = MARKETS.load(deps.storage, &collateral_denom)?; + + // 2. Compute health factor + let config = CONFIG.load(deps.storage)?; + + let addresses = address_provider::helpers::query_contract_addrs( + deps.as_ref(), + &config.address_provider, + vec![ + MarsAddressType::Oracle, + MarsAddressType::Incentives, + MarsAddressType::RewardsCollector, + MarsAddressType::Params, + ], + )?; + let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; + let incentives_addr = &addresses[&MarsAddressType::Incentives]; + let oracle_addr = &addresses[&MarsAddressType::Oracle]; + let params_addr = &addresses[&MarsAddressType::Params]; + + let (health, assets_positions) = get_health_and_positions( + &deps.as_ref(), + &env, + &liquidatee_addr, + "", + oracle_addr, + params_addr, + true, + )?; + + if !health.is_liquidatable() { + return Err(ContractError::CannotLiquidateHealthyPosition {}); + } + + let debt_market = if debt_denom != collateral_denom { + MARKETS.load(deps.storage, &debt_denom)? + } else { + collateral_market.clone() + }; + + // 3. Compute debt to repay and collateral to liquidate + let collateral_price = assets_positions + .get(&collateral_denom) + .ok_or(ContractError::CannotLiquidateWhenNoCollateralBalance {})? + .asset_price; + let debt_price = assets_positions + .get(&debt_denom) + .ok_or(ContractError::CannotLiquidateWhenNoDebtBalance {})? + .asset_price; + + let mut response = Response::new(); + + let user_debt_amount = + get_underlying_debt_amount(user_debt.amount_scaled, &debt_market, block_time)?; + + let collateral_params = query_asset_params(&deps.querier, params_addr, &collateral_denom)?; + let target_health_factor = query_target_health_factor(&deps.querier, params_addr)?; + + let user_collateral_amount = get_underlying_liquidity_amount( + user_collateral.amount_scaled, + &collateral_market, + block_time, + )?; + let ( + debt_amount_to_repay, + collateral_amount_to_liquidate, + collateral_amount_received_by_liquidator, + ) = calculate_liquidation_amounts( + user_collateral_amount, + collateral_price, + &collateral_params, + user_debt_amount, + sent_debt_amount, + debt_price, + target_health_factor, + &health, + )?; + let protocol_fee = collateral_amount_to_liquidate - collateral_amount_received_by_liquidator; + + let refund_amount = sent_debt_amount - debt_amount_to_repay; + + let collateral_amount_to_liquidate_scaled = get_scaled_liquidity_amount( + collateral_amount_to_liquidate, + &collateral_market, + block_time, + )?; + + let collateral_amount_received_by_liquidator_scaled = get_scaled_liquidity_amount( + collateral_amount_received_by_liquidator, + &collateral_market, + block_time, + )?; + + let protocol_fee_scaled = + get_scaled_liquidity_amount(protocol_fee, &collateral_market, block_time)?; + + // 4. Transfer collateral shares from the user to the liquidator and rewards-collector (protocol fee) + response = liquidatee.decrease_collateral( + deps.storage, + &collateral_market, + collateral_amount_to_liquidate_scaled, + incentives_addr, + response, + None, + )?; + response = recipient.increase_collateral( + deps.storage, + &collateral_market, + collateral_amount_received_by_liquidator_scaled, + incentives_addr, + response, + None, + )?; + if !protocol_fee.is_zero() { + response = User(rewards_collector_addr).increase_collateral( + deps.storage, + &collateral_market, + protocol_fee_scaled, + incentives_addr, + response, + None, + )?; + } + + // 5. Reduce the user's debt shares + let user_debt_amount_after = user_debt_amount.checked_sub(debt_amount_to_repay)?; + let user_debt_amount_scaled_after = + get_scaled_debt_amount(user_debt_amount_after, &debt_market, block_time)?; + + // Compute delta so it can be substracted to total debt + let debt_amount_scaled_delta = + user_debt.amount_scaled.checked_sub(user_debt_amount_scaled_after)?; + + liquidatee.decrease_debt(deps.storage, &debt_denom, debt_amount_scaled_delta)?; + + let market_debt_total_scaled_after = + debt_market.debt_total_scaled.checked_sub(debt_amount_scaled_delta)?; + + // 6. Update markets + let mut debt_market_after = debt_market; + response = apply_accumulated_interests( + deps.storage, + &env, + &mut debt_market_after, + rewards_collector_addr, + incentives_addr, + response, + )?; + debt_market_after.debt_total_scaled = market_debt_total_scaled_after; + response = update_interest_rates(&env, &mut debt_market_after, response)?; + MARKETS.save(deps.storage, &debt_denom, &debt_market_after)?; + + // 7. Build response + // refund sent amount in excess of actual debt amount to liquidate + if !refund_amount.is_zero() { + response = + response.add_message(build_send_asset_msg(&info.sender, &debt_denom, refund_amount)); + } + + Ok(response + .add_attribute("action", "liquidate") + .add_attribute("user", liquidatee) + .add_attribute("liquidator", info.sender.to_string()) + .add_attribute("recipient", recipient) + .add_attribute("collateral_denom", collateral_denom) + .add_attribute("collateral_amount", collateral_amount_to_liquidate) + .add_attribute("collateral_amount_scaled", collateral_amount_to_liquidate_scaled) + .add_attribute("debt_denom", debt_denom) + .add_attribute("debt_amount", debt_amount_to_repay) + .add_attribute("debt_amount_scaled", debt_amount_scaled_delta)) +} diff --git a/contracts/red-bank/src/query.rs b/contracts/red-bank/src/query.rs index 5fea2642b..68c89dab3 100644 --- a/contracts/red-bank/src/query.rs +++ b/contracts/red-bank/src/query.rs @@ -1,25 +1,27 @@ -use cosmwasm_std::{Addr, BlockInfo, Deps, Env, Order, StdError, StdResult, Uint128}; +use cosmwasm_std::{Addr, BlockInfo, Deps, Env, Order, StdResult, Uint128}; use cw_storage_plus::Bound; +use mars_interest_rate::{ + get_scaled_debt_amount, get_scaled_liquidity_amount, get_underlying_debt_amount, + get_underlying_liquidity_amount, +}; use mars_red_bank_types::{ address_provider::{self, MarsAddressType}, red_bank::{ - Collateral, ConfigResponse, Debt, Market, UncollateralizedLoanLimitResponse, - UserCollateralResponse, UserDebtResponse, UserHealthStatus, UserPositionResponse, + Collateral, ConfigResponse, Debt, Market, PaginatedUserCollateralResponse, + UncollateralizedLoanLimitResponse, UserCollateralResponse, UserDebtResponse, + UserHealthStatus, UserPositionResponse, }, + Metadata, }; use crate::{ error::ContractError, health, - interest_rates::{ - get_scaled_debt_amount, get_scaled_liquidity_amount, get_underlying_debt_amount, - get_underlying_liquidity_amount, - }, state::{COLLATERALS, CONFIG, DEBTS, MARKETS, OWNER, UNCOLLATERALIZED_LOAN_LIMITS}, }; -const DEFAULT_LIMIT: u32 = 5; -const MAX_LIMIT: u32 = 10; +const DEFAULT_LIMIT: u32 = 10; +const MAX_LIMIT: u32 = 30; pub fn query_config(deps: Deps) -> StdResult { let owner_state = OWNER.query(deps.storage)?; @@ -27,16 +29,12 @@ pub fn query_config(deps: Deps) -> StdResult { Ok(ConfigResponse { owner: owner_state.owner, proposed_new_owner: owner_state.proposed, - emergency_owner: owner_state.emergency_owner, address_provider: config.address_provider.to_string(), - close_factor: config.close_factor, }) } -pub fn query_market(deps: Deps, denom: String) -> StdResult { - MARKETS - .load(deps.storage, &denom) - .map_err(|_| StdError::generic_err(format!("failed to load market for: {denom}"))) +pub fn query_market(deps: Deps, denom: String) -> StdResult> { + MARKETS.may_load(deps.storage, &denom) } pub fn query_markets( @@ -97,7 +95,7 @@ pub fn query_user_debt( block: &BlockInfo, user_addr: Addr, denom: String, -) -> StdResult { +) -> Result { let Debt { amount_scaled, uncollateralized, @@ -121,7 +119,7 @@ pub fn query_user_debts( user_addr: Addr, start_after: Option, limit: Option, -) -> StdResult> { +) -> Result, ContractError> { let block_time = block.time.seconds(); let start = start_after.map(|denom| Bound::ExclusiveRaw(denom.into_bytes())); @@ -153,12 +151,15 @@ pub fn query_user_collateral( deps: Deps, block: &BlockInfo, user_addr: Addr, + account_id: Option, denom: String, -) -> StdResult { +) -> Result { + let acc_id = account_id.unwrap_or("".to_string()); + let Collateral { amount_scaled, enabled, - } = COLLATERALS.may_load(deps.storage, (&user_addr, &denom))?.unwrap_or_default(); + } = COLLATERALS.may_load(deps.storage, (&user_addr, &acc_id, &denom))?.unwrap_or_default(); let block_time = block.time.seconds(); let market = MARKETS.load(deps.storage, &denom)?; @@ -176,18 +177,33 @@ pub fn query_user_collaterals( deps: Deps, block: &BlockInfo, user_addr: Addr, + account_id: Option, start_after: Option, limit: Option, -) -> StdResult> { +) -> Result, ContractError> { + let res_v2 = query_user_collaterals_v2(deps, block, user_addr, account_id, start_after, limit)?; + Ok(res_v2.data) +} + +pub fn query_user_collaterals_v2( + deps: Deps, + block: &BlockInfo, + user_addr: Addr, + account_id: Option, + start_after: Option, + limit: Option, +) -> Result { let block_time = block.time.seconds(); let start = start_after.map(|denom| Bound::ExclusiveRaw(denom.into_bytes())); let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - COLLATERALS - .prefix(&user_addr) + let acc_id = account_id.unwrap_or("".to_string()); + + let user_collaterals_res: Result, ContractError> = COLLATERALS + .prefix((&user_addr, &acc_id)) .range(deps.storage, start, None, Order::Ascending) - .take(limit) + .take(limit + 1) // Fetch one extra item to determine if there are more .map(|item| { let (denom, collateral) = item?; @@ -203,7 +219,20 @@ pub fn query_user_collaterals( enabled: collateral.enabled, }) }) - .collect() + .collect(); + + let mut user_collaterals = user_collaterals_res?; + let has_more = user_collaterals.len() > limit; + if has_more { + user_collaterals.pop(); // Remove the extra item used for checking if there are more items + } + + Ok(PaginatedUserCollateralResponse { + data: user_collaterals, + metadata: Metadata { + has_more, + }, + }) } pub fn query_scaled_liquidity_amount( @@ -211,9 +240,9 @@ pub fn query_scaled_liquidity_amount( env: Env, denom: String, amount: Uint128, -) -> StdResult { +) -> Result { let market = MARKETS.load(deps.storage, &denom)?; - get_scaled_liquidity_amount(amount, &market, env.block.time.seconds()) + Ok(get_scaled_liquidity_amount(amount, &market, env.block.time.seconds())?) } pub fn query_scaled_debt_amount( @@ -221,9 +250,9 @@ pub fn query_scaled_debt_amount( env: Env, denom: String, amount: Uint128, -) -> StdResult { +) -> Result { let market = MARKETS.load(deps.storage, &denom)?; - get_scaled_debt_amount(amount, &market, env.block.time.seconds()) + Ok(get_scaled_debt_amount(amount, &market, env.block.time.seconds())?) } pub fn query_underlying_liquidity_amount( @@ -231,9 +260,9 @@ pub fn query_underlying_liquidity_amount( env: Env, denom: String, amount_scaled: Uint128, -) -> StdResult { +) -> Result { let market = MARKETS.load(deps.storage, &denom)?; - get_underlying_liquidity_amount(amount_scaled, &market, env.block.time.seconds()) + Ok(get_underlying_liquidity_amount(amount_scaled, &market, env.block.time.seconds())?) } pub fn query_underlying_debt_amount( @@ -241,24 +270,38 @@ pub fn query_underlying_debt_amount( env: Env, denom: String, amount_scaled: Uint128, -) -> StdResult { +) -> Result { let market = MARKETS.load(deps.storage, &denom)?; - get_underlying_debt_amount(amount_scaled, &market, env.block.time.seconds()) + Ok(get_underlying_debt_amount(amount_scaled, &market, env.block.time.seconds())?) } pub fn query_user_position( deps: Deps, env: Env, user_addr: Addr, + account_id: Option, + liquidation_pricing: bool, ) -> Result { let config = CONFIG.load(deps.storage)?; - let oracle_addr = address_provider::helpers::query_contract_addr( + + let addresses = address_provider::helpers::query_contract_addrs( deps, &config.address_provider, - MarsAddressType::Oracle, + vec![MarsAddressType::Oracle, MarsAddressType::Params], + )?; + let oracle_addr = &addresses[&MarsAddressType::Oracle]; + let params_addr = &addresses[&MarsAddressType::Params]; + + let acc_id = account_id.unwrap_or("".to_string()); + let positions = health::get_user_positions_map( + &deps, + &env, + &user_addr, + &acc_id, + oracle_addr, + params_addr, + liquidation_pricing, )?; - - let positions = health::get_user_positions_map(&deps, &env, &user_addr, &oracle_addr)?; let health = health::compute_position_health(&positions)?; let health_status = if let (Some(max_ltv_hf), Some(liq_threshold_hf)) = diff --git a/contracts/red-bank/src/repay.rs b/contracts/red-bank/src/repay.rs new file mode 100644 index 000000000..b9c0fcc8c --- /dev/null +++ b/contracts/red-bank/src/repay.rs @@ -0,0 +1,96 @@ +use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response, Uint128}; +use mars_interest_rate::{get_scaled_debt_amount, get_underlying_debt_amount}; +use mars_red_bank_types::{address_provider, address_provider::MarsAddressType}; +use mars_utils::helpers::build_send_asset_msg; + +use crate::{ + error::ContractError, + interest_rates::{apply_accumulated_interests, update_interest_rates}, + state::{CONFIG, DEBTS, MARKETS}, + user::User, +}; + +pub fn repay( + deps: DepsMut, + env: Env, + info: MessageInfo, + on_behalf_of: Option, + denom: String, + repay_amount: Uint128, +) -> Result { + let user_addr: Addr; + let user = if let Some(address) = on_behalf_of { + user_addr = deps.api.addr_validate(&address)?; + let user = User(&user_addr); + // Uncollateralized loans should not have 'on behalf of' because it creates accounting complexity for them + if !user.uncollateralized_loan_limit(deps.storage, &denom)?.is_zero() { + return Err(ContractError::CannotRepayUncollateralizedLoanOnBehalfOf {}); + } + user + } else { + User(&info.sender) + }; + + // Check new debt + let debt = DEBTS + .may_load(deps.storage, (user.address(), &denom))? + .ok_or(ContractError::CannotRepayZeroDebt {})?; + + let config = CONFIG.load(deps.storage)?; + + let addresses = address_provider::helpers::query_contract_addrs( + deps.as_ref(), + &config.address_provider, + vec![MarsAddressType::Incentives, MarsAddressType::RewardsCollector], + )?; + let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; + let incentives_addr = &addresses[&MarsAddressType::Incentives]; + + let mut market = MARKETS.load(deps.storage, &denom)?; + + let mut response = Response::new(); + + response = apply_accumulated_interests( + deps.storage, + &env, + &mut market, + rewards_collector_addr, + incentives_addr, + response, + )?; + + let debt_amount_scaled_before = debt.amount_scaled; + let debt_amount_before = + get_underlying_debt_amount(debt.amount_scaled, &market, env.block.time.seconds())?; + + // If repay amount exceeds debt, refund any excess amounts + let mut refund_amount = Uint128::zero(); + let mut debt_amount_after = Uint128::zero(); + if repay_amount > debt_amount_before { + refund_amount = repay_amount - debt_amount_before; + let refund_msg = build_send_asset_msg(&info.sender, &denom, refund_amount); + response = response.add_message(refund_msg); + } else { + debt_amount_after = debt_amount_before - repay_amount; + } + + let debt_amount_scaled_after = + get_scaled_debt_amount(debt_amount_after, &market, env.block.time.seconds())?; + + let debt_amount_scaled_delta = + debt_amount_scaled_before.checked_sub(debt_amount_scaled_after)?; + + market.decrease_debt(debt_amount_scaled_delta)?; + user.decrease_debt(deps.storage, &denom, debt_amount_scaled_delta)?; + + response = update_interest_rates(&env, &mut market, response)?; + MARKETS.save(deps.storage, &denom, &market)?; + + Ok(response + .add_attribute("action", "repay") + .add_attribute("sender", &info.sender) + .add_attribute("on_behalf_of", user) + .add_attribute("denom", denom) + .add_attribute("amount", repay_amount.checked_sub(refund_amount)?) + .add_attribute("amount_scaled", debt_amount_scaled_delta)) +} diff --git a/contracts/red-bank/src/state.rs b/contracts/red-bank/src/state.rs index 4eaf0be3b..075eb7c01 100644 --- a/contracts/red-bank/src/state.rs +++ b/contracts/red-bank/src/state.rs @@ -6,6 +6,7 @@ use mars_red_bank_types::red_bank::{Collateral, Config, Debt, Market}; pub const OWNER: Owner = Owner::new("owner"); pub const CONFIG: Item> = Item::new("config"); pub const MARKETS: Map<&str, Market> = Map::new("markets"); -pub const COLLATERALS: Map<(&Addr, &str), Collateral> = Map::new("collaterals"); +/// The key is: user address, account id (if any), collateral denom +pub const COLLATERALS: Map<(&Addr, &str, &str), Collateral> = Map::new("collaterals"); pub const DEBTS: Map<(&Addr, &str), Debt> = Map::new("debts"); pub const UNCOLLATERALIZED_LOAN_LIMITS: Map<(&Addr, &str), Uint128> = Map::new("limits"); diff --git a/contracts/red-bank/src/uncollateralized_loan.rs b/contracts/red-bank/src/uncollateralized_loan.rs new file mode 100644 index 000000000..7b4d839f9 --- /dev/null +++ b/contracts/red-bank/src/uncollateralized_loan.rs @@ -0,0 +1,51 @@ +use cosmwasm_std::{Addr, DepsMut, MessageInfo, Response, StdResult, Uint128}; +use mars_red_bank_types::red_bank::Debt; + +use crate::{ + error::ContractError, + state::{DEBTS, OWNER, UNCOLLATERALIZED_LOAN_LIMITS}, +}; + +/// Update uncollateralized loan limit by a given amount in base asset +pub fn update_uncollateralized_loan_limit( + deps: DepsMut, + info: MessageInfo, + user_addr: Addr, + denom: String, + new_limit: Uint128, +) -> Result { + OWNER.assert_owner(deps.storage, &info.sender)?; + + // Check that the user has no collateralized debt + let current_limit = UNCOLLATERALIZED_LOAN_LIMITS + .may_load(deps.storage, (&user_addr, &denom))? + .unwrap_or_else(Uint128::zero); + let current_debt = DEBTS + .may_load(deps.storage, (&user_addr, &denom))? + .map(|debt| debt.amount_scaled) + .unwrap_or_else(Uint128::zero); + if current_limit.is_zero() && !current_debt.is_zero() { + return Err(ContractError::UserHasCollateralizedDebt {}); + } + if !current_limit.is_zero() && new_limit.is_zero() && !current_debt.is_zero() { + return Err(ContractError::UserHasUncollateralizedDebt {}); + } + + UNCOLLATERALIZED_LOAN_LIMITS.save(deps.storage, (&user_addr, &denom), &new_limit)?; + + DEBTS.update(deps.storage, (&user_addr, &denom), |debt_opt: Option| -> StdResult<_> { + let mut debt = debt_opt.unwrap_or(Debt { + amount_scaled: Uint128::zero(), + uncollateralized: false, + }); + // if limit == 0 then uncollateralized = false, otherwise uncollateralized = true + debt.uncollateralized = !new_limit.is_zero(); + Ok(debt) + })?; + + Ok(Response::new() + .add_attribute("action", "update_uncollateralized_loan_limit") + .add_attribute("user", user_addr) + .add_attribute("denom", denom) + .add_attribute("new_allowance", new_limit)) +} diff --git a/contracts/red-bank/src/user.rs b/contracts/red-bank/src/user.rs index b8e7dd4b1..39ad2311e 100644 --- a/contracts/red-bank/src/user.rs +++ b/contracts/red-bank/src/user.rs @@ -48,8 +48,13 @@ impl<'a> User<'a> { } /// Load the user's collateral - pub fn collateral(&self, store: &dyn Storage, denom: &str) -> StdResult { - COLLATERALS.load(store, (self.0, denom)) + pub fn collateral( + &self, + store: &dyn Storage, + denom: &str, + account_id: &str, + ) -> StdResult { + COLLATERALS.load(store, (self.0, account_id, denom)) } /// Load the user's debt @@ -102,10 +107,13 @@ impl<'a> User<'a> { amount_scaled: Uint128, incentives_addr: &Addr, response: Response, + account_id: Option, ) -> StdResult { + let acc_id = account_id.clone().unwrap_or("".to_string()); + let mut amount_scaled_before = Uint128::zero(); - COLLATERALS.update(store, (self.0, &market.denom), |opt| -> StdResult<_> { + COLLATERALS.update(store, (self.0, &acc_id, &market.denom), |opt| -> StdResult<_> { match opt { Some(mut col) => { amount_scaled_before = col.amount_scaled; @@ -123,6 +131,7 @@ impl<'a> User<'a> { incentives_addr, market, amount_scaled_before, + account_id, )?; Ok(response.add_message(msg)) @@ -141,22 +150,26 @@ impl<'a> User<'a> { amount_scaled: Uint128, incentives_addr: &Addr, response: Response, + account_id: Option, ) -> StdResult { - let mut collateral = COLLATERALS.load(store, (self.0, &market.denom))?; + let acc_id = account_id.clone().unwrap_or("".to_string()); + + let mut collateral = COLLATERALS.load(store, (self.0, &acc_id, &market.denom))?; let amount_scaled_before = collateral.amount_scaled; collateral.amount_scaled = collateral.amount_scaled.checked_sub(amount_scaled)?; if collateral.amount_scaled.is_zero() { - COLLATERALS.remove(store, (self.0, &market.denom)); + COLLATERALS.remove(store, (self.0, &acc_id, &market.denom)); } else { - COLLATERALS.save(store, (self.0, &market.denom), &collateral)?; + COLLATERALS.save(store, (self.0, &acc_id, &market.denom), &collateral)?; } let msg = self.build_incentives_balance_changed_msg( incentives_addr, market, amount_scaled_before, + account_id, )?; Ok(response.add_message(msg)) @@ -171,11 +184,13 @@ impl<'a> User<'a> { incentives_addr: &Addr, market: &Market, user_amount_scaled_before: Uint128, + account_id: Option, ) -> StdResult { Ok(WasmMsg::Execute { contract_addr: incentives_addr.into(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: self.address().clone(), + account_id, denom: market.denom.clone(), user_amount_scaled_before, total_amount_scaled_before: market.collateral_total_scaled, diff --git a/contracts/red-bank/src/withdraw.rs b/contracts/red-bank/src/withdraw.rs new file mode 100644 index 000000000..890feec52 --- /dev/null +++ b/contracts/red-bank/src/withdraw.rs @@ -0,0 +1,155 @@ +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Uint128}; +use mars_interest_rate::{get_scaled_liquidity_amount, get_underlying_liquidity_amount}; +use mars_red_bank_types::{address_provider, address_provider::MarsAddressType, error::MarsError}; +use mars_utils::helpers::build_send_asset_msg; + +use crate::{ + error::ContractError, + health::assert_below_liq_threshold_after_withdraw, + interest_rates::{apply_accumulated_interests, update_interest_rates}, + state::{CONFIG, MARKETS}, + user::User, +}; + +pub fn withdraw( + deps: DepsMut, + env: Env, + info: MessageInfo, + denom: String, + amount: Option, + recipient: Option, + account_id: Option, + liquidation_related: bool, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + let addresses = address_provider::helpers::query_contract_addrs( + deps.as_ref(), + &config.address_provider, + vec![ + MarsAddressType::Oracle, + MarsAddressType::Incentives, + MarsAddressType::RewardsCollector, + MarsAddressType::Params, + MarsAddressType::CreditManager, + ], + )?; + let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; + let incentives_addr = &addresses[&MarsAddressType::Incentives]; + let oracle_addr = &addresses[&MarsAddressType::Oracle]; + let params_addr = &addresses[&MarsAddressType::Params]; + let credit_manager_addr = &addresses[&MarsAddressType::CreditManager]; + + // Don't allow red-bank users to create alternative account ids. + // Only allow credit-manager contract to create them. + // Even if account_id contains empty string we won't allow it. + if account_id.is_some() && info.sender != credit_manager_addr { + return Err(ContractError::Mars(MarsError::Unauthorized {})); + } + + let withdrawer = User(&info.sender); + let acc_id = account_id.clone().unwrap_or("".to_string()); + + let mut market = MARKETS.load(deps.storage, &denom)?; + + let collateral = withdrawer.collateral(deps.storage, &denom, &acc_id)?; + let withdrawer_balance_scaled_before = collateral.amount_scaled; + + if withdrawer_balance_scaled_before.is_zero() { + return Err(ContractError::UserNoCollateralBalance { + user: withdrawer.into(), + denom, + }); + } + + let withdrawer_balance_before = get_underlying_liquidity_amount( + withdrawer_balance_scaled_before, + &market, + env.block.time.seconds(), + )?; + + let withdraw_amount = match amount { + // Check user has sufficient balance to send back + Some(amount) if amount.is_zero() || amount > withdrawer_balance_before => { + return Err(ContractError::InvalidWithdrawAmount { + denom, + }); + } + Some(amount) => amount, + // If no amount is specified, the full balance is withdrawn + None => withdrawer_balance_before, + }; + + // if withdraw is part of the liquidation in credit manager we need to use correct pricing for the assets + let liquidation_related = info.sender == credit_manager_addr && liquidation_related; + + // if asset is used as collateral and user is borrowing we need to validate health factor after withdraw, + // otherwise no reasons to block the withdraw + if collateral.enabled + && withdrawer.is_borrowing(deps.storage) + && !assert_below_liq_threshold_after_withdraw( + &deps.as_ref(), + &env, + withdrawer.address(), + &acc_id, + oracle_addr, + params_addr, + &denom, + withdraw_amount, + liquidation_related, + )? + { + return Err(ContractError::InvalidHealthFactorAfterWithdraw {}); + } + + let mut response = Response::new(); + + // update indexes and interest rates + response = apply_accumulated_interests( + deps.storage, + &env, + &mut market, + rewards_collector_addr, + incentives_addr, + response, + )?; + + // reduce the withdrawer's scaled collateral amount + let withdrawer_balance_after = withdrawer_balance_before.checked_sub(withdraw_amount)?; + let withdrawer_balance_scaled_after = + get_scaled_liquidity_amount(withdrawer_balance_after, &market, env.block.time.seconds())?; + + let withdraw_amount_scaled = + withdrawer_balance_scaled_before.checked_sub(withdrawer_balance_scaled_after)?; + + response = withdrawer.decrease_collateral( + deps.storage, + &market, + withdraw_amount_scaled, + incentives_addr, + response, + account_id, + )?; + + market.decrease_collateral(withdraw_amount_scaled)?; + + response = update_interest_rates(&env, &mut market, response)?; + + MARKETS.save(deps.storage, &denom, &market)?; + + // send underlying asset to user or another recipient + let recipient_addr = if let Some(recipient) = recipient { + deps.api.addr_validate(&recipient)? + } else { + withdrawer.address().clone() + }; + + Ok(response + .add_message(build_send_asset_msg(&recipient_addr, &denom, withdraw_amount)) + .add_attribute("action", "withdraw") + .add_attribute("sender", withdrawer) + .add_attribute("recipient", recipient_addr) + .add_attribute("denom", denom) + .add_attribute("amount", withdraw_amount) + .add_attribute("amount_scaled", withdraw_amount_scaled)) +} diff --git a/contracts/red-bank/tests/all_tests.rs b/contracts/red-bank/tests/all_tests.rs new file mode 100644 index 000000000..14f00389d --- /dev/null +++ b/contracts/red-bank/tests/all_tests.rs @@ -0,0 +1 @@ +mod tests; diff --git a/contracts/red-bank/tests/files/Red Bank - Dynamic LB & CF test cases v1.1.xlsx b/contracts/red-bank/tests/files/Red Bank - Dynamic LB & CF test cases v1.1.xlsx new file mode 100644 index 000000000..6dea6c06c Binary files /dev/null and b/contracts/red-bank/tests/files/Red Bank - Dynamic LB & CF test cases v1.1.xlsx differ diff --git a/contracts/red-bank/tests/helpers.rs b/contracts/red-bank/tests/helpers.rs deleted file mode 100644 index 143bc09e8..000000000 --- a/contracts/red-bank/tests/helpers.rs +++ /dev/null @@ -1,286 +0,0 @@ -#![allow(dead_code)] - -use cosmwasm_schema::serde; -use cosmwasm_std::{ - from_binary, - testing::{MockApi, MockStorage}, - Addr, Coin, Decimal, Deps, DepsMut, Event, OwnedDeps, Uint128, -}; -use mars_red_bank::{ - contract::{instantiate, query}, - interest_rates::{ - calculate_applied_linear_interest_rate, compute_scaled_amount, compute_underlying_amount, - ScalingOperation, - }, - state::{COLLATERALS, DEBTS, MARKETS}, -}; -use mars_red_bank_types::red_bank::{ - Collateral, CreateOrUpdateConfig, Debt, InstantiateMsg, Market, QueryMsg, -}; -use mars_testing::{mock_dependencies, mock_env, mock_info, MarsMockQuerier, MockEnvParams}; - -pub fn set_collateral( - deps: DepsMut, - user_addr: &Addr, - denom: &str, - amount_scaled: Uint128, - enabled: bool, -) { - let collateral = Collateral { - amount_scaled, - enabled, - }; - COLLATERALS.save(deps.storage, (user_addr, denom), &collateral).unwrap(); -} - -pub fn unset_collateral(deps: DepsMut, user_addr: &Addr, denom: &str) { - COLLATERALS.remove(deps.storage, (user_addr, denom)); -} - -pub fn set_debt( - deps: DepsMut, - user_addr: &Addr, - denom: &str, - amount_scaled: impl Into, - uncollateralized: bool, -) { - let debt = Debt { - amount_scaled: amount_scaled.into(), - uncollateralized, - }; - DEBTS.save(deps.storage, (user_addr, denom), &debt).unwrap(); -} - -/// Find if a user has a debt position in the specified asset -pub fn has_debt_position(deps: Deps, user_addr: &Addr, denom: &str) -> bool { - DEBTS.may_load(deps.storage, (user_addr, denom)).unwrap().is_some() -} - -/// Find if a user has a collateral position in the specified asset, regardless of whether enabled -pub fn has_collateral_position(deps: Deps, user_addr: &Addr, denom: &str) -> bool { - COLLATERALS.may_load(deps.storage, (user_addr, denom)).unwrap().is_some() -} - -/// Find whether a user has a collateral position AND has it enabled in the specified asset -pub fn has_collateral_enabled(deps: Deps, user_addr: &Addr, denom: &str) -> bool { - COLLATERALS - .may_load(deps.storage, (user_addr, denom)) - .unwrap() - .map(|collateral| collateral.enabled) - .unwrap_or(false) -} - -pub fn th_setup(contract_balances: &[Coin]) -> OwnedDeps { - let mut deps = mock_dependencies(contract_balances); - let env = mock_env(MockEnvParams::default()); - let info = mock_info("owner"); - let config = CreateOrUpdateConfig { - address_provider: Some("address_provider".to_string()), - close_factor: Some(Decimal::from_ratio(1u128, 2u128)), - }; - let msg = InstantiateMsg { - owner: "owner".to_string(), - config, - }; - instantiate(deps.as_mut(), env, info, msg).unwrap(); - - deps.querier.set_oracle_price("uusd", Decimal::one()); - - deps -} - -pub fn th_query(deps: Deps, msg: QueryMsg) -> T { - from_binary(&query(deps, mock_env(MockEnvParams::default()), msg).unwrap()).unwrap() -} - -pub fn th_init_market(deps: DepsMut, denom: &str, market: &Market) -> Market { - let new_market = Market { - denom: denom.to_string(), - ..market.clone() - }; - - MARKETS.save(deps.storage, denom, &new_market).unwrap(); - - new_market -} - -#[derive(Default, Debug)] -pub struct TestInterestResults { - pub borrow_index: Decimal, - pub liquidity_index: Decimal, - pub borrow_rate: Decimal, - pub liquidity_rate: Decimal, - pub protocol_rewards_to_distribute: Uint128, - pub less_debt_scaled: Uint128, -} - -pub fn th_build_interests_updated_event(denom: &str, ir: &TestInterestResults) -> Event { - Event::new("interests_updated") - .add_attribute("denom", denom) - .add_attribute("borrow_index", ir.borrow_index.to_string()) - .add_attribute("liquidity_index", ir.liquidity_index.to_string()) - .add_attribute("borrow_rate", ir.borrow_rate.to_string()) - .add_attribute("liquidity_rate", ir.liquidity_rate.to_string()) -} - -/// Deltas to be using in expected indices/rates results -#[derive(Default, Debug)] -pub struct TestUtilizationDeltaInfo { - pub less_liquidity: Uint128, - pub more_debt: Uint128, - pub less_debt: Uint128, - /// Used when passing less debt to compute deltas in the actual scaled amount - pub user_current_debt_scaled: Uint128, -} - -/// Takes a market before an action (ie: a borrow) among some test parameters -/// used in that action and computes the expected indices and rates after that action. -pub fn th_get_expected_indices_and_rates( - market: &Market, - block_time: u64, - delta_info: TestUtilizationDeltaInfo, -) -> TestInterestResults { - if !delta_info.more_debt.is_zero() && !delta_info.less_debt.is_zero() { - panic!("Cannot have more debt and less debt at the same time"); - } - - if !delta_info.less_debt.is_zero() && delta_info.user_current_debt_scaled.is_zero() { - panic!("Cannot have less debt with 0 current user debt scaled"); - } - - let expected_indices = th_get_expected_indices(market, block_time); - - let expected_protocol_rewards_to_distribute = - th_get_expected_protocol_rewards(market, &expected_indices); - - // When borrowing, new computed index is used for scaled amount - let more_debt_scaled = compute_scaled_amount( - delta_info.more_debt, - expected_indices.borrow, - ScalingOperation::Ceil, - ) - .unwrap(); - - // When repaying, new computed index is used to get current debt and deduct amount - let less_debt_scaled = if !delta_info.less_debt.is_zero() { - let user_current_debt = compute_underlying_amount( - delta_info.user_current_debt_scaled, - expected_indices.borrow, - ScalingOperation::Ceil, - ) - .unwrap(); - - let user_new_debt = if delta_info.less_debt >= user_current_debt { - Uint128::zero() - } else { - user_current_debt - delta_info.less_debt - }; - - let user_new_debt_scaled = - compute_scaled_amount(user_new_debt, expected_indices.borrow, ScalingOperation::Ceil) - .unwrap(); - - delta_info.user_current_debt_scaled - user_new_debt_scaled - } else { - Uint128::zero() - }; - - // NOTE: Don't panic here so that the total repay of debt can be simulated - // when less debt is greater than outstanding debt - let new_debt_total_scaled = if (market.debt_total_scaled + more_debt_scaled) > less_debt_scaled - { - market.debt_total_scaled + more_debt_scaled - less_debt_scaled - } else { - Uint128::zero() - }; - let debt_total = compute_underlying_amount( - new_debt_total_scaled, - expected_indices.borrow, - ScalingOperation::Ceil, - ) - .unwrap(); - - let total_collateral = compute_underlying_amount( - market.collateral_total_scaled, - expected_indices.liquidity, - ScalingOperation::Truncate, - ) - .unwrap(); - - // Total collateral increased by accured protocol rewards - let total_collateral = total_collateral + expected_protocol_rewards_to_distribute; - let expected_utilization_rate = if !total_collateral.is_zero() { - Decimal::from_ratio(debt_total, total_collateral) - } else { - Decimal::zero() - }; - - // interest rates (make a copy and update those values to get the expeted irs) - let mut market_copy = market.clone(); - market_copy.update_interest_rates(expected_utilization_rate).unwrap(); - - TestInterestResults { - borrow_index: expected_indices.borrow, - liquidity_index: expected_indices.liquidity, - borrow_rate: market_copy.borrow_rate, - liquidity_rate: market_copy.liquidity_rate, - protocol_rewards_to_distribute: expected_protocol_rewards_to_distribute, - less_debt_scaled, - } -} - -/// Compute protocol income to be distributed (using values up to the instant -/// before the contract call is made) -pub fn th_get_expected_protocol_rewards( - market: &Market, - expected_indices: &TestExpectedIndices, -) -> Uint128 { - let previous_borrow_index = market.borrow_index; - let previous_debt_total = compute_underlying_amount( - market.debt_total_scaled, - previous_borrow_index, - ScalingOperation::Ceil, - ) - .unwrap(); - let current_debt_total = compute_underlying_amount( - market.debt_total_scaled, - expected_indices.borrow, - ScalingOperation::Ceil, - ) - .unwrap(); - let interest_accrued = if current_debt_total > previous_debt_total { - current_debt_total - previous_debt_total - } else { - Uint128::zero() - }; - interest_accrued * market.reserve_factor -} - -/// Expected results for applying accumulated interest -pub struct TestExpectedIndices { - pub liquidity: Decimal, - pub borrow: Decimal, -} - -pub fn th_get_expected_indices(market: &Market, block_time: u64) -> TestExpectedIndices { - let seconds_elapsed = block_time - market.indexes_last_updated; - // market indices - let expected_liquidity_index = calculate_applied_linear_interest_rate( - market.liquidity_index, - market.liquidity_rate, - seconds_elapsed, - ) - .unwrap(); - - let expected_borrow_index = calculate_applied_linear_interest_rate( - market.borrow_index, - market.borrow_rate, - seconds_elapsed, - ) - .unwrap(); - - TestExpectedIndices { - liquidity: expected_liquidity_index, - borrow: expected_borrow_index, - } -} diff --git a/contracts/red-bank/tests/test_liquidate.rs b/contracts/red-bank/tests/test_liquidate.rs deleted file mode 100644 index 8d1872841..000000000 --- a/contracts/red-bank/tests/test_liquidate.rs +++ /dev/null @@ -1,1374 +0,0 @@ -use std::cmp::min; - -use cosmwasm_std::{ - attr, coin, coins, - testing::{mock_info, MockApi, MockStorage}, - to_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal, Deps, OwnedDeps, StdError, StdResult, - SubMsg, Uint128, WasmMsg, -}; -use cw_utils::PaymentError; -use helpers::{ - has_collateral_position, set_collateral, th_build_interests_updated_event, - th_get_expected_indices, th_get_expected_indices_and_rates, th_init_market, th_setup, - TestUtilizationDeltaInfo, -}; -use mars_red_bank::{ - contract::execute, - error::ContractError, - execute::liquidation_compute_amounts, - interest_rates::{ - compute_scaled_amount, compute_underlying_amount, get_scaled_liquidity_amount, - ScalingOperation, SCALING_FACTOR, - }, - state::{COLLATERALS, CONFIG, DEBTS, MARKETS}, -}; -use mars_red_bank_types::{ - address_provider::MarsAddressType, - incentives, - red_bank::{Collateral, Debt, ExecuteMsg, InterestRateModel, Market}, -}; -use mars_testing::{mock_env, mock_env_at_block_time, MarsMockQuerier, MockEnvParams}; -use mars_utils::math; - -use crate::helpers::{set_debt, TestInterestResults}; - -mod helpers; - -struct TestSuite { - deps: OwnedDeps, - collateral_coin: Coin, - debt_coin: Coin, - uncollateralized_denom: &'static str, - collateral_price: Decimal, - debt_price: Decimal, - close_factor: Decimal, - collateral_market: Market, - debt_market: Market, -} - -fn setup_test() -> TestSuite { - let initial_collateral_coin = coin(1_000_000_000u128, "collateral"); - let initial_debt_coin = coin(2_000_000_000u128, "debt"); - let mut deps = th_setup(&[initial_collateral_coin.clone(), initial_debt_coin.clone()]); - - let close_factor = Decimal::from_ratio(1u128, 2u128); - CONFIG - .update(deps.as_mut().storage, |mut config| -> StdResult<_> { - config.close_factor = close_factor; - Ok(config) - }) - .unwrap(); - - let collateral_price = Decimal::from_ratio(2_u128, 1_u128); - let debt_price = Decimal::from_ratio(11_u128, 10_u128); - let uncollateralized_debt_price = Decimal::from_ratio(15_u128, 10_u128); - let uncollateralized_denom = "uncollateralized_debt"; - deps.querier.set_oracle_price(&initial_collateral_coin.denom, collateral_price); - deps.querier.set_oracle_price(&initial_debt_coin.denom, debt_price); - deps.querier.set_oracle_price(uncollateralized_denom, uncollateralized_debt_price); - - // for the test to pass, we need an interest rate model that gives non-zero rates - let mock_ir_model = InterestRateModel { - optimal_utilization_rate: Decimal::percent(80), - base: Decimal::percent(5), - slope_1: Decimal::zero(), - slope_2: Decimal::zero(), - }; - - let collateral_market = Market { - max_loan_to_value: Decimal::from_ratio(5u128, 10u128), - liquidation_threshold: Decimal::from_ratio(6u128, 10u128), - liquidation_bonus: Decimal::from_ratio(1u128, 10u128), - collateral_total_scaled: Uint128::new(1_500_000_000) * SCALING_FACTOR, - debt_total_scaled: Uint128::new(800_000_000) * SCALING_FACTOR, - liquidity_index: Decimal::one(), - borrow_index: Decimal::one(), - liquidity_rate: Decimal::from_ratio(2u128, 10u128), - borrow_rate: Decimal::from_ratio(2u128, 10u128), - interest_rate_model: mock_ir_model.clone(), - reserve_factor: Decimal::from_ratio(2u128, 100u128), - indexes_last_updated: 0, - ..Default::default() - }; - - let debt_market = Market { - max_loan_to_value: Decimal::from_ratio(6u128, 10u128), - collateral_total_scaled: Uint128::new(3_500_000_000) * SCALING_FACTOR, - debt_total_scaled: Uint128::new(1_800_000_000) * SCALING_FACTOR, - liquidity_index: Decimal::from_ratio(12u128, 10u128), - borrow_index: Decimal::from_ratio(14u128, 10u128), - liquidity_rate: Decimal::from_ratio(2u128, 10u128), - borrow_rate: Decimal::from_ratio(2u128, 10u128), - interest_rate_model: mock_ir_model, - reserve_factor: Decimal::from_ratio(3u128, 100u128), - indexes_last_updated: 0, - ..Default::default() - }; - - let uncollateralized_debt_market = Market { - denom: uncollateralized_denom.to_string(), - ..Default::default() - }; - - let collateral_market = - th_init_market(deps.as_mut(), &initial_collateral_coin.denom, &collateral_market); - let debt_market = th_init_market(deps.as_mut(), &initial_debt_coin.denom, &debt_market); - th_init_market(deps.as_mut(), uncollateralized_denom, &uncollateralized_debt_market); - - TestSuite { - deps, - collateral_coin: initial_collateral_coin, - debt_coin: initial_debt_coin, - uncollateralized_denom, - collateral_price, - debt_price, - close_factor, - collateral_market, - debt_market, - } -} - -fn rewards_collector_collateral(deps: Deps, denom: &str) -> Collateral { - COLLATERALS - .load( - deps.storage, - (&Addr::unchecked(MarsAddressType::RewardsCollector.to_string()), denom), - ) - .unwrap() -} - -struct TestExpectedAmountResults { - user_debt_repayed: Uint128, - user_debt_repayed_scaled: Uint128, - expected_refund_amount: Uint128, - expected_liquidated_collateral_amount: Uint128, - expected_liquidated_collateral_amount_scaled: Uint128, - expected_reward_amount_scaled: Uint128, - expected_debt_rates: TestInterestResults, -} - -fn expected_amounts( - block_time: u64, - user_debt_scaled: Uint128, - repay_amount: Uint128, - test_suite: &TestSuite, -) -> TestExpectedAmountResults { - let expected_debt_indices = th_get_expected_indices(&test_suite.debt_market, block_time); - let user_debt = compute_underlying_amount( - user_debt_scaled, - expected_debt_indices.borrow, - ScalingOperation::Ceil, - ) - .unwrap(); - - let max_repayable_debt = user_debt * test_suite.close_factor; - let amount_to_repay = min(repay_amount, max_repayable_debt); - let expected_refund_amount = if amount_to_repay < repay_amount { - repay_amount - amount_to_repay - } else { - Uint128::zero() - }; - - let expected_debt_rates = th_get_expected_indices_and_rates( - &test_suite.debt_market, - block_time, - TestUtilizationDeltaInfo { - less_debt: amount_to_repay, - user_current_debt_scaled: user_debt_scaled, - less_liquidity: expected_refund_amount, - ..Default::default() - }, - ); - - let expected_liquidated_collateral_amount = math::divide_uint128_by_decimal( - amount_to_repay - * test_suite.debt_price - * (Decimal::one() + test_suite.collateral_market.liquidation_bonus), - test_suite.collateral_price, - ) - .unwrap(); - - let expected_collateral_rates = th_get_expected_indices_and_rates( - &test_suite.collateral_market, - block_time, - TestUtilizationDeltaInfo { - less_liquidity: expected_liquidated_collateral_amount, - ..Default::default() - }, - ); - - let expected_liquidated_collateral_amount_scaled = compute_scaled_amount( - expected_liquidated_collateral_amount, - expected_collateral_rates.liquidity_index, - ScalingOperation::Truncate, - ) - .unwrap(); - - let expected_reward_amount_scaled = compute_scaled_amount( - expected_debt_rates.protocol_rewards_to_distribute, - expected_debt_rates.liquidity_index, - ScalingOperation::Truncate, - ) - .unwrap(); - - TestExpectedAmountResults { - user_debt_repayed: amount_to_repay, - user_debt_repayed_scaled: expected_debt_rates.less_debt_scaled, - expected_refund_amount, - expected_liquidated_collateral_amount, - expected_liquidated_collateral_amount_scaled, - expected_reward_amount_scaled, - expected_debt_rates, - } -} - -// recipient - can be liquidator or another address which can receive collateral -fn expected_messages( - user_addr: &Addr, - recipient_addr: &Addr, - user_collateral_scaled: Uint128, - recipient_collateral_scaled: Uint128, - collateral_market: &Market, - debt_market: &Market, -) -> Vec { - // there should be up to three messages updating indices at the incentives contract, in the - // order: - // - collateral denom, user - // - collateral denom, liquidator - // - debt denom, rewards collector (if rewards accrued > 0) - // - // NOTE that we don't expect a message to update rewards collector's index of the - // **collateral** asset, because the liquidation action does NOT change the collateral - // asset's utilization rate, it's interest rate does not need to be updated. - vec![ - SubMsg::new(WasmMsg::Execute { - contract_addr: MarsAddressType::Incentives.to_string(), - msg: to_binary(&incentives::ExecuteMsg::BalanceChange { - user_addr: user_addr.clone(), - denom: collateral_market.denom.clone(), - user_amount_scaled_before: user_collateral_scaled, - total_amount_scaled_before: collateral_market.collateral_total_scaled, - }) - .unwrap(), - funds: vec![], - }), - SubMsg::new(WasmMsg::Execute { - contract_addr: MarsAddressType::Incentives.to_string(), - msg: to_binary(&incentives::ExecuteMsg::BalanceChange { - user_addr: recipient_addr.clone(), - denom: collateral_market.denom.clone(), - user_amount_scaled_before: recipient_collateral_scaled, - total_amount_scaled_before: collateral_market.collateral_total_scaled, - }) - .unwrap(), - funds: vec![], - }), - SubMsg::new(WasmMsg::Execute { - contract_addr: MarsAddressType::Incentives.to_string(), - msg: to_binary(&incentives::ExecuteMsg::BalanceChange { - user_addr: Addr::unchecked(MarsAddressType::RewardsCollector.to_string()), - denom: debt_market.denom.clone(), - user_amount_scaled_before: Uint128::zero(), - total_amount_scaled_before: debt_market.collateral_total_scaled, - }) - .unwrap(), - funds: vec![], - }), - ] -} - -#[test] -fn liquidate_if_no_coins_sent() { - let TestSuite { - mut deps, - .. - } = setup_test(); - - let env = mock_env(MockEnvParams::default()); - let info = mock_info("liquidator", &[]); - let msg = ExecuteMsg::Liquidate { - user: "user".to_string(), - collateral_denom: "collateral".to_string(), - recipient: None, - }; - let error_res = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(error_res, PaymentError::NoFunds {}.into()); -} - -#[test] -fn liquidate_if_many_coins_sent() { - let TestSuite { - mut deps, - .. - } = setup_test(); - - let env = mock_env(MockEnvParams::default()); - let info = mock_info("liquidator", &[coin(100, "somecoin1"), coin(200, "somecoin2")]); - let msg = ExecuteMsg::Liquidate { - user: "user".to_string(), - collateral_denom: "collateral".to_string(), - recipient: None, - }; - let error_res = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(error_res, PaymentError::MultipleDenoms {}.into()); -} - -#[test] -fn liquidate_if_no_collateral() { - let TestSuite { - mut deps, - collateral_coin, - debt_coin, - .. - } = setup_test(); - - let liquidate_msg = ExecuteMsg::Liquidate { - user: "user".to_string(), - collateral_denom: collateral_coin.denom, - recipient: None, - }; - - let env = mock_env(MockEnvParams::default()); - let info = mock_info("liquidator", &coins(400_000_u128, debt_coin.denom)); - let error_res = execute(deps.as_mut(), env, info, liquidate_msg).unwrap_err(); - assert_eq!(error_res, ContractError::CannotLiquidateWhenNoCollateralBalance {}); -} - -#[test] -fn liquidate_if_only_uncollateralized_debt_exists() { - let TestSuite { - mut deps, - collateral_coin, - debt_coin, - uncollateralized_denom, - collateral_market, - .. - } = setup_test(); - - let user_addr = Addr::unchecked("user"); - - set_collateral( - deps.as_mut(), - &user_addr, - &collateral_market.denom, - Uint128::new(2_000_000), - true, - ); - set_debt(deps.as_mut(), &user_addr, uncollateralized_denom, Uint128::new(10_000), true); - - let liquidate_msg = ExecuteMsg::Liquidate { - user: user_addr.to_string(), - collateral_denom: collateral_coin.denom, - recipient: None, - }; - - let env = mock_env(MockEnvParams::default()); - let info = mock_info("liquidator", &coins(400_000_u128, debt_coin.denom)); - // trying to liquidate user with zero outstanding debt should fail (uncollateralized has not impact) - let error_res = execute(deps.as_mut(), env, info, liquidate_msg).unwrap_err(); - assert_eq!(error_res, ContractError::CannotLiquidateWhenNoDebtBalance {}); -} - -#[test] -fn liquidate_partially() { - let mut ts = setup_test(); - - let user_addr = Addr::unchecked("user"); - let liquidator_addr = Addr::unchecked("liquidator"); - - let user_collateral_scaled_before = Uint128::from(2_000_000u64) * SCALING_FACTOR; - let user_debt_scaled_before = compute_scaled_amount( - Uint128::from(3_000_000u64), - ts.debt_market.borrow_index, - ScalingOperation::Ceil, - ) - .unwrap(); - - set_collateral( - ts.deps.as_mut(), - &user_addr, - &ts.collateral_market.denom, - user_collateral_scaled_before, - true, - ); - set_debt(ts.deps.as_mut(), &user_addr, &ts.debt_market.denom, user_debt_scaled_before, false); - set_debt( - ts.deps.as_mut(), - &user_addr, - ts.uncollateralized_denom, - Uint128::new(10_000) * SCALING_FACTOR, - true, - ); - - let liquidate_msg = ExecuteMsg::Liquidate { - user: user_addr.to_string(), - collateral_denom: ts.collateral_market.denom.clone(), - recipient: None, - }; - - let debt_to_repay = Uint128::from(400_000_u64); - let block_time = 15_000_000; - let env = mock_env_at_block_time(block_time); - let info = mock_info( - liquidator_addr.as_str(), - &coins(debt_to_repay.u128(), ts.debt_market.denom.clone()), - ); - let res = execute(ts.deps.as_mut(), env, info, liquidate_msg).unwrap(); - - let TestExpectedAmountResults { - user_debt_repayed, - user_debt_repayed_scaled, - expected_liquidated_collateral_amount, - expected_liquidated_collateral_amount_scaled, - expected_reward_amount_scaled, - expected_debt_rates, - .. - } = expected_amounts(block_time, user_debt_scaled_before, debt_to_repay, &ts); - - let expected_msgs = expected_messages( - &user_addr, - &liquidator_addr, - user_collateral_scaled_before, - Uint128::zero(), - &ts.collateral_market, - &ts.debt_market, - ); - assert_eq!(res.messages, expected_msgs); - - mars_testing::assert_eq_vec( - res.attributes, - vec![ - attr("action", "liquidate"), - attr("user", user_addr.as_str()), - attr("liquidator", liquidator_addr.as_str()), - attr("recipient", liquidator_addr.as_str()), - attr("collateral_denom", ts.collateral_market.denom.as_str()), - attr("collateral_amount", expected_liquidated_collateral_amount), - attr("collateral_amount_scaled", expected_liquidated_collateral_amount_scaled), - attr("debt_denom", ts.debt_market.denom.as_str()), - attr("debt_amount", user_debt_repayed), - attr("debt_amount_scaled", user_debt_repayed_scaled), - ], - ); - assert_eq!( - res.events, - vec![th_build_interests_updated_event(&ts.debt_market.denom, &expected_debt_rates)] - ); - - let debt_market_after = MARKETS.load(&ts.deps.storage, &ts.debt_market.denom).unwrap(); - - // user's collateral scaled amount should have been correctly decreased - let collateral = COLLATERALS - .load(ts.deps.as_ref().storage, (&user_addr, &ts.collateral_market.denom)) - .unwrap(); - assert_eq!( - collateral.amount_scaled, - user_collateral_scaled_before - expected_liquidated_collateral_amount_scaled - ); - - // liquidator's collateral scaled amount should have been correctly increased - let collateral = COLLATERALS - .load(ts.deps.as_ref().storage, (&liquidator_addr, &ts.collateral_market.denom)) - .unwrap(); - assert_eq!(collateral.amount_scaled, expected_liquidated_collateral_amount_scaled); - - // check user's debt decreased by the appropriate amount - let debt = DEBTS.load(&ts.deps.storage, (&user_addr, &ts.debt_market.denom)).unwrap(); - assert_eq!(debt.amount_scaled, user_debt_scaled_before - user_debt_repayed_scaled); - - // check global debt decreased by the appropriate amount - assert_eq!( - debt_market_after.debt_total_scaled, - ts.debt_market.debt_total_scaled - user_debt_repayed_scaled - ); - - // rewards collector's collateral scaled amount **of the debt asset** should have been correctly increased - let collateral = rewards_collector_collateral(ts.deps.as_ref(), &ts.debt_market.denom); - assert_eq!(collateral.amount_scaled, expected_reward_amount_scaled); - - // global collateral scaled amount **of the debt asset** should have been correctly increased - assert_eq!( - debt_market_after.collateral_total_scaled, - ts.debt_market.collateral_total_scaled + expected_reward_amount_scaled - ); -} - -#[test] -fn liquidate_up_to_close_factor_with_refund() { - let mut ts = setup_test(); - - let user_addr = Addr::unchecked("user"); - let liquidator_addr = Addr::unchecked("liquidator"); - - let user_collateral_scaled_before = Uint128::from(2_000_000u64) * SCALING_FACTOR; - let user_debt_scaled_before = compute_scaled_amount( - Uint128::from(3_000_000u64), - ts.debt_market.borrow_index, - ScalingOperation::Ceil, - ) - .unwrap(); - - set_collateral( - ts.deps.as_mut(), - &user_addr, - &ts.collateral_market.denom, - user_collateral_scaled_before, - true, - ); - set_debt(ts.deps.as_mut(), &user_addr, &ts.debt_market.denom, user_debt_scaled_before, false); - - let liquidate_msg = ExecuteMsg::Liquidate { - user: user_addr.to_string(), - collateral_denom: ts.collateral_market.denom.clone(), - recipient: None, - }; - - let debt_to_repay = Uint128::from(10_000_000_u64); - let block_time = 16_000_000; - let env = mock_env_at_block_time(block_time); - let info = mock_info( - liquidator_addr.as_str(), - &coins(debt_to_repay.u128(), ts.debt_market.denom.clone()), - ); - let res = execute(ts.deps.as_mut(), env, info, liquidate_msg).unwrap(); - - let TestExpectedAmountResults { - user_debt_repayed, - user_debt_repayed_scaled, - expected_refund_amount, - expected_liquidated_collateral_amount, - expected_liquidated_collateral_amount_scaled, - expected_reward_amount_scaled, - expected_debt_rates, - .. - } = expected_amounts(block_time, user_debt_scaled_before, debt_to_repay, &ts); - - let mut expected_msgs = expected_messages( - &user_addr, - &liquidator_addr, - user_collateral_scaled_before, - Uint128::zero(), - &ts.collateral_market, - &ts.debt_market, - ); - expected_msgs.push(SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: liquidator_addr.to_string(), - amount: coins(expected_refund_amount.u128(), ts.debt_market.denom.clone()), - }))); - assert_eq!(res.messages, expected_msgs); - - mars_testing::assert_eq_vec( - vec![ - attr("action", "liquidate"), - attr("user", user_addr.as_str()), - attr("liquidator", liquidator_addr.as_str()), - attr("recipient", liquidator_addr.as_str()), - attr("collateral_denom", ts.collateral_market.denom.as_str()), - attr("collateral_amount", expected_liquidated_collateral_amount), - attr("collateral_amount_scaled", expected_liquidated_collateral_amount_scaled), - attr("debt_denom", ts.debt_market.denom.as_str()), - attr("debt_amount", user_debt_repayed), - attr("debt_amount_scaled", user_debt_repayed_scaled), - ], - res.attributes, - ); - assert_eq!( - res.events, - vec![th_build_interests_updated_event(&ts.debt_market.denom, &expected_debt_rates)], - ); - - let debt_market_after = MARKETS.load(&ts.deps.storage, &ts.debt_market.denom).unwrap(); - - // user's collateral scaled amount should have been correctly decreased - let collateral = COLLATERALS - .load(ts.deps.as_ref().storage, (&user_addr, &ts.collateral_market.denom)) - .unwrap(); - assert_eq!( - collateral.amount_scaled, - user_collateral_scaled_before - expected_liquidated_collateral_amount_scaled - ); - - // liquidator's collateral scaled amount should have been correctly increased - let collateral = COLLATERALS - .load(ts.deps.as_ref().storage, (&liquidator_addr, &ts.collateral_market.denom)) - .unwrap(); - assert_eq!(collateral.amount_scaled, expected_liquidated_collateral_amount_scaled); - - // check user's debt decreased by the appropriate amount - let debt = DEBTS.load(&ts.deps.storage, (&user_addr, &ts.debt_market.denom)).unwrap(); - assert_eq!(debt.amount_scaled, user_debt_scaled_before - expected_debt_rates.less_debt_scaled); - - // check global debt decreased by the appropriate amount - assert_eq!( - debt_market_after.debt_total_scaled, - ts.debt_market.debt_total_scaled - expected_debt_rates.less_debt_scaled - ); - - // rewards collector's collateral scaled amount **of the debt asset** should have been correctly increased - let collateral = rewards_collector_collateral(ts.deps.as_ref(), &ts.debt_market.denom); - assert_eq!(collateral.amount_scaled, expected_reward_amount_scaled); - - // global collateral scaled amount **of the debt asset** should have been correctly increased - assert_eq!( - debt_market_after.collateral_total_scaled, - ts.debt_market.collateral_total_scaled + expected_reward_amount_scaled - ); -} - -#[test] -fn liquidate_fully() { - let TestSuite { - mut deps, - collateral_price, - debt_price, - collateral_market, - debt_market, - .. - } = setup_test(); - - let user_addr = Addr::unchecked("user"); - let liquidator_addr = Addr::unchecked("liquidator"); - - let user_collateral_scaled_before = Uint128::new(100) * SCALING_FACTOR; - let user_debt_scaled_before = Uint128::new(400) * SCALING_FACTOR; - - set_collateral( - deps.as_mut(), - &user_addr, - &collateral_market.denom, - user_collateral_scaled_before, - true, - ); - set_debt(deps.as_mut(), &user_addr, &debt_market.denom, user_debt_scaled_before, false); - - let liquidate_msg = ExecuteMsg::Liquidate { - user: user_addr.to_string(), - collateral_denom: collateral_market.denom.clone(), - recipient: None, - }; - - let debt_to_repay = Uint128::from(300u128); - let block_time = 16_000_000; - let env = mock_env_at_block_time(block_time); - let info = mock_info( - liquidator_addr.as_str(), - &coins(debt_to_repay.u128(), debt_market.denom.clone()), - ); - let res = execute(deps.as_mut(), env, info, liquidate_msg).unwrap(); - - // get expected indices and rates for debt and collateral markets - let expected_collateral_indices = th_get_expected_indices(&collateral_market, block_time); - let user_collateral_balance = compute_underlying_amount( - user_collateral_scaled_before, - expected_collateral_indices.liquidity, - ScalingOperation::Truncate, - ) - .unwrap(); - - // Since debt is being over_repayed, we expect to liquidate total collateral - let expected_less_debt = math::divide_uint128_by_decimal( - math::divide_uint128_by_decimal(collateral_price * user_collateral_balance, debt_price) - .unwrap(), - Decimal::one() + collateral_market.liquidation_bonus, - ) - .unwrap(); - - let expected_refund_amount = debt_to_repay - expected_less_debt; - - let expected_debt_rates = th_get_expected_indices_and_rates( - &debt_market, - block_time, - TestUtilizationDeltaInfo { - less_debt: expected_less_debt, - user_current_debt_scaled: user_debt_scaled_before, - less_liquidity: expected_refund_amount, - ..Default::default() - }, - ); - - let debt_market_after = MARKETS.load(&deps.storage, &debt_market.denom).unwrap(); - - // since this is a full liquidation, the full amount of user's collateral shares should have - // been transferred to the liquidator - let expected_liquidated_collateral_amount_scaled = user_collateral_scaled_before; - - let mut expected_msgs = expected_messages( - &user_addr, - &liquidator_addr, - user_collateral_scaled_before, - Uint128::zero(), - &collateral_market, - &debt_market, - ); - expected_msgs.push(SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: liquidator_addr.to_string(), - amount: coins(expected_refund_amount.u128(), debt_market.denom.clone()), - }))); - assert_eq!(res.messages, expected_msgs); - - mars_testing::assert_eq_vec( - vec![ - attr("action", "liquidate"), - attr("user", user_addr.as_str()), - attr("liquidator", liquidator_addr.as_str()), - attr("recipient", liquidator_addr.as_str()), - attr("collateral_denom", collateral_market.denom.as_str()), - attr("collateral_amount", user_collateral_balance), - attr("collateral_amount_scaled", expected_liquidated_collateral_amount_scaled), - attr("debt_denom", debt_market.denom.as_str()), - attr("debt_amount", expected_less_debt), - attr("debt_amount_scaled", expected_debt_rates.less_debt_scaled), - ], - res.attributes, - ); - assert_eq!( - res.events, - vec![th_build_interests_updated_event(&debt_market.denom, &expected_debt_rates)], - ); - - // since this is a full liquidation, the user's collateral position should have been deleted - assert!(!has_collateral_position(deps.as_ref(), &user_addr, &collateral_market.denom)); - - // liquidator's collateral scaled amount should have been correctly increased - let collateral = COLLATERALS - .load(deps.as_ref().storage, (&liquidator_addr, &collateral_market.denom)) - .unwrap(); - assert_eq!(collateral.amount_scaled, expected_liquidated_collateral_amount_scaled); - - // check user's debt decreased by the appropriate amount - let debt = DEBTS.load(&deps.storage, (&user_addr, &debt_market.denom)).unwrap(); - assert_eq!(debt.amount_scaled, user_debt_scaled_before - expected_debt_rates.less_debt_scaled); - - // check global debt decreased by the appropriate amount - assert_eq!( - debt_market_after.debt_total_scaled, - debt_market.debt_total_scaled - expected_debt_rates.less_debt_scaled - ); -} - -/// FIXME: new clippy version warns to remove clone() from "collateral_market.clone()" but then it breaks compilation -#[allow(clippy::redundant_clone)] -#[test] -fn liquidate_partially_if_same_asset_for_debt_and_collateral() { - let TestSuite { - mut deps, - collateral_price, - collateral_market, - .. - } = setup_test(); - let debt_price = collateral_price; - - let user_addr = Addr::unchecked("user"); - let liquidator_addr = Addr::unchecked("liquidator"); - - let user_collateral_scaled_before = Uint128::from(2_000_000u64) * SCALING_FACTOR; - let user_debt_scaled_before = compute_scaled_amount( - Uint128::from(3_000_000u64), - collateral_market.borrow_index, - ScalingOperation::Ceil, - ) - .unwrap(); - - set_collateral( - deps.as_mut(), - &user_addr, - &collateral_market.denom, - user_collateral_scaled_before, - true, - ); - set_debt(deps.as_mut(), &user_addr, &collateral_market.denom, user_debt_scaled_before, false); - - let liquidate_msg = ExecuteMsg::Liquidate { - user: user_addr.to_string(), - collateral_denom: collateral_market.denom.clone(), - recipient: None, - }; - - let debt_to_repay = Uint128::from(400_000_u64); - let block_time = 15_000_000; - let env = mock_env_at_block_time(block_time); - let info = mock_info( - liquidator_addr.as_str(), - &coins(debt_to_repay.u128(), collateral_market.denom.clone()), - ); - let res = execute(deps.as_mut(), env.clone(), info, liquidate_msg).unwrap(); - - // get expected indices and rates for debt market - let expected_debt_rates = th_get_expected_indices_and_rates( - &collateral_market, - block_time, - TestUtilizationDeltaInfo { - less_debt: debt_to_repay, - user_current_debt_scaled: user_debt_scaled_before, - ..Default::default() - }, - ); - - let collateral_market_after = MARKETS.load(&deps.storage, &collateral_market.denom).unwrap(); - let debt_market_after = MARKETS.load(&deps.storage, &collateral_market.denom).unwrap(); - - let expected_liquidated_collateral_amount = math::divide_uint128_by_decimal( - debt_to_repay * debt_price * (Decimal::one() + collateral_market.liquidation_bonus), - collateral_price, - ) - .unwrap(); - - let expected_liquidated_collateral_amount_scaled = get_scaled_liquidity_amount( - expected_liquidated_collateral_amount, - &collateral_market_after, - env.block.time.seconds(), - ) - .unwrap(); - - let expected_reward_amount_scaled = compute_scaled_amount( - expected_debt_rates.protocol_rewards_to_distribute, - expected_debt_rates.liquidity_index, - ScalingOperation::Truncate, - ) - .unwrap(); - - let expected_msgs = expected_messages( - &user_addr, - &liquidator_addr, - user_collateral_scaled_before, - Uint128::zero(), - &collateral_market, - &collateral_market, - ); - assert_eq!(res.messages, expected_msgs); - - mars_testing::assert_eq_vec( - res.attributes, - vec![ - attr("action", "liquidate"), - attr("user", user_addr.as_str()), - attr("liquidator", liquidator_addr.as_str()), - attr("recipient", liquidator_addr.as_str()), - attr("collateral_denom", collateral_market.denom.as_str()), - attr("collateral_amount", expected_liquidated_collateral_amount), - attr("collateral_amount_scaled", expected_liquidated_collateral_amount_scaled), - attr("debt_denom", collateral_market.denom.as_str()), - attr("debt_amount", debt_to_repay), - attr("debt_amount_scaled", expected_debt_rates.less_debt_scaled), - ], - ); - assert_eq!( - res.events, - vec![th_build_interests_updated_event(&collateral_market.denom, &expected_debt_rates)] - ); - - // user's collateral scaled amount should have been correctly decreased - let collateral = - COLLATERALS.load(deps.as_ref().storage, (&user_addr, &collateral_market.denom)).unwrap(); - assert_eq!( - collateral.amount_scaled, - user_collateral_scaled_before - expected_liquidated_collateral_amount_scaled - ); - - // liquidator's collateral scaled amount should have been correctly increased - let collateral = COLLATERALS - .load(deps.as_ref().storage, (&liquidator_addr, &collateral_market.denom)) - .unwrap(); - assert_eq!(collateral.amount_scaled, expected_liquidated_collateral_amount_scaled); - - // check user's debt decreased by the appropriate amount - let debt = DEBTS.load(&deps.storage, (&user_addr, &collateral_market.denom)).unwrap(); - assert_eq!(debt.amount_scaled, user_debt_scaled_before - expected_debt_rates.less_debt_scaled); - - // check global debt decreased by the appropriate amount - assert_eq!( - debt_market_after.debt_total_scaled, - collateral_market.debt_total_scaled - expected_debt_rates.less_debt_scaled - ); - - // rewards collector's collateral scaled amount **of the debt asset** should have been correctly increased - let collateral = rewards_collector_collateral(deps.as_ref(), &collateral_market.denom); - assert_eq!(collateral.amount_scaled, expected_reward_amount_scaled); - - // global collateral scaled amount **of the debt asset** should have been correctly increased - assert_eq!( - debt_market_after.collateral_total_scaled, - collateral_market.collateral_total_scaled + expected_reward_amount_scaled - ); -} - -/// FIXME: new clippy version warns to remove clone() from "collateral_market.clone()" but then it breaks compilation -#[allow(clippy::redundant_clone)] -#[test] -fn liquidate_with_refund_if_same_asset_for_debt_and_collateral() { - let TestSuite { - mut deps, - collateral_price, - close_factor, - collateral_market, - .. - } = setup_test(); - let debt_price = collateral_price; - - let user_addr = Addr::unchecked("user"); - let liquidator_addr = Addr::unchecked("liquidator"); - - let user_collateral_scaled_before = Uint128::from(2_000_000u64) * SCALING_FACTOR; - let user_debt_scaled_before = compute_scaled_amount( - Uint128::from(3_000_000u64), - collateral_market.borrow_index, - ScalingOperation::Ceil, - ) - .unwrap(); - - set_collateral( - deps.as_mut(), - &user_addr, - &collateral_market.denom, - user_collateral_scaled_before, - true, - ); - set_debt(deps.as_mut(), &user_addr, &collateral_market.denom, user_debt_scaled_before, false); - - let liquidate_msg = ExecuteMsg::Liquidate { - user: user_addr.to_string(), - collateral_denom: collateral_market.denom.clone(), - recipient: None, - }; - - let debt_to_repay = Uint128::from(10_000_000_u64); - let block_time = 16_000_000; - let env = mock_env_at_block_time(block_time); - let info = mock_info( - liquidator_addr.as_str(), - &coins(debt_to_repay.u128(), collateral_market.denom.clone()), - ); - let res = execute(deps.as_mut(), env, info, liquidate_msg).unwrap(); - - // get expected indices and rates for debt and collateral markets - let expected_debt_indices = th_get_expected_indices(&collateral_market, block_time); - let user_debt_asset_total_debt = compute_underlying_amount( - user_debt_scaled_before, - expected_debt_indices.borrow, - ScalingOperation::Ceil, - ) - .unwrap(); - // since debt is being over_repayed, we expect to max out the liquidatable debt - let expected_less_debt = user_debt_asset_total_debt * close_factor; - - let expected_refund_amount = debt_to_repay - expected_less_debt; - - let expected_debt_rates = th_get_expected_indices_and_rates( - &collateral_market, - block_time, - TestUtilizationDeltaInfo { - less_debt: expected_less_debt, - user_current_debt_scaled: user_debt_scaled_before, - less_liquidity: expected_refund_amount, - ..Default::default() - }, - ); - - let expected_liquidated_collateral_amount = math::divide_uint128_by_decimal( - expected_less_debt * debt_price * (Decimal::one() + collateral_market.liquidation_bonus), - collateral_price, - ) - .unwrap(); - - let expected_collateral_rates = th_get_expected_indices_and_rates( - &collateral_market, - block_time, - TestUtilizationDeltaInfo { - less_liquidity: expected_liquidated_collateral_amount, - ..Default::default() - }, - ); - - let debt_market_after = MARKETS.load(&deps.storage, &collateral_market.denom).unwrap(); - - let expected_liquidated_collateral_amount_scaled = compute_scaled_amount( - expected_liquidated_collateral_amount, - expected_collateral_rates.liquidity_index, - ScalingOperation::Truncate, - ) - .unwrap(); - - let expected_reward_amount_scaled = compute_scaled_amount( - expected_debt_rates.protocol_rewards_to_distribute, - expected_debt_rates.liquidity_index, - ScalingOperation::Truncate, - ) - .unwrap(); - - let mut expected_msgs = expected_messages( - &user_addr, - &liquidator_addr, - user_collateral_scaled_before, - Uint128::zero(), - &collateral_market, - &collateral_market, - ); - expected_msgs.push(SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: liquidator_addr.to_string(), - amount: coins(expected_refund_amount.u128(), collateral_market.denom.clone()), - }))); - assert_eq!(res.messages, expected_msgs); - - mars_testing::assert_eq_vec( - vec![ - attr("action", "liquidate"), - attr("user", user_addr.as_str()), - attr("liquidator", liquidator_addr.as_str()), - attr("recipient", liquidator_addr.as_str()), - attr("collateral_denom", collateral_market.denom.as_str()), - attr("collateral_amount", expected_liquidated_collateral_amount), - attr("collateral_amount_scaled", expected_liquidated_collateral_amount_scaled), - attr("debt_denom", collateral_market.denom.as_str()), - attr("debt_amount", expected_less_debt), - attr("debt_amount_scaled", expected_debt_rates.less_debt_scaled), - ], - res.attributes, - ); - assert_eq!( - res.events, - vec![th_build_interests_updated_event(&collateral_market.denom, &expected_debt_rates)], - ); - - // user's collateral scaled amount should have been correctly decreased - let collateral = - COLLATERALS.load(deps.as_ref().storage, (&user_addr, &collateral_market.denom)).unwrap(); - assert_eq!( - collateral.amount_scaled, - user_collateral_scaled_before - expected_liquidated_collateral_amount_scaled - ); - - // liquidator's collateral scaled amount should have been correctly increased - let collateral = COLLATERALS - .load(deps.as_ref().storage, (&liquidator_addr, &collateral_market.denom)) - .unwrap(); - assert_eq!(collateral.amount_scaled, expected_liquidated_collateral_amount_scaled); - - // check user's debt decreased by the appropriate amount - let debt = DEBTS.load(&deps.storage, (&user_addr, &collateral_market.denom)).unwrap(); - assert_eq!(debt.amount_scaled, user_debt_scaled_before - expected_debt_rates.less_debt_scaled); - - // check global debt decreased by the appropriate amount - assert_eq!( - debt_market_after.debt_total_scaled, - collateral_market.debt_total_scaled - expected_debt_rates.less_debt_scaled - ); - - // rewards collector's collateral scaled amount **of the debt asset** should have been correctly increased - let collateral = rewards_collector_collateral(deps.as_ref(), &collateral_market.denom); - assert_eq!(collateral.amount_scaled, expected_reward_amount_scaled); - - // global collateral scaled amount **of the debt asset** should have been correctly increased - assert_eq!( - debt_market_after.collateral_total_scaled, - collateral_market.collateral_total_scaled + expected_reward_amount_scaled - ); -} - -#[test] -fn liquidate_with_recipient_for_underlying_collateral() { - let mut ts = setup_test(); - - let user_addr = Addr::unchecked("user"); - let liquidator_addr = Addr::unchecked("liquidator"); - let recipient_addr = Addr::unchecked("recipient"); - - let user_collateral_scaled_before = Uint128::from(2_000_000u64) * SCALING_FACTOR; - let user_debt_scaled_before = compute_scaled_amount( - Uint128::from(3_000_000u64), - ts.debt_market.borrow_index, - ScalingOperation::Ceil, - ) - .unwrap(); - - set_collateral( - ts.deps.as_mut(), - &user_addr, - &ts.collateral_market.denom, - user_collateral_scaled_before, - true, - ); - set_debt(ts.deps.as_mut(), &user_addr, &ts.debt_market.denom, user_debt_scaled_before, false); - - let liquidate_msg = ExecuteMsg::Liquidate { - user: user_addr.to_string(), - collateral_denom: ts.collateral_market.denom.clone(), - recipient: Some(recipient_addr.to_string()), - }; - - let debt_to_repay = Uint128::from(10_000_000_u64); - let block_time = 16_000_000; - let env = mock_env_at_block_time(block_time); - let info = mock_info( - liquidator_addr.as_str(), - &coins(debt_to_repay.u128(), ts.debt_market.denom.clone()), - ); - let res = execute(ts.deps.as_mut(), env, info, liquidate_msg).unwrap(); - - let TestExpectedAmountResults { - user_debt_repayed, - user_debt_repayed_scaled, - expected_refund_amount, - expected_liquidated_collateral_amount, - expected_liquidated_collateral_amount_scaled, - expected_reward_amount_scaled, - expected_debt_rates, - .. - } = expected_amounts(block_time, user_debt_scaled_before, debt_to_repay, &ts); - - let mut expected_msgs = expected_messages( - &user_addr, - &recipient_addr, - user_collateral_scaled_before, - Uint128::zero(), - &ts.collateral_market, - &ts.debt_market, - ); - expected_msgs.push(SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: liquidator_addr.to_string(), - amount: coins(expected_refund_amount.u128(), ts.debt_market.denom.clone()), - }))); - assert_eq!(res.messages, expected_msgs); - - mars_testing::assert_eq_vec( - vec![ - attr("action", "liquidate"), - attr("user", user_addr.as_str()), - attr("liquidator", liquidator_addr.as_str()), - attr("recipient", recipient_addr.as_str()), - attr("collateral_denom", ts.collateral_market.denom.as_str()), - attr("collateral_amount", expected_liquidated_collateral_amount), - attr("collateral_amount_scaled", expected_liquidated_collateral_amount_scaled), - attr("debt_denom", ts.debt_market.denom.as_str()), - attr("debt_amount", user_debt_repayed), - attr("debt_amount_scaled", user_debt_repayed_scaled), - ], - res.attributes, - ); - assert_eq!( - res.events, - vec![th_build_interests_updated_event(&ts.debt_market.denom, &expected_debt_rates)], - ); - - let debt_market_after = MARKETS.load(&ts.deps.storage, &ts.debt_market.denom).unwrap(); - - // user's collateral scaled amount should have been correctly decreased - let collateral = COLLATERALS - .load(ts.deps.as_ref().storage, (&user_addr, &ts.collateral_market.denom)) - .unwrap(); - assert_eq!( - collateral.amount_scaled, - user_collateral_scaled_before - expected_liquidated_collateral_amount_scaled - ); - - // liquidator's collateral should be empty - COLLATERALS - .load(ts.deps.as_ref().storage, (&liquidator_addr, &ts.collateral_market.denom)) - .unwrap_err(); - - // recipient's collateral scaled amount should have been correctly increased - let collateral = COLLATERALS - .load(ts.deps.as_ref().storage, (&recipient_addr, &ts.collateral_market.denom)) - .unwrap(); - assert_eq!(collateral.amount_scaled, expected_liquidated_collateral_amount_scaled); - - // check user's debt decreased by the appropriate amount - let debt = DEBTS.load(&ts.deps.storage, (&user_addr, &ts.debt_market.denom)).unwrap(); - assert_eq!(debt.amount_scaled, user_debt_scaled_before - expected_debt_rates.less_debt_scaled); - - // check global debt decreased by the appropriate amount - assert_eq!( - debt_market_after.debt_total_scaled, - ts.debt_market.debt_total_scaled - expected_debt_rates.less_debt_scaled - ); - - // rewards collector's collateral scaled amount **of the debt asset** should have been correctly increased - let collateral = rewards_collector_collateral(ts.deps.as_ref(), &ts.debt_market.denom); - assert_eq!(collateral.amount_scaled, expected_reward_amount_scaled); - - // global collateral scaled amount **of the debt asset** should have been correctly increased - assert_eq!( - debt_market_after.collateral_total_scaled, - ts.debt_market.collateral_total_scaled + expected_reward_amount_scaled - ); -} - -#[test] -fn liquidation_health_factor_check() { - // initialize collateral and debt markets - let available_liquidity_collateral = Uint128::from(1000000000u128); - let available_liquidity_debt = Uint128::from(2000000000u128); - let mut deps = th_setup(&[ - coin(available_liquidity_collateral.into(), "collateral"), - coin(available_liquidity_debt.into(), "debt"), - ]); - - deps.querier.set_oracle_price("collateral", Decimal::one()); - deps.querier.set_oracle_price("debt", Decimal::one()); - deps.querier.set_oracle_price("uncollateralized_debt", Decimal::one()); - - let collateral_ltv = Decimal::from_ratio(5u128, 10u128); - let collateral_liquidation_threshold = Decimal::from_ratio(7u128, 10u128); - let collateral_liquidation_bonus = Decimal::from_ratio(1u128, 10u128); - - let collateral_market = Market { - max_loan_to_value: collateral_ltv, - liquidation_threshold: collateral_liquidation_threshold, - liquidation_bonus: collateral_liquidation_bonus, - debt_total_scaled: Uint128::zero(), - liquidity_index: Decimal::one(), - borrow_index: Decimal::one(), - ..Default::default() - }; - let debt_market = Market { - max_loan_to_value: Decimal::from_ratio(6u128, 10u128), - debt_total_scaled: Uint128::new(20_000_000) * SCALING_FACTOR, - liquidity_index: Decimal::one(), - borrow_index: Decimal::one(), - ..Default::default() - }; - let uncollateralized_debt_market = Market { - denom: "uncollateralized_debt".to_string(), - ..Default::default() - }; - - // initialize markets - th_init_market(deps.as_mut(), "collateral", &collateral_market); - th_init_market(deps.as_mut(), "debt", &debt_market); - th_init_market(deps.as_mut(), "uncollateralized_debt", &uncollateralized_debt_market); - - // test health factor check - let healthy_user_addr = Addr::unchecked("healthy_user"); - - // set initial collateral and debt balances for user - let healthy_user_collateral_balance_scaled = Uint128::new(10_000_000) * SCALING_FACTOR; - set_collateral( - deps.as_mut(), - &healthy_user_addr, - "collateral", - healthy_user_collateral_balance_scaled, - true, - ); - - let healthy_user_debt_amount_scaled = - Uint128::new(healthy_user_collateral_balance_scaled.u128()) - * collateral_liquidation_threshold; - let healthy_user_debt = Debt { - amount_scaled: healthy_user_debt_amount_scaled, - uncollateralized: false, - }; - let uncollateralized_debt = Debt { - amount_scaled: Uint128::new(10_000) * SCALING_FACTOR, - uncollateralized: true, - }; - DEBTS.save(deps.as_mut().storage, (&healthy_user_addr, "debt"), &healthy_user_debt).unwrap(); - DEBTS - .save( - deps.as_mut().storage, - (&healthy_user_addr, "uncollateralized_debt"), - &uncollateralized_debt, - ) - .unwrap(); - - // perform liquidation (should fail because health factor is > 1) - let liquidator_addr = Addr::unchecked("liquidator"); - let debt_to_cover = Uint128::from(1_000_000u64); - - let liquidate_msg = ExecuteMsg::Liquidate { - user: healthy_user_addr.to_string(), - collateral_denom: "collateral".to_string(), - recipient: None, - }; - - let env = mock_env(MockEnvParams::default()); - let info = mock_info(liquidator_addr.as_str(), &coins(debt_to_cover.u128(), "debt")); - let error_res = execute(deps.as_mut(), env, info, liquidate_msg).unwrap_err(); - assert_eq!(error_res, ContractError::CannotLiquidateHealthyPosition {}); -} - -#[test] -fn liquidate_if_collateral_disabled() { - // initialize collateral and debt markets - let mut deps = th_setup(&[]); - - let collateral_market_1 = Market { - ..Default::default() - }; - let collateral_market_2 = Market { - ..Default::default() - }; - let debt_market = Market { - ..Default::default() - }; - - // initialize markets - th_init_market(deps.as_mut(), "collateral1", &collateral_market_1); - th_init_market(deps.as_mut(), "collateral2", &collateral_market_2); - th_init_market(deps.as_mut(), "debt", &debt_market); - - // Set user as having collateral and debt in respective markets - let user_addr = Addr::unchecked("user"); - set_collateral(deps.as_mut(), &user_addr, "collateral1", Uint128::new(123), true); - set_collateral(deps.as_mut(), &user_addr, "collateral2", Uint128::new(123), false); - - // perform liquidation (should fail because collateral2 isn't set as collateral for user) - let liquidator_addr = Addr::unchecked("liquidator"); - let debt_to_cover = Uint128::from(1_000_000u64); - - let liquidate_msg = ExecuteMsg::Liquidate { - user: user_addr.to_string(), - collateral_denom: "collateral2".to_string(), - recipient: None, - }; - - let env = mock_env(MockEnvParams::default()); - let info = mock_info(liquidator_addr.as_str(), &coins(debt_to_cover.u128(), "debt")); - let error_res = execute(deps.as_mut(), env, info, liquidate_msg).unwrap_err(); - assert_eq!( - error_res, - ContractError::CannotLiquidateWhenCollateralUnset { - denom: "collateral2".to_string() - } - ); -} - -#[test] -fn liquidator_cannot_receive_collaterals_without_spending_coins() { - let market = Market { - liquidity_index: Decimal::one(), - liquidation_bonus: Decimal::from_ratio(1u128, 10u128), - ..Default::default() - }; - let res_err = liquidation_compute_amounts( - Uint128::new(320000000), - Uint128::new(800), - Uint128::new(2), - &market, - Decimal::one(), - Decimal::from_ratio(300u128, 1u128), - 0, - Decimal::from_ratio(1u128, 2u128), - ) - .unwrap_err(); - assert_eq!(res_err, StdError::generic_err("Can't process liquidation. Invalid collateral_amount_to_liquidate (320) and debt_amount_to_repay (0)")) -} - -#[test] -fn cannot_liquidate_without_receiving_collaterals() { - let market = Market { - liquidity_index: Decimal::one(), - liquidation_bonus: Decimal::from_ratio(1u128, 10u128), - ..Default::default() - }; - let res_err = liquidation_compute_amounts( - Uint128::new(320000000), - Uint128::new(20), - Uint128::new(30), - &market, - Decimal::from_ratio(12u128, 1u128), - Decimal::one(), - 0, - Decimal::from_ratio(1u128, 2u128), - ) - .unwrap_err(); - assert_eq!(res_err, StdError::generic_err("Can't process liquidation. Invalid collateral_amount_to_liquidate (0) and debt_amount_to_repay (10)")) -} diff --git a/contracts/red-bank/tests/test_update_emergency_owner.rs b/contracts/red-bank/tests/test_update_emergency_owner.rs deleted file mode 100644 index c984b83d9..000000000 --- a/contracts/red-bank/tests/test_update_emergency_owner.rs +++ /dev/null @@ -1,75 +0,0 @@ -use cosmwasm_std::testing::{mock_env, mock_info}; -use mars_owner::{OwnerError::NotOwner, OwnerUpdate}; -use mars_red_bank::{contract::execute, error::ContractError}; -use mars_red_bank_types::red_bank::{ConfigResponse, ExecuteMsg, QueryMsg}; - -use crate::helpers::{th_query, th_setup}; - -mod helpers; - -#[test] -fn initialized_state() { - let deps = th_setup(&[]); - - let config: ConfigResponse = th_query(deps.as_ref(), QueryMsg::Config {}); - assert!(config.emergency_owner.is_none()); -} - -#[test] -fn only_owner_can_set_emergency_owner() { - let mut deps = th_setup(&[]); - - // only admin can propose new admins - let bad_guy = "bad_guy"; - let err = execute( - deps.as_mut(), - mock_env(), - mock_info(bad_guy, &[]), - ExecuteMsg::UpdateOwner(OwnerUpdate::SetEmergencyOwner { - emergency_owner: "new_emergency_owner".to_string(), - }), - ) - .unwrap_err(); - assert_eq!(err, ContractError::Owner(NotOwner {})); -} - -#[test] -fn set_and_clear_emergency_owner() { - let mut deps = th_setup(&[]); - - let original_config: ConfigResponse = th_query(deps.as_ref(), QueryMsg::Config {}); - - let emergency_owner = "new_emergency_owner"; - - execute( - deps.as_mut(), - mock_env(), - mock_info(&original_config.owner.clone().unwrap(), &[]), - ExecuteMsg::UpdateOwner(OwnerUpdate::SetEmergencyOwner { - emergency_owner: emergency_owner.to_string(), - }), - ) - .unwrap(); - - let new_config: ConfigResponse = th_query(deps.as_ref(), QueryMsg::Config {}); - - assert_eq!(new_config.owner, original_config.owner); - assert_eq!(new_config.proposed_new_owner, original_config.proposed_new_owner); - assert_eq!(new_config.emergency_owner, Some(emergency_owner.to_string())); - - // clear emergency owner - - execute( - deps.as_mut(), - mock_env(), - mock_info(&original_config.owner.clone().unwrap(), &[]), - ExecuteMsg::UpdateOwner(OwnerUpdate::ClearEmergencyOwner {}), - ) - .unwrap(); - - let new_config: ConfigResponse = th_query(deps.as_ref(), QueryMsg::Config {}); - - assert_eq!(new_config.owner, original_config.owner); - assert_eq!(new_config.proposed_new_owner, original_config.proposed_new_owner); - assert_eq!(new_config.emergency_owner, None); -} diff --git a/contracts/red-bank/tests/tests/helpers/mod.rs b/contracts/red-bank/tests/tests/helpers/mod.rs new file mode 100644 index 000000000..512cd91e2 --- /dev/null +++ b/contracts/red-bank/tests/tests/helpers/mod.rs @@ -0,0 +1,433 @@ +#![allow(dead_code)] + +use std::{collections::HashMap, fmt::Display, str::FromStr}; + +use anyhow::Result as AnyResult; +use cosmwasm_schema::serde; +use cosmwasm_std::{ + from_binary, + testing::{MockApi, MockStorage}, + Addr, Coin, Decimal, Deps, DepsMut, Event, OwnedDeps, Uint128, +}; +use cw_multi_test::AppResponse; +use mars_interest_rate::{ + calculate_applied_linear_interest_rate, compute_scaled_amount, compute_underlying_amount, + ScalingOperation, +}; +use mars_params::types::asset::{AssetParams, CmSettings, LiquidationBonus, RedBankSettings}; +use mars_red_bank::{ + contract::{instantiate, query}, + error::ContractError, + state::{COLLATERALS, DEBTS, MARKETS}, +}; +use mars_red_bank_types::red_bank::{ + Collateral, CreateOrUpdateConfig, Debt, InitOrUpdateAssetParams, InstantiateMsg, + InterestRateModel, Market, QueryMsg, UserCollateralResponse, UserDebtResponse, + UserHealthStatus, UserPositionResponse, +}; +use mars_testing::{mock_dependencies, mock_env, mock_info, MarsMockQuerier, MockEnvParams}; + +pub fn set_collateral( + deps: DepsMut, + user_addr: &Addr, + denom: &str, + amount_scaled: Uint128, + enabled: bool, +) { + let collateral = Collateral { + amount_scaled, + enabled, + }; + COLLATERALS.save(deps.storage, (user_addr, "", denom), &collateral).unwrap(); +} + +pub fn unset_collateral(deps: DepsMut, user_addr: &Addr, denom: &str) { + COLLATERALS.remove(deps.storage, (user_addr, "", denom)); +} + +pub fn set_debt( + deps: DepsMut, + user_addr: &Addr, + denom: &str, + amount_scaled: impl Into, + uncollateralized: bool, +) { + let debt = Debt { + amount_scaled: amount_scaled.into(), + uncollateralized, + }; + DEBTS.save(deps.storage, (user_addr, denom), &debt).unwrap(); +} + +/// Find if a user has a debt position in the specified asset +pub fn has_debt_position(deps: Deps, user_addr: &Addr, denom: &str) -> bool { + DEBTS.may_load(deps.storage, (user_addr, denom)).unwrap().is_some() +} + +/// Find if a user has a collateral position in the specified asset, regardless of whether enabled +pub fn has_collateral_position(deps: Deps, user_addr: &Addr, denom: &str) -> bool { + COLLATERALS.may_load(deps.storage, (user_addr, "", denom)).unwrap().is_some() +} + +/// Find whether a user has a collateral position AND has it enabled in the specified asset +pub fn has_collateral_enabled(deps: Deps, user_addr: &Addr, denom: &str) -> bool { + COLLATERALS + .may_load(deps.storage, (user_addr, "", denom)) + .unwrap() + .map(|collateral| collateral.enabled) + .unwrap_or(false) +} + +pub fn th_setup(contract_balances: &[Coin]) -> OwnedDeps { + let mut deps = mock_dependencies(contract_balances); + let env = mock_env(MockEnvParams::default()); + let info = mock_info("owner"); + let config = CreateOrUpdateConfig { + address_provider: Some("address_provider".to_string()), + }; + let msg = InstantiateMsg { + owner: "owner".to_string(), + config, + }; + instantiate(deps.as_mut(), env, info, msg).unwrap(); + + deps.querier.set_oracle_price("uusd", Decimal::one()); + + deps.querier.set_target_health_factor(Decimal::from_ratio(12u128, 10u128)); + + deps +} + +pub fn th_query(deps: Deps, msg: QueryMsg) -> T { + from_binary(&query(deps, mock_env(MockEnvParams::default()), msg).unwrap()).unwrap() +} + +pub fn th_init_market(deps: DepsMut, denom: &str, market: &Market) -> Market { + let new_market = Market { + denom: denom.to_string(), + ..market.clone() + }; + + MARKETS.save(deps.storage, denom, &new_market).unwrap(); + + new_market +} + +pub fn th_default_asset_params() -> AssetParams { + AssetParams { + denom: "todo".to_string(), + credit_manager: CmSettings { + whitelisted: false, + hls: None, + }, + red_bank: RedBankSettings { + deposit_enabled: true, + borrow_enabled: true, + }, + max_loan_to_value: Decimal::zero(), + liquidation_threshold: Decimal::one(), + liquidation_bonus: LiquidationBonus { + starting_lb: Decimal::percent(0u64), + slope: Decimal::one(), + min_lb: Decimal::percent(0u64), + max_lb: Decimal::percent(5u64), + }, + protocol_liquidation_fee: Decimal::percent(2u64), + deposit_cap: Uint128::MAX, + } +} + +#[derive(Default, Debug)] +pub struct TestInterestResults { + pub borrow_index: Decimal, + pub liquidity_index: Decimal, + pub borrow_rate: Decimal, + pub liquidity_rate: Decimal, + pub protocol_rewards_to_distribute: Uint128, + pub less_debt_scaled: Uint128, +} + +pub fn th_build_interests_updated_event(denom: &str, ir: &TestInterestResults) -> Event { + Event::new("interests_updated") + .add_attribute("denom", denom) + .add_attribute("borrow_index", ir.borrow_index.to_string()) + .add_attribute("liquidity_index", ir.liquidity_index.to_string()) + .add_attribute("borrow_rate", ir.borrow_rate.to_string()) + .add_attribute("liquidity_rate", ir.liquidity_rate.to_string()) +} + +/// Deltas to be using in expected indices/rates results +#[derive(Default, Debug)] +pub struct TestUtilizationDeltaInfo { + pub less_liquidity: Uint128, + pub more_debt: Uint128, + pub less_debt: Uint128, + /// Used when passing less debt to compute deltas in the actual scaled amount + pub user_current_debt_scaled: Uint128, +} + +/// Takes a market before an action (ie: a borrow) among some test parameters +/// used in that action and computes the expected indices and rates after that action. +pub fn th_get_expected_indices_and_rates( + market: &Market, + block_time: u64, + delta_info: TestUtilizationDeltaInfo, +) -> TestInterestResults { + if !delta_info.more_debt.is_zero() && !delta_info.less_debt.is_zero() { + panic!("Cannot have more debt and less debt at the same time"); + } + + if !delta_info.less_debt.is_zero() && delta_info.user_current_debt_scaled.is_zero() { + panic!("Cannot have less debt with 0 current user debt scaled"); + } + + let expected_indices = th_get_expected_indices(market, block_time); + + let expected_protocol_rewards_to_distribute = + th_get_expected_protocol_rewards(market, &expected_indices); + + // When borrowing, new computed index is used for scaled amount + let more_debt_scaled = th_get_scaled_debt_amount(delta_info.more_debt, expected_indices.borrow); + + // When repaying, new computed index is used to get current debt and deduct amount + let less_debt_scaled = if !delta_info.less_debt.is_zero() { + let user_current_debt = th_get_underlying_debt_amount( + delta_info.user_current_debt_scaled, + expected_indices.borrow, + ); + + let user_new_debt = + user_current_debt.checked_sub(delta_info.less_debt).unwrap_or(Uint128::zero()); + + let user_new_debt_scaled = + th_get_scaled_debt_amount(user_new_debt, expected_indices.borrow); + + delta_info.user_current_debt_scaled - user_new_debt_scaled + } else { + Uint128::zero() + }; + + // NOTE: Don't panic here so that the total repay of debt can be simulated + // when less debt is greater than outstanding debt + let new_debt_total_scaled = (market.debt_total_scaled + more_debt_scaled) + .checked_sub(less_debt_scaled) + .unwrap_or(Uint128::zero()); + let debt_total = th_get_underlying_debt_amount(new_debt_total_scaled, expected_indices.borrow); + + let total_collateral = th_get_underlying_liquidity_amount( + market.collateral_total_scaled, + expected_indices.liquidity, + ); + + // Total collateral increased by accured protocol rewards + let total_collateral = total_collateral + expected_protocol_rewards_to_distribute; + let expected_utilization_rate = if !total_collateral.is_zero() { + Decimal::from_ratio(debt_total, total_collateral) + } else { + Decimal::zero() + }; + + // interest rates (make a copy and update those values to get the expeted irs) + let mut market_copy = market.clone(); + market_copy.update_interest_rates(expected_utilization_rate).unwrap(); + + TestInterestResults { + borrow_index: expected_indices.borrow, + liquidity_index: expected_indices.liquidity, + borrow_rate: market_copy.borrow_rate, + liquidity_rate: market_copy.liquidity_rate, + protocol_rewards_to_distribute: expected_protocol_rewards_to_distribute, + less_debt_scaled, + } +} + +/// Compute protocol income to be distributed (using values up to the instant +/// before the contract call is made) +pub fn th_get_expected_protocol_rewards( + market: &Market, + expected_indices: &TestExpectedIndices, +) -> Uint128 { + let previous_borrow_index = market.borrow_index; + let previous_debt_total = + th_get_underlying_debt_amount(market.debt_total_scaled, previous_borrow_index); + let current_debt_total = + th_get_underlying_debt_amount(market.debt_total_scaled, expected_indices.borrow); + let interest_accrued = + current_debt_total.checked_sub(previous_debt_total).unwrap_or(Uint128::zero()); + interest_accrued * market.reserve_factor +} + +/// Expected results for applying accumulated interest +pub struct TestExpectedIndices { + pub liquidity: Decimal, + pub borrow: Decimal, +} + +pub fn th_get_expected_indices(market: &Market, block_time: u64) -> TestExpectedIndices { + let seconds_elapsed = block_time - market.indexes_last_updated; + // market indices + let expected_liquidity_index = calculate_applied_linear_interest_rate( + market.liquidity_index, + market.liquidity_rate, + seconds_elapsed, + ) + .unwrap(); + + let expected_borrow_index = calculate_applied_linear_interest_rate( + market.borrow_index, + market.borrow_rate, + seconds_elapsed, + ) + .unwrap(); + + TestExpectedIndices { + liquidity: expected_liquidity_index, + borrow: expected_borrow_index, + } +} + +pub fn th_get_scaled_liquidity_amount(amount: Uint128, liquidity_index: Decimal) -> Uint128 { + compute_scaled_amount(amount, liquidity_index, ScalingOperation::Truncate).unwrap() +} + +pub fn th_get_scaled_debt_amount(amount: Uint128, borrow_index: Decimal) -> Uint128 { + compute_scaled_amount(amount, borrow_index, ScalingOperation::Ceil).unwrap() +} + +pub fn th_get_underlying_liquidity_amount( + amount_scaled: Uint128, + liquidity_index: Decimal, +) -> Uint128 { + compute_underlying_amount(amount_scaled, liquidity_index, ScalingOperation::Truncate).unwrap() +} + +pub fn th_get_underlying_debt_amount(amount_scaled: Uint128, borrow_index: Decimal) -> Uint128 { + compute_underlying_amount(amount_scaled, borrow_index, ScalingOperation::Ceil).unwrap() +} + +pub fn liq_threshold_hf(position: &UserPositionResponse) -> Decimal { + match position.health_status { + UserHealthStatus::Borrowing { + liq_threshold_hf, + .. + } => liq_threshold_hf, + _ => panic!("User is not borrowing"), + } +} + +// Merge collaterals and debts for users. +// Return total amount_scaled for collateral / debt and balance amounts for denoms. +pub fn merge_collaterals_and_debts( + users_collaterals: &[&HashMap], + users_debts: &[&HashMap], +) -> (HashMap, HashMap, HashMap) { + let mut balances: HashMap = HashMap::new(); + + let mut merged_collaterals: HashMap = HashMap::new(); + + for user_collaterals in users_collaterals { + for (denom, collateral) in user_collaterals.iter() { + merged_collaterals + .entry(denom.clone()) + .and_modify(|v| { + *v += collateral.amount_scaled; + }) + .or_insert(collateral.amount_scaled); + balances + .entry(denom.clone()) + .and_modify(|v| { + *v += collateral.amount; + }) + .or_insert(collateral.amount); + } + } + + let mut merged_debts: HashMap = HashMap::new(); + + for user_debts in users_debts { + for (denom, debt) in user_debts.iter() { + merged_debts + .entry(denom.clone()) + .and_modify(|v| { + *v += debt.amount_scaled; + }) + .or_insert(debt.amount_scaled); + balances + .entry(denom.clone()) + .and_modify(|v| { + *v -= debt.amount; + }) + .or_insert(Uint128::zero()); // balance can't be negative + } + } + + (merged_collaterals, merged_debts, balances) +} + +pub fn assert_err(res: AnyResult, err: ContractError) { + match res { + Ok(_) => panic!("Result was not an error"), + Err(generic_err) => { + let contract_err: ContractError = generic_err.downcast().unwrap(); + assert_eq!(contract_err, err); + } + } +} + +pub fn assert_err_with_str(res: AnyResult, expected: impl Display) { + match res { + Ok(_) => panic!("Result was not an error"), + Err(generic_err) => { + let contract_err: ContractError = generic_err.downcast().unwrap(); + let msg = contract_err.to_string(); + println!("error: {}", msg); // print error for debugging + assert!(msg.contains(&format!("{expected}"))) + } + } +} + +pub fn osmo_asset_params() -> (InitOrUpdateAssetParams, AssetParams) { + default_asset_params_with("uosmo", Decimal::percent(70), Decimal::percent(78)) +} + +pub fn usdc_asset_params() -> (InitOrUpdateAssetParams, AssetParams) { + default_asset_params_with("uusdc", Decimal::percent(90), Decimal::percent(96)) +} + +pub fn default_asset_params_with( + denom: &str, + max_loan_to_value: Decimal, + liquidation_threshold: Decimal, +) -> (InitOrUpdateAssetParams, AssetParams) { + let market_params = InitOrUpdateAssetParams { + reserve_factor: Some(Decimal::percent(20)), + interest_rate_model: Some(InterestRateModel { + optimal_utilization_rate: Decimal::percent(10), + base: Decimal::percent(30), + slope_1: Decimal::percent(25), + slope_2: Decimal::percent(30), + }), + }; + let asset_params = AssetParams { + denom: denom.to_string(), + credit_manager: CmSettings { + whitelisted: false, + hls: None, + }, + red_bank: RedBankSettings { + deposit_enabled: true, + borrow_enabled: true, + }, + max_loan_to_value, + liquidation_threshold, + liquidation_bonus: LiquidationBonus { + starting_lb: Decimal::percent(1), + slope: Decimal::from_str("2.0").unwrap(), + min_lb: Decimal::percent(2), + max_lb: Decimal::percent(10), + }, + protocol_liquidation_fee: Decimal::percent(2), + deposit_cap: Uint128::MAX, + }; + (market_params, asset_params) +} diff --git a/contracts/red-bank/tests/tests/mod.rs b/contracts/red-bank/tests/tests/mod.rs new file mode 100644 index 000000000..63cccf890 --- /dev/null +++ b/contracts/red-bank/tests/tests/mod.rs @@ -0,0 +1,13 @@ +mod helpers; + +mod test_admin; +mod test_borrow; +mod test_credit_accounts; +mod test_deposit; +mod test_health; +mod test_liquidate; +mod test_misc; +mod test_payment; +mod test_query; +mod test_update_owner; +mod test_withdraw; diff --git a/contracts/red-bank/tests/test_admin.rs b/contracts/red-bank/tests/tests/test_admin.rs similarity index 60% rename from contracts/red-bank/tests/test_admin.rs rename to contracts/red-bank/tests/tests/test_admin.rs index 0aa0dc9bb..37033cedb 100644 --- a/contracts/red-bank/tests/test_admin.rs +++ b/contracts/red-bank/tests/tests/test_admin.rs @@ -1,9 +1,9 @@ use cosmwasm_std::{attr, coin, from_binary, testing::mock_info, Addr, Decimal, Event, Uint128}; -use mars_owner::{OwnerError::NotOwner, OwnerUpdate}; +use mars_interest_rate::{compute_scaled_amount, compute_underlying_amount, ScalingOperation}; +use mars_owner::OwnerError::NotOwner; use mars_red_bank::{ contract::{execute, instantiate, query}, error::ContractError, - interest_rates::{compute_scaled_amount, compute_underlying_amount, ScalingOperation}, state::{COLLATERALS, MARKETS}, }; use mars_red_bank_types::{ @@ -17,9 +17,7 @@ use mars_red_bank_types::{ use mars_testing::{mock_dependencies, mock_env, mock_env_at_block_time, MockEnvParams}; use mars_utils::error::ValidationError; -use crate::helpers::{th_get_expected_indices, th_init_market, th_setup}; - -mod helpers; +use super::helpers::{th_get_expected_indices, th_init_market, th_setup}; #[test] fn proper_initialization() { @@ -29,7 +27,6 @@ fn proper_initialization() { // Config with base params valid (just update the rest) let base_config = CreateOrUpdateConfig { address_provider: Some("address_provider".to_string()), - close_factor: None, }; // * @@ -37,7 +34,6 @@ fn proper_initialization() { // * let empty_config = CreateOrUpdateConfig { address_provider: None, - close_factor: None, }; let msg = InstantiateMsg { owner: "owner".to_string(), @@ -47,36 +43,10 @@ fn proper_initialization() { let error_res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap_err(); assert_eq!(error_res, MarsError::InstantiateParamsUnavailable {}.into()); - // * - // init config with close_factor greater than 1 - // * - let mut close_factor = Decimal::from_ratio(13u128, 10u128); - let config = CreateOrUpdateConfig { - close_factor: Some(close_factor), - ..base_config.clone() - }; - let msg = InstantiateMsg { - owner: "owner".to_string(), - config, - }; - let info = mock_info("owner", &[]); - let error_res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!( - error_res, - ValidationError::InvalidParam { - param_name: "close_factor".to_string(), - invalid_value: "1.3".to_string(), - predicate: "<= 1".to_string(), - } - .into() - ); - // * // init config with valid params // * - close_factor = Decimal::from_ratio(1u128, 2u128); let config = CreateOrUpdateConfig { - close_factor: Some(close_factor), ..base_config }; let msg = InstantiateMsg { @@ -104,10 +74,8 @@ fn update_config() { // * // init config with valid params // * - let mut close_factor = Decimal::from_ratio(1u128, 4u128); let init_config = CreateOrUpdateConfig { address_provider: Some("address_provider".to_string()), - close_factor: Some(close_factor), }; let msg = InstantiateMsg { owner: "owner".to_string(), @@ -121,42 +89,17 @@ fn update_config() { // non owner is not authorized // * let msg = ExecuteMsg::UpdateConfig { - config: init_config.clone(), + config: init_config, }; let info = mock_info("somebody", &[]); let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); assert_eq!(error_res, ContractError::Owner(NotOwner {})); - // * - // update config with close_factor - // * - close_factor = Decimal::from_ratio(13u128, 10u128); - let config = CreateOrUpdateConfig { - close_factor: Some(close_factor), - ..init_config - }; - let msg = ExecuteMsg::UpdateConfig { - config, - }; - let info = mock_info("owner", &[]); - let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!( - error_res, - ValidationError::InvalidParam { - param_name: "close_factor".to_string(), - invalid_value: "1.3".to_string(), - predicate: "<= 1".to_string(), - } - .into() - ); - // * // update config with all new params // * - close_factor = Decimal::from_ratio(1u128, 20u128); let config = CreateOrUpdateConfig { address_provider: Some("new_address_provider".to_string()), - close_factor: Some(close_factor), }; let msg = ExecuteMsg::UpdateConfig { config: config.clone(), @@ -173,7 +116,6 @@ fn update_config() { assert_eq!(new_config.owner.unwrap(), "owner".to_string()); assert_eq!(new_config.address_provider, Addr::unchecked(config.address_provider.unwrap())); - assert_eq!(new_config.close_factor, config.close_factor.unwrap()); } #[test] @@ -183,7 +125,6 @@ fn init_asset() { let config = CreateOrUpdateConfig { address_provider: Some("address_provider".to_string()), - close_factor: Some(Decimal::from_ratio(1u128, 2u128)), }; let msg = InstantiateMsg { owner: "owner".to_string(), @@ -196,18 +137,12 @@ fn init_asset() { optimal_utilization_rate: Decimal::one(), base: Decimal::percent(5), slope_1: Decimal::zero(), - slope_2: Decimal::zero(), + slope_2: Decimal::one(), }; let params = InitOrUpdateAssetParams { - max_loan_to_value: Some(Decimal::from_ratio(8u128, 10u128)), reserve_factor: Some(Decimal::from_ratio(1u128, 100u128)), - liquidation_threshold: Some(Decimal::one()), - liquidation_bonus: Some(Decimal::zero()), interest_rate_model: Some(ir_model.clone()), - deposit_enabled: Some(true), - borrow_enabled: Some(true), - deposit_cap: None, }; // non owner is not authorized @@ -273,10 +208,8 @@ fn init_asset() { // init asset with empty params { let empty_asset_params = InitOrUpdateAssetParams { - max_loan_to_value: None, - liquidation_threshold: None, - liquidation_bonus: None, - ..params.clone() + reserve_factor: None, + interest_rate_model: None, }; let msg = ExecuteMsg::InitAsset { denom: "someasset".to_string(), @@ -310,57 +243,14 @@ fn init_asset() { ); } - // init asset with max_loan_to_value greater than 1 - { - let invalid_asset_params = InitOrUpdateAssetParams { - max_loan_to_value: Some(Decimal::from_ratio(11u128, 10u128)), - ..params.clone() - }; - let msg = ExecuteMsg::InitAsset { - denom: "someasset".to_string(), - params: invalid_asset_params, - }; - let info = mock_info("owner", &[]); - let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!( - error_res, - ValidationError::InvalidParam { - param_name: "max_loan_to_value".to_string(), - invalid_value: "1.1".to_string(), - predicate: "<= 1".to_string(), - } - .into() - ); - } - - // init asset with liquidation_threshold greater than 1 - { - let invalid_asset_params = InitOrUpdateAssetParams { - liquidation_threshold: Some(Decimal::from_ratio(11u128, 10u128)), - ..params.clone() - }; - let msg = ExecuteMsg::InitAsset { - denom: "someasset".to_string(), - params: invalid_asset_params, - }; - let info = mock_info("owner", &[]); - let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!( - error_res, - ValidationError::InvalidParam { - param_name: "liquidation_threshold".to_string(), - invalid_value: "1.1".to_string(), - predicate: "<= 1".to_string(), - } - .into() - ); - } - - // init asset with liquidation_bonus greater than 1 + // init asset where optimal utilization rate > 1 { let invalid_asset_params = InitOrUpdateAssetParams { - liquidation_bonus: Some(Decimal::from_ratio(11u128, 10u128)), - ..params.clone() + interest_rate_model: Some(InterestRateModel { + optimal_utilization_rate: Decimal::percent(110), + ..ir_model + }), + ..params }; let msg = ExecuteMsg::InitAsset { denom: "someasset".to_string(), @@ -371,43 +261,20 @@ fn init_asset() { assert_eq!( error_res, ValidationError::InvalidParam { - param_name: "liquidation_bonus".to_string(), + param_name: "optimal_utilization_rate".to_string(), invalid_value: "1.1".to_string(), - predicate: "<= 1".to_string(), - } - .into() - ); - } - - // init asset where LTV >= liquidity threshold - { - let invalid_asset_params = InitOrUpdateAssetParams { - max_loan_to_value: Some(Decimal::from_ratio(5u128, 10u128)), - liquidation_threshold: Some(Decimal::from_ratio(5u128, 10u128)), - ..params.clone() - }; - let msg = ExecuteMsg::InitAsset { - denom: "someasset".to_string(), - params: invalid_asset_params, - }; - let info = mock_info("owner", &[]); - let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!( - error_res, - ValidationError::InvalidParam { - param_name: "liquidation_threshold".to_string(), - invalid_value: "0.5".to_string(), - predicate: "> 0.5 (max LTV)".to_string() + predicate: "<= 1".to_string() } .into() ); } - // init asset where optimal utilization rate > 1 + // init asset where slope_1 >= slope_2 { let invalid_asset_params = InitOrUpdateAssetParams { interest_rate_model: Some(InterestRateModel { - optimal_utilization_rate: Decimal::percent(110), + slope_1: Decimal::percent(10), + slope_2: Decimal::percent(10), ..ir_model }), ..params @@ -421,9 +288,9 @@ fn init_asset() { assert_eq!( error_res, ValidationError::InvalidParam { - param_name: "optimal_utilization_rate".to_string(), - invalid_value: "1.1".to_string(), - predicate: "<= 1".to_string() + param_name: "slope_1".to_string(), + invalid_value: "0.1".to_string(), + predicate: "< 0.1".to_string() } .into() ); @@ -443,7 +310,7 @@ fn init_asset() { assert_eq!(market.denom, "someasset"); // should have unlimited deposit cap - assert_eq!(market.deposit_cap, Uint128::MAX); + assert_eq!(market.reserve_factor, Decimal::from_ratio(1u128, 100u128)); assert_eq!(res.attributes, vec![attr("action", "init_asset"), attr("denom", "someasset")]); } @@ -468,7 +335,6 @@ fn update_asset() { let config = CreateOrUpdateConfig { address_provider: Some("address_provider".to_string()), - close_factor: Some(Decimal::from_ratio(1u128, 2u128)), }; let msg = InstantiateMsg { owner: "owner".to_string(), @@ -477,22 +343,18 @@ fn update_asset() { let info = mock_info("owner", &[]); instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + deps.querier.set_target_health_factor(Decimal::from_ratio(1u128, 2u128)); + let ir_model = InterestRateModel { optimal_utilization_rate: Decimal::one(), base: Decimal::percent(5), slope_1: Decimal::zero(), - slope_2: Decimal::zero(), + slope_2: Decimal::one(), }; let params = InitOrUpdateAssetParams { - max_loan_to_value: Some(Decimal::from_ratio(50u128, 100u128)), reserve_factor: Some(Decimal::from_ratio(1u128, 100u128)), - liquidation_threshold: Some(Decimal::from_ratio(80u128, 100u128)), - liquidation_bonus: Some(Decimal::from_ratio(10u128, 100u128)), interest_rate_model: Some(ir_model.clone()), - deposit_enabled: Some(true), - borrow_enabled: Some(true), - deposit_cap: None, }; // non owner is not authorized @@ -527,99 +389,6 @@ fn update_asset() { let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); } - // update asset with max_loan_to_value greater than 1 - { - let invalid_asset_params = InitOrUpdateAssetParams { - max_loan_to_value: Some(Decimal::from_ratio(11u128, 10u128)), - ..params.clone() - }; - let msg = ExecuteMsg::UpdateAsset { - denom: "someasset".to_string(), - params: invalid_asset_params, - }; - let info = mock_info("owner", &[]); - let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!( - error_res, - ValidationError::InvalidParam { - param_name: "max_loan_to_value".to_string(), - invalid_value: "1.1".to_string(), - predicate: "<= 1".to_string(), - } - .into() - ); - } - - // update asset with liquidation_threshold greater than 1 - { - let invalid_asset_params = InitOrUpdateAssetParams { - liquidation_threshold: Some(Decimal::from_ratio(11u128, 10u128)), - ..params.clone() - }; - let msg = ExecuteMsg::UpdateAsset { - denom: "someasset".to_string(), - params: invalid_asset_params, - }; - let info = mock_info("owner", &[]); - let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!( - error_res, - ValidationError::InvalidParam { - param_name: "liquidation_threshold".to_string(), - invalid_value: "1.1".to_string(), - predicate: "<= 1".to_string(), - } - .into() - ); - } - - // update asset with liquidation_bonus greater than 1 - { - let invalid_asset_params = InitOrUpdateAssetParams { - liquidation_bonus: Some(Decimal::from_ratio(11u128, 10u128)), - ..params.clone() - }; - let msg = ExecuteMsg::UpdateAsset { - denom: "someasset".to_string(), - params: invalid_asset_params, - }; - let info = mock_info("owner", &[]); - let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!( - error_res, - ValidationError::InvalidParam { - param_name: "liquidation_bonus".to_string(), - invalid_value: "1.1".to_string(), - predicate: "<= 1".to_string(), - } - .into() - ); - } - - // update asset where LTV >= liquidity threshold - { - let invalid_asset_params = InitOrUpdateAssetParams { - max_loan_to_value: Some(Decimal::from_ratio(6u128, 10u128)), - liquidation_threshold: Some(Decimal::from_ratio(5u128, 10u128)), - ..params - }; - let msg = ExecuteMsg::UpdateAsset { - denom: "someasset".to_string(), - params: invalid_asset_params, - }; - let info = mock_info("owner", &[]); - let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!( - error_res, - ValidationError::InvalidParam { - param_name: "liquidation_threshold".to_string(), - invalid_value: "0.5".to_string(), - predicate: "> 0.6 (max LTV)".to_string() - } - .into() - ); - } - // update asset where optimal utilization rate > 1 { let invalid_asset_params = InitOrUpdateAssetParams { @@ -649,14 +418,8 @@ fn update_asset() { // update asset with new params { let params = InitOrUpdateAssetParams { - max_loan_to_value: Some(Decimal::from_ratio(60u128, 100u128)), reserve_factor: Some(Decimal::from_ratio(10u128, 100u128)), - liquidation_threshold: Some(Decimal::from_ratio(90u128, 100u128)), - liquidation_bonus: Some(Decimal::from_ratio(12u128, 100u128)), interest_rate_model: Some(ir_model), - deposit_enabled: Some(true), - borrow_enabled: Some(true), - deposit_cap: Some(Uint128::new(10_000_000)), }; let msg = ExecuteMsg::UpdateAsset { denom: "someasset".to_string(), @@ -672,10 +435,7 @@ fn update_asset() { ); let new_market = MARKETS.load(&deps.storage, "someasset").unwrap(); - assert_eq!(params.max_loan_to_value.unwrap(), new_market.max_loan_to_value); assert_eq!(params.reserve_factor.unwrap(), new_market.reserve_factor); - assert_eq!(params.liquidation_threshold.unwrap(), new_market.liquidation_threshold); - assert_eq!(params.liquidation_bonus.unwrap(), new_market.liquidation_bonus); assert_eq!(params.interest_rate_model.unwrap(), new_market.interest_rate_model); } @@ -684,14 +444,8 @@ fn update_asset() { let market_before = MARKETS.load(&deps.storage, "someasset").unwrap(); let empty_asset_params = InitOrUpdateAssetParams { - max_loan_to_value: None, reserve_factor: None, - liquidation_threshold: None, - liquidation_bonus: None, interest_rate_model: None, - deposit_enabled: None, - borrow_enabled: None, - deposit_cap: None, }; let msg = ExecuteMsg::UpdateAsset { denom: "someasset".to_string(), @@ -706,11 +460,7 @@ fn update_asset() { let new_market = MARKETS.load(&deps.storage, "someasset").unwrap(); // should keep old params assert_eq!(market_before.borrow_rate, new_market.borrow_rate); - assert_eq!(market_before.max_loan_to_value, new_market.max_loan_to_value); assert_eq!(market_before.reserve_factor, new_market.reserve_factor); - assert_eq!(market_before.liquidation_threshold, new_market.liquidation_threshold); - assert_eq!(market_before.liquidation_bonus, new_market.liquidation_bonus); - assert_eq!(market_before.deposit_cap, new_market.deposit_cap); assert_eq!(market_before.interest_rate_model, new_market.interest_rate_model); } } @@ -721,7 +471,6 @@ fn update_asset_with_new_interest_rate_model_params() { let config = CreateOrUpdateConfig { address_provider: Some("address_provider".to_string()), - close_factor: Some(Decimal::from_ratio(1u128, 2u128)), }; let msg = InstantiateMsg { owner: "owner".to_string(), @@ -731,22 +480,18 @@ fn update_asset_with_new_interest_rate_model_params() { let env = mock_env(MockEnvParams::default()); instantiate(deps.as_mut(), env, info, msg).unwrap(); + deps.querier.set_target_health_factor(Decimal::from_ratio(1u128, 2u128)); + let ir_model = InterestRateModel { optimal_utilization_rate: Decimal::one(), base: Decimal::percent(5), slope_1: Decimal::zero(), - slope_2: Decimal::zero(), + slope_2: Decimal::one(), }; let params = InitOrUpdateAssetParams { - max_loan_to_value: Some(Decimal::from_ratio(50u128, 100u128)), reserve_factor: Some(Decimal::from_ratio(2u128, 100u128)), - liquidation_threshold: Some(Decimal::from_ratio(80u128, 100u128)), - liquidation_bonus: Some(Decimal::from_ratio(10u128, 100u128)), interest_rate_model: Some(ir_model.clone()), - deposit_enabled: Some(true), - borrow_enabled: Some(true), - deposit_cap: None, }; let msg = ExecuteMsg::InitAsset { @@ -857,14 +602,8 @@ fn update_asset_new_reserve_factor_accrues_interest_rate() { ); let params = InitOrUpdateAssetParams { - max_loan_to_value: None, reserve_factor: Some(Decimal::from_ratio(2_u128, 10_u128)), - liquidation_threshold: None, - liquidation_bonus: None, interest_rate_model: None, - deposit_enabled: None, - borrow_enabled: None, - deposit_cap: None, }; let msg = ExecuteMsg::UpdateAsset { denom: "somecoin".to_string(), @@ -939,127 +678,8 @@ fn update_asset_new_reserve_factor_accrues_interest_rate() { let collateral = COLLATERALS .load( deps.as_ref().storage, - (&Addr::unchecked(MarsAddressType::RewardsCollector.to_string()), "somecoin"), + (&Addr::unchecked(MarsAddressType::RewardsCollector.to_string()), "", "somecoin"), ) .unwrap(); assert_eq!(collateral.amount_scaled, expected_rewards_scaled); } - -#[test] -fn update_asset_by_emergency_owner() { - let mut deps = mock_dependencies(&[]); - let start_time = 100000000; - let env = mock_env_at_block_time(start_time); - - let config = CreateOrUpdateConfig { - address_provider: Some("address_provider".to_string()), - close_factor: Some(Decimal::from_ratio(1u128, 2u128)), - }; - let msg = InstantiateMsg { - owner: "owner".to_string(), - config, - }; - let info = mock_info("owner", &[]); - instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); - - let ir_model = InterestRateModel { - optimal_utilization_rate: Decimal::one(), - base: Decimal::percent(5), - slope_1: Decimal::zero(), - slope_2: Decimal::zero(), - }; - - let params = InitOrUpdateAssetParams { - max_loan_to_value: Some(Decimal::from_ratio(50u128, 100u128)), - reserve_factor: Some(Decimal::from_ratio(1u128, 100u128)), - liquidation_threshold: Some(Decimal::from_ratio(80u128, 100u128)), - liquidation_bonus: Some(Decimal::from_ratio(10u128, 100u128)), - interest_rate_model: Some(ir_model.clone()), - deposit_enabled: Some(true), - borrow_enabled: Some(true), - deposit_cap: None, - }; - - execute( - deps.as_mut(), - env.clone(), - mock_info("owner", &[]), - ExecuteMsg::UpdateOwner(OwnerUpdate::SetEmergencyOwner { - emergency_owner: "emergency_owner".to_string(), - }), - ) - .unwrap(); - - // emergency owner is authorized but can't update asset if not initialized first - { - let msg = ExecuteMsg::UpdateAsset { - denom: "someasset".to_string(), - params: params.clone(), - }; - let info = mock_info("emergency_owner", &[]); - let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!(error_res, ContractError::AssetNotInitialized {}); - } - - // initialize asset - { - let msg = ExecuteMsg::InitAsset { - denom: "someasset".to_string(), - params: params.clone(), - }; - let info = mock_info("owner", &[]); - let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); - } - - // update asset with borrow_enabled = true, should have not effect on the saved market - { - let old_market = MARKETS.load(&deps.storage, "someasset").unwrap(); - - let new_asset_params = InitOrUpdateAssetParams { - borrow_enabled: Some(true), - ..params - }; - let msg = ExecuteMsg::UpdateAsset { - denom: "someasset".to_string(), - params: new_asset_params, - }; - let info = mock_info("emergency_owner", &[]); - let res_err = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!(res_err, ContractError::Mars(MarsError::Unauthorized {})); - - let new_market = MARKETS.load(&deps.storage, "someasset").unwrap(); - assert_eq!(old_market, new_market) - } - - // update asset with new params, only borrow_enabled = false should have effect on the saved market - { - let mut old_market = MARKETS.load(&deps.storage, "someasset").unwrap(); - - let params = InitOrUpdateAssetParams { - max_loan_to_value: Some(Decimal::from_ratio(60u128, 100u128)), - reserve_factor: Some(Decimal::from_ratio(10u128, 100u128)), - liquidation_threshold: Some(Decimal::from_ratio(90u128, 100u128)), - liquidation_bonus: Some(Decimal::from_ratio(12u128, 100u128)), - interest_rate_model: Some(ir_model), - deposit_enabled: Some(false), - borrow_enabled: Some(false), - deposit_cap: Some(Uint128::new(10_000_000)), - }; - let msg = ExecuteMsg::UpdateAsset { - denom: "someasset".to_string(), - params, - }; - let info = mock_info("emergency_owner", &[]); - let res = execute(deps.as_mut(), env, info, msg).unwrap(); - assert!(res.messages.is_empty()); - assert_eq!( - res.attributes, - vec![attr("action", "emergency_update_asset"), attr("denom", "someasset")], - ); - - let new_market = MARKETS.load(&deps.storage, "someasset").unwrap(); - // old market should have only borrow_enabled updated - old_market.borrow_enabled = false; - assert_eq!(old_market, new_market); - } -} diff --git a/contracts/red-bank/tests/test_borrow.rs b/contracts/red-bank/tests/tests/test_borrow.rs similarity index 91% rename from contracts/red-bank/tests/test_borrow.rs rename to contracts/red-bank/tests/tests/test_borrow.rs index b6b672e38..6c56c2985 100644 --- a/contracts/red-bank/tests/test_borrow.rs +++ b/contracts/red-bank/tests/tests/test_borrow.rs @@ -2,24 +2,24 @@ use cosmwasm_std::{ attr, coin, coins, testing::mock_info, Addr, BankMsg, CosmosMsg, Decimal, SubMsg, Uint128, }; use cw_utils::PaymentError; -use helpers::{ - has_collateral_position, has_debt_position, set_collateral, th_build_interests_updated_event, - th_get_expected_indices_and_rates, th_init_market, th_setup, TestUtilizationDeltaInfo, +use mars_interest_rate::{ + calculate_applied_linear_interest_rate, compute_scaled_amount, compute_underlying_amount, + ScalingOperation, SCALING_FACTOR, }; +use mars_params::types::asset::{AssetParams, CmSettings, RedBankSettings}; use mars_red_bank::{ contract::execute, error::ContractError, - interest_rates::{ - calculate_applied_linear_interest_rate, compute_scaled_amount, compute_underlying_amount, - ScalingOperation, SCALING_FACTOR, - }, state::{DEBTS, MARKETS, UNCOLLATERALIZED_LOAN_LIMITS}, }; use mars_red_bank_types::red_bank::{ExecuteMsg, Market}; use mars_testing::{mock_env, mock_env_at_block_time, MockEnvParams}; -use mars_utils::math; -mod helpers; +use super::helpers::{ + has_collateral_position, has_debt_position, set_collateral, th_build_interests_updated_event, + th_default_asset_params, th_get_expected_indices_and_rates, th_init_market, th_setup, + TestUtilizationDeltaInfo, +}; #[test] fn borrow_and_repay() { @@ -60,7 +60,6 @@ fn borrow_and_repay() { let mock_market_3 = Market { borrow_index: Decimal::one(), liquidity_index: Decimal::from_ratio(11u128, 10u128), - max_loan_to_value: Decimal::from_ratio(7u128, 10u128), borrow_rate: Decimal::from_ratio(30u128, 100u128), reserve_factor: Decimal::from_ratio(3u128, 100u128), liquidity_rate: Decimal::from_ratio(20u128, 100u128), @@ -73,6 +72,16 @@ fn borrow_and_repay() { let market_2_initial = th_init_market(deps.as_mut(), "uusd", &mock_market_2); th_init_market(deps.as_mut(), "uatom", &mock_market_3); + deps.querier.set_redbank_params("uosmo", th_default_asset_params()); + deps.querier.set_redbank_params("uusd", th_default_asset_params()); + deps.querier.set_redbank_params( + "uatom", + AssetParams { + max_loan_to_value: Decimal::from_ratio(7u128, 10u128), + ..th_default_asset_params() + }, + ); + let borrower_addr = Addr::unchecked("borrower"); // Set user as having the market_collateral deposited @@ -482,7 +491,6 @@ fn repay_without_refund_on_behalf_of() { let mock_market = Market { liquidity_index: Decimal::one(), borrow_index: Decimal::one(), - max_loan_to_value: Decimal::from_ratio(50u128, 100u128), collateral_total_scaled: Uint128::new(1_000_000_000_000u128), ..Default::default() }; @@ -490,6 +498,21 @@ fn repay_without_refund_on_behalf_of() { let market_1_initial = th_init_market(deps.as_mut(), "depositedcoinnative", &mock_market); // collateral let market_2_initial = th_init_market(deps.as_mut(), "borrowedcoinnative", &mock_market); + deps.querier.set_redbank_params( + "depositedcoinnative", + AssetParams { + max_loan_to_value: Decimal::from_ratio(50u128, 100u128), + ..th_default_asset_params() + }, + ); + deps.querier.set_redbank_params( + "borrowedcoinnative", + AssetParams { + max_loan_to_value: Decimal::from_ratio(50u128, 100u128), + ..th_default_asset_params() + }, + ); + let borrower_addr = Addr::unchecked("borrower"); let user_addr = Addr::unchecked("user"); @@ -563,7 +586,6 @@ fn repay_with_refund_on_behalf_of() { let mock_market = Market { liquidity_index: Decimal::one(), borrow_index: Decimal::one(), - max_loan_to_value: Decimal::from_ratio(50u128, 100u128), collateral_total_scaled: Uint128::new(1_000_000_000_000u128), ..Default::default() }; @@ -571,6 +593,21 @@ fn repay_with_refund_on_behalf_of() { let market_1_initial = th_init_market(deps.as_mut(), "depositedcoinnative", &mock_market); // collateral let market_2_initial = th_init_market(deps.as_mut(), "borrowedcoinnative", &mock_market); + deps.querier.set_redbank_params( + "depositedcoinnative", + AssetParams { + max_loan_to_value: Decimal::from_ratio(50u128, 100u128), + ..th_default_asset_params() + }, + ); + deps.querier.set_redbank_params( + "borrowedcoinnative", + AssetParams { + max_loan_to_value: Decimal::from_ratio(50u128, 100u128), + ..th_default_asset_params() + }, + ); + let borrower_addr = Addr::unchecked("borrower"); let user_addr = Addr::unchecked("user"); @@ -672,7 +709,6 @@ fn borrow_uusd() { let mock_market = Market { liquidity_index: Decimal::one(), - max_loan_to_value: ltv, borrow_index: Decimal::from_ratio(20u128, 10u128), borrow_rate: Decimal::one(), liquidity_rate: Decimal::one(), @@ -683,6 +719,14 @@ fn borrow_uusd() { }; let market = th_init_market(deps.as_mut(), "uusd", &mock_market); + deps.querier.set_redbank_params( + "uusd", + AssetParams { + max_loan_to_value: ltv, + ..th_default_asset_params() + }, + ); + // Set user as having the market_collateral deposited let deposit_amount_scaled = Uint128::new(110_000) * SCALING_FACTOR; set_collateral(deps.as_mut(), &borrower_addr, "uusd", deposit_amount_scaled, true); @@ -751,7 +795,6 @@ fn borrow_full_liquidity_and_then_repay() { let mock_market = Market { liquidity_index: Decimal::one(), - max_loan_to_value: ltv, borrow_index: Decimal::one(), borrow_rate: Decimal::one(), liquidity_rate: Decimal::one(), @@ -763,6 +806,14 @@ fn borrow_full_liquidity_and_then_repay() { }; th_init_market(deps.as_mut(), "uusd", &mock_market); + deps.querier.set_redbank_params( + "uusd", + AssetParams { + max_loan_to_value: ltv, + ..th_default_asset_params() + }, + ); + // User should have amount of collateral more than initial liquidity in order to borrow full liquidity let deposit_amount = initial_liquidity + 1000u128; set_collateral( @@ -831,7 +882,6 @@ fn borrow_collateral_check() { // NOTE: base asset price (asset3) should be set to 1 by the oracle helper let mock_market_1 = Market { - max_loan_to_value: Decimal::from_ratio(8u128, 10u128), collateral_total_scaled: Uint128::new(10_000_000_000_000u128), debt_total_scaled: Uint128::zero(), liquidity_index: Decimal::one(), @@ -839,7 +889,6 @@ fn borrow_collateral_check() { ..Default::default() }; let mock_market_2 = Market { - max_loan_to_value: Decimal::from_ratio(6u128, 10u128), collateral_total_scaled: Uint128::new(10_000_000_000_000u128), debt_total_scaled: Uint128::zero(), liquidity_index: Decimal::one(), @@ -847,7 +896,6 @@ fn borrow_collateral_check() { ..Default::default() }; let mock_market_3 = Market { - max_loan_to_value: Decimal::from_ratio(4u128, 10u128), collateral_total_scaled: Uint128::new(10_000_000_000_000u128), debt_total_scaled: Uint128::zero(), liquidity_index: Decimal::one(), @@ -862,6 +910,22 @@ fn borrow_collateral_check() { // should get index 2 let market_3_initial = th_init_market(deps.as_mut(), "uusd", &mock_market_3); + let asset_params_1 = AssetParams { + max_loan_to_value: Decimal::from_ratio(8u128, 10u128), + ..th_default_asset_params() + }; + deps.querier.set_redbank_params("uatom", asset_params_1.clone()); + let asset_params_2 = AssetParams { + max_loan_to_value: Decimal::from_ratio(6u128, 10u128), + ..th_default_asset_params() + }; + deps.querier.set_redbank_params("uosmo", asset_params_2.clone()); + let asset_params_3 = AssetParams { + max_loan_to_value: Decimal::from_ratio(4u128, 10u128), + ..th_default_asset_params() + }; + deps.querier.set_redbank_params("uusd", asset_params_3.clone()); + let borrower_addr = Addr::unchecked("borrower"); let balance_1 = Uint128::new(4_000_000) * SCALING_FACTOR; @@ -873,7 +937,7 @@ fn borrow_collateral_check() { set_collateral(deps.as_mut(), &borrower_addr, &market_2_initial.denom, balance_2, true); set_collateral(deps.as_mut(), &borrower_addr, &market_3_initial.denom, balance_3, true); - let max_borrow_allowed_in_base_asset = (market_1_initial.max_loan_to_value + let max_borrow_allowed_in_base_asset = (asset_params_1.max_loan_to_value * compute_underlying_amount( balance_1, market_1_initial.liquidity_index, @@ -881,7 +945,7 @@ fn borrow_collateral_check() { ) .unwrap() * exchange_rate_1) - + (market_2_initial.max_loan_to_value + + (asset_params_2.max_loan_to_value * compute_underlying_amount( balance_2, market_2_initial.liquidity_index, @@ -889,7 +953,7 @@ fn borrow_collateral_check() { ) .unwrap() * exchange_rate_2) - + (market_3_initial.max_loan_to_value + + (asset_params_3.max_loan_to_value * compute_underlying_amount( balance_3, market_3_initial.liquidity_index, @@ -898,10 +962,10 @@ fn borrow_collateral_check() { .unwrap() * exchange_rate_3); let exceeding_borrow_amount = - math::divide_uint128_by_decimal(max_borrow_allowed_in_base_asset, exchange_rate_2).unwrap() + max_borrow_allowed_in_base_asset.checked_div_floor(exchange_rate_2).unwrap() + Uint128::from(100_u64); let permissible_borrow_amount = - math::divide_uint128_by_decimal(max_borrow_allowed_in_base_asset, exchange_rate_2).unwrap() + max_borrow_allowed_in_base_asset.checked_div_floor(exchange_rate_2).unwrap() - Uint128::from(100_u64); // borrow above the allowed amount given current collateral, should fail @@ -928,11 +992,23 @@ fn borrow_collateral_check() { fn cannot_borrow_if_market_not_enabled() { let mut deps = th_setup(&[]); - let mock_market = Market { - borrow_enabled: false, - ..Default::default() - }; - th_init_market(deps.as_mut(), "somecoin", &mock_market); + th_init_market(deps.as_mut(), "somecoin", &Market::default()); + + deps.querier.set_redbank_params( + "somecoin", + AssetParams { + credit_manager: CmSettings { + whitelisted: false, + + hls: None, + }, + red_bank: RedBankSettings { + deposit_enabled: false, + borrow_enabled: false, + }, + ..th_default_asset_params() + }, + ); // Check error when borrowing not allowed on market let env = mock_env(MockEnvParams::default()); @@ -962,13 +1038,20 @@ fn borrow_and_send_funds_to_another_user() { let mock_market = Market { liquidity_index: Decimal::one(), borrow_index: Decimal::one(), - max_loan_to_value: Decimal::from_ratio(5u128, 10u128), collateral_total_scaled: Uint128::new(1_000_000_000_000u128), debt_total_scaled: Uint128::zero(), ..Default::default() }; let market = th_init_market(deps.as_mut(), "uusd", &mock_market); + deps.querier.set_redbank_params( + "uusd", + AssetParams { + max_loan_to_value: Decimal::from_ratio(5u128, 10u128), + ..th_default_asset_params() + }, + ); + // Set user as having the market_collateral deposited let deposit_amount_scaled = Uint128::new(100_000) * SCALING_FACTOR; set_collateral(deps.as_mut(), &borrower_addr, &market.denom, deposit_amount_scaled, true); diff --git a/contracts/red-bank/tests/tests/test_credit_accounts.rs b/contracts/red-bank/tests/tests/test_credit_accounts.rs new file mode 100644 index 000000000..edbce8428 --- /dev/null +++ b/contracts/red-bank/tests/tests/test_credit_accounts.rs @@ -0,0 +1,126 @@ +use cosmwasm_std::{coin, Addr, Decimal, Uint128}; +use mars_red_bank::error::ContractError; +use mars_red_bank_types::red_bank::UserHealthStatus; +use mars_testing::integration::mock_env::MockEnvBuilder; + +use super::helpers::assert_err; +use crate::tests::helpers::{osmo_asset_params, usdc_asset_params}; + +#[test] +fn deposit_and_withdraw_for_credit_account_works() { + let owner = Addr::unchecked("owner"); + let mut mock_env = MockEnvBuilder::new(None, owner.clone()).build(); + + let red_bank = mock_env.red_bank.clone(); + let params = mock_env.params.clone(); + let oracle = mock_env.oracle.clone(); + + let funded_amt = 1_000_000_000_000u128; + let provider = Addr::unchecked("provider"); // provides collateral to be borrowed by others + let credit_manager = mock_env.credit_manager.clone(); + let account_id = "111".to_string(); + + // setup red-bank + let (market_params, asset_params) = osmo_asset_params(); + red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = usdc_asset_params(); + red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params); + params.init_params(&mut mock_env, asset_params); + + // setup oracle + oracle.set_price_source_fixed(&mut mock_env, "uosmo", Decimal::one()); + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(2u128, 1u128)); + + // fund accounts + mock_env.fund_accounts(&[&provider, &credit_manager], funded_amt, &["uosmo", "uusdc"]); + + // provider deposits collaterals + red_bank.deposit(&mut mock_env, &provider, coin(1000000000, "uusdc")).unwrap(); + + // credit manager deposits + let cm_osmo_deposit_amt = 100000000u128; + red_bank + .deposit_with_acc_id( + &mut mock_env, + &credit_manager, + coin(cm_osmo_deposit_amt, "uosmo"), + Some(account_id.clone()), + ) + .unwrap(); + + // credit manager try to borrow if no credit line set + let error_res = red_bank.borrow(&mut mock_env, &credit_manager, "uusdc", 100000000); + assert_err(error_res, ContractError::BorrowAmountExceedsGivenCollateral {}); + + // update credit line for credit manager + red_bank + .update_uncollateralized_loan_limit( + &mut mock_env, + &owner, + &credit_manager, + "uusdc", + Uint128::MAX, + ) + .unwrap(); + + // credit manager should be able to borrow + let cm_usdc_borrow_amt = 100000000u128; + red_bank.borrow(&mut mock_env, &credit_manager, "uusdc", cm_usdc_borrow_amt).unwrap(); + + // collateral is not tracked for credit manager (it is per account id). Debt is tracked for credit manager as a whole (not per account id) + let cm_collaterals = red_bank.query_user_collaterals(&mut mock_env, &credit_manager); + assert!(cm_collaterals.is_empty()); + let cm_debts = red_bank.query_user_debts(&mut mock_env, &credit_manager); + assert_eq!(cm_debts.len(), 1); + let cm_usdc_debt = cm_debts.get("uusdc").unwrap(); + assert!(cm_usdc_debt.uncollateralized); + assert_eq!(cm_usdc_debt.amount.u128(), cm_usdc_borrow_amt); + let cm_position = red_bank.query_user_position(&mut mock_env, &credit_manager); + assert!(cm_position.total_enabled_collateral.is_zero()); + assert!(cm_position.total_collateralized_debt.is_zero()); + assert_eq!(cm_position.health_status, UserHealthStatus::NotBorrowing); + + // collateral is tracked for credit manager account id. Debt is not tracked per account id + let cm_collaterals = red_bank.query_user_collaterals_with_acc_id( + &mut mock_env, + &credit_manager, + Some(account_id.clone()), + ); + assert_eq!(cm_collaterals.len(), 1); + let cm_osmo_collateral = cm_collaterals.get("uosmo").unwrap(); + assert_eq!(cm_osmo_collateral.amount.u128(), cm_osmo_deposit_amt); + let cm_position = red_bank.query_user_position_with_acc_id( + &mut mock_env, + &credit_manager, + Some(account_id.clone()), + ); + assert_eq!(cm_position.total_enabled_collateral.u128(), cm_osmo_deposit_amt); + assert!(cm_position.total_collateralized_debt.is_zero()); + assert_eq!(cm_position.health_status, UserHealthStatus::NotBorrowing); + + // withdraw total collateral for account id + red_bank + .withdraw_with_acc_id( + &mut mock_env, + &credit_manager, + "uosmo", + None, + Some(account_id.clone()), + None, + ) + .unwrap(); + + // check collaterals and debts for credit manager account id after withdraw + let cm_collaterals = red_bank.query_user_collaterals_with_acc_id( + &mut mock_env, + &credit_manager, + Some(account_id.clone()), + ); + assert!(cm_collaterals.is_empty()); + let cm_position = + red_bank.query_user_position_with_acc_id(&mut mock_env, &credit_manager, Some(account_id)); + assert!(cm_position.total_enabled_collateral.is_zero()); + assert!(cm_position.total_collateralized_debt.is_zero()); + assert_eq!(cm_position.health_status, UserHealthStatus::NotBorrowing); +} diff --git a/contracts/red-bank/tests/test_deposit.rs b/contracts/red-bank/tests/tests/test_deposit.rs similarity index 72% rename from contracts/red-bank/tests/test_deposit.rs rename to contracts/red-bank/tests/tests/test_deposit.rs index c64a7ecf4..81f9928e1 100644 --- a/contracts/red-bank/tests/test_deposit.rs +++ b/contracts/red-bank/tests/tests/test_deposit.rs @@ -3,26 +3,31 @@ use std::any::type_name; use cosmwasm_std::{ attr, coin, coins, testing::{mock_env, mock_info, MockApi, MockStorage}, - to_binary, Addr, Decimal, OwnedDeps, StdError, StdResult, SubMsg, Uint128, WasmMsg, + to_binary, Addr, Decimal, OwnedDeps, StdError, SubMsg, Uint128, WasmMsg, }; use cw_utils::PaymentError; -use helpers::{ - set_collateral, th_build_interests_updated_event, th_get_expected_indices_and_rates, th_setup, +use mars_interest_rate::{ + compute_scaled_amount, get_underlying_liquidity_amount, ScalingOperation, SCALING_FACTOR, }; +use mars_params::types::asset::{AssetParams, CmSettings, LiquidationBonus, RedBankSettings}; use mars_red_bank::{ contract::execute, error::ContractError, - interest_rates::{compute_scaled_amount, ScalingOperation, SCALING_FACTOR}, state::{COLLATERALS, MARKETS}, }; use mars_red_bank_types::{ address_provider::MarsAddressType, + error::MarsError, incentives, red_bank::{Collateral, ExecuteMsg, Market}, }; use mars_testing::{mock_env_at_block_time, MarsMockQuerier}; +use test_case::test_case; -mod helpers; +use super::helpers::{ + set_collateral, th_build_interests_updated_event, th_default_asset_params, + th_get_expected_indices_and_rates, th_setup, +}; struct TestSuite { deps: OwnedDeps, @@ -40,7 +45,6 @@ fn setup_test() -> TestSuite { let market = Market { denom: denom.to_string(), liquidity_index: Decimal::from_ratio(11u128, 10u128), - max_loan_to_value: Decimal::one(), borrow_index: Decimal::from_ratio(1u128, 1u128), borrow_rate: Decimal::from_ratio(10u128, 100u128), liquidity_rate: Decimal::from_ratio(10u128, 100u128), @@ -48,12 +52,46 @@ fn setup_test() -> TestSuite { collateral_total_scaled: Uint128::new(10_000_000) * SCALING_FACTOR, debt_total_scaled: Uint128::new(10_000_000) * SCALING_FACTOR, indexes_last_updated: 10000000, - deposit_cap: Uint128::new(12_000_000), ..Default::default() }; MARKETS.save(deps.as_mut().storage, denom, &market).unwrap(); + deps.querier.set_redbank_params( + denom, + AssetParams { + denom: denom.to_string(), + max_loan_to_value: Decimal::one(), + liquidation_threshold: Default::default(), + liquidation_bonus: LiquidationBonus { + starting_lb: Decimal::percent(0u64), + slope: Decimal::one(), + min_lb: Decimal::percent(0u64), + max_lb: Decimal::percent(5u64), + }, + credit_manager: CmSettings { + whitelisted: false, + hls: None, + }, + red_bank: RedBankSettings { + deposit_enabled: true, + borrow_enabled: true, + }, + protocol_liquidation_fee: Decimal::percent(2u64), + deposit_cap: Uint128::new(12_000_000), + }, + ); + + deps.querier.set_total_deposit( + denom, + get_underlying_liquidity_amount( + market.collateral_total_scaled, + &market, + market.indexes_last_updated, + ) + .unwrap(), + ); + TestSuite { deps, denom, @@ -75,6 +113,7 @@ fn depositing_with_no_coin_sent() { mock_env(), mock_info(depositor_addr.as_str(), &[]), ExecuteMsg::Deposit { + account_id: None, on_behalf_of: None, }, ) @@ -97,6 +136,7 @@ fn depositing_with_multiple_coins_sent() { mock_env(), mock_info(depositor_addr.as_str(), &sent_coins), ExecuteMsg::Deposit { + account_id: None, on_behalf_of: None, }, ) @@ -120,6 +160,7 @@ fn depositing_to_non_existent_market() { mock_env(), mock_info(depositor_addr.as_str(), &coins(123, false_denom)), ExecuteMsg::Deposit { + account_id: None, on_behalf_of: None, }, ) @@ -137,19 +178,27 @@ fn depositing_to_disabled_market() { } = setup_test(); // disable the market - MARKETS - .update(deps.as_mut().storage, denom, |opt| -> StdResult<_> { - let mut market = opt.unwrap(); - market.deposit_enabled = false; - Ok(market) - }) - .unwrap(); + deps.querier.set_redbank_params( + denom, + AssetParams { + credit_manager: CmSettings { + whitelisted: false, + hls: None, + }, + red_bank: RedBankSettings { + deposit_enabled: false, + borrow_enabled: true, + }, + ..th_default_asset_params() + }, + ); let err = execute( deps.as_mut(), mock_env(), mock_info(depositor_addr.as_str(), &coins(123, denom)), ExecuteMsg::Deposit { + account_id: None, on_behalf_of: None, }, ) @@ -162,52 +211,66 @@ fn depositing_to_disabled_market() { ); } -#[test] -fn depositing_above_cap() { +// note: the initial deposit amount set in the TestSuite is 11_000_000 uosmo +#[test_case( + 1_000_001, + 12_000_000, + false; + "deposit cap exceeded, should fail" +)] +#[test_case( + 999_999, + 12_000_000, + true; + "deposit cap not exceeded, should work" +)] +fn depositing_above_cap(amount_to_deposit: u128, deposit_cap: u128, exp_ok: bool) { let TestSuite { mut deps, denom, depositor_addr, + initial_market, .. } = setup_test(); - // set a deposit cap - MARKETS - .update(deps.as_mut().storage, denom, |opt| -> StdResult<_> { - let mut market = opt.unwrap(); - market.collateral_total_scaled = Uint128::new(9_000_000) * SCALING_FACTOR; - market.deposit_cap = Uint128::new(10_000_000); - Ok(market) - }) - .unwrap(); - - // try deposit with a big amount, should fail - let err = execute( - deps.as_mut(), - mock_env_at_block_time(10000100), - mock_info(depositor_addr.as_str(), &coins(1_000_001, denom)), - ExecuteMsg::Deposit { - on_behalf_of: None, + // set deposit cap + deps.querier.set_redbank_params( + denom, + AssetParams { + credit_manager: CmSettings { + whitelisted: false, + hls: None, + }, + red_bank: RedBankSettings { + deposit_enabled: true, + borrow_enabled: true, + }, + deposit_cap: Uint128::new(deposit_cap), + ..th_default_asset_params() }, - ) - .unwrap_err(); - assert_eq!( - err, - ContractError::DepositCapExceeded { - denom: denom.to_string() - } ); - // deposit a smaller amount, should work - let result = execute( + // try deposit with the given amount + let res = execute( deps.as_mut(), - mock_env_at_block_time(10000100), - mock_info(depositor_addr.as_str(), &coins(123, denom)), + mock_env_at_block_time(initial_market.indexes_last_updated), + mock_info(depositor_addr.as_str(), &coins(amount_to_deposit, denom)), ExecuteMsg::Deposit { + account_id: None, on_behalf_of: None, }, ); - assert!(result.is_ok()); + + if exp_ok { + assert!(res.is_ok()); + } else { + assert_eq!( + res, + Err(ContractError::DepositCapExceeded { + denom: denom.to_string(), + }), + ); + } } #[test] @@ -237,6 +300,7 @@ fn depositing_without_existing_position() { mock_env_at_block_time(block_time), mock_info(depositor_addr.as_str(), &coins(deposit_amount, denom)), ExecuteMsg::Deposit { + account_id: None, on_behalf_of: None, }, ) @@ -251,6 +315,7 @@ fn depositing_without_existing_position() { contract_addr: MarsAddressType::Incentives.to_string(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: depositor_addr.clone(), + account_id: None, denom: initial_market.denom.clone(), user_amount_scaled_before: Uint128::zero(), // NOTE: Protocol rewards accrued is zero, so here it's initial total supply @@ -286,7 +351,7 @@ fn depositing_without_existing_position() { // the depositor previously did not have a collateral position // a position should have been created with the correct scaled amount, and enabled by default - let collateral = COLLATERALS.load(deps.as_ref().storage, (&depositor_addr, denom)).unwrap(); + let collateral = COLLATERALS.load(deps.as_ref().storage, (&depositor_addr, "", denom)).unwrap(); assert_eq!( collateral, Collateral { @@ -327,6 +392,7 @@ fn depositing_with_existing_position() { mock_env_at_block_time(block_time), mock_info(depositor_addr.as_str(), &coins(deposit_amount, denom)), ExecuteMsg::Deposit { + account_id: None, on_behalf_of: None, }, ) @@ -341,6 +407,7 @@ fn depositing_with_existing_position() { contract_addr: MarsAddressType::Incentives.to_string(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: depositor_addr.clone(), + account_id: None, denom: initial_market.denom.clone(), user_amount_scaled_before: collateral_amount_scaled, // NOTE: Protocol rewards accrued is zero, so here it's initial total supply @@ -353,7 +420,7 @@ fn depositing_with_existing_position() { // the depositor's scaled collateral amount should have been increased // however, the `enabled` status should not been affected - let collateral = COLLATERALS.load(deps.as_ref().storage, (&depositor_addr, denom)).unwrap(); + let collateral = COLLATERALS.load(deps.as_ref().storage, (&depositor_addr, "", denom)).unwrap(); let expected = collateral_amount_scaled + expected_mint_amount; assert_eq!( collateral, @@ -398,6 +465,7 @@ fn depositing_on_behalf_of() { mock_env_at_block_time(block_time), mock_info(depositor_addr.as_str(), &coins(deposit_amount, denom)), ExecuteMsg::Deposit { + account_id: None, on_behalf_of: Some(on_behalf_of_addr.clone().into()), }, ) @@ -412,6 +480,7 @@ fn depositing_on_behalf_of() { contract_addr: MarsAddressType::Incentives.to_string(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: Addr::unchecked(MarsAddressType::RewardsCollector.to_string()), + account_id: None, denom: initial_market.denom.clone(), user_amount_scaled_before: Uint128::zero(), total_amount_scaled_before: initial_market.collateral_total_scaled, @@ -423,6 +492,7 @@ fn depositing_on_behalf_of() { contract_addr: MarsAddressType::Incentives.to_string(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: on_behalf_of_addr.clone(), + account_id: None, denom: initial_market.denom.clone(), user_amount_scaled_before: Uint128::zero(), // NOTE: New collateral shares were minted to the rewards collector first, so @@ -437,11 +507,12 @@ fn depositing_on_behalf_of() { ); // depositor should not have created a new collateral position - let opt = COLLATERALS.may_load(deps.as_ref().storage, (&depositor_addr, denom)).unwrap(); + let opt = COLLATERALS.may_load(deps.as_ref().storage, (&depositor_addr, "", denom)).unwrap(); assert!(opt.is_none()); // the recipient should have created a new collateral position - let collateral = COLLATERALS.load(deps.as_ref().storage, (&on_behalf_of_addr, denom)).unwrap(); + let collateral = + COLLATERALS.load(deps.as_ref().storage, (&on_behalf_of_addr, "", denom)).unwrap(); assert_eq!( collateral, Collateral { @@ -472,13 +543,15 @@ fn depositing_on_behalf_of_cannot_enable_collateral() { mock_env_at_block_time(block_time), mock_info(on_behalf_of_addr.as_str(), &coins(1u128, denom)), ExecuteMsg::Deposit { + account_id: None, on_behalf_of: None, }, ) .unwrap(); // 'on_behalf_of_addr' should have collateral enabled - let collateral = COLLATERALS.load(deps.as_ref().storage, (&on_behalf_of_addr, denom)).unwrap(); + let collateral = + COLLATERALS.load(deps.as_ref().storage, (&on_behalf_of_addr, "", denom)).unwrap(); assert!(collateral.enabled); // 'on_behalf_of_addr' disables asset as collateral @@ -494,7 +567,8 @@ fn depositing_on_behalf_of_cannot_enable_collateral() { .unwrap(); // verify asset is disabled as collateral for 'on_behalf_of_addr' - let collateral = COLLATERALS.load(deps.as_ref().storage, (&on_behalf_of_addr, denom)).unwrap(); + let collateral = + COLLATERALS.load(deps.as_ref().storage, (&on_behalf_of_addr, "", denom)).unwrap(); assert!(!collateral.enabled); // 'depositor_addr' deposits a small amount of funds to 'on_behalf_of_addr' to enable his asset as collateral @@ -503,12 +577,72 @@ fn depositing_on_behalf_of_cannot_enable_collateral() { mock_env_at_block_time(block_time), mock_info(depositor_addr.as_str(), &coins(1u128, denom)), ExecuteMsg::Deposit { + account_id: None, on_behalf_of: Some(on_behalf_of_addr.to_string()), }, ) .unwrap(); // 'on_behalf_of_addr' doesn't have the asset enabled as collateral - let collateral = COLLATERALS.load(deps.as_ref().storage, (&on_behalf_of_addr, denom)).unwrap(); + let collateral = + COLLATERALS.load(deps.as_ref().storage, (&on_behalf_of_addr, "", denom)).unwrap(); assert!(!collateral.enabled); } + +#[test] +fn depositing_on_behalf_of_credit_manager() { + let TestSuite { + mut deps, + denom, + depositor_addr, + .. + } = setup_test(); + + let err = execute( + deps.as_mut(), + mock_env(), + mock_info(depositor_addr.as_str(), &coins(123, denom)), + ExecuteMsg::Deposit { + account_id: None, + on_behalf_of: Some("credit_manager".to_string()), + }, + ) + .unwrap_err(); + assert_eq!(err, ContractError::Mars(MarsError::Unauthorized {})); +} + +#[test] +fn depositing_with_account_id_by_non_credit_manager_user() { + let TestSuite { + mut deps, + denom, + depositor_addr, + .. + } = setup_test(); + + // non-credit-manager user cannot deposit with account_id (even with empty string) + let err = execute( + deps.as_mut(), + mock_env(), + mock_info(depositor_addr.as_str(), &coins(123, denom)), + ExecuteMsg::Deposit { + account_id: Some("".to_string()), + on_behalf_of: None, + }, + ) + .unwrap_err(); + assert_eq!(err, ContractError::Mars(MarsError::Unauthorized {})); + + // non-credit-manager user cannot deposit with account_id + let err = execute( + deps.as_mut(), + mock_env(), + mock_info(depositor_addr.as_str(), &coins(123, denom)), + ExecuteMsg::Deposit { + account_id: Some("1234".to_string()), + on_behalf_of: None, + }, + ) + .unwrap_err(); + assert_eq!(err, ContractError::Mars(MarsError::Unauthorized {})); +} diff --git a/contracts/red-bank/tests/test_health.rs b/contracts/red-bank/tests/tests/test_health.rs similarity index 100% rename from contracts/red-bank/tests/test_health.rs rename to contracts/red-bank/tests/tests/test_health.rs diff --git a/contracts/red-bank/tests/tests/test_liquidate.rs b/contracts/red-bank/tests/tests/test_liquidate.rs new file mode 100644 index 000000000..3fdb852ce --- /dev/null +++ b/contracts/red-bank/tests/tests/test_liquidate.rs @@ -0,0 +1,1375 @@ +use std::{collections::HashMap, str::FromStr}; + +use cosmwasm_std::{ + attr, coin, + testing::{mock_dependencies, mock_env, mock_info}, + to_binary, Addr, Decimal, SubMsg, Uint128, WasmMsg, +}; +use cw_utils::PaymentError; +use mars_params::types::asset::{AssetParams, CmSettings, LiquidationBonus, RedBankSettings}; +use mars_red_bank::{contract::execute, error::ContractError}; +use mars_red_bank_types::{ + address_provider::MarsAddressType, + incentives, + red_bank::{ + ExecuteMsg, InitOrUpdateAssetParams, InterestRateModel, Market, QueryMsg, + UserCollateralResponse, UserDebtResponse, + }, +}; +use mars_testing::{ + integration::mock_env::{MockEnv, MockEnvBuilder}, + mock_env_at_block_time, +}; + +use super::helpers::{ + assert_err, liq_threshold_hf, merge_collaterals_and_debts, th_build_interests_updated_event, + th_get_expected_indices_and_rates, th_get_scaled_liquidity_amount, th_query, th_setup, + TestUtilizationDeltaInfo, +}; + +// NOTE: See spreadsheet with liquidation numbers for reference: +// contracts/red-bank/tests/files/Red Bank - Dynamic LB & CF test cases v1.1.xlsx + +#[test] +fn cannot_self_liquidate() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("liquidator", &[coin(100, "somecoin")]); + let msg = ExecuteMsg::Liquidate { + user: "liquidator".to_string(), + collateral_denom: "collateral".to_string(), + recipient: None, + }; + let error_res = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(error_res, ContractError::CannotLiquidateSelf {}); +} + +#[test] +fn liquidate_if_no_coins_sent() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("liquidator", &[]); + let msg = ExecuteMsg::Liquidate { + user: "user".to_string(), + collateral_denom: "collateral".to_string(), + recipient: None, + }; + let error_res = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(error_res, PaymentError::NoFunds {}.into()); +} + +#[test] +fn liquidate_if_many_coins_sent() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("liquidator", &[coin(100, "somecoin1"), coin(200, "somecoin2")]); + let msg = ExecuteMsg::Liquidate { + user: "user".to_string(), + collateral_denom: "collateral".to_string(), + recipient: None, + }; + let error_res = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(error_res, PaymentError::MultipleDenoms {}.into()); +} + +#[test] +fn liquidate_if_no_requested_collateral() { + let mut mock_env = MockEnvBuilder::new(None, Addr::unchecked("owner")).build(); + + let red_bank = mock_env.red_bank.clone(); + let oracle = mock_env.oracle.clone(); + + let (_, _, liquidatee, liquidator) = setup_env(&mut mock_env); + + // change price to be able to liquidate + oracle.set_price_source_fixed(&mut mock_env, "uosmo", Decimal::from_ratio(3u128, 1u128)); + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(85u128, 10u128)); + + // liquidate user + let error_res = red_bank.liquidate( + &mut mock_env, + &liquidator, + &liquidatee, + "other", + &[coin(1000, "uusdc")], + ); + assert_err(error_res, ContractError::CannotLiquidateWhenNoCollateralBalance {}); +} + +#[test] +fn liquidate_if_no_requested_debt() { + let mut mock_env = MockEnvBuilder::new(None, Addr::unchecked("owner")).build(); + + let red_bank = mock_env.red_bank.clone(); + let oracle = mock_env.oracle.clone(); + + let (_, _, liquidatee, liquidator) = setup_env(&mut mock_env); + + // change price to be able to liquidate + oracle.set_price_source_fixed(&mut mock_env, "uosmo", Decimal::from_ratio(3u128, 1u128)); + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(85u128, 10u128)); + + // liquidate user + let error_res = red_bank.liquidate( + &mut mock_env, + &liquidator, + &liquidatee, + "uosmo", + &[coin(1000, "other")], + ); + assert_err(error_res, ContractError::CannotLiquidateWhenNoDebtBalance {}); +} + +#[test] +fn liquidate_if_requested_collateral_disabled() { + let mut mock_env = MockEnvBuilder::new(None, Addr::unchecked("owner")).build(); + + let red_bank = mock_env.red_bank.clone(); + let oracle = mock_env.oracle.clone(); + + let (_, _, liquidatee, liquidator) = setup_env(&mut mock_env); + + // disable osmo collateral for liquidatee + red_bank.update_user_collateral_status(&mut mock_env, &liquidatee, "ujake", false); + + // change price to be able to liquidate + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(85u128, 10u128)); + + // liquidate user + let error_res = red_bank.liquidate( + &mut mock_env, + &liquidator, + &liquidatee, + "ujake", + &[coin(1000, "uusdc")], + ); + assert_err( + error_res, + ContractError::CannotLiquidateWhenCollateralUnset { + denom: "ujake".to_string(), + }, + ); +} + +#[test] +fn cannot_liquidate_healthy_position() { + let mut mock_env = MockEnvBuilder::new(None, Addr::unchecked("owner")) + .target_health_factor(Decimal::from_ratio(12u128, 10u128)) + .build(); + + let red_bank = mock_env.red_bank.clone(); + + let (_, _, liquidatee, liquidator) = setup_env(&mut mock_env); + + // liquidate user + let error_res = red_bank.liquidate( + &mut mock_env, + &liquidator, + &liquidatee, + "uosmo", + &[coin(1000, "uusdc")], + ); + assert_err(error_res, ContractError::CannotLiquidateHealthyPosition {}); +} + +#[test] +fn target_health_factor_reached_after_max_debt_repayed() { + let mut mock_env = MockEnvBuilder::new(None, Addr::unchecked("owner")) + .target_health_factor(Decimal::from_ratio(12u128, 10u128)) + .build(); + + let red_bank = mock_env.red_bank.clone(); + let oracle = mock_env.oracle.clone(); + let rewards_collector = mock_env.rewards_collector.clone(); + + let (funded_amt, provider, liquidatee, liquidator) = setup_env(&mut mock_env); + + // change price to be able to liquidate + oracle.set_price_source_fixed(&mut mock_env, "uosmo", Decimal::from_ratio(3u128, 1u128)); + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(85u128, 10u128)); + + // liquidatee should be liquidatable + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let prev_liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + + // liquidate user + let usdc_repay_amt = 2373; + red_bank + .liquidate( + &mut mock_env, + &liquidator, + &liquidatee, + "uosmo", + &[coin(usdc_repay_amt, "uusdc")], + ) + .unwrap(); + + // check provider positions + let provider_collaterals = red_bank.query_user_collaterals(&mut mock_env, &provider); + assert_eq!(provider_collaterals.len(), 2); + assert_eq!(provider_collaterals.get("uusdc").unwrap().amount.u128(), 1000000); + assert_eq!(provider_collaterals.get("untrn").unwrap().amount.u128(), 1000000); + let provider_debts = red_bank.query_user_debts(&mut mock_env, &provider); + assert_eq!(provider_debts.len(), 0); + + // check liquidatee positions + let liquidatee_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_collaterals.len(), 3); + assert_eq!(liquidatee_collaterals.get("uosmo").unwrap().amount.u128(), 2809); + assert_eq!(liquidatee_collaterals.get("ujake").unwrap().amount.u128(), 2000); + assert_eq!(liquidatee_collaterals.get("uatom").unwrap().amount.u128(), 900); + let liquidatee_debts = red_bank.query_user_debts(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_debts.len(), 2); + assert_eq!(liquidatee_debts.get("uusdc").unwrap().amount.u128(), 627); + assert_eq!(liquidatee_debts.get("untrn").unwrap().amount.u128(), 1200); + + // check liquidator positions + let liquidator_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidator); + assert_eq!(liquidator_collaterals.len(), 1); + assert_eq!(liquidator_collaterals.get("uosmo").unwrap().amount.u128(), 7182); + let liquidator_debts = red_bank.query_user_debts(&mut mock_env, &liquidator); + assert_eq!(liquidator_debts.len(), 0); + + // check rewards-collector positions (protocol fee) + let rc_collaterals = + red_bank.query_user_collaterals(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_collaterals.len(), 1); + assert_eq!(rc_collaterals.get("uosmo").unwrap().amount.u128(), 9); + let rc_debts = red_bank.query_user_debts(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_debts.len(), 0); + + let (merged_collaterals, merged_debts, merged_balances) = merge_collaterals_and_debts( + &[&provider_collaterals, &liquidatee_collaterals, &liquidator_collaterals, &rc_collaterals], + &[&provider_debts, &liquidatee_debts, &liquidator_debts, &rc_debts], + ); + + // check if users collaterals and debts are equal to markets scaled amounts + assert_users_and_markets_scaled_amounts(&mut mock_env, merged_collaterals, merged_debts); + + // check red bank underlying balances + assert_underlying_balances(&mock_env, merged_balances); + + // check liquidator account balance + let omso_liquidator_balance = mock_env.query_balance(&liquidator, "uosmo").unwrap(); + assert_eq!(omso_liquidator_balance.amount.u128(), funded_amt); + let usdc_liquidator_balance = mock_env.query_balance(&liquidator, "uusdc").unwrap(); + assert_eq!(usdc_liquidator_balance.amount.u128(), funded_amt - usdc_repay_amt); + + // liquidatee hf should improve + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + assert!(liq_threshold_hf > prev_liq_threshold_hf); + // it should be 1.2, but because of roundings it is hard to achieve an exact number + assert_eq!(liq_threshold_hf, Decimal::from_str("1.200016765864699471").unwrap()); +} + +#[test] +fn debt_amt_adjusted_to_total_debt_then_refund() { + let mut mock_env = MockEnvBuilder::new(None, Addr::unchecked("owner")) + .target_health_factor(Decimal::from_ratio(12u128, 10u128)) + .build(); + + let red_bank = mock_env.red_bank.clone(); + let oracle = mock_env.oracle.clone(); + let rewards_collector = mock_env.rewards_collector.clone(); + + let (funded_amt, provider, liquidatee, liquidator) = setup_env(&mut mock_env); + + // change price to be able to liquidate + oracle.set_price_source_fixed(&mut mock_env, "uosmo", Decimal::from_ratio(25u128, 10u128)); + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(755u128, 100u128)); + + // liquidatee should be liquidatable + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let prev_liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + + // liquidate user + let usdc_repay_amt = 3250; + red_bank + .liquidate( + &mut mock_env, + &liquidator, + &liquidatee, + "uosmo", + &[coin(usdc_repay_amt, "uusdc")], + ) + .unwrap(); + + // check provider positions + let provider_collaterals = red_bank.query_user_collaterals(&mut mock_env, &provider); + assert_eq!(provider_collaterals.len(), 2); + assert_eq!(provider_collaterals.get("uusdc").unwrap().amount.u128(), 1000000); + assert_eq!(provider_collaterals.get("untrn").unwrap().amount.u128(), 1000000); + let provider_debts = red_bank.query_user_debts(&mut mock_env, &provider); + assert_eq!(provider_debts.len(), 0); + + // check liquidatee positions (no usdc debt, fully repayed) + let liquidatee_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_collaterals.len(), 3); + assert_eq!(liquidatee_collaterals.get("uosmo").unwrap().amount.u128(), 34); + assert_eq!(liquidatee_collaterals.get("ujake").unwrap().amount.u128(), 2000); + assert_eq!(liquidatee_collaterals.get("uatom").unwrap().amount.u128(), 900); + let liquidatee_debts = red_bank.query_user_debts(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_debts.len(), 1); + assert_eq!(liquidatee_debts.get("untrn").unwrap().amount.u128(), 1200); + + // check liquidator positions + let liquidator_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidator); + assert_eq!(liquidator_collaterals.len(), 1); + assert_eq!(liquidator_collaterals.get("uosmo").unwrap().amount.u128(), 9948); + let liquidator_debts = red_bank.query_user_debts(&mut mock_env, &liquidator); + assert_eq!(liquidator_debts.len(), 0); + + // check rewards-collector positions (protocol fee) + let rc_collaterals = + red_bank.query_user_collaterals(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_collaterals.len(), 1); + assert_eq!(rc_collaterals.get("uosmo").unwrap().amount.u128(), 18); + let rc_debts = red_bank.query_user_debts(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_debts.len(), 0); + + let (merged_collaterals, merged_debts, merged_balances) = merge_collaterals_and_debts( + &[&provider_collaterals, &liquidatee_collaterals, &liquidator_collaterals, &rc_collaterals], + &[&provider_debts, &liquidatee_debts, &liquidator_debts, &rc_debts], + ); + + // check if users collaterals and debts are equal to markets scaled amounts + assert_users_and_markets_scaled_amounts(&mut mock_env, merged_collaterals, merged_debts); + + // check red bank underlying balances + assert_underlying_balances(&mock_env, merged_balances); + + // check liquidator account balance + let omso_liquidator_balance = mock_env.query_balance(&liquidator, "uosmo").unwrap(); + assert_eq!(omso_liquidator_balance.amount.u128(), funded_amt); + let usdc_liquidator_balance = mock_env.query_balance(&liquidator, "uusdc").unwrap(); + assert_eq!(usdc_liquidator_balance.amount.u128(), funded_amt - usdc_repay_amt + 250); // 250 refunded + + // liquidatee hf should improve + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + assert!(liq_threshold_hf > prev_liq_threshold_hf); +} + +#[test] +fn debt_amt_adjusted_to_max_allowed_by_requested_coin() { + let mut mock_env = MockEnvBuilder::new(None, Addr::unchecked("owner")) + .target_health_factor(Decimal::from_ratio(12u128, 10u128)) + .build(); + + let red_bank = mock_env.red_bank.clone(); + let oracle = mock_env.oracle.clone(); + let rewards_collector = mock_env.rewards_collector.clone(); + + let (funded_amt, provider, liquidatee, liquidator) = setup_env(&mut mock_env); + + // change price to be able to liquidate + oracle.set_price_source_fixed(&mut mock_env, "uosmo", Decimal::from_ratio(2u128, 1u128)); + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(64u128, 10u128)); + + // liquidatee should be liquidatable + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let prev_liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + + // liquidate user + let usdc_repay_amt = 2840; + red_bank + .liquidate( + &mut mock_env, + &liquidator, + &liquidatee, + "uosmo", + &[coin(usdc_repay_amt, "uusdc")], + ) + .unwrap(); + + // check provider positions + let provider_collaterals = red_bank.query_user_collaterals(&mut mock_env, &provider); + assert_eq!(provider_collaterals.len(), 2); + assert_eq!(provider_collaterals.get("uusdc").unwrap().amount.u128(), 1000000); + assert_eq!(provider_collaterals.get("untrn").unwrap().amount.u128(), 1000000); + let provider_debts = red_bank.query_user_debts(&mut mock_env, &provider); + assert_eq!(provider_debts.len(), 0); + + // check liquidatee positions + let liquidatee_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_collaterals.len(), 3); + assert_eq!(liquidatee_collaterals.get("uosmo").unwrap().amount.u128(), 4); + assert_eq!(liquidatee_collaterals.get("ujake").unwrap().amount.u128(), 2000); + assert_eq!(liquidatee_collaterals.get("uatom").unwrap().amount.u128(), 900); + let liquidatee_debts = red_bank.query_user_debts(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_debts.len(), 2); + assert_eq!(liquidatee_debts.get("uusdc").unwrap().amount.u128(), 160); + assert_eq!(liquidatee_debts.get("untrn").unwrap().amount.u128(), 1200); + + // check liquidator positions + let liquidator_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidator); + assert_eq!(liquidator_collaterals.len(), 1); + assert_eq!(liquidator_collaterals.get("uosmo").unwrap().amount.u128(), 9978); + let liquidator_debts = red_bank.query_user_debts(&mut mock_env, &liquidator); + assert_eq!(liquidator_debts.len(), 0); + + // check rewards-collector positions (protocol fee) + let rc_collaterals = + red_bank.query_user_collaterals(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_collaterals.len(), 1); + assert_eq!(rc_collaterals.get("uosmo").unwrap().amount.u128(), 18); + let rc_debts = red_bank.query_user_debts(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_debts.len(), 0); + + let (merged_collaterals, merged_debts, merged_balances) = merge_collaterals_and_debts( + &[&provider_collaterals, &liquidatee_collaterals, &liquidator_collaterals, &rc_collaterals], + &[&provider_debts, &liquidatee_debts, &liquidator_debts, &rc_debts], + ); + + // check if users collaterals and debts are equal to markets scaled amounts + assert_users_and_markets_scaled_amounts(&mut mock_env, merged_collaterals, merged_debts); + + // check red bank underlying balances + assert_underlying_balances(&mock_env, merged_balances); + + // check liquidator account balance + let omso_liquidator_balance = mock_env.query_balance(&liquidator, "uosmo").unwrap(); + assert_eq!(omso_liquidator_balance.amount.u128(), funded_amt); + let usdc_liquidator_balance = mock_env.query_balance(&liquidator, "uusdc").unwrap(); + assert_eq!(usdc_liquidator_balance.amount.u128(), funded_amt - usdc_repay_amt); + + // liquidatee hf should improve + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + assert!(liq_threshold_hf > prev_liq_threshold_hf); +} + +#[test] +fn debt_amt_no_adjustment_with_different_recipient() { + let mut mock_env = MockEnvBuilder::new(None, Addr::unchecked("owner")) + .target_health_factor(Decimal::from_ratio(12u128, 10u128)) + .build(); + + let red_bank = mock_env.red_bank.clone(); + let oracle = mock_env.oracle.clone(); + let rewards_collector = mock_env.rewards_collector.clone(); + + let (funded_amt, provider, liquidatee, liquidator) = setup_env(&mut mock_env); + + // change price to be able to liquidate + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(68u128, 10u128)); + + // liquidatee should be liquidatable + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let prev_liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + + // liquidate user + let usdc_repay_amt = 120; + let recipient = Addr::unchecked("recipient"); + red_bank + .liquidate_with_different_recipient( + &mut mock_env, + &liquidator, + &liquidatee, + "uosmo", + &[coin(usdc_repay_amt, "uusdc")], + Some(recipient.to_string()), + ) + .unwrap(); + + // check provider positions + let provider_collaterals = red_bank.query_user_collaterals(&mut mock_env, &provider); + assert_eq!(provider_collaterals.len(), 2); + assert_eq!(provider_collaterals.get("uusdc").unwrap().amount.u128(), 1000000); + assert_eq!(provider_collaterals.get("untrn").unwrap().amount.u128(), 1000000); + let provider_debts = red_bank.query_user_debts(&mut mock_env, &provider); + assert_eq!(provider_debts.len(), 0); + + // check liquidatee positions + let liquidatee_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_collaterals.len(), 3); + assert_eq!(liquidatee_collaterals.get("uosmo").unwrap().amount.u128(), 9593); + assert_eq!(liquidatee_collaterals.get("ujake").unwrap().amount.u128(), 2000); + assert_eq!(liquidatee_collaterals.get("uatom").unwrap().amount.u128(), 900); + let liquidatee_debts = red_bank.query_user_debts(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_debts.len(), 2); + assert_eq!(liquidatee_debts.get("uusdc").unwrap().amount.u128(), 2880); + assert_eq!(liquidatee_debts.get("untrn").unwrap().amount.u128(), 1200); + + // check liquidator positions + let liquidator_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidator); + assert_eq!(liquidator_collaterals.len(), 0); + let liquidator_debts = red_bank.query_user_debts(&mut mock_env, &liquidator); + assert_eq!(liquidator_debts.len(), 0); + + // check recipient positions + let recipient_collaterals = red_bank.query_user_collaterals(&mut mock_env, &recipient); + assert_eq!(recipient_collaterals.len(), 1); + assert_eq!(recipient_collaterals.get("uosmo").unwrap().amount.u128(), 407); + let recipient_debts = red_bank.query_user_debts(&mut mock_env, &recipient); + assert_eq!(recipient_debts.len(), 0); + + // check rewards-collector positions (protocol fee) + let rc_collaterals = + red_bank.query_user_collaterals(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_collaterals.len(), 0); + let rc_debts = red_bank.query_user_debts(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_debts.len(), 0); + + let (merged_collaterals, merged_debts, merged_balances) = merge_collaterals_and_debts( + &[ + &provider_collaterals, + &liquidatee_collaterals, + &liquidator_collaterals, + &recipient_collaterals, + &rc_collaterals, + ], + &[&provider_debts, &liquidatee_debts, &liquidator_debts, &recipient_debts, &rc_debts], + ); + + // check if users collaterals and debts are equal to markets scaled amounts + assert_users_and_markets_scaled_amounts(&mut mock_env, merged_collaterals, merged_debts); + + // check red bank underlying balances + assert_underlying_balances(&mock_env, merged_balances); + + // check liquidator account balance + let omso_liquidator_balance = mock_env.query_balance(&liquidator, "uosmo").unwrap(); + assert_eq!(omso_liquidator_balance.amount.u128(), funded_amt); + let usdc_liquidator_balance = mock_env.query_balance(&liquidator, "uusdc").unwrap(); + assert_eq!(usdc_liquidator_balance.amount.u128(), funded_amt - usdc_repay_amt); + + // liquidatee hf should improve + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + assert!(liq_threshold_hf > prev_liq_threshold_hf); +} + +#[test] +fn same_asset_for_debt_and_collateral_with_refund() { + let mut mock_env = MockEnvBuilder::new(None, Addr::unchecked("owner")) + .target_health_factor(Decimal::from_ratio(12u128, 10u128)) + .build(); + + let red_bank = mock_env.red_bank.clone(); + let params = mock_env.params.clone(); + let oracle = mock_env.oracle.clone(); + let rewards_collector = mock_env.rewards_collector.clone(); + + let funded_amt = 1_000_000_000_000u128; + let provider = Addr::unchecked("provider"); // provides collateral to be borrowed by others + let liquidatee = Addr::unchecked("liquidatee"); + let liquidator = Addr::unchecked("liquidator"); + + // setup red-bank + let (market_params, asset_params) = + default_asset_params_with("uosmo", Decimal::percent(70), Decimal::percent(78)); + red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = + default_asset_params_with("uatom", Decimal::percent(82), Decimal::percent(90)); + red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params); + params.init_params(&mut mock_env, asset_params); + + // setup oracle + oracle.set_price_source_fixed(&mut mock_env, "uosmo", Decimal::from_ratio(15u128, 10u128)); + oracle.set_price_source_fixed(&mut mock_env, "uatom", Decimal::from_ratio(10u128, 1u128)); + + // fund accounts + mock_env.fund_accounts(&[&provider, &liquidatee, &liquidator], funded_amt, &["uosmo", "uatom"]); + + // provider deposits collaterals + red_bank.deposit(&mut mock_env, &provider, coin(1000000, "uosmo")).unwrap(); + + // liquidatee deposits and borrows + red_bank.deposit(&mut mock_env, &liquidatee, coin(1000, "uosmo")).unwrap(); + red_bank.deposit(&mut mock_env, &liquidatee, coin(1000, "uatom")).unwrap(); + red_bank.borrow(&mut mock_env, &liquidatee, "uosmo", 3000).unwrap(); + + // change price to be able to liquidate + oracle.set_price_source_fixed(&mut mock_env, "uatom", Decimal::from_ratio(2u128, 1u128)); + + // liquidatee should be liquidatable + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let prev_liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + + // liquidate user + let osmo_repay_amt = 1000; + red_bank + .liquidate( + &mut mock_env, + &liquidator, + &liquidatee, + "uosmo", + &[coin(osmo_repay_amt, "uosmo")], + ) + .unwrap(); + + // check provider positions + let provider_collaterals = red_bank.query_user_collaterals(&mut mock_env, &provider); + assert_eq!(provider_collaterals.len(), 1); + assert_eq!(provider_collaterals.get("uosmo").unwrap().amount.u128(), 1000000); + let provider_debts = red_bank.query_user_debts(&mut mock_env, &provider); + assert_eq!(provider_debts.len(), 0); + + // check liquidatee positions + let liquidatee_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_collaterals.len(), 2); + assert_eq!(liquidatee_collaterals.get("uosmo").unwrap().amount.u128(), 1); + assert_eq!(liquidatee_collaterals.get("uatom").unwrap().amount.u128(), 1000); + let liquidatee_debts = red_bank.query_user_debts(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_debts.len(), 1); + assert_eq!(liquidatee_debts.get("uosmo").unwrap().amount.u128(), 2020); + + // check liquidator positions + let liquidator_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidator); + assert_eq!(liquidator_collaterals.len(), 1); + assert_eq!(liquidator_collaterals.get("uosmo").unwrap().amount.u128(), 999); + let liquidator_debts = red_bank.query_user_debts(&mut mock_env, &liquidator); + assert_eq!(liquidator_debts.len(), 0); + + // check rewards-collector positions (protocol fee) + let rc_collaterals = + red_bank.query_user_collaterals(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_collaterals.len(), 0); + let rc_debts = red_bank.query_user_debts(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_debts.len(), 0); + + let (merged_collaterals, merged_debts, merged_balances) = merge_collaterals_and_debts( + &[&provider_collaterals, &liquidatee_collaterals, &liquidator_collaterals, &rc_collaterals], + &[&provider_debts, &liquidatee_debts, &liquidator_debts, &rc_debts], + ); + + // check if users collaterals and debts are equal to markets scaled amounts + let markets = red_bank.query_markets(&mut mock_env); + assert_eq!(markets.len(), 2); + let osmo_market = markets.get("uosmo").unwrap(); + let atom_market = markets.get("uatom").unwrap(); + assert_eq!(merged_collaterals.get_or_default("uosmo"), osmo_market.collateral_total_scaled); + assert_eq!(merged_debts.get_or_default("uosmo"), osmo_market.debt_total_scaled); + assert_eq!(merged_collaterals.get_or_default("uatom"), atom_market.collateral_total_scaled); + assert_eq!(merged_debts.get_or_default("uatom"), atom_market.debt_total_scaled); + + // check red bank underlying balances + let balances = mock_env.query_all_balances(&red_bank.contract_addr); + assert_eq!(merged_balances.get("uosmo"), balances.get("uosmo")); + assert_eq!(merged_balances.get("uatom"), balances.get("uatom")); + + // check liquidator account balance + let usdc_liquidator_balance = mock_env.query_balance(&liquidator, "uosmo").unwrap(); + assert_eq!(usdc_liquidator_balance.amount.u128(), funded_amt - osmo_repay_amt + 20); // 20 refunded + + // liquidatee hf degradated + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + assert!(liq_threshold_hf < prev_liq_threshold_hf); +} + +#[test] +fn mdr_negative() { + let mut mock_env = MockEnvBuilder::new(None, Addr::unchecked("owner")) + .target_health_factor(Decimal::from_ratio(104u128, 100u128)) + .build(); + + let red_bank = mock_env.red_bank.clone(); + let params = mock_env.params.clone(); + let oracle = mock_env.oracle.clone(); + let rewards_collector = mock_env.rewards_collector.clone(); + + let funded_amt = 1_000_000_000_000u128; + let provider = Addr::unchecked("provider"); // provides collateral to be borrowed by others + let liquidatee = Addr::unchecked("liquidatee"); + let liquidator = Addr::unchecked("liquidator"); + + // setup red-bank + let (market_params, asset_params) = _default_asset_params_with( + "uosmo", + Decimal::percent(70), + Decimal::percent(98), + LiquidationBonus { + starting_lb: Decimal::percent(10), + slope: Decimal::from_str("2.0").unwrap(), + min_lb: Decimal::percent(10), + max_lb: Decimal::percent(10), + }, + ); + red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = + default_asset_params_with("ujake", Decimal::percent(50), Decimal::percent(55)); + red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = + default_asset_params_with("uusdc", Decimal::percent(82), Decimal::percent(90)); + red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params); + params.init_params(&mut mock_env, asset_params); + + // setup oracle + oracle.set_price_source_fixed(&mut mock_env, "uosmo", Decimal::from_ratio(3u128, 1u128)); + oracle.set_price_source_fixed(&mut mock_env, "ujake", Decimal::one()); + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(2u128, 1u128)); + + // fund accounts + mock_env.fund_accounts( + &[&provider, &liquidatee, &liquidator], + funded_amt, + &["uosmo", "ujake", "uusdc"], + ); + + // provider deposits collaterals + red_bank.deposit(&mut mock_env, &provider, coin(1000000, "uusdc")).unwrap(); + + // liquidatee deposits and borrows + red_bank.deposit(&mut mock_env, &liquidatee, coin(10000, "uosmo")).unwrap(); + red_bank.deposit(&mut mock_env, &liquidatee, coin(2000, "ujake")).unwrap(); + red_bank.borrow(&mut mock_env, &liquidatee, "uusdc", 3000).unwrap(); + + // change price to be able to liquidate + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(12u128, 1u128)); + + // liquidatee should be liquidatable + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let prev_liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + + // liquidate user + let usdc_repay_amt = 3000; + red_bank + .liquidate( + &mut mock_env, + &liquidator, + &liquidatee, + "uosmo", + &[coin(usdc_repay_amt, "uusdc")], + ) + .unwrap(); + + // check provider positions + let provider_collaterals = red_bank.query_user_collaterals(&mut mock_env, &provider); + assert_eq!(provider_collaterals.len(), 1); + assert_eq!(provider_collaterals.get("uusdc").unwrap().amount.u128(), 1000000); + let provider_debts = red_bank.query_user_debts(&mut mock_env, &provider); + assert_eq!(provider_debts.len(), 0); + + // check liquidatee positions + let liquidatee_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_collaterals.len(), 2); + assert_eq!(liquidatee_collaterals.get("uosmo").unwrap().amount.u128(), 4); + assert_eq!(liquidatee_collaterals.get("ujake").unwrap().amount.u128(), 2000); + let liquidatee_debts = red_bank.query_user_debts(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_debts.len(), 1); + assert_eq!(liquidatee_debts.get("uusdc").unwrap().amount.u128(), 728); + + // check liquidator positions + let liquidator_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidator); + assert_eq!(liquidator_collaterals.len(), 1); + assert_eq!(liquidator_collaterals.get("uosmo").unwrap().amount.u128(), 9978); + let liquidator_debts = red_bank.query_user_debts(&mut mock_env, &liquidator); + assert_eq!(liquidator_debts.len(), 0); + + // check rewards-collector positions (protocol fee) + let rc_collaterals = + red_bank.query_user_collaterals(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_collaterals.len(), 1); + assert_eq!(rc_collaterals.get("uosmo").unwrap().amount.u128(), 18); + let rc_debts = red_bank.query_user_debts(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_debts.len(), 0); + + let (merged_collaterals, merged_debts, merged_balances) = merge_collaterals_and_debts( + &[&provider_collaterals, &liquidatee_collaterals, &liquidator_collaterals, &rc_collaterals], + &[&provider_debts, &liquidatee_debts, &liquidator_debts, &rc_debts], + ); + + // check if users collaterals and debts are equal to markets scaled amounts + let markets = red_bank.query_markets(&mut mock_env); + assert_eq!(markets.len(), 3); + let osmo_market = markets.get("uosmo").unwrap(); + let jake_market = markets.get("ujake").unwrap(); + let usdc_market = markets.get("uusdc").unwrap(); + assert_eq!(merged_collaterals.get_or_default("uosmo"), osmo_market.collateral_total_scaled); + assert_eq!(merged_debts.get_or_default("uosmo"), osmo_market.debt_total_scaled); + assert_eq!(merged_collaterals.get_or_default("ujake"), jake_market.collateral_total_scaled); + assert_eq!(merged_debts.get_or_default("ujake"), jake_market.debt_total_scaled); + assert_eq!(merged_collaterals.get_or_default("uusdc"), usdc_market.collateral_total_scaled); + assert_eq!(merged_debts.get_or_default("uusdc"), usdc_market.debt_total_scaled); + + // check red bank underlying balances + let balances = mock_env.query_all_balances(&red_bank.contract_addr); + assert_eq!(merged_balances.get("uosmo"), balances.get("uosmo")); + assert_eq!(merged_balances.get("ujake"), balances.get("ujake")); + assert_eq!(merged_balances.get("uusdc"), balances.get("uusdc")); + + // check liquidator account balance + let usdc_liquidator_balance = mock_env.query_balance(&liquidator, "uusdc").unwrap(); + assert_eq!(usdc_liquidator_balance.amount.u128(), funded_amt - usdc_repay_amt + 728); // 728 refunded + + // liquidatee hf degradated + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + assert!(liq_threshold_hf < prev_liq_threshold_hf); +} + +#[test] +fn liquidate_uncollateralized_loan() { + let owner = Addr::unchecked("owner"); + let mut mock_env = MockEnvBuilder::new(None, owner.clone()).build(); + + // setup oracle and red-bank + let oracle = mock_env.oracle.clone(); + oracle.set_price_source_fixed(&mut mock_env, "uatom", Decimal::from_ratio(14u128, 1u128)); + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::one()); + let red_bank = mock_env.red_bank.clone(); + let params = mock_env.params.clone(); + let (market_params, asset_params) = + default_asset_params_with("uusdc", Decimal::percent(70), Decimal::percent(78)); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = + default_asset_params_with("uatom", Decimal::percent(70), Decimal::percent(78)); + red_bank.init_asset(&mut mock_env, "uatom", market_params); + params.init_params(&mut mock_env, asset_params); + + // fund provider account with usdc + let provider = Addr::unchecked("provider"); + let funded_usdc = 1_000_000_000_000u128; + mock_env.fund_account(&provider, &[coin(1_000_000_000_000u128, "uusdc")]); + + // fund provider account with usdc + let liquidator = Addr::unchecked("liquidator"); + mock_env.fund_account(&liquidator, &[coin(1_000_000_000_000u128, "uusdc")]); + + // deposits usdc to redbank + red_bank.deposit(&mut mock_env, &provider, coin(funded_usdc, "uusdc")).unwrap(); + + let borrower = Addr::unchecked("borrower"); + + // set uncollateralized loan limit for borrower + red_bank + .update_uncollateralized_loan_limit( + &mut mock_env, + &owner, + &borrower, + "uusdc", + Uint128::from(10_000_000_000u128), + ) + .unwrap(); + + // borrower borrows usdc + let borrow_amount = 98_000_000u128; + red_bank.borrow(&mut mock_env, &borrower, "uusdc", borrow_amount).unwrap(); + let balance = mock_env.query_balance(&borrower, "uusdc").unwrap(); + assert_eq!(balance.amount.u128(), borrow_amount); + + // try to liquidate, should fail because there are no collateralized loans + let error_res = red_bank.liquidate( + &mut mock_env, + &liquidator, + &borrower, + "uatom", + &[coin(borrow_amount, "uusdc")], + ); + assert_err(error_res, ContractError::CannotLiquidateWhenPositiveUncollateralizedLoanLimit {}); +} + +#[test] +fn response_verification() { + let provider = Addr::unchecked("provider"); // provides collateral to be borrowed by others + let liquidatee = Addr::unchecked("liquidatee"); + let liquidator = Addr::unchecked("liquidator"); + + let mut deps = th_setup(&[]); + + let env = mock_env_at_block_time(100_000); + let info = mock_info("owner", &[]); + + let (market_params, asset_params) = + default_asset_params_with("uosmo", Decimal::percent(70), Decimal::percent(78)); + execute( + deps.as_mut(), + env.clone(), + info.clone(), + ExecuteMsg::InitAsset { + denom: asset_params.denom.clone(), + params: market_params, + }, + ) + .unwrap(); + deps.querier.set_redbank_params(&asset_params.denom.clone(), asset_params); + let (market_params, asset_params) = + default_asset_params_with("uatom", Decimal::percent(82), Decimal::percent(90)); + execute( + deps.as_mut(), + env.clone(), + info.clone(), + ExecuteMsg::InitAsset { + denom: asset_params.denom.clone(), + params: market_params, + }, + ) + .unwrap(); + deps.querier.set_redbank_params(&asset_params.denom.clone(), asset_params); + let (market_params, asset_params) = + default_asset_params_with("uusdc", Decimal::percent(90), Decimal::percent(95)); + execute( + deps.as_mut(), + env.clone(), + info.clone(), + ExecuteMsg::InitAsset { + denom: asset_params.denom.clone(), + params: market_params, + }, + ) + .unwrap(); + deps.querier.set_redbank_params(&asset_params.denom.clone(), asset_params); + let (market_params, asset_params) = + default_asset_params_with("untrn", Decimal::percent(90), Decimal::percent(96)); + execute( + deps.as_mut(), + env.clone(), + info, + ExecuteMsg::InitAsset { + denom: asset_params.denom.clone(), + params: market_params, + }, + ) + .unwrap(); + deps.querier.set_redbank_params(&asset_params.denom.clone(), asset_params); + + deps.querier.set_oracle_price("uosmo", Decimal::from_ratio(4u128, 1u128)); + deps.querier.set_oracle_price("uatom", Decimal::from_ratio(82u128, 10u128)); + deps.querier.set_oracle_price("uusdc", Decimal::from_ratio(68u128, 10u128)); + deps.querier.set_oracle_price("untrn", Decimal::from_ratio(55u128, 10u128)); + + // no deposit yet, initialize total deposit to zero + deps.querier.set_total_deposit("uosmo", Uint128::zero()); + deps.querier.set_total_deposit("uatom", Uint128::zero()); + deps.querier.set_total_deposit("uusdc", Uint128::zero()); + deps.querier.set_total_deposit("untrn", Uint128::zero()); + + // provider deposits collaterals + execute( + deps.as_mut(), + env.clone(), + mock_info(provider.as_str(), &[coin(1000000, "uusdc")]), + ExecuteMsg::Deposit { + account_id: None, + on_behalf_of: None, + }, + ) + .unwrap(); + execute( + deps.as_mut(), + env.clone(), + mock_info(provider.as_str(), &[coin(1000000, "untrn")]), + ExecuteMsg::Deposit { + account_id: None, + on_behalf_of: None, + }, + ) + .unwrap(); + + // liquidatee deposits and borrows + execute( + deps.as_mut(), + env.clone(), + mock_info(liquidatee.as_str(), &[coin(10000, "uosmo")]), + ExecuteMsg::Deposit { + account_id: None, + on_behalf_of: None, + }, + ) + .unwrap(); + execute( + deps.as_mut(), + env.clone(), + mock_info(liquidatee.as_str(), &[coin(900, "uatom")]), + ExecuteMsg::Deposit { + account_id: None, + on_behalf_of: None, + }, + ) + .unwrap(); + execute( + deps.as_mut(), + env.clone(), + mock_info(liquidatee.as_str(), &[]), + ExecuteMsg::Borrow { + denom: "uusdc".to_string(), + amount: Uint128::from(3000u128), + recipient: None, + }, + ) + .unwrap(); + execute( + deps.as_mut(), + env, + mock_info(liquidatee.as_str(), &[]), + ExecuteMsg::Borrow { + denom: "untrn".to_string(), + amount: Uint128::from(1200u128), + recipient: None, + }, + ) + .unwrap(); + + // change price to be able to liquidate + deps.querier.set_oracle_price("uosmo", Decimal::from_ratio(2u128, 1u128)); + + let collateral_market: Market = th_query( + deps.as_ref(), + QueryMsg::Market { + denom: "uosmo".to_string(), + }, + ); + let debt_market: Market = th_query( + deps.as_ref(), + QueryMsg::Market { + denom: "uusdc".to_string(), + }, + ); + let liquidatee_collateral: UserCollateralResponse = th_query( + deps.as_ref(), + QueryMsg::UserCollateral { + user: liquidatee.to_string(), + account_id: None, + denom: "uosmo".to_string(), + }, + ); + let liquidatee_debt: UserDebtResponse = th_query( + deps.as_ref(), + QueryMsg::UserDebt { + user: liquidatee.to_string(), + denom: "uusdc".to_string(), + }, + ); + + let debt_to_repay = 2883_u128; + let block_time = 500_000; + let env = mock_env_at_block_time(block_time); + let info = mock_info(liquidator.as_str(), &[coin(debt_to_repay, "uusdc")]); + let res = execute( + deps.as_mut(), + env, + info, + ExecuteMsg::Liquidate { + user: liquidatee.to_string(), + collateral_denom: "uosmo".to_string(), + recipient: None, + }, + ) + .unwrap(); + + let expected_debt_rates = th_get_expected_indices_and_rates( + &debt_market, + block_time, + TestUtilizationDeltaInfo { + less_debt: Uint128::new(2883u128), + user_current_debt_scaled: liquidatee_debt.amount_scaled, + ..Default::default() + }, + ); + + let expected_collateral_rates = th_get_expected_indices_and_rates( + &collateral_market, + block_time, + TestUtilizationDeltaInfo::default(), + ); + + let debt_market_after: Market = th_query( + deps.as_ref(), + QueryMsg::Market { + denom: "uusdc".to_string(), + }, + ); + + assert_eq!(debt_market_after.borrow_index, expected_debt_rates.borrow_index); + assert_eq!(debt_market_after.liquidity_index, expected_debt_rates.liquidity_index); + + mars_testing::assert_eq_vec( + res.attributes, + vec![ + attr("action", "liquidate"), + attr("user", liquidatee.as_str()), + attr("liquidator", liquidator.as_str()), + attr("recipient", liquidator.as_str()), + attr("collateral_denom", "uosmo"), + attr("collateral_amount", Uint128::new(9998u128)), + attr( + "collateral_amount_scaled", + th_get_scaled_liquidity_amount( + Uint128::new(9998u128), + expected_collateral_rates.liquidity_index, + ), + ), + attr("debt_denom", "uusdc"), + attr("debt_amount", Uint128::new(2883u128)), + attr("debt_amount_scaled", expected_debt_rates.less_debt_scaled), + ], + ); + + assert_eq!(res.events, vec![th_build_interests_updated_event("uusdc", &expected_debt_rates)]); + + let expected_msgs = expected_messages( + &liquidatee, + &liquidator, + liquidatee_collateral.amount_scaled, + Uint128::zero(), + &collateral_market, + &debt_market, + ); + assert_eq!(res.messages, expected_msgs); +} + +#[test] +fn liquidation_uses_correct_price_kind() { + let mut mock_env = MockEnvBuilder::new(None, Addr::unchecked("owner")) + .target_health_factor(Decimal::from_ratio(12u128, 10u128)) + .build(); + + let red_bank = mock_env.red_bank.clone(); + let oracle = mock_env.oracle.clone(); + let pyth = mock_env.pyth.clone(); + + let (_funded_amt, provider, liquidatee, liquidator) = setup_env(&mut mock_env); + + // change price to be able to liquidate + oracle.set_price_source_fixed(&mut mock_env, "usd", Decimal::from_str("1000000").unwrap()); + oracle.set_price_source_pyth( + &mut mock_env, + "uusdc", + pyth.to_string(), + Decimal::percent(10u64), + Decimal::percent(15u64), + ); + + // liquidation should succeed because it uses simpler pricing for Pyth + red_bank + .liquidate(&mut mock_env, &liquidator, &liquidatee, "uosmo", &[coin(120, "uusdc")]) + .unwrap(); + + // confidence is higher than max_confidence so borrow will fail + red_bank.borrow(&mut mock_env, &provider, "uusdc", 300).unwrap_err(); +} + +// recipient - can be liquidator or another address which can receive collateral +fn expected_messages( + user_addr: &Addr, + recipient_addr: &Addr, + user_collateral_scaled: Uint128, + recipient_collateral_scaled: Uint128, + collateral_market: &Market, + debt_market: &Market, +) -> Vec { + // there should be up to three messages updating indices at the incentives contract, in the + // order: + // - collateral denom, user + // - collateral denom, liquidator + // - debt denom, rewards collector (if rewards accrued > 0) + // + // NOTE that we don't expect a message to update rewards collector's index of the + // **collateral** asset, because the liquidation action does NOT change the collateral + // asset's utilization rate, it's interest rate does not need to be updated. + vec![ + SubMsg::new(WasmMsg::Execute { + contract_addr: MarsAddressType::Incentives.to_string(), + msg: to_binary(&incentives::ExecuteMsg::BalanceChange { + user_addr: user_addr.clone(), + account_id: None, + denom: collateral_market.denom.clone(), + user_amount_scaled_before: user_collateral_scaled, + total_amount_scaled_before: collateral_market.collateral_total_scaled, + }) + .unwrap(), + funds: vec![], + }), + SubMsg::new(WasmMsg::Execute { + contract_addr: MarsAddressType::Incentives.to_string(), + msg: to_binary(&incentives::ExecuteMsg::BalanceChange { + user_addr: recipient_addr.clone(), + account_id: None, + denom: collateral_market.denom.clone(), + user_amount_scaled_before: recipient_collateral_scaled, + total_amount_scaled_before: collateral_market.collateral_total_scaled, + }) + .unwrap(), + funds: vec![], + }), + SubMsg::new(WasmMsg::Execute { + contract_addr: MarsAddressType::Incentives.to_string(), + msg: to_binary(&incentives::ExecuteMsg::BalanceChange { + user_addr: Addr::unchecked(MarsAddressType::RewardsCollector.to_string()), + account_id: None, + denom: collateral_market.denom.clone(), + user_amount_scaled_before: Uint128::zero(), + total_amount_scaled_before: collateral_market.collateral_total_scaled, + }) + .unwrap(), + funds: vec![], + }), + SubMsg::new(WasmMsg::Execute { + contract_addr: MarsAddressType::Incentives.to_string(), + msg: to_binary(&incentives::ExecuteMsg::BalanceChange { + user_addr: Addr::unchecked(MarsAddressType::RewardsCollector.to_string()), + account_id: None, + denom: debt_market.denom.clone(), + user_amount_scaled_before: Uint128::zero(), + total_amount_scaled_before: debt_market.collateral_total_scaled, + }) + .unwrap(), + funds: vec![], + }), + ] +} + +fn setup_env(mock_env: &mut MockEnv) -> (u128, Addr, Addr, Addr) { + let funded_amt = 1_000_000_000_000u128; + let provider = Addr::unchecked("provider"); // provides collateral to be borrowed by others + let liquidatee = Addr::unchecked("liquidatee"); + let liquidator = Addr::unchecked("liquidator"); + + // setup red-bank + let red_bank = mock_env.red_bank.clone(); + let params = mock_env.params.clone(); + let (market_params, asset_params) = + default_asset_params_with("uosmo", Decimal::percent(70), Decimal::percent(78)); + red_bank.init_asset(mock_env, &asset_params.denom, market_params); + params.init_params(mock_env, asset_params); + let (market_params, asset_params) = + default_asset_params_with("ujake", Decimal::percent(50), Decimal::percent(55)); + red_bank.init_asset(mock_env, &asset_params.denom, market_params); + params.init_params(mock_env, asset_params); + let (market_params, asset_params) = + default_asset_params_with("uatom", Decimal::percent(82), Decimal::percent(90)); + red_bank.init_asset(mock_env, &asset_params.denom, market_params); + params.init_params(mock_env, asset_params); + let (market_params, asset_params) = + default_asset_params_with("uusdc", Decimal::percent(90), Decimal::percent(95)); + red_bank.init_asset(mock_env, &asset_params.denom, market_params); + params.init_params(mock_env, asset_params); + let (market_params, asset_params) = + default_asset_params_with("untrn", Decimal::percent(90), Decimal::percent(96)); + red_bank.init_asset(mock_env, &asset_params.denom, market_params); + params.init_params(mock_env, asset_params); + + // setup oracle + let oracle = mock_env.oracle.clone(); + oracle.set_price_source_fixed(mock_env, "uosmo", Decimal::from_ratio(22u128, 10u128)); + oracle.set_price_source_fixed(mock_env, "ujake", Decimal::one()); + oracle.set_price_source_fixed(mock_env, "uatom", Decimal::from_ratio(82u128, 10u128)); + oracle.set_price_source_fixed(mock_env, "uusdc", Decimal::one()); + oracle.set_price_source_fixed(mock_env, "untrn", Decimal::from_ratio(55u128, 10u128)); + + // fund accounts + mock_env.fund_accounts( + &[&provider, &liquidatee, &liquidator], + funded_amt, + &["uosmo", "ujake", "uatom", "uusdc", "untrn", "other"], + ); + + // provider deposits collaterals + red_bank.deposit(mock_env, &provider, coin(1000000, "uusdc")).unwrap(); + red_bank.deposit(mock_env, &provider, coin(1000000, "untrn")).unwrap(); + + // liquidatee deposits and borrows + red_bank.deposit(mock_env, &liquidatee, coin(10000, "uosmo")).unwrap(); + red_bank.deposit(mock_env, &liquidatee, coin(2000, "ujake")).unwrap(); + red_bank.deposit(mock_env, &liquidatee, coin(900, "uatom")).unwrap(); + red_bank.borrow(mock_env, &liquidatee, "uusdc", 3000).unwrap(); + red_bank.borrow(mock_env, &liquidatee, "untrn", 1200).unwrap(); + + (funded_amt, provider, liquidatee, liquidator) +} + +fn assert_users_and_markets_scaled_amounts( + mock_env: &mut MockEnv, + merged_collaterals: HashMap, + merged_debts: HashMap, +) { + let red_bank = mock_env.red_bank.clone(); + + let markets = red_bank.query_markets(mock_env); + assert_eq!(markets.len(), 5); + let osmo_market = markets.get("uosmo").unwrap(); + let jake_market = markets.get("ujake").unwrap(); + let atom_market = markets.get("uatom").unwrap(); + let usdc_market = markets.get("uusdc").unwrap(); + let ntrn_market = markets.get("untrn").unwrap(); + assert_eq!(merged_collaterals.get_or_default("uosmo"), osmo_market.collateral_total_scaled); + assert_eq!(merged_debts.get_or_default("uosmo"), osmo_market.debt_total_scaled); + assert_eq!(merged_collaterals.get_or_default("ujake"), jake_market.collateral_total_scaled); + assert_eq!(merged_debts.get_or_default("ujake"), jake_market.debt_total_scaled); + assert_eq!(merged_collaterals.get_or_default("uatom"), atom_market.collateral_total_scaled); + assert_eq!(merged_debts.get_or_default("uatom"), atom_market.debt_total_scaled); + assert_eq!(merged_collaterals.get_or_default("uusdc"), usdc_market.collateral_total_scaled); + assert_eq!(merged_debts.get_or_default("uusdc"), usdc_market.debt_total_scaled); + assert_eq!(merged_collaterals.get_or_default("untrn"), ntrn_market.collateral_total_scaled); + assert_eq!(merged_debts.get_or_default("untrn"), ntrn_market.debt_total_scaled); +} + +fn assert_underlying_balances(mock_env: &MockEnv, merged_balances: HashMap) { + let red_bank = mock_env.red_bank.clone(); + + let balances = mock_env.query_all_balances(&red_bank.contract_addr); + assert_eq!(merged_balances.get("uosmo"), balances.get("uosmo")); + assert_eq!(merged_balances.get("ujake"), balances.get("ujake")); + assert_eq!(merged_balances.get("uatom"), balances.get("uatom")); + assert_eq!(merged_balances.get("uusdc"), balances.get("uusdc")); + assert_eq!(merged_balances.get("untrn"), balances.get("untrn")); +} + +fn default_asset_params_with( + denom: &str, + max_loan_to_value: Decimal, + liquidation_threshold: Decimal, +) -> (InitOrUpdateAssetParams, AssetParams) { + _default_asset_params_with( + denom, + max_loan_to_value, + liquidation_threshold, + LiquidationBonus { + starting_lb: Decimal::percent(1), + slope: Decimal::from_str("2.0").unwrap(), + min_lb: Decimal::percent(2), + max_lb: Decimal::percent(10), + }, + ) +} + +fn _default_asset_params_with( + denom: &str, + max_loan_to_value: Decimal, + liquidation_threshold: Decimal, + liquidation_bonus: LiquidationBonus, +) -> (InitOrUpdateAssetParams, AssetParams) { + let market_params = InitOrUpdateAssetParams { + reserve_factor: Some(Decimal::percent(20)), + interest_rate_model: Some(InterestRateModel { + optimal_utilization_rate: Decimal::percent(10), + base: Decimal::percent(30), + slope_1: Decimal::percent(25), + slope_2: Decimal::percent(30), + }), + }; + let asset_params = AssetParams { + denom: denom.to_string(), + credit_manager: CmSettings { + whitelisted: false, + hls: None, + }, + red_bank: RedBankSettings { + deposit_enabled: true, + borrow_enabled: true, + }, + max_loan_to_value, + liquidation_threshold, + liquidation_bonus, + protocol_liquidation_fee: Decimal::percent(2), + deposit_cap: Uint128::MAX, + }; + (market_params, asset_params) +} + +trait MapDefaultValue { + fn get_or_default(&self, key: &str) -> Uint128; +} + +impl MapDefaultValue for HashMap { + fn get_or_default(&self, key: &str) -> Uint128 { + self.get(key).cloned().unwrap_or(Uint128::zero()) + } +} diff --git a/contracts/red-bank/tests/test_misc.rs b/contracts/red-bank/tests/tests/test_misc.rs similarity index 91% rename from contracts/red-bank/tests/test_misc.rs rename to contracts/red-bank/tests/tests/test_misc.rs index 78ec0db60..a8b37f3e4 100644 --- a/contracts/red-bank/tests/test_misc.rs +++ b/contracts/red-bank/tests/tests/test_misc.rs @@ -1,27 +1,26 @@ use cosmwasm_std::{ attr, coin, coins, testing::mock_info, Addr, BankMsg, CosmosMsg, Decimal, SubMsg, Uint128, }; -use helpers::{ - has_collateral_enabled, has_collateral_position, has_debt_position, set_collateral, set_debt, - th_build_interests_updated_event, th_get_expected_indices_and_rates, th_init_market, th_setup, - TestUtilizationDeltaInfo, +use mars_interest_rate::{ + compute_scaled_amount, compute_underlying_amount, get_scaled_debt_amount, + get_updated_liquidity_index, ScalingOperation, SCALING_FACTOR, }; use mars_owner::OwnerError::NotOwner; +use mars_params::types::asset::AssetParams; use mars_red_bank::{ contract::execute, error::ContractError, health, - interest_rates::{ - compute_scaled_amount, compute_underlying_amount, get_scaled_debt_amount, - get_updated_liquidity_index, ScalingOperation, SCALING_FACTOR, - }, state::{DEBTS, MARKETS, UNCOLLATERALIZED_LOAN_LIMITS}, }; use mars_red_bank_types::red_bank::{Debt, ExecuteMsg, Market}; use mars_testing::{mock_env, mock_env_at_block_time, MockEnvParams}; -use mars_utils::math; -mod helpers; +use super::helpers::{ + has_collateral_enabled, has_collateral_position, has_debt_position, set_collateral, set_debt, + th_build_interests_updated_event, th_default_asset_params, th_get_expected_indices_and_rates, + th_init_market, th_setup, TestUtilizationDeltaInfo, +}; #[test] fn uncollateralized_loan_limits() { @@ -42,6 +41,7 @@ fn uncollateralized_loan_limits() { // should get index 0 let market_initial = th_init_market(deps.as_mut(), "somecoin", &mock_market); + deps.querier.set_redbank_params("somecoin", th_default_asset_params()); let mut block_time = mock_market.indexes_last_updated + 10000u64; let initial_uncollateralized_loan_limit = Uint128::from(2400_u128); @@ -207,24 +207,18 @@ fn update_asset_collateral() { let mock_market_1 = Market { liquidity_index: Decimal::one(), borrow_index: Decimal::one(), - max_loan_to_value: Decimal::from_ratio(40u128, 100u128), - liquidation_threshold: Decimal::from_ratio(60u128, 100u128), ..Default::default() }; let denom_2 = "depositedcoin2"; let mock_market_2 = Market { liquidity_index: Decimal::from_ratio(1u128, 2u128), borrow_index: Decimal::one(), - max_loan_to_value: Decimal::from_ratio(50u128, 100u128), - liquidation_threshold: Decimal::from_ratio(80u128, 100u128), ..Default::default() }; let denom_3 = "depositedcoin3"; let mock_market_3 = Market { liquidity_index: Decimal::one(), borrow_index: Decimal::from_ratio(2u128, 1u128), - max_loan_to_value: Decimal::from_ratio(20u128, 100u128), - liquidation_threshold: Decimal::from_ratio(40u128, 100u128), ..Default::default() }; @@ -232,6 +226,25 @@ fn update_asset_collateral() { let market_2_initial = th_init_market(deps.as_mut(), denom_2, &mock_market_2); let market_3_initial = th_init_market(deps.as_mut(), denom_3, &mock_market_3); + let asset_params_1 = AssetParams { + max_loan_to_value: Decimal::from_ratio(40u128, 100u128), + liquidation_threshold: Decimal::from_ratio(60u128, 100u128), + ..th_default_asset_params() + }; + deps.querier.set_redbank_params(denom_1, asset_params_1.clone()); + let asset_params_2 = AssetParams { + max_loan_to_value: Decimal::from_ratio(50u128, 100u128), + liquidation_threshold: Decimal::from_ratio(80u128, 100u128), + ..th_default_asset_params() + }; + deps.querier.set_redbank_params(denom_2, asset_params_2.clone()); + let asset_params_3 = AssetParams { + max_loan_to_value: Decimal::from_ratio(20u128, 100u128), + liquidation_threshold: Decimal::from_ratio(40u128, 100u128), + ..th_default_asset_params() + }; + deps.querier.set_redbank_params(denom_3, asset_params_3); + // Set the querier to return exchange rates let token_1_exchange_rate = Decimal::from_ratio(2u128, 1u128); let token_2_exchange_rate = Decimal::from_ratio(3u128, 1u128); @@ -309,7 +322,7 @@ fn update_asset_collateral() { ScalingOperation::Truncate, ) .unwrap() - * market_1_initial.liquidation_threshold + * asset_params_1.liquidation_threshold * token_1_exchange_rate; let token_2_weighted_lt_in_base_asset = compute_underlying_amount( token_2_balance_scaled, @@ -317,15 +330,13 @@ fn update_asset_collateral() { ScalingOperation::Truncate, ) .unwrap() - * market_2_initial.liquidation_threshold + * asset_params_2.liquidation_threshold * token_2_exchange_rate; let weighted_liquidation_threshold_in_base_asset = token_1_weighted_lt_in_base_asset + token_2_weighted_lt_in_base_asset; - let max_debt_for_valid_hf = math::divide_uint128_by_decimal( - weighted_liquidation_threshold_in_base_asset, - token_3_exchange_rate, - ) - .unwrap(); + let max_debt_for_valid_hf = weighted_liquidation_threshold_in_base_asset + .checked_div_floor(token_3_exchange_rate) + .unwrap(); let token_3_debt_scaled = get_scaled_debt_amount( max_debt_for_valid_hf, &market_3_initial, @@ -344,7 +355,10 @@ fn update_asset_collateral() { &deps.as_ref(), &env, &user_addr, + "", &Addr::unchecked("oracle"), + &Addr::unchecked("params"), + false, ) .unwrap(); let health = health::compute_position_health(&positions).unwrap(); diff --git a/contracts/red-bank/tests/test_payment.rs b/contracts/red-bank/tests/tests/test_payment.rs similarity index 94% rename from contracts/red-bank/tests/test_payment.rs rename to contracts/red-bank/tests/tests/test_payment.rs index 588997c7a..551b6cfbb 100644 --- a/contracts/red-bank/tests/test_payment.rs +++ b/contracts/red-bank/tests/tests/test_payment.rs @@ -1,15 +1,14 @@ -mod helpers; - use cosmwasm_std::{ coins, testing::{mock_env, mock_info}, Uint128, }; use cw_utils::PaymentError; -use helpers::th_setup; use mars_red_bank::contract; use mars_red_bank_types::red_bank::ExecuteMsg; +use super::helpers::th_setup; + /// The Red Bank contract has 6 user-facing functions: deposit, withdraw, borrow, /// repay, liquidate, and update_asset_collateral_status; amount these, 3 do not /// expect the user to send any payment. This test verifies that they properly @@ -30,6 +29,8 @@ fn rejecting_unexpected_payments() { denom: "".into(), amount: None, recipient: None, + account_id: None, + liquidation_related: None, }, ) .unwrap_err(); diff --git a/contracts/red-bank/tests/test_query.rs b/contracts/red-bank/tests/tests/test_query.rs similarity index 69% rename from contracts/red-bank/tests/test_query.rs rename to contracts/red-bank/tests/tests/test_query.rs index 8539dccaf..92a213bd4 100644 --- a/contracts/red-bank/tests/test_query.rs +++ b/contracts/red-bank/tests/tests/test_query.rs @@ -1,13 +1,12 @@ use cosmwasm_std::{testing::mock_env, Addr, Decimal, Uint128}; -use helpers::{set_collateral, th_init_market, th_setup}; +use mars_interest_rate::{get_scaled_debt_amount, get_underlying_debt_amount, SCALING_FACTOR}; use mars_red_bank::{ - interest_rates::{get_scaled_debt_amount, get_underlying_debt_amount, SCALING_FACTOR}, - query::{query_user_collaterals, query_user_debt, query_user_debts}, + query::{query_user_collaterals, query_user_collaterals_v2, query_user_debt, query_user_debts}, state::DEBTS, }; use mars_red_bank_types::red_bank::{Debt, Market, UserCollateralResponse, UserDebtResponse}; -mod helpers; +use super::helpers::{set_collateral, th_init_market, th_setup}; #[test] fn query_collateral() { @@ -31,7 +30,8 @@ fn query_collateral() { // Assert markets correctly return collateral status let collaterals = - query_user_collaterals(deps.as_ref(), &env.block, user_addr.clone(), None, None).unwrap(); + query_user_collaterals(deps.as_ref(), &env.block, user_addr.clone(), None, None, None) + .unwrap(); assert_eq!( collaterals, vec![UserCollateralResponse { @@ -47,7 +47,7 @@ fn query_collateral() { // Assert markets correctly return collateral status let collaterals = - query_user_collaterals(deps.as_ref(), &env.block, user_addr, None, None).unwrap(); + query_user_collaterals(deps.as_ref(), &env.block, user_addr, None, None, None).unwrap(); assert_eq!( collaterals, vec![ @@ -67,6 +67,79 @@ fn query_collateral() { ); } +#[test] +fn paginate_user_collaterals_v2() { + let mut deps = th_setup(&[]); + let env = mock_env(); + + let user_addr = Addr::unchecked("user"); + + let market_1 = th_init_market(deps.as_mut(), "uosmo", &Default::default()); + let market_2 = th_init_market(deps.as_mut(), "uatom", &Default::default()); + let market_3 = th_init_market(deps.as_mut(), "untrn", &Default::default()); + let market_4 = th_init_market(deps.as_mut(), "ujuno", &Default::default()); + let market_5 = th_init_market(deps.as_mut(), "uusdc", &Default::default()); + let market_6 = th_init_market(deps.as_mut(), "ujake", &Default::default()); + + set_collateral(deps.as_mut(), &user_addr, &market_1.denom, Uint128::one(), true); + set_collateral(deps.as_mut(), &user_addr, &market_2.denom, Uint128::one(), true); + set_collateral(deps.as_mut(), &user_addr, &market_3.denom, Uint128::one(), true); + set_collateral(deps.as_mut(), &user_addr, &market_4.denom, Uint128::one(), true); + set_collateral(deps.as_mut(), &user_addr, &market_5.denom, Uint128::one(), true); + set_collateral(deps.as_mut(), &user_addr, &market_6.denom, Uint128::one(), false); + + // Check pagination with default params + let collaterals = + query_user_collaterals_v2(deps.as_ref(), &env.block, user_addr.clone(), None, None, None) + .unwrap(); + assert_eq!( + to_denoms(&collaterals.data), + vec!["uatom", "ujake", "ujuno", "untrn", "uosmo", "uusdc"] + ); + assert!(!collaterals.metadata.has_more); + + // Paginate all collaterals + let collaterals = query_user_collaterals_v2( + deps.as_ref(), + &env.block, + user_addr.clone(), + None, + Some("uatom".to_string()), + Some(2), + ) + .unwrap(); + assert_eq!(to_denoms(&collaterals.data), vec!["ujake", "ujuno"]); + assert!(collaterals.metadata.has_more); + + let collaterals = query_user_collaterals_v2( + deps.as_ref(), + &env.block, + user_addr.clone(), + None, + Some("ujuno".to_string()), + Some(2), + ) + .unwrap(); + assert_eq!(to_denoms(&collaterals.data), vec!["untrn", "uosmo"]); + assert!(collaterals.metadata.has_more); + + let collaterals = query_user_collaterals_v2( + deps.as_ref(), + &env.block, + user_addr, + None, + Some("uosmo".to_string()), + Some(2), + ) + .unwrap(); + assert_eq!(to_denoms(&collaterals.data), vec!["uusdc"]); + assert!(!collaterals.metadata.has_more); +} + +fn to_denoms(res: &[UserCollateralResponse]) -> Vec<&str> { + res.iter().map(|item| item.denom.as_str()).collect() +} + #[test] fn test_query_user_debt() { let mut deps = th_setup(&[]); diff --git a/contracts/red-bank/tests/test_update_owner.rs b/contracts/red-bank/tests/tests/test_update_owner.rs similarity index 97% rename from contracts/red-bank/tests/test_update_owner.rs rename to contracts/red-bank/tests/tests/test_update_owner.rs index 6960a59ba..8cd8975f5 100644 --- a/contracts/red-bank/tests/test_update_owner.rs +++ b/contracts/red-bank/tests/tests/test_update_owner.rs @@ -3,9 +3,7 @@ use mars_owner::{OwnerError::NotOwner, OwnerUpdate}; use mars_red_bank::{contract::execute, error::ContractError}; use mars_red_bank_types::red_bank::{ConfigResponse, ExecuteMsg, QueryMsg}; -use crate::helpers::{th_query, th_setup}; - -mod helpers; +use super::helpers::{th_query, th_setup}; #[test] fn initialized_state() { diff --git a/contracts/red-bank/tests/test_withdraw.rs b/contracts/red-bank/tests/tests/test_withdraw.rs similarity index 68% rename from contracts/red-bank/tests/test_withdraw.rs rename to contracts/red-bank/tests/tests/test_withdraw.rs index 03aa82c30..d5d7af200 100644 --- a/contracts/red-bank/tests/test_withdraw.rs +++ b/contracts/red-bank/tests/tests/test_withdraw.rs @@ -1,30 +1,35 @@ +use std::str::FromStr; + use cosmwasm_std::{ attr, coin, coins, testing::{mock_env, mock_info, MockApi, MockStorage}, to_binary, Addr, BankMsg, CosmosMsg, Decimal, OwnedDeps, SubMsg, Uint128, WasmMsg, }; -use helpers::{ - has_collateral_position, set_collateral, th_build_interests_updated_event, - th_get_expected_indices_and_rates, th_setup, TestUtilizationDeltaInfo, +use mars_interest_rate::{ + compute_scaled_amount, compute_underlying_amount, get_scaled_liquidity_amount, + get_updated_borrow_index, get_updated_liquidity_index, ScalingOperation, SCALING_FACTOR, }; +use mars_params::types::asset::AssetParams; use mars_red_bank::{ contract::execute, error::ContractError, - interest_rates::{ - compute_scaled_amount, compute_underlying_amount, get_scaled_liquidity_amount, - get_updated_borrow_index, get_updated_liquidity_index, ScalingOperation, SCALING_FACTOR, - }, state::{COLLATERALS, DEBTS, MARKETS}, }; use mars_red_bank_types::{ address_provider::MarsAddressType, + error::MarsError, incentives, red_bank::{Collateral, Debt, ExecuteMsg, Market}, }; -use mars_testing::{mock_env_at_block_time, MarsMockQuerier}; -use mars_utils::math; +use mars_testing::{ + integration::mock_env::MockEnvBuilder, mock_env_at_block_time, MarsMockQuerier, +}; -mod helpers; +use super::helpers::{ + has_collateral_position, set_collateral, th_build_interests_updated_event, + th_default_asset_params, th_get_expected_indices_and_rates, th_setup, TestUtilizationDeltaInfo, +}; +use crate::tests::helpers::{assert_err_with_str, osmo_asset_params, usdc_asset_params}; struct TestSuite { deps: OwnedDeps, @@ -82,6 +87,8 @@ fn withdrawing_more_than_balance() { denom: denom.to_string(), amount: Some(Uint128::from(2000u128)), recipient: None, + account_id: None, + liquidation_related: None, }, ) .unwrap_err(); @@ -125,6 +132,8 @@ fn withdrawing_partially() { denom: denom.to_string(), amount: Some(withdraw_amount), recipient: None, + account_id: None, + liquidation_related: None, }, ) .unwrap(); @@ -178,6 +187,7 @@ fn withdrawing_partially() { contract_addr: MarsAddressType::Incentives.to_string(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: Addr::unchecked(MarsAddressType::RewardsCollector.to_string()), + account_id: None, denom: denom.to_string(), user_amount_scaled_before: Uint128::zero(), total_amount_scaled_before: initial_market.collateral_total_scaled, @@ -189,6 +199,7 @@ fn withdrawing_partially() { contract_addr: MarsAddressType::Incentives.to_string(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: withdrawer_addr.clone(), + account_id: None, denom: denom.to_string(), user_amount_scaled_before: initial_deposit_amount_scaled, total_amount_scaled_before: initial_market.collateral_total_scaled @@ -226,12 +237,13 @@ fn withdrawing_partially() { assert_eq!(market.collateral_total_scaled, expected_total_collateral_amount_scaled); // the user's collateral scaled amount should have been decreased - let collateral = COLLATERALS.load(deps.as_ref().storage, (&withdrawer_addr, denom)).unwrap(); + let collateral = + COLLATERALS.load(deps.as_ref().storage, (&withdrawer_addr, "", denom)).unwrap(); assert_eq!(collateral.amount_scaled, expected_withdraw_amount_scaled_remaining); // the reward collector's collateral scaled amount should have been increased let rewards_addr = Addr::unchecked(MarsAddressType::RewardsCollector.to_string()); - let collateral = COLLATERALS.load(deps.as_ref().storage, (&rewards_addr, denom)).unwrap(); + let collateral = COLLATERALS.load(deps.as_ref().storage, (&rewards_addr, "", denom)).unwrap(); assert_eq!(collateral.amount_scaled, expected_rewards_amount_scaled); } @@ -258,6 +270,8 @@ fn withdrawing_completely() { denom: denom.to_string(), amount: None, recipient: None, + account_id: None, + liquidation_related: None, }, ) .unwrap(); @@ -294,6 +308,7 @@ fn withdrawing_completely() { contract_addr: MarsAddressType::Incentives.to_string(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: Addr::unchecked(MarsAddressType::RewardsCollector.to_string()), + account_id: None, denom: denom.to_string(), user_amount_scaled_before: Uint128::zero(), total_amount_scaled_before: initial_market.collateral_total_scaled, @@ -305,6 +320,7 @@ fn withdrawing_completely() { contract_addr: MarsAddressType::Incentives.to_string(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: withdrawer_addr.clone(), + account_id: None, denom: denom.to_string(), user_amount_scaled_before: withdrawer_balance_scaled, total_amount_scaled_before: initial_market.collateral_total_scaled @@ -365,6 +381,8 @@ fn withdrawing_to_another_user() { denom: denom.to_string(), amount: None, recipient: Some(recipient_addr.to_string()), + account_id: None, + liquidation_related: None, }, ) .unwrap(); @@ -402,6 +420,7 @@ fn withdrawing_to_another_user() { contract_addr: MarsAddressType::Incentives.to_string(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: Addr::unchecked(MarsAddressType::RewardsCollector.to_string()), + account_id: None, denom: denom.to_string(), user_amount_scaled_before: Uint128::zero(), total_amount_scaled_before: initial_market.collateral_total_scaled, @@ -413,6 +432,7 @@ fn withdrawing_to_another_user() { contract_addr: MarsAddressType::Incentives.to_string(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: withdrawer_addr.clone(), + account_id: None, denom: denom.to_string(), user_amount_scaled_before: withdrawer_balance_scaled, total_amount_scaled_before: initial_market.collateral_total_scaled @@ -447,6 +467,7 @@ struct HealthCheckTestSuite { deps: OwnedDeps, denoms: [&'static str; 3], markets: [Market; 3], + asset_params: [AssetParams; 3], prices: [Decimal; 3], collaterals: [Collateral; 3], debts: [Debt; 3], @@ -466,8 +487,6 @@ fn setup_health_check_test() -> HealthCheckTestSuite { denom: denoms[0].to_string(), liquidity_index: Decimal::one(), borrow_index: Decimal::one(), - max_loan_to_value: Decimal::from_ratio(40u128, 100u128), - liquidation_threshold: Decimal::from_ratio(60u128, 100u128), collateral_total_scaled: Uint128::new(100_000) * SCALING_FACTOR, ..Default::default() }, @@ -475,8 +494,6 @@ fn setup_health_check_test() -> HealthCheckTestSuite { denom: denoms[1].to_string(), liquidity_index: Decimal::one(), borrow_index: Decimal::one(), - max_loan_to_value: Decimal::from_ratio(50u128, 100u128), - liquidation_threshold: Decimal::from_ratio(80u128, 100u128), collateral_total_scaled: Uint128::new(100_000) * SCALING_FACTOR, ..Default::default() }, @@ -484,13 +501,29 @@ fn setup_health_check_test() -> HealthCheckTestSuite { denom: denoms[2].to_string(), liquidity_index: Decimal::one(), borrow_index: Decimal::one(), - max_loan_to_value: Decimal::from_ratio(20u128, 100u128), - liquidation_threshold: Decimal::from_ratio(40u128, 100u128), collateral_total_scaled: Uint128::new(100_000) * SCALING_FACTOR, ..Default::default() }, ]; + let asset_params = [ + AssetParams { + max_loan_to_value: Decimal::from_ratio(40u128, 100u128), + liquidation_threshold: Decimal::from_ratio(60u128, 100u128), + ..th_default_asset_params() + }, + AssetParams { + max_loan_to_value: Decimal::from_ratio(50u128, 100u128), + liquidation_threshold: Decimal::from_ratio(80u128, 100u128), + ..th_default_asset_params() + }, + AssetParams { + max_loan_to_value: Decimal::from_ratio(20u128, 100u128), + liquidation_threshold: Decimal::from_ratio(40u128, 100u128), + ..th_default_asset_params() + }, + ]; + let prices = [ Decimal::from_ratio(3u128, 1u128), Decimal::from_ratio(2u128, 1u128), @@ -533,6 +566,11 @@ fn setup_health_check_test() -> HealthCheckTestSuite { .try_for_each(|(denom, market)| MARKETS.save(deps.as_mut().storage, denom, market)) .unwrap(); + denoms + .iter() + .zip(asset_params.iter()) + .for_each(|(denom, ap)| deps.querier.set_redbank_params(denom, ap.clone())); + denoms .iter() .zip(prices.iter()) @@ -540,7 +578,9 @@ fn setup_health_check_test() -> HealthCheckTestSuite { denoms.iter().zip(collaterals.iter()).for_each(|(denom, collateral)| { if !collateral.amount_scaled.is_zero() { - COLLATERALS.save(deps.as_mut().storage, (&withdrawer_addr, denom), collateral).unwrap(); + COLLATERALS + .save(deps.as_mut().storage, (&withdrawer_addr, "", denom), collateral) + .unwrap(); } }); @@ -554,6 +594,7 @@ fn setup_health_check_test() -> HealthCheckTestSuite { deps, denoms, markets, + asset_params, prices, collaterals, debts, @@ -565,6 +606,7 @@ fn setup_health_check_test() -> HealthCheckTestSuite { fn how_much_to_withdraw(suite: &HealthCheckTestSuite, block_time: u64) -> Uint128 { let HealthCheckTestSuite { markets, + asset_params, prices, collaterals, debts, @@ -577,7 +619,7 @@ fn how_much_to_withdraw(suite: &HealthCheckTestSuite, block_time: u64) -> Uint12 ScalingOperation::Truncate, ) .unwrap() - * markets[0].liquidation_threshold + * asset_params[0].liquidation_threshold * prices[0]; let token_3_weighted_lt_in_base_asset = compute_underlying_amount( @@ -586,7 +628,7 @@ fn how_much_to_withdraw(suite: &HealthCheckTestSuite, block_time: u64) -> Uint12 ScalingOperation::Truncate, ) .unwrap() - * markets[2].liquidation_threshold + * asset_params[2].liquidation_threshold * prices[2]; let weighted_liquidation_threshold_in_base_asset = @@ -601,13 +643,12 @@ fn how_much_to_withdraw(suite: &HealthCheckTestSuite, block_time: u64) -> Uint12 * prices[1]; // How much to withdraw in base asset to have health factor equal to one - let how_much_to_withdraw_in_base_asset = math::divide_uint128_by_decimal( - weighted_liquidation_threshold_in_base_asset - total_collateralized_debt_in_base_asset, - markets[2].liquidation_threshold, - ) - .unwrap(); + let how_much_to_withdraw_in_base_asset = (weighted_liquidation_threshold_in_base_asset + - total_collateralized_debt_in_base_asset) + .checked_div_floor(asset_params[2].liquidation_threshold) + .unwrap(); - math::divide_uint128_by_decimal(how_much_to_withdraw_in_base_asset, prices[2]).unwrap() + how_much_to_withdraw_in_base_asset.checked_div_floor(prices[2]).unwrap() } #[test] @@ -638,6 +679,8 @@ fn withdrawing_if_health_factor_not_met() { denom: denoms[2].to_string(), amount: Some(withdraw_amount), recipient: None, + account_id: None, + liquidation_related: None, }, ) .unwrap_err(); @@ -674,6 +717,8 @@ fn withdrawing_if_health_factor_met() { denom: denoms[2].to_string(), amount: Some(withdraw_amount), recipient: None, + account_id: None, + liquidation_related: None, }, ) .unwrap(); @@ -688,6 +733,7 @@ fn withdrawing_if_health_factor_met() { contract_addr: MarsAddressType::Incentives.to_string(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: withdrawer_addr.clone(), + account_id: None, denom: denoms[2].to_string(), user_amount_scaled_before: collaterals[2].amount_scaled, // NOTE: Protocol rewards accrued is zero, so here it's initial total supply @@ -710,9 +756,229 @@ fn withdrawing_if_health_factor_met() { let expected_collateral_total_amount_scaled_after = markets[2].collateral_total_scaled - expected_withdraw_amount_scaled; - let col = COLLATERALS.load(deps.as_ref().storage, (&withdrawer_addr, denoms[2])).unwrap(); + let col = COLLATERALS.load(deps.as_ref().storage, (&withdrawer_addr, "", denoms[2])).unwrap(); assert_eq!(col.amount_scaled, expected_withdrawer_balance_after); let market = MARKETS.load(deps.as_ref().storage, denoms[2]).unwrap(); assert_eq!(market.collateral_total_scaled, expected_collateral_total_amount_scaled_after); } + +// Withdraw should be blocked if circuit breakers are activated in oracle contract except for the +// case where the withdrawer is the credit manager contract and the withdraw is for liquidation. +#[test] +fn withdraw_for_credit_manager_works_during_liquidation() { + let owner = Addr::unchecked("owner"); + let mut mock_env = MockEnvBuilder::new(None, owner.clone()).build(); + + let red_bank = mock_env.red_bank.clone(); + let params = mock_env.params.clone(); + let oracle = mock_env.oracle.clone(); + let pyth = mock_env.pyth.clone(); + let credit_manager = mock_env.credit_manager.clone(); + + let funded_amt = 1_000_000_000_000u128; + let provider = Addr::unchecked("provider"); // provides collateral to be borrowed by others + let account_id = "111".to_string(); + + // setup red-bank + let (market_params, asset_params) = osmo_asset_params(); + red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = usdc_asset_params(); + red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params); + params.init_params(&mut mock_env, asset_params); + + // setup oracle + oracle.set_price_source_fixed(&mut mock_env, "uosmo", Decimal::one()); + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(2u128, 1u128)); + + // fund accounts + mock_env.fund_accounts(&[&provider, &credit_manager], funded_amt, &["uosmo", "uusdc"]); + + // provider deposits collaterals + red_bank.deposit(&mut mock_env, &provider, coin(1000000000, "uusdc")).unwrap(); + + // credit manager deposits + let cm_osmo_deposit_amt = 100000000u128; + red_bank + .deposit_with_acc_id( + &mut mock_env, + &credit_manager, + coin(cm_osmo_deposit_amt, "uosmo"), + Some(account_id.clone()), + ) + .unwrap(); + + // update credit line for credit manager + red_bank + .update_uncollateralized_loan_limit( + &mut mock_env, + &owner, + &credit_manager, + "uusdc", + Uint128::MAX, + ) + .unwrap(); + + // credit manager should be able to borrow + let cm_usdc_borrow_amt = 100000000u128; + red_bank.borrow(&mut mock_env, &credit_manager, "uusdc", cm_usdc_borrow_amt).unwrap(); + + // check collaterals for credit manager account id before withdraw + let cm_collaterals = red_bank.query_user_collaterals_with_acc_id( + &mut mock_env, + &credit_manager, + Some(account_id.clone()), + ); + assert_eq!(cm_collaterals.len(), 1); + let cm_osmo_collateral = cm_collaterals.get("uosmo").unwrap(); + assert_eq!(cm_osmo_collateral.amount.u128(), cm_osmo_deposit_amt); + + // activate circuit breakers using pyth mocked invalid price + oracle.set_price_source_fixed(&mut mock_env, "usd", Decimal::from_str("1000000").unwrap()); + oracle.set_price_source_pyth( + &mut mock_env, + "uusdc", + pyth.to_string(), + Decimal::percent(10u64), + Decimal::percent(15u64), + ); + + // try to withdraw total collateral for account id, should fail because of circuit breakers + let res = red_bank.withdraw_with_acc_id( + &mut mock_env, + &credit_manager, + "uosmo", + None, + Some(account_id.clone()), + None, + ); + assert_err_with_str( + res, + "Invalid price: price confidence deviation 0.748898678414096916 exceeds max allowed 0.1", + ); + + // withdraw total collateral for account id during liquidation, should pass + red_bank + .withdraw_with_acc_id( + &mut mock_env, + &credit_manager, + "uosmo", + None, + Some(account_id.clone()), + Some(true), + ) + .unwrap(); + + // check collaterals for credit manager account id after withdraw + let cm_collaterals = red_bank.query_user_collaterals_with_acc_id( + &mut mock_env, + &credit_manager, + Some(account_id), + ); + assert!(cm_collaterals.is_empty()); +} + +// Withdraw for a red bank user (without account id) should be blocked if circuit breakers are activated in oracle contract. +#[test] +fn withdraw_if_oracle_circuit_breakers_activated() { + let owner = Addr::unchecked("owner"); + let mut mock_env = MockEnvBuilder::new(None, owner).build(); + + let red_bank = mock_env.red_bank.clone(); + let params = mock_env.params.clone(); + let oracle = mock_env.oracle.clone(); + let pyth = mock_env.pyth.clone(); + + let funded_amt = 1_000_000_000_000u128; + let provider = Addr::unchecked("provider"); // provides collateral to be borrowed by others + let user = Addr::unchecked("user"); + + // setup red-bank + let (market_params, asset_params) = osmo_asset_params(); + red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = usdc_asset_params(); + red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params); + params.init_params(&mut mock_env, asset_params); + + // setup oracle + oracle.set_price_source_fixed(&mut mock_env, "uosmo", Decimal::one()); + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(2u128, 1u128)); + + // fund accounts + mock_env.fund_accounts(&[&provider, &user], funded_amt, &["uosmo", "uusdc"]); + + // provider deposits collaterals + red_bank.deposit(&mut mock_env, &provider, coin(1000000000, "uusdc")).unwrap(); + + // user deposits + let cm_osmo_deposit_amt = 100000000u128; + red_bank.deposit(&mut mock_env, &user, coin(cm_osmo_deposit_amt, "uosmo")).unwrap(); + + // user borrows + let cm_usdc_borrow_amt = 100u128; + red_bank.borrow(&mut mock_env, &user, "uusdc", cm_usdc_borrow_amt).unwrap(); + + // activate circuit breakers using pyth mocked invalid price + oracle.set_price_source_fixed(&mut mock_env, "usd", Decimal::from_str("1000000").unwrap()); + oracle.set_price_source_pyth( + &mut mock_env, + "uusdc", + pyth.to_string(), + Decimal::percent(10u64), + Decimal::percent(15u64), + ); + + // try to withdraw with different `liquidation_related` value, should fail because of circuit breakers + let expected_msg = + "Invalid price: price confidence deviation 0.748898678414096916 exceeds max allowed 0.1"; + let res = red_bank.withdraw_with_acc_id(&mut mock_env, &user, "uosmo", None, None, None); + assert_err_with_str(res, expected_msg); + let res = red_bank.withdraw_with_acc_id(&mut mock_env, &user, "uosmo", None, None, Some(false)); + assert_err_with_str(res, expected_msg); + let res = red_bank.withdraw_with_acc_id(&mut mock_env, &user, "uosmo", None, None, Some(true)); + assert_err_with_str(res, expected_msg); +} + +#[test] +fn withdrawing_with_account_id_by_non_credit_manager_user() { + let TestSuite { + mut deps, + denom, + withdrawer_addr, + .. + } = setup_test(); + + // non-credit-manager user cannot withdraw with account_id (even with empty string) + let err = execute( + deps.as_mut(), + mock_env(), + mock_info(withdrawer_addr.as_str(), &[]), + ExecuteMsg::Withdraw { + denom: denom.to_string(), + amount: Some(Uint128::from(2000u128)), + recipient: None, + account_id: Some("".to_string()), + liquidation_related: None, + }, + ) + .unwrap_err(); + assert_eq!(err, ContractError::Mars(MarsError::Unauthorized {})); + + // non-credit-manager user cannot withdraw with account_id + let err = execute( + deps.as_mut(), + mock_env(), + mock_info(withdrawer_addr.as_str(), &[]), + ExecuteMsg::Withdraw { + denom: denom.to_string(), + amount: Some(Uint128::from(2000u128)), + recipient: None, + account_id: Some("1234".to_string()), + liquidation_related: None, + }, + ) + .unwrap_err(); + assert_eq!(err, ContractError::Mars(MarsError::Unauthorized {})); +} diff --git a/contracts/rewards-collector/base/Cargo.toml b/contracts/rewards-collector/base/Cargo.toml index 7054631c7..81a0ca230 100644 --- a/contracts/rewards-collector/base/Cargo.toml +++ b/contracts/rewards-collector/base/Cargo.toml @@ -10,20 +10,12 @@ documentation = { workspace = true } keywords = { workspace = true } [lib] -crate-type = [ - "cdylib", - "rlib", -] -doctest = false - -[profile.release] -overflow-checks = true +crate-type = ["cdylib", "rlib"] +doctest = false [features] # for more explicit tests, cargo test --features=backtraces -backtraces = [ - "cosmwasm-std/backtraces", -] +backtraces = ["cosmwasm-std/backtraces"] [dependencies] cosmwasm-schema = { workspace = true } @@ -35,8 +27,3 @@ mars-utils = { workspace = true } schemars = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } - -[dev-dependencies] -mars-osmosis = { workspace = true } -mars-testing = { workspace = true } -osmosis-std = { workspace = true } diff --git a/contracts/rewards-collector/base/src/contract.rs b/contracts/rewards-collector/base/src/contract.rs index 17a39f599..40c2d26ec 100644 --- a/contracts/rewards-collector/base/src/contract.rs +++ b/contracts/rewards-collector/base/src/contract.rs @@ -8,6 +8,7 @@ use mars_red_bank_types::{ address_provider::{self, AddressResponseItem, MarsAddressType}, incentives, red_bank, rewards_collector::{ + credit_manager::{self, Action}, Config, ConfigResponse, ExecuteMsg, InstantiateMsg, QueryMsg, UpdateConfig, }, }; @@ -85,6 +86,10 @@ where denom, amount, } => self.withdraw_from_red_bank(deps, denom, amount), + ExecuteMsg::WithdrawFromCreditManager { + account_id, + actions, + } => self.withdraw_from_credit_manager(deps, account_id, actions), ExecuteMsg::DistributeRewards { denom, amount, @@ -182,6 +187,8 @@ where denom: denom.clone(), amount, recipient: None, + account_id: None, + liquidation_related: None, })?, funds: vec![], }); @@ -193,6 +200,42 @@ where .add_attribute("amount", stringify_option_amount(amount))) } + pub fn withdraw_from_credit_manager( + &self, + deps: DepsMut, + account_id: String, + actions: Vec, + ) -> ContractResult> { + let cfg = self.config.load(deps.storage)?; + + let valid_actions = actions.iter().all(|action| { + matches!(action, Action::Withdraw(..) | Action::WithdrawLiquidity { .. }) + }); + if !valid_actions { + return Err(ContractError::InvalidActionsForCreditManager {}); + } + + let cm_addr = address_provider::helpers::query_contract_addr( + deps.as_ref(), + &cfg.address_provider, + MarsAddressType::CreditManager, + )?; + + let withdraw_from_cm_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cm_addr.to_string(), + msg: to_binary(&credit_manager::ExecuteMsg::UpdateCreditAccount { + account_id: account_id.clone(), + actions, + })?, + funds: vec![], + }); + + Ok(Response::new() + .add_message(withdraw_from_cm_msg) + .add_attribute("action", "withdraw_from_credit_manager") + .add_attribute("account_id", account_id)) + } + pub fn claim_incentive_rewards( &self, deps: DepsMut, @@ -211,6 +254,7 @@ where let claim_msg = CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: incentives_addr.to_string(), msg: to_binary(&incentives::ExecuteMsg::ClaimRewards { + account_id: None, start_after_collateral_denom, start_after_incentive_denom, limit, diff --git a/contracts/rewards-collector/base/src/error.rs b/contracts/rewards-collector/base/src/error.rs index 83ab8d732..6744b846a 100644 --- a/contracts/rewards-collector/base/src/error.rs +++ b/contracts/rewards-collector/base/src/error.rs @@ -39,6 +39,9 @@ pub enum ContractError { InvalidRoute { reason: String, }, + + #[error("Invalid actions. Only Withdraw and WithdrawLiquidity is possible to pass for CreditManager")] + InvalidActionsForCreditManager {}, } pub type ContractResult = Result; diff --git a/contracts/rewards-collector/neutron/Cargo.toml b/contracts/rewards-collector/neutron/Cargo.toml index b3165b981..c567a31f2 100644 --- a/contracts/rewards-collector/neutron/Cargo.toml +++ b/contracts/rewards-collector/neutron/Cargo.toml @@ -10,36 +10,16 @@ documentation = { workspace = true } keywords = { workspace = true } [lib] -crate-type = [ - "cdylib", - "rlib", -] -doctest = false - -[profile.release] -overflow-checks = true +crate-type = ["cdylib", "rlib"] +doctest = false [features] # for more explicit tests, cargo test --features=backtraces -backtraces = [ - "cosmwasm-std/backtraces", -] +backtraces = ["cosmwasm-std/backtraces"] [dependencies] -cosmwasm-schema = { workspace = true } -cosmwasm-std = { workspace = true, features = ["stargate"] } -cw2 = { workspace = true } -cw-storage-plus = { workspace = true } -mars-owner = { workspace = true } -mars-red-bank-types = { workspace = true } -mars-rewards-collector-base = { workspace = true } -mars-utils = { workspace = true } -schemars = { workspace = true } -serde = { workspace = true } -thiserror = { workspace = true } -neutron-sdk = { workspace = true } - -[dev-dependencies] -mars-osmosis = { workspace = true } -mars-testing = { workspace = true } -osmosis-std = { workspace = true } +cosmwasm-std = { workspace = true, features = ["stargate"] } +cw2 = { workspace = true } +mars-red-bank-types = { workspace = true } +mars-rewards-collector-base = { workspace = true } +neutron-sdk = { workspace = true } diff --git a/contracts/rewards-collector/osmosis/Cargo.toml b/contracts/rewards-collector/osmosis/Cargo.toml index 2b4a2acb7..daabdbdd3 100644 --- a/contracts/rewards-collector/osmosis/Cargo.toml +++ b/contracts/rewards-collector/osmosis/Cargo.toml @@ -10,35 +10,23 @@ documentation = { workspace = true } keywords = { workspace = true } [lib] -crate-type = [ - "cdylib", - "rlib", -] -doctest = false - -[profile.release] -overflow-checks = true +crate-type = ["cdylib", "rlib"] +doctest = false [features] # for more explicit tests, cargo test --features=backtraces -backtraces = [ - "cosmwasm-std/backtraces", -] +backtraces = ["cosmwasm-std/backtraces"] [dependencies] -cosmwasm-schema = { workspace = true } -cosmwasm-std = { workspace = true, features = ["stargate"] } -cw2 = { workspace = true } -cw-storage-plus = { workspace = true } -mars-owner = { workspace = true } -mars-red-bank-types = { workspace = true } -mars-rewards-collector-base = { workspace = true } -mars-utils = { workspace = true } -schemars = { workspace = true } -serde = { workspace = true } -thiserror = { workspace = true } +cosmwasm-std = { workspace = true, features = ["stargate"] } +cw2 = { workspace = true } +mars-red-bank-types = { workspace = true } +mars-rewards-collector-base = { workspace = true } [dev-dependencies] mars-osmosis = { workspace = true } +mars-owner = { workspace = true } mars-testing = { workspace = true } +mars-utils = { workspace = true } osmosis-std = { workspace = true } +serde = { workspace = true } diff --git a/contracts/rewards-collector/osmosis/tests/all_tests.rs b/contracts/rewards-collector/osmosis/tests/all_tests.rs new file mode 100644 index 000000000..14f00389d --- /dev/null +++ b/contracts/rewards-collector/osmosis/tests/all_tests.rs @@ -0,0 +1 @@ +mod tests; diff --git a/contracts/rewards-collector/osmosis/tests/test_withdraw.rs b/contracts/rewards-collector/osmosis/tests/test_withdraw.rs deleted file mode 100644 index 788c71444..000000000 --- a/contracts/rewards-collector/osmosis/tests/test_withdraw.rs +++ /dev/null @@ -1,38 +0,0 @@ -use cosmwasm_std::{testing::mock_env, to_binary, CosmosMsg, SubMsg, Uint128, WasmMsg}; -use mars_red_bank_types::rewards_collector::ExecuteMsg; -use mars_rewards_collector_osmosis::entry::execute; -use mars_testing::mock_info; - -mod helpers; - -#[test] -fn withdrawing_from_red_bank() { - let mut deps = helpers::setup_test(); - - // anyone can execute a withdrawal - let res = execute( - deps.as_mut(), - mock_env(), - mock_info("jake"), - ExecuteMsg::WithdrawFromRedBank { - denom: "uatom".to_string(), - amount: Some(Uint128::new(42069)), - }, - ) - .unwrap(); - - assert_eq!(res.messages.len(), 1); - assert_eq!( - res.messages[0], - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: "red_bank".to_string(), - msg: to_binary(&mars_red_bank_types::red_bank::ExecuteMsg::Withdraw { - denom: "uatom".to_string(), - amount: Some(Uint128::new(42069)), - recipient: None - }) - .unwrap(), - funds: vec![] - })) - ) -} diff --git a/contracts/rewards-collector/osmosis/tests/helpers.rs b/contracts/rewards-collector/osmosis/tests/tests/helpers/mod.rs similarity index 93% rename from contracts/rewards-collector/osmosis/tests/helpers.rs rename to contracts/rewards-collector/osmosis/tests/tests/helpers/mod.rs index a7872d802..6760a31b3 100644 --- a/contracts/rewards-collector/osmosis/tests/helpers.rs +++ b/contracts/rewards-collector/osmosis/tests/tests/helpers/mod.rs @@ -5,11 +5,11 @@ use cosmwasm_std::{ testing::{mock_env, MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR}, Coin, Decimal, Deps, OwnedDeps, }; -use mars_osmosis::helpers::{Pool, QueryPoolResponse}; +use mars_osmosis::BalancerPool; use mars_red_bank_types::rewards_collector::{Config, InstantiateMsg, QueryMsg}; use mars_rewards_collector_osmosis::entry; use mars_testing::{mock_info, MarsMockQuerier}; -use osmosis_std::types::osmosis::gamm::v1beta1::PoolAsset; +use osmosis_std::types::osmosis::{gamm::v1beta1::PoolAsset, poolmanager::v1beta1::PoolResponse}; pub fn mock_instantiate_msg() -> InstantiateMsg { InstantiateMsg { @@ -93,10 +93,10 @@ fn prepare_query_pool_response( assets: &[Coin], weights: &[u64], shares: &Coin, -) -> QueryPoolResponse { - let pool = Pool { +) -> PoolResponse { + let pool = BalancerPool { address: "address".to_string(), - id: pool_id.to_string(), + id: pool_id, pool_params: None, future_pool_governor: "future_pool_governor".to_string(), total_shares: Some(osmosis_std::types::cosmos::base::v1beta1::Coin { @@ -106,8 +106,8 @@ fn prepare_query_pool_response( pool_assets: prepare_pool_assets(assets, weights), total_weight: "".to_string(), }; - QueryPoolResponse { - pool, + PoolResponse { + pool: Some(pool.to_any()), } } diff --git a/contracts/rewards-collector/osmosis/tests/tests/mod.rs b/contracts/rewards-collector/osmosis/tests/tests/mod.rs new file mode 100644 index 000000000..9bf248c6c --- /dev/null +++ b/contracts/rewards-collector/osmosis/tests/tests/mod.rs @@ -0,0 +1,7 @@ +mod helpers; + +mod test_admin; +mod test_distribute_rewards; +mod test_swap; +mod test_update_owner; +mod test_withdraw; diff --git a/contracts/rewards-collector/osmosis/tests/test_admin.rs b/contracts/rewards-collector/osmosis/tests/tests/test_admin.rs similarity index 98% rename from contracts/rewards-collector/osmosis/tests/test_admin.rs rename to contracts/rewards-collector/osmosis/tests/tests/test_admin.rs index 9fff2f58e..8cbe282b0 100644 --- a/contracts/rewards-collector/osmosis/tests/test_admin.rs +++ b/contracts/rewards-collector/osmosis/tests/tests/test_admin.rs @@ -6,9 +6,10 @@ use mars_rewards_collector_osmosis::entry::{execute, instantiate}; use mars_testing::mock_info; use mars_utils::error::ValidationError; -use crate::helpers::{mock_config, mock_instantiate_msg}; - -mod helpers; +use super::{ + helpers, + helpers::{mock_config, mock_instantiate_msg}, +}; #[test] fn instantiating() { diff --git a/contracts/rewards-collector/osmosis/tests/test_distribute_rewards.rs b/contracts/rewards-collector/osmosis/tests/tests/test_distribute_rewards.rs similarity index 99% rename from contracts/rewards-collector/osmosis/tests/test_distribute_rewards.rs rename to contracts/rewards-collector/osmosis/tests/tests/test_distribute_rewards.rs index 0334c8ea3..fd7373b68 100644 --- a/contracts/rewards-collector/osmosis/tests/test_distribute_rewards.rs +++ b/contracts/rewards-collector/osmosis/tests/tests/test_distribute_rewards.rs @@ -6,7 +6,7 @@ use mars_rewards_collector_base::ContractError; use mars_rewards_collector_osmosis::entry::execute; use mars_testing::{mock_env as mock_env_at_height_and_time, mock_info, MockEnvParams}; -mod helpers; +use super::helpers; #[test] fn distributing_rewards() { diff --git a/contracts/rewards-collector/osmosis/tests/test_swap.rs b/contracts/rewards-collector/osmosis/tests/tests/test_swap.rs similarity index 98% rename from contracts/rewards-collector/osmosis/tests/test_swap.rs rename to contracts/rewards-collector/osmosis/tests/tests/test_swap.rs index 331d67ab8..3bbaa7e38 100644 --- a/contracts/rewards-collector/osmosis/tests/test_swap.rs +++ b/contracts/rewards-collector/osmosis/tests/tests/test_swap.rs @@ -9,9 +9,7 @@ use mars_rewards_collector_osmosis::entry::execute; use mars_testing::mock_info; use osmosis_std::types::osmosis::twap::v1beta1::ArithmeticTwapToNowResponse; -use crate::helpers::mock_instantiate_msg; - -mod helpers; +use super::{helpers, helpers::mock_instantiate_msg}; #[test] fn swapping_asset() { diff --git a/contracts/rewards-collector/osmosis/tests/test_update_owner.rs b/contracts/rewards-collector/osmosis/tests/tests/test_update_owner.rs similarity index 97% rename from contracts/rewards-collector/osmosis/tests/test_update_owner.rs rename to contracts/rewards-collector/osmosis/tests/tests/test_update_owner.rs index 7b41394c5..102b46a91 100644 --- a/contracts/rewards-collector/osmosis/tests/test_update_owner.rs +++ b/contracts/rewards-collector/osmosis/tests/tests/test_update_owner.rs @@ -4,9 +4,7 @@ use mars_red_bank_types::rewards_collector::{ConfigResponse, ExecuteMsg, QueryMs use mars_rewards_collector_base::ContractError; use mars_rewards_collector_osmosis::entry::execute; -use crate::helpers::{query, setup_test}; - -mod helpers; +use super::helpers::{query, setup_test}; #[test] fn initialized_state() { diff --git a/contracts/rewards-collector/osmosis/tests/tests/test_withdraw.rs b/contracts/rewards-collector/osmosis/tests/tests/test_withdraw.rs new file mode 100644 index 000000000..9ce1f6616 --- /dev/null +++ b/contracts/rewards-collector/osmosis/tests/tests/test_withdraw.rs @@ -0,0 +1,129 @@ +use cosmwasm_std::{testing::mock_env, to_binary, CosmosMsg, Decimal, SubMsg, Uint128, WasmMsg}; +use mars_red_bank_types::rewards_collector::{ + credit_manager::{self, Action, ActionAmount, ActionCoin}, + ExecuteMsg, +}; +use mars_rewards_collector_base::ContractError; +use mars_rewards_collector_osmosis::entry::execute; +use mars_testing::mock_info; + +use super::helpers; + +#[test] +fn withdrawing_from_red_bank() { + let mut deps = helpers::setup_test(); + + // anyone can execute a withdrawal + let res = execute( + deps.as_mut(), + mock_env(), + mock_info("jake"), + ExecuteMsg::WithdrawFromRedBank { + denom: "uatom".to_string(), + amount: Some(Uint128::new(42069)), + }, + ) + .unwrap(); + + assert_eq!(res.messages.len(), 1); + assert_eq!( + res.messages[0], + SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "red_bank".to_string(), + msg: to_binary(&mars_red_bank_types::red_bank::ExecuteMsg::Withdraw { + denom: "uatom".to_string(), + amount: Some(Uint128::new(42069)), + recipient: None, + account_id: None, + liquidation_related: None + }) + .unwrap(), + funds: vec![] + })) + ) +} + +#[test] +fn withdrawing_from_cm_if_action_not_allowed() { + let mut deps = helpers::setup_test(); + + // anyone can execute a withdrawal + let error_res = execute( + deps.as_mut(), + mock_env(), + mock_info("jake"), + ExecuteMsg::WithdrawFromCreditManager { + account_id: "random_id".to_string(), + actions: vec![ + Action::Withdraw(ActionCoin { + denom: "uatom".to_string(), + amount: ActionAmount::Exact(Uint128::new(100)), + }), + Action::Unknown {}, + Action::WithdrawLiquidity { + lp_token: ActionCoin { + denom: "gamm/pool/1".to_string(), + amount: ActionAmount::AccountBalance, + }, + slippage: Decimal::percent(5), + }, + ], + }, + ) + .unwrap_err(); + assert_eq!(error_res, ContractError::InvalidActionsForCreditManager {}); +} + +#[test] +fn withdrawing_from_cm_successfully() { + let mut deps = helpers::setup_test(); + + let account_id = "random_id".to_string(); + let actions = vec![ + Action::Withdraw(ActionCoin { + denom: "uusdc".to_string(), + amount: ActionAmount::Exact(Uint128::new(100)), + }), + Action::WithdrawLiquidity { + lp_token: ActionCoin { + denom: "gamm/pool/1".to_string(), + amount: ActionAmount::AccountBalance, + }, + slippage: Decimal::percent(5), + }, + Action::Withdraw(ActionCoin { + denom: "uatom".to_string(), + amount: ActionAmount::Exact(Uint128::new(120)), + }), + Action::Withdraw(ActionCoin { + denom: "uosmo".to_string(), + amount: ActionAmount::Exact(Uint128::new(140)), + }), + ]; + + // anyone can execute a withdrawal + let res = execute( + deps.as_mut(), + mock_env(), + mock_info("jake"), + ExecuteMsg::WithdrawFromCreditManager { + account_id: account_id.clone(), + actions: actions.clone(), + }, + ) + .unwrap(); + + assert_eq!(res.messages.len(), 1); + assert_eq!( + res.messages[0], + SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "credit_manager".to_string(), + msg: to_binary(&credit_manager::ExecuteMsg::UpdateCreditAccount { + account_id, + actions + }) + .unwrap(), + funds: vec![] + })) + ) +} diff --git a/contracts/swapper/README.md b/contracts/swapper/README.md new file mode 100644 index 000000000..674764ff9 --- /dev/null +++ b/contracts/swapper/README.md @@ -0,0 +1,9 @@ +# Mars Swapper + +`mars-swapper-base` contains chain-agnostic logics for the swapper contract. Each chain specific implementation should implement its own route struct that should implement the `mars_swapper_base::Route` trait and then use the `mars_swapper_base::SwapBase` to implement the entry point functions. See `./osmosis/src/contract.rs` for an example. Each chain specific swapper will thus implement the same API. + +The swapper contracts should NEVER hold any funds and any funds sent to the contract except as part of executing the `SwapExactIn` message can be stolen by an attacker. See [Oak Audit 2023-08-01](https://github.com/oak-security/audit-reports/blob/master/Mars/2023-08-01%20Audit%20Report%20-%20Mars%20Red%20Bank%20Updates%20v1.0.pdf) issue 14. + +## Mars Swapper Mock + +Mock swapper contains a mock swapper contract to be used for testing purposes only. It only implements `ExecuteMsg::SwapExactIn` and `QueryMsg::EstimateExactInSwap`. When calling `ExecuteMsg::SwapExactIn` `denom_out` must be `uosmo` and the resulting amount will always be `1337uosmo`. The contract MUST be prefunded with this amount. diff --git a/contracts/swapper/astroport/Cargo.toml b/contracts/swapper/astroport/Cargo.toml index a285073d8..00f6aeb37 100644 --- a/contracts/swapper/astroport/Cargo.toml +++ b/contracts/swapper/astroport/Cargo.toml @@ -11,29 +11,27 @@ keywords = { workspace = true } [lib] crate-type = ["cdylib", "rlib"] - -[profile.release] -overflow-checks = true +doctest = false [features] -default = [] # for quicker tests, cargo test --lib # for more explicit tests, cargo test --features=backtraces -backtraces = ["cosmwasm-std/backtraces"] -library = [] +backtraces = ["cosmwasm-std/backtraces"] +default = [] +library = [] osmosis-test-tube = ["cw-it/osmosis-test-tube", "mars-testing/osmosis-test-tube"] [dependencies] +astroport = { workspace = true } cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw2 = { workspace = true } -mars-swapper-base = { workspace = true } -mars-oracle-wasm = { workspace = true } mars-red-bank-types = { workspace = true } -astroport = { workspace = true } +mars-swapper-base = { workspace = true } [dev-dependencies] anyhow = { workspace = true } -mars-testing = { workspace = true, features = ["astroport"] } cw-it = { workspace = true, features = ["astroport", "astroport-multi-test"] } -test-case = "3.0.0" +mars-oracle-wasm = { workspace = true } +mars-testing = { workspace = true, features = ["astroport"] } +test-case = { workspace = true } diff --git a/contracts/swapper/astroport/src/route.rs b/contracts/swapper/astroport/src/route.rs index 996667fbf..96cebe3a6 100644 --- a/contracts/swapper/astroport/src/route.rs +++ b/contracts/swapper/astroport/src/route.rs @@ -56,6 +56,7 @@ impl AstroportRoute { contract_addr: self.oracle.clone(), msg: to_binary(&mars_red_bank_types::oracle::QueryMsg::Price { denom: denom.to_string(), + kind: None, })?, })) .map(|res| res.price) diff --git a/contracts/swapper/astroport/tests/all_tests.rs b/contracts/swapper/astroport/tests/all_tests.rs new file mode 100644 index 000000000..14f00389d --- /dev/null +++ b/contracts/swapper/astroport/tests/all_tests.rs @@ -0,0 +1 @@ +mod tests; diff --git a/contracts/swapper/astroport/tests/tests/mod.rs b/contracts/swapper/astroport/tests/tests/mod.rs new file mode 100644 index 000000000..1d69d56e7 --- /dev/null +++ b/contracts/swapper/astroport/tests/tests/mod.rs @@ -0,0 +1,4 @@ +mod test_queries; +mod test_routes; +mod test_swap; +mod test_transfer_result; diff --git a/contracts/swapper/astroport/tests/test_queries.rs b/contracts/swapper/astroport/tests/tests/test_queries.rs similarity index 100% rename from contracts/swapper/astroport/tests/test_queries.rs rename to contracts/swapper/astroport/tests/tests/test_queries.rs diff --git a/contracts/swapper/astroport/tests/test_routes.rs b/contracts/swapper/astroport/tests/tests/test_routes.rs similarity index 100% rename from contracts/swapper/astroport/tests/test_routes.rs rename to contracts/swapper/astroport/tests/tests/test_routes.rs diff --git a/contracts/swapper/astroport/tests/test_swap.rs b/contracts/swapper/astroport/tests/tests/test_swap.rs similarity index 92% rename from contracts/swapper/astroport/tests/test_swap.rs rename to contracts/swapper/astroport/tests/tests/test_swap.rs index 002f25c2e..b2ab8870b 100644 --- a/contracts/swapper/astroport/tests/test_swap.rs +++ b/contracts/swapper/astroport/tests/tests/test_swap.rs @@ -54,9 +54,11 @@ const DEFAULT_LIQ: [u128; 2] = [10000000000000000u128, 10000000000000000u128]; #[test_case(PoolType::Stable { amp: 10u64 }, "uatom", &DEFAULT_LIQ, &[6,6], Decimal::percent(5), true => panics ; "stable swap no route")] #[test_case(PoolType::Xyk {}, "uatom", &DEFAULT_LIQ, &[10,6], Decimal::percent(1), false; "xyk 10:6 decimals, even pool")] #[test_case(PoolType::Xyk {}, "uatom", &DEFAULT_LIQ, &[6,18], Decimal::percent(1), false; "xyk 6:18 decimals, even pool")] -#[test_case(PoolType::Stable { amp: 10u64 }, "uatom", &[100000000000,10000000000000], &[6,8], Decimal::percent(50), false; "stable 6:8 decimals, even adjusted pool")] -#[test_case(PoolType::Stable { amp: 10u64 }, "uatom", &[1000000000000,100000000000], &[7,6], Decimal::percent(50), false; "stable 8:6 decimals, even adjusted pool")] +#[test_case(PoolType::Stable { amp: 10u64 }, "uatom", &[100000000000,10000000000000], &[6,8], Decimal::percent(10), false; "stable 6:8 decimals, even adjusted pool")] +#[test_case(PoolType::Stable { amp: 10u64 }, "uatom", &[1000000000000,100000000000], &[7,6], Decimal::percent(10), false; "stable 8:6 decimals, even adjusted pool")] #[test_case(PoolType::Stable { amp: 10u64 }, "uatom", &[100000000000,100000000000000000000000], &[6,18], Decimal::percent(5), false; "stable 6:18 decimals, even adjusted pool")] +#[test_case(PoolType::Xyk {}, "uatom", &DEFAULT_LIQ, &[6,6], Decimal::percent(11), false => panics ; "xyk max slippage exceeded")] +#[test_case(PoolType::Stable { amp: 10u64 }, "uatom", &DEFAULT_LIQ, &[6,6], Decimal::percent(11), false => panics ; "stable max slippage exceeded")] fn swap( pool_type: PoolType, denom_out: &str, diff --git a/contracts/swapper/astroport/tests/test_transfer_result.rs b/contracts/swapper/astroport/tests/tests/test_transfer_result.rs similarity index 100% rename from contracts/swapper/astroport/tests/test_transfer_result.rs rename to contracts/swapper/astroport/tests/tests/test_transfer_result.rs diff --git a/contracts/swapper/base/Cargo.toml b/contracts/swapper/base/Cargo.toml index 42c5d3221..2eedb45fc 100644 --- a/contracts/swapper/base/Cargo.toml +++ b/contracts/swapper/base/Cargo.toml @@ -10,10 +10,7 @@ documentation = { workspace = true } keywords = { workspace = true } [lib] -crate-type = ["cdylib", "rlib"] - -[profile.release] -overflow-checks = true +doctest = false [features] # for quicker tests, cargo test --lib diff --git a/contracts/swapper/base/src/contract.rs b/contracts/swapper/base/src/contract.rs index 21408efc6..f597aa0e6 100644 --- a/contracts/swapper/base/src/contract.rs +++ b/contracts/swapper/base/src/contract.rs @@ -14,6 +14,9 @@ use mars_red_bank_types::swapper::{ use crate::{ContractError, ContractResult, Route}; +// Max allowed slippage percentage for swap +const MAX_SLIPPAGE_PERCENTAGE: u64 = 10; + pub struct SwapBase<'a, Q, M, R> where Q: CustomQuery, @@ -161,6 +164,14 @@ where denom_out: String, slippage: Decimal, ) -> ContractResult> { + let max_slippage = Decimal::percent(MAX_SLIPPAGE_PERCENTAGE); + if slippage > max_slippage { + return Err(ContractError::MaxSlippageExceeded { + max_slippage, + slippage, + }); + } + let swap_msg = self .routes .load(deps.storage, (coin_in.denom.clone(), denom_out.clone())) diff --git a/contracts/swapper/base/src/error.rs b/contracts/swapper/base/src/error.rs index f1e8e59ba..57047c5b4 100644 --- a/contracts/swapper/base/src/error.rs +++ b/contracts/swapper/base/src/error.rs @@ -1,5 +1,5 @@ use cosmwasm_std::{ - CheckedFromRatioError, CheckedMultiplyFractionError, CheckedMultiplyRatioError, + CheckedFromRatioError, CheckedMultiplyFractionError, CheckedMultiplyRatioError, Decimal, DecimalRangeExceeded, OverflowError, StdError, }; use mars_owner::OwnerError; @@ -50,6 +50,12 @@ pub enum ContractError { from: String, to: String, }, + + #[error("Max slippage of {max_slippage} exceeded. Slippage is {slippage}")] + MaxSlippageExceeded { + max_slippage: Decimal, + slippage: Decimal, + }, } pub type ContractResult = Result; diff --git a/contracts/swapper/mock/Cargo.toml b/contracts/swapper/mock/Cargo.toml index 924918749..97f755bf3 100644 --- a/contracts/swapper/mock/Cargo.toml +++ b/contracts/swapper/mock/Cargo.toml @@ -11,6 +11,7 @@ keywords = { workspace = true } [lib] crate-type = ["cdylib", "rlib"] +doctest = false [features] # for quicker tests, cargo test --lib @@ -21,7 +22,3 @@ library = [] [dependencies] cosmwasm-std = { workspace = true } mars-red-bank-types = { workspace = true } - -[dev-dependencies] -anyhow = { workspace = true } -cw-multi-test = { workspace = true } diff --git a/contracts/swapper/osmosis/Cargo.toml b/contracts/swapper/osmosis/Cargo.toml index 7bdb7ebc7..ce5a9c817 100644 --- a/contracts/swapper/osmosis/Cargo.toml +++ b/contracts/swapper/osmosis/Cargo.toml @@ -11,9 +11,7 @@ keywords = { workspace = true } [lib] crate-type = ["cdylib", "rlib"] - -[profile.release] -overflow-checks = true +doctest = false [features] # for quicker tests, cargo test --lib @@ -32,5 +30,5 @@ osmosis-std = { workspace = true } mars-red-bank-types = { workspace = true } [dev-dependencies] -anyhow = { workspace = true } -cw-it = { workspace = true, features = ["osmosis-test-tube"] } +anyhow = { workspace = true } +cw-it = { workspace = true, features = ["osmosis-test-tube"] } diff --git a/contracts/swapper/osmosis/src/route.rs b/contracts/swapper/osmosis/src/route.rs index 81486db4a..85a12f706 100644 --- a/contracts/swapper/osmosis/src/route.rs +++ b/contracts/swapper/osmosis/src/route.rs @@ -2,11 +2,11 @@ use std::fmt; use cosmwasm_schema::cw_serde; use cosmwasm_std::{BlockInfo, CosmosMsg, Decimal, Empty, Env, Fraction, QuerierWrapper, Uint128}; -use mars_osmosis::helpers::{has_denom, query_arithmetic_twap_price, query_pool}; +use mars_osmosis::helpers::{query_arithmetic_twap_price, query_pool, CommonPoolData}; use mars_red_bank_types::swapper::EstimateExactInSwapResponse; use mars_swapper_base::{ContractError, ContractResult, Route}; use osmosis_std::types::osmosis::gamm::v1beta1::MsgSwapExactAmountIn; -pub use osmosis_std::types::osmosis::poolmanager::v1beta1::SwapAmountInRoute; +pub use osmosis_std::types::osmosis::poolmanager::v1beta1::SwapAmountInRoute as OsmosisSwapAmountInRoute; use crate::helpers::hashset; @@ -16,6 +16,17 @@ const TWAP_WINDOW_SIZE_SECONDS: u64 = 600u64; #[cw_serde] pub struct OsmosisRoute(pub Vec); +/// SwapAmountInRoute instead of using `osmosis_std::types::osmosis::poolmanager::v1beta1::SwapAmountInRoute` +/// to keep consistency for pool_id representation as u64. +/// +/// SwapAmountInRoute from osmosis package uses as_str serializer/deserializer, so it expects pool_id +/// as a String, but JSON schema doesn't correctly represent it. +#[cw_serde] +pub struct SwapAmountInRoute { + pub pool_id: u64, + pub token_out_denom: String, +} + impl fmt::Display for OsmosisRoute { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let s = self @@ -52,8 +63,9 @@ impl Route for OsmosisRoute { let mut seen_denoms = hashset(&[denom_in]); for (i, step) in steps.iter().enumerate() { let pool = query_pool(querier, step.pool_id)?; + let pool_denoms = pool.get_pool_denoms(); - if !has_denom(prev_denom_out, &pool.pool_assets) { + if !pool_denoms.contains(&prev_denom_out.to_string()) { return Err(ContractError::InvalidRoute { reason: format!( "step {}: pool {} does not contain input denom {}", @@ -64,7 +76,7 @@ impl Route for OsmosisRoute { }); } - if !has_denom(&step.token_out_denom, &pool.pool_assets) { + if !pool_denoms.contains(&step.token_out_denom) { return Err(ContractError::InvalidRoute { reason: format!( "step {}: pool {} does not contain output denom {}", @@ -117,9 +129,17 @@ impl Route for OsmosisRoute { let out_amount = query_out_amount(querier, &env.block, coin_in, steps)?; let min_out_amount = (Decimal::one() - slippage) * out_amount; + let routes: Vec<_> = steps + .iter() + .map(|step| OsmosisSwapAmountInRoute { + pool_id: step.pool_id, + token_out_denom: step.token_out_denom.clone(), + }) + .collect(); + let swap_msg: CosmosMsg = MsgSwapExactAmountIn { sender: env.contract.address.to_string(), - routes: steps.to_vec(), + routes, token_in: Some(osmosis_std::types::cosmos::base::v1beta1::Coin { denom: coin_in.denom.clone(), amount: coin_in.amount.to_string(), diff --git a/contracts/swapper/osmosis/tests/all_tests.rs b/contracts/swapper/osmosis/tests/all_tests.rs new file mode 100644 index 000000000..14f00389d --- /dev/null +++ b/contracts/swapper/osmosis/tests/all_tests.rs @@ -0,0 +1 @@ +mod tests; diff --git a/contracts/swapper/osmosis/tests/helpers.rs b/contracts/swapper/osmosis/tests/tests/helpers/mod.rs similarity index 100% rename from contracts/swapper/osmosis/tests/helpers.rs rename to contracts/swapper/osmosis/tests/tests/helpers/mod.rs diff --git a/contracts/swapper/osmosis/tests/tests/mod.rs b/contracts/swapper/osmosis/tests/tests/mod.rs new file mode 100644 index 000000000..51ee89c92 --- /dev/null +++ b/contracts/swapper/osmosis/tests/tests/mod.rs @@ -0,0 +1,8 @@ +mod helpers; + +mod test_enumerate_routes; +mod test_estimate; +mod test_instantiate; +mod test_set_route; +mod test_swap; +mod test_update_admin; diff --git a/contracts/swapper/osmosis/tests/test_enumerate_routes.rs b/contracts/swapper/osmosis/tests/tests/test_enumerate_routes.rs similarity index 96% rename from contracts/swapper/osmosis/tests/test_enumerate_routes.rs rename to contracts/swapper/osmosis/tests/tests/test_enumerate_routes.rs index 9a6358bc1..fa55a4fa5 100644 --- a/contracts/swapper/osmosis/tests/test_enumerate_routes.rs +++ b/contracts/swapper/osmosis/tests/tests/test_enumerate_routes.rs @@ -5,12 +5,9 @@ use std::collections::HashMap; use cosmwasm_std::coin; use cw_it::osmosis_test_tube::{Gamm, Module, OsmosisTestApp, SigningAccount, Wasm}; use mars_red_bank_types::swapper::{ExecuteMsg, QueryMsg, RouteResponse}; -use mars_swapper_osmosis::route::OsmosisRoute; -use osmosis_std::types::osmosis::poolmanager::v1beta1::SwapAmountInRoute; +use mars_swapper_osmosis::route::{OsmosisRoute, SwapAmountInRoute}; -use crate::helpers::instantiate_contract; - -pub mod helpers; +use super::helpers::instantiate_contract; #[test] fn enumerating_routes() { diff --git a/contracts/swapper/osmosis/tests/test_estimate.rs b/contracts/swapper/osmosis/tests/tests/test_estimate.rs similarity index 99% rename from contracts/swapper/osmosis/tests/test_estimate.rs rename to contracts/swapper/osmosis/tests/tests/test_estimate.rs index 8ac107320..42aeff7d8 100644 --- a/contracts/swapper/osmosis/tests/test_estimate.rs +++ b/contracts/swapper/osmosis/tests/tests/test_estimate.rs @@ -3,12 +3,10 @@ use cw_it::osmosis_test_tube::{Gamm, Module, OsmosisTestApp, RunnerResult, Wasm} use mars_red_bank_types::swapper::{EstimateExactInSwapResponse, ExecuteMsg, QueryMsg}; use mars_swapper_osmosis::route::{OsmosisRoute, SwapAmountInRoute}; -use crate::helpers::{ +use super::helpers::{ assert_err, instantiate_contract, query_price_from_pool, swap_to_create_twap_records, }; -pub mod helpers; - #[test] fn error_on_route_not_found() { let app = OsmosisTestApp::new(); diff --git a/contracts/swapper/osmosis/tests/test_instantiate.rs b/contracts/swapper/osmosis/tests/tests/test_instantiate.rs similarity index 94% rename from contracts/swapper/osmosis/tests/test_instantiate.rs rename to contracts/swapper/osmosis/tests/tests/test_instantiate.rs index 3c0008aa8..684230e7d 100644 --- a/contracts/swapper/osmosis/tests/test_instantiate.rs +++ b/contracts/swapper/osmosis/tests/tests/test_instantiate.rs @@ -3,9 +3,7 @@ use cw_it::osmosis_test_tube::{Account, Module, OsmosisTestApp, Wasm}; use mars_owner::OwnerResponse; use mars_red_bank_types::swapper::{InstantiateMsg, QueryMsg}; -use crate::helpers::{instantiate_contract, wasm_file}; - -pub mod helpers; +use super::helpers::{instantiate_contract, wasm_file}; #[test] fn owner_set_on_instantiate() { diff --git a/contracts/swapper/osmosis/tests/test_set_route.rs b/contracts/swapper/osmosis/tests/tests/test_set_route.rs similarity index 99% rename from contracts/swapper/osmosis/tests/test_set_route.rs rename to contracts/swapper/osmosis/tests/tests/test_set_route.rs index 83cdcbf85..150d23325 100644 --- a/contracts/swapper/osmosis/tests/test_set_route.rs +++ b/contracts/swapper/osmosis/tests/tests/test_set_route.rs @@ -5,9 +5,7 @@ use mars_red_bank_types::swapper::{ExecuteMsg, QueryMsg, RouteResponse}; use mars_swapper_base::ContractError; use mars_swapper_osmosis::route::{OsmosisRoute, SwapAmountInRoute}; -use crate::helpers::{assert_err, instantiate_contract}; - -pub mod helpers; +use super::helpers::{assert_err, instantiate_contract}; #[test] fn only_owner_can_set_routes() { diff --git a/contracts/swapper/osmosis/tests/test_swap.rs b/contracts/swapper/osmosis/tests/tests/test_swap.rs similarity index 85% rename from contracts/swapper/osmosis/tests/test_swap.rs rename to contracts/swapper/osmosis/tests/tests/test_swap.rs index ec1c7bc1b..9f39206ca 100644 --- a/contracts/swapper/osmosis/tests/test_swap.rs +++ b/contracts/swapper/osmosis/tests/tests/test_swap.rs @@ -7,12 +7,10 @@ use mars_red_bank_types::swapper::ExecuteMsg; use mars_swapper_base::ContractError; use mars_swapper_osmosis::route::{OsmosisRoute, SwapAmountInRoute}; -use crate::helpers::{ +use super::helpers::{ assert_err, instantiate_contract, query_balance, swap_to_create_twap_records, }; -pub mod helpers; - #[test] fn transfer_callback_only_internal() { let app = OsmosisTestApp::new(); @@ -46,6 +44,41 @@ fn transfer_callback_only_internal() { ); } +#[test] +fn max_slippage_exeeded() { + let app = OsmosisTestApp::new(); + let wasm = Wasm::new(&app); + + let accs = app + .init_accounts(&[coin(1_000_000_000_000, "uosmo"), coin(1_000_000_000_000, "umars")], 2) + .unwrap(); + let owner = &accs[0]; + let other_guy = &accs[1]; + + let contract_addr = instantiate_contract(&wasm, owner); + + let res_err = wasm + .execute( + &contract_addr, + &ExecuteMsg::::SwapExactIn { + coin_in: coin(1_000_000, "umars"), + denom_out: "uosmo".to_string(), + slippage: Decimal::percent(11), + }, + &[coin(1_000_000, "umars")], + other_guy, + ) + .unwrap_err(); + + assert_err( + res_err, + ContractError::MaxSlippageExceeded { + max_slippage: Decimal::percent(10), + slippage: Decimal::percent(11), + }, + ); +} + #[test] fn swap_exact_in_slippage_too_high() { let app = OsmosisTestApp::new(); diff --git a/contracts/swapper/osmosis/tests/test_update_admin.rs b/contracts/swapper/osmosis/tests/tests/test_update_admin.rs similarity index 98% rename from contracts/swapper/osmosis/tests/test_update_admin.rs rename to contracts/swapper/osmosis/tests/tests/test_update_admin.rs index 84abb85e0..a47e7ba8e 100644 --- a/contracts/swapper/osmosis/tests/test_update_admin.rs +++ b/contracts/swapper/osmosis/tests/tests/test_update_admin.rs @@ -4,9 +4,7 @@ use mars_owner::{OwnerResponse, OwnerUpdate}; use mars_red_bank_types::swapper::{ExecuteMsg, QueryMsg}; use mars_swapper_osmosis::route::OsmosisRoute; -use crate::helpers::instantiate_contract; - -pub mod helpers; +use super::helpers::instantiate_contract; #[test] fn initial_state() { diff --git a/coverage_grcov.Makefile.toml b/coverage_grcov.Makefile.toml index 8bc7601e2..68cf6b7f4 100644 --- a/coverage_grcov.Makefile.toml +++ b/coverage_grcov.Makefile.toml @@ -28,7 +28,8 @@ LLVM_PROFILE_FILE = "${COVERAGE_PROF_OUTPUT}/coverage-%p-%m.profraw" [tasks.install-grcov] condition = { env_not_set = ["SKIP_INSTALL_GRCOV"] } private = true -install_crate = { crate_name = "grcov" } +command = "cargo" +args = ["install", "grcov", "--locked"] [tasks.coverage-grcov] condition = { rust_version = { min = "1.60.0" } } diff --git a/files/types_diff_v1_0_0__mars_v2.txt b/files/types_diff_v1_0_0__mars_v2.txt new file mode 100644 index 000000000..ab5575313 --- /dev/null +++ b/files/types_diff_v1_0_0__mars_v2.txt @@ -0,0 +1,1514 @@ +diff --git a/packages/types/Cargo.toml b/packages/types/Cargo.toml +index 7d900594..d4475675 100644 +--- a/packages/types/Cargo.toml ++++ b/packages/types/Cargo.toml +@@ -1,5 +1,6 @@ + [package] + name = "mars-red-bank-types" ++description = "Messages and types for Red Bank smart contracts" + version = { workspace = true } + authors = { workspace = true } + edition = { workspace = true } +@@ -22,7 +23,5 @@ cosmwasm-schema = { workspace = true } + cosmwasm-std = { workspace = true } + mars-owner = { workspace = true } + mars-utils = { workspace = true } ++strum = { workspace = true, features = ["derive"] } + thiserror = { workspace = true } +- +-[dev-dependencies] +-mars-testing = { workspace = true } +diff --git a/packages/types/src/address_provider.rs b/packages/types/src/address_provider.rs +index cab8fbfd..c212ec89 100644 +--- a/packages/types/src/address_provider.rs ++++ b/packages/types/src/address_provider.rs +@@ -3,14 +3,17 @@ use std::{any::type_name, fmt, str::FromStr}; + use cosmwasm_schema::{cw_serde, QueryResponses}; + use cosmwasm_std::StdError; + use mars_owner::OwnerUpdate; ++use strum::EnumIter; + + #[cw_serde] +-#[derive(Copy, Eq, Hash)] ++#[derive(Copy, Eq, Hash, EnumIter)] + pub enum MarsAddressType { + Incentives, + Oracle, + RedBank, + RewardsCollector, ++ Params, ++ CreditManager, + /// Protocol admin is an ICS-27 interchain account controlled by Mars Hub's x/gov module. + /// This account will take the owner and admin roles of red-bank contracts. + /// +@@ -30,18 +33,23 @@ pub enum MarsAddressType { + /// NOTE: This is a Mars Hub address with the `mars` bech32 prefix, which may not be recognized + /// by the `api.addr_validate` method. + SafetyFund, ++ /// The swapper contract on the chain ++ Swapper, + } + + impl fmt::Display for MarsAddressType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { ++ MarsAddressType::CreditManager => "credit_manager", + MarsAddressType::FeeCollector => "fee_collector", + MarsAddressType::Incentives => "incentives", + MarsAddressType::Oracle => "oracle", ++ MarsAddressType::Params => "params", + MarsAddressType::ProtocolAdmin => "protocol_admin", + MarsAddressType::RedBank => "red_bank", + MarsAddressType::RewardsCollector => "rewards_collector", + MarsAddressType::SafetyFund => "safety_fund", ++ MarsAddressType::Swapper => "swapper", + }; + write!(f, "{s}") + } +@@ -52,13 +60,16 @@ impl FromStr for MarsAddressType { + + fn from_str(s: &str) -> Result { + match s { ++ "credit_manager" => Ok(MarsAddressType::CreditManager), + "fee_collector" => Ok(MarsAddressType::FeeCollector), + "incentives" => Ok(MarsAddressType::Incentives), + "oracle" => Ok(MarsAddressType::Oracle), ++ "params" => Ok(MarsAddressType::Params), + "protocol_admin" => Ok(MarsAddressType::ProtocolAdmin), + "red_bank" => Ok(MarsAddressType::RedBank), + "rewards_collector" => Ok(MarsAddressType::RewardsCollector), + "safety_fund" => Ok(MarsAddressType::SafetyFund), ++ "swapper" => Ok(MarsAddressType::Swapper), + _ => Err(StdError::parse_err(type_name::(), s)), + } + } +@@ -194,3 +205,25 @@ pub mod helpers { + .map(|res| res.address) + } + } ++ ++#[cfg(test)] ++mod tests { ++ use std::str::FromStr; ++ ++ use strum::IntoEnumIterator; ++ ++ use super::MarsAddressType; ++ ++ #[test] ++ fn mars_address_type_fmt_and_from_string() { ++ for address_type in MarsAddressType::iter() { ++ assert_eq!(MarsAddressType::from_str(&address_type.to_string()).unwrap(), address_type); ++ } ++ } ++ ++ #[test] ++ #[should_panic] ++ fn mars_address_type_from_str_invalid_string() { ++ MarsAddressType::from_str("invalid_address_type").unwrap(); ++ } ++} +diff --git a/packages/types/src/error.rs b/packages/types/src/error.rs +index 0a2c667d..e9cb6311 100644 +--- a/packages/types/src/error.rs ++++ b/packages/types/src/error.rs +@@ -1,4 +1,6 @@ +-use cosmwasm_std::StdError; ++use cosmwasm_std::{ ++ CheckedFromRatioError, CheckedMultiplyFractionError, DivideByZeroError, OverflowError, StdError, ++}; + use thiserror::Error; + + #[derive(Error, Debug, PartialEq)] +@@ -22,6 +24,18 @@ pub enum MarsError { + Deserialize { + target_type: String, + }, ++ ++ #[error("{0}")] ++ Overflow(#[from] OverflowError), ++ ++ #[error("{0}")] ++ DivideByZero(#[from] DivideByZeroError), ++ ++ #[error("{0}")] ++ CheckedFromRatio(#[from] CheckedFromRatioError), ++ ++ #[error("{0}")] ++ CheckedMultiplyFraction(#[from] CheckedMultiplyFractionError), + } + + impl From for StdError { +diff --git a/packages/types/src/incentives.rs b/packages/types/src/incentives.rs +index 1ec39a76..c94cad79 100644 +--- a/packages/types/src/incentives.rs ++++ b/packages/types/src/incentives.rs +@@ -1,5 +1,5 @@ + use cosmwasm_schema::{cw_serde, QueryResponses}; +-use cosmwasm_std::{Addr, Decimal, Uint128}; ++use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; + use mars_owner::OwnerUpdate; + + /// Global configuration +@@ -7,20 +7,15 @@ use mars_owner::OwnerUpdate; + pub struct Config { + /// Address provider + pub address_provider: Addr, +- /// Mars Token Denom +- pub mars_denom: String, ++ /// The maximum number of incentive denoms that can be whitelisted at any given time. This is ++ /// a guard against accidentally whitelisting too many denoms, which could cause max gas errors. ++ pub max_whitelisted_denoms: u8, + } + + /// Incentive Metadata for a given incentive + #[cw_serde] +-pub struct AssetIncentive { +- /// How much MARS per second is emitted to be then distributed to all Red Bank depositors +- pub emission_per_second: Uint128, +- /// Start time of the incentive (in seconds) since the UNIX epoch (00:00:00 on 1970-01-01 UTC) +- pub start_time: u64, +- /// How many seconds the incentives last +- pub duration: u64, +- /// Total MARS assigned for distribution since the start of the incentive ++pub struct IncentiveState { ++ /// An index that represents how many incentive tokens have been distributed per unit of collateral + pub index: Decimal, + /// Last time (in seconds) index was updated + pub last_updated: u64, +@@ -28,30 +23,54 @@ pub struct AssetIncentive { + + /// Incentive Metadata for a given incentive denom + #[cw_serde] +-pub struct AssetIncentiveResponse { +- /// Asset denom +- pub denom: String, +- /// How much MARS per second is emitted to be then distributed to all Red Bank depositors +- pub emission_per_second: Uint128, +- /// Start time of the incentive (in seconds) since the UNIX epoch (00:00:00 on 1970-01-01 UTC) +- pub start_time: u64, +- /// How many seconds the incentives last +- pub duration: u64, +- /// Total MARS assigned for distribution since the start of the incentive ++pub struct IncentiveStateResponse { ++ /// The denom for which users get the incentive if they provide collateral in the Red Bank ++ pub collateral_denom: String, ++ /// The denom of the token these incentives are paid with ++ pub incentive_denom: String, ++ /// An index that represents how many incentive tokens have been distributed per unit of collateral + pub index: Decimal, + /// Last time (in seconds) index was updated + pub last_updated: u64, + } + +-impl AssetIncentiveResponse { +- pub fn from(denom: String, ai: AssetIncentive) -> Self { ++impl IncentiveStateResponse { ++ pub fn from( ++ collateral_denom: impl Into, ++ incentive_denom: impl Into, ++ is: IncentiveState, ++ ) -> Self { ++ Self { ++ collateral_denom: collateral_denom.into(), ++ incentive_denom: incentive_denom.into(), ++ index: is.index, ++ last_updated: is.last_updated, ++ } ++ } ++} ++ ++#[cw_serde] ++pub struct WhitelistEntry { ++ /// The incentive token denom that is whitelisted ++ pub denom: String, ++ /// The minimum emission rate per second for this incentive token ++ pub min_emission_rate: Uint128, ++} ++ ++impl From<&(&str, u128)> for WhitelistEntry { ++ fn from((denom, min_emission_rate): &(&str, u128)) -> Self { ++ Self { ++ denom: denom.to_string(), ++ min_emission_rate: Uint128::from(*min_emission_rate), ++ } ++ } ++} ++ ++impl From<(String, Uint128)> for WhitelistEntry { ++ fn from((denom, min_emission_rate): (String, Uint128)) -> Self { + Self { + denom, +- emission_per_second: ai.emission_per_second, +- start_time: ai.start_time, +- duration: ai.duration, +- index: ai.index, +- last_updated: ai.last_updated, ++ min_emission_rate, + } + } + } +@@ -62,26 +81,41 @@ pub struct InstantiateMsg { + pub owner: String, + /// Address provider + pub address_provider: String, +- /// Mars token denom +- pub mars_denom: String, ++ /// The amount of time in seconds for each incentive epoch. This is the minimum amount of time ++ /// that an incentive can last, and each incentive must be a multiple of this duration. ++ pub epoch_duration: u64, ++ /// The maximum number of incentive denoms that can be whitelisted at any given time. This is ++ /// a guard against accidentally whitelisting too many denoms, which could cause max gas errors. ++ pub max_whitelisted_denoms: u8, + } + + #[cw_serde] + pub enum ExecuteMsg { +- /// Set incentive params for an asset to its depositor at Red Bank. +- /// +- /// If there is no incentive for the asset, all params are required. +- /// New incentive can be set (rescheduled) if current one has finished (current_block_time > start_time + duration). ++ /// Add or remove incentive denoms from the whitelist. Only admin can do this. ++ UpdateWhitelist { ++ /// The denoms to add to the whitelist as well as a minimum emission rate per second for ++ /// each. If the denom is already in the whitelist, the minimum emission rate will be updated. ++ add_denoms: Vec, ++ /// The denoms to remove from the whitelist. This will update the index of the incentive ++ /// state and then remove any active incentive schedules. ++ /// ++ /// NB: If any incentive schedules are still active for this incentive denom, the incentive ++ /// tokens will be trapped forever in the contract. ++ remove_denoms: Vec, ++ }, ++ /// Add incentives for a given collateral denom and incentive denom pair + SetAssetIncentive { +- /// Asset denom associated with the incentives +- denom: String, +- /// How many MARS will be assigned per second to be distributed among all Red Bank +- /// depositors +- emission_per_second: Option, ++ /// The denom of the collatearal token to receive incentives ++ collateral_denom: String, ++ /// The denom of the token to give incentives with ++ incentive_denom: String, ++ /// How many `incentive_denom` tokens will be assigned per second to be distributed among ++ /// all Red Bank depositors ++ emission_per_second: Uint128, + /// Start time of the incentive (in seconds) since the UNIX epoch (00:00:00 on 1970-01-01 UTC). +- start_time: Option, ++ start_time: u64, + /// How many seconds the incentives last +- duration: Option, ++ duration: u64, + }, + + /// Handle balance change updating user and asset rewards. +@@ -91,6 +125,8 @@ pub enum ExecuteMsg { + /// User address. Address is trusted as it must be validated by the Red Bank + /// contract before calling this method + user_addr: Addr, ++ /// Credit account id (Rover) ++ account_id: Option, + /// Denom of the asset of which deposited balance is changed + denom: String, + /// The user's scaled collateral amount up to the instant before the change +@@ -101,12 +137,26 @@ pub enum ExecuteMsg { + + /// Claim rewards. MARS rewards accrued by the user will be staked into xMARS before + /// being sent. +- ClaimRewards {}, ++ ClaimRewards { ++ /// Credit account id (Rover) ++ account_id: Option, ++ /// Start pagination after this collateral denom ++ start_after_collateral_denom: Option, ++ /// Start pagination after this incentive denom. If supplied you must also supply ++ /// start_after_collateral_denom. ++ start_after_incentive_denom: Option, ++ /// The maximum number of results to return. If not set, 5 is used. If larger than 10, ++ /// 10 is used. ++ limit: Option, ++ }, + + /// Update contract config (only callable by owner) + UpdateConfig { ++ /// The address provider contract address + address_provider: Option, +- mars_denom: Option, ++ /// The maximum number of incentive denoms that can be whitelisted at any given time. This is ++ /// a guard against accidentally whitelisting too many denoms, which could cause max gas errors. ++ max_whitelisted_denoms: Option, + }, + + /// Manages admin role state +@@ -116,28 +166,128 @@ pub enum ExecuteMsg { + #[cw_serde] + #[derive(QueryResponses)] + pub enum QueryMsg { ++ /// Query all active incentive emissions for a collateral denom ++ #[returns(Vec)] ++ ActiveEmissions { ++ /// The denom of the token that users supply as collateral to receive incentives ++ collateral_denom: String, ++ }, ++ + /// Query contract config + #[returns(ConfigResponse)] + Config {}, + +- /// Query info about asset incentive for a given denom +- #[returns(AssetIncentiveResponse)] +- AssetIncentive { +- denom: String, ++ /// Query info about the state of an incentive for a given collateral and incentive denom pair ++ #[returns(IncentiveStateResponse)] ++ IncentiveState { ++ /// The denom of the token that users supply as collateral to receive incentives ++ collateral_denom: String, ++ /// The denom of the token which is used to give incentives with ++ incentive_denom: String, + }, + +- /// Enumerate asset incentives with pagination +- #[returns(Vec)] +- AssetIncentives { +- start_after: Option, ++ /// Enumerate incentive states with pagination ++ #[returns(Vec)] ++ IncentiveStates { ++ /// Start pagination after this collateral denom ++ start_after_collateral_denom: Option, ++ /// Start pagination after this incentive denom. If supplied you must also supply ++ /// start_after_collateral_denom. ++ start_after_incentive_denom: Option, ++ /// The maximum number of results to return. If not set, 5 is used. If larger than 10, ++ /// 10 is used. + limit: Option, + }, + +- /// Query user current unclaimed rewards ++ /// Queries the planned emission rate for a given collateral and incentive denom tuple at the ++ /// specified unix timestamp. The emission rate returned is the amount of incentive tokens ++ /// that will be emitted per second for each unit of collateral supplied during the epoch. ++ /// NB: that the returned value can change if someone adds incentives to the contract. + #[returns(Uint128)] ++ Emission { ++ /// The denom of the token that users supply as collateral to receive incentives ++ collateral_denom: String, ++ /// The denom of the token which is used to give incentives with ++ incentive_denom: String, ++ /// The unix timestamp in second to query the emission rate at. ++ timestamp: u64, ++ }, ++ ++ /// Enumerate all incentive emission rates with pagination for a specified collateral and ++ /// indentive denom pair ++ #[returns(Vec)] ++ Emissions { ++ /// The denom of the token that users supply as collateral to receive incentives ++ collateral_denom: String, ++ /// The denom of the token which is used to give incentives with ++ incentive_denom: String, ++ /// Start pagination after this timestamp ++ start_after_timestamp: Option, ++ /// The maximum number of results to return. If not set, 5 is used. If larger than 10, ++ /// 10 is used. ++ limit: Option, ++ }, ++ ++ /// Query user current unclaimed rewards ++ #[returns(Vec)] + UserUnclaimedRewards { ++ /// The user address for which to query unclaimed rewards + user: String, ++ /// Credit account id (Rover) ++ account_id: Option, ++ /// Start pagination after this collateral denom ++ start_after_collateral_denom: Option, ++ /// Start pagination after this incentive denom. If supplied you must also supply ++ /// start_after_collateral_denom. ++ start_after_incentive_denom: Option, ++ /// The maximum number of results to return. If not set, 5 is used. If larger than 10, ++ /// 10 is used. ++ limit: Option, + }, ++ ++ /// Queries the incentive denom whitelist. Returns a Vec<(String, Uint128)> containing the ++ /// denoms of all whitelisted incentive denoms, as well as the minimum emission rate for each. ++ #[returns(Vec)] ++ Whitelist {}, ++} ++ ++#[cw_serde] ++pub struct MigrateMsg {} ++ ++#[cw_serde] ++pub struct EmissionResponse { ++ /// The unix timestamp in seconds at which the emission epoch starts ++ pub epoch_start: u64, ++ /// The emission rate returned is the amount of incentive tokens that will be emitted per ++ /// second for each unit of collateral supplied during the epoch. ++ pub emission_rate: Uint128, ++} ++ ++impl From<(u64, Uint128)> for EmissionResponse { ++ fn from((epoch_start, emission_rate): (u64, Uint128)) -> Self { ++ Self { ++ epoch_start, ++ emission_rate, ++ } ++ } ++} ++ ++#[cw_serde] ++/// The currently active emission for a given incentive denom ++pub struct ActiveEmission { ++ /// The denom for which incentives are being distributed ++ pub denom: String, ++ /// The amount of incentive tokens that are being emitted per second ++ pub emission_rate: Uint128, ++} ++ ++impl From<(String, Uint128)> for ActiveEmission { ++ fn from((denom, emission_rate): (String, Uint128)) -> Self { ++ Self { ++ denom, ++ emission_rate, ++ } ++ } + } + + #[cw_serde] +@@ -148,6 +298,11 @@ pub struct ConfigResponse { + pub proposed_new_owner: Option, + /// Address provider + pub address_provider: Addr, +- /// Mars Token Denom +- pub mars_denom: String, ++ /// The maximum number of incentive denoms that can be whitelisted at any given time. This is ++ /// a guard against accidentally whitelisting too many denoms, which could cause max gas errors. ++ pub max_whitelisted_denoms: u8, ++ /// The epoch duration in seconds ++ pub epoch_duration: u64, ++ /// The count of the number of whitelisted incentive denoms ++ pub whitelist_count: u8, + } +diff --git a/packages/types/src/lib.rs b/packages/types/src/lib.rs +index 7ec7c2e0..5750ce35 100644 +--- a/packages/types/src/lib.rs ++++ b/packages/types/src/lib.rs +@@ -4,3 +4,17 @@ pub mod incentives; + pub mod oracle; + pub mod red_bank; + pub mod rewards_collector; ++pub mod swapper; ++ ++use cosmwasm_schema::cw_serde; ++ ++#[cw_serde] ++pub struct PaginationResponse { ++ pub data: Vec, ++ pub metadata: Metadata, ++} ++ ++#[cw_serde] ++pub struct Metadata { ++ pub has_more: bool, ++} +diff --git a/packages/types/src/oracle/mod.rs b/packages/types/src/oracle/mod.rs +new file mode 100644 +index 00000000..65238184 +--- /dev/null ++++ b/packages/types/src/oracle/mod.rs +@@ -0,0 +1,5 @@ ++pub mod msg; ++pub mod wasm_oracle; ++ ++pub use msg::*; ++pub use wasm_oracle::*; +diff --git a/packages/types/src/oracle.rs b/packages/types/src/oracle/msg.rs +similarity index 65% +rename from packages/types/src/oracle.rs +rename to packages/types/src/oracle/msg.rs +index d5b9d02c..572aae9b 100644 +--- a/packages/types/src/oracle.rs ++++ b/packages/types/src/oracle/msg.rs +@@ -1,13 +1,15 @@ + use cosmwasm_schema::{cw_serde, QueryResponses}; +-use cosmwasm_std::Decimal; ++use cosmwasm_std::{Decimal, Empty}; + use mars_owner::OwnerUpdate; + + #[cw_serde] +-pub struct InstantiateMsg { ++pub struct InstantiateMsg { + /// The contract's owner, who can update config and price sources + pub owner: String, + /// The asset in which prices are denominated in + pub base_denom: String, ++ /// Custom init params ++ pub custom_init: Option, + } + + #[cw_serde] +@@ -17,7 +19,7 @@ pub struct Config { + } + + #[cw_serde] +-pub enum ExecuteMsg { ++pub enum ExecuteMsg { + /// Specify the price source to be used for a coin + /// + /// NOTE: The input parameters for method are chain-specific. +@@ -31,6 +33,19 @@ pub enum ExecuteMsg { + }, + /// Manages admin role state + UpdateOwner(OwnerUpdate), ++ /// Update contract config (only callable by owner) ++ UpdateConfig { ++ base_denom: Option, ++ }, ++ /// Custom messages defined by the contract ++ Custom(C), ++} ++ ++/// Differentiator for the action (liquidate, withdraw, borrow etc.) being performed. ++#[cw_serde] ++pub enum ActionKind { ++ Default, ++ Liquidation, + } + + #[cw_serde] +@@ -61,6 +76,7 @@ pub enum QueryMsg { + #[returns(PriceResponse)] + Price { + denom: String, ++ kind: Option, + }, + /// Enumerate all coins' prices. + /// +@@ -70,6 +86,7 @@ pub enum QueryMsg { + Prices { + start_after: Option, + limit: Option, ++ kind: Option, + }, + } + +@@ -96,19 +113,44 @@ pub struct PriceResponse { + } + + pub mod helpers { +- use cosmwasm_std::{Decimal, QuerierWrapper, StdResult}; ++ use cosmwasm_std::{Decimal, QuerierWrapper, StdError, StdResult}; + +- use super::{PriceResponse, QueryMsg}; ++ use super::{ActionKind, PriceResponse, QueryMsg}; ++ use crate::oracle::ActionKind::Liquidation; + + pub fn query_price( + querier: &QuerierWrapper, + oracle: impl Into, + denom: impl Into, ++ ) -> StdResult { ++ let denom = denom.into(); ++ let res: PriceResponse = querier ++ .query_wasm_smart( ++ oracle.into(), ++ &QueryMsg::Price { ++ denom: denom.clone(), ++ kind: Some(ActionKind::Default), ++ }, ++ ) ++ .map_err(|e| { ++ StdError::generic_err(format!( ++ "failed to query price for denom: {}. Error: {}", ++ denom, e ++ )) ++ })?; ++ Ok(res.price) ++ } ++ ++ pub fn query_price_for_liquidate( ++ querier: &QuerierWrapper, ++ oracle: impl Into, ++ denom: impl Into, + ) -> StdResult { + let res: PriceResponse = querier.query_wasm_smart( + oracle.into(), + &QueryMsg::Price { + denom: denom.into(), ++ kind: Some(Liquidation), + }, + )?; + Ok(res.price) +diff --git a/packages/types/src/oracle/wasm_oracle.rs b/packages/types/src/oracle/wasm_oracle.rs +new file mode 100644 +index 00000000..1d772612 +--- /dev/null ++++ b/packages/types/src/oracle/wasm_oracle.rs +@@ -0,0 +1,23 @@ ++use cosmwasm_schema::cw_serde; ++use cosmwasm_std::Uint128; ++ ++#[cw_serde] ++pub struct WasmOracleCustomInitParams { ++ /// The Astroport factory contract address ++ pub astroport_factory: String, ++} ++ ++#[cw_serde] ++pub enum WasmOracleCustomExecuteMsg { ++ RecordTwapSnapshots { ++ denoms: Vec, ++ }, ++} ++ ++#[cw_serde] ++pub struct AstroportTwapSnapshot { ++ /// Timestamp of the most recent TWAP data update ++ pub timestamp: u64, ++ /// Cumulative price of the asset retrieved by the most recent TWAP data update ++ pub price_cumulative: Uint128, ++} +diff --git a/packages/types/src/red_bank/interest_rate_model.rs b/packages/types/src/red_bank/interest_rate_model.rs +index 9e2af312..26f844ea 100644 +--- a/packages/types/src/red_bank/interest_rate_model.rs ++++ b/packages/types/src/red_bank/interest_rate_model.rs +@@ -1,6 +1,8 @@ + use cosmwasm_schema::cw_serde; +-use cosmwasm_std::{Decimal, StdError, StdResult}; +-use mars_utils::{error::ValidationError, helpers::decimal_param_le_one, math}; ++use cosmwasm_std::Decimal; ++use mars_utils::{error::ValidationError, helpers::decimal_param_le_one}; ++ ++use crate::error::MarsError; + + #[cw_serde] + #[derive(Eq, Default)] +@@ -9,40 +11,47 @@ pub struct InterestRateModel { + pub optimal_utilization_rate: Decimal, + /// Base rate + pub base: Decimal, +- /// Slope parameter for interest rate model function when utilization_rate < optimal_utilization_rate ++ /// Slope parameter for interest rate model function when utilization_rate <= optimal_utilization_rate + pub slope_1: Decimal, +- /// Slope parameter for interest rate model function when utilization_rate >= optimal_utilization_rate ++ /// Slope parameter for interest rate model function when utilization_rate > optimal_utilization_rate + pub slope_2: Decimal, + } + + impl InterestRateModel { + pub fn validate(&self) -> Result<(), ValidationError> { + decimal_param_le_one(self.optimal_utilization_rate, "optimal_utilization_rate")?; ++ ++ if self.slope_1 >= self.slope_2 { ++ return Err(ValidationError::InvalidParam { ++ param_name: "slope_1".to_string(), ++ invalid_value: self.slope_1.to_string(), ++ predicate: format!("< {}", self.slope_2), ++ }); ++ } ++ + Ok(()) + } + +- pub fn get_borrow_rate(&self, current_utilization_rate: Decimal) -> StdResult { ++ pub fn get_borrow_rate(&self, current_utilization_rate: Decimal) -> Result { + let new_borrow_rate = if current_utilization_rate <= self.optimal_utilization_rate { + if current_utilization_rate.is_zero() { +- // prevent division by zero when optimal_utilization_rate is zero ++ // prevent division by zero when current_utilization_rate is zero + self.base + } else { + // The borrow interest rates increase slowly with utilization + self.base +- + self.slope_1.checked_mul(math::divide_decimal_by_decimal( +- current_utilization_rate, +- self.optimal_utilization_rate, +- )?)? ++ + self.slope_1.checked_mul( ++ current_utilization_rate.checked_div(self.optimal_utilization_rate)?, ++ )? + } + } else { + // The borrow interest rates increase sharply with utilization + self.base + + self.slope_1 +- + math::divide_decimal_by_decimal( +- self.slope_2 +- .checked_mul(current_utilization_rate - self.optimal_utilization_rate)?, +- Decimal::one() - self.optimal_utilization_rate, +- )? ++ + self ++ .slope_2 ++ .checked_mul(current_utilization_rate - self.optimal_utilization_rate)? ++ .checked_div(Decimal::one() - self.optimal_utilization_rate)? + }; + Ok(new_borrow_rate) + } +@@ -52,12 +61,11 @@ impl InterestRateModel { + borrow_rate: Decimal, + current_utilization_rate: Decimal, + reserve_factor: Decimal, +- ) -> StdResult { +- borrow_rate ++ ) -> Result { ++ Ok(borrow_rate + .checked_mul(current_utilization_rate)? + // This operation should not underflow as reserve_factor is checked to be <= 1 +- .checked_mul(Decimal::one() - reserve_factor) +- .map_err(StdError::from) ++ .checked_mul(Decimal::one() - reserve_factor)?) + } + } + +@@ -91,21 +99,13 @@ mod tests { + + market.update_interest_rates(utilization_rate).unwrap(); + +- let expected_borrow_rate = model.base +- + math::divide_decimal_by_decimal( +- model.slope_1.checked_mul(utilization_rate).unwrap(), +- model.optimal_utilization_rate, +- ) +- .unwrap(); ++ let expected_borrow_rate = ++ model.base + model.slope_1 * utilization_rate / model.optimal_utilization_rate; + + assert_eq!(market.borrow_rate, expected_borrow_rate); + assert_eq!( + market.liquidity_rate, +- expected_borrow_rate +- .checked_mul(utilization_rate) +- .unwrap() +- .checked_mul(Decimal::one() - reserve_factor) +- .unwrap() ++ expected_borrow_rate * utilization_rate * (Decimal::one() - reserve_factor) + ); + } + +@@ -124,11 +124,7 @@ mod tests { + let new_borrow_rate = model.get_borrow_rate(current_utilization_rate).unwrap(); + + let expected_borrow_rate = model.base +- + math::divide_decimal_by_decimal( +- model.slope_1.checked_mul(current_utilization_rate).unwrap(), +- model.optimal_utilization_rate, +- ) +- .unwrap(); ++ + model.slope_1 * current_utilization_rate / model.optimal_utilization_rate; + + assert_eq!(new_borrow_rate, expected_borrow_rate); + } +@@ -139,11 +135,7 @@ mod tests { + let new_borrow_rate = model.get_borrow_rate(current_utilization_rate).unwrap(); + + let expected_borrow_rate = model.base +- + math::divide_decimal_by_decimal( +- model.slope_1.checked_mul(current_utilization_rate).unwrap(), +- model.optimal_utilization_rate, +- ) +- .unwrap(); ++ + model.slope_1 * current_utilization_rate / model.optimal_utilization_rate; + + assert_eq!(new_borrow_rate, expected_borrow_rate); + } +@@ -155,14 +147,8 @@ mod tests { + + let expected_borrow_rate = model.base + + model.slope_1 +- + math::divide_decimal_by_decimal( +- model +- .slope_2 +- .checked_mul(current_utilization_rate - model.optimal_utilization_rate) +- .unwrap(), +- Decimal::one() - model.optimal_utilization_rate, +- ) +- .unwrap(); ++ + model.slope_2 * (current_utilization_rate - model.optimal_utilization_rate) ++ / (Decimal::one() - model.optimal_utilization_rate); + + assert_eq!(new_borrow_rate, expected_borrow_rate); + } +@@ -179,9 +165,7 @@ mod tests { + let current_utilization_rate = Decimal::percent(100); + let new_borrow_rate = model.get_borrow_rate(current_utilization_rate).unwrap(); + +- let expected_borrow_rate = Decimal::percent(7); +- +- assert_eq!(new_borrow_rate, expected_borrow_rate); ++ assert_eq!(new_borrow_rate, Decimal::percent(7)); + } + + // current utilization rate == 0% and optimal utilization rate == 0% +@@ -196,9 +180,7 @@ mod tests { + let current_utilization_rate = Decimal::percent(0); + let new_borrow_rate = model.get_borrow_rate(current_utilization_rate).unwrap(); + +- let expected_borrow_rate = Decimal::percent(2); +- +- assert_eq!(new_borrow_rate, expected_borrow_rate); ++ assert_eq!(new_borrow_rate, Decimal::percent(2)); + } + + // current utilization rate == 20% and optimal utilization rate == 0% +@@ -213,9 +195,8 @@ mod tests { + let current_utilization_rate = Decimal::percent(20); + let new_borrow_rate = model.get_borrow_rate(current_utilization_rate).unwrap(); + +- let expected_borrow_rate = model.base +- + model.slope_1 +- + model.slope_2.checked_mul(current_utilization_rate).unwrap(); ++ let expected_borrow_rate = ++ model.base + model.slope_1 + model.slope_2 * current_utilization_rate; + + assert_eq!(new_borrow_rate, expected_borrow_rate); + } +diff --git a/packages/types/src/red_bank/market.rs b/packages/types/src/red_bank/market.rs +index cb5f9688..2ffe0a04 100644 +--- a/packages/types/src/red_bank/market.rs ++++ b/packages/types/src/red_bank/market.rs +@@ -1,9 +1,6 @@ + use cosmwasm_schema::cw_serde; + use cosmwasm_std::{Decimal, StdResult, Uint128}; +-use mars_utils::{ +- error::ValidationError, +- helpers::{decimal_param_le_one, decimal_param_lt_one}, +-}; ++use mars_utils::{error::ValidationError, helpers::decimal_param_lt_one}; + + use crate::red_bank::InterestRateModel; + +@@ -11,14 +8,6 @@ use crate::red_bank::InterestRateModel; + pub struct Market { + /// Denom of the asset + pub denom: String, +- +- /// Max base asset that can be borrowed per "base asset" collateral when using the asset as collateral +- pub max_loan_to_value: Decimal, +- /// Base asset amount in debt position per "base asset" of asset collateral that if surpassed makes the user's position liquidatable. +- pub liquidation_threshold: Decimal, +- /// Bonus amount of collateral liquidator get when repaying user's debt (Will get collateral +- /// from user in an amount equal to debt repayed + bonus) +- pub liquidation_bonus: Decimal, + /// Portion of the borrow rate that is kept as protocol rewards + pub reserve_factor: Decimal, + +@@ -40,13 +29,6 @@ pub struct Market { + pub collateral_total_scaled: Uint128, + /// Total debt scaled for the market's currency + pub debt_total_scaled: Uint128, +- +- /// If false cannot deposit +- pub deposit_enabled: bool, +- /// If false cannot borrow +- pub borrow_enabled: bool, +- /// Deposit Cap (defined in terms of the asset) +- pub deposit_cap: Uint128, + } + + impl Default for Market { +@@ -57,18 +39,11 @@ impl Default for Market { + liquidity_index: Decimal::one(), + borrow_rate: Decimal::zero(), + liquidity_rate: Decimal::zero(), +- max_loan_to_value: Decimal::zero(), + reserve_factor: Decimal::zero(), + indexes_last_updated: 0, + collateral_total_scaled: Uint128::zero(), + debt_total_scaled: Uint128::zero(), +- liquidation_threshold: Decimal::one(), +- liquidation_bonus: Decimal::zero(), + interest_rate_model: InterestRateModel::default(), +- deposit_enabled: true, +- borrow_enabled: true, +- // By default the cap should be unlimited (no cap) +- deposit_cap: Uint128::MAX, + } + } + } +@@ -76,18 +51,6 @@ impl Default for Market { + impl Market { + pub fn validate(&self) -> Result<(), ValidationError> { + decimal_param_lt_one(self.reserve_factor, "reserve_factor")?; +- decimal_param_le_one(self.max_loan_to_value, "max_loan_to_value")?; +- decimal_param_le_one(self.liquidation_threshold, "liquidation_threshold")?; +- decimal_param_le_one(self.liquidation_bonus, "liquidation_bonus")?; +- +- // liquidation_threshold should be greater than max_loan_to_value +- if self.liquidation_threshold <= self.max_loan_to_value { +- return Err(ValidationError::InvalidParam { +- param_name: "liquidation_threshold".to_string(), +- invalid_value: self.liquidation_threshold.to_string(), +- predicate: format!("> {} (max LTV)", self.max_loan_to_value), +- }); +- } + + self.interest_rate_model.validate()?; + +diff --git a/packages/types/src/red_bank/msg.rs b/packages/types/src/red_bank/msg.rs +index c7ad2029..b80f7ec8 100644 +--- a/packages/types/src/red_bank/msg.rs ++++ b/packages/types/src/red_bank/msg.rs +@@ -8,8 +8,6 @@ use crate::red_bank::InterestRateModel; + pub struct InstantiateMsg { + /// Contract's owner + pub owner: String, +- /// Contract's emergency owner +- pub emergency_owner: String, + /// Market configuration + pub config: CreateOrUpdateConfig, + } +@@ -19,9 +17,6 @@ pub enum ExecuteMsg { + /// Manages owner state + UpdateOwner(OwnerUpdate), + +- /// Manages emergency owner state +- UpdateEmergencyOwner(OwnerUpdate), +- + /// Update contract config (only owner can call) + UpdateConfig { + config: CreateOrUpdateConfig, +@@ -59,8 +54,8 @@ pub enum ExecuteMsg { + /// Deposit native coins. Deposited coins must be sent in the transaction + /// this call is made + Deposit { +- /// Address that will receive the coins +- on_behalf_of: Option, ++ /// Credit account id (Rover) ++ account_id: Option, + }, + + /// Withdraw native coins +@@ -71,6 +66,11 @@ pub enum ExecuteMsg { + amount: Option, + /// The address where the withdrawn amount is sent + recipient: Option, ++ /// Credit account id (Rover) ++ account_id: Option, ++ // Withdraw action related to liquidation process initiated in credit manager. ++ // This flag is used to identify different way for pricing assets during liquidation. ++ liquidation_related: Option, + }, + + /// Borrow native coins. If borrow allowed, amount is added to caller's debt +@@ -117,30 +117,15 @@ pub enum ExecuteMsg { + #[cw_serde] + pub struct CreateOrUpdateConfig { + pub address_provider: Option, +- pub close_factor: Option, + } + + #[cw_serde] + pub struct InitOrUpdateAssetParams { + /// Portion of the borrow rate that is kept as protocol rewards + pub reserve_factor: Option, +- /// Max uusd that can be borrowed per uusd of collateral when using the asset as collateral +- pub max_loan_to_value: Option, +- /// uusd amount in debt position per uusd of asset collateral that if surpassed makes the user's position liquidatable. +- pub liquidation_threshold: Option, +- /// Bonus amount of collateral liquidator get when repaying user's debt (Will get collateral +- /// from user in an amount equal to debt repayed + bonus) +- pub liquidation_bonus: Option, + + /// Interest rate strategy to calculate borrow_rate and liquidity_rate + pub interest_rate_model: Option, +- +- /// If false cannot deposit +- pub deposit_enabled: Option, +- /// If false cannot borrow +- pub borrow_enabled: Option, +- /// Deposit Cap defined in terms of the asset (Unlimited by default) +- pub deposit_cap: Option, + } + + #[cw_serde] +@@ -197,6 +182,7 @@ pub enum QueryMsg { + #[returns(crate::red_bank::UserCollateralResponse)] + UserCollateral { + user: String, ++ account_id: Option, + denom: String, + }, + +@@ -204,6 +190,16 @@ pub enum QueryMsg { + #[returns(Vec)] + UserCollaterals { + user: String, ++ account_id: Option, ++ start_after: Option, ++ limit: Option, ++ }, ++ ++ /// Get all collateral positions for a user ++ #[returns(crate::red_bank::PaginatedUserCollateralResponse)] ++ UserCollateralsV2 { ++ user: String, ++ account_id: Option, + start_after: Option, + limit: Option, + }, +@@ -212,6 +208,14 @@ pub enum QueryMsg { + #[returns(crate::red_bank::UserPositionResponse)] + UserPosition { + user: String, ++ account_id: Option, ++ }, ++ ++ /// Get user position for liquidation ++ #[returns(crate::red_bank::UserPositionResponse)] ++ UserPositionLiquidationPricing { ++ user: String, ++ account_id: Option, + }, + + /// Get liquidity scaled amount for a given underlying asset amount. +diff --git a/packages/types/src/red_bank/types.rs b/packages/types/src/red_bank/types.rs +index c6cd1202..d6b14044 100644 +--- a/packages/types/src/red_bank/types.rs ++++ b/packages/types/src/red_bank/types.rs +@@ -1,21 +1,13 @@ + use cosmwasm_schema::cw_serde; + use cosmwasm_std::{Decimal, Uint128}; +-use mars_utils::{error::ValidationError, helpers::decimal_param_le_one}; ++ ++use crate::PaginationResponse; + + /// Global configuration + #[cw_serde] + pub struct Config { + /// Address provider returns addresses for all protocol contracts + pub address_provider: T, +- /// Maximum percentage of outstanding debt that can be covered by a liquidator +- pub close_factor: Decimal, +-} +- +-impl Config { +- pub fn validate(&self) -> Result<(), ValidationError> { +- decimal_param_le_one(self.close_factor, "close_factor")?; +- Ok(()) +- } + } + + #[cw_serde] +@@ -70,14 +62,8 @@ pub struct ConfigResponse { + pub owner: Option, + /// The contract's proposed owner + pub proposed_new_owner: Option, +- /// The contract's emergency owner +- pub emergency_owner: Option, +- /// The contract's proposed emergency owner +- pub proposed_new_emergency_owner: Option, + /// Address provider returns addresses for all protocol contracts + pub address_provider: String, +- /// Maximum percentage of outstanding debt that can be covered by a liquidator +- pub close_factor: Decimal, + } + + #[cw_serde] +@@ -112,6 +98,8 @@ pub struct UserCollateralResponse { + pub enabled: bool, + } + ++pub type PaginatedUserCollateralResponse = PaginationResponse; ++ + #[cw_serde] + pub struct UserPositionResponse { + /// Total value of all enabled collateral assets. +diff --git a/packages/types/src/rewards_collector.rs b/packages/types/src/rewards_collector.rs +index ce69309a..149490eb 100644 +--- a/packages/types/src/rewards_collector.rs ++++ b/packages/types/src/rewards_collector.rs +@@ -1,11 +1,13 @@ + use cosmwasm_schema::{cw_serde, QueryResponses}; +-use cosmwasm_std::{Addr, Api, Decimal, StdResult, Uint128}; ++use cosmwasm_std::{Addr, Api, Coin, Decimal, StdResult, Uint128}; + use mars_owner::OwnerUpdate; + use mars_utils::{ + error::ValidationError, + helpers::{decimal_param_le_one, integer_param_gt_zero, validate_native_denom}, + }; + ++use self::credit_manager::Action; ++ + const MAX_SLIPPAGE_TOLERANCE_PERCENTAGE: u64 = 50; + + #[cw_serde] +@@ -26,6 +28,8 @@ pub struct InstantiateMsg { + pub timeout_seconds: u64, + /// Maximum percentage of price movement (minimum amount you accept to receive during swap) + pub slippage_tolerance: Decimal, ++ /// Neutron Ibc config ++ pub neutron_ibc_config: Option, + } + + #[cw_serde] +@@ -44,6 +48,15 @@ pub struct Config { + pub timeout_seconds: u64, + /// Maximum percentage of price movement (minimum amount you accept to receive during swap) + pub slippage_tolerance: Decimal, ++ /// Neutron IBC config ++ pub neutron_ibc_config: Option, ++} ++ ++#[cw_serde] ++pub struct NeutronIbcConfig { ++ pub source_port: String, ++ pub acc_fee: Vec, ++ pub timeout_fee: Vec, + } + + impl Config { +@@ -77,6 +90,7 @@ impl Config { + channel_id: msg.channel_id, + timeout_seconds: msg.timeout_seconds, + slippage_tolerance: msg.slippage_tolerance, ++ neutron_ibc_config: msg.neutron_ibc_config, + }) + } + } +@@ -98,10 +112,12 @@ pub struct UpdateConfig { + pub timeout_seconds: Option, + /// Maximum percentage of price movement (minimum amount you accept to receive during swap) + pub slippage_tolerance: Option, ++ /// Neutron Ibc config ++ pub neutron_ibc_config: Option, + } + + #[cw_serde] +-pub enum ExecuteMsg { ++pub enum ExecuteMsg { + /// Manages admin role state + UpdateOwner(OwnerUpdate), + +@@ -110,22 +126,18 @@ pub enum ExecuteMsg { + new_cfg: UpdateConfig, + }, + +- /// Configure the route for swapping an asset +- /// +- /// This is chain-specific, and can include parameters such as slippage tolerance and the routes +- /// for multi-step swaps +- SetRoute { +- denom_in: String, +- denom_out: String, +- route: Route, +- }, +- + /// Withdraw coins from the red bank + WithdrawFromRedBank { + denom: String, + amount: Option, + }, + ++ /// Withdraw coins from the credit manager ++ WithdrawFromCreditManager { ++ account_id: String, ++ actions: Vec, ++ }, ++ + /// Distribute the accrued protocol income between the safety fund and the fee modules on mars hub, + /// according to the split set in config. + /// Callable by any address. +@@ -144,7 +156,16 @@ pub enum ExecuteMsg { + /// + /// We wanted to leave protocol rewards in the red-bank so they continue to work as liquidity (until the bot invokes WithdrawFromRedBank). + /// As an side effect to this, if the market is incentivised with MARS tokens, the contract will also accrue MARS token incentives. +- ClaimIncentiveRewards {}, ++ ClaimIncentiveRewards { ++ /// Start pagination after this collateral denom ++ start_after_collateral_denom: Option, ++ /// Start pagination after this incentive denom. If supplied you must also supply ++ /// start_after_collateral_denom. ++ start_after_incentive_denom: Option, ++ /// The maximum number of results to return. If not set, 5 is used. If larger than 10, ++ /// 10 is used. ++ limit: Option, ++ }, + } + + #[cw_serde] +@@ -167,6 +188,8 @@ pub struct ConfigResponse { + pub timeout_seconds: u64, + /// Maximum percentage of price movement (minimum amount you accept to receive during swap) + pub slippage_tolerance: Decimal, ++ /// Neutron Ibc config ++ pub neutron_ibc_config: Option, + } + + #[cw_serde] +@@ -175,29 +198,40 @@ pub enum QueryMsg { + /// Get config parameters + #[returns(ConfigResponse)] + Config {}, +- /// Get routes for swapping an input denom into an output denom. +- /// +- /// NOTE: The response type of this query is chain-specific. +- #[returns(RouteResponse)] +- Route { +- denom_in: String, +- denom_out: String, +- }, +- /// Enumerate all swap routes. +- /// +- /// NOTE: The response type of this query is chain-specific. +- #[returns(Vec>)] +- Routes { +- start_after: Option<(String, String)>, +- limit: Option, +- }, + } + +-#[cw_serde] +-pub struct RouteResponse { +- pub denom_in: String, +- pub denom_out: String, +- pub route: Route, +-} ++// TODO: rover is private repo for now so can't use it as a dependency. Use rover types once repo is public. ++pub mod credit_manager { ++ use cosmwasm_schema::cw_serde; ++ use cosmwasm_std::{Coin, Uint128}; ++ ++ #[cw_serde] ++ pub enum ExecuteMsg { ++ UpdateCreditAccount { ++ account_id: String, ++ actions: Vec, ++ }, ++ } + +-pub type RoutesResponse = Vec>; ++ #[cw_serde] ++ pub enum Action { ++ Withdraw(Coin), ++ WithdrawLiquidity { ++ lp_token: ActionCoin, ++ minimum_receive: Vec, ++ }, ++ Unknown {}, // Used to simulate allowance only for: Withdraw and WithdrawLiquidity ++ } ++ ++ #[cw_serde] ++ pub struct ActionCoin { ++ pub denom: String, ++ pub amount: ActionAmount, ++ } ++ ++ #[cw_serde] ++ pub enum ActionAmount { ++ Exact(Uint128), ++ AccountBalance, ++ } ++} +diff --git a/packages/types/src/swapper/adapter.rs b/packages/types/src/swapper/adapter.rs +new file mode 100644 +index 00000000..e5390172 +--- /dev/null ++++ b/packages/types/src/swapper/adapter.rs +@@ -0,0 +1,110 @@ ++use cosmwasm_schema::cw_serde; ++use cosmwasm_std::{to_binary, Addr, Api, Coin, CosmosMsg, Decimal, Empty, StdResult, WasmMsg}; ++ ++use crate::swapper::ExecuteMsg; ++ ++#[cw_serde] ++pub struct SwapperBase(T); ++ ++impl SwapperBase { ++ pub fn new(address: T) -> SwapperBase { ++ SwapperBase(address) ++ } ++ ++ pub fn address(&self) -> &T { ++ &self.0 ++ } ++} ++ ++pub type SwapperUnchecked = SwapperBase; ++pub type Swapper = SwapperBase; ++ ++impl From for SwapperUnchecked { ++ fn from(s: Swapper) -> Self { ++ Self(s.address().to_string()) ++ } ++} ++ ++impl SwapperUnchecked { ++ pub fn check(&self, api: &dyn Api) -> StdResult { ++ Ok(SwapperBase::new(api.addr_validate(self.address())?)) ++ } ++} ++ ++impl Swapper { ++ /// Generate message for performing a swapper ++ pub fn swap_exact_in_msg( ++ &self, ++ coin_in: &Coin, ++ denom_out: &str, ++ slippage: Decimal, ++ ) -> StdResult { ++ Ok(CosmosMsg::Wasm(WasmMsg::Execute { ++ contract_addr: self.address().to_string(), ++ msg: to_binary(&ExecuteMsg::::SwapExactIn { ++ coin_in: coin_in.clone(), ++ denom_out: denom_out.to_string(), ++ slippage, ++ })?, ++ funds: vec![coin_in.clone()], ++ })) ++ } ++} ++ ++#[cfg(test)] ++mod tests { ++ use cosmwasm_std::testing::MockApi; ++ ++ use super::*; ++ ++ #[test] ++ fn test_swapper_unchecked_from_swapper() { ++ let swapper = Swapper::new(Addr::unchecked("swapper")); ++ let swapper_unchecked = SwapperUnchecked::from(swapper.clone()); ++ assert_eq!(swapper_unchecked.address(), "swapper"); ++ assert_eq!(swapper_unchecked.check(&MockApi::default()).unwrap(), swapper); ++ } ++ ++ #[test] ++ fn test_swapper_unchecked_check() { ++ let swapper = SwapperUnchecked::new("swapper".to_string()); ++ assert_eq!( ++ swapper.check(&MockApi::default()).unwrap(), ++ Swapper::new(Addr::unchecked("swapper".to_string())) ++ ); ++ } ++ ++ #[test] ++ fn test_new_and_address() { ++ // Swapper ++ let swapper = Swapper::new(Addr::unchecked("swapper")); ++ assert_eq!(swapper.address(), &Addr::unchecked("swapper")); ++ ++ // SwapperUnchecked ++ let swapper_unchecked = SwapperUnchecked::new("swapper".to_string()); ++ assert_eq!(swapper_unchecked.address(), "swapper"); ++ } ++ ++ #[test] ++ fn test_swapper_swap_exact_in_msg() { ++ let swapper = Swapper::new(Addr::unchecked("swapper")); ++ let coin_in = Coin::new(100, "in"); ++ let denom_out = "out"; ++ let slippage = Decimal::percent(1); ++ ++ let msg = swapper.swap_exact_in_msg(&coin_in, denom_out, slippage).unwrap(); ++ assert_eq!( ++ msg, ++ CosmosMsg::Wasm(WasmMsg::Execute { ++ contract_addr: "swapper".to_string(), ++ msg: to_binary(&ExecuteMsg::::SwapExactIn { ++ coin_in: coin_in.clone(), ++ denom_out: denom_out.to_string(), ++ slippage, ++ }) ++ .unwrap(), ++ funds: vec![coin_in], ++ }) ++ ); ++ } ++} +diff --git a/packages/types/src/swapper/mod.rs b/packages/types/src/swapper/mod.rs +new file mode 100644 +index 00000000..6071d783 +--- /dev/null ++++ b/packages/types/src/swapper/mod.rs +@@ -0,0 +1,4 @@ ++pub mod adapter; ++pub mod msgs; ++ ++pub use self::{adapter::*, msgs::*}; +diff --git a/packages/types/src/swapper/msgs.rs b/packages/types/src/swapper/msgs.rs +new file mode 100644 +index 00000000..bd270548 +--- /dev/null ++++ b/packages/types/src/swapper/msgs.rs +@@ -0,0 +1,77 @@ ++use cosmwasm_schema::{cw_serde, QueryResponses}; ++use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; ++use mars_owner::OwnerUpdate; ++ ++#[cw_serde] ++pub struct InstantiateMsg { ++ /// The contract's owner, who can update config ++ pub owner: String, ++} ++ ++#[cw_serde] ++pub enum ExecuteMsg { ++ /// Manges owner role state ++ UpdateOwner(OwnerUpdate), ++ /// Configure the route for swapping an asset ++ /// ++ /// This is chain-specific, and can include parameters such as slippage tolerance and the routes ++ /// for multi-step swaps ++ SetRoute { ++ denom_in: String, ++ denom_out: String, ++ route: Route, ++ }, ++ /// Perform a swapper with an exact-in amount. Requires slippage allowance %. ++ SwapExactIn { ++ coin_in: Coin, ++ denom_out: String, ++ slippage: Decimal, ++ }, ++ /// Send swapper results back to swapper. Also refunds extra if sent more than needed. Internal use only. ++ TransferResult { ++ recipient: Addr, ++ denom_in: String, ++ denom_out: String, ++ }, ++} ++ ++#[cw_serde] ++#[derive(QueryResponses)] ++pub enum QueryMsg { ++ /// Query contract owner config ++ #[returns(mars_owner::OwnerResponse)] ++ Owner {}, ++ /// Get route for swapping an input denom into an output denom ++ #[returns(RouteResponse)] ++ Route { ++ denom_in: String, ++ denom_out: String, ++ }, ++ /// Enumerate all swapper routes ++ #[returns(RoutesResponse)] ++ Routes { ++ start_after: Option<(String, String)>, ++ limit: Option, ++ }, ++ /// Return current spot price swapping In for Out ++ /// Warning: Do not use this as an oracle price feed. Use Mars-Oracle for pricing. ++ #[returns(EstimateExactInSwapResponse)] ++ EstimateExactInSwap { ++ coin_in: Coin, ++ denom_out: String, ++ }, ++} ++ ++#[cw_serde] ++pub struct RouteResponse { ++ pub denom_in: String, ++ pub denom_out: String, ++ pub route: Route, ++} ++ ++pub type RoutesResponse = Vec>; ++ ++#[cw_serde] ++pub struct EstimateExactInSwapResponse { ++ pub amount: Uint128, ++} diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 323deee13..dcde9740e 100755 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -10,32 +10,28 @@ documentation = { workspace = true } keywords = { workspace = true } [lib] -crate-type = [ - "cdylib", - "rlib", -] doctest = false [features] # for more explicit tests, cargo test --features=backtraces -backtraces = [ - "cosmwasm-std/backtraces", -] +backtraces = ["cosmwasm-std/backtraces"] [dev-dependencies] anyhow = { workspace = true } cosmwasm-std = { workspace = true } -cw-multi-test = { workspace = true } cw-it = { workspace = true, features = ["osmosis-test-tube"] } -mars-oracle-osmosis = { workspace = true } +cw-multi-test = { workspace = true } +mars-incentives = { workspace = true } mars-oracle-base = { workspace = true } +mars-oracle-osmosis = { workspace = true } mars-osmosis = { workspace = true } +mars-params = { workspace = true } mars-red-bank = { workspace = true } mars-red-bank-types = { workspace = true } mars-rewards-collector-osmosis = { workspace = true } +mars-swapper-osmosis = { workspace = true } mars-testing = { workspace = true } mars-utils = { workspace = true } osmosis-std = { workspace = true } osmosis-test-tube = { workspace = true } serde = { workspace = true } -mars-swapper-osmosis = { workspace = true } diff --git a/integration-tests/tests/helpers.rs b/integration-tests/tests/helpers.rs index aeb6c59c4..a8a3c51bb 100644 --- a/integration-tests/tests/helpers.rs +++ b/integration-tests/tests/helpers.rs @@ -1,9 +1,9 @@ #![allow(dead_code)] use anyhow::Result as AnyResult; -use cosmwasm_std::{Coin, Decimal}; +use cosmwasm_std::{Coin, Decimal, Fraction, Uint128}; use cw_multi_test::AppResponse; -use mars_red_bank::error::ContractError; +use mars_params::types::asset::{AssetParams, CmSettings, LiquidationBonus, RedBankSettings}; use mars_red_bank_types::red_bank::{ InitOrUpdateAssetParams, InterestRateModel, UserHealthStatus, UserPositionResponse, }; @@ -13,44 +13,72 @@ use osmosis_std::types::osmosis::{ }; use osmosis_test_tube::{Account, ExecuteResponse, OsmosisTestApp, Runner, SigningAccount}; -pub fn default_asset_params() -> InitOrUpdateAssetParams { - InitOrUpdateAssetParams { +pub fn default_asset_params(denom: &str) -> (InitOrUpdateAssetParams, AssetParams) { + let market_params = InitOrUpdateAssetParams { reserve_factor: Some(Decimal::percent(20)), - max_loan_to_value: Some(Decimal::percent(60)), - liquidation_threshold: Some(Decimal::percent(80)), - liquidation_bonus: Some(Decimal::percent(10)), interest_rate_model: Some(InterestRateModel { optimal_utilization_rate: Decimal::percent(10), base: Decimal::percent(30), slope_1: Decimal::percent(25), slope_2: Decimal::percent(30), }), - deposit_enabled: Some(true), - borrow_enabled: Some(true), - deposit_cap: None, - } + }; + let asset_params = AssetParams { + denom: denom.to_string(), + credit_manager: CmSettings { + whitelisted: false, + hls: None, + }, + red_bank: RedBankSettings { + deposit_enabled: true, + borrow_enabled: true, + }, + max_loan_to_value: Decimal::percent(60), + liquidation_threshold: Decimal::percent(80), + liquidation_bonus: LiquidationBonus { + starting_lb: Decimal::percent(0u64), + slope: Decimal::one(), + min_lb: Decimal::percent(0u64), + max_lb: Decimal::percent(5u64), + }, + protocol_liquidation_fee: Decimal::percent(2u64), + deposit_cap: Uint128::MAX, + }; + (market_params, asset_params) } pub fn default_asset_params_with( + denom: &str, max_loan_to_value: Decimal, liquidation_threshold: Decimal, - liquidation_bonus: Decimal, -) -> InitOrUpdateAssetParams { - InitOrUpdateAssetParams { + liquidation_bonus: LiquidationBonus, +) -> (InitOrUpdateAssetParams, AssetParams) { + let market_params = InitOrUpdateAssetParams { reserve_factor: Some(Decimal::percent(20)), - max_loan_to_value: Some(max_loan_to_value), - liquidation_threshold: Some(liquidation_threshold), - liquidation_bonus: Some(liquidation_bonus), interest_rate_model: Some(InterestRateModel { optimal_utilization_rate: Decimal::percent(10), base: Decimal::percent(30), slope_1: Decimal::percent(25), slope_2: Decimal::percent(30), }), - deposit_enabled: Some(true), - borrow_enabled: Some(true), - deposit_cap: None, - } + }; + let asset_params = AssetParams { + denom: denom.to_string(), + credit_manager: CmSettings { + whitelisted: false, + hls: None, + }, + red_bank: RedBankSettings { + deposit_enabled: true, + borrow_enabled: true, + }, + max_loan_to_value, + liquidation_threshold, + liquidation_bonus, + protocol_liquidation_fee: Decimal::percent(2u64), + deposit_cap: Uint128::MAX, + }; + (market_params, asset_params) } pub fn is_user_liquidatable(position: &UserPositionResponse) -> bool { @@ -63,6 +91,35 @@ pub fn is_user_liquidatable(position: &UserPositionResponse) -> bool { } } +pub fn liq_threshold_hf(position: &UserPositionResponse) -> Decimal { + match position.health_status { + UserHealthStatus::Borrowing { + liq_threshold_hf, + .. + } if liq_threshold_hf < Decimal::one() => liq_threshold_hf, + _ => panic!("User is not liquidatable"), + } +} + +pub fn calculate_max_debt_repayable( + thf: Decimal, + tlf: Decimal, + collateral_liq_th: Decimal, + debt_price: Decimal, + position: &UserPositionResponse, +) -> Uint128 { + let max_debt_repayable_numerator = (thf * position.total_collateralized_debt) + - position.weighted_liquidation_threshold_collateral; + let max_debt_repayable_denominator = thf - (collateral_liq_th * (Decimal::one() + tlf)); + + let max_debt_repayable_value = max_debt_repayable_numerator.multiply_ratio( + max_debt_repayable_denominator.denominator(), + max_debt_repayable_denominator.numerator(), + ); + + max_debt_repayable_value.div_floor(debt_price) +} + pub mod osmosis { use std::fmt::Display; @@ -160,11 +217,21 @@ pub fn swap( .unwrap() } -pub fn assert_err(res: AnyResult, err: ContractError) { +pub fn assert_red_bank_err(res: AnyResult, err: mars_red_bank::error::ContractError) { + match res { + Ok(_) => panic!("Result was not an error"), + Err(generic_err) => { + let contract_err: mars_red_bank::error::ContractError = generic_err.downcast().unwrap(); + assert_eq!(contract_err, err); + } + } +} + +pub fn assert_incentives_err(res: AnyResult, err: mars_incentives::ContractError) { match res { Ok(_) => panic!("Result was not an error"), Err(generic_err) => { - let contract_err: ContractError = generic_err.downcast().unwrap(); + let contract_err: mars_incentives::ContractError = generic_err.downcast().unwrap(); assert_eq!(contract_err, err); } } diff --git a/integration-tests/tests/test_incentives.rs b/integration-tests/tests/test_incentives.rs index e15bb075e..ee0a30985 100644 --- a/integration-tests/tests/test_incentives.rs +++ b/integration-tests/tests/test_incentives.rs @@ -18,7 +18,11 @@ fn rewards_claim() { let mut mock_env = MockEnvBuilder::new(None, owner).build(); let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); + let params = mock_env.params.clone(); + + let (market_params, asset_params) = default_asset_params("uusdc"); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, asset_params); let incentives = mock_env.incentives.clone(); incentives.whitelist_incentive_denoms(&mut mock_env, &[("umars", 3)]); @@ -42,12 +46,12 @@ fn rewards_claim() { let user_collateral = red_bank.query_user_collateral(&mut mock_env, &user, "uusdc"); assert_eq!(user_collateral.amount.u128(), funded_amt); - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert!(rewards_balance.is_empty()); mock_env.increment_by_time(86400); // 24 hours - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::new(864000)); incentives.claim_rewards(&mut mock_env, &user).unwrap(); @@ -57,8 +61,185 @@ fn rewards_claim() { let mars_balance = mock_env.query_balance(&incentives.contract_addr, "umars").unwrap(); assert_eq!(mars_balance.amount, Uint128::from(ONE_WEEK_IN_SEC * 10 - 864000)); - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); + assert!(rewards_balance.is_empty()); +} + +// Credit accounts can deposit / withdraw from Red Bank and accure rewards in incentives contract. +#[test] +fn rewards_claim_for_credit_account() { + let owner = Addr::unchecked("owner"); + let mut mock_env = MockEnvBuilder::new(None, owner).build(); + + let red_bank = mock_env.red_bank.clone(); + let params = mock_env.params.clone(); + + let (market_params, asset_params) = default_asset_params("uusdc"); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, asset_params); + + let incentives = mock_env.incentives.clone(); + incentives.whitelist_incentive_denoms(&mut mock_env, &[("umars", 3)]); + incentives.init_asset_incentive_from_current_block( + &mut mock_env, + "uusdc", + "umars", + 10, + ONE_WEEK_IN_SEC, + ); + + // setup credit accounts + let credit_manager = mock_env.credit_manager.clone(); + let funded_amt = 10_000_000_000u128; + mock_env.fund_account(&credit_manager, &[coin(funded_amt, "uusdc")]); + + let acc_id_1 = "101".to_string(); + let acc_id_2 = "205".to_string(); + let funded_amt_acc_id_1 = 600_000_000u128; // 60% of total deposited amount + let funded_amt_acc_id_2 = 400_000_000u128; // 40% of total deposited amount + + // credit accounts deposit to Red Bank + red_bank + .deposit_with_acc_id( + &mut mock_env, + &credit_manager, + coin(funded_amt_acc_id_1, "uusdc"), + Some(acc_id_1.clone()), + ) + .unwrap(); + red_bank + .deposit_with_acc_id( + &mut mock_env, + &credit_manager, + coin(funded_amt_acc_id_2, "uusdc"), + Some(acc_id_2.clone()), + ) + .unwrap(); + let user_collateral = red_bank.query_user_collateral(&mut mock_env, &credit_manager, "uusdc"); + assert_eq!(user_collateral.amount.u128(), 0); + let acc_id_1_collateral = red_bank.query_user_collateral_with_acc_id( + &mut mock_env, + &credit_manager, + Some(acc_id_1.clone()), + "uusdc", + ); + assert_eq!(acc_id_1_collateral.amount.u128(), funded_amt_acc_id_1); + let acc_id_2_collateral = red_bank.query_user_collateral_with_acc_id( + &mut mock_env, + &credit_manager, + Some(acc_id_2.clone()), + "uusdc", + ); + assert_eq!(acc_id_2_collateral.amount.u128(), funded_amt_acc_id_2); + + // no rewards in the deposit block + let rewards_balance_acc_id_1 = incentives + .query_unclaimed_rewards_with_acc_id(&mut mock_env, &credit_manager, Some(acc_id_1.clone())) + .unwrap(); + assert!(rewards_balance_acc_id_1.is_empty()); + let rewards_balance_acc_id_2 = incentives + .query_unclaimed_rewards_with_acc_id(&mut mock_env, &credit_manager, Some(acc_id_2.clone())) + .unwrap(); + assert!(rewards_balance_acc_id_2.is_empty()); + + // move 24 hours + mock_env.increment_by_time(86400); + + // credit accounts should accure rewards proportionally + let rewards_balance_acc_id_1 = incentives + .query_unclaimed_rewards_with_acc_id(&mut mock_env, &credit_manager, Some(acc_id_1.clone())) + .unwrap(); + assert_eq!(rewards_balance_acc_id_1[0].amount, Uint128::new(518400)); // 60% * 864000 + let rewards_balance_acc_id_2 = incentives + .query_unclaimed_rewards_with_acc_id(&mut mock_env, &credit_manager, Some(acc_id_2.clone())) + .unwrap(); + assert_eq!(rewards_balance_acc_id_2[0].amount, Uint128::new(345600)); // 40% * 864000 + + // query credit manager rewards without account id should return zero + let rewards_balance = + incentives.query_unclaimed_rewards(&mut mock_env, &credit_manager).unwrap(); assert!(rewards_balance.is_empty()); + // claiming credit manager rewards without account id should have no effect + incentives.claim_rewards(&mut mock_env, &credit_manager).unwrap(); + + // claim rewards for credit accounts + incentives + .claim_rewards_with_acc_id(&mut mock_env, &credit_manager, Some(acc_id_1.clone())) + .unwrap(); + let rewards_balance_acc_id_1 = incentives + .query_unclaimed_rewards_with_acc_id(&mut mock_env, &credit_manager, Some(acc_id_1.clone())) + .unwrap(); + assert!(rewards_balance_acc_id_1.is_empty()); + incentives + .claim_rewards_with_acc_id(&mut mock_env, &credit_manager, Some(acc_id_2.clone())) + .unwrap(); + let rewards_balance_acc_id_2 = incentives + .query_unclaimed_rewards_with_acc_id(&mut mock_env, &credit_manager, Some(acc_id_2.clone())) + .unwrap(); + assert!(rewards_balance_acc_id_2.is_empty()); + + // credit accounts withdraw from Red Bank + let withdraw_amt_acc_id_1 = 300_000_000u128; + let withdraw_amt_acc_id_2 = 100_000_000u128; + red_bank + .withdraw_with_acc_id( + &mut mock_env, + &credit_manager, + "uusdc", + Some(Uint128::from(withdraw_amt_acc_id_1)), + Some(acc_id_1.clone()), + None, + ) + .unwrap(); + red_bank + .withdraw_with_acc_id( + &mut mock_env, + &credit_manager, + "uusdc", + Some(Uint128::from(withdraw_amt_acc_id_2)), + Some(acc_id_2.clone()), + None, + ) + .unwrap(); + let user_collateral = red_bank.query_user_collateral(&mut mock_env, &credit_manager, "uusdc"); + assert_eq!(user_collateral.amount.u128(), 0); + let acc_id_1_collateral = red_bank.query_user_collateral_with_acc_id( + &mut mock_env, + &credit_manager, + Some(acc_id_1.clone()), + "uusdc", + ); + assert_eq!(acc_id_1_collateral.amount.u128(), funded_amt_acc_id_1 - withdraw_amt_acc_id_1); + let acc_id_2_collateral = red_bank.query_user_collateral_with_acc_id( + &mut mock_env, + &credit_manager, + Some(acc_id_2.clone()), + "uusdc", + ); + assert_eq!(acc_id_2_collateral.amount.u128(), funded_amt_acc_id_2 - withdraw_amt_acc_id_2); + + // move 24 hours + mock_env.increment_by_time(86400); + + // credit accounts should accure rewards proportionally + let rewards_balance_acc_id_1 = incentives + .query_unclaimed_rewards_with_acc_id(&mut mock_env, &credit_manager, Some(acc_id_1.clone())) + .unwrap(); + assert_eq!(rewards_balance_acc_id_1[0].amount, Uint128::new(432000)); // 50% * 864000 + let rewards_balance_acc_id_2 = incentives + .query_unclaimed_rewards_with_acc_id(&mut mock_env, &credit_manager, Some(acc_id_2.clone())) + .unwrap(); + assert_eq!(rewards_balance_acc_id_2[0].amount, Uint128::new(432000)); // 50% * 864000 + + // claim rewards for credit accounts + incentives.claim_rewards_with_acc_id(&mut mock_env, &credit_manager, Some(acc_id_1)).unwrap(); + incentives.claim_rewards_with_acc_id(&mut mock_env, &credit_manager, Some(acc_id_2)).unwrap(); + + // check balances for umars in credit manager and incentives contracts + let balance = mock_env.query_balance(&credit_manager, "umars").unwrap(); + assert_eq!(balance.amount, Uint128::new(864000 + 864000)); + let mars_balance = mock_env.query_balance(&incentives.contract_addr, "umars").unwrap(); + assert_eq!(mars_balance.amount, Uint128::from(ONE_WEEK_IN_SEC * 10 - 864000 - 864000)); } // User A deposited usdc in the redbank when incentives were 5 emissions per second @@ -71,9 +252,17 @@ fn emissions_rates() { let mut mock_env = MockEnvBuilder::new(None, owner).build(); let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uosmo", default_asset_params()); - red_bank.init_asset(&mut mock_env, "umars", default_asset_params()); + let params = mock_env.params.clone(); + + let (market_params, asset_params) = default_asset_params("uusdc"); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("uosmo"); + red_bank.init_asset(&mut mock_env, "uosmo", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("umars"); + red_bank.init_asset(&mut mock_env, "umars", market_params); + params.init_params(&mut mock_env, asset_params); let incentives = mock_env.incentives.clone(); incentives.whitelist_incentive_denoms(&mut mock_env, &[("umars", 3)]); @@ -100,12 +289,12 @@ fn emissions_rates() { let user_collateral = red_bank.query_user_collateral(&mut mock_env, &user, "uusdc"); assert_eq!(user_collateral.amount.u128(), funded_amt); - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert!(rewards_balance.is_empty()); mock_env.increment_by_time(86400); // 24 hours - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::new(432000)); // 86400*5 incentives.claim_rewards(&mut mock_env, &user).unwrap(); @@ -113,7 +302,7 @@ fn emissions_rates() { let balance = mock_env.query_balance(&user, "umars").unwrap(); assert_eq!(balance.amount, Uint128::new(432000)); - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert!(rewards_balance.is_empty()); incentives.init_asset_incentive_from_current_block( @@ -132,12 +321,12 @@ fn emissions_rates() { let user_collateral = red_bank.query_user_collateral(&mut mock_env, &user, "uosmo"); assert_eq!(user_collateral.amount.u128(), funded_amt); - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert!(rewards_balance.is_empty()); mock_env.increment_by_time(86400); // 24 hours - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::new(1296000)); // 432000 + (86400*10) incentives.claim_rewards(&mut mock_env, &user).unwrap(); @@ -145,7 +334,7 @@ fn emissions_rates() { let balance = mock_env.query_balance(&user, "umars").unwrap(); assert_eq!(balance.amount, Uint128::new(1728000)); // 1296000 + 432000 - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert!(rewards_balance.is_empty()); } @@ -157,9 +346,17 @@ fn no_incentives_accrued_after_withdraw() { let mut mock_env = MockEnvBuilder::new(None, owner).build(); let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uosmo", default_asset_params()); - red_bank.init_asset(&mut mock_env, "umars", default_asset_params()); + let params = mock_env.params.clone(); + + let (market_params, asset_params) = default_asset_params("uusdc"); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("uosmo"); + red_bank.init_asset(&mut mock_env, "uosmo", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("umars"); + red_bank.init_asset(&mut mock_env, "umars", market_params); + params.init_params(&mut mock_env, asset_params); let incentives = mock_env.incentives.clone(); incentives.whitelist_incentive_denoms(&mut mock_env, &[("umars", 3)]); @@ -186,12 +383,12 @@ fn no_incentives_accrued_after_withdraw() { let user_collateral = red_bank.query_user_collateral(&mut mock_env, &user, "uusdc"); assert_eq!(user_collateral.amount.u128(), funded_amt); - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert!(rewards_balance.is_empty()); mock_env.increment_by_time(86400); // 24 hours - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::new(432000)); // 86400 * 5 incentives.claim_rewards(&mut mock_env, &user).unwrap(); @@ -199,7 +396,7 @@ fn no_incentives_accrued_after_withdraw() { let balance = mock_env.query_balance(&user, "umars").unwrap(); assert_eq!(balance.amount, Uint128::new(432000)); - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert!(rewards_balance.is_empty()); red_bank.withdraw(&mut mock_env, &user, "uusdc", None).unwrap(); @@ -210,12 +407,12 @@ fn no_incentives_accrued_after_withdraw() { let user_collateral = red_bank.query_user_collateral(&mut mock_env, &user, "uosmo"); assert_eq!(user_collateral.amount, Uint128::zero()); - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert!(rewards_balance.is_empty()); mock_env.increment_by_time(86400); // 24 hours - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert!(rewards_balance.is_empty()); } @@ -226,10 +423,20 @@ fn multiple_assets() { let mut mock_env = MockEnvBuilder::new(None, owner).build(); let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uosmo", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uatom", default_asset_params()); - red_bank.init_asset(&mut mock_env, "umars", default_asset_params()); + let params = mock_env.params.clone(); + + let (market_params, asset_params) = default_asset_params("uusdc"); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("uosmo"); + red_bank.init_asset(&mut mock_env, "uosmo", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("uatom"); + red_bank.init_asset(&mut mock_env, "uatom", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("umars"); + red_bank.init_asset(&mut mock_env, "umars", market_params); + params.init_params(&mut mock_env, asset_params); // set incentives let incentives = mock_env.incentives.clone(); @@ -285,12 +492,12 @@ fn multiple_assets() { let user_collateral = red_bank.query_user_collateral(&mut mock_env, &user, "uosmo"); assert_eq!(user_collateral.amount.u128(), funded_amt); - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert!(rewards_balance.is_empty()); mock_env.increment_by_time(86400); // 24 hours - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::new(1555200)); } @@ -302,7 +509,11 @@ fn multiple_users() { let mut mock_env = MockEnvBuilder::new(None, owner).build(); let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); + let params = mock_env.params.clone(); + + let (market_params, asset_params) = default_asset_params("uusdc"); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, asset_params); // set incentives let incentives = mock_env.incentives.clone(); @@ -343,18 +554,18 @@ fn multiple_users() { let user_collateral = red_bank.query_user_collateral(&mut mock_env, &user_b, "uusdc"); assert_eq!(user_collateral.amount.u128(), funded_amt_two); - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user_a); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user_a).unwrap(); assert!(rewards_balance.is_empty()); - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user_b); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user_b).unwrap(); assert!(rewards_balance.is_empty()); mock_env.increment_by_time(86400); // 24 hours - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user_a); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user_a).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::new(144000)); // (86400*5) * (1/3) - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user_b); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user_b).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::new(288000)); // (86400*5)/2 * (2/3) // User A withdraws, user B holds @@ -363,10 +574,10 @@ fn multiple_users() { mock_env.increment_by_time(86400); // 24 hours - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user_a); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user_a).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::new(144000)); // stays the same - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user_b); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user_b).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::new(720000)); // 288000 + (86400*5) } @@ -385,9 +596,17 @@ fn rewards_distributed_among_users_and_rewards_collector() { // setup red-bank assets let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uosmo", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uatom", default_asset_params()); + let params = mock_env.params.clone(); + + let (market_params, asset_params) = default_asset_params("uusdc"); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("uosmo"); + red_bank.init_asset(&mut mock_env, "uosmo", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("uatom"); + red_bank.init_asset(&mut mock_env, "uatom", market_params); + params.init_params(&mut mock_env, asset_params); // fund user accounts let user_a = Addr::unchecked("user_a"); @@ -472,15 +691,18 @@ fn rewards_distributed_among_users_and_rewards_collector() { assert_eq!(uosmo_collateral_rc.amount, Uint128::zero()); // rewards-collector accrue rewards - let rewards_balance_rc = - incentives.query_unclaimed_rewards(&mut mock_env, &rewards_collector.contract_addr); + let rewards_balance_rc = incentives + .query_unclaimed_rewards(&mut mock_env, &rewards_collector.contract_addr) + .unwrap(); assert!(!rewards_balance_rc.is_empty()); println!("rewards_balance_rc: {:?}", rewards_balance_rc); // sum of unclaimed rewards should be equal to total umars available for finished incentive - let rewards_balance_user_a = incentives.query_unclaimed_rewards(&mut mock_env, &user_a); + let rewards_balance_user_a = + incentives.query_unclaimed_rewards(&mut mock_env, &user_a).unwrap(); println!("rewards_balance_user_a: {:?}", rewards_balance_user_a); - let rewards_balance_user_b = incentives.query_unclaimed_rewards(&mut mock_env, &user_b); + let rewards_balance_user_b = + incentives.query_unclaimed_rewards(&mut mock_env, &user_b).unwrap(); println!("rewards_balance_user_b: {:?}", rewards_balance_user_b); let total_claimed_rewards = rewards_balance_rc[0].amount + rewards_balance_user_a[0].amount diff --git a/integration-tests/tests/test_liquidations.rs b/integration-tests/tests/test_liquidations.rs deleted file mode 100644 index 32406a01c..000000000 --- a/integration-tests/tests/test_liquidations.rs +++ /dev/null @@ -1,249 +0,0 @@ -use cosmwasm_std::{coin, Addr, Decimal, Uint128}; -use mars_red_bank_types::red_bank::UserHealthStatus; -use mars_testing::integration::mock_env::MockEnvBuilder; -use mars_utils::math; - -use crate::helpers::{default_asset_params, default_asset_params_with, is_user_liquidatable}; - -mod helpers; - -#[test] -fn liquidate_collateralized_loan() { - let close_factor = Decimal::percent(40); - let atom_price = Decimal::from_ratio(12u128, 1u128); - let osmo_price = Decimal::from_ratio(15u128, 10u128); - let atom_max_ltv = Decimal::percent(60); - let osmo_max_ltv = Decimal::percent(80); - let atom_liq_threshold = Decimal::percent(75); - let osmo_liq_threshold = Decimal::percent(90); - let atom_liq_bonus = Decimal::percent(2); - let osmo_liq_bonus = Decimal::percent(5); - - let owner = Addr::unchecked("owner"); - let mut mock_env = MockEnvBuilder::new(None, owner).close_factor(close_factor).build(); - - // setup oracle and red-bank - let oracle = mock_env.oracle.clone(); - oracle.set_price_source_fixed(&mut mock_env, "uatom", atom_price); - oracle.set_price_source_fixed(&mut mock_env, "uosmo", osmo_price); - oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::one()); - let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset( - &mut mock_env, - "uatom", - default_asset_params_with(atom_max_ltv, atom_liq_threshold, atom_liq_bonus), - ); - red_bank.init_asset( - &mut mock_env, - "uosmo", - default_asset_params_with(osmo_max_ltv, osmo_liq_threshold, osmo_liq_bonus), - ); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); - - // fund provider account with usdc - let provider = Addr::unchecked("provider"); - let funded_usdc = 1_000_000_000_000u128; - mock_env.fund_account(&provider, &[coin(funded_usdc, "uusdc")]); - - // fund borrow account with atom and osmo - let borrower = Addr::unchecked("borrower"); - let funded_atom = 1_250_000_000u128; - let funded_osmo = 15_200_000_000u128; - mock_env.fund_account(&borrower, &[coin(funded_atom, "uatom")]); - mock_env.fund_account(&borrower, &[coin(funded_osmo, "uosmo")]); - - // fund liquidator account with usdc - let liquidator = Addr::unchecked("liquidator"); - mock_env.fund_account(&liquidator, &[coin(1_000_000_000_000u128, "uusdc")]); - - // deposits collaterals - red_bank.deposit(&mut mock_env, &provider, coin(funded_usdc, "uusdc")).unwrap(); - red_bank.deposit(&mut mock_env, &borrower, coin(funded_atom, "uatom")).unwrap(); - red_bank.deposit(&mut mock_env, &borrower, coin(funded_osmo, "uosmo")).unwrap(); - - // check HF for borrower - let borrower_position = red_bank.query_user_position(&mut mock_env, &borrower); - assert_eq!(borrower_position.health_status, UserHealthStatus::NotBorrowing); - - // try to borrow more than max LTV, should fail - let max_borrow = atom_max_ltv * (atom_price * Uint128::from(funded_atom)) - + osmo_max_ltv * (osmo_price * Uint128::from(funded_osmo)); - red_bank.borrow(&mut mock_env, &borrower, "uusdc", max_borrow.u128() + 1).unwrap_err(); - - // borrow max allowed amount - red_bank.borrow(&mut mock_env, &borrower, "uusdc", max_borrow.u128()).unwrap(); - let borrower_position = red_bank.query_user_position(&mut mock_env, &borrower); - assert!(!is_user_liquidatable(&borrower_position)); - - // decrease atom price - let atom_price = Decimal::from_ratio(6u128, 1u128); - oracle.set_price_source_fixed(&mut mock_env, "uatom", atom_price); - - // check HF after atom price decrease, should be < 1 - let borrower_position = red_bank.query_user_position(&mut mock_env, &borrower); - assert!(is_user_liquidatable(&borrower_position)); - - // values before liquidation - let redbank_osmo_balance_before = - mock_env.query_balance(&red_bank.contract_addr, "uosmo").unwrap(); - let redbank_usdc_balance_before = - mock_env.query_balance(&red_bank.contract_addr, "uusdc").unwrap(); - let liquidator_osmo_balance_before = mock_env.query_balance(&liquidator, "uosmo").unwrap(); - let liquidator_usdc_balance_before = mock_env.query_balance(&liquidator, "uusdc").unwrap(); - let market_osmo_before = red_bank.query_market(&mut mock_env, "uosmo"); - let market_usdc_before = red_bank.query_market(&mut mock_env, "uusdc"); - let borrower_osmo_collateral_before = - red_bank.query_user_collateral(&mut mock_env, &borrower, "uosmo"); - let borrower_usdc_debt_before = red_bank.query_user_debt(&mut mock_env, &borrower, "uusdc"); - let liquidator_osmo_collateral_before = - red_bank.query_user_collateral(&mut mock_env, &liquidator, "uosmo"); - let borrower_position_before = red_bank.query_user_position(&mut mock_env, &borrower); - - // liquidate borrower (more than close factor in order to get refund) - let max_amount_to_repay = - Uint128::one() * (close_factor * borrower_position_before.total_collateralized_debt); - let osmo_amount_to_liquidate = math::divide_uint128_by_decimal( - max_amount_to_repay * (Decimal::one() + osmo_liq_bonus), - osmo_price, - ) - .unwrap(); - let refund_amount = 15_000_000u128; - red_bank - .liquidate( - &mut mock_env, - &liquidator, - &borrower, - "uosmo", - coin(max_amount_to_repay.u128() + refund_amount, "uusdc"), - ) - .unwrap(); - - // redbank usdc balance is increased by repayed amount - let redbank_usdc_balance = mock_env.query_balance(&red_bank.contract_addr, "uusdc").unwrap(); - assert_eq!( - redbank_usdc_balance.amount, - redbank_usdc_balance_before.amount + max_amount_to_repay - ); - // redbank osmo balance is the same - we need to withdraw funds (collateral) manually - let redbank_osmo_balance = mock_env.query_balance(&red_bank.contract_addr, "uosmo").unwrap(); - assert_eq!(redbank_osmo_balance.amount, redbank_osmo_balance_before.amount); - - // liquidator usdc balance should be decreased by repayed amount - let liquidator_usdc_balance = mock_env.query_balance(&liquidator, "uusdc").unwrap(); - assert_eq!( - liquidator_usdc_balance.amount, - liquidator_usdc_balance_before.amount - max_amount_to_repay - ); - // liquidator osmo balance is the same - we need to withdraw funds (collateral) manually - let liquidator_osmo_balance = mock_env.query_balance(&liquidator, "uosmo").unwrap(); - assert_eq!(liquidator_osmo_balance.amount, liquidator_osmo_balance_before.amount); - - // usdc debt market is decreased by scaled repayed amount - let market_usdc = red_bank.query_market(&mut mock_env, "uusdc"); - let scaled_max_amount_to_repay = - red_bank.query_scaled_debt_amount(&mut mock_env, coin(max_amount_to_repay.u128(), "uusdc")); - assert_eq!( - market_usdc.debt_total_scaled, - market_usdc_before.debt_total_scaled - scaled_max_amount_to_repay - ); - // osmo collateral market is the same - we need to withdraw funds (collateral) manually - let market_osmo = red_bank.query_market(&mut mock_env, "uosmo"); - assert_eq!(market_osmo.collateral_total_scaled, market_osmo_before.collateral_total_scaled); - - // borrower usdc debt is decreased by repayed amount - let borrower_usdc_debt = red_bank.query_user_debt(&mut mock_env, &borrower, "uusdc"); - assert_eq!(borrower_usdc_debt.amount, borrower_usdc_debt_before.amount - max_amount_to_repay); - // borrower osmo collateral is decreased by liquidated amount - let borrower_osmo_collateral = - red_bank.query_user_collateral(&mut mock_env, &borrower, "uosmo"); - assert_eq!( - borrower_osmo_collateral.amount, - borrower_osmo_collateral_before.amount - osmo_amount_to_liquidate - ); - // liquidator osmo collateral is increased by liquidated amount - let liquidator_osmo_collateral = - red_bank.query_user_collateral(&mut mock_env, &liquidator, "uosmo"); - assert_eq!( - liquidator_osmo_collateral.amount, - liquidator_osmo_collateral_before.amount + osmo_amount_to_liquidate - ); - - // withdraw collateral for liquidator - red_bank.withdraw(&mut mock_env, &liquidator, "uosmo", None).unwrap(); - // redbank osmo balance is decreased by liquidated amount - let redbank_osmo_balance = mock_env.query_balance(&red_bank.contract_addr, "uosmo").unwrap(); - assert_eq!( - redbank_osmo_balance.amount, - redbank_osmo_balance_before.amount - osmo_amount_to_liquidate - ); - // liquidator osmo balance is increased by liquidated amount - let liquidator_osmo_balance = mock_env.query_balance(&liquidator, "uosmo").unwrap(); - assert_eq!( - liquidator_osmo_balance.amount, - liquidator_osmo_balance_before.amount + osmo_amount_to_liquidate - ); - // liquidator osmo collateral after withdraw is the same as before liquidation - let liquidator_osmo_collateral = - red_bank.query_user_collateral(&mut mock_env, &liquidator, "uosmo"); - assert_eq!(liquidator_osmo_collateral.amount, liquidator_osmo_collateral_before.amount); - // osmo collateral market is decreased by liquidated amount - let market_osmo = red_bank.query_market(&mut mock_env, "uosmo"); - let scaled_amount_to_liquidate = red_bank.query_scaled_liquidity_amount( - &mut mock_env, - coin(osmo_amount_to_liquidate.u128(), "uosmo"), - ); - assert_eq!( - market_osmo.collateral_total_scaled, - market_osmo_before.collateral_total_scaled - scaled_amount_to_liquidate - ); -} - -#[test] -fn liquidate_uncollateralized_loan() { - let owner = Addr::unchecked("owner"); - let mut mock_env = MockEnvBuilder::new(None, owner.clone()).build(); - - // setup oracle and red-bank - let oracle = mock_env.oracle.clone(); - oracle.set_price_source_fixed(&mut mock_env, "uatom", Decimal::from_ratio(14u128, 1u128)); - oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::one()); - let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset(&mut mock_env, "uatom", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); - - // fund provider account with usdc - let provider = Addr::unchecked("provider"); - let funded_usdc = 1_000_000_000_000u128; - mock_env.fund_account(&provider, &[coin(1_000_000_000_000u128, "uusdc")]); - - // fund provider account with usdc - let liquidator = Addr::unchecked("liquidator"); - mock_env.fund_account(&liquidator, &[coin(1_000_000_000_000u128, "uusdc")]); - - // deposits usdc to redbank - red_bank.deposit(&mut mock_env, &provider, coin(funded_usdc, "uusdc")).unwrap(); - - let borrower = Addr::unchecked("borrower"); - - // set uncollateralized loan limit for borrower - red_bank - .update_uncollateralized_loan_limit( - &mut mock_env, - &owner, - &borrower, - "uusdc", - Uint128::from(10_000_000_000u128), - ) - .unwrap(); - - // borrower borrows usdc - let borrow_amount = 98_000_000u128; - red_bank.borrow(&mut mock_env, &borrower, "uusdc", borrow_amount).unwrap(); - let balance = mock_env.query_balance(&borrower, "uusdc").unwrap(); - assert_eq!(balance.amount.u128(), borrow_amount); - - // try to liquidate, should fail because there are no collateralized loans - red_bank - .liquidate(&mut mock_env, &liquidator, &borrower, "uatom", coin(borrow_amount, "uusdc")) - .unwrap_err(); -} diff --git a/integration-tests/tests/test_oracles.rs b/integration-tests/tests/test_oracles.rs index cfe1e2f84..bd74cb96f 100644 --- a/integration-tests/tests/test_oracles.rs +++ b/integration-tests/tests/test_oracles.rs @@ -6,6 +6,7 @@ use mars_oracle_osmosis::{ msg::PriceSourceResponse, Downtime, DowntimeDetector, OsmosisPriceSourceChecked, OsmosisPriceSourceUnchecked, }; +use mars_params::msg::AssetParamsUpdate; use mars_red_bank_types::{ address_provider::{ ExecuteMsg::SetAddress, InstantiateMsg as InstantiateAddr, MarsAddressType, @@ -36,6 +37,7 @@ const OSMOSIS_RED_BANK_CONTRACT_NAME: &str = "mars-red-bank"; const OSMOSIS_ADDR_PROVIDER_CONTRACT_NAME: &str = "mars-address-provider"; const OSMOSIS_REWARDS_CONTRACT_NAME: &str = "mars-rewards-collector-osmosis"; const OSMOSIS_INCENTIVES_CONTRACT_NAME: &str = "mars-incentives"; +const OSMOSIS_PARAMS_CONTRACT_NAME: &str = "mars-params"; #[test] fn querying_xyk_lp_price_if_no_price_for_tokens() { @@ -86,6 +88,7 @@ fn querying_xyk_lp_price_if_no_price_for_tokens() { &contract_addr, &QueryMsg::Price { denom: "umars_uatom_lp".to_string(), + kind: None, }, ) .unwrap_err(); @@ -197,6 +200,7 @@ fn querying_xyk_lp_price_success() { &contract_addr, &QueryMsg::Price { denom: "umars_uatom_lp".to_string(), + kind: None, }, ) .unwrap(); @@ -261,6 +265,7 @@ fn query_spot_price() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -396,6 +401,7 @@ fn update_spot_with_different_pool() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -422,6 +428,7 @@ fn update_spot_with_different_pool() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -471,6 +478,7 @@ fn query_spot_price_after_lp_change() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -483,6 +491,7 @@ fn query_spot_price_after_lp_change() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -555,6 +564,7 @@ fn query_geometric_twap_price_with_downtime_detector() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ); assert_err(res.unwrap_err(), "chain is recovering from downtime"); @@ -568,6 +578,7 @@ fn query_geometric_twap_price_with_downtime_detector() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -640,6 +651,7 @@ fn query_arithmetic_twap_price() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -654,6 +666,7 @@ fn query_arithmetic_twap_price() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -727,6 +740,7 @@ fn query_geometric_twap_price() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -741,6 +755,7 @@ fn query_geometric_twap_price() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -807,6 +822,7 @@ fn compare_spot_and_twap_price() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -847,6 +863,7 @@ fn compare_spot_and_twap_price() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -887,6 +904,7 @@ fn compare_spot_and_twap_price() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -934,6 +952,7 @@ fn redbank_should_fail_if_no_price() { wasm.execute( &red_bank_addr, &Deposit { + account_id: None, on_behalf_of: None, }, &[coin(1_000_000, "uatom")], @@ -996,6 +1015,7 @@ fn redbank_quering_oracle_successfully() { wasm.execute( &red_bank_addr, &Deposit { + account_id: None, on_behalf_of: None, }, &[coin(1_000_000, "uatom")], @@ -1047,7 +1067,6 @@ fn setup_redbank(wasm: &Wasm, signer: &SigningAccount) -> (Strin owner: signer.address(), config: CreateOrUpdateConfig { address_provider: Some(addr_provider_addr.clone()), - close_factor: Some(Decimal::percent(10)), }, }, ); @@ -1081,6 +1100,17 @@ fn setup_redbank(wasm: &Wasm, signer: &SigningAccount) -> (Strin }, ); + let params_addr = instantiate_contract( + wasm, + signer, + OSMOSIS_PARAMS_CONTRACT_NAME, + &mars_params::msg::InstantiateMsg { + owner: (signer.address()), + address_provider: addr_provider_addr.clone(), + target_health_factor: Decimal::from_str("1.05").unwrap(), + }, + ); + wasm.execute( &addr_provider_addr, &SetAddress { @@ -1125,26 +1155,73 @@ fn setup_redbank(wasm: &Wasm, signer: &SigningAccount) -> (Strin ) .unwrap(); + wasm.execute( + &addr_provider_addr, + &SetAddress { + address_type: MarsAddressType::Params, + address: params_addr.clone(), + }, + &[], + signer, + ) + .unwrap(); + + // We can simulate credit manager contract balance with own params address (used by params contract for deposit caps logic) + wasm.execute( + &addr_provider_addr, + &SetAddress { + address_type: MarsAddressType::CreditManager, + address: params_addr.clone(), + }, + &[], + signer, + ) + .unwrap(); + + let (market_params, asset_params) = default_asset_params("uosmo"); + wasm.execute( &red_bank_addr, &ExecuteRedBank::InitAsset { denom: "uosmo".to_string(), - params: default_asset_params(), + params: market_params, }, &[], signer, ) .unwrap(); + wasm.execute( + ¶ms_addr, + &mars_params::msg::ExecuteMsg::UpdateAssetParams(AssetParamsUpdate::AddOrUpdate { + params: asset_params.into(), + }), + &[], + signer, + ) + .unwrap(); + + let (market_params, asset_params) = default_asset_params("uatom"); wasm.execute( &red_bank_addr, &ExecuteRedBank::InitAsset { denom: "uatom".to_string(), - params: default_asset_params(), + params: market_params, }, &[], signer, ) .unwrap(); + + wasm.execute( + ¶ms_addr, + &mars_params::msg::ExecuteMsg::UpdateAssetParams(AssetParamsUpdate::AddOrUpdate { + params: asset_params.into(), + }), + &[], + signer, + ) + .unwrap(); + (oracle_addr, red_bank_addr) } diff --git a/integration-tests/tests/test_rover_flow.rs b/integration-tests/tests/test_rover_flow.rs index 8aeb72c32..0abc8c868 100644 --- a/integration-tests/tests/test_rover_flow.rs +++ b/integration-tests/tests/test_rover_flow.rs @@ -3,7 +3,7 @@ use mars_red_bank::error::ContractError; use mars_red_bank_types::red_bank::UserHealthStatus; use mars_testing::integration::mock_env::MockEnvBuilder; -use crate::helpers::{assert_err, default_asset_params}; +use crate::helpers::{assert_red_bank_err, default_asset_params}; mod helpers; @@ -18,9 +18,16 @@ fn rover_flow() { oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(5u128, 10u128)); oracle.set_price_source_fixed(&mut mock_env, "uatom", Decimal::from_ratio(12u128, 1u128)); let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset(&mut mock_env, "uosmo", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uatom", default_asset_params()); + let params = mock_env.params.clone(); + let (market_params, asset_params) = default_asset_params("uusdc"); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("uosmo"); + red_bank.init_asset(&mut mock_env, "uosmo", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("uatom"); + red_bank.init_asset(&mut mock_env, "uatom", market_params); + params.init_params(&mut mock_env, asset_params); let rover = Addr::unchecked("rover"); @@ -43,7 +50,7 @@ fn rover_flow() { // rover can't borrow above the credit line let res_err = red_bank.borrow(&mut mock_env, &rover, "uusdc", rover_uusdc_limit + 1u128); - assert_err(res_err, ContractError::BorrowAmountExceedsUncollateralizedLoanLimit {}); + assert_red_bank_err(res_err, ContractError::BorrowAmountExceedsUncollateralizedLoanLimit {}); // rover borrows the entire line of credit let balance = mock_env.query_balance(&rover, "uusdc").unwrap(); @@ -69,7 +76,7 @@ fn rover_flow() { // can't borrow above the credit line let res_err = red_bank.borrow(&mut mock_env, &rover, "uusdc", 1u128); - assert_err(res_err, ContractError::BorrowAmountExceedsUncollateralizedLoanLimit {}); + assert_red_bank_err(res_err, ContractError::BorrowAmountExceedsUncollateralizedLoanLimit {}); // rover should be healthy (NotBorrowing because uncollateralized debt is not included in HF calculation) let position = red_bank.query_user_position(&mut mock_env, &rover); @@ -83,7 +90,7 @@ fn rover_flow() { "uusdc", Uint128::zero(), ); - assert_err(res_err, ContractError::UserHasUncollateralizedDebt {}); + assert_red_bank_err(res_err, ContractError::UserHasUncollateralizedDebt {}); let debt = red_bank.query_user_debt(&mut mock_env, &rover, "uusdc"); assert!(debt.uncollateralized); assert_eq!(debt.amount.u128(), rover_uusdc_limit); @@ -122,5 +129,5 @@ fn rover_flow() { "uusdc", Uint128::from(rover_uusdc_limit), ); - assert_err(res_err, ContractError::UserHasCollateralizedDebt {}); + assert_red_bank_err(res_err, ContractError::UserHasCollateralizedDebt {}); } diff --git a/integration-tests/tests/test_user_flow.rs b/integration-tests/tests/test_user_flow.rs index 713fe5a9c..8f3813b76 100644 --- a/integration-tests/tests/test_user_flow.rs +++ b/integration-tests/tests/test_user_flow.rs @@ -1,10 +1,11 @@ use std::str::FromStr; use cosmwasm_std::{coin, Addr, Decimal, Uint128}; +use mars_params::types::asset::LiquidationBonus; use mars_red_bank::error::ContractError; use mars_testing::integration::mock_env::{MockEnv, MockEnvBuilder, RedBank}; -use crate::helpers::{assert_err, default_asset_params, default_asset_params_with}; +use crate::helpers::{assert_red_bank_err, default_asset_params, default_asset_params_with}; mod helpers; @@ -18,8 +19,13 @@ fn user_flow() { oracle.set_price_source_fixed(&mut mock_env, "uatom", Decimal::from_ratio(12u128, 1u128)); oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::one()); let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset(&mut mock_env, "uatom", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); + let params = mock_env.params.clone(); + let (market_params, asset_params) = default_asset_params("uatom"); + red_bank.init_asset(&mut mock_env, "uatom", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("uusdc"); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, asset_params); // fund user_1 account with atom let user_1 = Addr::unchecked("user_1"); @@ -108,8 +114,13 @@ fn borrow_exact_liquidity() { oracle.set_price_source_fixed(&mut mock_env, "uatom", Decimal::from_ratio(12u128, 1u128)); oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::one()); let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset(&mut mock_env, "uatom", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); + let params = mock_env.params.clone(); + let (market_params, asset_params) = default_asset_params("uatom"); + red_bank.init_asset(&mut mock_env, "uatom", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("uusdc"); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, asset_params); // fund provider account with usdc let provider = Addr::unchecked("provider"); @@ -229,8 +240,13 @@ fn prepare_debt_for_repayment() -> (MockEnv, RedBank, Addr) { oracle.set_price_source_fixed(&mut mock_env, "uatom", Decimal::from_ratio(12u128, 1u128)); oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::one()); let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset(&mut mock_env, "uatom", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); + let params = mock_env.params.clone(); + let (market_params, asset_params) = default_asset_params("uatom"); + red_bank.init_asset(&mut mock_env, "uatom", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("uusdc"); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, asset_params); // fund user_1 account with atom let user_1 = Addr::unchecked("user_1"); @@ -275,17 +291,20 @@ fn internally_tracked_balances_used_for_borrow() { let borrower = Addr::unchecked("borrower"); let borrower2 = Addr::unchecked("borrower2"); - let close_factor = Decimal::percent(40); let atom_price = Decimal::from_ratio(12u128, 1u128); let osmo_price = Decimal::from_ratio(15u128, 10u128); let atom_max_ltv = Decimal::percent(60); let osmo_max_ltv = Decimal::percent(80); let atom_liq_threshold = Decimal::percent(75); let osmo_liq_threshold = Decimal::percent(90); - let atom_liq_bonus = Decimal::percent(2); - let osmo_liq_bonus = Decimal::percent(5); + let liq_bonus = LiquidationBonus { + starting_lb: Decimal::percent(0u64), + slope: Decimal::one(), + min_lb: Decimal::percent(0u64), + max_lb: Decimal::percent(5u64), + }; - let mut mock_env = MockEnvBuilder::new(None, owner).close_factor(close_factor).build(); + let mut mock_env = MockEnvBuilder::new(None, owner).build(); // setup oracle prices let oracle = mock_env.oracle.clone(); @@ -294,16 +313,15 @@ fn internally_tracked_balances_used_for_borrow() { // setup Red Bank assets let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset( - &mut mock_env, - "uatom", - default_asset_params_with(atom_max_ltv, atom_liq_threshold, atom_liq_bonus), - ); - red_bank.init_asset( - &mut mock_env, - "uosmo", - default_asset_params_with(osmo_max_ltv, osmo_liq_threshold, osmo_liq_bonus), - ); + let params = mock_env.params.clone(); + let (market_params, asset_params) = + default_asset_params_with("uatom", atom_max_ltv, atom_liq_threshold, liq_bonus.clone()); + red_bank.init_asset(&mut mock_env, "uatom", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = + default_asset_params_with("uosmo", osmo_max_ltv, osmo_liq_threshold, liq_bonus); + red_bank.init_asset(&mut mock_env, "uosmo", market_params); + params.init_params(&mut mock_env, asset_params); // initial deposit amount let funded_atom = 1u128; // 1 uatom @@ -325,7 +343,7 @@ fn internally_tracked_balances_used_for_borrow() { mock_env.fund_account(&borrower2, &[coin(funded_osmo, "uosmo")]); red_bank.deposit(&mut mock_env, &borrower2, coin(funded_osmo, "uosmo")).unwrap(); let res = red_bank.borrow(&mut mock_env, &borrower2, "uatom", donated_atom); - assert_err( + assert_red_bank_err( res, ContractError::InvalidBorrowAmount { denom: "uatom".to_string(), @@ -356,8 +374,13 @@ fn interest_rates_accured_based_on_internally_tracked_balances() { oracle.set_price_source_fixed(&mut mock_env, "uatom", Decimal::from_ratio(12u128, 1u128)); oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::one()); let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset(&mut mock_env, "uatom", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); + let params = mock_env.params.clone(); + let (market_params, asset_params) = default_asset_params("uatom"); + red_bank.init_asset(&mut mock_env, "uatom", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("uusdc"); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, asset_params); // fund user_1 account with usdc let user_1 = Addr::unchecked("user_1"); diff --git a/packages/chains/osmosis/Cargo.toml b/packages/chains/osmosis/Cargo.toml index d2af81999..7f44f9934 100755 --- a/packages/chains/osmosis/Cargo.toml +++ b/packages/chains/osmosis/Cargo.toml @@ -21,3 +21,4 @@ backtraces = ["cosmwasm-std/backtraces"] cosmwasm-std = { workspace = true } osmosis-std = { workspace = true } serde = { workspace = true } +prost = { workspace = true } diff --git a/packages/chains/osmosis/src/helpers.rs b/packages/chains/osmosis/src/helpers.rs index 1503569fd..875d38a13 100644 --- a/packages/chains/osmosis/src/helpers.rs +++ b/packages/chains/osmosis/src/helpers.rs @@ -3,36 +3,85 @@ use std::str::FromStr; use cosmwasm_std::{ coin, Decimal, Empty, QuerierWrapper, QueryRequest, StdError, StdResult, Uint128, }; -/// FIXME: migrate to Spot queries from PoolManager once whitelisted in https://github.com/osmosis-labs/osmosis/blob/main/wasmbinding/stargate_whitelist.go#L127 -#[allow(deprecated)] -use osmosis_std::types::osmosis::gamm::v1beta1::QueryPoolRequest as PoolRequest; use osmosis_std::{ shim::{Duration, Timestamp}, types::{ cosmos::base::v1beta1::Coin, osmosis::{ + concentratedliquidity::v1beta1::Pool as ConcentratedLiquidityPool, downtimedetector::v1beta1::DowntimedetectorQuerier, gamm::{ - v1beta1::{PoolAsset, PoolParams}, - v2::GammQuerier, + poolmodels::stableswap::v1beta1::Pool as StableSwapPool, + v1beta1::Pool as BalancerPool, }, + poolmanager::v1beta1::{PoolRequest, PoolResponse, PoolmanagerQuerier}, twap::v1beta1::TwapQuerier, }, }, }; -use serde::{Deserialize, Serialize}; - -// NOTE: Use custom Pool (`id` type as String) due to problem with json (de)serialization discrepancy between go and rust side. -// https://github.com/osmosis-labs/osmosis-rust/issues/42 -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct Pool { - pub id: String, - pub address: String, - pub pool_params: Option, - pub future_pool_governor: String, - pub pool_assets: Vec, - pub total_shares: Option, - pub total_weight: String, +use prost::Message; + +// Get denoms from different type of the pool +pub trait CommonPoolData { + fn get_pool_id(&self) -> u64; + fn get_pool_denoms(&self) -> Vec; +} + +#[derive(Debug, PartialEq)] +pub enum Pool { + Balancer(BalancerPool), + StableSwap(StableSwapPool), + ConcentratedLiquidity(ConcentratedLiquidityPool), +} + +impl CommonPoolData for Pool { + fn get_pool_id(&self) -> u64 { + match self { + Pool::Balancer(pool) => pool.id, + Pool::StableSwap(pool) => pool.id, + Pool::ConcentratedLiquidity(pool) => pool.id, + } + } + + fn get_pool_denoms(&self) -> Vec { + match self { + Pool::Balancer(pool) => pool + .pool_assets + .iter() + .flat_map(|asset| &asset.token) + .map(|token| token.denom.clone()) + .collect(), + Pool::StableSwap(pool) => { + pool.pool_liquidity.iter().map(|pl| pl.denom.clone()).collect() + } + Pool::ConcentratedLiquidity(pool) => { + vec![pool.token0.clone(), pool.token1.clone()] + } + } + } +} + +impl TryFrom for Pool { + type Error = StdError; + + fn try_from(value: osmosis_std::shim::Any) -> Result { + if let Ok(pool) = BalancerPool::decode(value.value.as_slice()) { + return Ok(Pool::Balancer(pool)); + } + + if let Ok(pool) = StableSwapPool::decode(value.value.as_slice()) { + return Ok(Pool::StableSwap(pool)); + } + + if let Ok(pool) = ConcentratedLiquidityPool::decode(value.value.as_slice()) { + return Ok(Pool::ConcentratedLiquidity(pool)); + } + + Err(StdError::parse_err( + "Pool", + "Unsupported pool: must be either `Balancer`, `StableSwap` or `ConcentratedLiquidity`.", + )) + } } impl Pool { @@ -48,39 +97,24 @@ impl Pool { } } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct QueryPoolResponse { - pub pool: Pool, -} - /// Query an Osmosis pool's coin depths and the supply of of liquidity token -/// -/// FIXME: migrate to Spot queries from PoolManager once whitelisted in https://github.com/osmosis-labs/osmosis/blob/main/wasmbinding/stargate_whitelist.go#L127 -#[allow(deprecated)] pub fn query_pool(querier: &QuerierWrapper, pool_id: u64) -> StdResult { let req: QueryRequest = PoolRequest { pool_id, } .into(); - let res: QueryPoolResponse = querier.query(&req)?; - Ok(res.pool) -} - -pub fn has_denom(denom: &str, pool_assets: &[PoolAsset]) -> bool { - pool_assets.iter().flat_map(|asset| &asset.token).any(|coin| coin.denom == denom) + let res: PoolResponse = querier.query(&req)?; + res.pool.ok_or_else(|| StdError::not_found("pool"))?.try_into() // convert `Any` to `Pool` } /// Query the spot price of a coin, denominated in OSMO -/// -/// FIXME: migrate to Spot queries from PoolManager once whitelisted in https://github.com/osmosis-labs/osmosis/blob/main/wasmbinding/stargate_whitelist.go#L127 -#[allow(deprecated)] pub fn query_spot_price( querier: &QuerierWrapper, pool_id: u64, base_denom: &str, quote_denom: &str, ) -> StdResult { - let spot_price_res = GammQuerier::new(querier).spot_price( + let spot_price_res = PoolmanagerQuerier::new(querier).spot_price( pool_id, base_denom.to_string(), quote_denom.to_string(), @@ -154,12 +188,14 @@ pub fn recovered_since_downtime_of_length( #[cfg(test)] mod tests { + use osmosis_std::types::osmosis::gamm::v1beta1::PoolAsset; + use super::*; #[test] fn unwrapping_coin() { - let pool = Pool { - id: "1111".to_string(), + let pool = BalancerPool { + id: 1111, address: "".to_string(), pool_params: None, future_pool_governor: "".to_string(), @@ -191,4 +227,99 @@ mod tests { let res = Pool::unwrap_coin(&pool.pool_assets[1].token).unwrap(); assert_eq!(res, coin(430, "denom_2")); } + + #[test] + fn common_data_for_balancer_pool() { + let balancer_pool = BalancerPool { + id: 1111, + address: "".to_string(), + pool_params: None, + future_pool_governor: "".to_string(), + pool_assets: vec![ + PoolAsset { + token: Some(Coin { + denom: "denom_1".to_string(), + amount: "123".to_string(), + }), + weight: "500".to_string(), + }, + PoolAsset { + token: Some(Coin { + denom: "denom_2".to_string(), + amount: "430".to_string(), + }), + weight: "500".to_string(), + }, + ], + total_shares: None, + total_weight: "".to_string(), + }; + + let any_pool = balancer_pool.to_any(); + let pool: Pool = any_pool.try_into().unwrap(); + + assert_eq!(balancer_pool.id, pool.get_pool_id()); + assert_eq!(vec!["denom_1".to_string(), "denom_2".to_string()], pool.get_pool_denoms()) + } + + #[test] + fn common_data_for_stable_swap_pool() { + let stable_swap_pool = StableSwapPool { + address: "".to_string(), + id: 4444, + pool_params: None, + future_pool_governor: "".to_string(), + total_shares: None, + pool_liquidity: vec![ + Coin { + denom: "denom_1".to_string(), + amount: "123".to_string(), + }, + Coin { + denom: "denom_2".to_string(), + amount: "430".to_string(), + }, + ], + scaling_factors: vec![], + scaling_factor_controller: "".to_string(), + }; + + let any_pool = stable_swap_pool.to_any(); + let pool: Pool = any_pool.try_into().unwrap(); + + assert_eq!(stable_swap_pool.id, pool.get_pool_id()); + assert_eq!(vec!["denom_1".to_string(), "denom_2".to_string()], pool.get_pool_denoms()) + } + + #[test] + fn common_data_for_concentrated_liquidity_pool() { + let concentrated_liquidity_pool = ConcentratedLiquidityPool { + address: "pool_address".to_string(), + incentives_address: "incentives_address".to_string(), + spread_rewards_address: "spread_rewards_address".to_string(), + id: 1066, + current_tick_liquidity: "3820025893854099618.699762490947860933".to_string(), + token0: "uosmo".to_string(), + token1: "ibc/0CD3A0285E1341859B5E86B6AB7682F023D03E97607CCC1DC95706411D866DF7" + .to_string(), + current_sqrt_price: "656651.537483144215151633465586753226461989".to_string(), + current_tick: 102311912, + tick_spacing: 100, + exponent_at_price_one: -6, + spread_factor: "0.002000000000000000".to_string(), + last_liquidity_update: None, + }; + + let any_pool = concentrated_liquidity_pool.to_any(); + let pool: Pool = any_pool.try_into().unwrap(); + + assert_eq!(concentrated_liquidity_pool.id, pool.get_pool_id()); + assert_eq!( + vec![ + "uosmo".to_string(), + "ibc/0CD3A0285E1341859B5E86B6AB7682F023D03E97607CCC1DC95706411D866DF7".to_string() + ], + pool.get_pool_denoms() + ); + } } diff --git a/packages/chains/osmosis/src/lib.rs b/packages/chains/osmosis/src/lib.rs index 1630fabcd..8d7ed0ea0 100644 --- a/packages/chains/osmosis/src/lib.rs +++ b/packages/chains/osmosis/src/lib.rs @@ -1 +1,8 @@ pub mod helpers; + +pub use osmosis_std::types::osmosis::{ + concentratedliquidity::v1beta1::Pool as ConcentratedLiquidityPool, + gamm::{ + poolmodels::stableswap::v1beta1::Pool as StableSwapPool, v1beta1::Pool as BalancerPool, + }, +}; diff --git a/packages/health/Cargo.toml b/packages/health/Cargo.toml index 536d1e4c1..4be4d5a53 100644 --- a/packages/health/Cargo.toml +++ b/packages/health/Cargo.toml @@ -20,6 +20,7 @@ backtraces = ["cosmwasm-std/backtraces"] [dependencies] cosmwasm-std = { workspace = true } +mars-params = { workspace = true } mars-red-bank-types = { workspace = true } thiserror = { workspace = true } diff --git a/packages/health/src/health.rs b/packages/health/src/health.rs index e35573c35..f48e1e508 100644 --- a/packages/health/src/health.rs +++ b/packages/health/src/health.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, fmt}; use cosmwasm_std::{Addr, Coin, Decimal, Fraction, QuerierWrapper, StdResult, Uint128}; -use mars_red_bank_types::red_bank::Market; +use mars_params::types::asset::AssetParams; use crate::{error::HealthError, query::MarsQuerier}; @@ -123,11 +123,11 @@ impl Health { p.collateral_amount += c.amount; } None => { - let Market { + let AssetParams { max_loan_to_value, liquidation_threshold, .. - } = querier.query_market(&c.denom)?; + } = querier.query_asset_params(&c.denom)?; positions.insert( c.denom.clone(), @@ -151,11 +151,11 @@ impl Health { p.debt_amount += d.amount; } None => { - let Market { + let AssetParams { max_loan_to_value, liquidation_threshold, .. - } = querier.query_market(&d.denom)?; + } = querier.query_asset_params(&d.denom)?; positions.insert( d.denom.clone(), diff --git a/packages/health/src/query.rs b/packages/health/src/query.rs index 0ecfc2bdc..29bf5ae41 100644 --- a/packages/health/src/query.rs +++ b/packages/health/src/query.rs @@ -1,32 +1,26 @@ use cosmwasm_std::{Addr, Decimal, QuerierWrapper, StdResult}; -use mars_red_bank_types::{ - oracle::{self, PriceResponse}, - red_bank::{self, Market}, -}; +use mars_params::types::asset::AssetParams; +use mars_red_bank_types::oracle::{self, ActionKind, PriceResponse}; pub struct MarsQuerier<'a> { querier: &'a QuerierWrapper<'a>, oracle_addr: &'a Addr, - red_bank_addr: &'a Addr, + params_addr: &'a Addr, } impl<'a> MarsQuerier<'a> { - pub fn new( - querier: &'a QuerierWrapper, - oracle_addr: &'a Addr, - red_bank_addr: &'a Addr, - ) -> Self { + pub fn new(querier: &'a QuerierWrapper, oracle_addr: &'a Addr, params_addr: &'a Addr) -> Self { MarsQuerier { querier, oracle_addr, - red_bank_addr, + params_addr, } } - pub fn query_market(&self, denom: &str) -> StdResult { + pub fn query_asset_params(&self, denom: &str) -> StdResult { self.querier.query_wasm_smart( - self.red_bank_addr, - &red_bank::QueryMsg::Market { + self.params_addr, + &mars_params::msg::QueryMsg::AssetParams { denom: denom.to_string(), }, ) @@ -40,6 +34,21 @@ impl<'a> MarsQuerier<'a> { self.oracle_addr, &oracle::QueryMsg::Price { denom: denom.to_string(), + kind: Some(ActionKind::Default), + }, + )?; + Ok(price) + } + + pub fn query_price_for_liquidate(&self, denom: &str) -> StdResult { + let PriceResponse { + price, + .. + } = self.querier.query_wasm_smart( + self.oracle_addr, + &oracle::QueryMsg::Price { + denom: denom.to_string(), + kind: Some(ActionKind::Liquidation), }, )?; Ok(price) diff --git a/packages/health/tests/test_from_coins_to_positions.rs b/packages/health/tests/test_from_coins_to_positions.rs index 58451d1cc..9aa85fe0a 100644 --- a/packages/health/tests/test_from_coins_to_positions.rs +++ b/packages/health/tests/test_from_coins_to_positions.rs @@ -7,6 +7,7 @@ use mars_health::{ health::{Health, Position}, query::MarsQuerier, }; +use mars_params::types::asset::{AssetParams, CmSettings, LiquidationBonus, RedBankSettings}; use mars_red_bank_types::red_bank::Market; use mars_testing::MarsMockQuerier; @@ -102,9 +103,10 @@ fn from_coins_to_positions() { assert_eq!( positions, - StdError::generic_err( - "Querier contract error: [mock]: could not find the market for invalid_denom" - ) + StdError::GenericErr { + msg: "Querier contract error: [mock]: could not find the params for invalid_denom" + .to_string() + } ); } @@ -120,18 +122,62 @@ fn mock_setup() -> MarsMockQuerier { // Set Markets let osmo_market = Market { denom: "osmo".to_string(), - max_loan_to_value: Decimal::from_atomics(50u128, 2).unwrap(), - liquidation_threshold: Decimal::from_atomics(55u128, 2).unwrap(), ..Default::default() }; mock_querier.set_redbank_market(osmo_market); + mock_querier.set_redbank_params( + "osmo", + AssetParams { + denom: "osmo".to_string(), + credit_manager: CmSettings { + whitelisted: false, + hls: None, + }, + red_bank: RedBankSettings { + deposit_enabled: false, + borrow_enabled: false, + }, + max_loan_to_value: Decimal::from_atomics(50u128, 2).unwrap(), + liquidation_threshold: Decimal::from_atomics(55u128, 2).unwrap(), + liquidation_bonus: LiquidationBonus { + starting_lb: Decimal::percent(0u64), + slope: Decimal::one(), + min_lb: Decimal::percent(0u64), + max_lb: Decimal::percent(5u64), + }, + protocol_liquidation_fee: Decimal::zero(), + deposit_cap: Default::default(), + }, + ); let atom_market = Market { denom: "atom".to_string(), - max_loan_to_value: Decimal::from_atomics(70u128, 2).unwrap(), - liquidation_threshold: Decimal::from_atomics(75u128, 2).unwrap(), ..Default::default() }; mock_querier.set_redbank_market(atom_market); + mock_querier.set_redbank_params( + "atom", + AssetParams { + denom: "atom".to_string(), + credit_manager: CmSettings { + whitelisted: false, + hls: None, + }, + red_bank: RedBankSettings { + deposit_enabled: false, + borrow_enabled: false, + }, + max_loan_to_value: Decimal::from_atomics(70u128, 2).unwrap(), + liquidation_threshold: Decimal::from_atomics(75u128, 2).unwrap(), + liquidation_bonus: LiquidationBonus { + starting_lb: Decimal::percent(0u64), + slope: Decimal::one(), + min_lb: Decimal::percent(0u64), + max_lb: Decimal::percent(5u64), + }, + protocol_liquidation_fee: Decimal::zero(), + deposit_cap: Default::default(), + }, + ); // Set prices in the oracle mock_querier.set_oracle_price("osmo", Decimal::from_atomics(23654u128, 4).unwrap()); diff --git a/packages/health/tests/test_health_from_coins.rs b/packages/health/tests/test_health_from_coins.rs index c3eac2122..234cedcea 100644 --- a/packages/health/tests/test_health_from_coins.rs +++ b/packages/health/tests/test_health_from_coins.rs @@ -5,6 +5,7 @@ use cosmwasm_std::{ Uint128, }; use mars_health::{error::HealthError, health::Health}; +use mars_params::types::asset::{AssetParams, CmSettings, LiquidationBonus, RedBankSettings}; use mars_red_bank_types::red_bank::Market; use mars_testing::MarsMockQuerier; @@ -15,18 +16,62 @@ fn health_success_from_coins() { // Set Markets let osmo_market = Market { denom: "osmo".to_string(), - max_loan_to_value: Decimal::from_atomics(50u128, 2).unwrap(), - liquidation_threshold: Decimal::from_atomics(55u128, 2).unwrap(), ..Default::default() }; mock_querier.set_redbank_market(osmo_market); + mock_querier.set_redbank_params( + "osmo", + AssetParams { + denom: "osmo".to_string(), + credit_manager: CmSettings { + whitelisted: false, + hls: None, + }, + red_bank: RedBankSettings { + deposit_enabled: true, + borrow_enabled: true, + }, + max_loan_to_value: Decimal::from_atomics(50u128, 2).unwrap(), + liquidation_threshold: Decimal::from_atomics(55u128, 2).unwrap(), + liquidation_bonus: LiquidationBonus { + starting_lb: Decimal::percent(0u64), + slope: Decimal::one(), + min_lb: Decimal::percent(0u64), + max_lb: Decimal::percent(5u64), + }, + protocol_liquidation_fee: Decimal::zero(), + deposit_cap: Uint128::MAX, + }, + ); let atom_market = Market { denom: "atom".to_string(), - max_loan_to_value: Decimal::from_atomics(70u128, 2).unwrap(), - liquidation_threshold: Decimal::from_atomics(75u128, 2).unwrap(), ..Default::default() }; mock_querier.set_redbank_market(atom_market); + mock_querier.set_redbank_params( + "atom", + AssetParams { + denom: "atom".to_string(), + credit_manager: CmSettings { + whitelisted: false, + hls: None, + }, + red_bank: RedBankSettings { + deposit_enabled: true, + borrow_enabled: true, + }, + max_loan_to_value: Decimal::from_atomics(70u128, 2).unwrap(), + liquidation_threshold: Decimal::from_atomics(75u128, 2).unwrap(), + liquidation_bonus: LiquidationBonus { + starting_lb: Decimal::percent(0u64), + slope: Decimal::one(), + min_lb: Decimal::percent(0u64), + max_lb: Decimal::percent(5u64), + }, + protocol_liquidation_fee: Decimal::zero(), + deposit_cap: Uint128::MAX, + }, + ); // Set prices in the oracle mock_querier.set_oracle_price("osmo", Decimal::from_atomics(23654u128, 4).unwrap()); @@ -68,11 +113,33 @@ fn health_error_from_coins() { // Set Markets let osmo_market = Market { denom: "osmo".to_string(), - max_loan_to_value: Decimal::from_atomics(50u128, 2).unwrap(), - liquidation_threshold: Decimal::from_atomics(55u128, 2).unwrap(), ..Default::default() }; mock_querier.set_redbank_market(osmo_market); + mock_querier.set_redbank_params( + "osmo", + AssetParams { + denom: "osmo".to_string(), + credit_manager: CmSettings { + whitelisted: false, + hls: None, + }, + red_bank: RedBankSettings { + deposit_enabled: false, + borrow_enabled: false, + }, + max_loan_to_value: Decimal::from_atomics(50u128, 2).unwrap(), + liquidation_threshold: Decimal::from_atomics(55u128, 2).unwrap(), + liquidation_bonus: LiquidationBonus { + starting_lb: Decimal::percent(0u64), + slope: Decimal::one(), + min_lb: Decimal::percent(0u64), + max_lb: Decimal::percent(5u64), + }, + protocol_liquidation_fee: Decimal::zero(), + deposit_cap: Default::default(), + }, + ); // Set prices in the oracle mock_querier.set_oracle_price("osmo", Decimal::MAX); diff --git a/packages/interest-rate/Cargo.toml b/packages/interest-rate/Cargo.toml new file mode 100644 index 000000000..905f8f5d8 --- /dev/null +++ b/packages/interest-rate/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "mars-interest-rate" +description = "Computations related to interest rates" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +keywords = { workspace = true } + +[lib] +doctest = false + +[features] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +mars-red-bank-types = { workspace = true } +mars-utils = { workspace = true } diff --git a/packages/interest-rate/src/lib.rs b/packages/interest-rate/src/lib.rs new file mode 100644 index 000000000..e8631da1c --- /dev/null +++ b/packages/interest-rate/src/lib.rs @@ -0,0 +1,222 @@ +use cosmwasm_std::{Decimal, StdError, StdResult, Uint128}; +use mars_red_bank_types::{error::MarsError, red_bank::Market}; + +/// Scaling factor used to keep more precision during division / multiplication by index. +pub const SCALING_FACTOR: Uint128 = Uint128::new(1_000_000); + +const SECONDS_PER_YEAR: u64 = 31536000u64; + +pub fn calculate_applied_linear_interest_rate( + index: Decimal, + rate: Decimal, + time_elapsed: u64, +) -> StdResult { + let rate_factor = rate.checked_mul(Decimal::from_ratio( + Uint128::from(time_elapsed), + Uint128::from(SECONDS_PER_YEAR), + ))?; + index.checked_mul(Decimal::one() + rate_factor).map_err(StdError::from) +} + +/// Get scaled liquidity amount from an underlying amount, a Market and timestamp in seconds +/// Liquidity amounts are always truncated to make sure rounding errors accumulate in favor of +/// the protocol +/// NOTE: Calling this function when interests for the market are up to date with the current block +/// and index is not, will use the wrong interest rate to update the index. +/// NOTE: This function should not be used when calculating how much scaled amount is getting +/// burned from given underlying withdraw amount. In that case, all math should be done in underlying +/// amounts then get scaled back again +pub fn get_scaled_liquidity_amount( + amount: Uint128, + market: &Market, + timestamp: u64, +) -> Result { + compute_scaled_amount( + amount, + get_updated_liquidity_index(market, timestamp)?, + ScalingOperation::Truncate, + ) +} + +/// Get underlying liquidity amount from a scaled amount, a Market and timestamp in seconds +/// Liquidity amounts are always truncated to make sure rounding errors accumulate in favor of +/// the protocol +/// NOTE: Calling this function when interests for the market are up to date with the current block +/// and index is not, will use the wrong interest rate to update the index. +pub fn get_underlying_liquidity_amount( + amount_scaled: Uint128, + market: &Market, + timestamp: u64, +) -> Result { + compute_underlying_amount( + amount_scaled, + get_updated_liquidity_index(market, timestamp)?, + ScalingOperation::Truncate, + ) +} + +/// Get scaled borrow amount from an underlying amount, a Market and timestamp in seconds +/// Debt amounts are always ceiled to make sure rounding errors accumulate in favor of +/// the protocol +/// NOTE: Calling this function when interests for the market are up to date with the current block +/// and index is not, will use the wrong interest rate to update the index. +/// NOTE: This function should not be used when calculating how much scaled amount is getting +/// repaid from a sent underlying amount. In that case, all math should be done in underlying +/// amounts then get scaled back again +pub fn get_scaled_debt_amount( + amount: Uint128, + market: &Market, + timestamp: u64, +) -> Result { + compute_scaled_amount( + amount, + get_updated_borrow_index(market, timestamp)?, + ScalingOperation::Ceil, + ) +} + +/// Get underlying borrow amount from a scaled amount, a Market and timestamp in seconds +/// Debt amounts are always ceiled so as for rounding errors to accumulate in favor of +/// the protocol +/// NOTE: Calling this function when interests for the market are up to date with the current block +/// and index is not, will use the wrong interest rate to update the index. +pub fn get_underlying_debt_amount( + amount_scaled: Uint128, + market: &Market, + timestamp: u64, +) -> Result { + compute_underlying_amount( + amount_scaled, + get_updated_borrow_index(market, timestamp)?, + ScalingOperation::Ceil, + ) +} + +pub enum ScalingOperation { + Truncate, + Ceil, +} + +/// Scales the amount dividing by an index in order to compute interest rates. Before dividing, +/// the value is multiplied by SCALING_FACTOR for greater precision. +/// Example: +/// Current index is 10. We deposit 6.123456 OSMO (6123456 uosmo). Scaled amount will be +/// 6123456 / 10 = 612345 so we loose some precision. In order to avoid this situation +/// we scale the amount by SCALING_FACTOR. +pub fn compute_scaled_amount( + amount: Uint128, + index: Decimal, + scaling_operation: ScalingOperation, +) -> Result { + // Scale by SCALING_FACTOR to have better precision + let scaled_amount = amount.checked_mul(SCALING_FACTOR)?; + match scaling_operation { + ScalingOperation::Truncate => Ok(scaled_amount.checked_div_floor(index)?), + ScalingOperation::Ceil => Ok(scaled_amount.checked_div_ceil(index)?), + } +} + +/// Descales the amount introduced by `get_scaled_amount`, returning the underlying amount. +/// As interest rate is accumulated the index used to descale the amount should be bigger than the one used to scale it. +pub fn compute_underlying_amount( + scaled_amount: Uint128, + index: Decimal, + scaling_operation: ScalingOperation, +) -> Result { + // Multiply scaled amount by decimal (index) + let before_scaling_factor = scaled_amount * index; + + // Descale by SCALING_FACTOR which is introduced when scaling the amount + match scaling_operation { + ScalingOperation::Truncate => Ok(before_scaling_factor.checked_div(SCALING_FACTOR)?), + ScalingOperation::Ceil => { + let scaling_factor_dec = Decimal::from_ratio(SCALING_FACTOR, Uint128::one()); + Ok(before_scaling_factor.checked_div_ceil(scaling_factor_dec)?) + } + } +} + +/// Return applied interest rate for borrow index according to passed blocks +/// NOTE: Calling this function when interests for the market are up to date with the current block +/// and index is not, will use the wrong interest rate to update the index. +pub fn get_updated_borrow_index(market: &Market, timestamp: u64) -> StdResult { + if market.indexes_last_updated < timestamp { + let time_elapsed = timestamp - market.indexes_last_updated; + + if !market.borrow_rate.is_zero() { + let updated_index = calculate_applied_linear_interest_rate( + market.borrow_index, + market.borrow_rate, + time_elapsed, + ); + return updated_index; + } + } + + Ok(market.borrow_index) +} + +/// Return applied interest rate for liquidity index according to passed blocks +/// NOTE: Calling this function when interests for the market are up to date with the current block +/// and index is not, will use the wrong interest rate to update the index. +pub fn get_updated_liquidity_index(market: &Market, timestamp: u64) -> StdResult { + if market.indexes_last_updated > timestamp { + return Err(StdError::generic_err( + format!("Cannot compute updated liquidity index for a timestamp: {} smaller than last updated timestamp for market: {}", timestamp, market.indexes_last_updated) + )); + } + + if market.indexes_last_updated < timestamp { + let time_elapsed = timestamp - market.indexes_last_updated; + + if !market.liquidity_rate.is_zero() { + let updated_index = calculate_applied_linear_interest_rate( + market.liquidity_index, + market.liquidity_rate, + time_elapsed, + ); + return updated_index; + } + } + + Ok(market.liquidity_index) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn accumulated_index_calculation() { + let index = Decimal::from_ratio(1u128, 10u128); + let rate = Decimal::from_ratio(2u128, 10u128); + let time_elapsed = 15768000; // half a year + let accumulated = + calculate_applied_linear_interest_rate(index, rate, time_elapsed).unwrap(); + + assert_eq!(accumulated, Decimal::from_ratio(11u128, 100u128)); + } + + #[test] + fn liquidity_and_debt_rounding() { + let start = Uint128::from(100_000_000_000_u128); + let market = Market { + liquidity_index: Decimal::from_ratio(3_u128, 1_u128), + borrow_index: Decimal::from_ratio(3_u128, 1_u128), + indexes_last_updated: 1, + ..Default::default() + }; + + let scaled_amount_liquidity = get_scaled_liquidity_amount(start, &market, 1).unwrap(); + let scaled_amount_debt = get_scaled_debt_amount(start, &market, 1).unwrap(); + assert_eq!(Uint128::from(33_333_333_333_333_333_u128), scaled_amount_liquidity); + assert_eq!(Uint128::from(33_333_333_333_333_334_u128), scaled_amount_debt); + + let back_to_underlying_liquidity = + get_underlying_liquidity_amount(scaled_amount_liquidity, &market, 1).unwrap(); + let back_to_underlying_debt = + get_underlying_debt_amount(scaled_amount_debt, &market, 1).unwrap(); + assert_eq!(Uint128::from(99_999_999_999_u128), back_to_underlying_liquidity); + assert_eq!(Uint128::from(100_000_000_001_u128), back_to_underlying_debt); + } +} diff --git a/packages/liquidation/Cargo.toml b/packages/liquidation/Cargo.toml new file mode 100644 index 000000000..472fe809f --- /dev/null +++ b/packages/liquidation/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "mars-liquidation" +description = "Helper functions to calculate liquidation amounts" +version = "1.0.0" +authors = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +keywords = { workspace = true } + +[lib] +doctest = false + +[features] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] + +[dependencies] +cosmwasm-std = { workspace = true } +mars-health = { workspace = true } +mars-params = { workspace = true } +thiserror = { workspace = true } diff --git a/packages/liquidation/README.md b/packages/liquidation/README.md new file mode 100644 index 000000000..37dc7216f --- /dev/null +++ b/packages/liquidation/README.md @@ -0,0 +1,7 @@ +# Mars Health + +Functions used for evaluating the liquidation of user positions at Mars Protocol. + +## License + +Contents of this crate are open source under [GNU General Public License v3](../../LICENSE) or later. diff --git a/packages/liquidation/src/error.rs b/packages/liquidation/src/error.rs new file mode 100644 index 000000000..0d1988d29 --- /dev/null +++ b/packages/liquidation/src/error.rs @@ -0,0 +1,23 @@ +use cosmwasm_std::{ + CheckedFromRatioError, CheckedMultiplyFractionError, CheckedMultiplyRatioError, OverflowError, + StdError, +}; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum LiquidationError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + Overflow(#[from] OverflowError), + + #[error("{0}")] + CheckedMultiplyRatio(#[from] CheckedMultiplyRatioError), + + #[error("{0}")] + CheckedMultiplyFraction(#[from] CheckedMultiplyFractionError), + + #[error("{0}")] + CheckedFromRatio(#[from] CheckedFromRatioError), +} diff --git a/packages/liquidation/src/lib.rs b/packages/liquidation/src/lib.rs new file mode 100644 index 000000000..26187e200 --- /dev/null +++ b/packages/liquidation/src/lib.rs @@ -0,0 +1,2 @@ +pub mod error; +pub mod liquidation; diff --git a/packages/liquidation/src/liquidation.rs b/packages/liquidation/src/liquidation.rs new file mode 100644 index 000000000..54a00d5c1 --- /dev/null +++ b/packages/liquidation/src/liquidation.rs @@ -0,0 +1,169 @@ +use std::{ + cmp::{max, min}, + ops::Add, +}; + +use cosmwasm_std::{Decimal, StdError, Uint128}; +use mars_health::health::Health; +use mars_params::types::asset::AssetParams; + +use crate::error::LiquidationError; + +/// Within this new system, the close factor (CF) will be determined dynamically using a parameter +/// known as the Target Health Factor (THF). The THF determines the ideal HF a position should be left +/// at immediately after the position has been liquidated. The CF, in turn, is a result of this parameter: +/// the maximum amount of debt that can be repaid to take the position to the THF. +/// For example, if the THF is 1.10 and a position gets liquidated at HF = 0.98, then the maximum +/// amount of debt a liquidator can repay (in other words, the CF) will be an amount such that the HF +/// after the liquidation is at maximum 1.10. +/// +/// The formula to calculate the maximum debt that can be repaid by a liquidator is as follows: +/// MDR_value = (THF * total_debt_value - liq_th_collateral_value) / (THF - (requested_collateral_liq_th * (1 + LB))) +/// where: +/// MDR - Maximum Debt Repayable +/// THF - Target Health Factor +/// total_debt_value - Value of debt before the liquidation happens +/// liq_th_collateral_value - Value of collateral before the liquidation happens adjusted to liquidation threshold +/// requested_collateral_liq_th - Liquidation threshold of requested collateral +/// LB - Liquidation Bonus +/// +/// PLF (Protocol Liqudiation Fee) is charged as a % of the LB. +/// For example, if we define the PLF as 10%, then the PLF would be deducted from the LB, so upon a liquidation: +/// - The liquidator receives 90% of the LB. +/// - The remaining 10% is sent to the protocol as PLF. +#[allow(clippy::too_many_arguments)] +pub fn calculate_liquidation_amounts( + collateral_amount: Uint128, + collateral_price: Decimal, + collateral_params: &AssetParams, + debt_amount: Uint128, + debt_requested_to_repay: Uint128, + debt_price: Decimal, + target_health_factor: Decimal, + health: &Health, +) -> Result<(Uint128, Uint128, Uint128), LiquidationError> { + // if health.liquidatable == true, save to unwrap + let liquidation_health_factor = health.liquidation_health_factor.unwrap(); + + let user_collateral_value = collateral_amount.checked_mul_floor(collateral_price)?; + + let liquidation_bonus = calculate_liquidation_bonus( + liquidation_health_factor, + health.total_collateral_value, + health.total_debt_value, + collateral_params, + )?; + + // All debt is liquidatable: When MDR < 0, it means even repaying the whole debt is not going to be enough + // to bring the account back to the THF, so the liquidator should be able to repay all the available debt. + // Given the numerator in the MDR formula is always > 0, MDR < 0 happens when the denominator is < 0 + // (we include the case where it’s 0 given it would make MDR = infinite). + let formula = collateral_params.liquidation_threshold * (Decimal::one() + liquidation_bonus); + let max_debt_repayable_amount = if formula < target_health_factor { + let max_debt_repayable_numerator = (target_health_factor * health.total_debt_value) + - health.liquidation_threshold_adjusted_collateral; + + let max_debt_repayable_denominator = target_health_factor - formula; + + let max_debt_repayable_value = + max_debt_repayable_numerator.checked_div_floor(max_debt_repayable_denominator)?; + + let max_debt_repayable_amount = max_debt_repayable_value.checked_div_floor(debt_price)?; + Some(max_debt_repayable_amount) + } else { + None + }; + + // calculate possible debt to repay based on available collateral + let debt_amount_possible_to_repay = user_collateral_value + .checked_div_floor(Decimal::one().add(liquidation_bonus))? + .checked_div_floor(debt_price)?; + + let debt_amount_to_repay = *[ + Some(debt_amount), + Some(debt_requested_to_repay), + max_debt_repayable_amount, + Some(debt_amount_possible_to_repay), + ] + .iter() + .flatten() + .min() + .ok_or_else(|| StdError::generic_err("Minimum not found"))?; + + let debt_value_to_repay = debt_amount_to_repay.checked_mul_floor(debt_price)?; + + let collateral_amount_to_liquidate = debt_value_to_repay + .checked_mul_floor(liquidation_bonus.add(Decimal::one()))? + .checked_div_floor(collateral_price)?; + + // In some edges scenarios: + // - if debt_amount_to_repay = 0, some liquidators could drain collaterals and all their coins + // would be refunded, i.e.: without spending coins. + // - if collateral_amount_to_liquidate is 0, some users could liquidate without receiving collaterals + // in return. + if (!collateral_amount_to_liquidate.is_zero() && debt_amount_to_repay.is_zero()) + || (collateral_amount_to_liquidate.is_zero() && !debt_amount_to_repay.is_zero()) + { + return Err(LiquidationError::Std(StdError::generic_err( + format!("Can't process liquidation. Invalid collateral_amount_to_liquidate ({collateral_amount_to_liquidate}) and debt_amount_to_repay ({debt_amount_to_repay})") + ))); + } + + let lb_value = debt_value_to_repay.checked_mul_floor(liquidation_bonus)?; + + // Use ceiling in favour of protocol + let protocol_fee_value = + lb_value.checked_mul_ceil(collateral_params.protocol_liquidation_fee)?; + let protocol_fee_amount = protocol_fee_value.checked_div_floor(collateral_price)?; + + let collateral_amount_received_by_liquidator = + collateral_amount_to_liquidate - protocol_fee_amount; + + Ok(( + debt_amount_to_repay, + collateral_amount_to_liquidate, + collateral_amount_received_by_liquidator, + )) +} + +/// The LB will depend on the Health Factor and a couple other parameters as follows: +/// Liquidation Bonus = min( +/// starting_lb + (slope * (1 - HF)), +/// max( +/// min(CR - 1, max_lb), +/// min_lb +/// ) +/// ) +/// `CR` is the Collateralization Ratio of the position calculated as `CR = Total Assets / Total Debt`. +fn calculate_liquidation_bonus( + liquidation_health_factor: Decimal, + total_collateral_value: Uint128, + total_debt_value: Uint128, + collateral_params: &AssetParams, +) -> Result { + let collateralization_ratio = + Decimal::checked_from_ratio(total_collateral_value, total_debt_value)?; + + // (CR - 1) can't be negative + let collateralization_ratio_adjusted = if collateralization_ratio > Decimal::one() { + collateralization_ratio - Decimal::one() + } else { + Decimal::zero() + }; + + let max_lb_adjusted = max( + min(collateralization_ratio_adjusted, collateral_params.liquidation_bonus.max_lb), + collateral_params.liquidation_bonus.min_lb, + ); + + let calculated_bonus = collateral_params.liquidation_bonus.starting_lb.checked_add( + collateral_params + .liquidation_bonus + .slope + .checked_mul(Decimal::one() - liquidation_health_factor)?, + )?; + + let liquidation_bonus = min(calculated_bonus, max_lb_adjusted); + + Ok(liquidation_bonus) +} diff --git a/packages/testing/Cargo.toml b/packages/testing/Cargo.toml index e6d6cc18a..2e0de5e30 100644 --- a/packages/testing/Cargo.toml +++ b/packages/testing/Cargo.toml @@ -14,24 +14,25 @@ keywords = { workspace = true } doctest = false [features] -default = [] -# for quicker tests, cargo test --lib -# for more explicit tests, cargo test --features=backtraces -astroport = ["cw-it/astroport", "dep:astroport"] +astroport = ["cw-it/astroport", "dep:astroport"] +backtraces = ["cosmwasm-std/backtraces", "osmosis-std/backtraces"] +default = [] osmosis-test-tube = ["cw-it/osmosis-test-tube"] -backtraces = ["cosmwasm-std/backtraces", "osmosis-std/backtraces"] [dependencies] anyhow = { workspace = true } astroport = { workspace = true, optional = true } cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } osmosis-std = { workspace = true } mars-address-provider = { workspace = true } mars-incentives = { workspace = true } +mars-mock-pyth = { workspace = true } mars-oracle-osmosis = { workspace = true } mars-oracle-wasm = { workspace = true } mars-osmosis = { workspace = true } mars-owner = { workspace = true } +mars-params = { workspace = true } mars-red-bank = { workspace = true } mars-red-bank-types = { workspace = true } mars-rewards-collector-osmosis = { workspace = true } @@ -39,7 +40,6 @@ mars-swapper-astroport = { workspace = true } prost = { workspace = true } pyth-sdk-cw = { workspace = true } - [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -cw-it = { workspace = true, features = ["multi-test"] } -cw-multi-test = { workspace = true } +cw-it = { workspace = true, features = ["multi-test"] } +cw-multi-test = { workspace = true } diff --git a/packages/testing/src/incentives_querier.rs b/packages/testing/src/incentives_querier.rs index 55421b3c3..1d2b76c8d 100644 --- a/packages/testing/src/incentives_querier.rs +++ b/packages/testing/src/incentives_querier.rs @@ -32,6 +32,7 @@ impl IncentivesQuerier { let ret: ContractResult = match query { QueryMsg::UserUnclaimedRewards { user: _, + account_id: _, start_after_collateral_denom: _, start_after_incentive_denom: _, limit: _, diff --git a/packages/testing/src/integration/mock_contracts.rs b/packages/testing/src/integration/mock_contracts.rs index 5a3bc7b03..826fb98a3 100644 --- a/packages/testing/src/integration/mock_contracts.rs +++ b/packages/testing/src/integration/mock_contracts.rs @@ -49,3 +49,21 @@ pub fn mock_rewards_collector_osmosis_contract() -> Box> { ); Box::new(contract) } + +pub fn mock_params_osmosis_contract() -> Box> { + let contract = ContractWrapper::new( + mars_params::contract::execute, + mars_params::contract::instantiate, + mars_params::contract::query, + ); + Box::new(contract) +} + +pub fn mock_pyth_contract() -> Box> { + let contract = ContractWrapper::new( + mars_mock_pyth::contract::execute, + mars_mock_pyth::contract::instantiate, + mars_mock_pyth::contract::query, + ); + Box::new(contract) +} diff --git a/packages/testing/src/integration/mock_env.rs b/packages/testing/src/integration/mock_env.rs index d4bf7cba5..95ab2e404 100644 --- a/packages/testing/src/integration/mock_env.rs +++ b/packages/testing/src/integration/mock_env.rs @@ -1,14 +1,20 @@ #![allow(dead_code)] -use std::mem::take; +use std::{collections::HashMap, default::Default, mem::take, str::FromStr}; use anyhow::Result as AnyResult; use cosmwasm_std::{coin, Addr, Coin, Decimal, Empty, StdResult, Uint128}; use cw_multi_test::{App, AppResponse, BankSudo, BasicApp, Executor, SudoMsg}; use mars_oracle_osmosis::OsmosisPriceSourceUnchecked; +use mars_params::{msg::AssetParamsUpdate, types::asset::AssetParams}; use mars_red_bank_types::{ address_provider::{self, MarsAddressType}, - incentives, oracle, + incentives, + oracle::{ + self, + ActionKind::{Default as ActionDefault, Liquidation}, + PriceResponse, + }, red_bank::{ self, CreateOrUpdateConfig, InitOrUpdateAssetParams, Market, UncollateralizedLoanLimitResponse, UserCollateralResponse, UserDebtResponse, @@ -16,10 +22,12 @@ use mars_red_bank_types::{ }, rewards_collector, }; +use pyth_sdk_cw::PriceIdentifier; use crate::integration::mock_contracts::{ mock_address_provider_contract, mock_incentives_contract, mock_oracle_osmosis_contract, - mock_red_bank_contract, mock_rewards_collector_osmosis_contract, + mock_params_osmosis_contract, mock_pyth_contract, mock_red_bank_contract, + mock_rewards_collector_osmosis_contract, }; pub struct MockEnv { @@ -30,6 +38,9 @@ pub struct MockEnv { pub oracle: Oracle, pub red_bank: RedBank, pub rewards_collector: RewardsCollector, + pub params: Params, + pub credit_manager: Addr, + pub pyth: Addr, } #[derive(Clone)] @@ -57,6 +68,11 @@ pub struct RewardsCollector { pub contract_addr: Addr, } +#[derive(Clone)] +pub struct Params { + pub contract_addr: Addr, +} + impl MockEnv { pub fn increment_by_blocks(&mut self, num_of_blocks: u64) { self.app.update_block(|block| { @@ -74,6 +90,13 @@ impl MockEnv { }) } + pub fn fund_accounts(&mut self, addrs: &[&Addr], amount: u128, denoms: &[&str]) { + for addr in addrs { + let coins: Vec<_> = denoms.iter().map(|&d| coin(amount, d)).collect(); + self.fund_account(addr, &coins); + } + } + pub fn fund_account(&mut self, addr: &Addr, coins: &[Coin]) { self.app .sudo(SudoMsg::Bank(BankSudo::Mint { @@ -86,6 +109,11 @@ impl MockEnv { pub fn query_balance(&self, addr: &Addr, denom: &str) -> StdResult { self.app.wrap().query_balance(addr, denom) } + + pub fn query_all_balances(&self, addr: &Addr) -> HashMap { + let res: Vec = self.app.wrap().query_all_balances(addr).unwrap(); + res.into_iter().map(|r| (r.denom, r.amount)).collect() + } } impl Incentives { @@ -156,10 +184,20 @@ impl Incentives { } pub fn claim_rewards(&self, env: &mut MockEnv, sender: &Addr) -> AnyResult { + self.claim_rewards_with_acc_id(env, sender, None) + } + + pub fn claim_rewards_with_acc_id( + &self, + env: &mut MockEnv, + sender: &Addr, + account_id: Option, + ) -> AnyResult { env.app.execute_contract( sender.clone(), self.contract_addr.clone(), &incentives::ExecuteMsg::ClaimRewards { + account_id, start_after_collateral_denom: None, start_after_incentive_denom: None, limit: None, @@ -168,19 +206,26 @@ impl Incentives { ) } - pub fn query_unclaimed_rewards(&self, env: &mut MockEnv, user: &Addr) -> Vec { - env.app - .wrap() - .query_wasm_smart( - self.contract_addr.clone(), - &incentives::QueryMsg::UserUnclaimedRewards { - user: user.to_string(), - start_after_collateral_denom: None, - start_after_incentive_denom: None, - limit: None, - }, - ) - .unwrap() + pub fn query_unclaimed_rewards(&self, env: &mut MockEnv, user: &Addr) -> StdResult> { + self.query_unclaimed_rewards_with_acc_id(env, user, None) + } + + pub fn query_unclaimed_rewards_with_acc_id( + &self, + env: &mut MockEnv, + user: &Addr, + account_id: Option, + ) -> StdResult> { + env.app.wrap().query_wasm_smart( + self.contract_addr.clone(), + &incentives::QueryMsg::UserUnclaimedRewards { + account_id, + user: user.to_string(), + start_after_collateral_denom: None, + start_after_incentive_denom: None, + limit: None, + }, + ) } } @@ -200,6 +245,63 @@ impl Oracle { ) .unwrap(); } + + pub fn set_price_source_pyth( + &self, + env: &mut MockEnv, + denom: &str, + pyth_addr: String, + max_confidence: Decimal, + max_deviation: Decimal, + ) { + env.app + .execute_contract( + env.owner.clone(), + self.contract_addr.clone(), + &oracle::ExecuteMsg::<_, Empty>::SetPriceSource { + denom: denom.to_string(), + price_source: OsmosisPriceSourceUnchecked::Pyth { + contract_addr: pyth_addr, + price_feed_id: PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(), + max_staleness: 30u64, + max_confidence, + max_deviation, + denom_decimals: 6u8, + }, + }, + &[], + ) + .unwrap(); + } + + pub fn query_price(&self, env: &mut MockEnv, denom: &str) -> PriceResponse { + env.app + .wrap() + .query_wasm_smart( + self.contract_addr.clone(), + &oracle::QueryMsg::Price { + denom: denom.to_string(), + kind: Some(ActionDefault), + }, + ) + .unwrap() + } + + pub fn query_price_for_liquidate(&self, env: &mut MockEnv, denom: &str) -> PriceResponse { + env.app + .wrap() + .query_wasm_smart( + self.contract_addr.clone(), + &oracle::QueryMsg::Price { + denom: denom.to_string(), + kind: Some(Liquidation), + }, + ) + .unwrap() + } } impl RedBank { @@ -217,11 +319,42 @@ impl RedBank { .unwrap(); } + pub fn update_user_collateral_status( + &self, + env: &mut MockEnv, + sender: &Addr, + denom: &str, + enabled: bool, + ) { + env.app + .execute_contract( + sender.clone(), + self.contract_addr.clone(), + &red_bank::ExecuteMsg::UpdateAssetCollateralStatus { + denom: denom.to_string(), + enable: enabled, + }, + &[], + ) + .unwrap(); + } + pub fn deposit(&self, env: &mut MockEnv, sender: &Addr, coin: Coin) -> AnyResult { + self.deposit_with_acc_id(env, sender, coin, None) + } + + pub fn deposit_with_acc_id( + &self, + env: &mut MockEnv, + sender: &Addr, + coin: Coin, + account_id: Option, + ) -> AnyResult { env.app.execute_contract( sender.clone(), self.contract_addr.clone(), &red_bank::ExecuteMsg::Deposit { + account_id, on_behalf_of: None, }, &[coin], @@ -264,6 +397,18 @@ impl RedBank { sender: &Addr, denom: &str, amount: Option, + ) -> AnyResult { + self.withdraw_with_acc_id(env, sender, denom, amount, None, None) + } + + pub fn withdraw_with_acc_id( + &self, + env: &mut MockEnv, + sender: &Addr, + denom: &str, + amount: Option, + account_id: Option, + liquidation_related: Option, ) -> AnyResult { env.app.execute_contract( sender.clone(), @@ -272,6 +417,8 @@ impl RedBank { denom: denom.to_string(), amount, recipient: None, + account_id, + liquidation_related, }, &[], ) @@ -283,7 +430,26 @@ impl RedBank { liquidator: &Addr, user: &Addr, collateral_denom: &str, - coin: Coin, + send_funds: &[Coin], + ) -> AnyResult { + self.liquidate_with_different_recipient( + env, + liquidator, + user, + collateral_denom, + send_funds, + None, + ) + } + + pub fn liquidate_with_different_recipient( + &self, + env: &mut MockEnv, + liquidator: &Addr, + user: &Addr, + collateral_denom: &str, + send_funds: &[Coin], + recipient: Option, ) -> AnyResult { env.app.execute_contract( liquidator.clone(), @@ -291,9 +457,9 @@ impl RedBank { &red_bank::ExecuteMsg::Liquidate { user: user.to_string(), collateral_denom: collateral_denom.to_string(), - recipient: None, + recipient, }, - &[coin], + send_funds, ) } @@ -329,6 +495,21 @@ impl RedBank { .unwrap() } + pub fn query_markets(&self, env: &mut MockEnv) -> HashMap { + let res: Vec = env + .app + .wrap() + .query_wasm_smart( + self.contract_addr.clone(), + &red_bank::QueryMsg::Markets { + start_after: None, + limit: Some(100), + }, + ) + .unwrap(); + res.into_iter().map(|r| (r.denom.clone(), r)).collect() + } + pub fn query_user_debt(&self, env: &mut MockEnv, user: &Addr, denom: &str) -> UserDebtResponse { env.app .wrap() @@ -342,11 +523,41 @@ impl RedBank { .unwrap() } + pub fn query_user_debts( + &self, + env: &mut MockEnv, + user: &Addr, + ) -> HashMap { + let res: Vec = env + .app + .wrap() + .query_wasm_smart( + self.contract_addr.clone(), + &red_bank::QueryMsg::UserDebts { + user: user.to_string(), + start_after: None, + limit: Some(100), + }, + ) + .unwrap(); + res.into_iter().map(|r| (r.denom.clone(), r)).collect() + } + pub fn query_user_collateral( &self, env: &mut MockEnv, user: &Addr, denom: &str, + ) -> UserCollateralResponse { + self.query_user_collateral_with_acc_id(env, user, None, denom) + } + + pub fn query_user_collateral_with_acc_id( + &self, + env: &mut MockEnv, + user: &Addr, + account_id: Option, + denom: &str, ) -> UserCollateralResponse { env.app .wrap() @@ -354,19 +565,60 @@ impl RedBank { self.contract_addr.clone(), &red_bank::QueryMsg::UserCollateral { user: user.to_string(), + account_id, denom: denom.to_string(), }, ) .unwrap() } + pub fn query_user_collaterals( + &self, + env: &mut MockEnv, + user: &Addr, + ) -> HashMap { + self.query_user_collaterals_with_acc_id(env, user, None) + } + + pub fn query_user_collaterals_with_acc_id( + &self, + env: &mut MockEnv, + user: &Addr, + account_id: Option, + ) -> HashMap { + let res: Vec = env + .app + .wrap() + .query_wasm_smart( + self.contract_addr.clone(), + &red_bank::QueryMsg::UserCollaterals { + user: user.to_string(), + account_id, + start_after: None, + limit: Some(100), + }, + ) + .unwrap(); + res.into_iter().map(|r| (r.denom.clone(), r)).collect() + } + pub fn query_user_position(&self, env: &mut MockEnv, user: &Addr) -> UserPositionResponse { + self.query_user_position_with_acc_id(env, user, None) + } + + pub fn query_user_position_with_acc_id( + &self, + env: &mut MockEnv, + user: &Addr, + account_id: Option, + ) -> UserPositionResponse { env.app .wrap() .query_wasm_smart( self.contract_addr.clone(), &red_bank::QueryMsg::UserPosition { user: user.to_string(), + account_id, }, ) .unwrap() @@ -446,17 +698,43 @@ impl RewardsCollector { } } +impl Params { + pub fn init_params(&self, env: &mut MockEnv, params: AssetParams) { + env.app + .execute_contract( + env.owner.clone(), + self.contract_addr.clone(), + &mars_params::msg::ExecuteMsg::UpdateAssetParams(AssetParamsUpdate::AddOrUpdate { + params: params.into(), + }), + &[], + ) + .unwrap(); + } + + pub fn query_params(&self, env: &mut MockEnv, denom: &str) -> AssetParams { + env.app + .wrap() + .query_wasm_smart( + self.contract_addr.clone(), + &mars_params::msg::QueryMsg::AssetParams { + denom: denom.to_string(), + }, + ) + .unwrap() + } +} + pub struct MockEnvBuilder { app: BasicApp, admin: Option, owner: Addr, - emergency_owner: Addr, chain_prefix: String, mars_denom: String, base_denom: String, base_denom_decimals: u8, - close_factor: Decimal, + target_health_factor: Decimal, // rewards-collector params safety_tax_rate: Decimal, @@ -465,6 +743,8 @@ pub struct MockEnvBuilder { slippage_tolerance: Decimal, pyth_contract_addr: String, + + credit_manager_contract_addr: String, } impl MockEnvBuilder { @@ -472,19 +752,20 @@ impl MockEnvBuilder { Self { app: App::default(), admin, - owner: owner.clone(), - emergency_owner: owner, + owner, chain_prefix: "".to_string(), // empty prefix for multitest because deployed contracts have addresses such as contract1, contract2 etc which are invalid in address-provider mars_denom: "umars".to_string(), base_denom: "uosmo".to_string(), base_denom_decimals: 6u8, - close_factor: Decimal::percent(80), + target_health_factor: Decimal::from_str("1.05").unwrap(), safety_tax_rate: Decimal::percent(50), safety_fund_denom: "uusdc".to_string(), fee_collector_denom: "uusdc".to_string(), slippage_tolerance: Decimal::percent(5), pyth_contract_addr: "osmo1svg55quy7jjee6dn0qx85qxxvx5cafkkw4tmqpcjr9dx99l0zrhs4usft5" .to_string(), // correct bech32 addr to pass validation + credit_manager_contract_addr: + "osmo1q7khj532p2fyvmnu83tul6xddl6yl0d0kmrzdz2pfel3lkxem92sw6zqrl".to_string(), } } @@ -503,8 +784,8 @@ impl MockEnvBuilder { self } - pub fn close_factor(&mut self, percentage: Decimal) -> &mut Self { - self.close_factor = percentage; + pub fn target_health_factor(&mut self, thf: Decimal) -> &mut Self { + self.target_health_factor = thf; self } @@ -539,6 +820,8 @@ impl MockEnvBuilder { let oracle_addr = self.deploy_oracle_osmosis(); let red_bank_addr = self.deploy_red_bank(&address_provider_addr); let rewards_collector_addr = self.deploy_rewards_collector_osmosis(&address_provider_addr); + let params_addr = self.deploy_params_osmosis(&address_provider_addr); + let pyth_addr = self.deploy_mock_pyth(); self.update_address_provider( &address_provider_addr, @@ -556,6 +839,13 @@ impl MockEnvBuilder { MarsAddressType::RewardsCollector, &rewards_collector_addr, ); + self.update_address_provider(&address_provider_addr, MarsAddressType::Params, ¶ms_addr); + let cm_addr = Addr::unchecked(&self.credit_manager_contract_addr); + self.update_address_provider( + &address_provider_addr, + MarsAddressType::CreditManager, + &cm_addr, + ); MockEnv { app: take(&mut self.app), @@ -575,6 +865,11 @@ impl MockEnvBuilder { rewards_collector: RewardsCollector { contract_addr: rewards_collector_addr, }, + params: Params { + contract_addr: params_addr, + }, + credit_manager: cm_addr, + pyth: pyth_addr, } } @@ -646,7 +941,6 @@ impl MockEnvBuilder { owner: self.owner.to_string(), config: CreateOrUpdateConfig { address_provider: Some(address_provider_addr.to_string()), - close_factor: Some(self.close_factor), }, }, &[], @@ -681,6 +975,33 @@ impl MockEnvBuilder { .unwrap() } + fn deploy_params_osmosis(&mut self, address_provider_addr: &Addr) -> Addr { + let code_id = self.app.store_code(mock_params_osmosis_contract()); + + self.app + .instantiate_contract( + code_id, + self.owner.clone(), + &mars_params::msg::InstantiateMsg { + owner: self.owner.to_string(), + address_provider: address_provider_addr.to_string(), + target_health_factor: self.target_health_factor, + }, + &[], + "params", + None, + ) + .unwrap() + } + + pub fn deploy_mock_pyth(&mut self) -> Addr { + let code_id = self.app.store_code(mock_pyth_contract()); + + self.app + .instantiate_contract(code_id, self.owner.clone(), &Empty {}, &[], "mock-pyth", None) + .unwrap() + } + fn update_address_provider( &mut self, address_provider_addr: &Addr, diff --git a/packages/testing/src/lib.rs b/packages/testing/src/lib.rs index 62e49d5b9..eceaed296 100644 --- a/packages/testing/src/lib.rs +++ b/packages/testing/src/lib.rs @@ -12,6 +12,7 @@ mod mock_address_provider; mod mocks; mod oracle_querier; mod osmosis_querier; +mod params_querier; mod pyth_querier; mod red_bank_querier; mod redemption_rate_querier; diff --git a/packages/testing/src/mars_mock_querier.rs b/packages/testing/src/mars_mock_querier.rs index 6b95abb4a..c0255c7bb 100644 --- a/packages/testing/src/mars_mock_querier.rs +++ b/packages/testing/src/mars_mock_querier.rs @@ -9,11 +9,11 @@ use mars_oracle_osmosis::{ stride::{Price, RedemptionRateResponse}, DowntimeDetector, }; -use mars_osmosis::helpers::QueryPoolResponse; +use mars_params::types::asset::AssetParams; use mars_red_bank_types::{address_provider, incentives, oracle, red_bank}; use osmosis_std::types::osmosis::{ downtimedetector::v1beta1::RecoveredSinceDowntimeOfLengthResponse, - poolmanager::v1beta1::SpotPriceResponse, + poolmanager::v1beta1::{PoolResponse, SpotPriceResponse}, twap::v1beta1::{ArithmeticTwapToNowResponse, GeometricTwapToNowResponse}, }; use pyth_sdk_cw::{PriceFeedResponse, PriceIdentifier}; @@ -23,6 +23,7 @@ use crate::{ mock_address_provider, oracle_querier::OracleQuerier, osmosis_querier::{OsmosisQuerier, PriceKey}, + params_querier::ParamsQuerier, pyth_querier::PythQuerier, red_bank_querier::RedBankQuerier, redemption_rate_querier::RedemptionRateQuerier, @@ -36,6 +37,7 @@ pub struct MarsMockQuerier { pyth_querier: PythQuerier, redbank_querier: RedBankQuerier, redemption_rate_querier: RedemptionRateQuerier, + params_querier: ParamsQuerier, } impl Querier for MarsMockQuerier { @@ -64,6 +66,7 @@ impl MarsMockQuerier { pyth_querier: PythQuerier::default(), redbank_querier: RedBankQuerier::default(), redemption_rate_querier: Default::default(), + params_querier: ParamsQuerier::default(), } } @@ -73,6 +76,10 @@ impl MarsMockQuerier { self.base.update_balance(contract_addr.to_string(), contract_balances.to_vec()); } + pub fn update_balances(&mut self, addr: impl Into, balance: Vec) { + self.base.update_balance(addr, balance); + } + pub fn set_oracle_price(&mut self, denom: &str, price: Decimal) { self.oracle_querier.prices.insert(denom.to_string(), price); } @@ -93,7 +100,7 @@ impl MarsMockQuerier { ); } - pub fn set_query_pool_response(&mut self, pool_id: u64, pool_response: QueryPoolResponse) { + pub fn set_query_pool_response(&mut self, pool_id: u64, pool_response: PoolResponse) { self.osmosis_querier.pools.insert(pool_id, pool_response); } @@ -182,6 +189,14 @@ impl MarsMockQuerier { .insert((user.into(), collateral.denom.clone()), collateral); } + pub fn set_red_bank_user_debt( + &mut self, + user: impl Into, + debt: red_bank::UserDebtResponse, + ) { + self.redbank_querier.users_denoms_debts.insert((user.into(), debt.denom.clone()), debt); + } + pub fn set_redbank_user_position( &mut self, user_address: String, @@ -190,6 +205,18 @@ impl MarsMockQuerier { self.redbank_querier.users_positions.insert(user_address, position); } + pub fn set_redbank_params(&mut self, denom: &str, params: AssetParams) { + self.params_querier.params.insert(denom.to_string(), params); + } + + pub fn set_target_health_factor(&mut self, thf: Decimal) { + self.params_querier.target_health_factor = thf; + } + + pub fn set_total_deposit(&mut self, denom: impl Into, amount: impl Into) { + self.params_querier.total_deposits.insert(denom.into(), amount.into()); + } + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { match &request { QueryRequest::Wasm(WasmQuery::Smart { @@ -240,6 +267,11 @@ impl MarsMockQuerier { return self.redemption_rate_querier.handle_query(redemption_rate_req); } + // Params Queries + if let Ok(params_query) = from_binary::(msg) { + return self.params_querier.handle_query(params_query); + } + panic!("[mock]: Unsupported wasm query: {msg:?}"); } diff --git a/packages/testing/src/oracle_querier.rs b/packages/testing/src/oracle_querier.rs index 8c1459377..3fbd243da 100644 --- a/packages/testing/src/oracle_querier.rs +++ b/packages/testing/src/oracle_querier.rs @@ -13,6 +13,7 @@ impl OracleQuerier { let ret: ContractResult = match query { QueryMsg::Price { denom, + kind: _, } => { let option_price = self.prices.get(&denom); diff --git a/packages/testing/src/osmosis_querier.rs b/packages/testing/src/osmosis_querier.rs index 539ba769a..9773e8772 100644 --- a/packages/testing/src/osmosis_querier.rs +++ b/packages/testing/src/osmosis_querier.rs @@ -1,12 +1,11 @@ use std::collections::HashMap; use cosmwasm_std::{to_binary, Binary, ContractResult, QuerierResult, SystemError}; -use mars_osmosis::helpers::QueryPoolResponse; use osmosis_std::types::osmosis::{ downtimedetector::v1beta1::{ RecoveredSinceDowntimeOfLengthRequest, RecoveredSinceDowntimeOfLengthResponse, }, - poolmanager::v1beta1::{PoolRequest, SpotPriceRequest, SpotPriceResponse}, + poolmanager::v1beta1::{PoolRequest, PoolResponse, SpotPriceRequest, SpotPriceResponse}, twap::v1beta1::{ ArithmeticTwapToNowRequest, ArithmeticTwapToNowResponse, GeometricTwapToNowRequest, GeometricTwapToNowResponse, @@ -23,7 +22,7 @@ pub struct PriceKey { #[derive(Clone, Default)] pub struct OsmosisQuerier { - pub pools: HashMap, + pub pools: HashMap, pub spot_prices: HashMap, pub arithmetic_twap_prices: HashMap, @@ -34,7 +33,7 @@ pub struct OsmosisQuerier { impl OsmosisQuerier { pub fn handle_stargate_query(&self, path: &str, data: &Binary) -> Result { - if path == "/osmosis.gamm.v1beta1.Query/Pool" { + if path == "/osmosis.poolmanager.v1beta1.Query/Pool" { let parse_osmosis_query: Result = Message::decode(data.as_slice()); if let Ok(osmosis_query) = parse_osmosis_query { @@ -42,7 +41,7 @@ impl OsmosisQuerier { } } - if path == "/osmosis.gamm.v2.Query/SpotPrice" { + if path == "/osmosis.poolmanager.v1beta1.Query/SpotPrice" { let parse_osmosis_query: Result = Message::decode(data.as_slice()); if let Ok(osmosis_query) = parse_osmosis_query { diff --git a/packages/testing/src/params_querier.rs b/packages/testing/src/params_querier.rs new file mode 100644 index 000000000..f3598e2ba --- /dev/null +++ b/packages/testing/src/params_querier.rs @@ -0,0 +1,37 @@ +use std::collections::HashMap; + +use cosmwasm_std::{to_binary, Binary, Coin, ContractResult, Decimal, QuerierResult, Uint128}; +use mars_params::{msg::QueryMsg, types::asset::AssetParams}; + +#[derive(Default)] +pub struct ParamsQuerier { + pub target_health_factor: Decimal, + pub params: HashMap, + pub total_deposits: HashMap, +} + +impl ParamsQuerier { + pub fn handle_query(&self, query: QueryMsg) -> QuerierResult { + let ret: ContractResult = match query { + QueryMsg::TargetHealthFactor {} => to_binary(&self.target_health_factor).into(), + QueryMsg::AssetParams { + denom, + } => match self.params.get(&denom) { + Some(params) => to_binary(¶ms).into(), + None => Err(format!("[mock]: could not find the params for {denom}")).into(), + }, + QueryMsg::TotalDeposit { + denom, + } => match self.total_deposits.get(&denom) { + Some(amount) => to_binary(&Coin { + denom, + amount: *amount, + }) + .into(), + None => Err(format!("[mock]: could not find total deposit for {denom}")).into(), + }, + _ => Err("[mock]: Unsupported params query".to_string()).into(), + }; + Ok(ret).into() + } +} diff --git a/packages/testing/src/red_bank_querier.rs b/packages/testing/src/red_bank_querier.rs index b4984d268..05902a61c 100644 --- a/packages/testing/src/red_bank_querier.rs +++ b/packages/testing/src/red_bank_querier.rs @@ -2,13 +2,14 @@ use std::collections::HashMap; use cosmwasm_std::{to_binary, Binary, ContractResult, QuerierResult}; use mars_red_bank_types::red_bank::{ - Market, QueryMsg, UserCollateralResponse, UserPositionResponse, + Market, QueryMsg, UserCollateralResponse, UserDebtResponse, UserPositionResponse, }; #[derive(Default)] pub struct RedBankQuerier { pub markets: HashMap, pub users_denoms_collaterals: HashMap<(String, String), UserCollateralResponse>, + pub users_denoms_debts: HashMap<(String, String), UserDebtResponse>, pub users_positions: HashMap, } @@ -17,19 +18,28 @@ impl RedBankQuerier { let ret: ContractResult = match query { QueryMsg::Market { denom, - } => match self.markets.get(&denom) { - Some(market) => to_binary(&market).into(), - None => Err(format!("[mock]: could not find the market for {denom}")).into(), - }, + } => { + let maybe_market = self.markets.get(&denom); + to_binary(&maybe_market).into() + } QueryMsg::UserCollateral { user, + account_id: _, denom, } => match self.users_denoms_collaterals.get(&(user.clone(), denom)) { Some(collateral) => to_binary(&collateral).into(), None => Err(format!("[mock]: could not find the collateral for {user}")).into(), }, + QueryMsg::UserDebt { + user, + denom, + } => match self.users_denoms_debts.get(&(user.clone(), denom)) { + Some(debt) => to_binary(&debt).into(), + None => Err(format!("[mock]: could not find the debt for {user}")).into(), + }, QueryMsg::UserPosition { user, + account_id: _, } => match self.users_positions.get(&user) { Some(market) => to_binary(&market).into(), None => Err(format!("[mock]: could not find the position for {user}")).into(), diff --git a/packages/testing/src/wasm_oracle.rs b/packages/testing/src/wasm_oracle.rs index 88e0076b5..6b1f55b53 100644 --- a/packages/testing/src/wasm_oracle.rs +++ b/packages/testing/src/wasm_oracle.rs @@ -168,6 +168,7 @@ impl<'a> WasmOracleTestRobot<'a> { pub fn query_price(&self, denom: &str) -> mars_red_bank_types::oracle::PriceResponse { let msg = &mars_red_bank_types::oracle::msg::QueryMsg::Price { denom: denom.to_string(), + kind: None, }; self.wasm().query(&self.mars_oracle_contract_addr, &msg).unwrap() } @@ -180,6 +181,7 @@ impl<'a> WasmOracleTestRobot<'a> { let msg = &mars_red_bank_types::oracle::msg::QueryMsg::Prices { start_after, limit, + kind: None, }; self.wasm().query(&self.mars_oracle_contract_addr, &msg).unwrap() } diff --git a/packages/types/Cargo.toml b/packages/types/Cargo.toml index 5956de1c8..d4475675f 100644 --- a/packages/types/Cargo.toml +++ b/packages/types/Cargo.toml @@ -23,5 +23,5 @@ cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } mars-owner = { workspace = true } mars-utils = { workspace = true } +strum = { workspace = true, features = ["derive"] } thiserror = { workspace = true } -strum = { workspace = true, features = ["derive"] } diff --git a/packages/types/src/address_provider.rs b/packages/types/src/address_provider.rs index 7fdfd7d13..c212ec890 100644 --- a/packages/types/src/address_provider.rs +++ b/packages/types/src/address_provider.rs @@ -12,6 +12,8 @@ pub enum MarsAddressType { Oracle, RedBank, RewardsCollector, + Params, + CreditManager, /// Protocol admin is an ICS-27 interchain account controlled by Mars Hub's x/gov module. /// This account will take the owner and admin roles of red-bank contracts. /// @@ -38,9 +40,11 @@ pub enum MarsAddressType { impl fmt::Display for MarsAddressType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = match self { + MarsAddressType::CreditManager => "credit_manager", MarsAddressType::FeeCollector => "fee_collector", MarsAddressType::Incentives => "incentives", MarsAddressType::Oracle => "oracle", + MarsAddressType::Params => "params", MarsAddressType::ProtocolAdmin => "protocol_admin", MarsAddressType::RedBank => "red_bank", MarsAddressType::RewardsCollector => "rewards_collector", @@ -56,9 +60,11 @@ impl FromStr for MarsAddressType { fn from_str(s: &str) -> Result { match s { + "credit_manager" => Ok(MarsAddressType::CreditManager), "fee_collector" => Ok(MarsAddressType::FeeCollector), "incentives" => Ok(MarsAddressType::Incentives), "oracle" => Ok(MarsAddressType::Oracle), + "params" => Ok(MarsAddressType::Params), "protocol_admin" => Ok(MarsAddressType::ProtocolAdmin), "red_bank" => Ok(MarsAddressType::RedBank), "rewards_collector" => Ok(MarsAddressType::RewardsCollector), diff --git a/packages/types/src/error.rs b/packages/types/src/error.rs index 0a2c667db..e9cb63111 100644 --- a/packages/types/src/error.rs +++ b/packages/types/src/error.rs @@ -1,4 +1,6 @@ -use cosmwasm_std::StdError; +use cosmwasm_std::{ + CheckedFromRatioError, CheckedMultiplyFractionError, DivideByZeroError, OverflowError, StdError, +}; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -22,6 +24,18 @@ pub enum MarsError { Deserialize { target_type: String, }, + + #[error("{0}")] + Overflow(#[from] OverflowError), + + #[error("{0}")] + DivideByZero(#[from] DivideByZeroError), + + #[error("{0}")] + CheckedFromRatio(#[from] CheckedFromRatioError), + + #[error("{0}")] + CheckedMultiplyFraction(#[from] CheckedMultiplyFractionError), } impl From for StdError { diff --git a/packages/types/src/incentives.rs b/packages/types/src/incentives.rs index 8aceee19b..db37b8375 100644 --- a/packages/types/src/incentives.rs +++ b/packages/types/src/incentives.rs @@ -125,6 +125,8 @@ pub enum ExecuteMsg { /// User address. Address is trusted as it must be validated by the Red Bank /// contract before calling this method user_addr: Addr, + /// Credit account id (Rover) + account_id: Option, /// Denom of the asset of which deposited balance is changed denom: String, /// The user's scaled collateral amount up to the instant before the change @@ -136,6 +138,8 @@ pub enum ExecuteMsg { /// Claim rewards. MARS rewards accrued by the user will be staked into xMARS before /// being sent. ClaimRewards { + /// Credit account id (Rover) + account_id: Option, /// Start pagination after this collateral denom start_after_collateral_denom: Option, /// Start pagination after this incentive denom. If supplied you must also supply @@ -229,6 +233,8 @@ pub enum QueryMsg { UserUnclaimedRewards { /// The user address for which to query unclaimed rewards user: String, + /// Credit account id (Rover) + account_id: Option, /// Start pagination after this collateral denom start_after_collateral_denom: Option, /// Start pagination after this incentive denom. If supplied you must also supply @@ -246,7 +252,19 @@ pub enum QueryMsg { } #[cw_serde] -pub struct MigrateMsg {} +pub struct V2Updates { + /// The amount of time in seconds for each incentive epoch. This is the minimum amount of time + /// that an incentive can last, and each incentive must be a multiple of this duration. + pub epoch_duration: u64, + /// The maximum number of incentive denoms that can be whitelisted at any given time. This is + /// a guard against accidentally whitelisting too many denoms, which could cause max gas errors. + pub max_whitelisted_denoms: u8, +} + +#[cw_serde] +pub enum MigrateMsg { + V1_0_0ToV2_0_0(V2Updates), +} #[cw_serde] pub struct EmissionResponse { @@ -297,4 +315,6 @@ pub struct ConfigResponse { pub max_whitelisted_denoms: u8, /// The epoch duration in seconds pub epoch_duration: u64, + /// The count of the number of whitelisted incentive denoms + pub whitelist_count: u8, } diff --git a/packages/types/src/lib.rs b/packages/types/src/lib.rs index ee8a737d6..5750ce359 100644 --- a/packages/types/src/lib.rs +++ b/packages/types/src/lib.rs @@ -5,3 +5,16 @@ pub mod oracle; pub mod red_bank; pub mod rewards_collector; pub mod swapper; + +use cosmwasm_schema::cw_serde; + +#[cw_serde] +pub struct PaginationResponse { + pub data: Vec, + pub metadata: Metadata, +} + +#[cw_serde] +pub struct Metadata { + pub has_more: bool, +} diff --git a/packages/types/src/oracle/msg.rs b/packages/types/src/oracle/msg.rs index 96db6d1b0..572aae9bc 100644 --- a/packages/types/src/oracle/msg.rs +++ b/packages/types/src/oracle/msg.rs @@ -41,6 +41,13 @@ pub enum ExecuteMsg { Custom(C), } +/// Differentiator for the action (liquidate, withdraw, borrow etc.) being performed. +#[cw_serde] +pub enum ActionKind { + Default, + Liquidation, +} + #[cw_serde] #[derive(QueryResponses)] pub enum QueryMsg { @@ -69,6 +76,7 @@ pub enum QueryMsg { #[returns(PriceResponse)] Price { denom: String, + kind: Option, }, /// Enumerate all coins' prices. /// @@ -78,6 +86,7 @@ pub enum QueryMsg { Prices { start_after: Option, limit: Option, + kind: Option, }, } @@ -106,7 +115,8 @@ pub struct PriceResponse { pub mod helpers { use cosmwasm_std::{Decimal, QuerierWrapper, StdError, StdResult}; - use super::{PriceResponse, QueryMsg}; + use super::{ActionKind, PriceResponse, QueryMsg}; + use crate::oracle::ActionKind::Liquidation; pub fn query_price( querier: &QuerierWrapper, @@ -119,6 +129,7 @@ pub mod helpers { oracle.into(), &QueryMsg::Price { denom: denom.clone(), + kind: Some(ActionKind::Default), }, ) .map_err(|e| { @@ -129,4 +140,19 @@ pub mod helpers { })?; Ok(res.price) } + + pub fn query_price_for_liquidate( + querier: &QuerierWrapper, + oracle: impl Into, + denom: impl Into, + ) -> StdResult { + let res: PriceResponse = querier.query_wasm_smart( + oracle.into(), + &QueryMsg::Price { + denom: denom.into(), + kind: Some(Liquidation), + }, + )?; + Ok(res.price) + } } diff --git a/packages/types/src/red_bank/interest_rate_model.rs b/packages/types/src/red_bank/interest_rate_model.rs index 9e2af3129..26f844eaa 100644 --- a/packages/types/src/red_bank/interest_rate_model.rs +++ b/packages/types/src/red_bank/interest_rate_model.rs @@ -1,6 +1,8 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Decimal, StdError, StdResult}; -use mars_utils::{error::ValidationError, helpers::decimal_param_le_one, math}; +use cosmwasm_std::Decimal; +use mars_utils::{error::ValidationError, helpers::decimal_param_le_one}; + +use crate::error::MarsError; #[cw_serde] #[derive(Eq, Default)] @@ -9,40 +11,47 @@ pub struct InterestRateModel { pub optimal_utilization_rate: Decimal, /// Base rate pub base: Decimal, - /// Slope parameter for interest rate model function when utilization_rate < optimal_utilization_rate + /// Slope parameter for interest rate model function when utilization_rate <= optimal_utilization_rate pub slope_1: Decimal, - /// Slope parameter for interest rate model function when utilization_rate >= optimal_utilization_rate + /// Slope parameter for interest rate model function when utilization_rate > optimal_utilization_rate pub slope_2: Decimal, } impl InterestRateModel { pub fn validate(&self) -> Result<(), ValidationError> { decimal_param_le_one(self.optimal_utilization_rate, "optimal_utilization_rate")?; + + if self.slope_1 >= self.slope_2 { + return Err(ValidationError::InvalidParam { + param_name: "slope_1".to_string(), + invalid_value: self.slope_1.to_string(), + predicate: format!("< {}", self.slope_2), + }); + } + Ok(()) } - pub fn get_borrow_rate(&self, current_utilization_rate: Decimal) -> StdResult { + pub fn get_borrow_rate(&self, current_utilization_rate: Decimal) -> Result { let new_borrow_rate = if current_utilization_rate <= self.optimal_utilization_rate { if current_utilization_rate.is_zero() { - // prevent division by zero when optimal_utilization_rate is zero + // prevent division by zero when current_utilization_rate is zero self.base } else { // The borrow interest rates increase slowly with utilization self.base - + self.slope_1.checked_mul(math::divide_decimal_by_decimal( - current_utilization_rate, - self.optimal_utilization_rate, - )?)? + + self.slope_1.checked_mul( + current_utilization_rate.checked_div(self.optimal_utilization_rate)?, + )? } } else { // The borrow interest rates increase sharply with utilization self.base + self.slope_1 - + math::divide_decimal_by_decimal( - self.slope_2 - .checked_mul(current_utilization_rate - self.optimal_utilization_rate)?, - Decimal::one() - self.optimal_utilization_rate, - )? + + self + .slope_2 + .checked_mul(current_utilization_rate - self.optimal_utilization_rate)? + .checked_div(Decimal::one() - self.optimal_utilization_rate)? }; Ok(new_borrow_rate) } @@ -52,12 +61,11 @@ impl InterestRateModel { borrow_rate: Decimal, current_utilization_rate: Decimal, reserve_factor: Decimal, - ) -> StdResult { - borrow_rate + ) -> Result { + Ok(borrow_rate .checked_mul(current_utilization_rate)? // This operation should not underflow as reserve_factor is checked to be <= 1 - .checked_mul(Decimal::one() - reserve_factor) - .map_err(StdError::from) + .checked_mul(Decimal::one() - reserve_factor)?) } } @@ -91,21 +99,13 @@ mod tests { market.update_interest_rates(utilization_rate).unwrap(); - let expected_borrow_rate = model.base - + math::divide_decimal_by_decimal( - model.slope_1.checked_mul(utilization_rate).unwrap(), - model.optimal_utilization_rate, - ) - .unwrap(); + let expected_borrow_rate = + model.base + model.slope_1 * utilization_rate / model.optimal_utilization_rate; assert_eq!(market.borrow_rate, expected_borrow_rate); assert_eq!( market.liquidity_rate, - expected_borrow_rate - .checked_mul(utilization_rate) - .unwrap() - .checked_mul(Decimal::one() - reserve_factor) - .unwrap() + expected_borrow_rate * utilization_rate * (Decimal::one() - reserve_factor) ); } @@ -124,11 +124,7 @@ mod tests { let new_borrow_rate = model.get_borrow_rate(current_utilization_rate).unwrap(); let expected_borrow_rate = model.base - + math::divide_decimal_by_decimal( - model.slope_1.checked_mul(current_utilization_rate).unwrap(), - model.optimal_utilization_rate, - ) - .unwrap(); + + model.slope_1 * current_utilization_rate / model.optimal_utilization_rate; assert_eq!(new_borrow_rate, expected_borrow_rate); } @@ -139,11 +135,7 @@ mod tests { let new_borrow_rate = model.get_borrow_rate(current_utilization_rate).unwrap(); let expected_borrow_rate = model.base - + math::divide_decimal_by_decimal( - model.slope_1.checked_mul(current_utilization_rate).unwrap(), - model.optimal_utilization_rate, - ) - .unwrap(); + + model.slope_1 * current_utilization_rate / model.optimal_utilization_rate; assert_eq!(new_borrow_rate, expected_borrow_rate); } @@ -155,14 +147,8 @@ mod tests { let expected_borrow_rate = model.base + model.slope_1 - + math::divide_decimal_by_decimal( - model - .slope_2 - .checked_mul(current_utilization_rate - model.optimal_utilization_rate) - .unwrap(), - Decimal::one() - model.optimal_utilization_rate, - ) - .unwrap(); + + model.slope_2 * (current_utilization_rate - model.optimal_utilization_rate) + / (Decimal::one() - model.optimal_utilization_rate); assert_eq!(new_borrow_rate, expected_borrow_rate); } @@ -179,9 +165,7 @@ mod tests { let current_utilization_rate = Decimal::percent(100); let new_borrow_rate = model.get_borrow_rate(current_utilization_rate).unwrap(); - let expected_borrow_rate = Decimal::percent(7); - - assert_eq!(new_borrow_rate, expected_borrow_rate); + assert_eq!(new_borrow_rate, Decimal::percent(7)); } // current utilization rate == 0% and optimal utilization rate == 0% @@ -196,9 +180,7 @@ mod tests { let current_utilization_rate = Decimal::percent(0); let new_borrow_rate = model.get_borrow_rate(current_utilization_rate).unwrap(); - let expected_borrow_rate = Decimal::percent(2); - - assert_eq!(new_borrow_rate, expected_borrow_rate); + assert_eq!(new_borrow_rate, Decimal::percent(2)); } // current utilization rate == 20% and optimal utilization rate == 0% @@ -213,9 +195,8 @@ mod tests { let current_utilization_rate = Decimal::percent(20); let new_borrow_rate = model.get_borrow_rate(current_utilization_rate).unwrap(); - let expected_borrow_rate = model.base - + model.slope_1 - + model.slope_2.checked_mul(current_utilization_rate).unwrap(); + let expected_borrow_rate = + model.base + model.slope_1 + model.slope_2 * current_utilization_rate; assert_eq!(new_borrow_rate, expected_borrow_rate); } diff --git a/packages/types/src/red_bank/market.rs b/packages/types/src/red_bank/market.rs index cb5f96884..2ffe0a041 100644 --- a/packages/types/src/red_bank/market.rs +++ b/packages/types/src/red_bank/market.rs @@ -1,9 +1,6 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Decimal, StdResult, Uint128}; -use mars_utils::{ - error::ValidationError, - helpers::{decimal_param_le_one, decimal_param_lt_one}, -}; +use mars_utils::{error::ValidationError, helpers::decimal_param_lt_one}; use crate::red_bank::InterestRateModel; @@ -11,14 +8,6 @@ use crate::red_bank::InterestRateModel; pub struct Market { /// Denom of the asset pub denom: String, - - /// Max base asset that can be borrowed per "base asset" collateral when using the asset as collateral - pub max_loan_to_value: Decimal, - /// Base asset amount in debt position per "base asset" of asset collateral that if surpassed makes the user's position liquidatable. - pub liquidation_threshold: Decimal, - /// Bonus amount of collateral liquidator get when repaying user's debt (Will get collateral - /// from user in an amount equal to debt repayed + bonus) - pub liquidation_bonus: Decimal, /// Portion of the borrow rate that is kept as protocol rewards pub reserve_factor: Decimal, @@ -40,13 +29,6 @@ pub struct Market { pub collateral_total_scaled: Uint128, /// Total debt scaled for the market's currency pub debt_total_scaled: Uint128, - - /// If false cannot deposit - pub deposit_enabled: bool, - /// If false cannot borrow - pub borrow_enabled: bool, - /// Deposit Cap (defined in terms of the asset) - pub deposit_cap: Uint128, } impl Default for Market { @@ -57,18 +39,11 @@ impl Default for Market { liquidity_index: Decimal::one(), borrow_rate: Decimal::zero(), liquidity_rate: Decimal::zero(), - max_loan_to_value: Decimal::zero(), reserve_factor: Decimal::zero(), indexes_last_updated: 0, collateral_total_scaled: Uint128::zero(), debt_total_scaled: Uint128::zero(), - liquidation_threshold: Decimal::one(), - liquidation_bonus: Decimal::zero(), interest_rate_model: InterestRateModel::default(), - deposit_enabled: true, - borrow_enabled: true, - // By default the cap should be unlimited (no cap) - deposit_cap: Uint128::MAX, } } } @@ -76,18 +51,6 @@ impl Default for Market { impl Market { pub fn validate(&self) -> Result<(), ValidationError> { decimal_param_lt_one(self.reserve_factor, "reserve_factor")?; - decimal_param_le_one(self.max_loan_to_value, "max_loan_to_value")?; - decimal_param_le_one(self.liquidation_threshold, "liquidation_threshold")?; - decimal_param_le_one(self.liquidation_bonus, "liquidation_bonus")?; - - // liquidation_threshold should be greater than max_loan_to_value - if self.liquidation_threshold <= self.max_loan_to_value { - return Err(ValidationError::InvalidParam { - param_name: "liquidation_threshold".to_string(), - invalid_value: self.liquidation_threshold.to_string(), - predicate: format!("> {} (max LTV)", self.max_loan_to_value), - }); - } self.interest_rate_model.validate()?; diff --git a/packages/types/src/red_bank/msg.rs b/packages/types/src/red_bank/msg.rs index 3a29d96e2..f3a93e256 100644 --- a/packages/types/src/red_bank/msg.rs +++ b/packages/types/src/red_bank/msg.rs @@ -54,6 +54,9 @@ pub enum ExecuteMsg { /// Deposit native coins. Deposited coins must be sent in the transaction /// this call is made Deposit { + /// Credit account id (Rover) + account_id: Option, + /// Address that will receive the coins on_behalf_of: Option, }, @@ -66,6 +69,11 @@ pub enum ExecuteMsg { amount: Option, /// The address where the withdrawn amount is sent recipient: Option, + /// Credit account id (Rover) + account_id: Option, + // Withdraw action related to liquidation process initiated in credit manager. + // This flag is used to identify different way for pricing assets during liquidation. + liquidation_related: Option, }, /// Borrow native coins. If borrow allowed, amount is added to caller's debt @@ -112,30 +120,15 @@ pub enum ExecuteMsg { #[cw_serde] pub struct CreateOrUpdateConfig { pub address_provider: Option, - pub close_factor: Option, } #[cw_serde] pub struct InitOrUpdateAssetParams { /// Portion of the borrow rate that is kept as protocol rewards pub reserve_factor: Option, - /// Max uusd that can be borrowed per uusd of collateral when using the asset as collateral - pub max_loan_to_value: Option, - /// uusd amount in debt position per uusd of asset collateral that if surpassed makes the user's position liquidatable. - pub liquidation_threshold: Option, - /// Bonus amount of collateral liquidator get when repaying user's debt (Will get collateral - /// from user in an amount equal to debt repayed + bonus) - pub liquidation_bonus: Option, /// Interest rate strategy to calculate borrow_rate and liquidity_rate pub interest_rate_model: Option, - - /// If false cannot deposit - pub deposit_enabled: Option, - /// If false cannot borrow - pub borrow_enabled: Option, - /// Deposit Cap defined in terms of the asset (Unlimited by default) - pub deposit_cap: Option, } #[cw_serde] @@ -192,6 +185,7 @@ pub enum QueryMsg { #[returns(crate::red_bank::UserCollateralResponse)] UserCollateral { user: String, + account_id: Option, denom: String, }, @@ -199,6 +193,16 @@ pub enum QueryMsg { #[returns(Vec)] UserCollaterals { user: String, + account_id: Option, + start_after: Option, + limit: Option, + }, + + /// Get all collateral positions for a user + #[returns(crate::red_bank::PaginatedUserCollateralResponse)] + UserCollateralsV2 { + user: String, + account_id: Option, start_after: Option, limit: Option, }, @@ -207,6 +211,14 @@ pub enum QueryMsg { #[returns(crate::red_bank::UserPositionResponse)] UserPosition { user: String, + account_id: Option, + }, + + /// Get user position for liquidation + #[returns(crate::red_bank::UserPositionResponse)] + UserPositionLiquidationPricing { + user: String, + account_id: Option, }, /// Get liquidity scaled amount for a given underlying asset amount. diff --git a/packages/types/src/red_bank/types.rs b/packages/types/src/red_bank/types.rs index 20b37bc10..d6b140449 100644 --- a/packages/types/src/red_bank/types.rs +++ b/packages/types/src/red_bank/types.rs @@ -1,21 +1,13 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Decimal, Uint128}; -use mars_utils::{error::ValidationError, helpers::decimal_param_le_one}; + +use crate::PaginationResponse; /// Global configuration #[cw_serde] pub struct Config { /// Address provider returns addresses for all protocol contracts pub address_provider: T, - /// Maximum percentage of outstanding debt that can be covered by a liquidator - pub close_factor: Decimal, -} - -impl Config { - pub fn validate(&self) -> Result<(), ValidationError> { - decimal_param_le_one(self.close_factor, "close_factor")?; - Ok(()) - } } #[cw_serde] @@ -70,12 +62,8 @@ pub struct ConfigResponse { pub owner: Option, /// The contract's proposed owner pub proposed_new_owner: Option, - /// The contract's emergency owner - pub emergency_owner: Option, /// Address provider returns addresses for all protocol contracts pub address_provider: String, - /// Maximum percentage of outstanding debt that can be covered by a liquidator - pub close_factor: Decimal, } #[cw_serde] @@ -110,6 +98,8 @@ pub struct UserCollateralResponse { pub enabled: bool, } +pub type PaginatedUserCollateralResponse = PaginationResponse; + #[cw_serde] pub struct UserPositionResponse { /// Total value of all enabled collateral assets. diff --git a/packages/types/src/rewards_collector.rs b/packages/types/src/rewards_collector.rs index 748e51c0a..c7b0e2d3a 100644 --- a/packages/types/src/rewards_collector.rs +++ b/packages/types/src/rewards_collector.rs @@ -6,6 +6,8 @@ use mars_utils::{ helpers::{decimal_param_le_one, integer_param_gt_zero, validate_native_denom}, }; +use self::credit_manager::Action; + const MAX_SLIPPAGE_TOLERANCE_PERCENTAGE: u64 = 50; #[cw_serde] @@ -130,6 +132,12 @@ pub enum ExecuteMsg { amount: Option, }, + /// Withdraw coins from the credit manager + WithdrawFromCreditManager { + account_id: String, + actions: Vec, + }, + /// Distribute the accrued protocol income between the safety fund and the fee modules on mars hub, /// according to the split set in config. /// Callable by any address. @@ -191,3 +199,39 @@ pub enum QueryMsg { #[returns(ConfigResponse)] Config {}, } + +// TODO: rover is private repo for now so can't use it as a dependency. Use rover types once repo is public. +pub mod credit_manager { + use cosmwasm_schema::cw_serde; + use cosmwasm_std::{Decimal, Uint128}; + + #[cw_serde] + pub enum ExecuteMsg { + UpdateCreditAccount { + account_id: String, + actions: Vec, + }, + } + + #[cw_serde] + pub enum Action { + Withdraw(ActionCoin), + WithdrawLiquidity { + lp_token: ActionCoin, + slippage: Decimal, // value validated in credit-manager + }, + Unknown {}, // Used to simulate allowance only for: Withdraw and WithdrawLiquidity + } + + #[cw_serde] + pub struct ActionCoin { + pub denom: String, + pub amount: ActionAmount, + } + + #[cw_serde] + pub enum ActionAmount { + Exact(Uint128), + AccountBalance, + } +} diff --git a/packages/utils/src/lib.rs b/packages/utils/src/lib.rs index cc73f475f..17d5321cc 100644 --- a/packages/utils/src/lib.rs +++ b/packages/utils/src/lib.rs @@ -1,3 +1,2 @@ pub mod error; pub mod helpers; -pub mod math; diff --git a/packages/utils/src/math.rs b/packages/utils/src/math.rs deleted file mode 100644 index 19c9d64ad..000000000 --- a/packages/utils/src/math.rs +++ /dev/null @@ -1,261 +0,0 @@ -use std::convert::TryInto; - -use cosmwasm_std::{ - CheckedFromRatioError, Decimal, Fraction, OverflowError, OverflowOperation, StdError, - StdResult, Uint128, Uint256, -}; - -pub fn uint128_checked_div_with_ceil( - numerator: Uint128, - denominator: Uint128, -) -> StdResult { - let mut result = numerator.checked_div(denominator)?; - - if !numerator.checked_rem(denominator)?.is_zero() { - result += Uint128::from(1_u128); - } - - Ok(result) -} - -/// Divide 'a' by 'b'. -pub fn divide_decimal_by_decimal(a: Decimal, b: Decimal) -> StdResult { - Decimal::checked_from_ratio(a.numerator(), b.numerator()).map_err(|e| match e { - CheckedFromRatioError::Overflow => StdError::overflow(OverflowError { - operation: OverflowOperation::Mul, - operand1: a.numerator().to_string(), - operand2: a.denominator().to_string(), - }), - CheckedFromRatioError::DivideByZero => { - StdError::divide_by_zero(cosmwasm_std::DivideByZeroError { - operand: b.to_string(), - }) - } - }) -} - -/// Divide Uint128 by Decimal. -/// (Uint128 / numerator / denominator) is equal to (Uint128 * denominator / numerator). -pub fn divide_uint128_by_decimal(a: Uint128, b: Decimal) -> StdResult { - // (Uint128 / numerator / denominator) is equal to (Uint128 * denominator / numerator). - let numerator_u256 = a.full_mul(b.denominator()); - let denominator_u256 = Uint256::from(b.numerator()); - - let result_u256 = numerator_u256 / denominator_u256; - - let result = result_u256.try_into()?; - Ok(result) -} - -/// Divide Uint128 by Decimal, rounding up to the nearest integer. -pub fn divide_uint128_by_decimal_and_ceil(a: Uint128, b: Decimal) -> StdResult { - // (Uint128 / numerator / denominator) is equal to (Uint128 * denominator / numerator). - let numerator_u256 = a.full_mul(b.denominator()); - let denominator_u256 = Uint256::from(b.numerator()); - - let mut result_u256 = numerator_u256 / denominator_u256; - - if numerator_u256.checked_rem(denominator_u256)? > Uint256::zero() { - result_u256 += Uint256::from(1_u32); - } - - let result = result_u256.try_into()?; - Ok(result) -} - -/// Multiply Uint128 by Decimal, rounding up to the nearest integer. -pub fn multiply_uint128_by_decimal_and_ceil(a: Uint128, b: Decimal) -> StdResult { - let numerator_u256 = a.full_mul(b.numerator()); - let denominator_u256 = Uint256::from(b.denominator()); - - let mut result_u256 = numerator_u256 / denominator_u256; - - if numerator_u256.checked_rem(denominator_u256)? > Uint256::zero() { - result_u256 += Uint256::from(1_u32); - } - - let result = result_u256.try_into()?; - Ok(result) -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use cosmwasm_std::{ConversionOverflowError, OverflowOperation}; - - use super::*; - - const DECIMAL_FRACTIONAL: Uint128 = Uint128::new(1_000_000_000_000_000_000u128); // 1*10**18 - const DECIMAL_FRACTIONAL_SQUARED: Uint128 = - Uint128::new(1_000_000_000_000_000_000_000_000_000_000_000_000u128); // (1*10**18)**2 = 1*10**36 - - #[test] - fn test_uint128_checked_div_with_ceil() { - let a = Uint128::new(120u128); - let b = Uint128::zero(); - uint128_checked_div_with_ceil(a, b).unwrap_err(); - - let a = Uint128::new(120u128); - let b = Uint128::new(60_u128); - let c = uint128_checked_div_with_ceil(a, b).unwrap(); - assert_eq!(c, Uint128::new(2u128)); - - let a = Uint128::new(120u128); - let b = Uint128::new(119_u128); - let c = uint128_checked_div_with_ceil(a, b).unwrap(); - assert_eq!(c, Uint128::new(2u128)); - - let a = Uint128::new(120u128); - let b = Uint128::new(120_u128); - let c = uint128_checked_div_with_ceil(a, b).unwrap(); - assert_eq!(c, Uint128::new(1u128)); - - let a = Uint128::new(120u128); - let b = Uint128::new(121_u128); - let c = uint128_checked_div_with_ceil(a, b).unwrap(); - assert_eq!(c, Uint128::new(1u128)); - - let a = Uint128::zero(); - let b = Uint128::new(121_u128); - let c = uint128_checked_div_with_ceil(a, b).unwrap(); - assert_eq!(c, Uint128::zero()); - } - - #[test] - fn checked_decimal_division() { - let a = Decimal::from_ratio(99988u128, 100u128); - let b = Decimal::from_ratio(24997u128, 100u128); - let c = divide_decimal_by_decimal(a, b).unwrap(); - assert_eq!(c, Decimal::from_str("4.0").unwrap()); - - let a = Decimal::from_ratio(123456789u128, 1000000u128); - let b = Decimal::from_ratio(33u128, 1u128); - let c = divide_decimal_by_decimal(a, b).unwrap(); - assert_eq!(c, Decimal::from_str("3.741114818181818181").unwrap()); - - let a = Decimal::MAX; - let b = Decimal::MAX; - let c = divide_decimal_by_decimal(a, b).unwrap(); - assert_eq!(c, Decimal::one()); - - // Note: DivideByZeroError is not public so we just check if dividing by zero returns error - let a = Decimal::one(); - let b = Decimal::zero(); - divide_decimal_by_decimal(a, b).unwrap_err(); - - let a = Decimal::MAX; - let b = Decimal::from_ratio(1u128, DECIMAL_FRACTIONAL); - let res_error = divide_decimal_by_decimal(a, b).unwrap_err(); - assert_eq!( - res_error, - OverflowError::new(OverflowOperation::Mul, Uint128::MAX, DECIMAL_FRACTIONAL).into() - ); - } - - #[test] - fn test_divide_uint128_by_decimal() { - let a = Uint128::new(120u128); - let b = Decimal::from_ratio(120u128, 15u128); - let c = divide_uint128_by_decimal(a, b).unwrap(); - assert_eq!(c, Uint128::new(15u128)); - - let a = Uint128::new(DECIMAL_FRACTIONAL.u128()); - let b = Decimal::from_ratio(DECIMAL_FRACTIONAL.u128(), 1u128); - let c = divide_uint128_by_decimal(a, b).unwrap(); - assert_eq!(c, Uint128::new(1u128)); - - let a = Uint128::new(DECIMAL_FRACTIONAL.u128()); - let b = Decimal::from_ratio(1u128, DECIMAL_FRACTIONAL.u128()); - let c = divide_uint128_by_decimal(a, b).unwrap(); - assert_eq!(c, Uint128::new(DECIMAL_FRACTIONAL_SQUARED.u128())); - - let a = Uint128::MAX; - let b = Decimal::one(); - let c = divide_uint128_by_decimal(a, b).unwrap(); - assert_eq!(c, Uint128::MAX); - - let a = Uint128::new(1_000_000_000_000_000_000); - let b = Decimal::from_ratio(1u128, DECIMAL_FRACTIONAL); - let c = divide_uint128_by_decimal(a, b).unwrap(); - assert_eq!(c, Uint128::new(1_000_000_000_000_000_000_000_000_000_000_000_000)); - - // Division is truncated - let a = Uint128::new(100); - let b = Decimal::from_ratio(3u128, 1u128); - let c = divide_uint128_by_decimal(a, b).unwrap(); - assert_eq!(c, Uint128::new(33)); - - let a = Uint128::new(75); - let b = Decimal::from_ratio(100u128, 1u128); - let c = divide_uint128_by_decimal(a, b).unwrap(); - assert_eq!(c, Uint128::new(0)); - - // Overflow - let a = Uint128::MAX; - let b = Decimal::from_ratio(1_u128, 10_u128); - let res_error = divide_uint128_by_decimal(a, b).unwrap_err(); - assert_eq!( - res_error, - ConversionOverflowError::new( - "Uint256", - "Uint128", - "3402823669209384634633746074317682114550" - ) - .into() - ); - } - - #[test] - fn test_divide_uint128_by_decimal_and_ceil() { - let a = Uint128::new(120u128); - let b = Decimal::from_ratio(120u128, 15u128); - let c = divide_uint128_by_decimal_and_ceil(a, b).unwrap(); - assert_eq!(c, Uint128::new(15u128)); - - let a = Uint128::new(DECIMAL_FRACTIONAL.u128()); - let b = Decimal::from_ratio(DECIMAL_FRACTIONAL.u128(), 1u128); - let c = divide_uint128_by_decimal_and_ceil(a, b).unwrap(); - assert_eq!(c, Uint128::new(1u128)); - - let a = Uint128::new(DECIMAL_FRACTIONAL.u128()); - let b = Decimal::from_ratio(1u128, DECIMAL_FRACTIONAL.u128()); - let c = divide_uint128_by_decimal_and_ceil(a, b).unwrap(); - assert_eq!(c, Uint128::new(DECIMAL_FRACTIONAL_SQUARED.u128())); - - let a = Uint128::MAX; - let b = Decimal::one(); - let c = divide_uint128_by_decimal_and_ceil(a, b).unwrap(); - assert_eq!(c, Uint128::MAX); - - let a = Uint128::new(1_000_000_000_000_000_000); - let b = Decimal::from_ratio(1u128, DECIMAL_FRACTIONAL); - let c = divide_uint128_by_decimal_and_ceil(a, b).unwrap(); - assert_eq!(c, Uint128::new(1_000_000_000_000_000_000_000_000_000_000_000_000)); - - // Division is rounded up - let a = Uint128::new(100); - let b = Decimal::from_ratio(3u128, 1u128); - let c = divide_uint128_by_decimal_and_ceil(a, b).unwrap(); - assert_eq!(c, Uint128::new(34)); - - let a = Uint128::new(75); - let b = Decimal::from_ratio(100u128, 1u128); - let c = divide_uint128_by_decimal_and_ceil(a, b).unwrap(); - assert_eq!(c, Uint128::new(1)); - - // Overflow - let a = Uint128::MAX; - let b = Decimal::from_ratio(1_u128, 10_u128); - let res_error = divide_uint128_by_decimal_and_ceil(a, b).unwrap_err(); - assert_eq!( - res_error, - ConversionOverflowError::new( - "Uint256", - "Uint128", - "3402823669209384634633746074317682114550" - ) - .into() - ); - } -} diff --git a/schema.Makefile.toml b/schema.Makefile.toml index e52c006e8..ae6f07438 100644 --- a/schema.Makefile.toml +++ b/schema.Makefile.toml @@ -15,6 +15,7 @@ fn main() -> std::io::Result<()> { "mars-incentives", "mars-red-bank", "mars-rewards-collector-base", + "mars-params", "mars-swapper-osmosis", "mars-swapper-astroport", "mars-oracle-osmosis", diff --git a/schemas/mars-address-provider/mars-address-provider.json b/schemas/mars-address-provider/mars-address-provider.json index 5ea2304a6..a8ce16fdd 100644 --- a/schemas/mars-address-provider/mars-address-provider.json +++ b/schemas/mars-address-provider/mars-address-provider.json @@ -1,6 +1,6 @@ { "contract_name": "mars-address-provider", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -76,7 +76,9 @@ "incentives", "oracle", "red_bank", - "rewards_collector" + "rewards_collector", + "params", + "credit_manager" ] }, { @@ -278,7 +280,9 @@ "incentives", "oracle", "red_bank", - "rewards_collector" + "rewards_collector", + "params", + "credit_manager" ] }, { @@ -348,7 +352,9 @@ "incentives", "oracle", "red_bank", - "rewards_collector" + "rewards_collector", + "params", + "credit_manager" ] }, { @@ -421,7 +427,9 @@ "incentives", "oracle", "red_bank", - "rewards_collector" + "rewards_collector", + "params", + "credit_manager" ] }, { @@ -494,7 +502,9 @@ "incentives", "oracle", "red_bank", - "rewards_collector" + "rewards_collector", + "params", + "credit_manager" ] }, { diff --git a/schemas/mars-incentives/mars-incentives.json b/schemas/mars-incentives/mars-incentives.json index 7ef20e6f0..d2d6badd1 100644 --- a/schemas/mars-incentives/mars-incentives.json +++ b/schemas/mars-incentives/mars-incentives.json @@ -1,6 +1,6 @@ { "contract_name": "mars-incentives", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -141,6 +141,13 @@ "user_amount_scaled_before" ], "properties": { + "account_id": { + "description": "Credit account id (Rover)", + "type": [ + "string", + "null" + ] + }, "denom": { "description": "Denom of the asset of which deposited balance is changed", "type": "string" @@ -185,6 +192,13 @@ "claim_rewards": { "type": "object", "properties": { + "account_id": { + "description": "Credit account id (Rover)", + "type": [ + "string", + "null" + ] + }, "limit": { "description": "The maximum number of results to return. If not set, 5 is used. If larger than 10, 10 is used.", "type": [ @@ -571,6 +585,13 @@ "user" ], "properties": { + "account_id": { + "description": "Credit account id (Rover)", + "type": [ + "string", + "null" + ] + }, "limit": { "description": "The maximum number of results to return. If not set, 5 is used. If larger than 10, 10 is used.", "type": [ @@ -667,7 +688,8 @@ "required": [ "address_provider", "epoch_duration", - "max_whitelisted_denoms" + "max_whitelisted_denoms", + "whitelist_count" ], "properties": { "address_provider": { @@ -703,6 +725,12 @@ "string", "null" ] + }, + "whitelist_count": { + "description": "The count of the number of whitelisted incentive denoms", + "type": "integer", + "format": "uint8", + "minimum": 0.0 } }, "additionalProperties": false, diff --git a/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json b/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json index d221cbbc0..a6324eacb 100644 --- a/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json +++ b/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json @@ -1,6 +1,6 @@ { "contract_name": "mars-oracle-osmosis", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -447,6 +447,8 @@ "required": [ "contract_addr", "denom_decimals", + "max_confidence", + "max_deviation", "max_staleness", "price_feed_id" ], @@ -461,6 +463,22 @@ "format": "uint8", "minimum": 0.0 }, + "max_confidence": { + "description": "The maximum confidence deviation allowed for an oracle price.\n\nThe confidence is measured as the percent of the confidence interval value provided by the oracle as compared to the weighted average value of the price.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "max_deviation": { + "description": "The maximum deviation (percentage) between current and EMA price", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, "max_staleness": { "description": "The maximum number of seconds since the last price was by an oracle, before rejecting the price as too stale", "type": "integer", @@ -704,6 +722,16 @@ "properties": { "denom": { "type": "string" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/definitions/ActionKind" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false @@ -721,6 +749,16 @@ "prices": { "type": "object", "properties": { + "kind": { + "anyOf": [ + { + "$ref": "#/definitions/ActionKind" + }, + { + "type": "null" + } + ] + }, "limit": { "type": [ "integer", @@ -741,7 +779,17 @@ }, "additionalProperties": false } - ] + ], + "definitions": { + "ActionKind": { + "description": "Differentiator for the action (liquidate, withdraw, borrow etc.) being performed.", + "type": "string", + "enum": [ + "default", + "liquidation" + ] + } + } }, "migrate": null, "sudo": null, diff --git a/schemas/mars-oracle-wasm/mars-oracle-wasm.json b/schemas/mars-oracle-wasm/mars-oracle-wasm.json index fb7bfaf98..c5e3474dd 100644 --- a/schemas/mars-oracle-wasm/mars-oracle-wasm.json +++ b/schemas/mars-oracle-wasm/mars-oracle-wasm.json @@ -1,6 +1,6 @@ { "contract_name": "mars-oracle-wasm", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -356,6 +356,8 @@ "required": [ "contract_addr", "denom_decimals", + "max_confidence", + "max_deviation", "max_staleness", "price_feed_id" ], @@ -370,6 +372,22 @@ "format": "uint8", "minimum": 0.0 }, + "max_confidence": { + "description": "The maximum confidence deviation allowed for an oracle price.\n\nThe confidence is measured as the percent of the confidence interval value provided by the oracle as compared to the weighted average value of the price.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "max_deviation": { + "description": "The maximum deviation (percentage) between current and EMA price", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, "max_staleness": { "description": "The maximum number of seconds since the last price was by an oracle, before rejecting the price as too stale", "type": "integer", @@ -377,7 +395,7 @@ "minimum": 0.0 }, "price_feed_id": { - "description": "Price feed id of an asset from the list: https://pyth.network/developers/price-feed-ids", + "description": "Price feed id of an asset from the list: https://pyth.network/developers/price-feed-ids We can't verify what denoms consist of the price feed. Be very careful when adding it !!!", "allOf": [ { "$ref": "#/definitions/Identifier" @@ -479,6 +497,16 @@ "properties": { "denom": { "type": "string" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/definitions/ActionKind" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false @@ -496,6 +524,16 @@ "prices": { "type": "object", "properties": { + "kind": { + "anyOf": [ + { + "$ref": "#/definitions/ActionKind" + }, + { + "type": "null" + } + ] + }, "limit": { "type": [ "integer", @@ -516,7 +554,17 @@ }, "additionalProperties": false } - ] + ], + "definitions": { + "ActionKind": { + "description": "Differentiator for the action (liquidate, withdraw, borrow etc.) being performed.", + "type": "string", + "enum": [ + "default", + "liquidation" + ] + } + } }, "migrate": null, "sudo": null, diff --git a/schemas/mars-params/mars-params.json b/schemas/mars-params/mars-params.json new file mode 100644 index 000000000..2350dddbc --- /dev/null +++ b/schemas/mars-params/mars-params.json @@ -0,0 +1,1510 @@ +{ + "contract_name": "mars-params", + "contract_version": "2.0.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "address_provider", + "owner", + "target_health_factor" + ], + "properties": { + "address_provider": { + "description": "Address of the address provider contract", + "type": "string" + }, + "owner": { + "description": "Contract's owner", + "type": "string" + }, + "target_health_factor": { + "description": "Determines the ideal HF a position should be left at immediately after the position has been liquidated.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "update_owner" + ], + "properties": { + "update_owner": { + "$ref": "#/definitions/OwnerUpdate" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_target_health_factor" + ], + "properties": { + "update_target_health_factor": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_asset_params" + ], + "properties": { + "update_asset_params": { + "$ref": "#/definitions/AssetParamsUpdate" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_vault_config" + ], + "properties": { + "update_vault_config": { + "$ref": "#/definitions/VaultConfigUpdate" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "emergency_update" + ], + "properties": { + "emergency_update": { + "$ref": "#/definitions/EmergencyUpdate" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "AssetParamsBase_for_String": { + "type": "object", + "required": [ + "credit_manager", + "denom", + "deposit_cap", + "liquidation_bonus", + "liquidation_threshold", + "max_loan_to_value", + "protocol_liquidation_fee", + "red_bank" + ], + "properties": { + "credit_manager": { + "$ref": "#/definitions/CmSettings_for_String" + }, + "denom": { + "type": "string" + }, + "deposit_cap": { + "$ref": "#/definitions/Uint128" + }, + "liquidation_bonus": { + "$ref": "#/definitions/LiquidationBonus" + }, + "liquidation_threshold": { + "$ref": "#/definitions/Decimal" + }, + "max_loan_to_value": { + "$ref": "#/definitions/Decimal" + }, + "protocol_liquidation_fee": { + "$ref": "#/definitions/Decimal" + }, + "red_bank": { + "$ref": "#/definitions/RedBankSettings" + } + }, + "additionalProperties": false + }, + "AssetParamsUpdate": { + "oneOf": [ + { + "type": "object", + "required": [ + "add_or_update" + ], + "properties": { + "add_or_update": { + "type": "object", + "required": [ + "params" + ], + "properties": { + "params": { + "$ref": "#/definitions/AssetParamsBase_for_String" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "CmEmergencyUpdate": { + "oneOf": [ + { + "type": "object", + "required": [ + "set_zero_max_ltv_on_vault" + ], + "properties": { + "set_zero_max_ltv_on_vault": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "set_zero_deposit_cap_on_vault" + ], + "properties": { + "set_zero_deposit_cap_on_vault": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "disallow_coin" + ], + "properties": { + "disallow_coin": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + }, + "CmSettings_for_String": { + "type": "object", + "required": [ + "whitelisted" + ], + "properties": { + "hls": { + "anyOf": [ + { + "$ref": "#/definitions/HlsParamsBase_for_String" + }, + { + "type": "null" + } + ] + }, + "whitelisted": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "EmergencyUpdate": { + "oneOf": [ + { + "type": "object", + "required": [ + "credit_manager" + ], + "properties": { + "credit_manager": { + "$ref": "#/definitions/CmEmergencyUpdate" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "red_bank" + ], + "properties": { + "red_bank": { + "$ref": "#/definitions/RedBankEmergencyUpdate" + } + }, + "additionalProperties": false + } + ] + }, + "HlsAssetType_for_String": { + "oneOf": [ + { + "type": "object", + "required": [ + "coin" + ], + "properties": { + "coin": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "vault" + ], + "properties": { + "vault": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "HlsParamsBase_for_String": { + "type": "object", + "required": [ + "correlations", + "liquidation_threshold", + "max_loan_to_value" + ], + "properties": { + "correlations": { + "description": "Given this asset is debt, correlations are the only allowed collateral which are permitted to fulfill the HLS strategy", + "type": "array", + "items": { + "$ref": "#/definitions/HlsAssetType_for_String" + } + }, + "liquidation_threshold": { + "$ref": "#/definitions/Decimal" + }, + "max_loan_to_value": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + }, + "LiquidationBonus": { + "description": "The LB will depend on the Health Factor and a couple other parameters as follows: Liquidation Bonus = min( b + (slope * (1 - HF)), max( min(CR - 1, max_lb), min_lb ) )", + "type": "object", + "required": [ + "max_lb", + "min_lb", + "slope", + "starting_lb" + ], + "properties": { + "max_lb": { + "description": "Maximum LB that can be granted to a liquidator; in other words, the maxLB establishes a ceiling to the LB. This is a precautionary parameter to mitigate liquidated users being over-punished.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "min_lb": { + "description": "Minimum LB that will be granted to liquidators even when the position is undercollateralized.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "slope": { + "description": "Defines the slope at which the LB increases as the HF decreases. The higher the slope, the faster the LB increases as the HF decreases.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "starting_lb": { + "description": "Marks the level at which the LB starts when HF drops marginally below 1. If set at 1%, at HF = 0.999 the LB will be 1%. If set at 0%, the LB starts increasing from 0% as the HF drops below 1.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "OwnerUpdate": { + "oneOf": [ + { + "description": "Proposes a new owner to take role. Only current owner can execute.", + "type": "object", + "required": [ + "propose_new_owner" + ], + "properties": { + "propose_new_owner": { + "type": "object", + "required": [ + "proposed" + ], + "properties": { + "proposed": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Clears the currently proposed owner. Only current owner can execute.", + "type": "string", + "enum": [ + "clear_proposed" + ] + }, + { + "description": "Promotes the proposed owner to be the current one. Only the proposed owner can execute.", + "type": "string", + "enum": [ + "accept_proposed" + ] + }, + { + "description": "Throws away the keys to the Owner role forever. Once done, no owner can ever be set later.", + "type": "string", + "enum": [ + "abolish_owner_role" + ] + }, + { + "description": "A separate entity managed by Owner that can be used for granting specific emergency powers.", + "type": "object", + "required": [ + "set_emergency_owner" + ], + "properties": { + "set_emergency_owner": { + "type": "object", + "required": [ + "emergency_owner" + ], + "properties": { + "emergency_owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Remove the entity in the Emergency Owner role", + "type": "string", + "enum": [ + "clear_emergency_owner" + ] + } + ] + }, + "RedBankEmergencyUpdate": { + "oneOf": [ + { + "type": "object", + "required": [ + "disable_borrowing" + ], + "properties": { + "disable_borrowing": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + }, + "RedBankSettings": { + "type": "object", + "required": [ + "borrow_enabled", + "deposit_enabled" + ], + "properties": { + "borrow_enabled": { + "type": "boolean" + }, + "deposit_enabled": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "VaultConfigBase_for_String": { + "type": "object", + "required": [ + "addr", + "deposit_cap", + "liquidation_threshold", + "max_loan_to_value", + "whitelisted" + ], + "properties": { + "addr": { + "type": "string" + }, + "deposit_cap": { + "$ref": "#/definitions/Coin" + }, + "hls": { + "anyOf": [ + { + "$ref": "#/definitions/HlsParamsBase_for_String" + }, + { + "type": "null" + } + ] + }, + "liquidation_threshold": { + "$ref": "#/definitions/Decimal" + }, + "max_loan_to_value": { + "$ref": "#/definitions/Decimal" + }, + "whitelisted": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "VaultConfigUpdate": { + "oneOf": [ + { + "type": "object", + "required": [ + "add_or_update" + ], + "properties": { + "add_or_update": { + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "$ref": "#/definitions/VaultConfigBase_for_String" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "owner": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "asset_params" + ], + "properties": { + "asset_params": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "all_asset_params" + ], + "properties": { + "all_asset_params": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "vault_config" + ], + "properties": { + "vault_config": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "description": "Address of vault", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "all_vault_configs" + ], + "properties": { + "all_vault_configs": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "target_health_factor" + ], + "properties": { + "target_health_factor": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Compute the total amount deposited of the given asset across Red Bank and Credit Manager.", + "type": "object", + "required": [ + "total_deposit" + ], + "properties": { + "total_deposit": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "migrate": null, + "sudo": null, + "responses": { + "all_asset_params": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_AssetParamsBase_for_Addr", + "type": "array", + "items": { + "$ref": "#/definitions/AssetParamsBase_for_Addr" + }, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "AssetParamsBase_for_Addr": { + "type": "object", + "required": [ + "credit_manager", + "denom", + "deposit_cap", + "liquidation_bonus", + "liquidation_threshold", + "max_loan_to_value", + "protocol_liquidation_fee", + "red_bank" + ], + "properties": { + "credit_manager": { + "$ref": "#/definitions/CmSettings_for_Addr" + }, + "denom": { + "type": "string" + }, + "deposit_cap": { + "$ref": "#/definitions/Uint128" + }, + "liquidation_bonus": { + "$ref": "#/definitions/LiquidationBonus" + }, + "liquidation_threshold": { + "$ref": "#/definitions/Decimal" + }, + "max_loan_to_value": { + "$ref": "#/definitions/Decimal" + }, + "protocol_liquidation_fee": { + "$ref": "#/definitions/Decimal" + }, + "red_bank": { + "$ref": "#/definitions/RedBankSettings" + } + }, + "additionalProperties": false + }, + "CmSettings_for_Addr": { + "type": "object", + "required": [ + "whitelisted" + ], + "properties": { + "hls": { + "anyOf": [ + { + "$ref": "#/definitions/HlsParamsBase_for_Addr" + }, + { + "type": "null" + } + ] + }, + "whitelisted": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "HlsAssetType_for_Addr": { + "oneOf": [ + { + "type": "object", + "required": [ + "coin" + ], + "properties": { + "coin": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "vault" + ], + "properties": { + "vault": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "HlsParamsBase_for_Addr": { + "type": "object", + "required": [ + "correlations", + "liquidation_threshold", + "max_loan_to_value" + ], + "properties": { + "correlations": { + "description": "Given this asset is debt, correlations are the only allowed collateral which are permitted to fulfill the HLS strategy", + "type": "array", + "items": { + "$ref": "#/definitions/HlsAssetType_for_Addr" + } + }, + "liquidation_threshold": { + "$ref": "#/definitions/Decimal" + }, + "max_loan_to_value": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + }, + "LiquidationBonus": { + "description": "The LB will depend on the Health Factor and a couple other parameters as follows: Liquidation Bonus = min( b + (slope * (1 - HF)), max( min(CR - 1, max_lb), min_lb ) )", + "type": "object", + "required": [ + "max_lb", + "min_lb", + "slope", + "starting_lb" + ], + "properties": { + "max_lb": { + "description": "Maximum LB that can be granted to a liquidator; in other words, the maxLB establishes a ceiling to the LB. This is a precautionary parameter to mitigate liquidated users being over-punished.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "min_lb": { + "description": "Minimum LB that will be granted to liquidators even when the position is undercollateralized.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "slope": { + "description": "Defines the slope at which the LB increases as the HF decreases. The higher the slope, the faster the LB increases as the HF decreases.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "starting_lb": { + "description": "Marks the level at which the LB starts when HF drops marginally below 1. If set at 1%, at HF = 0.999 the LB will be 1%. If set at 0%, the LB starts increasing from 0% as the HF drops below 1.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "RedBankSettings": { + "type": "object", + "required": [ + "borrow_enabled", + "deposit_enabled" + ], + "properties": { + "borrow_enabled": { + "type": "boolean" + }, + "deposit_enabled": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "all_vault_configs": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_VaultConfigBase_for_Addr", + "type": "array", + "items": { + "$ref": "#/definitions/VaultConfigBase_for_Addr" + }, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "HlsAssetType_for_Addr": { + "oneOf": [ + { + "type": "object", + "required": [ + "coin" + ], + "properties": { + "coin": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "vault" + ], + "properties": { + "vault": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "HlsParamsBase_for_Addr": { + "type": "object", + "required": [ + "correlations", + "liquidation_threshold", + "max_loan_to_value" + ], + "properties": { + "correlations": { + "description": "Given this asset is debt, correlations are the only allowed collateral which are permitted to fulfill the HLS strategy", + "type": "array", + "items": { + "$ref": "#/definitions/HlsAssetType_for_Addr" + } + }, + "liquidation_threshold": { + "$ref": "#/definitions/Decimal" + }, + "max_loan_to_value": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "VaultConfigBase_for_Addr": { + "type": "object", + "required": [ + "addr", + "deposit_cap", + "liquidation_threshold", + "max_loan_to_value", + "whitelisted" + ], + "properties": { + "addr": { + "$ref": "#/definitions/Addr" + }, + "deposit_cap": { + "$ref": "#/definitions/Coin" + }, + "hls": { + "anyOf": [ + { + "$ref": "#/definitions/HlsParamsBase_for_Addr" + }, + { + "type": "null" + } + ] + }, + "liquidation_threshold": { + "$ref": "#/definitions/Decimal" + }, + "max_loan_to_value": { + "$ref": "#/definitions/Decimal" + }, + "whitelisted": { + "type": "boolean" + } + }, + "additionalProperties": false + } + } + }, + "asset_params": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AssetParamsBase_for_Addr", + "type": "object", + "required": [ + "credit_manager", + "denom", + "deposit_cap", + "liquidation_bonus", + "liquidation_threshold", + "max_loan_to_value", + "protocol_liquidation_fee", + "red_bank" + ], + "properties": { + "credit_manager": { + "$ref": "#/definitions/CmSettings_for_Addr" + }, + "denom": { + "type": "string" + }, + "deposit_cap": { + "$ref": "#/definitions/Uint128" + }, + "liquidation_bonus": { + "$ref": "#/definitions/LiquidationBonus" + }, + "liquidation_threshold": { + "$ref": "#/definitions/Decimal" + }, + "max_loan_to_value": { + "$ref": "#/definitions/Decimal" + }, + "protocol_liquidation_fee": { + "$ref": "#/definitions/Decimal" + }, + "red_bank": { + "$ref": "#/definitions/RedBankSettings" + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "CmSettings_for_Addr": { + "type": "object", + "required": [ + "whitelisted" + ], + "properties": { + "hls": { + "anyOf": [ + { + "$ref": "#/definitions/HlsParamsBase_for_Addr" + }, + { + "type": "null" + } + ] + }, + "whitelisted": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "HlsAssetType_for_Addr": { + "oneOf": [ + { + "type": "object", + "required": [ + "coin" + ], + "properties": { + "coin": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "vault" + ], + "properties": { + "vault": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "HlsParamsBase_for_Addr": { + "type": "object", + "required": [ + "correlations", + "liquidation_threshold", + "max_loan_to_value" + ], + "properties": { + "correlations": { + "description": "Given this asset is debt, correlations are the only allowed collateral which are permitted to fulfill the HLS strategy", + "type": "array", + "items": { + "$ref": "#/definitions/HlsAssetType_for_Addr" + } + }, + "liquidation_threshold": { + "$ref": "#/definitions/Decimal" + }, + "max_loan_to_value": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + }, + "LiquidationBonus": { + "description": "The LB will depend on the Health Factor and a couple other parameters as follows: Liquidation Bonus = min( b + (slope * (1 - HF)), max( min(CR - 1, max_lb), min_lb ) )", + "type": "object", + "required": [ + "max_lb", + "min_lb", + "slope", + "starting_lb" + ], + "properties": { + "max_lb": { + "description": "Maximum LB that can be granted to a liquidator; in other words, the maxLB establishes a ceiling to the LB. This is a precautionary parameter to mitigate liquidated users being over-punished.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "min_lb": { + "description": "Minimum LB that will be granted to liquidators even when the position is undercollateralized.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "slope": { + "description": "Defines the slope at which the LB increases as the HF decreases. The higher the slope, the faster the LB increases as the HF decreases.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "starting_lb": { + "description": "Marks the level at which the LB starts when HF drops marginally below 1. If set at 1%, at HF = 0.999 the LB will be 1%. If set at 0%, the LB starts increasing from 0% as the HF drops below 1.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "RedBankSettings": { + "type": "object", + "required": [ + "borrow_enabled", + "deposit_enabled" + ], + "properties": { + "borrow_enabled": { + "type": "boolean" + }, + "deposit_enabled": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "owner": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OwnerResponse", + "description": "Returned from Owner.query()", + "type": "object", + "required": [ + "abolished", + "initialized" + ], + "properties": { + "abolished": { + "type": "boolean" + }, + "emergency_owner": { + "type": [ + "string", + "null" + ] + }, + "initialized": { + "type": "boolean" + }, + "owner": { + "type": [ + "string", + "null" + ] + }, + "proposed": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "target_health_factor": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Decimal", + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "total_deposit": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TotalDepositResponse", + "type": "object", + "required": [ + "amount", + "cap", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "cap": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "vault_config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "VaultConfigBase_for_Addr", + "type": "object", + "required": [ + "addr", + "deposit_cap", + "liquidation_threshold", + "max_loan_to_value", + "whitelisted" + ], + "properties": { + "addr": { + "$ref": "#/definitions/Addr" + }, + "deposit_cap": { + "$ref": "#/definitions/Coin" + }, + "hls": { + "anyOf": [ + { + "$ref": "#/definitions/HlsParamsBase_for_Addr" + }, + { + "type": "null" + } + ] + }, + "liquidation_threshold": { + "$ref": "#/definitions/Decimal" + }, + "max_loan_to_value": { + "$ref": "#/definitions/Decimal" + }, + "whitelisted": { + "type": "boolean" + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "HlsAssetType_for_Addr": { + "oneOf": [ + { + "type": "object", + "required": [ + "coin" + ], + "properties": { + "coin": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "vault" + ], + "properties": { + "vault": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "HlsParamsBase_for_Addr": { + "type": "object", + "required": [ + "correlations", + "liquidation_threshold", + "max_loan_to_value" + ], + "properties": { + "correlations": { + "description": "Given this asset is debt, correlations are the only allowed collateral which are permitted to fulfill the HLS strategy", + "type": "array", + "items": { + "$ref": "#/definitions/HlsAssetType_for_Addr" + } + }, + "liquidation_threshold": { + "$ref": "#/definitions/Decimal" + }, + "max_loan_to_value": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + } + } +} diff --git a/schemas/mars-red-bank/mars-red-bank.json b/schemas/mars-red-bank/mars-red-bank.json index 5f07b26ca..76cb7ca91 100644 --- a/schemas/mars-red-bank/mars-red-bank.json +++ b/schemas/mars-red-bank/mars-red-bank.json @@ -1,6 +1,6 @@ { "contract_name": "mars-red-bank", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -34,23 +34,9 @@ "string", "null" ] - }, - "close_factor": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] } }, "additionalProperties": false - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" } } }, @@ -204,6 +190,13 @@ "deposit": { "type": "object", "properties": { + "account_id": { + "description": "Credit account id (Rover)", + "type": [ + "string", + "null" + ] + }, "on_behalf_of": { "description": "Address that will receive the coins", "type": [ @@ -230,6 +223,13 @@ "denom" ], "properties": { + "account_id": { + "description": "Credit account id (Rover)", + "type": [ + "string", + "null" + ] + }, "amount": { "description": "Amount to be withdrawn. If None is specified, the full amount will be withdrawn.", "anyOf": [ @@ -245,6 +245,12 @@ "description": "Asset to withdraw", "type": "string" }, + "liquidation_related": { + "type": [ + "boolean", + "null" + ] + }, "recipient": { "description": "The address where the withdrawn amount is sent", "type": [ @@ -393,16 +399,6 @@ "string", "null" ] - }, - "close_factor": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] } }, "additionalProperties": false @@ -414,31 +410,6 @@ "InitOrUpdateAssetParams": { "type": "object", "properties": { - "borrow_enabled": { - "description": "If false cannot borrow", - "type": [ - "boolean", - "null" - ] - }, - "deposit_cap": { - "description": "Deposit Cap defined in terms of the asset (Unlimited by default)", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "deposit_enabled": { - "description": "If false cannot deposit", - "type": [ - "boolean", - "null" - ] - }, "interest_rate_model": { "description": "Interest rate strategy to calculate borrow_rate and liquidity_rate", "anyOf": [ @@ -450,39 +421,6 @@ } ] }, - "liquidation_bonus": { - "description": "Bonus amount of collateral liquidator get when repaying user's debt (Will get collateral from user in an amount equal to debt repayed + bonus)", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "liquidation_threshold": { - "description": "uusd amount in debt position per uusd of asset collateral that if surpassed makes the user's position liquidatable.", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "max_loan_to_value": { - "description": "Max uusd that can be borrowed per uusd of collateral when using the asset as collateral", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, "reserve_factor": { "description": "Portion of the borrow rate that is kept as protocol rewards", "anyOf": [ @@ -523,7 +461,7 @@ ] }, "slope_1": { - "description": "Slope parameter for interest rate model function when utilization_rate < optimal_utilization_rate", + "description": "Slope parameter for interest rate model function when utilization_rate <= optimal_utilization_rate", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -531,7 +469,7 @@ ] }, "slope_2": { - "description": "Slope parameter for interest rate model function when utilization_rate >= optimal_utilization_rate", + "description": "Slope parameter for interest rate model function when utilization_rate > optimal_utilization_rate", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -831,6 +769,12 @@ "user" ], "properties": { + "account_id": { + "type": [ + "string", + "null" + ] + }, "denom": { "type": "string" }, @@ -856,6 +800,54 @@ "user" ], "properties": { + "account_id": { + "type": [ + "string", + "null" + ] + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + }, + "user": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Get all collateral positions for a user", + "type": "object", + "required": [ + "user_collaterals_v2" + ], + "properties": { + "user_collaterals_v2": { + "type": "object", + "required": [ + "user" + ], + "properties": { + "account_id": { + "type": [ + "string", + "null" + ] + }, "limit": { "type": [ "integer", @@ -892,6 +884,40 @@ "user" ], "properties": { + "account_id": { + "type": [ + "string", + "null" + ] + }, + "user": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Get user position for liquidation", + "type": "object", + "required": [ + "user_position_liquidation_pricing" + ], + "properties": { + "user_position_liquidation_pricing": { + "type": "object", + "required": [ + "user" + ], + "properties": { + "account_id": { + "type": [ + "string", + "null" + ] + }, "user": { "type": "string" } @@ -1021,29 +1047,13 @@ "title": "ConfigResponse", "type": "object", "required": [ - "address_provider", - "close_factor" + "address_provider" ], "properties": { "address_provider": { "description": "Address provider returns addresses for all protocol contracts", "type": "string" }, - "close_factor": { - "description": "Maximum percentage of outstanding debt that can be covered by a liquidator", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, - "emergency_owner": { - "description": "The contract's emergency owner", - "type": [ - "string", - "null" - ] - }, "owner": { "description": "The contract's owner", "type": [ @@ -1059,41 +1069,25 @@ ] } }, - "additionalProperties": false, - "definitions": { - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - } - } + "additionalProperties": false }, "market": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Market", "type": "object", "required": [ - "borrow_enabled", "borrow_index", "borrow_rate", "collateral_total_scaled", "debt_total_scaled", "denom", - "deposit_cap", - "deposit_enabled", "indexes_last_updated", "interest_rate_model", - "liquidation_bonus", - "liquidation_threshold", "liquidity_index", "liquidity_rate", - "max_loan_to_value", "reserve_factor" ], "properties": { - "borrow_enabled": { - "description": "If false cannot borrow", - "type": "boolean" - }, "borrow_index": { "description": "Borrow index (Used to compute borrow interest)", "allOf": [ @@ -1130,18 +1124,6 @@ "description": "Denom of the asset", "type": "string" }, - "deposit_cap": { - "description": "Deposit Cap (defined in terms of the asset)", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "deposit_enabled": { - "description": "If false cannot deposit", - "type": "boolean" - }, "indexes_last_updated": { "description": "Timestamp (seconds) where indexes and where last updated", "type": "integer", @@ -1156,22 +1138,6 @@ } ] }, - "liquidation_bonus": { - "description": "Bonus amount of collateral liquidator get when repaying user's debt (Will get collateral from user in an amount equal to debt repayed + bonus)", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, - "liquidation_threshold": { - "description": "Base asset amount in debt position per \"base asset\" of asset collateral that if surpassed makes the user's position liquidatable.", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, "liquidity_index": { "description": "Liquidity index (Used to compute deposit interest)", "allOf": [ @@ -1188,14 +1154,6 @@ } ] }, - "max_loan_to_value": { - "description": "Max base asset that can be borrowed per \"base asset\" collateral when using the asset as collateral", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, "reserve_factor": { "description": "Portion of the borrow rate that is kept as protocol rewards", "allOf": [ @@ -1237,7 +1195,7 @@ ] }, "slope_1": { - "description": "Slope parameter for interest rate model function when utilization_rate < optimal_utilization_rate", + "description": "Slope parameter for interest rate model function when utilization_rate <= optimal_utilization_rate", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -1245,7 +1203,7 @@ ] }, "slope_2": { - "description": "Slope parameter for interest rate model function when utilization_rate >= optimal_utilization_rate", + "description": "Slope parameter for interest rate model function when utilization_rate > optimal_utilization_rate", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -1299,7 +1257,7 @@ ] }, "slope_1": { - "description": "Slope parameter for interest rate model function when utilization_rate < optimal_utilization_rate", + "description": "Slope parameter for interest rate model function when utilization_rate <= optimal_utilization_rate", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -1307,7 +1265,7 @@ ] }, "slope_2": { - "description": "Slope parameter for interest rate model function when utilization_rate >= optimal_utilization_rate", + "description": "Slope parameter for interest rate model function when utilization_rate > optimal_utilization_rate", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -1320,28 +1278,18 @@ "Market": { "type": "object", "required": [ - "borrow_enabled", "borrow_index", "borrow_rate", "collateral_total_scaled", "debt_total_scaled", "denom", - "deposit_cap", - "deposit_enabled", "indexes_last_updated", "interest_rate_model", - "liquidation_bonus", - "liquidation_threshold", "liquidity_index", "liquidity_rate", - "max_loan_to_value", "reserve_factor" ], "properties": { - "borrow_enabled": { - "description": "If false cannot borrow", - "type": "boolean" - }, "borrow_index": { "description": "Borrow index (Used to compute borrow interest)", "allOf": [ @@ -1378,18 +1326,6 @@ "description": "Denom of the asset", "type": "string" }, - "deposit_cap": { - "description": "Deposit Cap (defined in terms of the asset)", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "deposit_enabled": { - "description": "If false cannot deposit", - "type": "boolean" - }, "indexes_last_updated": { "description": "Timestamp (seconds) where indexes and where last updated", "type": "integer", @@ -1404,22 +1340,6 @@ } ] }, - "liquidation_bonus": { - "description": "Bonus amount of collateral liquidator get when repaying user's debt (Will get collateral from user in an amount equal to debt repayed + bonus)", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, - "liquidation_threshold": { - "description": "Base asset amount in debt position per \"base asset\" of asset collateral that if surpassed makes the user's position liquidatable.", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, "liquidity_index": { "description": "Liquidity index (Used to compute deposit interest)", "allOf": [ @@ -1436,14 +1356,6 @@ } ] }, - "max_loan_to_value": { - "description": "Max base asset that can be borrowed per \"base asset\" collateral when using the asset as collateral", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, "reserve_factor": { "description": "Portion of the borrow rate that is kept as protocol rewards", "allOf": [ @@ -1645,6 +1557,81 @@ } } }, + "user_collaterals_v2": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PaginationResponse_for_UserCollateralResponse", + "type": "object", + "required": [ + "data", + "metadata" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/UserCollateralResponse" + } + }, + "metadata": { + "$ref": "#/definitions/Metadata" + } + }, + "additionalProperties": false, + "definitions": { + "Metadata": { + "type": "object", + "required": [ + "has_more" + ], + "properties": { + "has_more": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "UserCollateralResponse": { + "type": "object", + "required": [ + "amount", + "amount_scaled", + "denom", + "enabled" + ], + "properties": { + "amount": { + "description": "Underlying asset amount that is actually deposited at the current block", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "amount_scaled": { + "description": "Scaled collateral amount stored in contract state", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "denom": { + "description": "Asset denom", + "type": "string" + }, + "enabled": { + "description": "Wether the user is using asset as collateral or not", + "type": "boolean" + } + }, + "additionalProperties": false + } + } + }, "user_debt": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "UserDebtResponse", @@ -1823,6 +1810,91 @@ ] } } + }, + "user_position_liquidation_pricing": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "UserPositionResponse", + "type": "object", + "required": [ + "health_status", + "total_collateralized_debt", + "total_enabled_collateral", + "weighted_liquidation_threshold_collateral", + "weighted_max_ltv_collateral" + ], + "properties": { + "health_status": { + "$ref": "#/definitions/UserHealthStatus" + }, + "total_collateralized_debt": { + "description": "Total value of all collateralized debts. If the user has an uncollateralized loan limit in an asset, the debt in this asset will not be included in this value.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "total_enabled_collateral": { + "description": "Total value of all enabled collateral assets. If an asset is disabled as collateral, it will not be included in this value.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "weighted_liquidation_threshold_collateral": { + "$ref": "#/definitions/Uint128" + }, + "weighted_max_ltv_collateral": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "UserHealthStatus": { + "oneOf": [ + { + "type": "string", + "enum": [ + "not_borrowing" + ] + }, + { + "type": "object", + "required": [ + "borrowing" + ], + "properties": { + "borrowing": { + "type": "object", + "required": [ + "liq_threshold_hf", + "max_ltv_hf" + ], + "properties": { + "liq_threshold_hf": { + "$ref": "#/definitions/Decimal" + }, + "max_ltv_hf": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + } + } } } } diff --git a/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json b/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json index e0b95cadc..4a4e229bb 100644 --- a/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json +++ b/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json @@ -1,6 +1,6 @@ { "contract_name": "mars-rewards-collector-base", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -195,6 +195,35 @@ }, "additionalProperties": false }, + { + "description": "Withdraw coins from the credit manager", + "type": "object", + "required": [ + "withdraw_from_credit_manager" + ], + "properties": { + "withdraw_from_credit_manager": { + "type": "object", + "required": [ + "account_id", + "actions" + ], + "properties": { + "account_id": { + "type": "string" + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/definitions/Action" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Distribute the accrued protocol income between the safety fund and the fee modules on mars hub, according to the split set in config. Callable by any address.", "type": "object", @@ -300,6 +329,98 @@ } ], "definitions": { + "Action": { + "oneOf": [ + { + "type": "object", + "required": [ + "withdraw" + ], + "properties": { + "withdraw": { + "$ref": "#/definitions/ActionCoin" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "withdraw_liquidity" + ], + "properties": { + "withdraw_liquidity": { + "type": "object", + "required": [ + "lp_token", + "slippage" + ], + "properties": { + "lp_token": { + "$ref": "#/definitions/ActionCoin" + }, + "slippage": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "unknown" + ], + "properties": { + "unknown": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "ActionAmount": { + "oneOf": [ + { + "type": "string", + "enum": [ + "account_balance" + ] + }, + { + "type": "object", + "required": [ + "exact" + ], + "properties": { + "exact": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + ] + }, + "ActionCoin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/ActionAmount" + }, + "denom": { + "type": "string" + } + }, + "additionalProperties": false + }, "Coin": { "type": "object", "required": [ diff --git a/schemas/mars-swapper-astroport/mars-swapper-astroport.json b/schemas/mars-swapper-astroport/mars-swapper-astroport.json index 93ef8f7c2..778ee5515 100644 --- a/schemas/mars-swapper-astroport/mars-swapper-astroport.json +++ b/schemas/mars-swapper-astroport/mars-swapper-astroport.json @@ -1,6 +1,6 @@ { "contract_name": "mars-swapper-astroport", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-swapper-osmosis/mars-swapper-osmosis.json b/schemas/mars-swapper-osmosis/mars-swapper-osmosis.json index 5fd1ac9ee..6d4b6a009 100644 --- a/schemas/mars-swapper-osmosis/mars-swapper-osmosis.json +++ b/schemas/mars-swapper-osmosis/mars-swapper-osmosis.json @@ -1,6 +1,6 @@ { "contract_name": "mars-swapper-osmosis", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -232,6 +232,7 @@ ] }, "SwapAmountInRoute": { + "description": "SwapAmountInRoute instead of using `osmosis_std::types::osmosis::poolmanager::v1beta1::SwapAmountInRoute` to keep consistency for pool_id representation as u64.\n\nSwapAmountInRoute from osmosis package uses as_str serializer/deserializer, so it expects pool_id as a String, but JSON schema doesn't correctly represent it.", "type": "object", "required": [ "pool_id", @@ -246,7 +247,8 @@ "token_out_denom": { "type": "string" } - } + }, + "additionalProperties": false }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", diff --git a/scripts/deploy/addresses/osmo-test-5.json b/scripts/deploy/addresses/osmo-test-5.json index b113d001d..561d4776c 100644 --- a/scripts/deploy/addresses/osmo-test-5.json +++ b/scripts/deploy/addresses/osmo-test-5.json @@ -1,7 +1,9 @@ { - "address-provider": "osmo1wlm6dc0vnncu2v5z26rv97plmlkmalm84uwqatrlftc4gmp8ahgqs6r4py", - "red-bank": "osmo1dl4rylasnd7mtfzlkdqn2gr0ss4gvyykpvr6d7t5ylzf6z535n9s5jjt8u", - "incentives": "osmo1zyz57xf82963mcsgqu3hq5y0h9mrltm4ttq2qe5mjth9ezp3375qe0sm7d", - "oracle": "osmo1khe29uw3t85nmmp3mtr8dls7v2qwsfk3tndu5h4w5g2r5tzlz5qqarq2e2", - "rewards-collector": "osmo1u5pcjue4grmg8lh7xrz2nvpy79xlzknwqkczfkyeyx9zzzj76tpq4tgrcs" + "address-provider": "osmo1sm42690a2836cy0ufzaffvsc5e29xagm267ef7jm7acn82f7f4nsh320uv", + "red-bank": "osmo1hs4sm0fah9rk4mz8e56v4n76g0q9fffdkkjm3f8tjagkdx78pqcq75pk0a", + "incentives": "osmo1nu0k6g294jela67vyth6nwr3l42gutq2m07pg9927f7v7tuv0d4sre9fr7", + "oracle": "osmo1dxu93scjdnx42txdp9d4hm3snffvnzmkp4jpc9sml8xlu3ncgamsl2lx58", + "rewards-collector": "osmo1q8gsh9ugl68yx03as3se8w7yegzcpdggy56zxt0mxsjxjvsggyqqedj03f", + "swapper": "osmo1ee9cq8dcknmw43znznx6vuupx5ku0tt505agccgaz5gn48mhe45s3kwwfm", + "params": "osmo1h334tvddn82m4apm08rm9k6kt32ws7vy0c4n30ngrvu6h6yxh8eq9l9jfh" } diff --git a/scripts/deploy/base/deployer.ts b/scripts/deploy/base/deployer.ts index 6ee676aae..75cb7294b 100644 --- a/scripts/deploy/base/deployer.ts +++ b/scripts/deploy/base/deployer.ts @@ -1,4 +1,10 @@ -import { AssetConfig, DeploymentConfig, OracleConfig, isAstroportRoute } from '../../types/config' +import { + AssetConfig, + DeploymentConfig, + OracleConfig, + isAstroportRoute, + VaultConfig, +} from '../../types/config' import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' import * as fs from 'fs' import { printBlue, printGreen, printRed, printYellow } from '../../utils/chalk' @@ -11,11 +17,17 @@ import assert from 'assert' import { SwapperExecuteMsg } from '../../types/config' import { InstantiateMsg as AstroportSwapperInstantiateMsg } from '../../types/generated/mars-swapper-astroport/MarsSwapperAstroport.types' import { InstantiateMsg as OsmosisSwapperInstantiateMsg } from '../../types/generated/mars-swapper-osmosis/MarsSwapperOsmosis.types' +import { InstantiateMsg as ParamsInstantiateMsg } from '../../types/generated/mars-params/MarsParams.types' +import { ExecuteMsg as ParamsExecuteMsg } from '../../types/generated/mars-params/MarsParams.types' import { InstantiateMsg as RedBankInstantiateMsg, + ExecuteMsg as RedBankExecuteMsg, QueryMsg as RedBankQueryMsg, } from '../../types/generated/mars-red-bank/MarsRedBank.types' -import { InstantiateMsg as AddressProviderInstantiateMsg } from '../../types/generated/mars-address-provider/MarsAddressProvider.types' +import { + AddressResponseItem, + InstantiateMsg as AddressProviderInstantiateMsg, +} from '../../types/generated/mars-address-provider/MarsAddressProvider.types' import { InstantiateMsg as IncentivesInstantiateMsg } from '../../types/generated/mars-incentives/MarsIncentives.types' import { InstantiateMsg as RewardsInstantiateMsg } from '../../types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.types' import { @@ -51,7 +63,7 @@ export class Deployer { Number(accountBalance.amount) / 1e6 } ${this.config.chainPrefix})`, ) - if (Number(accountBalance.amount) < 1_000_000 && this.config.chainId === 'osmo-test-4') { + if (Number(accountBalance.amount) < 1_000_000 && this.config.chainId === 'osmo-test-5') { printRed( `not enough ${this.config.chainPrefix} tokens to complete action, you may need to go to a test faucet to get more tokens.`, ) @@ -118,7 +130,6 @@ export class Deployer { owner: this.deployerAddress, config: { address_provider: this.storage.addresses['address-provider']!, - close_factor: '0.5', }, } await this.instantiate('red-bank', this.storage.codeIds['red-bank']!, msg) @@ -166,6 +177,109 @@ export class Deployer { await this.instantiate('swapper', this.storage.codeIds.swapper!, msg) } + async instantiateParams() { + const msg: ParamsInstantiateMsg = { + owner: this.deployerAddress, + address_provider: this.storage.addresses['address-provider']!, + target_health_factor: this.config.targetHealthFactor, + } + await this.instantiate('params', this.storage.codeIds.params!, msg) + } + + async updateAssetParams(assetConfig: AssetConfig) { + if (this.storage.execute.assetsUpdated.includes(assetConfig.denom)) { + printBlue(`${assetConfig.symbol} already updated in Params contract`) + return + } + printBlue(`Updating ${assetConfig.symbol}...`) + + const msg: ParamsExecuteMsg = { + update_asset_params: { + add_or_update: { + params: { + credit_manager: { + hls: assetConfig.credit_manager.hls, + whitelisted: assetConfig.credit_manager.whitelisted, + }, + denom: assetConfig.denom, + liquidation_bonus: assetConfig.liquidation_bonus, + liquidation_threshold: assetConfig.liquidation_threshold, + protocol_liquidation_fee: assetConfig.protocol_liquidation_fee, + max_loan_to_value: assetConfig.max_loan_to_value, + red_bank: { + borrow_enabled: assetConfig.red_bank.borrow_enabled, + deposit_enabled: assetConfig.red_bank.borrow_enabled, + }, + deposit_cap: assetConfig.deposit_cap, + }, + }, + }, + } + + await this.client.execute(this.deployerAddress, this.storage.addresses['params']!, msg, 'auto') + + printYellow(`${assetConfig.symbol} updated.`) + } + + async initializeMarket(assetConfig: AssetConfig) { + if (this.storage.execute.marketsUpdated.includes(assetConfig.denom)) { + printBlue(`${assetConfig.symbol} already initialized in red-bank contract`) + return + } + printBlue(`Initializing ${assetConfig.symbol}...`) + + const msg: RedBankExecuteMsg = { + init_asset: { + denom: assetConfig.denom, + params: { + reserve_factor: assetConfig.reserve_factor, + interest_rate_model: { + optimal_utilization_rate: assetConfig.interest_rate_model.optimal_utilization_rate, + base: assetConfig.interest_rate_model.base, + slope_1: assetConfig.interest_rate_model.slope_1, + slope_2: assetConfig.interest_rate_model.slope_2, + }, + }, + }, + } + + await this.client.execute( + this.deployerAddress, + this.storage.addresses['red-bank']!, + msg, + 'auto', + ) + + printYellow(`${assetConfig.symbol} initialized`) + + this.storage.execute.marketsUpdated.push(assetConfig.denom) + } + + async updateVaultConfig(vaultConfig: VaultConfig) { + if (this.storage.execute.vaultsUpdated.includes(vaultConfig.addr)) { + printBlue(`${vaultConfig.symbol} already updated in Params contract`) + return + } + printBlue(`Updating ${vaultConfig.symbol}...`) + + const msg: ParamsExecuteMsg = { + update_vault_config: { + add_or_update: { + config: { + addr: vaultConfig.addr, + deposit_cap: vaultConfig.deposit_cap, + liquidation_threshold: vaultConfig.liquidation_threshold, + whitelisted: vaultConfig.whitelisted, + max_loan_to_value: vaultConfig.max_loan_to_value, + }, + }, + }, + } + + await this.client.execute(this.deployerAddress, this.storage.addresses['params']!, msg, 'auto') + + printYellow(`${vaultConfig.symbol} updated.`) + } async setRoutes() { printBlue('Setting Swapper Routes') for (const route of this.config.swapRoutes) { @@ -197,38 +311,42 @@ export class Deployer { async updateAddressProvider() { printBlue('Updating addresses in Address Provider...') - const addressesToSet = [ + const addressesToSet: AddressResponseItem[] = [ { + address: this.storage.addresses['rewards-collector']!, address_type: 'rewards_collector', - address: this.storage.addresses['rewards-collector'], }, { + address: this.storage.addresses.incentives!, address_type: 'incentives', - address: this.storage.addresses.incentives, }, { + address: this.storage.addresses.oracle!, address_type: 'oracle', - address: this.storage.addresses.oracle, }, { + address: this.storage.addresses['red-bank']!, address_type: 'red_bank', - address: this.storage.addresses['red-bank'], }, { - address_type: 'fee_collector', address: this.config.feeCollectorAddr, + address_type: 'fee_collector', }, { - address_type: 'safety_fund', address: this.config.safetyFundAddr, + address_type: 'safety_fund', }, { - address_type: 'protocol_admin', address: this.config.protocolAdminAddr, + address_type: 'protocol_admin', }, { + address: this.storage.addresses.swapper!, address_type: 'swapper', - address: this.storage.addresses.swapper, + }, + { + address: this.storage.addresses.params!, + address_type: 'params', }, ] @@ -249,46 +367,6 @@ export class Deployer { printGreen('Address Provider update completed') } - async initializeAsset(assetConfig: AssetConfig) { - if (this.storage.execute.assetsInitialized.includes(assetConfig.denom)) { - printBlue(`${assetConfig.symbol} already initialized.`) - return - } - printBlue(`Initializing ${assetConfig.symbol}...`) - - const msg = { - init_asset: { - denom: assetConfig.denom, - params: { - max_loan_to_value: assetConfig.max_loan_to_value, - reserve_factor: assetConfig.reserve_factor, - liquidation_threshold: assetConfig.liquidation_threshold, - liquidation_bonus: assetConfig.liquidation_bonus, - interest_rate_model: { - optimal_utilization_rate: assetConfig.interest_rate_model.optimal_utilization_rate, - base: assetConfig.interest_rate_model.base, - slope_1: assetConfig.interest_rate_model.slope_1, - slope_2: assetConfig.interest_rate_model.slope_2, - }, - deposit_cap: assetConfig.deposit_cap, - deposit_enabled: assetConfig.deposit_enabled, - borrow_enabled: assetConfig.borrow_enabled, - }, - }, - } - - await this.client.execute( - this.deployerAddress, - this.storage.addresses['red-bank']!, - msg, - 'auto', - ) - - printYellow(`${assetConfig.symbol} initialized`) - - this.storage.execute.assetsInitialized.push(assetConfig.denom) - } - async recordTwapSnapshots(denoms: string[]) { const msg: WasmOracleExecuteMsg = { custom: { @@ -599,6 +677,23 @@ export class Deployer { assert.equal(swapperConfig.proposed, this.config.multisigAddr) } + async updateParamsContractOwner() { + const msg = { + update_owner: { + propose_new_owner: { + proposed: this.storage.owner, + }, + }, + } + await this.client.execute(this.deployerAddress, this.storage.addresses.params!, msg, 'auto') + printYellow('Owner updated to Mutlisig for Params') + const paramsConfig = (await this.client.queryContractSmart(this.storage.addresses.params!, { + owner: {}, + })) as { proposed: string } + + assert.equal(paramsConfig.proposed, this.config.multisigAddr) + } + async updateAddressProviderContractOwner() { const msg = { update_owner: { diff --git a/scripts/deploy/base/index.ts b/scripts/deploy/base/index.ts index 86c48bc53..ef812eb5d 100644 --- a/scripts/deploy/base/index.ts +++ b/scripts/deploy/base/index.ts @@ -19,6 +19,7 @@ export const taskRunner = async (config: DeploymentConfig) => { `mars_rewards_collector_${config.rewardsCollectorName}.wasm`, ) await deployer.upload('swapper', `mars_swapper_${config.swapperDexName}.wasm`) + await deployer.upload('params', `mars_params.wasm`) // Instantiate contracts deployer.setOwnerAddr() @@ -28,14 +29,19 @@ export const taskRunner = async (config: DeploymentConfig) => { await deployer.instantiateOracle(config.oracleCustomInitParams) await deployer.instantiateRewards() await deployer.instantiateSwapper() + await deployer.instantiateParams() await deployer.saveDeploymentAddrsToFile() // setup - await deployer.updateAddressProvider() - await deployer.setRoutes() + await deployer.updateAddressProvider() // CreditManager address in address-provider should be set once known for (const asset of config.assets) { - await deployer.initializeAsset(asset) + await deployer.updateAssetParams(asset) + await deployer.initializeMarket(asset) + } + for (const vault of config.vaults) { + await deployer.updateVaultConfig(vault) } + await deployer.setRoutes() for (const oracleConfig of config.oracleConfigs) { await deployer.setOracle(oracleConfig) } @@ -46,7 +52,7 @@ export const taskRunner = async (config: DeploymentConfig) => { await deployer.executeBorrow() await deployer.executeRepay() await deployer.executeWithdraw() - await deployer.executeRewardsSwap() + // await deployer.executeRewardsSwap() } if (config.multisigAddr) { @@ -55,6 +61,7 @@ export const taskRunner = async (config: DeploymentConfig) => { await deployer.updateOracleContractOwner() await deployer.updateRewardsContractOwner() await deployer.updateSwapperContractOwner() + await deployer.updateParamsContractOwner() await deployer.updateAddressProviderContractOwner() printGreen('It is confirmed that all contracts have transferred ownership to the Multisig') } else { diff --git a/scripts/deploy/base/storage.ts b/scripts/deploy/base/storage.ts index b23fd319c..768ae3ee0 100644 --- a/scripts/deploy/base/storage.ts +++ b/scripts/deploy/base/storage.ts @@ -28,7 +28,12 @@ export class Storage implements StorageItems { return new this(chainId, { addresses: {}, codeIds: {}, - execute: { assetsInitialized: [], addressProviderUpdated: {} }, + execute: { + assetsUpdated: [], + marketsUpdated: [], + vaultsUpdated: [], + addressProviderUpdated: {}, + }, }) } } diff --git a/scripts/deploy/neutron/config_mainnet.ts b/scripts/deploy/neutron/config_mainnet.ts index 96b360b40..f7ba6a1e3 100644 --- a/scripts/deploy/neutron/config_mainnet.ts +++ b/scripts/deploy/neutron/config_mainnet.ts @@ -76,6 +76,8 @@ export const atomOracle: OracleConfig = { price_feed_id: pythAtomID, denom_decimals: 6, max_staleness: 60, + max_confidence: '0.1', // FIXME: provide correct values + max_deviation: '0.1', // FIXME: provide correct values }, }, } @@ -88,6 +90,8 @@ export const axlUSDCOracle: OracleConfig = { price_feed_id: pythUsdcID, denom_decimals: 6, max_staleness: 60, + max_confidence: '0.1', // FIXME: provide correct values + max_deviation: '0.1', // FIXME: provide correct values }, }, } @@ -264,55 +268,91 @@ export const usdcMarsRoute = { export const ntrnAsset: AssetConfig = { denom: 'untrn', max_loan_to_value: '0.35', - reserve_factor: '0.1', liquidation_threshold: '0.40', - liquidation_bonus: '0.15', + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + // liquidation_bonus: '0.15', + symbol: 'NTRN', + credit_manager: { + whitelisted: false, + }, + red_bank: { + borrow_enabled: true, + deposit_enabled: true, + }, + deposit_cap: '5000000000000', + reserve_factor: '0.1', interest_rate_model: { optimal_utilization_rate: '0.6', base: '0', slope_1: '0.15', slope_2: '3', }, - deposit_cap: '5000000000000', - deposit_enabled: true, - borrow_enabled: true, - symbol: 'NTRN', } export const atomAsset: AssetConfig = { denom: atomDenom, max_loan_to_value: '0.68', - reserve_factor: '0.1', liquidation_threshold: '0.7', - liquidation_bonus: '0.1', + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + // liquidation_bonus: '0.1', + symbol: 'ATOM', + credit_manager: { + whitelisted: false, + }, + red_bank: { + borrow_enabled: true, + deposit_enabled: true, + }, + deposit_cap: '150000000000', + reserve_factor: '0.1', interest_rate_model: { optimal_utilization_rate: '0.7', base: '0', slope_1: '0.2', slope_2: '3', }, - deposit_cap: '150000000000', - deposit_enabled: true, - borrow_enabled: true, - symbol: 'ATOM', } export const axlUSDCAsset: AssetConfig = { denom: axlUsdcDenom, max_loan_to_value: '0.74', - reserve_factor: '0.1', liquidation_threshold: '0.75', - liquidation_bonus: '0.1', + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + // liquidation_bonus: '0.1', + symbol: 'axlUSDC', + credit_manager: { + whitelisted: false, + }, + red_bank: { + borrow_enabled: true, + deposit_enabled: true, + }, + deposit_cap: '500000000000', + reserve_factor: '0.1', interest_rate_model: { optimal_utilization_rate: '0.8', base: '0', slope_1: '0.125', slope_2: '2', }, - deposit_cap: '500000000000', - deposit_enabled: true, - borrow_enabled: true, - symbol: 'axlUSDC', } export const neutronMainnetConfig: DeploymentConfig = { @@ -334,7 +374,6 @@ export const neutronMainnetConfig: DeploymentConfig = { multisigAddr: protocolAdminAddr, slippage_tolerance: '0.01', base_asset_symbol: 'NTRN', - second_asset_symbol: 'ATOM', runTests: false, mainnet: true, feeCollectorDenom: marsDenom, @@ -345,8 +384,8 @@ export const neutronMainnetConfig: DeploymentConfig = { feeCollectorAddr: feeCollectorAddr, swapperDexName: 'astroport', assets: [ntrnAsset, atomAsset, axlUSDCAsset], + vaults: [], oracleConfigs: [usdOracle, axlUSDCOracle, marsOracle, atomOracle, ntrnOracle], - maxCloseFactor: '0.5', oracleCustomInitParams: { astroport_factory: astroportFactory, }, diff --git a/scripts/deploy/neutron/config_testnet.ts b/scripts/deploy/neutron/config_testnet.ts index 24bc0db21..612b31caa 100644 --- a/scripts/deploy/neutron/config_testnet.ts +++ b/scripts/deploy/neutron/config_testnet.ts @@ -63,6 +63,8 @@ export const atomOracle: OracleConfig = { price_feed_id: pythAtomID, denom_decimals: 6, max_staleness: 300, // 5 minutes + max_confidence: '0.1', + max_deviation: '0.1', }, }, } @@ -75,6 +77,8 @@ export const axlUSDCOracle: OracleConfig = { price_feed_id: pythUsdcID, denom_decimals: 6, max_staleness: 300, // 5 minutes + max_confidence: '0.1', + max_deviation: '0.1', }, }, } @@ -223,55 +227,91 @@ export const usdcMarsRoute = { export const ntrnAsset: AssetConfig = { denom: 'untrn', max_loan_to_value: '0.35', - reserve_factor: '0.1', liquidation_threshold: '0.40', - liquidation_bonus: '0.15', + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + // liquidation_bonus: '0.15', + symbol: 'NTRN', + credit_manager: { + whitelisted: false, + }, + red_bank: { + borrow_enabled: true, + deposit_enabled: true, + }, + deposit_cap: '5000000000000', + reserve_factor: '0.1', interest_rate_model: { optimal_utilization_rate: '0.6', base: '0', slope_1: '0.15', slope_2: '3', }, - deposit_cap: '5000000000000', - deposit_enabled: true, - borrow_enabled: true, - symbol: 'NTRN', } export const atomAsset: AssetConfig = { denom: atomDenom, max_loan_to_value: '0.68', - reserve_factor: '0.1', liquidation_threshold: '0.7', - liquidation_bonus: '0.1', + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + // liquidation_bonus: '0.1', + symbol: 'ATOM', + credit_manager: { + whitelisted: false, + }, + red_bank: { + borrow_enabled: true, + deposit_enabled: true, + }, + deposit_cap: '150000000000', + reserve_factor: '0.1', interest_rate_model: { optimal_utilization_rate: '0.7', base: '0', slope_1: '0.2', slope_2: '3', }, - deposit_cap: '150000000000', - deposit_enabled: true, - borrow_enabled: true, - symbol: 'ATOM', } export const axlUSDCAsset: AssetConfig = { denom: axlUsdcDenom, max_loan_to_value: '0.74', - reserve_factor: '0.1', liquidation_threshold: '0.75', - liquidation_bonus: '0.1', + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + // liquidation_bonus: '0.1', + symbol: 'axlUSDC', + credit_manager: { + whitelisted: false, + }, + red_bank: { + borrow_enabled: true, + deposit_enabled: true, + }, + deposit_cap: '500000000000', + reserve_factor: '0.1', interest_rate_model: { optimal_utilization_rate: '0.8', base: '0', slope_1: '0.125', slope_2: '2', }, - deposit_cap: '500000000000', - deposit_enabled: true, - borrow_enabled: true, - symbol: 'axlUSDC', } export const neutronTestnetConfig: DeploymentConfig = { @@ -293,7 +333,6 @@ export const neutronTestnetConfig: DeploymentConfig = { 'bundle bundle orchard jeans office umbrella bird around taxi arrive infant discover elder they joy misery photo crunch gift fancy pledge attend adult eight', slippage_tolerance: '0.01', base_asset_symbol: 'NTRN', - second_asset_symbol: 'ATOM', runTests: true, mainnet: false, feeCollectorDenom: marsDenom, @@ -304,8 +343,8 @@ export const neutronTestnetConfig: DeploymentConfig = { feeCollectorAddr: feeCollectorAddr, swapperDexName: 'astroport', assets: [ntrnAsset, atomAsset, axlUSDCAsset], + vaults: [], oracleConfigs: [usdOracle, axlUSDCOracle, atomOracle, ntrnOracle], - maxCloseFactor: '0.5', oracleCustomInitParams: { astroport_factory: astroportFactory, }, diff --git a/scripts/deploy/neutron/config_testnet_multisig.ts b/scripts/deploy/neutron/config_testnet_multisig.ts index 00e5f9878..552e75eb0 100644 --- a/scripts/deploy/neutron/config_testnet_multisig.ts +++ b/scripts/deploy/neutron/config_testnet_multisig.ts @@ -86,6 +86,8 @@ export const axlUSDCOracle: OracleConfig = { price_feed_id: pythUsdcID, denom_decimals: 6, max_staleness: 300, // 5 minutes + max_confidence: '0.1', + max_deviation: '0.1', }, }, } @@ -262,55 +264,91 @@ export const usdcMarsRoute = { export const ntrnAsset: AssetConfig = { denom: 'untrn', max_loan_to_value: '0.35', - reserve_factor: '0.1', liquidation_threshold: '0.40', - liquidation_bonus: '0.15', + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + // liquidation_bonus: '0.15', + symbol: 'NTRN', + credit_manager: { + whitelisted: false, + }, + red_bank: { + borrow_enabled: true, + deposit_enabled: true, + }, + deposit_cap: '5000000000000', + reserve_factor: '0.1', interest_rate_model: { optimal_utilization_rate: '0.6', base: '0', slope_1: '0.15', slope_2: '3', }, - deposit_cap: '5000000000000', - deposit_enabled: true, - borrow_enabled: true, - symbol: 'NTRN', } export const atomAsset: AssetConfig = { denom: atomDenom, max_loan_to_value: '0.68', - reserve_factor: '0.1', liquidation_threshold: '0.7', - liquidation_bonus: '0.1', + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + // liquidation_bonus: '0.1', + symbol: 'ATOM', + credit_manager: { + whitelisted: false, + }, + red_bank: { + borrow_enabled: true, + deposit_enabled: true, + }, + deposit_cap: '150000000000', + reserve_factor: '0.1', interest_rate_model: { optimal_utilization_rate: '0.7', base: '0', slope_1: '0.2', slope_2: '3', }, - deposit_cap: '150000000000', - deposit_enabled: true, - borrow_enabled: true, - symbol: 'ATOM', } export const axlUSDCAsset: AssetConfig = { denom: axlUsdcDenom, max_loan_to_value: '0.74', - reserve_factor: '0.1', liquidation_threshold: '0.75', - liquidation_bonus: '0.1', + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + // liquidation_bonus: '0.1', + symbol: 'axlUSDC', + credit_manager: { + whitelisted: false, + }, + red_bank: { + borrow_enabled: true, + deposit_enabled: true, + }, + deposit_cap: '500000000000', + reserve_factor: '0.1', interest_rate_model: { optimal_utilization_rate: '0.8', base: '0', slope_1: '0.125', slope_2: '2', }, - deposit_cap: '500000000000', - deposit_enabled: true, - borrow_enabled: true, - symbol: 'axlUSDC', } export const neutronTetstnetMultisigConfig: DeploymentConfig = { @@ -333,7 +371,6 @@ export const neutronTetstnetMultisigConfig: DeploymentConfig = { multisigAddr: protocolAdminAddr, slippage_tolerance: '0.01', base_asset_symbol: 'NTRN', - second_asset_symbol: 'ATOM', runTests: false, mainnet: false, feeCollectorDenom: marsDenom, @@ -344,8 +381,8 @@ export const neutronTetstnetMultisigConfig: DeploymentConfig = { feeCollectorAddr: feeCollectorAddr, swapperDexName: 'astroport', assets: [ntrnAsset, atomAsset, axlUSDCAsset], + vaults: [], oracleConfigs: [usdOracle, axlUSDCOracle, marsOracle, atomOracle, ntrnOracle], - maxCloseFactor: '0.5', oracleCustomInitParams: { astroport_factory: astroportFactory, }, diff --git a/scripts/deploy/osmosis/config.ts b/scripts/deploy/osmosis/config.ts index 0031c49bd..3a59e3047 100644 --- a/scripts/deploy/osmosis/config.ts +++ b/scripts/deploy/osmosis/config.ts @@ -26,109 +26,175 @@ const marsOsmoPool = 907 export const osmoAsset: AssetConfig = { denom: 'uosmo', max_loan_to_value: '0.59', - reserve_factor: '0.2', liquidation_threshold: '0.61', - liquidation_bonus: '0.15', + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + symbol: 'OSMO', + credit_manager: { + whitelisted: true, + }, + red_bank: { + borrow_enabled: true, + deposit_enabled: true, + }, + deposit_cap: '2500000000000', + reserve_factor: '0.2', interest_rate_model: { optimal_utilization_rate: '0.6', base: '0', slope_1: '0.15', slope_2: '3', }, - deposit_cap: '2500000000000', - deposit_enabled: true, - borrow_enabled: true, - symbol: 'OSMO', } export const atomAsset: AssetConfig = { denom: atom, max_loan_to_value: '0.68', - reserve_factor: '0.2', liquidation_threshold: '0.7', - liquidation_bonus: '0.15', + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + symbol: 'ATOM', + credit_manager: { + whitelisted: true, + }, + red_bank: { + borrow_enabled: true, + deposit_enabled: true, + }, + deposit_cap: '100000000000', + reserve_factor: '0.2', interest_rate_model: { optimal_utilization_rate: '0.6', base: '0', slope_1: '0.15', slope_2: '3', }, - deposit_cap: '100000000000', - deposit_enabled: true, - borrow_enabled: true, - symbol: 'ATOM', } export const atomAssetTest: AssetConfig = { denom: atomTest, max_loan_to_value: '0.68', - reserve_factor: '0.2', liquidation_threshold: '0.7', - liquidation_bonus: '0.15', + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + symbol: 'ATOM', + credit_manager: { + whitelisted: true, + }, + red_bank: { + borrow_enabled: true, + deposit_enabled: true, + }, + deposit_cap: '100000000000', + reserve_factor: '0.2', interest_rate_model: { optimal_utilization_rate: '0.6', base: '0', slope_1: '0.15', slope_2: '3', }, - deposit_cap: '100000000000', - deposit_enabled: true, - borrow_enabled: true, - symbol: 'ATOM', } export const axlUSDCAsset: AssetConfig = { denom: axlUSDC, max_loan_to_value: '0.74', - reserve_factor: '0.2', liquidation_threshold: '0.75', - liquidation_bonus: '0.1', + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + symbol: 'axlUSDC', + credit_manager: { + whitelisted: true, + }, + red_bank: { + borrow_enabled: true, + deposit_enabled: true, + }, + deposit_cap: '500000000000', + reserve_factor: '0.2', interest_rate_model: { optimal_utilization_rate: '0.8', base: '0', slope_1: '0.2', slope_2: '2', }, - deposit_cap: '500000000000', - deposit_enabled: true, - borrow_enabled: true, - symbol: 'axlUSDC', } export const axlUSDCAssetTest: AssetConfig = { denom: usdcTest, max_loan_to_value: '0.74', - reserve_factor: '0.2', liquidation_threshold: '0.75', - liquidation_bonus: '0.1', + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + symbol: 'axlUSDC', + credit_manager: { + whitelisted: true, + }, + red_bank: { + borrow_enabled: true, + deposit_enabled: true, + }, + deposit_cap: '500000000000', + reserve_factor: '0.2', interest_rate_model: { optimal_utilization_rate: '0.8', base: '0', slope_1: '0.2', slope_2: '2', }, - deposit_cap: '500000000000', - deposit_enabled: true, - borrow_enabled: true, - symbol: 'axlUSDC', } export const marsAssetTest: AssetConfig = { denom: marsTest, max_loan_to_value: '0.74', - reserve_factor: '0.2', liquidation_threshold: '0.75', - liquidation_bonus: '0.1', + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + symbol: 'mars', + credit_manager: { + whitelisted: true, + }, + red_bank: { + borrow_enabled: true, + deposit_enabled: true, + }, + deposit_cap: '500000000000', + reserve_factor: '0.2', interest_rate_model: { optimal_utilization_rate: '0.8', base: '0', slope_1: '0.2', slope_2: '2', }, - deposit_cap: '500000000000', - deposit_enabled: true, - borrow_enabled: true, - symbol: 'mars', } // export const osmoOracle: OracleConfig = { @@ -206,7 +272,6 @@ export const osmosisTestnetConfig: DeploymentConfig = { 'elevator august inherit simple buddy giggle zone despair marine rich swim danger blur people hundred faint ladder wet toe strong blade utility trial process', slippage_tolerance: '0.01', base_asset_symbol: 'OSMO', - second_asset_symbol: 'ATOM', runTests: false, mainnet: false, feeCollectorDenom: marsTest, @@ -219,8 +284,8 @@ export const osmosisTestnetConfig: DeploymentConfig = { feeCollectorAddr: feeCollectorAddr, swapperDexName: 'osmosis', assets: [osmoAsset, atomAsset, axlUSDCAsset], + vaults: [], oracleConfigs: [atomOracle, axlUSDCOracle], - maxCloseFactor: '0.5', incentiveEpochDuration: 86400, maxWhitelistedIncentiveDenoms: 10, targetHealthFactor: '1.2', @@ -245,7 +310,6 @@ export const osmosisTestMultisig: DeploymentConfig = { 'elevator august inherit simple buddy giggle zone despair marine rich swim danger blur people hundred faint ladder wet toe strong blade utility trial process', slippage_tolerance: '0.01', base_asset_symbol: 'OSMO', - second_asset_symbol: 'ATOM', multisigAddr: 'osmo14w4x949nwcrqgfe53pxs3k7x53p0gvlrq34l5n', runTests: false, mainnet: false, @@ -259,8 +323,8 @@ export const osmosisTestMultisig: DeploymentConfig = { feeCollectorAddr: feeCollectorAddr, swapperDexName: 'osmosis', assets: [osmoAsset, atomAsset, axlUSDCAsset], + vaults: [], oracleConfigs: [atomOracle, axlUSDCOracle], - maxCloseFactor: '0.5', incentiveEpochDuration: 86400, maxWhitelistedIncentiveDenoms: 10, targetHealthFactor: '1.2', @@ -283,7 +347,6 @@ export const osmosisMainnet: DeploymentConfig = { deployerMnemonic: 'TO BE INSERTED AT TIME OF DEPLOYMENT', slippage_tolerance: '0.01', base_asset_symbol: 'OSMO', - second_asset_symbol: 'ATOM', multisigAddr: 'osmo14w4x949nwcrqgfe53pxs3k7x53p0gvlrq34l5n', runTests: false, mainnet: true, @@ -326,8 +389,8 @@ export const osmosisMainnet: DeploymentConfig = { feeCollectorAddr: feeCollectorAddr, swapperDexName: 'osmosis', assets: [osmoAsset, atomAsset, axlUSDCAsset], + vaults: [], oracleConfigs: [atomOracle, axlUSDCOracle], - maxCloseFactor: '0.5', incentiveEpochDuration: 86400, maxWhitelistedIncentiveDenoms: 10, targetHealthFactor: '1.2', @@ -351,7 +414,6 @@ export const osmosisLocalConfig: DeploymentConfig = { 'notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius', slippage_tolerance: '0.05', base_asset_symbol: 'OSMO', - second_asset_symbol: 'ATOM', runTests: false, mainnet: false, feeCollectorDenom: axlUSDC, @@ -364,9 +426,9 @@ export const osmosisLocalConfig: DeploymentConfig = { feeCollectorAddr: feeCollectorAddr, swapperDexName: 'osmosis', assets: [osmoAsset, atomAsset, axlUSDCAsset], + vaults: [], oracleConfigs: [atomOracle, axlUSDCOracle], - maxCloseFactor: '0.5', - incentiveEpochDuration: 86400, + incentiveEpochDuration: 604800, // 1 week maxWhitelistedIncentiveDenoms: 10, targetHealthFactor: '1.2', } diff --git a/scripts/deploy/osmosis/mainIndex.ts b/scripts/deploy/osmosis/mainIndex.ts index 6b0cb614c..c1b5b291e 100644 --- a/scripts/deploy/osmosis/mainIndex.ts +++ b/scripts/deploy/osmosis/mainIndex.ts @@ -1,5 +1,5 @@ import { taskRunner } from '../base' -import { osmosisMainnet } from './config.js' +import { osmosisMainnet } from './mainnetConfig' void (async function () { await taskRunner(osmosisMainnet) diff --git a/scripts/deploy/osmosis/mainnetConfig.ts b/scripts/deploy/osmosis/mainnetConfig.ts new file mode 100644 index 000000000..8085df2cd --- /dev/null +++ b/scripts/deploy/osmosis/mainnetConfig.ts @@ -0,0 +1,191 @@ +import { DeploymentConfig, AssetConfig, OracleConfig } from '../../types/config' + +// Mainnet: +const osmo = 'uosmo' +const atom = 'ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2' +const axlUSDC = 'ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858' +const mars = 'ibc/573FCD90FACEE750F55A8864EF7D38265F07E5A9273FA0E8DAFD39951332B580' + +const pythContractAddr = 'UPDATE' +const protocolAdminAddr = 'osmo14w4x949nwcrqgfe53pxs3k7x53p0gvlrq34l5n' + +// note the following addresses are all 'mars' bech32 prefix +const safetyFundAddr = 'mars1s4hgh56can3e33e0zqpnjxh0t5wdf7u3pze575' +const feeCollectorAddr = 'mars17xpfvakm2amg962yls6f84z3kell8c5ldy6e7x' + +export const osmoAsset: AssetConfig = { + credit_manager: { + whitelisted: true, + }, + symbol: 'OSMO', + denom: osmo, + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + liquidation_threshold: '0.61', + max_loan_to_value: '0.59', + red_bank: { + borrow_enabled: true, + deposit_enabled: true, + }, + deposit_cap: '2500000000000', + reserve_factor: '0.2', + interest_rate_model: { + optimal_utilization_rate: '0.8', + base: '0', + slope_1: '0.2', + slope_2: '2', + }, +} + +export const atomAsset: AssetConfig = { + credit_manager: { + whitelisted: true, + }, + symbol: 'ATOM', + denom: atom, + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + liquidation_threshold: '0.7', + max_loan_to_value: '0.68', + red_bank: { + borrow_enabled: true, + deposit_enabled: true, + }, + deposit_cap: '100000000000', + reserve_factor: '0.2', + interest_rate_model: { + optimal_utilization_rate: '0.8', + base: '0', + slope_1: '0.2', + slope_2: '2', + }, +} + +export const axlUSDCAsset: AssetConfig = { + credit_manager: { + whitelisted: true, + }, + symbol: 'axlUSDC', + denom: axlUSDC, + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + liquidation_threshold: '0.75', + max_loan_to_value: '0.74', + red_bank: { + borrow_enabled: true, + deposit_enabled: true, + }, + deposit_cap: '500000000000', + reserve_factor: '0.2', + interest_rate_model: { + optimal_utilization_rate: '0.8', + base: '0', + slope_1: '0.2', + slope_2: '2', + }, +} + +export const atomOracle: OracleConfig = { + denom: atom, + price_source: { + pyth: { + contract_addr: pythContractAddr, + price_feed_id: 'UPDATE', + max_staleness: 60, + denom_decimals: 6, + max_confidence: '5', + max_deviation: '4', + }, + }, +} +export const axlUSDCOracle: OracleConfig = { + denom: axlUSDC, + price_source: { + geometric_twap: { + pool_id: 678, + window_size: 1800, + downtime_detector: { downtime: 'duration30m', recovery: 7200 }, + }, + }, +} + +export const osmosisMainnet: DeploymentConfig = { + oracleName: 'osmosis', + oracleBaseDenom: 'uusd', + rewardsCollectorName: 'osmosis', + atomDenom: atom, + baseAssetDenom: osmo, + gasPrice: '0.1uosmo', + chainId: 'osmosis-1', + chainPrefix: 'osmo', + channelId: 'channel-557', + marsDenom: mars, + rewardsCollectorTimeoutSeconds: 600, + rpcEndpoint: 'https://rpc.osmosis.zone', + safetyFundFeeShare: '0.5', + deployerMnemonic: 'TO BE INSERTED AT TIME OF DEPLOYMENT', + slippage_tolerance: '0.01', + base_asset_symbol: 'OSMO', + multisigAddr: 'osmo14w4x949nwcrqgfe53pxs3k7x53p0gvlrq34l5n', + runTests: false, + mainnet: true, + feeCollectorDenom: mars, + safetyFundDenom: axlUSDC, + swapRoutes: [ + { denom_in: osmo, denom_out: axlUSDC, route: [{ pool_id: 678, token_out_denom: axlUSDC }] }, + { + denom_in: atom, + denom_out: axlUSDC, + route: [ + { pool_id: 1, token_out_denom: osmo }, + { pool_id: 678, token_out_denom: axlUSDC }, + ], + }, + { + denom_in: osmo, + denom_out: mars, + route: [{ pool_id: 907, token_out_denom: mars }], + }, + { + denom_in: atom, + denom_out: mars, + route: [ + { pool_id: 1, token_out_denom: 'uosmo' }, + { pool_id: 907, token_out_denom: mars }, + ], + }, + { + denom_in: axlUSDC, + denom_out: mars, + route: [ + { pool_id: 678, token_out_denom: osmo }, + { pool_id: 907, token_out_denom: mars }, + ], + }, + ], + safetyFundAddr: safetyFundAddr, + protocolAdminAddr: protocolAdminAddr, + feeCollectorAddr: feeCollectorAddr, + swapperDexName: 'osmosis', + assets: [osmoAsset, atomAsset, axlUSDCAsset], + vaults: [], + oracleConfigs: [atomOracle, axlUSDCOracle], + targetHealthFactor: '1.2', + incentiveEpochDuration: 604800, // 1 week + maxWhitelistedIncentiveDenoms: 10, +} diff --git a/scripts/deploy/osmosis/multisig.ts b/scripts/deploy/osmosis/multisig.ts index 78676d111..49edbf6f4 100644 --- a/scripts/deploy/osmosis/multisig.ts +++ b/scripts/deploy/osmosis/multisig.ts @@ -1,5 +1,5 @@ import { taskRunner } from '../base' -import { osmosisTestMultisig } from './config.js' +import { osmosisTestMultisig } from './testnetConfig' void (async function () { await taskRunner(osmosisTestMultisig) diff --git a/scripts/deploy/osmosis/testIndex.ts b/scripts/deploy/osmosis/testIndex.ts index ead3a96be..4f22107d6 100644 --- a/scripts/deploy/osmosis/testIndex.ts +++ b/scripts/deploy/osmosis/testIndex.ts @@ -1,5 +1,5 @@ import { taskRunner } from '../base' -import { osmosisTestnetConfig } from './config.js' +import { osmosisTestnetConfig } from './testnetConfig' void (async function () { await taskRunner(osmosisTestnetConfig) diff --git a/scripts/deploy/osmosis/testnetConfig.ts b/scripts/deploy/osmosis/testnetConfig.ts new file mode 100644 index 000000000..b920aa280 --- /dev/null +++ b/scripts/deploy/osmosis/testnetConfig.ts @@ -0,0 +1,253 @@ +import { DeploymentConfig, AssetConfig, OracleConfig, VaultConfig } from '../../types/config' + +// assets based off of OSMO-TEST-5: https://docs.osmosis.zone/osmosis-core/asset-info/ +const uosmo = 'uosmo' +const aUSDC = 'ibc/6F34E1BD664C36CE49ACC28E60D62559A5F96C4F9A6CCE4FC5A67B2852E24CFE' // axelar USDC +const atom = 'ibc/A8C2D23A1E6F95DA4E48BA349667E322BD7A6C996D8A4AAE8BA72E190F3D1477' +const mars = 'ibc/2E7368A14AC9AB7870F32CFEA687551C5064FA861868EDF7437BC877358A81F9' +const usdcOsmo = 'gamm/pool/5' +const atomOsmo = 'gamm/pool/12' + +const protocolAdminAddr = 'osmo14w4x949nwcrqgfe53pxs3k7x53p0gvlrq34l5n' + +// note the following addresses are all 'mars' bech32 prefix +const safetyFundAddr = 'mars1s4hgh56can3e33e0zqpnjxh0t5wdf7u3pze575' +const feeCollectorAddr = 'mars17xpfvakm2amg962yls6f84z3kell8c5ldy6e7x' + +export const osmoAsset: AssetConfig = { + credit_manager: { + whitelisted: true, + }, + symbol: 'OSMO', + denom: uosmo, + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + liquidation_threshold: '0.61', + max_loan_to_value: '0.59', + red_bank: { + borrow_enabled: true, + deposit_enabled: true, + }, + deposit_cap: '2500000000000', + reserve_factor: '0.2', + interest_rate_model: { + optimal_utilization_rate: '0.8', + base: '0', + slope_1: '0.2', + slope_2: '2', + }, +} + +export const atomAsset: AssetConfig = { + credit_manager: { + whitelisted: true, + }, + symbol: 'ATOM', + denom: atom, + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + liquidation_threshold: '0.7', + max_loan_to_value: '0.68', + red_bank: { + borrow_enabled: true, + deposit_enabled: true, + }, + deposit_cap: '100000000000', + reserve_factor: '0.2', + interest_rate_model: { + optimal_utilization_rate: '0.8', + base: '0', + slope_1: '0.2', + slope_2: '2', + }, +} + +export const USDCAsset: AssetConfig = { + credit_manager: { + whitelisted: true, + }, + symbol: 'aUSDC', + denom: aUSDC, + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + liquidation_threshold: '0.75', + max_loan_to_value: '0.74', + red_bank: { + borrow_enabled: true, + deposit_enabled: true, + }, + deposit_cap: '500000000000', + reserve_factor: '0.2', + interest_rate_model: { + optimal_utilization_rate: '0.8', + base: '0', + slope_1: '0.2', + slope_2: '2', + }, +} + +export const usdcOsmoVault: VaultConfig = { + addr: 'osmo1l3q4mrhkzjyernjhg8lz2t52ddw589y5qc0z7y8y28h6y5wcl46sg9n28j', + symbol: 'usdcOsmoVault', + deposit_cap: { + denom: aUSDC, + amount: '1000000000', + }, + liquidation_threshold: '0.65', + max_loan_to_value: '0.63', + whitelisted: true, +} + +export const atomOsmoVault: VaultConfig = { + addr: 'osmo1m45ap4rq4m2mfjkcqu9ks9mxmyx2hvx0cdca9sjmrg46q7lghzqqhxxup5', + symbol: 'atomOsmoVault', + deposit_cap: { + denom: aUSDC, + amount: '1000000000', + }, + liquidation_threshold: '0.65', + max_loan_to_value: '0.63', + whitelisted: true, +} + +export const osmoOracle: OracleConfig = { + denom: uosmo, + price_source: { + fixed: { + price: '1', + }, + }, +} + +export const atomOracle: OracleConfig = { + denom: atom, + price_source: { + geometric_twap: { + downtime_detector: { downtime: 'duration30m', recovery: 7200 }, + window_size: 1800, + pool_id: 12, + }, + }, +} +export const USDCOracle: OracleConfig = { + denom: aUSDC, + price_source: { + staked_geometric_twap: { + transitive_denom: uosmo, + pool_id: 5, + window_size: 1800, + downtime_detector: { downtime: 'duration30m', recovery: 7200 }, + }, + }, +} + +export const usdcOsmoOracle: OracleConfig = { + denom: usdcOsmo, + price_source: { + xyk_liquidity_token: { + pool_id: 5, + }, + }, +} + +export const atomOsmoOracle: OracleConfig = { + denom: atomOsmo, + price_source: { + xyk_liquidity_token: { + pool_id: 12, + }, + }, +} + +export const osmosisTestnetConfig: DeploymentConfig = { + oracleName: 'osmosis', + oracleBaseDenom: 'uosmo', + rewardsCollectorName: 'osmosis', + atomDenom: atom, + baseAssetDenom: uosmo, + gasPrice: '0.1uosmo', + chainId: 'osmo-test-5', + chainPrefix: 'osmo', + channelId: 'channel-2083', + marsDenom: mars, + rewardsCollectorTimeoutSeconds: 600, + rpcEndpoint: 'https://rpc.osmotest5.osmosis.zone', + safetyFundFeeShare: '0.5', + deployerMnemonic: + 'elevator august inherit simple buddy giggle zone despair marine rich swim danger blur people hundred faint ladder wet toe strong blade utility trial process', + slippage_tolerance: '0.01', + base_asset_symbol: 'OSMO', + runTests: false, + mainnet: false, + feeCollectorDenom: mars, + safetyFundDenom: aUSDC, + swapRoutes: [ + { denom_in: atom, denom_out: uosmo, route: [{ pool_id: 12, token_out_denom: uosmo }] }, + { denom_in: uosmo, denom_out: atom, route: [{ pool_id: 12, token_out_denom: atom }] }, + { denom_in: aUSDC, denom_out: uosmo, route: [{ pool_id: 5, token_out_denom: uosmo }] }, + { denom_in: uosmo, denom_out: aUSDC, route: [{ pool_id: 5, token_out_denom: aUSDC }] }, + ], + safetyFundAddr: safetyFundAddr, + protocolAdminAddr: protocolAdminAddr, + feeCollectorAddr: feeCollectorAddr, + swapperDexName: 'osmosis', + assets: [osmoAsset, atomAsset, USDCAsset], + vaults: [usdcOsmoVault, atomOsmoVault], + oracleConfigs: [osmoOracle, atomOracle, USDCOracle, atomOsmoOracle, usdcOsmoOracle], + targetHealthFactor: '1.2', + incentiveEpochDuration: 604800, // 1 week + maxWhitelistedIncentiveDenoms: 10, +} + +export const osmosisTestMultisig: DeploymentConfig = { + oracleName: 'osmosis', + oracleBaseDenom: 'uusd', + rewardsCollectorName: 'osmosis', + atomDenom: atom, + baseAssetDenom: 'uosmo', + gasPrice: '0.1uosmo', + chainId: 'osmo-test-5', + chainPrefix: 'osmo', + channelId: 'channel-2083', + marsDenom: mars, + rewardsCollectorTimeoutSeconds: 600, + rpcEndpoint: 'https://rpc.osmotest5.osmosis.zone', + safetyFundFeeShare: '0.5', + deployerMnemonic: + 'elevator august inherit simple buddy giggle zone despair marine rich swim danger blur people hundred faint ladder wet toe strong blade utility trial process', + slippage_tolerance: '0.01', + base_asset_symbol: 'OSMO', + multisigAddr: 'osmo14w4x949nwcrqgfe53pxs3k7x53p0gvlrq34l5n', + runTests: false, + mainnet: false, + feeCollectorDenom: mars, + safetyFundDenom: aUSDC, + swapRoutes: [ + { denom_in: atom, denom_out: 'uosmo', route: [{ pool_id: 12, token_out_denom: 'uosmo' }] }, + ], + safetyFundAddr: safetyFundAddr, + protocolAdminAddr: protocolAdminAddr, + feeCollectorAddr: feeCollectorAddr, + swapperDexName: 'osmosis', + assets: [osmoAsset, atomAsset, USDCAsset], + vaults: [usdcOsmoVault, atomOsmoVault], + oracleConfigs: [osmoOracle, atomOracle, USDCOracle, atomOsmoOracle, usdcOsmoOracle], + targetHealthFactor: '1.2', + incentiveEpochDuration: 604800, // 1 week + maxWhitelistedIncentiveDenoms: 10, +} diff --git a/scripts/package.json b/scripts/package.json index 81867c6ce..37bb16a58 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -18,9 +18,9 @@ "format-check": "prettier --ignore-path .gitignore --check ." }, "dependencies": { - "@cosmjs/cosmwasm-stargate": "^0.30.1", - "@cosmjs/proto-signing": "^0.30.1", - "@cosmjs/stargate": "^0.30.1", + "@cosmjs/cosmwasm-stargate": "^0.31.0", + "@cosmjs/proto-signing": "^0.31.0", + "@cosmjs/stargate": "^0.31.0", "@cosmwasm/ts-codegen": "^0.30.1", "chalk": "4.1.2", "cosmjs-types": "^0.8.0", @@ -28,13 +28,13 @@ "ts-codegen": "^0.0.0" }, "devDependencies": { - "@types/node": "^20.2.5", - "@typescript-eslint/eslint-plugin": "^5.59.9", - "@typescript-eslint/parser": "^5.59.9", + "@types/node": "^20.3.3", + "@typescript-eslint/eslint-plugin": "^5.61.0", + "@typescript-eslint/parser": "^5.61.0", "cosmjs-types": "^0.8.0", - "eslint": "^8.42.0", + "eslint": "^8.44.0", "eslint-config-prettier": "^8.8.0", "prettier": "^2.8.8", - "typescript": "^5.1.3" + "typescript": "^5.1.6" } } diff --git a/scripts/types/config.ts b/scripts/types/config.ts index 225793b39..413242d33 100644 --- a/scripts/types/config.ts +++ b/scripts/types/config.ts @@ -5,7 +5,16 @@ import { WasmOracleCustomInitParams, WasmPriceSourceForString, } from './generated/mars-oracle-wasm/MarsOracleWasm.types' +import { + CmSettingsForString, + Coin, + Decimal, + HlsParamsBaseForString, + LiquidationBonus, + RedBankSettings, +} from './generated/mars-params/MarsParams.types' import { NeutronIbcConfig } from './generated/mars-rewards-collector-base/MarsRewardsCollectorBase.types' +import { Uint128 } from './generated/mars-red-bank/MarsRedBank.types' type SwapRoute = { denom_in: string @@ -45,7 +54,6 @@ export interface DeploymentConfig { deployerMnemonic: string slippage_tolerance: string base_asset_symbol: string - second_asset_symbol: string multisigAddr?: string runTests: boolean mainnet: boolean @@ -53,9 +61,9 @@ export interface DeploymentConfig { safetyFundAddr: string protocolAdminAddr: string feeCollectorAddr: string - maxCloseFactor: string swapperDexName: string assets: AssetConfig[] + vaults: VaultConfig[] oracleConfigs: OracleConfig[] oracleCustomInitParams?: WasmOracleCustomInitParams incentiveEpochDuration: number @@ -64,21 +72,31 @@ export interface DeploymentConfig { } export interface AssetConfig { + symbol: string + credit_manager: CmSettingsForString denom: string - max_loan_to_value: string + liquidation_bonus: LiquidationBonus + liquidation_threshold: Decimal + max_loan_to_value: Decimal + protocol_liquidation_fee: Decimal + red_bank: RedBankSettings + deposit_cap: Uint128 reserve_factor: string - liquidation_threshold: string - liquidation_bonus: string interest_rate_model: { optimal_utilization_rate: string base: string slope_1: string slope_2: string } - deposit_cap: string - deposit_enabled: boolean - borrow_enabled: boolean +} +export interface VaultConfig { + addr: string symbol: string + deposit_cap: Coin + hls?: HlsParamsBaseForString | null + liquidation_threshold: Decimal + max_loan_to_value: Decimal + whitelisted: boolean } export interface OracleConfig { diff --git a/scripts/types/generated/mars-address-provider/MarsAddressProvider.types.ts b/scripts/types/generated/mars-address-provider/MarsAddressProvider.types.ts index 09c1df316..695663ba9 100644 --- a/scripts/types/generated/mars-address-provider/MarsAddressProvider.types.ts +++ b/scripts/types/generated/mars-address-provider/MarsAddressProvider.types.ts @@ -20,7 +20,7 @@ export type ExecuteMsg = update_owner: OwnerUpdate } export type MarsAddressType = - | ('incentives' | 'oracle' | 'red_bank' | 'rewards_collector') + | ('incentives' | 'oracle' | 'red_bank' | 'rewards_collector' | 'params' | 'credit_manager') | 'protocol_admin' | 'fee_collector' | 'safety_fund' diff --git a/scripts/types/generated/mars-incentives/MarsIncentives.client.ts b/scripts/types/generated/mars-incentives/MarsIncentives.client.ts index 566f4bbc2..17a229869 100644 --- a/scripts/types/generated/mars-incentives/MarsIncentives.client.ts +++ b/scripts/types/generated/mars-incentives/MarsIncentives.client.ts @@ -72,11 +72,13 @@ export interface MarsIncentivesReadOnlyInterface { startAfterTimestamp?: number }) => Promise userUnclaimedRewards: ({ + accountId, limit, startAfterCollateralDenom, startAfterIncentiveDenom, user, }: { + accountId?: string limit?: number startAfterCollateralDenom?: string startAfterIncentiveDenom?: string @@ -186,11 +188,13 @@ export class MarsIncentivesQueryClient implements MarsIncentivesReadOnlyInterfac }) } userUnclaimedRewards = async ({ + accountId, limit, startAfterCollateralDenom, startAfterIncentiveDenom, user, }: { + accountId?: string limit?: number startAfterCollateralDenom?: string startAfterIncentiveDenom?: string @@ -198,6 +202,7 @@ export class MarsIncentivesQueryClient implements MarsIncentivesReadOnlyInterfac }): Promise => { return this.client.queryContractSmart(this.contractAddress, { user_unclaimed_rewards: { + account_id: accountId, limit, start_after_collateral_denom: startAfterCollateralDenom, start_after_incentive_denom: startAfterIncentiveDenom, @@ -246,11 +251,13 @@ export interface MarsIncentivesInterface extends MarsIncentivesReadOnlyInterface ) => Promise balanceChange: ( { + accountId, denom, totalAmountScaledBefore, userAddr, userAmountScaledBefore, }: { + accountId?: string denom: string totalAmountScaledBefore: Uint128 userAddr: Addr @@ -262,10 +269,12 @@ export interface MarsIncentivesInterface extends MarsIncentivesReadOnlyInterface ) => Promise claimRewards: ( { + accountId, limit, startAfterCollateralDenom, startAfterIncentiveDenom, }: { + accountId?: string limit?: number startAfterCollateralDenom?: string startAfterIncentiveDenom?: string @@ -377,11 +386,13 @@ export class MarsIncentivesClient } balanceChange = async ( { + accountId, denom, totalAmountScaledBefore, userAddr, userAmountScaledBefore, }: { + accountId?: string denom: string totalAmountScaledBefore: Uint128 userAddr: Addr @@ -396,6 +407,7 @@ export class MarsIncentivesClient this.contractAddress, { balance_change: { + account_id: accountId, denom, total_amount_scaled_before: totalAmountScaledBefore, user_addr: userAddr, @@ -409,10 +421,12 @@ export class MarsIncentivesClient } claimRewards = async ( { + accountId, limit, startAfterCollateralDenom, startAfterIncentiveDenom, }: { + accountId?: string limit?: number startAfterCollateralDenom?: string startAfterIncentiveDenom?: string @@ -426,6 +440,7 @@ export class MarsIncentivesClient this.contractAddress, { claim_rewards: { + account_id: accountId, limit, start_after_collateral_denom: startAfterCollateralDenom, start_after_incentive_denom: startAfterIncentiveDenom, diff --git a/scripts/types/generated/mars-incentives/MarsIncentives.react-query.ts b/scripts/types/generated/mars-incentives/MarsIncentives.react-query.ts index 9386101c4..3014ffed2 100644 --- a/scripts/types/generated/mars-incentives/MarsIncentives.react-query.ts +++ b/scripts/types/generated/mars-incentives/MarsIncentives.react-query.ts @@ -94,6 +94,7 @@ export function useMarsIncentivesWhitelistQuery({ export interface MarsIncentivesUserUnclaimedRewardsQuery extends MarsIncentivesReactQuery { args: { + accountId?: string limit?: number startAfterCollateralDenom?: string startAfterIncentiveDenom?: string @@ -110,6 +111,7 @@ export function useMarsIncentivesUserUnclaimedRewardsQuery( () => client ? client.userUnclaimedRewards({ + accountId: args.accountId, limit: args.limit, startAfterCollateralDenom: args.startAfterCollateralDenom, startAfterIncentiveDenom: args.startAfterIncentiveDenom, @@ -304,6 +306,7 @@ export function useMarsIncentivesUpdateConfigMutation( export interface MarsIncentivesClaimRewardsMutation { client: MarsIncentivesClient msg: { + accountId?: string limit?: number startAfterCollateralDenom?: string startAfterIncentiveDenom?: string @@ -329,6 +332,7 @@ export function useMarsIncentivesClaimRewardsMutation( export interface MarsIncentivesBalanceChangeMutation { client: MarsIncentivesClient msg: { + accountId?: string denom: string totalAmountScaledBefore: Uint128 userAddr: Addr diff --git a/scripts/types/generated/mars-incentives/MarsIncentives.types.ts b/scripts/types/generated/mars-incentives/MarsIncentives.types.ts index f3b2ca4de..a463fd8f4 100644 --- a/scripts/types/generated/mars-incentives/MarsIncentives.types.ts +++ b/scripts/types/generated/mars-incentives/MarsIncentives.types.ts @@ -29,6 +29,7 @@ export type ExecuteMsg = } | { balance_change: { + account_id?: string | null denom: string total_amount_scaled_before: Uint128 user_addr: Addr @@ -37,6 +38,7 @@ export type ExecuteMsg = } | { claim_rewards: { + account_id?: string | null limit?: number | null start_after_collateral_denom?: string | null start_after_incentive_denom?: string | null @@ -111,6 +113,7 @@ export type QueryMsg = } | { user_unclaimed_rewards: { + account_id?: string | null limit?: number | null start_after_collateral_denom?: string | null start_after_incentive_denom?: string | null @@ -131,6 +134,7 @@ export interface ConfigResponse { max_whitelisted_denoms: number owner?: string | null proposed_new_owner?: string | null + whitelist_count: number } export type ArrayOfEmissionResponse = EmissionResponse[] export interface EmissionResponse { diff --git a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts index c70068b26..1cf40c87d 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts @@ -20,6 +20,7 @@ import { GeometricTwap, RedemptionRateForString, QueryMsg, + ActionKind, ConfigResponse, PriceResponse, PriceSourceResponseForString, @@ -37,11 +38,13 @@ export interface MarsOracleOsmosisReadOnlyInterface { limit?: number startAfter?: string }) => Promise - price: ({ denom }: { denom: string }) => Promise + price: ({ denom, kind }: { denom: string; kind?: ActionKind }) => Promise prices: ({ + kind, limit, startAfter, }: { + kind?: ActionKind limit?: number startAfter?: string }) => Promise @@ -86,22 +89,26 @@ export class MarsOracleOsmosisQueryClient implements MarsOracleOsmosisReadOnlyIn }, }) } - price = async ({ denom }: { denom: string }): Promise => { + price = async ({ denom, kind }: { denom: string; kind?: ActionKind }): Promise => { return this.client.queryContractSmart(this.contractAddress, { price: { denom, + kind, }, }) } prices = async ({ + kind, limit, startAfter, }: { + kind?: ActionKind limit?: number startAfter?: string }): Promise => { return this.client.queryContractSmart(this.contractAddress, { prices: { + kind, limit, start_after: startAfter, }, diff --git a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.react-query.ts b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.react-query.ts index 739c3a028..3dd813bc3 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.react-query.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.react-query.ts @@ -21,6 +21,7 @@ import { GeometricTwap, RedemptionRateForString, QueryMsg, + ActionKind, ConfigResponse, PriceResponse, PriceSourceResponseForString, @@ -67,6 +68,7 @@ export interface MarsOracleOsmosisReactQuery { export interface MarsOracleOsmosisPricesQuery extends MarsOracleOsmosisReactQuery { args: { + kind?: ActionKind limit?: number startAfter?: string } @@ -81,6 +83,7 @@ export function useMarsOracleOsmosisPricesQuery({ () => client ? client.prices({ + kind: args.kind, limit: args.limit, startAfter: args.startAfter, }) @@ -92,6 +95,7 @@ export interface MarsOracleOsmosisPriceQuery extends MarsOracleOsmosisReactQuery { args: { denom: string + kind?: ActionKind } } export function useMarsOracleOsmosisPriceQuery({ @@ -105,6 +109,7 @@ export function useMarsOracleOsmosisPriceQuery({ client ? client.price({ denom: args.denom, + kind: args.kind, }) : Promise.reject(new Error('Invalid client')), { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, diff --git a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts index d1ee479fc..980e1835a 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts @@ -84,6 +84,8 @@ export type OsmosisPriceSourceForString = pyth: { contract_addr: string denom_decimals: number + max_confidence: Decimal + max_deviation: Decimal max_staleness: number price_feed_id: Identifier [k: string]: unknown @@ -174,14 +176,17 @@ export type QueryMsg = | { price: { denom: string + kind?: ActionKind | null } } | { prices: { + kind?: ActionKind | null limit?: number | null start_after?: string | null } } +export type ActionKind = 'default' | 'liquidation' export interface ConfigResponse { base_denom: string owner?: string | null diff --git a/scripts/types/generated/mars-oracle-wasm/MarsOracleWasm.client.ts b/scripts/types/generated/mars-oracle-wasm/MarsOracleWasm.client.ts index b33fc1dce..71d56e287 100644 --- a/scripts/types/generated/mars-oracle-wasm/MarsOracleWasm.client.ts +++ b/scripts/types/generated/mars-oracle-wasm/MarsOracleWasm.client.ts @@ -17,6 +17,7 @@ import { OwnerUpdate, WasmOracleCustomExecuteMsg, QueryMsg, + ActionKind, ConfigResponse, PriceResponse, PriceSourceResponseForString, @@ -34,11 +35,13 @@ export interface MarsOracleWasmReadOnlyInterface { limit?: number startAfter?: string }) => Promise - price: ({ denom }: { denom: string }) => Promise + price: ({ denom, kind }: { denom: string; kind?: ActionKind }) => Promise prices: ({ + kind, limit, startAfter, }: { + kind?: ActionKind limit?: number startAfter?: string }) => Promise @@ -83,22 +86,26 @@ export class MarsOracleWasmQueryClient implements MarsOracleWasmReadOnlyInterfac }, }) } - price = async ({ denom }: { denom: string }): Promise => { + price = async ({ denom, kind }: { denom: string; kind?: ActionKind }): Promise => { return this.client.queryContractSmart(this.contractAddress, { price: { denom, + kind, }, }) } prices = async ({ + kind, limit, startAfter, }: { + kind?: ActionKind limit?: number startAfter?: string }): Promise => { return this.client.queryContractSmart(this.contractAddress, { prices: { + kind, limit, start_after: startAfter, }, diff --git a/scripts/types/generated/mars-oracle-wasm/MarsOracleWasm.react-query.ts b/scripts/types/generated/mars-oracle-wasm/MarsOracleWasm.react-query.ts index 07d20b201..fb528281f 100644 --- a/scripts/types/generated/mars-oracle-wasm/MarsOracleWasm.react-query.ts +++ b/scripts/types/generated/mars-oracle-wasm/MarsOracleWasm.react-query.ts @@ -18,6 +18,7 @@ import { OwnerUpdate, WasmOracleCustomExecuteMsg, QueryMsg, + ActionKind, ConfigResponse, PriceResponse, PriceSourceResponseForString, @@ -60,6 +61,7 @@ export interface MarsOracleWasmReactQuery { export interface MarsOracleWasmPricesQuery extends MarsOracleWasmReactQuery { args: { + kind?: ActionKind limit?: number startAfter?: string } @@ -74,6 +76,7 @@ export function useMarsOracleWasmPricesQuery({ () => client ? client.prices({ + kind: args.kind, limit: args.limit, startAfter: args.startAfter, }) @@ -85,6 +88,7 @@ export interface MarsOracleWasmPriceQuery extends MarsOracleWasmReactQuery { args: { denom: string + kind?: ActionKind } } export function useMarsOracleWasmPriceQuery({ @@ -98,6 +102,7 @@ export function useMarsOracleWasmPriceQuery({ client ? client.price({ denom: args.denom, + kind: args.kind, }) : Promise.reject(new Error('Invalid client')), { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, diff --git a/scripts/types/generated/mars-oracle-wasm/MarsOracleWasm.types.ts b/scripts/types/generated/mars-oracle-wasm/MarsOracleWasm.types.ts index bef4d7aaf..7cbe5f6af 100644 --- a/scripts/types/generated/mars-oracle-wasm/MarsOracleWasm.types.ts +++ b/scripts/types/generated/mars-oracle-wasm/MarsOracleWasm.types.ts @@ -58,6 +58,8 @@ export type WasmPriceSourceForString = pyth: { contract_addr: string denom_decimals: number + max_confidence: Decimal + max_deviation: Decimal max_staleness: number price_feed_id: Identifier } @@ -102,14 +104,17 @@ export type QueryMsg = | { price: { denom: string + kind?: ActionKind | null } } | { prices: { + kind?: ActionKind | null limit?: number | null start_after?: string | null } } +export type ActionKind = 'default' | 'liquidation' export interface ConfigResponse { base_denom: string owner?: string | null diff --git a/scripts/types/generated/mars-params/MarsParams.client.ts b/scripts/types/generated/mars-params/MarsParams.client.ts new file mode 100644 index 000000000..4722ea540 --- /dev/null +++ b/scripts/types/generated/mars-params/MarsParams.client.ts @@ -0,0 +1,273 @@ +// @ts-nocheck +/** + * This file was automatically generated by @cosmwasm/ts-codegen@0.30.1. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run the @cosmwasm/ts-codegen generate command to regenerate this file. + */ + +import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from '@cosmjs/cosmwasm-stargate' +import { StdFee } from '@cosmjs/amino' +import { + Decimal, + InstantiateMsg, + ExecuteMsg, + OwnerUpdate, + AssetParamsUpdate, + HlsAssetTypeForString, + Uint128, + VaultConfigUpdate, + EmergencyUpdate, + CmEmergencyUpdate, + RedBankEmergencyUpdate, + AssetParamsBaseForString, + CmSettingsForString, + HlsParamsBaseForString, + LiquidationBonus, + RedBankSettings, + VaultConfigBaseForString, + Coin, + QueryMsg, + HlsAssetTypeForAddr, + Addr, + ArrayOfAssetParamsBaseForAddr, + AssetParamsBaseForAddr, + CmSettingsForAddr, + HlsParamsBaseForAddr, + ArrayOfVaultConfigBaseForAddr, + VaultConfigBaseForAddr, + OwnerResponse, + TotalDepositResponse, +} from './MarsParams.types' +export interface MarsParamsReadOnlyInterface { + contractAddress: string + owner: () => Promise + assetParams: ({ denom }: { denom: string }) => Promise + allAssetParams: ({ + limit, + startAfter, + }: { + limit?: number + startAfter?: string + }) => Promise + vaultConfig: ({ address }: { address: string }) => Promise + allVaultConfigs: ({ + limit, + startAfter, + }: { + limit?: number + startAfter?: string + }) => Promise + targetHealthFactor: () => Promise + totalDeposit: ({ denom }: { denom: string }) => Promise +} +export class MarsParamsQueryClient implements MarsParamsReadOnlyInterface { + client: CosmWasmClient + contractAddress: string + + constructor(client: CosmWasmClient, contractAddress: string) { + this.client = client + this.contractAddress = contractAddress + this.owner = this.owner.bind(this) + this.assetParams = this.assetParams.bind(this) + this.allAssetParams = this.allAssetParams.bind(this) + this.vaultConfig = this.vaultConfig.bind(this) + this.allVaultConfigs = this.allVaultConfigs.bind(this) + this.targetHealthFactor = this.targetHealthFactor.bind(this) + this.totalDeposit = this.totalDeposit.bind(this) + } + + owner = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + owner: {}, + }) + } + assetParams = async ({ denom }: { denom: string }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + asset_params: { + denom, + }, + }) + } + allAssetParams = async ({ + limit, + startAfter, + }: { + limit?: number + startAfter?: string + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + all_asset_params: { + limit, + start_after: startAfter, + }, + }) + } + vaultConfig = async ({ address }: { address: string }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + vault_config: { + address, + }, + }) + } + allVaultConfigs = async ({ + limit, + startAfter, + }: { + limit?: number + startAfter?: string + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + all_vault_configs: { + limit, + start_after: startAfter, + }, + }) + } + targetHealthFactor = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + target_health_factor: {}, + }) + } + totalDeposit = async ({ denom }: { denom: string }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + total_deposit: { + denom, + }, + }) + } +} +export interface MarsParamsInterface extends MarsParamsReadOnlyInterface { + contractAddress: string + sender: string + updateOwner: ( + ownerUpdate: OwnerUpdate, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[], + ) => Promise + updateTargetHealthFactor: ( + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[], + ) => Promise + updateAssetParams: ( + assetParamsUpdate: AssetParamsUpdate, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[], + ) => Promise + updateVaultConfig: ( + vaultConfigUpdate: VaultConfigUpdate, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[], + ) => Promise + emergencyUpdate: ( + emergencyUpdate: EmergencyUpdate, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[], + ) => Promise +} +export class MarsParamsClient extends MarsParamsQueryClient implements MarsParamsInterface { + client: SigningCosmWasmClient + sender: string + contractAddress: string + + constructor(client: SigningCosmWasmClient, sender: string, contractAddress: string) { + super(client, contractAddress) + this.client = client + this.sender = sender + this.contractAddress = contractAddress + this.updateOwner = this.updateOwner.bind(this) + this.updateTargetHealthFactor = this.updateTargetHealthFactor.bind(this) + this.updateAssetParams = this.updateAssetParams.bind(this) + this.updateVaultConfig = this.updateVaultConfig.bind(this) + this.emergencyUpdate = this.emergencyUpdate.bind(this) + } + + updateOwner = async ( + ownerUpdate: OwnerUpdate, + fee: number | StdFee | 'auto' = 'auto', + memo?: string, + _funds?: Coin[], + ): Promise => { + return await this.client.execute( + this.sender, + this.contractAddress, + { + update_owner: ownerUpdate, + }, + fee, + memo, + _funds, + ) + } + updateTargetHealthFactor = async ( + fee: number | StdFee | 'auto' = 'auto', + memo?: string, + _funds?: Coin[], + ): Promise => { + return await this.client.execute( + this.sender, + this.contractAddress, + { + update_target_health_factor: {}, + }, + fee, + memo, + _funds, + ) + } + updateAssetParams = async ( + assetParamsUpdate: AssetParamsUpdate, + fee: number | StdFee | 'auto' = 'auto', + memo?: string, + _funds?: Coin[], + ): Promise => { + return await this.client.execute( + this.sender, + this.contractAddress, + { + update_asset_params: assetParamsUpdate, + }, + fee, + memo, + _funds, + ) + } + updateVaultConfig = async ( + vaultConfigUpdate: VaultConfigUpdate, + fee: number | StdFee | 'auto' = 'auto', + memo?: string, + _funds?: Coin[], + ): Promise => { + return await this.client.execute( + this.sender, + this.contractAddress, + { + update_vault_config: vaultConfigUpdate, + }, + fee, + memo, + _funds, + ) + } + emergencyUpdate = async ( + emergencyUpdate: EmergencyUpdate, + fee: number | StdFee | 'auto' = 'auto', + memo?: string, + _funds?: Coin[], + ): Promise => { + return await this.client.execute( + this.sender, + this.contractAddress, + { + emergency_update: emergencyUpdate, + }, + fee, + memo, + _funds, + ) + } +} diff --git a/scripts/types/generated/mars-params/MarsParams.react-query.ts b/scripts/types/generated/mars-params/MarsParams.react-query.ts new file mode 100644 index 000000000..54b185c5b --- /dev/null +++ b/scripts/types/generated/mars-params/MarsParams.react-query.ts @@ -0,0 +1,322 @@ +// @ts-nocheck +/** + * This file was automatically generated by @cosmwasm/ts-codegen@0.30.1. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run the @cosmwasm/ts-codegen generate command to regenerate this file. + */ + +import { UseQueryOptions, useQuery, useMutation, UseMutationOptions } from '@tanstack/react-query' +import { ExecuteResult } from '@cosmjs/cosmwasm-stargate' +import { StdFee } from '@cosmjs/amino' +import { + Decimal, + InstantiateMsg, + ExecuteMsg, + OwnerUpdate, + AssetParamsUpdate, + HlsAssetTypeForString, + Uint128, + VaultConfigUpdate, + EmergencyUpdate, + CmEmergencyUpdate, + RedBankEmergencyUpdate, + AssetParamsBaseForString, + CmSettingsForString, + HlsParamsBaseForString, + LiquidationBonus, + RedBankSettings, + VaultConfigBaseForString, + Coin, + QueryMsg, + HlsAssetTypeForAddr, + Addr, + ArrayOfAssetParamsBaseForAddr, + AssetParamsBaseForAddr, + CmSettingsForAddr, + HlsParamsBaseForAddr, + ArrayOfVaultConfigBaseForAddr, + VaultConfigBaseForAddr, + OwnerResponse, + TotalDepositResponse, +} from './MarsParams.types' +import { MarsParamsQueryClient, MarsParamsClient } from './MarsParams.client' +export const marsParamsQueryKeys = { + contract: [ + { + contract: 'marsParams', + }, + ] as const, + address: (contractAddress: string | undefined) => + [{ ...marsParamsQueryKeys.contract[0], address: contractAddress }] as const, + owner: (contractAddress: string | undefined, args?: Record) => + [{ ...marsParamsQueryKeys.address(contractAddress)[0], method: 'owner', args }] as const, + assetParams: (contractAddress: string | undefined, args?: Record) => + [{ ...marsParamsQueryKeys.address(contractAddress)[0], method: 'asset_params', args }] as const, + allAssetParams: (contractAddress: string | undefined, args?: Record) => + [ + { ...marsParamsQueryKeys.address(contractAddress)[0], method: 'all_asset_params', args }, + ] as const, + vaultConfig: (contractAddress: string | undefined, args?: Record) => + [{ ...marsParamsQueryKeys.address(contractAddress)[0], method: 'vault_config', args }] as const, + allVaultConfigs: (contractAddress: string | undefined, args?: Record) => + [ + { ...marsParamsQueryKeys.address(contractAddress)[0], method: 'all_vault_configs', args }, + ] as const, + targetHealthFactor: (contractAddress: string | undefined, args?: Record) => + [ + { ...marsParamsQueryKeys.address(contractAddress)[0], method: 'target_health_factor', args }, + ] as const, + totalDeposit: (contractAddress: string | undefined, args?: Record) => + [ + { ...marsParamsQueryKeys.address(contractAddress)[0], method: 'total_deposit', args }, + ] as const, +} +export interface MarsParamsReactQuery { + client: MarsParamsQueryClient | undefined + options?: Omit< + UseQueryOptions, + "'queryKey' | 'queryFn' | 'initialData'" + > & { + initialData?: undefined + } +} +export interface MarsParamsTotalDepositQuery + extends MarsParamsReactQuery { + args: { + denom: string + } +} +export function useMarsParamsTotalDepositQuery({ + client, + args, + options, +}: MarsParamsTotalDepositQuery) { + return useQuery( + marsParamsQueryKeys.totalDeposit(client?.contractAddress, args), + () => + client + ? client.totalDeposit({ + denom: args.denom, + }) + : Promise.reject(new Error('Invalid client')), + { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, + ) +} +export interface MarsParamsTargetHealthFactorQuery + extends MarsParamsReactQuery {} +export function useMarsParamsTargetHealthFactorQuery({ + client, + options, +}: MarsParamsTargetHealthFactorQuery) { + return useQuery( + marsParamsQueryKeys.targetHealthFactor(client?.contractAddress), + () => (client ? client.targetHealthFactor() : Promise.reject(new Error('Invalid client'))), + { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, + ) +} +export interface MarsParamsAllVaultConfigsQuery + extends MarsParamsReactQuery { + args: { + limit?: number + startAfter?: string + } +} +export function useMarsParamsAllVaultConfigsQuery({ + client, + args, + options, +}: MarsParamsAllVaultConfigsQuery) { + return useQuery( + marsParamsQueryKeys.allVaultConfigs(client?.contractAddress, args), + () => + client + ? client.allVaultConfigs({ + limit: args.limit, + startAfter: args.startAfter, + }) + : Promise.reject(new Error('Invalid client')), + { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, + ) +} +export interface MarsParamsVaultConfigQuery + extends MarsParamsReactQuery { + args: { + address: string + } +} +export function useMarsParamsVaultConfigQuery({ + client, + args, + options, +}: MarsParamsVaultConfigQuery) { + return useQuery( + marsParamsQueryKeys.vaultConfig(client?.contractAddress, args), + () => + client + ? client.vaultConfig({ + address: args.address, + }) + : Promise.reject(new Error('Invalid client')), + { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, + ) +} +export interface MarsParamsAllAssetParamsQuery + extends MarsParamsReactQuery { + args: { + limit?: number + startAfter?: string + } +} +export function useMarsParamsAllAssetParamsQuery({ + client, + args, + options, +}: MarsParamsAllAssetParamsQuery) { + return useQuery( + marsParamsQueryKeys.allAssetParams(client?.contractAddress, args), + () => + client + ? client.allAssetParams({ + limit: args.limit, + startAfter: args.startAfter, + }) + : Promise.reject(new Error('Invalid client')), + { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, + ) +} +export interface MarsParamsAssetParamsQuery + extends MarsParamsReactQuery { + args: { + denom: string + } +} +export function useMarsParamsAssetParamsQuery({ + client, + args, + options, +}: MarsParamsAssetParamsQuery) { + return useQuery( + marsParamsQueryKeys.assetParams(client?.contractAddress, args), + () => + client + ? client.assetParams({ + denom: args.denom, + }) + : Promise.reject(new Error('Invalid client')), + { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, + ) +} +export interface MarsParamsOwnerQuery extends MarsParamsReactQuery {} +export function useMarsParamsOwnerQuery({ + client, + options, +}: MarsParamsOwnerQuery) { + return useQuery( + marsParamsQueryKeys.owner(client?.contractAddress), + () => (client ? client.owner() : Promise.reject(new Error('Invalid client'))), + { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, + ) +} +export interface MarsParamsEmergencyUpdateMutation { + client: MarsParamsClient + msg: EmergencyUpdate + args?: { + fee?: number | StdFee | 'auto' + memo?: string + funds?: Coin[] + } +} +export function useMarsParamsEmergencyUpdateMutation( + options?: Omit< + UseMutationOptions, + 'mutationFn' + >, +) { + return useMutation( + ({ client, msg, args: { fee, memo, funds } = {} }) => + client.emergencyUpdate(msg, fee, memo, funds), + options, + ) +} +export interface MarsParamsUpdateVaultConfigMutation { + client: MarsParamsClient + msg: VaultConfigUpdate + args?: { + fee?: number | StdFee | 'auto' + memo?: string + funds?: Coin[] + } +} +export function useMarsParamsUpdateVaultConfigMutation( + options?: Omit< + UseMutationOptions, + 'mutationFn' + >, +) { + return useMutation( + ({ client, msg, args: { fee, memo, funds } = {} }) => + client.updateVaultConfig(msg, fee, memo, funds), + options, + ) +} +export interface MarsParamsUpdateAssetParamsMutation { + client: MarsParamsClient + msg: AssetParamsUpdate + args?: { + fee?: number | StdFee | 'auto' + memo?: string + funds?: Coin[] + } +} +export function useMarsParamsUpdateAssetParamsMutation( + options?: Omit< + UseMutationOptions, + 'mutationFn' + >, +) { + return useMutation( + ({ client, msg, args: { fee, memo, funds } = {} }) => + client.updateAssetParams(msg, fee, memo, funds), + options, + ) +} +export interface MarsParamsUpdateTargetHealthFactorMutation { + client: MarsParamsClient + args?: { + fee?: number | StdFee | 'auto' + memo?: string + funds?: Coin[] + } +} +export function useMarsParamsUpdateTargetHealthFactorMutation( + options?: Omit< + UseMutationOptions, + 'mutationFn' + >, +) { + return useMutation( + ({ client, msg, args: { fee, memo, funds } = {} }) => + client.updateTargetHealthFactor(msg, fee, memo, funds), + options, + ) +} +export interface MarsParamsUpdateOwnerMutation { + client: MarsParamsClient + msg: OwnerUpdate + args?: { + fee?: number | StdFee | 'auto' + memo?: string + funds?: Coin[] + } +} +export function useMarsParamsUpdateOwnerMutation( + options?: Omit< + UseMutationOptions, + 'mutationFn' + >, +) { + return useMutation( + ({ client, msg, args: { fee, memo, funds } = {} }) => client.updateOwner(msg, fee, memo, funds), + options, + ) +} diff --git a/scripts/types/generated/mars-params/MarsParams.types.ts b/scripts/types/generated/mars-params/MarsParams.types.ts new file mode 100644 index 000000000..d27e9d118 --- /dev/null +++ b/scripts/types/generated/mars-params/MarsParams.types.ts @@ -0,0 +1,215 @@ +// @ts-nocheck +/** + * This file was automatically generated by @cosmwasm/ts-codegen@0.30.1. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run the @cosmwasm/ts-codegen generate command to regenerate this file. + */ + +export type Decimal = string +export interface InstantiateMsg { + address_provider: string + owner: string + target_health_factor: Decimal +} +export type ExecuteMsg = + | { + update_owner: OwnerUpdate + } + | { + update_target_health_factor: Decimal + } + | { + update_asset_params: AssetParamsUpdate + } + | { + update_vault_config: VaultConfigUpdate + } + | { + emergency_update: EmergencyUpdate + } +export type OwnerUpdate = + | { + propose_new_owner: { + proposed: string + } + } + | 'clear_proposed' + | 'accept_proposed' + | 'abolish_owner_role' + | { + set_emergency_owner: { + emergency_owner: string + } + } + | 'clear_emergency_owner' +export type AssetParamsUpdate = { + add_or_update: { + params: AssetParamsBaseForString + } +} +export type HlsAssetTypeForString = + | { + coin: { + denom: string + } + } + | { + vault: { + addr: string + } + } +export type Uint128 = string +export type VaultConfigUpdate = { + add_or_update: { + config: VaultConfigBaseForString + } +} +export type EmergencyUpdate = + | { + credit_manager: CmEmergencyUpdate + } + | { + red_bank: RedBankEmergencyUpdate + } +export type CmEmergencyUpdate = + | { + set_zero_max_ltv_on_vault: string + } + | { + set_zero_deposit_cap_on_vault: string + } + | { + disallow_coin: string + } +export type RedBankEmergencyUpdate = { + disable_borrowing: string +} +export interface AssetParamsBaseForString { + credit_manager: CmSettingsForString + denom: string + deposit_cap: Uint128 + liquidation_bonus: LiquidationBonus + liquidation_threshold: Decimal + max_loan_to_value: Decimal + protocol_liquidation_fee: Decimal + red_bank: RedBankSettings +} +export interface CmSettingsForString { + hls?: HlsParamsBaseForString | null + whitelisted: boolean +} +export interface HlsParamsBaseForString { + correlations: HlsAssetTypeForString[] + liquidation_threshold: Decimal + max_loan_to_value: Decimal +} +export interface LiquidationBonus { + max_lb: Decimal + min_lb: Decimal + slope: Decimal + starting_lb: Decimal +} +export interface RedBankSettings { + borrow_enabled: boolean + deposit_enabled: boolean +} +export interface VaultConfigBaseForString { + addr: string + deposit_cap: Coin + hls?: HlsParamsBaseForString | null + liquidation_threshold: Decimal + max_loan_to_value: Decimal + whitelisted: boolean +} +export interface Coin { + amount: Uint128 + denom: string + [k: string]: unknown +} +export type QueryMsg = + | { + owner: {} + } + | { + asset_params: { + denom: string + } + } + | { + all_asset_params: { + limit?: number | null + start_after?: string | null + } + } + | { + vault_config: { + address: string + } + } + | { + all_vault_configs: { + limit?: number | null + start_after?: string | null + } + } + | { + target_health_factor: {} + } + | { + total_deposit: { + denom: string + } + } +export type HlsAssetTypeForAddr = + | { + coin: { + denom: string + } + } + | { + vault: { + addr: Addr + } + } +export type Addr = string +export type ArrayOfAssetParamsBaseForAddr = AssetParamsBaseForAddr[] +export interface AssetParamsBaseForAddr { + credit_manager: CmSettingsForAddr + denom: string + deposit_cap: Uint128 + liquidation_bonus: LiquidationBonus + liquidation_threshold: Decimal + max_loan_to_value: Decimal + protocol_liquidation_fee: Decimal + red_bank: RedBankSettings +} +export interface CmSettingsForAddr { + hls?: HlsParamsBaseForAddr | null + whitelisted: boolean +} +export interface HlsParamsBaseForAddr { + correlations: HlsAssetTypeForAddr[] + liquidation_threshold: Decimal + max_loan_to_value: Decimal +} +export type ArrayOfVaultConfigBaseForAddr = VaultConfigBaseForAddr[] +export interface VaultConfigBaseForAddr { + addr: Addr + deposit_cap: Coin + hls?: HlsParamsBaseForAddr | null + liquidation_threshold: Decimal + max_loan_to_value: Decimal + whitelisted: boolean +} +export interface OwnerResponse { + abolished: boolean + emergency_owner?: string | null + initialized: boolean + owner?: string | null + proposed?: string | null +} +export interface TotalDepositResponse { + amount: Uint128 + cap: Uint128 + denom: string +} diff --git a/scripts/types/generated/mars-params/bundle.ts b/scripts/types/generated/mars-params/bundle.ts new file mode 100644 index 000000000..c78830ef8 --- /dev/null +++ b/scripts/types/generated/mars-params/bundle.ts @@ -0,0 +1,13 @@ +// @ts-nocheck +/** + * This file was automatically generated by @cosmwasm/ts-codegen@0.30.1. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run the @cosmwasm/ts-codegen generate command to regenerate this file. + */ + +import * as _12 from './MarsParams.types' +import * as _13 from './MarsParams.client' +import * as _14 from './MarsParams.react-query' +export namespace contracts { + export const MarsParams = { ..._12, ..._13, ..._14 } +} diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts index a29545a4b..d6c5cad7f 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts @@ -8,11 +8,11 @@ import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from '@cosmjs/cosmwasm-stargate' import { Coin, StdFee } from '@cosmjs/amino' import { - Decimal, InstantiateMsg, CreateOrUpdateConfig, ExecuteMsg, OwnerUpdate, + Decimal, Uint128, InitOrUpdateAssetParams, InterestRateModel, @@ -24,6 +24,8 @@ import { ArrayOfUncollateralizedLoanLimitResponse, UserCollateralResponse, ArrayOfUserCollateralResponse, + PaginationResponseForUserCollateralResponse, + Metadata, UserDebtResponse, ArrayOfUserDebtResponse, UserHealthStatus, @@ -67,22 +69,50 @@ export interface MarsRedBankReadOnlyInterface { user: string }) => Promise userCollateral: ({ + accountId, denom, user, }: { + accountId?: string denom: string user: string }) => Promise userCollaterals: ({ + accountId, limit, startAfter, user, }: { + accountId?: string limit?: number startAfter?: string user: string }) => Promise - userPosition: ({ user }: { user: string }) => Promise + userCollateralsV2: ({ + accountId, + limit, + startAfter, + user, + }: { + accountId?: string + limit?: number + startAfter?: string + user: string + }) => Promise + userPosition: ({ + accountId, + user, + }: { + accountId?: string + user: string + }) => Promise + userPositionLiquidationPricing: ({ + accountId, + user, + }: { + accountId?: string + user: string + }) => Promise scaledLiquidityAmount: ({ amount, denom }: { amount: Uint128; denom: string }) => Promise scaledDebtAmount: ({ amount, denom }: { amount: Uint128; denom: string }) => Promise underlyingLiquidityAmount: ({ @@ -116,7 +146,9 @@ export class MarsRedBankQueryClient implements MarsRedBankReadOnlyInterface { this.userDebts = this.userDebts.bind(this) this.userCollateral = this.userCollateral.bind(this) this.userCollaterals = this.userCollaterals.bind(this) + this.userCollateralsV2 = this.userCollateralsV2.bind(this) this.userPosition = this.userPosition.bind(this) + this.userPositionLiquidationPricing = this.userPositionLiquidationPricing.bind(this) this.scaledLiquidityAmount = this.scaledLiquidityAmount.bind(this) this.scaledDebtAmount = this.scaledDebtAmount.bind(this) this.underlyingLiquidityAmount = this.underlyingLiquidityAmount.bind(this) @@ -212,39 +244,86 @@ export class MarsRedBankQueryClient implements MarsRedBankReadOnlyInterface { }) } userCollateral = async ({ + accountId, denom, user, }: { + accountId?: string denom: string user: string }): Promise => { return this.client.queryContractSmart(this.contractAddress, { user_collateral: { + account_id: accountId, denom, user, }, }) } userCollaterals = async ({ + accountId, limit, startAfter, user, }: { + accountId?: string limit?: number startAfter?: string user: string }): Promise => { return this.client.queryContractSmart(this.contractAddress, { user_collaterals: { + account_id: accountId, + limit, + start_after: startAfter, + user, + }, + }) + } + userCollateralsV2 = async ({ + accountId, + limit, + startAfter, + user, + }: { + accountId?: string + limit?: number + startAfter?: string + user: string + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + user_collaterals_v2: { + account_id: accountId, limit, start_after: startAfter, user, }, }) } - userPosition = async ({ user }: { user: string }): Promise => { + userPosition = async ({ + accountId, + user, + }: { + accountId?: string + user: string + }): Promise => { return this.client.queryContractSmart(this.contractAddress, { user_position: { + account_id: accountId, + user, + }, + }) + } + userPositionLiquidationPricing = async ({ + accountId, + user, + }: { + accountId?: string + user: string + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + user_position_liquidation_pricing: { + account_id: accountId, user, }, }) @@ -365,8 +444,10 @@ export interface MarsRedBankInterface extends MarsRedBankReadOnlyInterface { ) => Promise deposit: ( { + accountId, onBehalfOf, }: { + accountId?: string onBehalfOf?: string }, fee?: number | StdFee | 'auto', @@ -375,12 +456,16 @@ export interface MarsRedBankInterface extends MarsRedBankReadOnlyInterface { ) => Promise withdraw: ( { + accountId, amount, denom, + liquidationRelated, recipient, }: { + accountId?: string amount?: Uint128 denom: string + liquidationRelated?: boolean recipient?: string }, fee?: number | StdFee | 'auto', @@ -584,8 +669,10 @@ export class MarsRedBankClient extends MarsRedBankQueryClient implements MarsRed } deposit = async ( { + accountId, onBehalfOf, }: { + accountId?: string onBehalfOf?: string }, fee: number | StdFee | 'auto' = 'auto', @@ -597,6 +684,7 @@ export class MarsRedBankClient extends MarsRedBankQueryClient implements MarsRed this.contractAddress, { deposit: { + account_id: accountId, on_behalf_of: onBehalfOf, }, }, @@ -607,12 +695,16 @@ export class MarsRedBankClient extends MarsRedBankQueryClient implements MarsRed } withdraw = async ( { + accountId, amount, denom, + liquidationRelated, recipient, }: { + accountId?: string amount?: Uint128 denom: string + liquidationRelated?: boolean recipient?: string }, fee: number | StdFee | 'auto' = 'auto', @@ -624,8 +716,10 @@ export class MarsRedBankClient extends MarsRedBankQueryClient implements MarsRed this.contractAddress, { withdraw: { + account_id: accountId, amount, denom, + liquidation_related: liquidationRelated, recipient, }, }, diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts index 394377bf0..bb32ebe4a 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts @@ -9,11 +9,11 @@ import { UseQueryOptions, useQuery, useMutation, UseMutationOptions } from '@tan import { ExecuteResult } from '@cosmjs/cosmwasm-stargate' import { StdFee, Coin } from '@cosmjs/amino' import { - Decimal, InstantiateMsg, CreateOrUpdateConfig, ExecuteMsg, OwnerUpdate, + Decimal, Uint128, InitOrUpdateAssetParams, InterestRateModel, @@ -25,6 +25,8 @@ import { ArrayOfUncollateralizedLoanLimitResponse, UserCollateralResponse, ArrayOfUserCollateralResponse, + PaginationResponseForUserCollateralResponse, + Metadata, UserDebtResponse, ArrayOfUserDebtResponse, UserHealthStatus, @@ -79,10 +81,25 @@ export const marsRedBankQueryKeys = { [ { ...marsRedBankQueryKeys.address(contractAddress)[0], method: 'user_collaterals', args }, ] as const, + userCollateralsV2: (contractAddress: string | undefined, args?: Record) => + [ + { ...marsRedBankQueryKeys.address(contractAddress)[0], method: 'user_collaterals_v2', args }, + ] as const, userPosition: (contractAddress: string | undefined, args?: Record) => [ { ...marsRedBankQueryKeys.address(contractAddress)[0], method: 'user_position', args }, ] as const, + userPositionLiquidationPricing: ( + contractAddress: string | undefined, + args?: Record, + ) => + [ + { + ...marsRedBankQueryKeys.address(contractAddress)[0], + method: 'user_position_liquidation_pricing', + args, + }, + ] as const, scaledLiquidityAmount: (contractAddress: string | undefined, args?: Record) => [ { @@ -220,9 +237,34 @@ export function useMarsRedBankScaledLiquidityAmountQuery({ { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, ) } +export interface MarsRedBankUserPositionLiquidationPricingQuery + extends MarsRedBankReactQuery { + args: { + accountId?: string + user: string + } +} +export function useMarsRedBankUserPositionLiquidationPricingQuery({ + client, + args, + options, +}: MarsRedBankUserPositionLiquidationPricingQuery) { + return useQuery( + marsRedBankQueryKeys.userPositionLiquidationPricing(client?.contractAddress, args), + () => + client + ? client.userPositionLiquidationPricing({ + accountId: args.accountId, + user: args.user, + }) + : Promise.reject(new Error('Invalid client')), + { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, + ) +} export interface MarsRedBankUserPositionQuery extends MarsRedBankReactQuery { args: { + accountId?: string user: string } } @@ -236,6 +278,33 @@ export function useMarsRedBankUserPositionQuery({ () => client ? client.userPosition({ + accountId: args.accountId, + user: args.user, + }) + : Promise.reject(new Error('Invalid client')), + { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, + ) +} +export interface MarsRedBankUserCollateralsV2Query + extends MarsRedBankReactQuery { + args: { + accountId?: string + limit?: number + startAfter?: string + user: string + } +} +export function useMarsRedBankUserCollateralsV2Query< + TData = PaginationResponseForUserCollateralResponse, +>({ client, args, options }: MarsRedBankUserCollateralsV2Query) { + return useQuery( + marsRedBankQueryKeys.userCollateralsV2(client?.contractAddress, args), + () => + client + ? client.userCollateralsV2({ + accountId: args.accountId, + limit: args.limit, + startAfter: args.startAfter, user: args.user, }) : Promise.reject(new Error('Invalid client')), @@ -245,6 +314,7 @@ export function useMarsRedBankUserPositionQuery({ export interface MarsRedBankUserCollateralsQuery extends MarsRedBankReactQuery { args: { + accountId?: string limit?: number startAfter?: string user: string @@ -260,6 +330,7 @@ export function useMarsRedBankUserCollateralsQuery client ? client.userCollaterals({ + accountId: args.accountId, limit: args.limit, startAfter: args.startAfter, user: args.user, @@ -271,6 +342,7 @@ export function useMarsRedBankUserCollateralsQuery extends MarsRedBankReactQuery { args: { + accountId?: string denom: string user: string } @@ -285,6 +357,7 @@ export function useMarsRedBankUserCollateralQuery client ? client.userCollateral({ + accountId: args.accountId, denom: args.denom, user: args.user, }) @@ -536,8 +609,10 @@ export function useMarsRedBankBorrowMutation( export interface MarsRedBankWithdrawMutation { client: MarsRedBankClient msg: { + accountId?: string amount?: Uint128 denom: string + liquidationRelated?: boolean recipient?: string } args?: { @@ -560,6 +635,7 @@ export function useMarsRedBankWithdrawMutation( export interface MarsRedBankDepositMutation { client: MarsRedBankClient msg: { + accountId?: string onBehalfOf?: string } args?: { diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts index 10a39d40e..5595744e7 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts @@ -5,14 +5,12 @@ * and run the @cosmwasm/ts-codegen generate command to regenerate this file. */ -export type Decimal = string export interface InstantiateMsg { config: CreateOrUpdateConfig owner: string } export interface CreateOrUpdateConfig { address_provider?: string | null - close_factor?: Decimal | null } export type ExecuteMsg = | { @@ -44,13 +42,16 @@ export type ExecuteMsg = } | { deposit: { + account_id?: string | null on_behalf_of?: string | null } } | { withdraw: { + account_id?: string | null amount?: Uint128 | null denom: string + liquidation_related?: boolean | null recipient?: string | null } } @@ -94,15 +95,10 @@ export type OwnerUpdate = } } | 'clear_emergency_owner' +export type Decimal = string export type Uint128 = string export interface InitOrUpdateAssetParams { - borrow_enabled?: boolean | null - deposit_cap?: Uint128 | null - deposit_enabled?: boolean | null interest_rate_model?: InterestRateModel | null - liquidation_bonus?: Decimal | null - liquidation_threshold?: Decimal | null - max_loan_to_value?: Decimal | null reserve_factor?: Decimal | null } export interface InterestRateModel { @@ -154,12 +150,22 @@ export type QueryMsg = } | { user_collateral: { + account_id?: string | null denom: string user: string } } | { user_collaterals: { + account_id?: string | null + limit?: number | null + start_after?: string | null + user: string + } + } + | { + user_collaterals_v2: { + account_id?: string | null limit?: number | null start_after?: string | null user: string @@ -167,6 +173,13 @@ export type QueryMsg = } | { user_position: { + account_id?: string | null + user: string + } + } + | { + user_position_liquidation_pricing: { + account_id?: string | null user: string } } @@ -196,27 +209,19 @@ export type QueryMsg = } export interface ConfigResponse { address_provider: string - close_factor: Decimal - emergency_owner?: string | null owner?: string | null proposed_new_owner?: string | null } export interface Market { - borrow_enabled: boolean borrow_index: Decimal borrow_rate: Decimal collateral_total_scaled: Uint128 debt_total_scaled: Uint128 denom: string - deposit_cap: Uint128 - deposit_enabled: boolean indexes_last_updated: number interest_rate_model: InterestRateModel - liquidation_bonus: Decimal - liquidation_threshold: Decimal liquidity_index: Decimal liquidity_rate: Decimal - max_loan_to_value: Decimal reserve_factor: Decimal } export type ArrayOfMarket = Market[] @@ -232,6 +237,13 @@ export interface UserCollateralResponse { enabled: boolean } export type ArrayOfUserCollateralResponse = UserCollateralResponse[] +export interface PaginationResponseForUserCollateralResponse { + data: UserCollateralResponse[] + metadata: Metadata +} +export interface Metadata { + has_more: boolean +} export interface UserDebtResponse { amount: Uint128 amount_scaled: Uint128 diff --git a/scripts/types/generated/mars-red-bank/bundle.ts b/scripts/types/generated/mars-red-bank/bundle.ts index 1277c55d3..98116bdb5 100644 --- a/scripts/types/generated/mars-red-bank/bundle.ts +++ b/scripts/types/generated/mars-red-bank/bundle.ts @@ -5,9 +5,9 @@ * and run the @cosmwasm/ts-codegen generate command to regenerate this file. */ -import * as _12 from './MarsRedBank.types' -import * as _13 from './MarsRedBank.client' -import * as _14 from './MarsRedBank.react-query' +import * as _15 from './MarsRedBank.types' +import * as _16 from './MarsRedBank.client' +import * as _17 from './MarsRedBank.react-query' export namespace contracts { - export const MarsRedBank = { ..._12, ..._13, ..._14 } + export const MarsRedBank = { ..._15, ..._16, ..._17 } } diff --git a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.client.ts b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.client.ts index 44808d0c6..b52768c4a 100644 --- a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.client.ts +++ b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.client.ts @@ -15,7 +15,10 @@ import { Coin, ExecuteMsg, OwnerUpdate, + Action, + ActionAmount, UpdateConfig, + ActionCoin, QueryMsg, ConfigResponse, } from './MarsRewardsCollectorBase.types' @@ -73,6 +76,18 @@ export interface MarsRewardsCollectorBaseInterface memo?: string, _funds?: Coin[], ) => Promise + withdrawFromCreditManager: ( + { + accountId, + actions, + }: { + accountId: string + actions: Action[] + }, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[], + ) => Promise distributeRewards: ( { amount, @@ -128,6 +143,7 @@ export class MarsRewardsCollectorBaseClient this.updateOwner = this.updateOwner.bind(this) this.updateConfig = this.updateConfig.bind(this) this.withdrawFromRedBank = this.withdrawFromRedBank.bind(this) + this.withdrawFromCreditManager = this.withdrawFromCreditManager.bind(this) this.distributeRewards = this.distributeRewards.bind(this) this.swapAsset = this.swapAsset.bind(this) this.claimIncentiveRewards = this.claimIncentiveRewards.bind(this) @@ -199,6 +215,32 @@ export class MarsRewardsCollectorBaseClient _funds, ) } + withdrawFromCreditManager = async ( + { + accountId, + actions, + }: { + accountId: string + actions: Action[] + }, + fee: number | StdFee | 'auto' = 'auto', + memo?: string, + _funds?: Coin[], + ): Promise => { + return await this.client.execute( + this.sender, + this.contractAddress, + { + withdraw_from_credit_manager: { + account_id: accountId, + actions, + }, + }, + fee, + memo, + _funds, + ) + } distributeRewards = async ( { amount, diff --git a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.react-query.ts b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.react-query.ts index 5db72cd7f..90b128d6b 100644 --- a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.react-query.ts +++ b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.react-query.ts @@ -16,7 +16,10 @@ import { Coin, ExecuteMsg, OwnerUpdate, + Action, + ActionAmount, UpdateConfig, + ActionCoin, QueryMsg, ConfigResponse, } from './MarsRewardsCollectorBase.types' @@ -130,6 +133,38 @@ export function useMarsRewardsCollectorBaseDistributeRewardsMutation( options, ) } +export interface MarsRewardsCollectorBaseWithdrawFromCreditManagerMutation { + client: MarsRewardsCollectorBaseClient + msg: { + accountId: string + actions: Action[] + } + args?: { + fee?: number | StdFee | 'auto' + memo?: string + funds?: Coin[] + } +} +export function useMarsRewardsCollectorBaseWithdrawFromCreditManagerMutation( + options?: Omit< + UseMutationOptions< + ExecuteResult, + Error, + MarsRewardsCollectorBaseWithdrawFromCreditManagerMutation + >, + 'mutationFn' + >, +) { + return useMutation< + ExecuteResult, + Error, + MarsRewardsCollectorBaseWithdrawFromCreditManagerMutation + >( + ({ client, msg, args: { fee, memo, funds } = {} }) => + client.withdrawFromCreditManager(msg, fee, memo, funds), + options, + ) +} export interface MarsRewardsCollectorBaseWithdrawFromRedBankMutation { client: MarsRewardsCollectorBaseClient msg: { diff --git a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.types.ts b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.types.ts index eaef259bb..a526497c2 100644 --- a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.types.ts +++ b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.types.ts @@ -43,6 +43,12 @@ export type ExecuteMsg = denom: string } } + | { + withdraw_from_credit_manager: { + account_id: string + actions: Action[] + } + } | { distribute_rewards: { amount?: Uint128 | null @@ -77,6 +83,24 @@ export type OwnerUpdate = } } | 'clear_emergency_owner' +export type Action = + | { + withdraw: ActionCoin + } + | { + withdraw_liquidity: { + lp_token: ActionCoin + slippage: Decimal + } + } + | { + unknown: {} + } +export type ActionAmount = + | 'account_balance' + | { + exact: Uint128 + } export interface UpdateConfig { address_provider?: string | null channel_id?: string | null @@ -87,6 +111,10 @@ export interface UpdateConfig { slippage_tolerance?: Decimal | null timeout_seconds?: number | null } +export interface ActionCoin { + amount: ActionAmount + denom: string +} export type QueryMsg = { config: {} } diff --git a/scripts/types/generated/mars-rewards-collector-base/bundle.ts b/scripts/types/generated/mars-rewards-collector-base/bundle.ts index 7b5021ad3..61bb087c8 100644 --- a/scripts/types/generated/mars-rewards-collector-base/bundle.ts +++ b/scripts/types/generated/mars-rewards-collector-base/bundle.ts @@ -5,9 +5,9 @@ * and run the @cosmwasm/ts-codegen generate command to regenerate this file. */ -import * as _15 from './MarsRewardsCollectorBase.types' -import * as _16 from './MarsRewardsCollectorBase.client' -import * as _17 from './MarsRewardsCollectorBase.react-query' +import * as _18 from './MarsRewardsCollectorBase.types' +import * as _19 from './MarsRewardsCollectorBase.client' +import * as _20 from './MarsRewardsCollectorBase.react-query' export namespace contracts { - export const MarsRewardsCollectorBase = { ..._15, ..._16, ..._17 } + export const MarsRewardsCollectorBase = { ..._18, ..._19, ..._20 } } diff --git a/scripts/types/generated/mars-swapper-astroport/bundle.ts b/scripts/types/generated/mars-swapper-astroport/bundle.ts index d829c0c49..9176313d6 100644 --- a/scripts/types/generated/mars-swapper-astroport/bundle.ts +++ b/scripts/types/generated/mars-swapper-astroport/bundle.ts @@ -5,9 +5,9 @@ * and run the @cosmwasm/ts-codegen generate command to regenerate this file. */ -import * as _18 from './MarsSwapperAstroport.types' -import * as _19 from './MarsSwapperAstroport.client' -import * as _20 from './MarsSwapperAstroport.react-query' +import * as _21 from './MarsSwapperAstroport.types' +import * as _22 from './MarsSwapperAstroport.client' +import * as _23 from './MarsSwapperAstroport.react-query' export namespace contracts { - export const MarsSwapperAstroport = { ..._18, ..._19, ..._20 } + export const MarsSwapperAstroport = { ..._21, ..._22, ..._23 } } diff --git a/scripts/types/generated/mars-swapper-osmosis/MarsSwapperOsmosis.types.ts b/scripts/types/generated/mars-swapper-osmosis/MarsSwapperOsmosis.types.ts index 0046c3fb7..0df6ac650 100644 --- a/scripts/types/generated/mars-swapper-osmosis/MarsSwapperOsmosis.types.ts +++ b/scripts/types/generated/mars-swapper-osmosis/MarsSwapperOsmosis.types.ts @@ -55,7 +55,6 @@ export type Addr = string export interface SwapAmountInRoute { pool_id: number token_out_denom: string - [k: string]: unknown } export interface Coin { amount: Uint128 diff --git a/scripts/types/generated/mars-swapper-osmosis/bundle.ts b/scripts/types/generated/mars-swapper-osmosis/bundle.ts index 66a5acd81..6b3187946 100644 --- a/scripts/types/generated/mars-swapper-osmosis/bundle.ts +++ b/scripts/types/generated/mars-swapper-osmosis/bundle.ts @@ -5,9 +5,9 @@ * and run the @cosmwasm/ts-codegen generate command to regenerate this file. */ -import * as _21 from './MarsSwapperOsmosis.types' -import * as _22 from './MarsSwapperOsmosis.client' -import * as _23 from './MarsSwapperOsmosis.react-query' +import * as _24 from './MarsSwapperOsmosis.types' +import * as _25 from './MarsSwapperOsmosis.client' +import * as _26 from './MarsSwapperOsmosis.react-query' export namespace contracts { - export const MarsSwapperOsmosis = { ..._21, ..._22, ..._23 } + export const MarsSwapperOsmosis = { ..._24, ..._25, ..._26 } } diff --git a/scripts/types/msg.ts b/scripts/types/msg.ts index 995958884..f3ca45fd6 100644 --- a/scripts/types/msg.ts +++ b/scripts/types/msg.ts @@ -1,3 +1,4 @@ +import { InstantiateMsg as ParamsInstantiateMsg } from './generated/mars-params/MarsParams.types' import { InstantiateMsg as AstroportSwapperInstantiateMsg } from './generated/mars-swapper-astroport/MarsSwapperAstroport.types' import { InstantiateMsg as RedBankInstantiateMsg } from './generated/mars-red-bank/MarsRedBank.types' import { InstantiateMsg as AddressProviderInstantiateMsg } from './generated/mars-address-provider/MarsAddressProvider.types' @@ -13,6 +14,7 @@ export type InstantiateMsgs = | IncentivesInstantiateMsg | WasmOracleInstantiateMsg | RewardsInstantiateMsg + | ParamsInstantiateMsg | AstroportSwapperInstantiateMsg | OsmosisSwapperInstantiateMsg | OsmosisOracleInstantiateMsg diff --git a/scripts/types/storageItems.ts b/scripts/types/storageItems.ts index 48e361593..2ec983b9d 100644 --- a/scripts/types/storageItems.ts +++ b/scripts/types/storageItems.ts @@ -20,8 +20,9 @@ export interface StorageItems { execute: { addressProviderUpdated: Record - assetsInitialized: string[] - secondAssetInitialized?: boolean + assetsUpdated: string[] + marketsUpdated: string[] + vaultsUpdated: string[] oraclePriceSet?: boolean smokeTest?: boolean } diff --git a/scripts/yarn.lock b/scripts/yarn.lock index ecbfacbcf..18c9665e7 100644 --- a/scripts/yarn.lock +++ b/scripts/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@aashutoshrathi/word-wrap@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" + integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== + "@ampproject/remapping@^2.1.0", "@ampproject/remapping@^2.2.0": version "2.2.1" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" @@ -1054,138 +1059,138 @@ "@noble/hashes" "^1.0.0" protobufjs "^6.8.8" -"@cosmjs/amino@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/amino/-/amino-0.30.1.tgz#7c18c14627361ba6c88e3495700ceea1f76baace" - integrity sha512-yNHnzmvAlkETDYIpeCTdVqgvrdt1qgkOXwuRVi8s27UKI5hfqyE9fJ/fuunXE6ZZPnKkjIecDznmuUOMrMvw4w== - dependencies: - "@cosmjs/crypto" "^0.30.1" - "@cosmjs/encoding" "^0.30.1" - "@cosmjs/math" "^0.30.1" - "@cosmjs/utils" "^0.30.1" - -"@cosmjs/cosmwasm-stargate@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/cosmwasm-stargate/-/cosmwasm-stargate-0.30.1.tgz#6f9ca310f75433a3e30d683bc6aa24eadb345d79" - integrity sha512-W/6SLUCJAJGBN+sJLXouLZikVgmqDd9LCdlMzQaxczcCHTWeJAmRvOiZGSZaSy3shw/JN1qc6g6PKpvTVgj10A== - dependencies: - "@cosmjs/amino" "^0.30.1" - "@cosmjs/crypto" "^0.30.1" - "@cosmjs/encoding" "^0.30.1" - "@cosmjs/math" "^0.30.1" - "@cosmjs/proto-signing" "^0.30.1" - "@cosmjs/stargate" "^0.30.1" - "@cosmjs/tendermint-rpc" "^0.30.1" - "@cosmjs/utils" "^0.30.1" - cosmjs-types "^0.7.1" +"@cosmjs/amino@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/amino/-/amino-0.31.0.tgz#49b33047295002804ad51bdf7ec0c2c97f1b553d" + integrity sha512-xJ5CCEK7H79FTpOuEmlpSzVI+ZeYESTVvO3wHDgbnceIyAne3C68SvyaKqLUR4uJB0Z4q4+DZHbqW6itUiv4lA== + dependencies: + "@cosmjs/crypto" "^0.31.0" + "@cosmjs/encoding" "^0.31.0" + "@cosmjs/math" "^0.31.0" + "@cosmjs/utils" "^0.31.0" + +"@cosmjs/cosmwasm-stargate@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/cosmwasm-stargate/-/cosmwasm-stargate-0.31.0.tgz#a9ea82471ca035b8d7f6ae640ad44b5f497be8c6" + integrity sha512-l6aX++3LhaAGZO46qIgrrNF40lYhOrdPfl35Z32ks6Wf3mwgbQEZwaxnoGzwUePY7/yaIiEFJ1JO6MlVPZVuag== + dependencies: + "@cosmjs/amino" "^0.31.0" + "@cosmjs/crypto" "^0.31.0" + "@cosmjs/encoding" "^0.31.0" + "@cosmjs/math" "^0.31.0" + "@cosmjs/proto-signing" "^0.31.0" + "@cosmjs/stargate" "^0.31.0" + "@cosmjs/tendermint-rpc" "^0.31.0" + "@cosmjs/utils" "^0.31.0" + cosmjs-types "^0.8.0" long "^4.0.0" pako "^2.0.2" -"@cosmjs/crypto@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/crypto/-/crypto-0.30.1.tgz#21e94d5ca8f8ded16eee1389d2639cb5c43c3eb5" - integrity sha512-rAljUlake3MSXs9xAm87mu34GfBLN0h/1uPPV6jEwClWjNkAMotzjC0ab9MARy5FFAvYHL3lWb57bhkbt2GtzQ== +"@cosmjs/crypto@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/crypto/-/crypto-0.31.0.tgz#0be3867ada0155da19c45a51f5fde08e84f9ec4b" + integrity sha512-UaqCe6Tgh0pe1QlZ66E13t6FlIF86QrnBXXq+EN7Xe1Rouza3fJ1ojGlPleJZkBoq3tAyYVIOOqdZIxtVj/sIQ== dependencies: - "@cosmjs/encoding" "^0.30.1" - "@cosmjs/math" "^0.30.1" - "@cosmjs/utils" "^0.30.1" + "@cosmjs/encoding" "^0.31.0" + "@cosmjs/math" "^0.31.0" + "@cosmjs/utils" "^0.31.0" "@noble/hashes" "^1" bn.js "^5.2.0" elliptic "^6.5.4" - libsodium-wrappers "^0.7.6" + libsodium-wrappers-sumo "^0.7.11" -"@cosmjs/encoding@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/encoding/-/encoding-0.30.1.tgz#b5c4e0ef7ceb1f2753688eb96400ed70f35c6058" - integrity sha512-rXmrTbgqwihORwJ3xYhIgQFfMSrwLu1s43RIK9I8EBudPx3KmnmyAKzMOVsRDo9edLFNuZ9GIvysUCwQfq3WlQ== +"@cosmjs/encoding@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/encoding/-/encoding-0.31.0.tgz#9a6fd80b59c35fc20638a6436128ad0be681eafc" + integrity sha512-NYGQDRxT7MIRSlcbAezwxK0FqnaSPKCH7O32cmfpHNWorFxhy9lwmBoCvoe59Kd0HmArI4h+NGzLEfX3OLnA4Q== dependencies: base64-js "^1.3.0" bech32 "^1.1.4" readonly-date "^1.0.0" -"@cosmjs/json-rpc@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/json-rpc/-/json-rpc-0.30.1.tgz#16f21305fc167598c8a23a45549b85106b2372bc" - integrity sha512-pitfC/2YN9t+kXZCbNuyrZ6M8abnCC2n62m+JtU9vQUfaEtVsgy+1Fk4TRQ175+pIWSdBMFi2wT8FWVEE4RhxQ== +"@cosmjs/json-rpc@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/json-rpc/-/json-rpc-0.31.0.tgz#38fda21188f2046db4a111fb5463ccde3c3751d7" + integrity sha512-Ix2Cil2qysiLNrX+E0w3vtwCrqxGVq8jklpLA7B2vtMrw7tru/rS65fdFSy8ep0wUNLL6Ud32VXa5K0YObDOMA== dependencies: - "@cosmjs/stream" "^0.30.1" + "@cosmjs/stream" "^0.31.0" xstream "^11.14.0" -"@cosmjs/math@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/math/-/math-0.30.1.tgz#8b816ef4de5d3afa66cb9fdfb5df2357a7845b8a" - integrity sha512-yaoeI23pin9ZiPHIisa6qqLngfnBR/25tSaWpkTm8Cy10MX70UF5oN4+/t1heLaM6SSmRrhk3psRkV4+7mH51Q== +"@cosmjs/math@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/math/-/math-0.31.0.tgz#c9fc5f8191df7c2375945d2eacce327dfbf26414" + integrity sha512-Sb/8Ry/+gKJaYiV6X8q45kxXC9FoV98XCY1WXtu0JQwOi61VCG2VXsURQnVvZ/EhR/CuT/swOlNKrqEs3da0fw== dependencies: bn.js "^5.2.0" -"@cosmjs/proto-signing@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/proto-signing/-/proto-signing-0.30.1.tgz#f0dda372488df9cd2677150b89b3e9c72b3cb713" - integrity sha512-tXh8pPYXV4aiJVhTKHGyeZekjj+K9s2KKojMB93Gcob2DxUjfKapFYBMJSgfKPuWUPEmyr8Q9km2hplI38ILgQ== - dependencies: - "@cosmjs/amino" "^0.30.1" - "@cosmjs/crypto" "^0.30.1" - "@cosmjs/encoding" "^0.30.1" - "@cosmjs/math" "^0.30.1" - "@cosmjs/utils" "^0.30.1" - cosmjs-types "^0.7.1" +"@cosmjs/proto-signing@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/proto-signing/-/proto-signing-0.31.0.tgz#7056963457cd967f53f56c2ab4491638e5ade2c0" + integrity sha512-JNlyOJRkn8EKB9mCthkjr6lVX6eyVQ09PFdmB4/DR874E62dFTvQ+YvyKMAgN7K7Dcjj26dVlAD3f6Xs7YOGDg== + dependencies: + "@cosmjs/amino" "^0.31.0" + "@cosmjs/crypto" "^0.31.0" + "@cosmjs/encoding" "^0.31.0" + "@cosmjs/math" "^0.31.0" + "@cosmjs/utils" "^0.31.0" + cosmjs-types "^0.8.0" long "^4.0.0" -"@cosmjs/socket@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/socket/-/socket-0.30.1.tgz#00b22f4b5e2ab01f4d82ccdb7b2e59536bfe5ce0" - integrity sha512-r6MpDL+9N+qOS/D5VaxnPaMJ3flwQ36G+vPvYJsXArj93BjgyFB7BwWwXCQDzZ+23cfChPUfhbINOenr8N2Kow== +"@cosmjs/socket@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/socket/-/socket-0.31.0.tgz#ffcae48251a68b4a1c37a1c996d8b123cd8ad5ac" + integrity sha512-WDh9gTyiP3OCXvSAJJn33+Ef3XqMWag+bpR1TdMBxTmlTxuvU+kPy4cf6P2OF+jkkUBEA5Se2EAju0eFbJMT+w== dependencies: - "@cosmjs/stream" "^0.30.1" + "@cosmjs/stream" "^0.31.0" isomorphic-ws "^4.0.1" ws "^7" xstream "^11.14.0" -"@cosmjs/stargate@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/stargate/-/stargate-0.30.1.tgz#e1b22e1226cffc6e93914a410755f1f61057ba04" - integrity sha512-RdbYKZCGOH8gWebO7r6WvNnQMxHrNXInY/gPHPzMjbQF6UatA6fNM2G2tdgS5j5u7FTqlCI10stNXrknaNdzog== +"@cosmjs/stargate@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/stargate/-/stargate-0.31.0.tgz#a7df1eaf1363513529607abaa52a5045aaaee0fd" + integrity sha512-GYhk9lzZPj/QmYHC0VV/4AMoRzVcOP+EnB1YZCoWlBdLuVmpBYKRagJqWIrIwdk1E0gF2ZoESd2TYfdh1fqIpg== dependencies: "@confio/ics23" "^0.6.8" - "@cosmjs/amino" "^0.30.1" - "@cosmjs/encoding" "^0.30.1" - "@cosmjs/math" "^0.30.1" - "@cosmjs/proto-signing" "^0.30.1" - "@cosmjs/stream" "^0.30.1" - "@cosmjs/tendermint-rpc" "^0.30.1" - "@cosmjs/utils" "^0.30.1" - cosmjs-types "^0.7.1" + "@cosmjs/amino" "^0.31.0" + "@cosmjs/encoding" "^0.31.0" + "@cosmjs/math" "^0.31.0" + "@cosmjs/proto-signing" "^0.31.0" + "@cosmjs/stream" "^0.31.0" + "@cosmjs/tendermint-rpc" "^0.31.0" + "@cosmjs/utils" "^0.31.0" + cosmjs-types "^0.8.0" long "^4.0.0" protobufjs "~6.11.3" xstream "^11.14.0" -"@cosmjs/stream@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/stream/-/stream-0.30.1.tgz#ba038a2aaf41343696b1e6e759d8e03a9516ec1a" - integrity sha512-Fg0pWz1zXQdoxQZpdHRMGvUH5RqS6tPv+j9Eh7Q953UjMlrwZVo0YFLC8OTf/HKVf10E4i0u6aM8D69Q6cNkgQ== +"@cosmjs/stream@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/stream/-/stream-0.31.0.tgz#7faf0f5ccd5ceffdd3b5d9fb81e292bb7a930b2c" + integrity sha512-Y+aSHwhHkLGIaQOdqRob+yga2zr9ifl9gZDKD+B7+R5pdWN5f2TTDhYWxA6YZcZ6xRmfr7u8a7tDh7iYLC/zKA== dependencies: xstream "^11.14.0" -"@cosmjs/tendermint-rpc@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/tendermint-rpc/-/tendermint-rpc-0.30.1.tgz#c16378892ba1ac63f72803fdf7567eab9d4f0aa0" - integrity sha512-Z3nCwhXSbPZJ++v85zHObeUggrEHVfm1u18ZRwXxFE9ZMl5mXTybnwYhczuYOl7KRskgwlB+rID0WYACxj4wdQ== - dependencies: - "@cosmjs/crypto" "^0.30.1" - "@cosmjs/encoding" "^0.30.1" - "@cosmjs/json-rpc" "^0.30.1" - "@cosmjs/math" "^0.30.1" - "@cosmjs/socket" "^0.30.1" - "@cosmjs/stream" "^0.30.1" - "@cosmjs/utils" "^0.30.1" +"@cosmjs/tendermint-rpc@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/tendermint-rpc/-/tendermint-rpc-0.31.0.tgz#df82f634ff08fc377dfdccea43a31d92b5b0eaf1" + integrity sha512-yo9xbeuI6UoEKIhFZ9g0dvUKLqnBzwdpEc/uldQygQc51j38gQVwFko+6sjmhieJqRYYvrYumcbJMiV6GFM9aA== + dependencies: + "@cosmjs/crypto" "^0.31.0" + "@cosmjs/encoding" "^0.31.0" + "@cosmjs/json-rpc" "^0.31.0" + "@cosmjs/math" "^0.31.0" + "@cosmjs/socket" "^0.31.0" + "@cosmjs/stream" "^0.31.0" + "@cosmjs/utils" "^0.31.0" axios "^0.21.2" readonly-date "^1.0.0" xstream "^11.14.0" -"@cosmjs/utils@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/utils/-/utils-0.30.1.tgz#6d92582341be3c2ec8d82090253cfa4b7f959edb" - integrity sha512-KvvX58MGMWh7xA+N+deCfunkA/ZNDvFLw4YbOmX3f/XBIkqrVY7qlotfy2aNb1kgp6h4B6Yc8YawJPDTfvWX7g== +"@cosmjs/utils@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/utils/-/utils-0.31.0.tgz#3a7ac16856dcff63bbf1bb11e31f975f71ef4f21" + integrity sha512-nNcycZWUYLNJlrIXgpcgVRqdl6BXjF4YlXdxobQWpW9Tikk61bEGeAFhDYtC0PwHlokCNw0KxWiHGJL4nL7Q5A== "@cosmwasm/ts-codegen@^0.30.1": version "0.30.1" @@ -1232,14 +1237,14 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz#cdd35dce4fa1a89a4fd42b1599eb35b3af408884" integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ== -"@eslint/eslintrc@^2.0.3": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.3.tgz#4910db5505f4d503f27774bf356e3704818a0331" - integrity sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ== +"@eslint/eslintrc@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.0.tgz#82256f164cc9e0b59669efc19d57f8092706841d" + integrity sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.5.2" + espree "^9.6.0" globals "^13.19.0" ignore "^5.2.0" import-fresh "^3.2.1" @@ -1247,10 +1252,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.42.0": - version "8.42.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.42.0.tgz#484a1d638de2911e6f5a30c12f49c7e4a3270fb6" - integrity sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw== +"@eslint/js@8.44.0": + version "8.44.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.44.0.tgz#961a5903c74139390478bdc808bcde3fc45ab7af" + integrity sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw== "@humanwhocodes/config-array@^0.11.10": version "0.11.10" @@ -1536,11 +1541,16 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== -"@types/node@*", "@types/node@>=13.7.0", "@types/node@^20.2.5": +"@types/node@*", "@types/node@>=13.7.0": version "20.3.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.0.tgz#719498898d5defab83c3560f45d8498f58d11938" integrity sha512-cumHmIAf6On83X7yP+LrsEyUOf/YlociZelmpRYaGFydoaPdxdt80MAbu6vWerQT2COCp2nPvHdsbD7tHn/YlQ== +"@types/node@^20.3.3": + version "20.3.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.3.tgz#329842940042d2b280897150e023e604d11657d6" + integrity sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw== + "@types/prettier@^2.6.1": version "2.7.3" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.3.tgz#3e51a17e291d01d17d3fc61422015a933af7a08f" @@ -1563,88 +1573,88 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^5.59.9": - version "5.59.9" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.9.tgz#2604cfaf2b306e120044f901e20c8ed926debf15" - integrity sha512-4uQIBq1ffXd2YvF7MAvehWKW3zVv/w+mSfRAu+8cKbfj3nwzyqJLNcZJpQ/WZ1HLbJDiowwmQ6NO+63nCA+fqA== +"@typescript-eslint/eslint-plugin@^5.61.0": + version "5.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.61.0.tgz#a1a5290cf33863b4db3fb79350b3c5275a7b1223" + integrity sha512-A5l/eUAug103qtkwccSCxn8ZRwT+7RXWkFECdA4Cvl1dOlDUgTpAOfSEElZn2uSUxhdDpnCdetrf0jvU4qrL+g== dependencies: "@eslint-community/regexpp" "^4.4.0" - "@typescript-eslint/scope-manager" "5.59.9" - "@typescript-eslint/type-utils" "5.59.9" - "@typescript-eslint/utils" "5.59.9" + "@typescript-eslint/scope-manager" "5.61.0" + "@typescript-eslint/type-utils" "5.61.0" + "@typescript-eslint/utils" "5.61.0" debug "^4.3.4" - grapheme-splitter "^1.0.4" + graphemer "^1.4.0" ignore "^5.2.0" natural-compare-lite "^1.4.0" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/parser@^5.59.9": - version "5.59.9" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.9.tgz#a85c47ccdd7e285697463da15200f9a8561dd5fa" - integrity sha512-FsPkRvBtcLQ/eVK1ivDiNYBjn3TGJdXy2fhXX+rc7czWl4ARwnpArwbihSOHI2Peg9WbtGHrbThfBUkZZGTtvQ== +"@typescript-eslint/parser@^5.61.0": + version "5.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.61.0.tgz#7fbe3e2951904bb843f8932ebedd6e0635bffb70" + integrity sha512-yGr4Sgyh8uO6fSi9hw3jAFXNBHbCtKKFMdX2IkT3ZqpKmtAq3lHS4ixB/COFuAIJpwl9/AqF7j72ZDWYKmIfvg== dependencies: - "@typescript-eslint/scope-manager" "5.59.9" - "@typescript-eslint/types" "5.59.9" - "@typescript-eslint/typescript-estree" "5.59.9" + "@typescript-eslint/scope-manager" "5.61.0" + "@typescript-eslint/types" "5.61.0" + "@typescript-eslint/typescript-estree" "5.61.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.59.9": - version "5.59.9" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.9.tgz#eadce1f2733389cdb58c49770192c0f95470d2f4" - integrity sha512-8RA+E+w78z1+2dzvK/tGZ2cpGigBZ58VMEHDZtpE1v+LLjzrYGc8mMaTONSxKyEkz3IuXFM0IqYiGHlCsmlZxQ== +"@typescript-eslint/scope-manager@5.61.0": + version "5.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.61.0.tgz#b670006d069c9abe6415c41f754b1b5d949ef2b2" + integrity sha512-W8VoMjoSg7f7nqAROEmTt6LoBpn81AegP7uKhhW5KzYlehs8VV0ZW0fIDVbcZRcaP3aPSW+JZFua+ysQN+m/Nw== dependencies: - "@typescript-eslint/types" "5.59.9" - "@typescript-eslint/visitor-keys" "5.59.9" + "@typescript-eslint/types" "5.61.0" + "@typescript-eslint/visitor-keys" "5.61.0" -"@typescript-eslint/type-utils@5.59.9": - version "5.59.9" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.9.tgz#53bfaae2e901e6ac637ab0536d1754dfef4dafc2" - integrity sha512-ksEsT0/mEHg9e3qZu98AlSrONAQtrSTljL3ow9CGej8eRo7pe+yaC/mvTjptp23Xo/xIf2mLZKC6KPv4Sji26Q== +"@typescript-eslint/type-utils@5.61.0": + version "5.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.61.0.tgz#e90799eb2045c4435ea8378cb31cd8a9fddca47a" + integrity sha512-kk8u//r+oVK2Aj3ph/26XdH0pbAkC2RiSjUYhKD+PExemG4XSjpGFeyZ/QM8lBOa7O8aGOU+/yEbMJgQv/DnCg== dependencies: - "@typescript-eslint/typescript-estree" "5.59.9" - "@typescript-eslint/utils" "5.59.9" + "@typescript-eslint/typescript-estree" "5.61.0" + "@typescript-eslint/utils" "5.61.0" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.59.9": - version "5.59.9" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.9.tgz#3b4e7ae63718ce1b966e0ae620adc4099a6dcc52" - integrity sha512-uW8H5NRgTVneSVTfiCVffBb8AbwWSKg7qcA4Ot3JI3MPCJGsB4Db4BhvAODIIYE5mNj7Q+VJkK7JxmRhk2Lyjw== +"@typescript-eslint/types@5.61.0": + version "5.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.61.0.tgz#e99ff11b5792d791554abab0f0370936d8ca50c0" + integrity sha512-ldyueo58KjngXpzloHUog/h9REmHl59G1b3a5Sng1GfBo14BkS3ZbMEb3693gnP1k//97lh7bKsp6/V/0v1veQ== -"@typescript-eslint/typescript-estree@5.59.9": - version "5.59.9" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.9.tgz#6bfea844e468427b5e72034d33c9fffc9557392b" - integrity sha512-pmM0/VQ7kUhd1QyIxgS+aRvMgw+ZljB3eDb+jYyp6d2bC0mQWLzUDF+DLwCTkQ3tlNyVsvZRXjFyV0LkU/aXjA== +"@typescript-eslint/typescript-estree@5.61.0": + version "5.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.61.0.tgz#4c7caca84ce95bb41aa585d46a764bcc050b92f3" + integrity sha512-Fud90PxONnnLZ36oR5ClJBLTLfU4pIWBmnvGwTbEa2cXIqj70AEDEmOmpkFComjBZ/037ueKrOdHuYmSFVD7Rw== dependencies: - "@typescript-eslint/types" "5.59.9" - "@typescript-eslint/visitor-keys" "5.59.9" + "@typescript-eslint/types" "5.61.0" + "@typescript-eslint/visitor-keys" "5.61.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.59.9": - version "5.59.9" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.9.tgz#adee890107b5ffe02cd46fdaa6c2125fb3c6c7c4" - integrity sha512-1PuMYsju/38I5Ggblaeb98TOoUvjhRvLpLa1DoTOFaLWqaXl/1iQ1eGurTXgBY58NUdtfTXKP5xBq7q9NDaLKg== +"@typescript-eslint/utils@5.61.0": + version "5.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.61.0.tgz#5064838a53e91c754fffbddd306adcca3fe0af36" + integrity sha512-mV6O+6VgQmVE6+xzlA91xifndPW9ElFW8vbSF0xCT/czPXVhwDewKila1jOyRwa9AE19zKnrr7Cg5S3pJVrTWQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.59.9" - "@typescript-eslint/types" "5.59.9" - "@typescript-eslint/typescript-estree" "5.59.9" + "@typescript-eslint/scope-manager" "5.61.0" + "@typescript-eslint/types" "5.61.0" + "@typescript-eslint/typescript-estree" "5.61.0" eslint-scope "^5.1.1" semver "^7.3.7" -"@typescript-eslint/visitor-keys@5.59.9": - version "5.59.9" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.9.tgz#9f86ef8e95aca30fb5a705bb7430f95fc58b146d" - integrity sha512-bT7s0td97KMaLwpEBckbzj/YohnvXtqbe2XgqNvTl6RJVakY5mvENOTPvw5u66nljfZxthESpDozs86U+oLY8Q== +"@typescript-eslint/visitor-keys@5.61.0": + version "5.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.61.0.tgz#c79414fa42158fd23bd2bb70952dc5cdbb298140" + integrity sha512-50XQ5VdbWrX06mQXhy93WywSFZZGsv3EOjq+lqp6WC2t+j3mb6A9xYVdrRxafvK88vg9k9u+CT4l6D8PEatjKg== dependencies: - "@typescript-eslint/types" "5.59.9" + "@typescript-eslint/types" "5.61.0" eslint-visitor-keys "^3.3.0" acorn-jsx@^5.3.2: @@ -1652,10 +1662,10 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.8.0: - version "8.8.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" - integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== +acorn@^8.9.0: + version "8.9.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.9.0.tgz#78a16e3b2bcc198c10822786fa6679e245db5b59" + integrity sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ== ajv@^6.10.0, ajv@^6.12.4: version "6.12.6" @@ -2001,14 +2011,6 @@ core-js-compat@^3.21.0, core-js-compat@^3.22.1: dependencies: browserslist "^4.21.5" -cosmjs-types@^0.7.1: - version "0.7.2" - resolved "https://registry.yarnpkg.com/cosmjs-types/-/cosmjs-types-0.7.2.tgz#a757371abd340949c5bd5d49c6f8379ae1ffd7e2" - integrity sha512-vf2uLyktjr/XVAgEq0DjMxeAWh1yYREe7AMHDKd7EiHVqxBPCaBS+qEEQUkXbR9ndnckqr1sUG8BQhazh4X5lA== - dependencies: - long "^4.0.0" - protobufjs "~6.11.2" - cosmjs-types@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/cosmjs-types/-/cosmjs-types-0.8.0.tgz#2ed78f3e990f770229726f95f3ef5bf9e2b6859b" @@ -2178,15 +2180,15 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== -eslint@^8.42.0: - version "8.42.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.42.0.tgz#7bebdc3a55f9ed7167251fe7259f75219cade291" - integrity sha512-ulg9Ms6E1WPf67PHaEY4/6E2tEn5/f7FXGzr3t9cBMugOmf1INYvuUwwh1aXQN4MfJ6a5K2iNwP3w4AColvI9A== +eslint@^8.44.0: + version "8.44.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.44.0.tgz#51246e3889b259bbcd1d7d736a0c10add4f0e500" + integrity sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.4.0" - "@eslint/eslintrc" "^2.0.3" - "@eslint/js" "8.42.0" + "@eslint/eslintrc" "^2.1.0" + "@eslint/js" "8.44.0" "@humanwhocodes/config-array" "^0.11.10" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" @@ -2198,7 +2200,7 @@ eslint@^8.42.0: escape-string-regexp "^4.0.0" eslint-scope "^7.2.0" eslint-visitor-keys "^3.4.1" - espree "^9.5.2" + espree "^9.6.0" esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" @@ -2218,17 +2220,17 @@ eslint@^8.42.0: lodash.merge "^4.6.2" minimatch "^3.1.2" natural-compare "^1.4.0" - optionator "^0.9.1" + optionator "^0.9.3" strip-ansi "^6.0.1" strip-json-comments "^3.1.0" text-table "^0.2.0" -espree@^9.5.2: - version "9.5.2" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.2.tgz#e994e7dc33a082a7a82dceaf12883a829353215b" - integrity sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw== +espree@^9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.0.tgz#80869754b1c6560f32e3b6929194a3fe07c5b82f" + integrity sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A== dependencies: - acorn "^8.8.0" + acorn "^8.9.0" acorn-jsx "^5.3.2" eslint-visitor-keys "^3.4.1" @@ -2519,11 +2521,6 @@ graceful-fs@^4.1.15, graceful-fs@^4.2.9: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -grapheme-splitter@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" - integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== - graphemer@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" @@ -2864,17 +2861,17 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -libsodium-wrappers@^0.7.6: +libsodium-sumo@^0.7.11: version "0.7.11" - resolved "https://registry.yarnpkg.com/libsodium-wrappers/-/libsodium-wrappers-0.7.11.tgz#53bd20606dffcc54ea2122133c7da38218f575f7" - integrity sha512-SrcLtXj7BM19vUKtQuyQKiQCRJPgbpauzl3s0rSwD+60wtHqSUuqcoawlMDheCJga85nKOQwxNYQxf/CKAvs6Q== - dependencies: - libsodium "^0.7.11" + resolved "https://registry.yarnpkg.com/libsodium-sumo/-/libsodium-sumo-0.7.11.tgz#ab0389e2424fca5c1dc8c4fd394906190da88a11" + integrity sha512-bY+7ph7xpk51Ez2GbE10lXAQ5sJma6NghcIDaSPbM/G9elfrjLa0COHl/7P6Wb/JizQzl5UQontOOP1z0VwbLA== -libsodium@^0.7.11: +libsodium-wrappers-sumo@^0.7.11: version "0.7.11" - resolved "https://registry.yarnpkg.com/libsodium/-/libsodium-0.7.11.tgz#cd10aae7bcc34a300cc6ad0ac88fcca674cfbc2e" - integrity sha512-WPfJ7sS53I2s4iM58QxY3Inb83/6mjlYgcmZs7DJsvDlnmVUwNinBCi5vBT43P6bHRy01O4zsMU2CoVR6xJ40A== + resolved "https://registry.yarnpkg.com/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.11.tgz#d96329ee3c0e7ec7f5fcf4cdde16cc3a1ae91d82" + integrity sha512-DGypHOmJbB1nZn89KIfGOAkDgfv5N6SBGC3Qvmy/On0P0WD1JQvNRS/e3UL3aFF+xC0m+MYz5M+MnRnK2HMrKQ== + dependencies: + libsodium-sumo "^0.7.11" locate-path@^5.0.0: version "5.0.0" @@ -3099,17 +3096,17 @@ onetime@^2.0.0: dependencies: mimic-fn "^1.0.0" -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== +optionator@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" + integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== dependencies: + "@aashutoshrathi/word-wrap" "^1.2.3" deep-is "^0.1.3" fast-levenshtein "^2.0.6" levn "^0.4.1" prelude-ls "^1.2.1" type-check "^0.4.0" - word-wrap "^1.2.3" os-tmpdir@~1.0.2: version "1.0.2" @@ -3627,10 +3624,10 @@ type@^2.7.2: resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== -typescript@^5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.3.tgz#8d84219244a6b40b6fb2b33cc1c062f715b9e826" - integrity sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw== +typescript@^5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" + integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" @@ -3708,11 +3705,6 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -word-wrap@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== - wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"