From b35f421337afe0a2fad28766f266afb3a387e822 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Fri, 14 Jun 2024 15:31:01 +0100 Subject: [PATCH 01/98] feat: e2e test scaffolding --- Cargo.lock | 2395 +++++++++++++++-- contracts/sumtree-orderbook/Cargo.toml | 3 +- contracts/sumtree-orderbook/src/contract.rs | 3 + contracts/sumtree-orderbook/src/msg.rs | 3 + contracts/sumtree-orderbook/src/query.rs | 9 +- .../src/tests/e2e/cases/mod.rs | 2 + .../tests/e2e/cases/test_orders_success.rs | 157 ++ .../src/tests/e2e/cases/utils.rs | 422 +++ .../sumtree-orderbook/src/tests/e2e/mod.rs | 5 + .../src/tests/e2e/modules/cosmwasm_pool.rs | 43 + .../src/tests/e2e/modules/mod.rs | 1 + .../src/tests/e2e/test_env.rs | 240 ++ contracts/sumtree-orderbook/src/tests/mod.rs | 2 + 13 files changed, 3025 insertions(+), 260 deletions(-) create mode 100644 contracts/sumtree-orderbook/src/tests/e2e/cases/mod.rs create mode 100644 contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs create mode 100644 contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs create mode 100644 contracts/sumtree-orderbook/src/tests/e2e/mod.rs create mode 100644 contracts/sumtree-orderbook/src/tests/e2e/modules/cosmwasm_pool.rs create mode 100644 contracts/sumtree-orderbook/src/tests/e2e/modules/mod.rs create mode 100644 contracts/sumtree-orderbook/src/tests/e2e/test_env.rs diff --git a/Cargo.lock b/Cargo.lock index ebecff7..84a17f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "ahash" version = "0.7.8" @@ -13,18 +28,53 @@ dependencies = [ "version_check", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "anyhow" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" +[[package]] +name = "async-trait" +version = "0.1.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.49", +] + [[package]] name = "autocfg" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base16ct" version = "0.2.0" @@ -49,6 +99,57 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bindgen" +version = "0.69.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +dependencies = [ + "bitflags 2.5.0", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.49", + "which", +] + +[[package]] +name = "bip32" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e141fb0f8be1c7b45887af94c88b182472b57c96b56773250ae00cd6a14a164" +dependencies = [ + "bs58", + "hmac", + "k256", + "rand_core 0.6.4", + "ripemd", + "sha2 0.10.8", + "subtle", + "zeroize", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + [[package]] name = "block-buffer" version = "0.9.0" @@ -73,6 +174,21 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56953345e39537a3e18bdaeba4cb0c58a78c1f61f361dc0fa7c5c7340ae87c5f" +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "sha2 0.10.8", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "byteorder" version = "1.5.0" @@ -84,6 +200,24 @@ name = "bytes" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] [[package]] name = "cfg-if" @@ -100,12 +234,71 @@ dependencies = [ "num-traits", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "cosmos-sdk-proto" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32560304ab4c365791fd307282f76637213d8083c1a98490c35159cd67852237" +dependencies = [ + "prost 0.12.3", + "prost-types 0.12.3", + "tendermint-proto", +] + +[[package]] +name = "cosmrs" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47126f5364df9387b9d8559dcef62e99010e1d4098f39eb3f7ee4b5c254e40ea" +dependencies = [ + "bip32", + "cosmos-sdk-proto", + "ecdsa", + "eyre", + "k256", + "rand_core 0.6.4", + "serde", + "serde_json", + "signature", + "subtle-encoding", + "tendermint", + "tendermint-rpc", + "thiserror", +] + [[package]] name = "cosmwasm-crypto" version = "1.5.5" @@ -219,6 +412,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "curve25519-dalek-ng" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.6.4", + "subtle-ng", + "zeroize", +] + [[package]] name = "cw-multi-test" version = "0.18.1" @@ -289,6 +495,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "derivative" version = "2.2.0" @@ -321,6 +536,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.49", +] + [[package]] name = "dyn-clone" version = "1.0.16" @@ -341,6 +567,29 @@ dependencies = [ "spki", ] +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-consensus" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" +dependencies = [ + "curve25519-dalek-ng", + "hex", + "rand_core 0.6.4", + "sha2 0.9.9", + "zeroize", +] + [[package]] name = "ed25519-zebra" version = "3.1.0" @@ -348,7 +597,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" dependencies = [ "curve25519-dalek", - "hashbrown", + "hashbrown 0.12.3", "hex", "rand_core 0.6.4", "serde", @@ -381,6 +630,41 @@ dependencies = [ "zeroize", ] +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "ff" version = "0.13.0" @@ -391,12 +675,98 @@ dependencies = [ "subtle", ] +[[package]] +name = "flex-error" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c606d892c9de11507fa0dcffc116434f94e105d0bbdc4e405b61519464c49d7b" +dependencies = [ + "eyre", + "paste", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + [[package]] name = "forward_ref" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", + "pin-utils", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -415,10 +785,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "group" version = "0.13.0" @@ -430,6 +814,25 @@ dependencies = [ "subtle", ] +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -439,6 +842,18 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "hex" version = "0.4.3" @@ -455,43 +870,285 @@ dependencies = [ ] [[package]] -name = "itertools" -version = "0.10.5" +name = "home" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "either", + "windows-sys 0.52.0", ] [[package]] -name = "itertools" -version = "0.11.0" +name = "http" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ - "either", + "bytes", + "fnv", + "itoa", ] [[package]] -name = "itertools" -version = "0.12.1" +name = "http-body" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ - "either", + "bytes", + "http", + "pin-project-lite", ] [[package]] -name = "itoa" -version = "1.0.10" +name = "httparse" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "d0e7a4dd27b9476dc40cb050d3632d3bba3a70ddbff012285f7f8559a1e7e545" [[package]] -name = "k256" -version = "0.13.1" +name = "httpdate" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.49", +] + +[[package]] +name = "idna" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed" +dependencies = [ + "icu_normalizer", + "icu_properties", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown 0.14.5", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" dependencies = [ "cfg-if", "ecdsa", @@ -501,12 +1158,117 @@ dependencies = [ "signature", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libloading" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +dependencies = [ + "cfg-if", + "windows-targets 0.52.5", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -516,6 +1278,25 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -528,17 +1309,23 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + [[package]] name = "osmosis-std" -version = "0.16.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75895e4db1a81ca29118e366365744f64314938327e4eedba8e6e462fb15e94f" +checksum = "ca66dca7e8c9b11b995cd41a44c038134ccca4469894d663d8a9452d6e716241" dependencies = [ "chrono", "cosmwasm-std", - "osmosis-std-derive 0.16.2", - "prost 0.11.9", - "prost-types", + "osmosis-std-derive 0.20.1", + "prost 0.12.3", + "prost-types 0.12.3", "schemars", "serde", "serde-cw-value", @@ -558,373 +1345,1473 @@ dependencies = [ [[package]] name = "osmosis-std-derive" -version = "0.16.2" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47f0b2f22adb341bb59e5a3a1b464dde033181954bd055b9ae86d6511ba465b" +checksum = "c5ebdfd1bc8ed04db596e110c6baa9b174b04f6ed1ec22c666ddc5cb3fa91bd7" dependencies = [ "itertools 0.10.5", "proc-macro2", - "prost-types", + "prost-types 0.11.9", "quote", "syn 1.0.109", ] [[package]] -name = "pkcs8" -version = "0.10.2" +name = "osmosis-test-tube" +version = "25.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +checksum = "5eb35dcc9adc1b39e23dfae07c9f04a60187fde57a52b7762434ea6548581a1a" dependencies = [ - "der", - "spki", + "base64", + "bindgen", + "cosmrs", + "cosmwasm-std", + "osmosis-std", + "prost 0.12.3", + "serde", + "serde_json", + "test-tube", + "thiserror", ] [[package]] -name = "ppv-lite86" -version = "0.2.17" +name = "paste" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] -name = "proc-macro2" -version = "1.0.78" +name = "peg" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "8a625d12ad770914cbf7eff6f9314c3ef803bfe364a1b20bc36ddf56673e71e5" dependencies = [ - "unicode-ident", + "peg-macros", + "peg-runtime", ] [[package]] -name = "prost" -version = "0.11.9" +name = "peg-macros" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f241d42067ed3ab6a4fece1db720838e1418f36d868585a27931f95d6bc03582" +dependencies = [ + "peg-runtime", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-runtime" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.49", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "prettyplease" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3928fb5db768cb86f891ff014f0144589297e3c6a1aba6ed7cecfdace270c7" +dependencies = [ + "proc-macro2", + "syn 2.0.49", +] + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" +dependencies = [ + "bytes", + "prost-derive 0.11.9", +] + +[[package]] +name = "prost" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +dependencies = [ + "bytes", + "prost-derive 0.12.3", +] + +[[package]] +name = "prost-derive" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +dependencies = [ + "anyhow", + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "prost-derive" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +dependencies = [ + "anyhow", + "itertools 0.11.0", + "proc-macro2", + "quote", + "syn 2.0.49", +] + +[[package]] +name = "prost-types" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" +dependencies = [ + "prost 0.11.9", +] + +[[package]] +name = "prost-types" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +dependencies = [ + "prost 0.12.3", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "schemars" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 1.0.109", +] + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +dependencies = [ + "bitflags 2.5.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" + +[[package]] +name = "serde" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-cw-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75d32da6b8ed758b7d850b6c3c08f1d7df51a4df3cb201296e63e34a78e99d4" +dependencies = [ + "serde", +] + +[[package]] +name = "serde-json-wasm" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9213a07d53faa0b8dd81e767a54a8188a242fdb9be99ab75ec576a774bfdd7" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_bytes" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.49", +] + +[[package]] +name = "serde_derive_internals" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "serde_json" +version = "1.0.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.49", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "subtle-encoding" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcb1ed7b8330c5eed5441052651dd7a12c75e2ed88f2ec024ae1fa3a5e59945" +dependencies = [ + "zeroize", +] + +[[package]] +name = "subtle-ng" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" + +[[package]] +name = "sumtree-orderbook" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test", + "cw-storage-plus", + "cw-utils", + "cw2", + "osmosis-std", + "osmosis-std-derive 0.15.3", + "osmosis-test-tube", + "prost 0.11.9", + "rand", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.49", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tendermint" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15ab8f0a25d0d2ad49ac615da054d6a76aa6603ff95f7d18bafdd34450a1a04b" +dependencies = [ + "bytes", + "digest 0.10.7", + "ed25519", + "ed25519-consensus", + "flex-error", + "futures", + "k256", + "num-traits", + "once_cell", + "prost 0.12.3", + "prost-types 0.12.3", + "ripemd", + "serde", + "serde_bytes", + "serde_json", + "serde_repr", + "sha2 0.10.8", + "signature", + "subtle", + "subtle-encoding", + "tendermint-proto", + "time", + "zeroize", +] + +[[package]] +name = "tendermint-config" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1a02da769166e2052cd537b1a97c78017632c2d9e19266367b27e73910434fc" +dependencies = [ + "flex-error", + "serde", + "serde_json", + "tendermint", + "toml", + "url", +] + +[[package]] +name = "tendermint-proto" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b797dd3d2beaaee91d2f065e7bdf239dc8d80bba4a183a288bc1279dd5a69a1e" +dependencies = [ + "bytes", + "flex-error", + "num-derive", + "num-traits", + "prost 0.12.3", + "prost-types 0.12.3", + "serde", + "serde_bytes", + "subtle-encoding", + "time", +] + +[[package]] +name = "tendermint-rpc" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71afae8bb5f6b14ed48d4e1316a643b6c2c3cbad114f510be77b4ed20b7b3e42" +dependencies = [ + "async-trait", + "bytes", + "flex-error", + "futures", + "getrandom", + "peg", + "pin-project", + "rand", + "reqwest", + "semver", + "serde", + "serde_bytes", + "serde_json", + "subtle", + "subtle-encoding", + "tendermint", + "tendermint-config", + "tendermint-proto", + "thiserror", + "time", + "tokio", + "tracing", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "test-tube" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804bb9bda992b6cda6f883e7973cb999d4da90d21257fb918d6a693407148681" +dependencies = [ + "base64", + "cosmrs", + "cosmwasm-std", + "osmosis-std", + "prost 0.12.3", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "thiserror" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.49", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.49", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ - "bytes", - "prost-derive 0.11.9", + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.49", + "wasm-bindgen-shared", ] [[package]] -name = "prost" -version = "0.12.3" +name = "wasm-bindgen-futures" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ - "bytes", - "prost-derive 0.12.3", + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "prost-derive" -version = "0.11.9" +name = "wasm-bindgen-macro" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ - "anyhow", - "itertools 0.10.5", - "proc-macro2", "quote", - "syn 1.0.109", + "wasm-bindgen-macro-support", ] [[package]] -name = "prost-derive" -version = "0.12.3" +name = "wasm-bindgen-macro-support" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ - "anyhow", - "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.49", + "wasm-bindgen-backend", + "wasm-bindgen-shared", ] [[package]] -name = "prost-types" -version = "0.11.9" +name = "wasm-bindgen-shared" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ - "prost 0.11.9", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "quote" -version = "1.0.35" +name = "which" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" dependencies = [ - "proc-macro2", + "either", + "home", + "once_cell", + "rustix", ] [[package]] -name = "rand" -version = "0.8.5" +name = "winapi-util" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "libc", - "rand_chacha", - "rand_core 0.6.4", + "windows-sys 0.52.0", ] [[package]] -name = "rand_chacha" -version = "0.3.1" +name = "windows-sys" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "windows-targets 0.48.5", ] [[package]] -name = "rand_core" -version = "0.5.1" +name = "windows-sys" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", +] [[package]] -name = "rand_core" -version = "0.6.4" +name = "windows-targets" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "getrandom", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] -name = "rfc6979" -version = "0.4.0" +name = "windows-targets" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "hmac", - "subtle", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] -name = "ryu" -version = "1.0.16" +name = "windows_aarch64_gnullvm" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] -name = "schemars" -version = "0.8.16" +name = "windows_aarch64_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" -dependencies = [ - "dyn-clone", - "schemars_derive", - "serde", - "serde_json", -] +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] -name = "schemars_derive" -version = "0.8.16" +name = "windows_aarch64_msvc" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 1.0.109", -] +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] -name = "sec1" -version = "0.7.3" +name = "windows_aarch64_msvc" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] -name = "semver" -version = "1.0.21" +name = "windows_i686_gnu" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] -name = "serde" -version = "1.0.196" +name = "windows_i686_gnu" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" -dependencies = [ - "serde_derive", -] +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" [[package]] -name = "serde-cw-value" -version = "0.7.0" +name = "windows_i686_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75d32da6b8ed758b7d850b6c3c08f1d7df51a4df3cb201296e63e34a78e99d4" -dependencies = [ - "serde", -] +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] -name = "serde-json-wasm" -version = "0.5.2" +name = "windows_i686_msvc" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e9213a07d53faa0b8dd81e767a54a8188a242fdb9be99ab75ec576a774bfdd7" -dependencies = [ - "serde", -] +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] -name = "serde_derive" -version = "1.0.196" +name = "windows_i686_msvc" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.49", -] +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] -name = "serde_derive_internals" -version = "0.26.0" +name = "windows_x86_64_gnu" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] -name = "serde_json" -version = "1.0.113" +name = "windows_x86_64_gnu" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" -dependencies = [ - "itoa", - "ryu", - "serde", -] +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] -name = "sha2" -version = "0.9.9" +name = "windows_x86_64_gnullvm" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", -] +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] -name = "sha2" -version = "0.10.8" +name = "windows_x86_64_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.7", -] +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] -name = "signature" -version = "2.2.0" +name = "windows_x86_64_msvc" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", -] +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] -name = "spki" -version = "0.7.3" +name = "windows_x86_64_msvc" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "base64ct", - "der", + "cfg-if", + "windows-sys 0.48.0", ] [[package]] -name = "static_assertions" -version = "1.1.0" +name = "write16" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" [[package]] -name = "subtle" -version = "2.5.0" +name = "writeable" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] -name = "sumtree-orderbook" -version = "0.1.0" +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-multi-test", - "cw-storage-plus", - "cw-utils", - "cw2", - "osmosis-std", - "osmosis-std-derive 0.15.3", - "prost 0.11.9", - "rand", - "schemars", "serde", - "thiserror", + "stable_deref_trait", + "yoke-derive", + "zerofrom", ] [[package]] -name = "syn" -version = "1.0.109" +name = "yoke-derive" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" dependencies = [ "proc-macro2", "quote", - "unicode-ident", + "syn 2.0.49", + "synstructure", ] [[package]] -name = "syn" -version = "2.0.49" +name = "zerofrom" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" dependencies = [ "proc-macro2", "quote", - "unicode-ident", + "syn 2.0.49", + "synstructure", ] [[package]] -name = "thiserror" -version = "1.0.57" +name = "zeroize" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" dependencies = [ - "thiserror-impl", + "zeroize_derive", ] [[package]] -name = "thiserror-impl" -version = "1.0.57" +name = "zeroize_derive" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", @@ -932,31 +2819,23 @@ dependencies = [ ] [[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +name = "zerovec" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] [[package]] -name = "zeroize" -version = "1.7.0" +name = "zerovec-derive" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.49", +] diff --git a/contracts/sumtree-orderbook/Cargo.toml b/contracts/sumtree-orderbook/Cargo.toml index 9eed4dc..e194377 100644 --- a/contracts/sumtree-orderbook/Cargo.toml +++ b/contracts/sumtree-orderbook/Cargo.toml @@ -52,7 +52,7 @@ schemars = "0.8.15" serde = { version = "1.0.189", default-features = false, features = ["derive"] } thiserror = { version = "1.0.49" } osmosis-std-derive = "0.15.3" -osmosis-std = "0.16.0" +osmosis-std = "0.25.0" prost = { version = "0.11.2", default-features = false, features = [ "prost-derive", ] } @@ -61,3 +61,4 @@ prost = { version = "0.11.2", default-features = false, features = [ [dev-dependencies] cw-multi-test = "0.18.0" rand = "0.8.4" +osmosis-test-tube = "25.0.0" diff --git a/contracts/sumtree-orderbook/src/contract.rs b/contracts/sumtree-orderbook/src/contract.rs index 2521f7e..94762dd 100644 --- a/contracts/sumtree-orderbook/src/contract.rs +++ b/contracts/sumtree-orderbook/src/contract.rs @@ -143,6 +143,9 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> ContractResult { } => Ok(to_json_binary(&query::orders_by_owner( deps, owner, start_from, end_at, limit, )?)?), + QueryMsg::Order { tick_id, order_id } => { + Ok(to_json_binary(&query::order(deps, tick_id, order_id)?)?) + } QueryMsg::Denoms {} => Ok(to_json_binary(&query::denoms(deps)?)?), QueryMsg::GetMakerFee {} => Ok(to_json_binary(&state::get_maker_fee(deps.storage)?)?), diff --git a/contracts/sumtree-orderbook/src/msg.rs b/contracts/sumtree-orderbook/src/msg.rs index 8577344..86aa30b 100644 --- a/contracts/sumtree-orderbook/src/msg.rs +++ b/contracts/sumtree-orderbook/src/msg.rs @@ -119,6 +119,9 @@ pub enum QueryMsg { limit: Option, }, + #[returns(crate::types::LimitOrder)] + Order { tick_id: i64, order_id: u64 }, + #[returns(DenomsResponse)] Denoms {}, } diff --git a/contracts/sumtree-orderbook/src/query.rs b/contracts/sumtree-orderbook/src/query.rs index f2754e9..edd3d0a 100644 --- a/contracts/sumtree-orderbook/src/query.rs +++ b/contracts/sumtree-orderbook/src/query.rs @@ -11,7 +11,9 @@ use crate::{ GetTotalPoolLiquidityResponse, SpotPriceResponse, TickIdAndState, }, order, - state::{get_directional_liquidity, get_orders_by_owner, IS_ACTIVE, ORDERBOOK, TICK_STATE}, + state::{ + get_directional_liquidity, get_orders_by_owner, orders, IS_ACTIVE, ORDERBOOK, TICK_STATE, + }, sudo::ensure_swap_fee, tick_math::tick_to_price, types::{FilterOwnerOrders, LimitOrder, MarketOrder, OrderDirection}, @@ -214,3 +216,8 @@ pub(crate) fn denoms(deps: Deps) -> ContractResult { base_denom: orderbook.base_denom, }) } + +pub(crate) fn order(deps: Deps, tick_id: i64, order_id: u64) -> ContractResult { + let order = orders().load(deps.storage, &(tick_id, order_id))?; + Ok(order) +} diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/mod.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/mod.rs new file mode 100644 index 0000000..4dcf09b --- /dev/null +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/mod.rs @@ -0,0 +1,2 @@ +mod test_orders_success; +mod utils; diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs new file mode 100644 index 0000000..32ec544 --- /dev/null +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs @@ -0,0 +1,157 @@ +use cosmwasm_std::Decimal256; +use osmosis_test_tube::{Module, OsmosisTestApp}; + +use super::utils::{assert, orders}; +use crate::{ + constants::{MAX_TICK, MIN_TICK}, + setup, + tests::e2e::modules::cosmwasm_pool::CosmwasmPool, + types::OrderDirection, +}; + +struct BasicFulfilledOrderTestCase { + name: &'static str, + placed_amount: u128, + filled_amount: u128, + tick_id: i64, + claim_bounty: Option, + order_direction: OrderDirection, + claimer: &'static str, +} + +#[test] +fn test_basic_order() { + let cases = vec![ + BasicFulfilledOrderTestCase { + name: "basic fulfilled ask", + placed_amount: 10u128, + filled_amount: 10u128, + tick_id: 0, + claim_bounty: None, + order_direction: OrderDirection::Ask, + claimer: "user1", + }, + BasicFulfilledOrderTestCase { + name: "basic fulfilled bid", + placed_amount: 10u128, + filled_amount: 10u128, + tick_id: 0, + claim_bounty: None, + order_direction: OrderDirection::Bid, + claimer: "user1", + }, + BasicFulfilledOrderTestCase { + name: "basic partially filled ask", + placed_amount: 10u128, + filled_amount: 5u128, + tick_id: 0, + claim_bounty: None, + order_direction: OrderDirection::Ask, + claimer: "user1", + }, + BasicFulfilledOrderTestCase { + name: "basic partially filled bid", + placed_amount: 10u128, + filled_amount: 5u128, + tick_id: 0, + claim_bounty: None, + order_direction: OrderDirection::Bid, + claimer: "user1", + }, + BasicFulfilledOrderTestCase { + name: "basic fulfilled ask with bounty", + placed_amount: 100u128, + filled_amount: 100u128, + tick_id: 0, + claim_bounty: Some(Decimal256::percent(1)), + order_direction: OrderDirection::Ask, + claimer: "user1", + }, + BasicFulfilledOrderTestCase { + name: "basic fulfilled ask with bounty with external claimant", + placed_amount: 100u128, + filled_amount: 100u128, + tick_id: 0, + claim_bounty: Some(Decimal256::percent(1)), + order_direction: OrderDirection::Ask, + claimer: "user2", + }, + ]; + for case in cases { + let app = OsmosisTestApp::new(); + let cp = CosmwasmPool::new(&app); + let t = setup!(&app, "quote", "base"); + let (expected_bid_tick, expected_ask_tick) = if case.order_direction == OrderDirection::Ask + { + (MIN_TICK, case.tick_id) + } else { + (case.tick_id, MAX_TICK) + }; + + // Place limit + orders::place_limit( + &t, + case.tick_id, + case.order_direction, + case.placed_amount, + case.claim_bounty, + "user1", + ) + .unwrap(); + match case.order_direction { + OrderDirection::Ask => { + assert::pool_liquidity(&t, case.placed_amount, 0u8, case.name); + assert::pool_balance(&t, case.placed_amount, 0u8, case.name); + } + OrderDirection::Bid => { + assert::pool_liquidity(&t, 0u8, case.placed_amount, case.name); + assert::pool_balance(&t, 0u8, case.placed_amount, case.name); + } + } + assert::spot_price(&t, expected_bid_tick, expected_ask_tick, case.name); + + // Fill limit order + orders::place_market_success( + &cp, + &t, + case.order_direction.opposite(), + case.filled_amount, + "user2", + ); + match case.order_direction { + OrderDirection::Ask => { + assert::pool_liquidity(&t, case.placed_amount - case.filled_amount, 0u8, case.name); + assert::pool_balance( + &t, + case.placed_amount - case.filled_amount, + case.filled_amount, + case.name, + ); + } + OrderDirection::Bid => { + assert::pool_liquidity(&t, 0u8, case.placed_amount - case.filled_amount, case.name); + assert::pool_balance( + &t, + case.filled_amount, + case.placed_amount - case.filled_amount, + case.name, + ); + } + } + assert::spot_price(&t, expected_bid_tick, expected_ask_tick, case.name); + + // Claim limit + orders::claim_success(&t, case.claimer, 0, 0); + match case.order_direction { + OrderDirection::Ask => { + assert::pool_liquidity(&t, case.placed_amount - case.filled_amount, 0u8, case.name); + assert::pool_balance(&t, case.placed_amount - case.filled_amount, 0u8, case.name); + } + OrderDirection::Bid => { + assert::pool_liquidity(&t, 0u8, case.placed_amount - case.filled_amount, case.name); + assert::pool_balance(&t, 0u8, case.placed_amount - case.filled_amount, case.name); + } + } + assert::spot_price(&t, expected_bid_tick, expected_ask_tick, case.name); + } +} diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs new file mode 100644 index 0000000..8ce9861 --- /dev/null +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs @@ -0,0 +1,422 @@ +#[macro_export] +macro_rules! setup { + ($($app:expr, $quote_denom:expr, $base_denom:expr),* ) => {{ + $($app.init_account(&[ + cosmwasm_std::Coin::new(1, $quote_denom), + cosmwasm_std::Coin::new(1, $base_denom), + ]) + .unwrap(); + + let t = $crate::tests::e2e::test_env::TestEnvBuilder::new() + .with_account( + "user1", + vec![ + cosmwasm_std::Coin::new(2_000, $quote_denom), + cosmwasm_std::Coin::new(2_000, $base_denom), + ], + ) + .with_account( + "user2", + vec![ + cosmwasm_std::Coin::new(2_000, $quote_denom), + cosmwasm_std::Coin::new(2_000, $base_denom), + ], + ) + .with_instantiate_msg($crate::msg::InstantiateMsg { + base_denom: $base_denom.to_string(), + quote_denom: $quote_denom.to_string(), + }) + .build(&$app); + + let $crate::msg::DenomsResponse { + quote_denom, + base_denom, + } = t.contract.query(&$crate::msg::QueryMsg::Denoms {}).unwrap(); + + assert_eq!(quote_denom, "quote"); + assert_eq!(base_denom, "base"); + + let $crate::msg::GetTotalPoolLiquidityResponse { + total_pool_liquidity, + } = t + .contract + .query(&$crate::msg::QueryMsg::GetTotalPoolLiquidity {}) + .unwrap(); + + assert_eq!( + total_pool_liquidity, + vec![ + cosmwasm_std::Coin::new(0, "base"), + cosmwasm_std::Coin::new(0, "quote"), + ] + ); + + let is_active: bool = t.contract.query(&$crate::msg::QueryMsg::IsActive {}).unwrap(); + + assert!(is_active); + + t)* + }}; +} + +pub mod assert { + use crate::{ + msg::{DenomsResponse, GetTotalPoolLiquidityResponse, QueryMsg, SpotPriceResponse}, + tests::e2e::test_env::TestEnv, + tick_math::tick_to_price, + }; + use cosmwasm_std::{Coin, Coins}; + use osmosis_test_tube::{cosmrs::proto::prost::Message, RunnerExecuteResult}; + + pub fn pool_liquidity( + t: &TestEnv, + base_liquidity: impl Into, + quote_liquidity: impl Into, + label: &str, + ) { + let DenomsResponse { + quote_denom, + base_denom, + } = t.contract.get_denoms(); + let GetTotalPoolLiquidityResponse { + total_pool_liquidity, + } = t + .contract + .query(&QueryMsg::GetTotalPoolLiquidity {}) + .unwrap(); + assert_eq!( + total_pool_liquidity, + vec![ + Coin::new(base_liquidity.into(), base_denom), + Coin::new(quote_liquidity.into(), quote_denom) + ], + "{}: pool liquidity did not match", + label + ); + } + + pub fn pool_balance( + t: &TestEnv, + base_liquidity: impl Into, + quote_liquidity: impl Into, + label: &str, + ) { + let DenomsResponse { + quote_denom, + base_denom, + } = t.contract.get_denoms(); + t.assert_contract_balances( + [ + Coin::new(base_liquidity.into(), base_denom), + Coin::new(quote_liquidity.into(), quote_denom), + ] + .iter() + .filter(|x| !x.amount.is_zero()) + .cloned() + .collect::>() + .as_slice(), + label, + ); + } + + pub fn spot_price(t: &TestEnv, bid_tick: i64, ask_tick: i64, label: &str) { + let bid_price = tick_to_price(bid_tick).unwrap(); + let ask_price = tick_to_price(ask_tick).unwrap(); + let DenomsResponse { + quote_denom, + base_denom, + } = t.contract.get_denoms(); + + for (base_denom, quote_denom, price, direction) in [ + (base_denom.clone(), quote_denom.clone(), ask_price, "ask"), + (quote_denom, base_denom, bid_price, "bid"), + ] { + let SpotPriceResponse { spot_price } = t + .contract + .query(&QueryMsg::SpotPrice { + base_asset_denom: base_denom, + quote_asset_denom: quote_denom, + }) + .unwrap(); + + assert_eq!( + spot_price.to_string(), + price.to_string(), + "{}: {} price did not match", + label, + direction + ); + } + } + + pub fn with_balance_changes( + t: &TestEnv, + changes: &[(&str, Vec)], + action: impl FnOnce() -> RunnerExecuteResult, + ) -> RunnerExecuteResult { + let pre_balances: Vec<(String, Coins)> = changes + .iter() + .map(|(sender, _)| { + ( + sender.to_string(), + Coins::try_from(t.get_balance(sender)).unwrap(), + ) + }) + .collect(); + let result = action(); + let post_balances: Vec<(String, Coins)> = changes + .iter() + .map(|(sender, _)| { + ( + sender.to_string(), + Coins::try_from(t.get_balance(sender)).unwrap(), + ) + }) + .collect(); + + for (sender, balance_change) in changes.iter().cloned() { + let pre_balance = pre_balances + .iter() + .find(|(s, _)| s == sender) + .unwrap() + .1 + .clone(); + let post_balance = post_balances + .iter() + .find(|(s, _)| s == sender) + .unwrap() + .1 + .clone(); + for coin in balance_change { + let pre_amount = pre_balance.amount_of(&coin.denom); + let post_amount = post_balance.amount_of(&coin.denom); + let change = post_amount.saturating_sub(pre_amount); + assert_eq!(change, coin.amount); + } + } + + result + } +} + +pub mod orders { + use std::str::FromStr; + + use cosmwasm_std::{coins, Coin, Decimal, Decimal256, Uint128, Uint256}; + + use osmosis_std::types::{ + cosmwasm::wasm::v1::MsgExecuteContractResponse, + osmosis::poolmanager::v1beta1::{ + MsgSwapExactAmountIn, MsgSwapExactAmountInResponse, SwapAmountInRoute, + }, + }; + use osmosis_test_tube::{Account, OsmosisTestApp, RunnerExecuteResult}; + + use crate::{ + msg::{AllTicksResponse, CalcOutAmtGivenInResponse, DenomsResponse, ExecuteMsg, QueryMsg}, + tests::e2e::{modules::cosmwasm_pool::CosmwasmPool, test_env::TestEnv}, + types::{LimitOrder, OrderDirection}, + }; + + use super::assert::with_balance_changes; + + pub fn place_limit( + t: &TestEnv, + tick_id: i64, + order_direction: OrderDirection, + quantity: impl Into, + claim_bounty: Option, + sender: &str, + ) -> RunnerExecuteResult { + let DenomsResponse { + quote_denom, + base_denom, + } = t.contract.query(&QueryMsg::Denoms {}).unwrap(); + + let denom = if order_direction == OrderDirection::Bid { + quote_denom + } else { + base_denom + }; + + let quantity_u128: Uint128 = quantity.into(); + + t.contract.execute( + &ExecuteMsg::PlaceLimit { + tick_id, + order_direction, + quantity: quantity_u128, + claim_bounty, + }, + &coins(quantity_u128.u128(), denom), + &t.accounts[sender], + ) + } + + pub fn place_market( + cp: &CosmwasmPool, + t: &TestEnv, + order_direction: OrderDirection, + quantity: impl Into, + sender: &str, + ) -> RunnerExecuteResult { + let pool_id = t.contract.pool_id; + let quantity_u128: Uint128 = quantity.into(); + let DenomsResponse { + base_denom, + quote_denom, + } = t.contract.query(&QueryMsg::Denoms {}).unwrap(); + + let token_out_denom = if order_direction == OrderDirection::Bid { + base_denom.clone() + } else { + quote_denom.clone() + }; + let token_in_denom = if order_direction == OrderDirection::Bid { + quote_denom + } else { + base_denom + }; + + cp.swap_exact_amount_in( + MsgSwapExactAmountIn { + sender: t.accounts[sender].address(), + routes: vec![SwapAmountInRoute { + pool_id, + token_out_denom, + }], + token_in: Some(Coin::new(quantity_u128.u128(), token_in_denom).into()), + token_out_min_amount: Uint128::one().to_string(), + }, + &t.accounts[sender], + ) + } + + pub fn place_market_success( + cp: &CosmwasmPool, + t: &TestEnv, + order_direction: OrderDirection, + quantity: impl Into + Clone, + sender: &str, + ) { + let quantity_u128: Uint128 = quantity.clone().into(); + let DenomsResponse { + base_denom, + quote_denom, + } = t.contract.query(&QueryMsg::Denoms {}).unwrap(); + + let token_out_denom = if order_direction == OrderDirection::Bid { + base_denom.clone() + } else { + quote_denom.clone() + }; + let token_in_denom = if order_direction == OrderDirection::Bid { + quote_denom + } else { + base_denom + }; + + let CalcOutAmtGivenInResponse { token_out } = t + .contract + .query(&QueryMsg::CalcOutAmountGivenIn { + token_in: Coin::new(quantity_u128.u128(), token_in_denom.clone()), + token_out_denom, + swap_fee: Decimal::zero(), + }) + .unwrap(); + + with_balance_changes( + t, + &[( + &t.accounts[sender].address(), + vec![Coin::new( + Uint128::from_str(&token_out.amount.to_string()) + .unwrap() + .u128(), + token_out.denom, + )], + )], + || place_market(cp, t, order_direction, quantity, sender), + ) + .unwrap(); + } + + pub fn claim( + t: &TestEnv, + sender: &str, + tick_id: i64, + order_id: u64, + ) -> RunnerExecuteResult { + t.contract.execute( + &ExecuteMsg::ClaimLimit { order_id, tick_id }, + &[], + &t.accounts[sender], + ) + } + + pub fn claim_success(t: &TestEnv, sender: &str, tick_id: i64, order_id: u64) { + let order: LimitOrder = t + .contract + .query(&QueryMsg::Order { order_id, tick_id }) + .unwrap(); + let AllTicksResponse { ticks } = t + .contract + .query(&QueryMsg::AllTicks { + start_from: Some(tick_id), + end_at: None, + limit: Some(1), + }) + .unwrap(); + let tick = ticks.first().unwrap().tick_state.clone(); + let tick_values: crate::types::TickValues = tick.get_values(order.order_direction); + let mut expected_amount_u256 = tick_values + .effective_total_amount_swapped + .checked_sub(order.etas) + .unwrap() + .to_uint_floor(); + let mut bounty_amount_256 = Uint256::zero(); + if let Some(bounty) = order.claim_bounty { + if order.owner != t.accounts[sender].address() { + bounty_amount_256 = Decimal256::from_ratio(expected_amount_u256, Uint256::one()) + .checked_mul(bounty) + .unwrap() + .to_uint_floor(); + expected_amount_u256 = expected_amount_u256.checked_sub(bounty_amount_256).unwrap(); + } + } + + let bounty_amount = Uint128::try_from(bounty_amount_256).unwrap(); + let expected_amount = Uint128::try_from(expected_amount_u256).unwrap(); + + let DenomsResponse { + base_denom, + quote_denom, + } = t.contract.get_denoms(); + let expected_denom = if order.order_direction == OrderDirection::Bid { + base_denom + } else { + quote_denom + }; + + with_balance_changes( + t, + [ + ( + order.owner.as_str(), + vec![Coin::new(expected_amount.u128(), expected_denom.clone())], + ), + ( + &t.accounts[sender].address(), + vec![Coin::new(bounty_amount.u128(), expected_denom)], + ), + ] + .iter() + .filter(|x| x.1.iter().all(|y| !y.amount.is_zero())) + .cloned() + .collect::)>>() + .as_slice(), + || claim(t, sender, tick_id, order_id), + ) + .unwrap(); + } +} diff --git a/contracts/sumtree-orderbook/src/tests/e2e/mod.rs b/contracts/sumtree-orderbook/src/tests/e2e/mod.rs new file mode 100644 index 0000000..3286abc --- /dev/null +++ b/contracts/sumtree-orderbook/src/tests/e2e/mod.rs @@ -0,0 +1,5 @@ +#![cfg(all(not(tarpaulin), not(feature = "skip-integration-test")))] + +mod cases; +mod modules; +mod test_env; diff --git a/contracts/sumtree-orderbook/src/tests/e2e/modules/cosmwasm_pool.rs b/contracts/sumtree-orderbook/src/tests/e2e/modules/cosmwasm_pool.rs new file mode 100644 index 0000000..7c6039b --- /dev/null +++ b/contracts/sumtree-orderbook/src/tests/e2e/modules/cosmwasm_pool.rs @@ -0,0 +1,43 @@ +use osmosis_std::types::osmosis::cosmwasmpool::v1beta1::{ + ContractInfoByPoolIdRequest, ContractInfoByPoolIdResponse, MsgCreateCosmWasmPool, + MsgCreateCosmWasmPoolResponse, +}; +use osmosis_std::types::osmosis::poolmanager::v1beta1::{ + MsgSwapExactAmountIn, MsgSwapExactAmountInResponse, MsgSwapExactAmountOut, + MsgSwapExactAmountOutResponse, +}; +use osmosis_test_tube::{fn_execute, fn_query}; + +use osmosis_test_tube::Module; +use osmosis_test_tube::Runner; + +pub struct CosmwasmPool<'a, R: Runner<'a>> { + runner: &'a R, +} + +impl<'a, R: Runner<'a>> Module<'a, R> for CosmwasmPool<'a, R> { + fn new(runner: &'a R) -> Self { + Self { runner } + } +} + +impl<'a, R> CosmwasmPool<'a, R> +where + R: Runner<'a>, +{ + fn_execute! { + pub create_cosmwasm_pool: MsgCreateCosmWasmPool => MsgCreateCosmWasmPoolResponse + } + + fn_execute! { + pub swap_exact_amount_in: MsgSwapExactAmountIn => MsgSwapExactAmountInResponse + } + + fn_execute! { + pub swap_exact_amount_out: MsgSwapExactAmountOut => MsgSwapExactAmountOutResponse + } + + fn_query! { + pub contract_info_by_pool_id ["/osmosis.cosmwasmpool.v1beta1.Query/ContractInfoByPoolId"]: ContractInfoByPoolIdRequest => ContractInfoByPoolIdResponse + } +} diff --git a/contracts/sumtree-orderbook/src/tests/e2e/modules/mod.rs b/contracts/sumtree-orderbook/src/tests/e2e/modules/mod.rs new file mode 100644 index 0000000..941af34 --- /dev/null +++ b/contracts/sumtree-orderbook/src/tests/e2e/modules/mod.rs @@ -0,0 +1 @@ +pub mod cosmwasm_pool; diff --git a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs new file mode 100644 index 0000000..485f268 --- /dev/null +++ b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs @@ -0,0 +1,240 @@ +use std::{collections::HashMap, path::PathBuf}; + +use crate::{ + msg::{DenomsResponse, ExecuteMsg, InstantiateMsg, QueryMsg}, + ContractError, +}; + +use cosmwasm_std::{to_json_binary, Coin}; +use osmosis_std::types::{ + cosmos::bank::v1beta1::QueryAllBalancesRequest, + cosmwasm::wasm::v1::MsgExecuteContractResponse, + osmosis::cosmwasmpool::v1beta1::{ + ContractInfoByPoolIdRequest, ContractInfoByPoolIdResponse, MsgCreateCosmWasmPool, + }, +}; +use osmosis_test_tube::{ + osmosis_std::types::osmosis::cosmwasmpool::v1beta1::UploadCosmWasmPoolCodeAndWhiteListProposal, + GovWithAppAccess, +}; + +use osmosis_test_tube::{ + Account, Bank, Module, OsmosisTestApp, RunnerError, RunnerExecuteResult, RunnerResult, + SigningAccount, Wasm, +}; +use serde::de::DeserializeOwned; + +use super::modules::cosmwasm_pool::CosmwasmPool; + +pub struct TestEnv<'a> { + pub app: &'a OsmosisTestApp, + pub creator: SigningAccount, + pub contract: OrderbookContract<'a>, + pub accounts: HashMap, +} + +impl<'a> TestEnv<'a> { + pub fn _assert_account_balances( + &self, + account: &str, + expected_balances: Vec, + ignore_denoms: Vec<&str>, + ) { + let account_balances: Vec = self + ._get_account_balance(account) + .iter() + .filter(|coin| !ignore_denoms.contains(&coin.denom.as_str())) + .cloned() + .collect(); + + assert_eq!(account_balances, expected_balances); + } + + pub fn assert_contract_balances(&self, expected_balances: &[Coin], label: &str) { + let contract_balances: Vec = self.get_balance(&self.contract.contract_addr); + + assert_eq!( + contract_balances, expected_balances, + "{}: contract balances did not match", + label + ); + } + + pub fn get_balance(&self, address: &str) -> Vec { + let account_balances: Vec = Bank::new(self.app) + .query_all_balances(&QueryAllBalancesRequest { + address: address.to_string(), + pagination: None, + }) + .unwrap() + .balances + .into_iter() + .map(|coin| Coin::new(coin.amount.parse().unwrap(), coin.denom)) + .collect(); + + account_balances + } + + pub fn _get_account_balance(&self, account: &str) -> Vec { + let account = self.accounts.get(account).unwrap(); + + self.get_balance(&account.address()) + } +} + +pub struct TestEnvBuilder { + account_balances: HashMap>, + instantiate_msg: Option, +} + +impl TestEnvBuilder { + pub fn new() -> Self { + Self { + account_balances: HashMap::new(), + instantiate_msg: None, + } + } + + pub fn with_instantiate_msg(mut self, msg: InstantiateMsg) -> Self { + self.instantiate_msg = Some(msg); + self + } + + pub fn with_account(mut self, account: &str, balance: Vec) -> Self { + self.account_balances.insert(account.to_string(), balance); + self + } + pub fn build(self, app: &'_ OsmosisTestApp) -> TestEnv<'_> { + let accounts: HashMap<_, _> = self + .account_balances + .into_iter() + .map(|(account, balance)| { + let balance: Vec<_> = balance + .into_iter() + .chain(vec![Coin::new(1000000000000, "uosmo")]) + .collect(); + + (account, app.init_account(&balance).unwrap()) + }) + .collect(); + + let creator = app + .init_account(&[Coin::new(1000000000000000u128, "uosmo")]) + .unwrap(); + + let instantiate_msg = self.instantiate_msg.expect("instantiate msg not set"); + let instantiate_msg = InstantiateMsg { ..instantiate_msg }; + + let contract = OrderbookContract::deploy(app, &instantiate_msg, &creator).unwrap(); + + TestEnv { + app, + creator, + contract, + accounts, + } + } +} + +pub struct OrderbookContract<'a> { + app: &'a OsmosisTestApp, + pub code_id: u64, + pub pool_id: u64, + pub contract_addr: String, +} + +impl<'a> OrderbookContract<'a> { + pub fn deploy( + app: &'a OsmosisTestApp, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + ) -> Result { + let cp = CosmwasmPool::new(app); + let gov = GovWithAppAccess::new(app); + + let code_id = 1; // temporary solution + gov.propose_and_execute( + UploadCosmWasmPoolCodeAndWhiteListProposal::TYPE_URL.to_string(), + UploadCosmWasmPoolCodeAndWhiteListProposal { + title: String::from("store test cosmwasm pool code"), + description: String::from("test"), + wasm_byte_code: Self::get_wasm_byte_code(), + }, + signer.address(), + signer, + )?; + + let res = cp.create_cosmwasm_pool( + MsgCreateCosmWasmPool { + code_id, + instantiate_msg: to_json_binary(instantiate_msg).unwrap().to_vec(), + sender: signer.address(), + }, + signer, + )?; + + let pool_id = res.data.pool_id; + + let ContractInfoByPoolIdResponse { + contract_address, + code_id: _, + } = cp.contract_info_by_pool_id(&ContractInfoByPoolIdRequest { pool_id })?; + + Ok(Self { + app, + code_id, + pool_id, + contract_addr: contract_address, + }) + } + + pub fn execute( + &self, + msg: &ExecuteMsg, + funds: &[Coin], + signer: &SigningAccount, + ) -> RunnerExecuteResult { + let wasm = Wasm::new(self.app); + wasm.execute(&self.contract_addr, msg, funds, signer) + } + + pub fn query(&self, msg: &QueryMsg) -> RunnerResult + where + Res: ?Sized + DeserializeOwned, + { + let wasm = Wasm::new(self.app); + wasm.query(&self.contract_addr, msg) + } + + pub fn get_wasm_byte_code() -> Vec { + let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + std::fs::read( + manifest_path + .join("..") + .join("..") + .join("target") + .join("wasm32-unknown-unknown") + .join("release") + .join("sumtree_orderbook.wasm"), + ) + .unwrap() + } + + pub fn get_denoms(&self) -> DenomsResponse { + self.query(&QueryMsg::Denoms {}).unwrap() + } +} + +pub fn assert_contract_err(expected: ContractError, actual: RunnerError) { + match actual { + RunnerError::ExecuteError { msg } => { + if !msg.contains(&expected.to_string()) { + panic!( + "assertion failed:\n\n must contain \t: \"{}\",\n actual \t: \"{}\"\n", + expected, msg + ) + } + } + _ => panic!("unexpected error, expect execute error but got: {}", actual), + }; +} diff --git a/contracts/sumtree-orderbook/src/tests/mod.rs b/contracts/sumtree-orderbook/src/tests/mod.rs index 4dada36..770bb97 100644 --- a/contracts/sumtree-orderbook/src/tests/mod.rs +++ b/contracts/sumtree-orderbook/src/tests/mod.rs @@ -10,3 +10,5 @@ pub mod test_sudo; pub mod test_tick; pub mod test_tick_math; mod test_utils; + +pub mod e2e; From 713ec252617d525d06fa19e49c47c29c9ec19f96 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Fri, 14 Jun 2024 17:50:13 +0100 Subject: [PATCH 02/98] feat: fuzz testing first pass --- contracts/sumtree-orderbook/src/contract.rs | 1 + contracts/sumtree-orderbook/src/msg.rs | 3 + contracts/sumtree-orderbook/src/query.rs | 7 +- .../src/tests/e2e/cases/mod.rs | 1 + .../src/tests/e2e/cases/test_fuzz.rs | 234 ++++++++++++++++++ .../src/tests/e2e/cases/utils.rs | 47 +++- .../src/tests/e2e/test_env.rs | 5 + 7 files changed, 296 insertions(+), 2 deletions(-) create mode 100644 contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs diff --git a/contracts/sumtree-orderbook/src/contract.rs b/contracts/sumtree-orderbook/src/contract.rs index 94762dd..59948db 100644 --- a/contracts/sumtree-orderbook/src/contract.rs +++ b/contracts/sumtree-orderbook/src/contract.rs @@ -146,6 +146,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> ContractResult { QueryMsg::Order { tick_id, order_id } => { Ok(to_json_binary(&query::order(deps, tick_id, order_id)?)?) } + QueryMsg::OrderbookState {} => Ok(to_json_binary(&query::orderbook_state(deps)?)?), QueryMsg::Denoms {} => Ok(to_json_binary(&query::denoms(deps)?)?), QueryMsg::GetMakerFee {} => Ok(to_json_binary(&state::get_maker_fee(deps.storage)?)?), diff --git a/contracts/sumtree-orderbook/src/msg.rs b/contracts/sumtree-orderbook/src/msg.rs index 86aa30b..9162fff 100644 --- a/contracts/sumtree-orderbook/src/msg.rs +++ b/contracts/sumtree-orderbook/src/msg.rs @@ -122,6 +122,9 @@ pub enum QueryMsg { #[returns(crate::types::LimitOrder)] Order { tick_id: i64, order_id: u64 }, + #[returns(crate::types::Orderbook)] + OrderbookState {}, + #[returns(DenomsResponse)] Denoms {}, } diff --git a/contracts/sumtree-orderbook/src/query.rs b/contracts/sumtree-orderbook/src/query.rs index edd3d0a..c200870 100644 --- a/contracts/sumtree-orderbook/src/query.rs +++ b/contracts/sumtree-orderbook/src/query.rs @@ -16,7 +16,7 @@ use crate::{ }, sudo::ensure_swap_fee, tick_math::tick_to_price, - types::{FilterOwnerOrders, LimitOrder, MarketOrder, OrderDirection}, + types::{FilterOwnerOrders, LimitOrder, MarketOrder, OrderDirection, Orderbook}, ContractError, }; @@ -221,3 +221,8 @@ pub(crate) fn order(deps: Deps, tick_id: i64, order_id: u64) -> ContractResult ContractResult { + let orderbook = ORDERBOOK.load(deps.storage)?; + Ok(orderbook) +} diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/mod.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/mod.rs index 4dcf09b..95a9452 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/mod.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/mod.rs @@ -1,2 +1,3 @@ +mod test_fuzz; mod test_orders_success; mod utils; diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs new file mode 100644 index 0000000..894b7d5 --- /dev/null +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -0,0 +1,234 @@ +use cosmwasm_std::{Coin, Coins, Decimal}; +use cosmwasm_std::{Decimal256, Uint128}; +use osmosis_test_tube::{Account, Module, OsmosisTestApp}; +use rand::Rng; +use rand::{rngs::StdRng, SeedableRng}; + +use super::utils::orders; +use crate::constants::{MAX_TICK, MIN_TICK}; +use crate::msg::{AllTicksResponse, CalcOutAmtGivenInResponse, QueryMsg, SpotPriceResponse}; +use crate::tests::e2e::modules::cosmwasm_pool::CosmwasmPool; +use crate::tests::test_utils::decimal256_from_u128; +use crate::types::Orderbook; +use crate::{ + msg::{DenomsResponse, GetTotalPoolLiquidityResponse}, + setup, + tests::e2e::test_env::TestEnv, + types::OrderDirection, +}; + +#[test] +fn test_order_fuzz() { + let seed: u64 = 123456789; + let amount_orders = 100; + let mut rng = StdRng::seed_from_u64(seed); + + let app = OsmosisTestApp::new(); + let cp = CosmwasmPool::new(&app); + let mut t = setup!(app, "quote", "base"); + let mut orders = vec![]; + for i in 0..amount_orders { + let username = format!("user{}", i); + let chosen_tick = place_random_order(&mut t, &mut rng, &username); + let is_cancelled = rng.gen_bool(0.1); + if is_cancelled { + orders::cancel_limit_success(&t, &username, chosen_tick, i); + println!("cancelled order: {}", i); + } else { + orders.push((chosen_tick, i)); + } + assert_tick_invariants(&mut t); + } + + for order_direction in [OrderDirection::Bid, OrderDirection::Ask] { + let GetTotalPoolLiquidityResponse { + total_pool_liquidity, + } = t + .contract + .query(&QueryMsg::GetTotalPoolLiquidity {}) + .unwrap(); + let mut liquidity = if order_direction == OrderDirection::Bid { + Coins::try_from(total_pool_liquidity.clone()) + .unwrap() + .amount_of("base") + } else { + Coins::try_from(total_pool_liquidity.clone()) + .unwrap() + .amount_of("quote") + }; + + let mut user_id = 0; + while !liquidity.is_zero() && user_id < 1000 { + let amount_raw = Uint128::from(rng.gen::()); + let (token_in_denom, token_out_denom) = if order_direction == OrderDirection::Bid { + ("quote", "base") + } else { + ("base", "quote") + }; + let SpotPriceResponse { spot_price } = t + .contract + .query(&QueryMsg::SpotPrice { + base_asset_denom: token_in_denom.to_string(), + quote_asset_denom: token_out_denom.to_string(), + }) + .unwrap(); + + let liquidity_at_price_u256 = if order_direction == OrderDirection::Bid { + decimal256_from_u128(liquidity) + .checked_div(Decimal256::from(spot_price)) + .unwrap() + } else { + decimal256_from_u128(liquidity) + .checked_mul(Decimal256::from(spot_price)) + .unwrap() + } + .to_uint_floor(); + let liquidity_at_price = Uint128::try_from(liquidity_at_price_u256).unwrap(); + let amount = amount_raw.min(liquidity_at_price); + let expected_out = + t.contract + .query::(&QueryMsg::CalcOutAmountGivenIn { + token_in: Coin::new(amount.u128(), token_in_denom.to_string()), + token_out_denom: token_out_denom.to_string(), + swap_fee: Decimal::zero(), + }); + if amount.is_zero() || expected_out.is_err() { + user_id += 1; + continue; + } + let username = format!("user{}{}", order_direction, user_id); + + t.add_account( + &username, + vec![ + Coin::new(amount.u128(), token_in_denom), + Coin::new(1000000000000000u128, "uosmo"), + ], + ); + orders::place_market_success(&cp, &t, order_direction, amount, &username); + let GetTotalPoolLiquidityResponse { + total_pool_liquidity, + } = t + .contract + .query(&QueryMsg::GetTotalPoolLiquidity {}) + .unwrap(); + liquidity = if order_direction == OrderDirection::Bid { + Coins::try_from(total_pool_liquidity.clone()) + .unwrap() + .amount_of("base") + } else { + Coins::try_from(total_pool_liquidity.clone()) + .unwrap() + .amount_of("quote") + }; + assert_tick_invariants(&mut t); + user_id += 1; + } + println!("Placed {} orders in {} direction", user_id, order_direction); + let GetTotalPoolLiquidityResponse { + total_pool_liquidity, + } = t + .contract + .query(&QueryMsg::GetTotalPoolLiquidity {}) + .unwrap(); + println!("Total pool liquidity: {:?}", total_pool_liquidity); + } +} + +fn place_random_order(t: &mut TestEnv, rng: &mut StdRng, username: &str) -> i64 { + let quantity = Uint128::from(rng.gen::()); + let order_direction = if rng.gen_bool(0.5) { + OrderDirection::Bid + } else { + OrderDirection::Ask + }; + let DenomsResponse { + base_denom, + quote_denom, + } = t.contract.get_denoms(); + let expected_denom = if order_direction == OrderDirection::Bid { + "e_denom + } else { + &base_denom + }; + t.add_account( + username, + vec![ + Coin::new(quantity.u128(), expected_denom), + Coin::new(1000000000000000u128, "uosmo"), + ], + ); + assert!(t.accounts.contains_key(username)); + let tick_id = (rng.gen::() as i64).min(MAX_TICK).max(MIN_TICK); + let has_claim_bounty = rng.gen_bool(0.8); + let claim_bounty = if has_claim_bounty { + Some(Decimal256::percent(rng.gen_range(0..=1))) + } else { + None + }; + + orders::place_limit( + t, + tick_id, + order_direction, + quantity, + claim_bounty, + username, + ) + .unwrap(); + + println!( + "username: {}, sender: {}, tick_id: {}, order_direction: {}, quantity: {}, claim_bounty: {}", + username, + t.accounts[username].address(), + tick_id, + order_direction, + quantity, + claim_bounty.unwrap_or_default() + ); + tick_id +} + +fn assert_tick_invariants(t: &mut TestEnv) { + let AllTicksResponse { ticks } = t + .contract + .query(&QueryMsg::AllTicks { + start_from: None, + end_at: None, + limit: None, + }) + .unwrap(); + + let ticks_with_bid_amount = ticks.iter().filter(|tick| { + !tick + .tick_state + .get_values(OrderDirection::Bid) + .total_amount_of_liquidity + .is_zero() + }); + let ticks_with_ask_amount = ticks.iter().filter(|tick| { + !tick + .tick_state + .get_values(OrderDirection::Ask) + .total_amount_of_liquidity + .is_zero() + }); + let max_tick_with_bid = ticks_with_bid_amount.max_by_key(|tick| tick.tick_id); + let min_tick_with_ask = ticks_with_ask_amount.min_by_key(|tick| tick.tick_id); + + let Orderbook { + next_ask_tick, + next_bid_tick, + .. + } = t.contract.query(&QueryMsg::OrderbookState {}).unwrap(); + assert_eq!( + next_ask_tick, + min_tick_with_ask.map_or(MAX_TICK, |tick| tick.tick_id) + ); + println!("next_ask_tick: {}", next_ask_tick); + assert_eq!( + next_bid_tick, + max_tick_with_bid.map_or(MIN_TICK, |tick| tick.tick_id) + ); + println!("next_bid_tick: {}", next_bid_tick); +} diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs index 8ce9861..b5ce272 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs @@ -191,7 +191,11 @@ pub mod assert { let pre_amount = pre_balance.amount_of(&coin.denom); let post_amount = post_balance.amount_of(&coin.denom); let change = post_amount.saturating_sub(pre_amount); - assert_eq!(change, coin.amount); + assert_eq!( + change, coin.amount, + "Did not receive expected amount change, expected: {}{}, got: {}{}", + coin.amount, coin.denom, change, coin.denom + ); } } @@ -419,4 +423,45 @@ pub mod orders { ) .unwrap(); } + + pub fn cancel_limit( + t: &TestEnv, + sender: &str, + tick_id: i64, + order_id: u64, + ) -> RunnerExecuteResult { + t.contract.execute( + &ExecuteMsg::CancelLimit { order_id, tick_id }, + &[], + &t.accounts[sender], + ) + } + + pub fn cancel_limit_success(t: &TestEnv, sender: &str, tick_id: i64, order_id: u64) { + let order: LimitOrder = t + .contract + .query(&QueryMsg::Order { order_id, tick_id }) + .unwrap(); + let order_direction = order.order_direction; + let quantity = order.quantity; + let DenomsResponse { + base_denom, + quote_denom, + } = t.contract.get_denoms(); + let token_in_denom = if order_direction == OrderDirection::Bid { + quote_denom + } else { + base_denom + }; + + with_balance_changes( + t, + &[( + &t.accounts[sender].address(), + vec![Coin::new(quantity.u128(), token_in_denom)], + )], + || cancel_limit(t, sender, tick_id, order_id), + ) + .unwrap(); + } } diff --git a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs index 485f268..2fdf497 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs @@ -34,6 +34,11 @@ pub struct TestEnv<'a> { } impl<'a> TestEnv<'a> { + pub fn add_account(&mut self, username: &str, balance: Vec) { + let account = self.app.init_account(&balance).unwrap(); + self.accounts.insert(username.to_string(), account); + } + pub fn _assert_account_balances( &self, account: &str, From cc9c2f8003e10eaad3d05ceff19a27730002f637 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Fri, 14 Jun 2024 18:04:40 +0100 Subject: [PATCH 03/98] test: added claiming to fuzz tests --- .../src/tests/e2e/cases/test_fuzz.rs | 54 +++++++++++++------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index 894b7d5..8931b16 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -9,7 +9,7 @@ use crate::constants::{MAX_TICK, MIN_TICK}; use crate::msg::{AllTicksResponse, CalcOutAmtGivenInResponse, QueryMsg, SpotPriceResponse}; use crate::tests::e2e::modules::cosmwasm_pool::CosmwasmPool; use crate::tests::test_utils::decimal256_from_u128; -use crate::types::Orderbook; +use crate::types::{LimitOrder, Orderbook}; use crate::{ msg::{DenomsResponse, GetTotalPoolLiquidityResponse}, setup, @@ -35,7 +35,7 @@ fn test_order_fuzz() { orders::cancel_limit_success(&t, &username, chosen_tick, i); println!("cancelled order: {}", i); } else { - orders.push((chosen_tick, i)); + orders.push((username, chosen_tick, i)); } assert_tick_invariants(&mut t); } @@ -59,7 +59,7 @@ fn test_order_fuzz() { let mut user_id = 0; while !liquidity.is_zero() && user_id < 1000 { - let amount_raw = Uint128::from(rng.gen::()); + let amount_raw = rng.gen_range(0..=liquidity.u128()); let (token_in_denom, token_out_denom) = if order_direction == OrderDirection::Bid { ("quote", "base") } else { @@ -84,15 +84,15 @@ fn test_order_fuzz() { } .to_uint_floor(); let liquidity_at_price = Uint128::try_from(liquidity_at_price_u256).unwrap(); - let amount = amount_raw.min(liquidity_at_price); + let amount = amount_raw.min(liquidity_at_price.u128()); let expected_out = t.contract .query::(&QueryMsg::CalcOutAmountGivenIn { - token_in: Coin::new(amount.u128(), token_in_denom.to_string()), + token_in: Coin::new(amount, token_in_denom.to_string()), token_out_denom: token_out_denom.to_string(), swap_fee: Decimal::zero(), }); - if amount.is_zero() || expected_out.is_err() { + if amount == 0 || expected_out.is_err() { user_id += 1; continue; } @@ -101,7 +101,7 @@ fn test_order_fuzz() { t.add_account( &username, vec![ - Coin::new(amount.u128(), token_in_denom), + Coin::new(amount, token_in_denom), Coin::new(1000000000000000u128, "uosmo"), ], ); @@ -133,6 +133,30 @@ fn test_order_fuzz() { .unwrap(); println!("Total pool liquidity: {:?}", total_pool_liquidity); } + for (username, tick_id, order_id) in orders.iter() { + t.add_account( + "claimant", + vec![ + Coin::new(1, "base"), + Coin::new(1, "quote"), + Coin::new(1000000000u128, "uosmo"), + ], + ); + let order: LimitOrder = t + .contract + .query(&QueryMsg::Order { + order_id: *order_id, + tick_id: *tick_id, + }) + .unwrap(); + let sender = if order.claim_bounty.is_some() { + "claimant" + } else { + username + }; + orders::claim_success(&t, sender, order.tick_id, order.order_id); + println!("Claimed order: {}", order_id); + } } fn place_random_order(t: &mut TestEnv, rng: &mut StdRng, username: &str) -> i64 { @@ -221,14 +245,10 @@ fn assert_tick_invariants(t: &mut TestEnv) { next_bid_tick, .. } = t.contract.query(&QueryMsg::OrderbookState {}).unwrap(); - assert_eq!( - next_ask_tick, - min_tick_with_ask.map_or(MAX_TICK, |tick| tick.tick_id) - ); - println!("next_ask_tick: {}", next_ask_tick); - assert_eq!( - next_bid_tick, - max_tick_with_bid.map_or(MIN_TICK, |tick| tick.tick_id) - ); - println!("next_bid_tick: {}", next_bid_tick); + if let Some(min_tick_with_ask) = min_tick_with_ask { + assert_eq!(next_ask_tick, min_tick_with_ask.tick_id); + } + if let Some(max_tick_with_bid) = max_tick_with_bid { + assert_eq!(next_bid_tick, max_tick_with_bid.tick_id); + } } From 24edf77be46d3d7527df4b4d1f581b74b4803a0c Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Fri, 14 Jun 2024 21:22:34 +0100 Subject: [PATCH 04/98] test: improved fuzz testing algorithm --- .../src/tests/e2e/cases/test_fuzz.rs | 129 +++++++----------- .../src/tests/e2e/cases/utils.rs | 73 ++++++++-- .../src/tests/e2e/test_env.rs | 2 +- 3 files changed, 115 insertions(+), 89 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index 8931b16..cf16e5e 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -1,15 +1,16 @@ use cosmwasm_std::{Coin, Coins, Decimal}; use cosmwasm_std::{Decimal256, Uint128}; +use osmosis_test_tube::cosmrs::bip32::secp256k1::elliptic_curve::bigint::Uint; use osmosis_test_tube::{Account, Module, OsmosisTestApp}; use rand::Rng; use rand::{rngs::StdRng, SeedableRng}; -use super::utils::orders; +use super::utils::{assert, orders}; use crate::constants::{MAX_TICK, MIN_TICK}; -use crate::msg::{AllTicksResponse, CalcOutAmtGivenInResponse, QueryMsg, SpotPriceResponse}; +use crate::msg::{CalcOutAmtGivenInResponse, QueryMsg, SpotPriceResponse}; use crate::tests::e2e::modules::cosmwasm_pool::CosmwasmPool; -use crate::tests::test_utils::decimal256_from_u128; -use crate::types::{LimitOrder, Orderbook}; +use crate::tick_math::{amount_to_value, RoundingDirection}; +use crate::types::LimitOrder; use crate::{ msg::{DenomsResponse, GetTotalPoolLiquidityResponse}, setup, @@ -20,7 +21,7 @@ use crate::{ #[test] fn test_order_fuzz() { let seed: u64 = 123456789; - let amount_orders = 100; + let amount_orders = 5000; let mut rng = StdRng::seed_from_u64(seed); let app = OsmosisTestApp::new(); @@ -30,14 +31,13 @@ fn test_order_fuzz() { for i in 0..amount_orders { let username = format!("user{}", i); let chosen_tick = place_random_order(&mut t, &mut rng, &username); - let is_cancelled = rng.gen_bool(0.1); + let is_cancelled = rng.gen_bool(0.2); if is_cancelled { orders::cancel_limit_success(&t, &username, chosen_tick, i); - println!("cancelled order: {}", i); } else { orders.push((username, chosen_tick, i)); } - assert_tick_invariants(&mut t); + assert::tick_invariants(&mut t); } for order_direction in [OrderDirection::Bid, OrderDirection::Ask] { @@ -58,7 +58,7 @@ fn test_order_fuzz() { }; let mut user_id = 0; - while !liquidity.is_zero() && user_id < 1000 { + while liquidity.gt(&Uint128::one()) && user_id < amount_orders { let amount_raw = rng.gen_range(0..=liquidity.u128()); let (token_in_denom, token_out_denom) = if order_direction == OrderDirection::Bid { ("quote", "base") @@ -73,16 +73,14 @@ fn test_order_fuzz() { }) .unwrap(); - let liquidity_at_price_u256 = if order_direction == OrderDirection::Bid { - decimal256_from_u128(liquidity) - .checked_div(Decimal256::from(spot_price)) - .unwrap() - } else { - decimal256_from_u128(liquidity) - .checked_mul(Decimal256::from(spot_price)) - .unwrap() - } - .to_uint_floor(); + let liquidity_at_price_u256 = amount_to_value( + order_direction.opposite(), + liquidity, + Decimal256::from(spot_price), + RoundingDirection::Down, + ) + .unwrap(); + let liquidity_at_price = Uint128::try_from(liquidity_at_price_u256).unwrap(); let amount = amount_raw.min(liquidity_at_price.u128()); let expected_out = @@ -121,18 +119,20 @@ fn test_order_fuzz() { .unwrap() .amount_of("quote") }; - assert_tick_invariants(&mut t); + assert::tick_invariants(&mut t); user_id += 1; } println!("Placed {} orders in {} direction", user_id, order_direction); - let GetTotalPoolLiquidityResponse { - total_pool_liquidity, - } = t - .contract - .query(&QueryMsg::GetTotalPoolLiquidity {}) - .unwrap(); - println!("Total pool liquidity: {:?}", total_pool_liquidity); } + + let GetTotalPoolLiquidityResponse { + total_pool_liquidity, + } = t + .contract + .query(&QueryMsg::GetTotalPoolLiquidity {}) + .unwrap(); + println!("Total remaining pool liquidity: {:?}", total_pool_liquidity); + for (username, tick_id, order_id) in orders.iter() { t.add_account( "claimant", @@ -154,13 +154,22 @@ fn test_order_fuzz() { } else { username }; - orders::claim_success(&t, sender, order.tick_id, order.order_id); - println!("Claimed order: {}", order_id); + // We cannot verify how much to expect as tick is synced as part of the claim process + // Hence orders::claim is used instead of orders::claim_success + orders::claim(&t, sender, order.tick_id, order.order_id).unwrap(); + + let maybe_order = t.contract.query::(&QueryMsg::Order { + order_id: *order_id, + tick_id: *tick_id, + }); + if let Ok(order) = maybe_order { + println!("order: {:?}", order); + } } } fn place_random_order(t: &mut TestEnv, rng: &mut StdRng, username: &str) -> i64 { - let quantity = Uint128::from(rng.gen::()); + let quantity = Uint128::from(rng.gen::()); let order_direction = if rng.gen_bool(0.5) { OrderDirection::Bid } else { @@ -183,7 +192,7 @@ fn place_random_order(t: &mut TestEnv, rng: &mut StdRng, username: &str) -> i64 ], ); assert!(t.accounts.contains_key(username)); - let tick_id = (rng.gen::() as i64).min(MAX_TICK).max(MIN_TICK); + let tick_id = rng.gen_range(-10..=10); let has_claim_bounty = rng.gen_bool(0.8); let claim_bounty = if has_claim_bounty { Some(Decimal256::percent(rng.gen_range(0..=1))) @@ -201,54 +210,14 @@ fn place_random_order(t: &mut TestEnv, rng: &mut StdRng, username: &str) -> i64 ) .unwrap(); - println!( - "username: {}, sender: {}, tick_id: {}, order_direction: {}, quantity: {}, claim_bounty: {}", - username, - t.accounts[username].address(), - tick_id, - order_direction, - quantity, - claim_bounty.unwrap_or_default() - ); + // println!( + // "username: {}, sender: {}, tick_id: {}, order_direction: {}, quantity: {}, claim_bounty: {}", + // username, + // t.accounts[username].address(), + // tick_id, + // order_direction, + // quantity, + // claim_bounty.unwrap_or_default() + // ); tick_id } - -fn assert_tick_invariants(t: &mut TestEnv) { - let AllTicksResponse { ticks } = t - .contract - .query(&QueryMsg::AllTicks { - start_from: None, - end_at: None, - limit: None, - }) - .unwrap(); - - let ticks_with_bid_amount = ticks.iter().filter(|tick| { - !tick - .tick_state - .get_values(OrderDirection::Bid) - .total_amount_of_liquidity - .is_zero() - }); - let ticks_with_ask_amount = ticks.iter().filter(|tick| { - !tick - .tick_state - .get_values(OrderDirection::Ask) - .total_amount_of_liquidity - .is_zero() - }); - let max_tick_with_bid = ticks_with_bid_amount.max_by_key(|tick| tick.tick_id); - let min_tick_with_ask = ticks_with_ask_amount.min_by_key(|tick| tick.tick_id); - - let Orderbook { - next_ask_tick, - next_bid_tick, - .. - } = t.contract.query(&QueryMsg::OrderbookState {}).unwrap(); - if let Some(min_tick_with_ask) = min_tick_with_ask { - assert_eq!(next_ask_tick, min_tick_with_ask.tick_id); - } - if let Some(max_tick_with_bid) = max_tick_with_bid { - assert_eq!(next_bid_tick, max_tick_with_bid.tick_id); - } -} diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs index b5ce272..52833d6 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs @@ -61,9 +61,13 @@ macro_rules! setup { pub mod assert { use crate::{ - msg::{DenomsResponse, GetTotalPoolLiquidityResponse, QueryMsg, SpotPriceResponse}, + msg::{ + AllTicksResponse, DenomsResponse, GetTotalPoolLiquidityResponse, QueryMsg, + SpotPriceResponse, + }, tests::e2e::test_env::TestEnv, tick_math::tick_to_price, + types::{OrderDirection, Orderbook}, }; use cosmwasm_std::{Coin, Coins}; use osmosis_test_tube::{cosmrs::proto::prost::Message, RunnerExecuteResult}; @@ -201,6 +205,46 @@ pub mod assert { result } + + pub fn tick_invariants(t: &mut TestEnv) { + let AllTicksResponse { ticks } = t + .contract + .query(&QueryMsg::AllTicks { + start_from: None, + end_at: None, + limit: None, + }) + .unwrap(); + + let ticks_with_bid_amount = ticks.iter().filter(|tick| { + !tick + .tick_state + .get_values(OrderDirection::Bid) + .total_amount_of_liquidity + .is_zero() + }); + let ticks_with_ask_amount = ticks.iter().filter(|tick| { + !tick + .tick_state + .get_values(OrderDirection::Ask) + .total_amount_of_liquidity + .is_zero() + }); + let max_tick_with_bid = ticks_with_bid_amount.max_by_key(|tick| tick.tick_id); + let min_tick_with_ask = ticks_with_ask_amount.min_by_key(|tick| tick.tick_id); + + let Orderbook { + next_ask_tick, + next_bid_tick, + .. + } = t.contract.query(&QueryMsg::OrderbookState {}).unwrap(); + if let Some(min_tick_with_ask) = min_tick_with_ask { + assert!(next_ask_tick <= min_tick_with_ask.tick_id); + } + if let Some(max_tick_with_bid) = max_tick_with_bid { + assert!(next_bid_tick >= max_tick_with_bid.tick_id); + } + } } pub mod orders { @@ -219,6 +263,7 @@ pub mod orders { use crate::{ msg::{AllTicksResponse, CalcOutAmtGivenInResponse, DenomsResponse, ExecuteMsg, QueryMsg}, tests::e2e::{modules::cosmwasm_pool::CosmwasmPool, test_env::TestEnv}, + tick_math::{amount_to_value, tick_to_price, RoundingDirection}, types::{LimitOrder, OrderDirection}, }; @@ -366,31 +411,43 @@ pub mod orders { let AllTicksResponse { ticks } = t .contract .query(&QueryMsg::AllTicks { - start_from: Some(tick_id), + start_from: Some(order.tick_id), end_at: None, limit: Some(1), }) .unwrap(); let tick = ticks.first().unwrap().tick_state.clone(); let tick_values: crate::types::TickValues = tick.get_values(order.order_direction); - let mut expected_amount_u256 = tick_values + let expected_amount_u256 = tick_values .effective_total_amount_swapped .checked_sub(order.etas) .unwrap() - .to_uint_floor(); + .to_uint_floor() + .min(Uint256::from(order.quantity.u128())); + let expected_amount = Uint128::try_from(expected_amount_u256).unwrap(); + let price = tick_to_price(order.tick_id).unwrap(); + let mut expected_received_u256 = amount_to_value( + order.order_direction, + expected_amount, + price, + RoundingDirection::Down, + ) + .unwrap(); let mut bounty_amount_256 = Uint256::zero(); if let Some(bounty) = order.claim_bounty { if order.owner != t.accounts[sender].address() { - bounty_amount_256 = Decimal256::from_ratio(expected_amount_u256, Uint256::one()) + bounty_amount_256 = Decimal256::from_ratio(expected_received_u256, Uint256::one()) .checked_mul(bounty) .unwrap() .to_uint_floor(); - expected_amount_u256 = expected_amount_u256.checked_sub(bounty_amount_256).unwrap(); + expected_received_u256 = expected_received_u256 + .checked_sub(bounty_amount_256) + .unwrap(); } } let bounty_amount = Uint128::try_from(bounty_amount_256).unwrap(); - let expected_amount = Uint128::try_from(expected_amount_u256).unwrap(); + let expected_received = Uint128::try_from(expected_received_u256).unwrap(); let DenomsResponse { base_denom, @@ -407,7 +464,7 @@ pub mod orders { [ ( order.owner.as_str(), - vec![Coin::new(expected_amount.u128(), expected_denom.clone())], + vec![Coin::new(expected_received.u128(), expected_denom.clone())], ), ( &t.accounts[sender].address(), diff --git a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs index 2fdf497..674278b 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs @@ -230,7 +230,7 @@ impl<'a> OrderbookContract<'a> { } } -pub fn assert_contract_err(expected: ContractError, actual: RunnerError) { +pub fn _assert_contract_err(expected: ContractError, actual: RunnerError) { match actual { RunnerError::ExecuteError { msg } => { if !msg.contains(&expected.to_string()) { From f756b45c8b3b0c5ac37f037a328f51fdfb835fe6 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Sat, 15 Jun 2024 18:56:39 +0100 Subject: [PATCH 05/98] fix: fixed an issue with tick syncing logic --- contracts/sumtree-orderbook/src/order.rs | 33 ++- .../src/tests/e2e/cases/test_fuzz.rs | 227 +++++++++++++----- .../tests/e2e/cases/test_orders_success.rs | 58 ++++- .../src/tests/e2e/cases/utils.rs | 19 +- .../src/tests/e2e/test_env.rs | 23 +- .../sumtree-orderbook/src/tests/test_order.rs | 38 ++- .../sumtree-orderbook/src/tests/test_utils.rs | 15 +- contracts/sumtree-orderbook/src/tick.rs | 3 +- 8 files changed, 315 insertions(+), 101 deletions(-) diff --git a/contracts/sumtree-orderbook/src/order.rs b/contracts/sumtree-orderbook/src/order.rs index bd6a298..097867e 100644 --- a/contracts/sumtree-orderbook/src/order.rs +++ b/contracts/sumtree-orderbook/src/order.rs @@ -43,6 +43,19 @@ pub fn place_limit( ContractError::InvalidQuantity { quantity } ); + let tick_price = tick_to_price(tick_id)?; + let amount_out = amount_to_value( + order_direction, + quantity, + tick_price, + RoundingDirection::Down, + )?; + + ensure!( + !amount_out.is_zero(), + ContractError::InvalidQuantity { quantity } + ); + // If applicable, ensure claim_bounty is between 0 and 0.01. // We set a conservative upper bound of 1% for claim bounties as a guardrail. if let Some(claim_bounty_value) = claim_bounty { @@ -615,19 +628,20 @@ pub(crate) fn claim_order( // Sync the tick the order is on to ensure correct ETAS let bid_tick_values = tick_state.get_values(OrderDirection::Bid); let ask_tick_values = tick_state.get_values(OrderDirection::Ask); - sync_tick( - storage, - tick_id, - bid_tick_values.effective_total_amount_swapped, - ask_tick_values.effective_total_amount_swapped, - )?; + + let (bid_etas, ask_etas) = match order.order_direction { + OrderDirection::Bid => (order.etas, bid_tick_values.effective_total_amount_swapped), + OrderDirection::Ask => (ask_tick_values.effective_total_amount_swapped, order.etas), + }; + + sync_tick(storage, tick_id, bid_etas, ask_etas)?; // Re-fetch tick post sync call let tick_state = TICK_STATE .may_load(storage, tick_id)? .ok_or(ContractError::InvalidTickId { tick_id })?; let tick_values = tick_state.get_values(order.order_direction); - + println!("tick_values: {:?}, order: {:?}", tick_values, order); // Early exit if nothing has been filled ensure!( tick_values.effective_total_amount_swapped > order.etas, @@ -639,7 +653,7 @@ pub(crate) fn claim_order( // we don't claim more than the order has available. let amount_filled_dec = tick_values .effective_total_amount_swapped - .checked_sub(order.etas)? + .checked_sub(tick_values.cumulative_realized_cancels)? .min(Decimal256::from_ratio(order.quantity, 1u128)); let amount_filled = Uint128::try_from(amount_filled_dec.to_uint_floor())?; @@ -696,6 +710,9 @@ pub(crate) fn claim_order( amount = amount.checked_sub(maker_fee_amount)?; } + // Cannot send a zero amount, may be zero'd out by rounding + ensure!(!amount.is_zero(), ContractError::ZeroClaim); + // Claimed amount always goes to the order owner let bank_msg = MsgSend256 { from_address: contract_address.to_string(), diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index cf16e5e..43601a3 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -1,15 +1,14 @@ -use cosmwasm_std::{Coin, Coins, Decimal}; +use cosmwasm_std::{Coin, Coins, Decimal, Uint256}; use cosmwasm_std::{Decimal256, Uint128}; -use osmosis_test_tube::cosmrs::bip32::secp256k1::elliptic_curve::bigint::Uint; -use osmosis_test_tube::{Account, Module, OsmosisTestApp}; +use osmosis_test_tube::{Module, OsmosisTestApp}; use rand::Rng; use rand::{rngs::StdRng, SeedableRng}; use super::utils::{assert, orders}; use crate::constants::{MAX_TICK, MIN_TICK}; -use crate::msg::{CalcOutAmtGivenInResponse, QueryMsg, SpotPriceResponse}; +use crate::msg::{AllTicksResponse, CalcOutAmtGivenInResponse, QueryMsg, SpotPriceResponse}; use crate::tests::e2e::modules::cosmwasm_pool::CosmwasmPool; -use crate::tick_math::{amount_to_value, RoundingDirection}; +use crate::tick_math::{amount_to_value, tick_to_price, RoundingDirection}; use crate::types::LimitOrder; use crate::{ msg::{DenomsResponse, GetTotalPoolLiquidityResponse}, @@ -19,25 +18,48 @@ use crate::{ }; #[test] -fn test_order_fuzz() { +fn test_order_fuzz_large_orders_small_range() { + run_fuzz(2000, (-10, 10), 0.2); +} + +#[test] +fn test_order_fuzz_small_orders_large_range() { + run_fuzz(100, (MIN_TICK, MAX_TICK), 0.2); +} + +#[test] +fn test_order_fuzz_small_orders_small_range() { + run_fuzz(100, (-10, 0), 0.1); +} + +#[test] +fn test_order_fuzz_large_cancelled_orders_small_range() { + run_fuzz(1000, (MIN_TICK, MIN_TICK + 20), 0.8); +} + +// #[test] +// fn test_order_fuzz_very_large_orders_no_bounds() { +// run_fuzz(3000, (-750, 750), 0.2); +// } + +fn run_fuzz(amount_limit_orders: u64, tick_range: (i64, i64), cancel_probability: f64) { let seed: u64 = 123456789; - let amount_orders = 5000; let mut rng = StdRng::seed_from_u64(seed); let app = OsmosisTestApp::new(); let cp = CosmwasmPool::new(&app); let mut t = setup!(app, "quote", "base"); let mut orders = vec![]; - for i in 0..amount_orders { + for i in 0..amount_limit_orders { let username = format!("user{}", i); - let chosen_tick = place_random_order(&mut t, &mut rng, &username); - let is_cancelled = rng.gen_bool(0.2); + let chosen_tick = place_random_order(&mut t, &mut rng, &username, tick_range); + let is_cancelled = rng.gen_bool(cancel_probability); if is_cancelled { orders::cancel_limit_success(&t, &username, chosen_tick, i); } else { orders.push((username, chosen_tick, i)); } - assert::tick_invariants(&mut t); + assert::tick_invariants(&t); } for order_direction in [OrderDirection::Bid, OrderDirection::Ask] { @@ -57,53 +79,28 @@ fn test_order_fuzz() { .amount_of("quote") }; + let mut zero_amount_returns = 0; let mut user_id = 0; - while liquidity.gt(&Uint128::one()) && user_id < amount_orders { - let amount_raw = rng.gen_range(0..=liquidity.u128()); - let (token_in_denom, token_out_denom) = if order_direction == OrderDirection::Bid { - ("quote", "base") - } else { - ("base", "quote") - }; - let SpotPriceResponse { spot_price } = t - .contract - .query(&QueryMsg::SpotPrice { - base_asset_denom: token_in_denom.to_string(), - quote_asset_denom: token_out_denom.to_string(), - }) - .unwrap(); - - let liquidity_at_price_u256 = amount_to_value( - order_direction.opposite(), - liquidity, - Decimal256::from(spot_price), - RoundingDirection::Down, - ) - .unwrap(); + while liquidity.gt(&Uint128::one()) { + let username = format!("user{}{}", order_direction, user_id); + let placed_amount = place_random_market( + &cp, + &mut t, + &mut rng, + &username, + order_direction, + liquidity.u128(), + ); - let liquidity_at_price = Uint128::try_from(liquidity_at_price_u256).unwrap(); - let amount = amount_raw.min(liquidity_at_price.u128()); - let expected_out = - t.contract - .query::(&QueryMsg::CalcOutAmountGivenIn { - token_in: Coin::new(amount, token_in_denom.to_string()), - token_out_denom: token_out_denom.to_string(), - swap_fee: Decimal::zero(), - }); - if amount == 0 || expected_out.is_err() { - user_id += 1; + user_id += 1; + if placed_amount == 0 { + zero_amount_returns += 1; + if zero_amount_returns == 100 { + break; + } continue; } - let username = format!("user{}{}", order_direction, user_id); - t.add_account( - &username, - vec![ - Coin::new(amount, token_in_denom), - Coin::new(1000000000000000u128, "uosmo"), - ], - ); - orders::place_market_success(&cp, &t, order_direction, amount, &username); let GetTotalPoolLiquidityResponse { total_pool_liquidity, } = t @@ -119,10 +116,12 @@ fn test_order_fuzz() { .unwrap() .amount_of("quote") }; - assert::tick_invariants(&mut t); - user_id += 1; + assert::tick_invariants(&t); } - println!("Placed {} orders in {} direction", user_id, order_direction); + println!( + "Placed {} market orders in {} direction", + user_id, order_direction + ); } let GetTotalPoolLiquidityResponse { @@ -154,9 +153,43 @@ fn test_order_fuzz() { } else { username }; + let AllTicksResponse { ticks } = t + .contract + .query(&QueryMsg::AllTicks { + start_from: Some(order.tick_id), + end_at: None, + limit: Some(1), + }) + .unwrap(); + let tick = ticks.first().unwrap(); + let price = tick_to_price(tick.tick_id).unwrap(); + let value = amount_to_value( + order.order_direction, + order.quantity, + price, + RoundingDirection::Down, + ) + .unwrap(); + let contract_balance = Coins::try_from(t.get_balance(&t.contract.contract_addr)).unwrap(); + // We cannot verify how much to expect as tick is synced as part of the claim process // Hence orders::claim is used instead of orders::claim_success - orders::claim(&t, sender, order.tick_id, order.order_id).unwrap(); + match orders::claim(&t, sender, order.tick_id, order.order_id) { + Ok(_) => {} + Err(e) => { + println!("Failed to claim order {}: {:?}", order.order_id, e); + println!("contract_balance: {:?}", contract_balance); + println!( + "order etas: {}, price: {}, value: {}, tick etas: {}", + order.etas, + price, + value, + tick.tick_state + .get_values(order.order_direction) + .effective_total_amount_swapped + ); + } + } let maybe_order = t.contract.query::(&QueryMsg::Order { order_id: *order_id, @@ -168,7 +201,12 @@ fn test_order_fuzz() { } } -fn place_random_order(t: &mut TestEnv, rng: &mut StdRng, username: &str) -> i64 { +fn place_random_order( + t: &mut TestEnv, + rng: &mut StdRng, + username: &str, + tick_range: (i64, i64), +) -> i64 { let quantity = Uint128::from(rng.gen::()); let order_direction = if rng.gen_bool(0.5) { OrderDirection::Bid @@ -184,15 +222,28 @@ fn place_random_order(t: &mut TestEnv, rng: &mut StdRng, username: &str) -> i64 } else { &base_denom }; + let tick_id = rng.gen_range(tick_range.0..=tick_range.1); + let price = tick_to_price(tick_id).unwrap(); + let min = Uint128::try_from( + amount_to_value( + order_direction.opposite(), + Uint128::one(), + price, + RoundingDirection::Up, + ) + .unwrap() + .min(Uint256::from(Uint128::MAX)), + ) + .unwrap(); + t.add_account( username, vec![ - Coin::new(quantity.u128(), expected_denom), + Coin::new(quantity.u128().max(min.u128()), expected_denom), Coin::new(1000000000000000u128, "uosmo"), ], ); - assert!(t.accounts.contains_key(username)); - let tick_id = rng.gen_range(-10..=10); + let has_claim_bounty = rng.gen_bool(0.8); let claim_bounty = if has_claim_bounty { Some(Decimal256::percent(rng.gen_range(0..=1))) @@ -204,7 +255,7 @@ fn place_random_order(t: &mut TestEnv, rng: &mut StdRng, username: &str) -> i64 t, tick_id, order_direction, - quantity, + quantity.max(min), claim_bounty, username, ) @@ -221,3 +272,55 @@ fn place_random_order(t: &mut TestEnv, rng: &mut StdRng, username: &str) -> i64 // ); tick_id } + +fn place_random_market( + cp: &CosmwasmPool, + t: &mut TestEnv, + rng: &mut StdRng, + username: &str, + order_direction: OrderDirection, + max: u128, +) -> u128 { + let (token_in_denom, token_out_denom) = if order_direction == OrderDirection::Bid { + ("quote", "base") + } else { + ("base", "quote") + }; + let SpotPriceResponse { spot_price } = t + .contract + .query(&QueryMsg::SpotPrice { + base_asset_denom: token_in_denom.to_string(), + quote_asset_denom: token_out_denom.to_string(), + }) + .unwrap(); + let liquidity_at_price_u256 = amount_to_value( + order_direction.opposite(), + Uint128::from(max), + Decimal256::from(spot_price), + RoundingDirection::Up, + ) + .unwrap(); + + let liquidity_at_price = Uint128::try_from(liquidity_at_price_u256).unwrap(); + let amount = rng.gen_range(0..=liquidity_at_price.u128()); + let expected_out = + t.contract + .query::(&QueryMsg::CalcOutAmountGivenIn { + token_in: Coin::new(amount, token_in_denom.to_string()), + token_out_denom: token_out_denom.to_string(), + swap_fee: Decimal::zero(), + }); + if amount == 0 || expected_out.is_err() || expected_out.unwrap().token_out.amount == "0" { + return 0; + } + + t.add_account( + username, + vec![ + Coin::new(amount, token_in_denom), + Coin::new(1000000000000000u128, "uosmo"), + ], + ); + orders::place_market_success(cp, t, order_direction, amount, username).unwrap(); + amount +} diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs index 32ec544..a8432d9 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::Decimal256; +use cosmwasm_std::{Decimal256, Uint128}; use osmosis_test_tube::{Module, OsmosisTestApp}; use super::utils::{assert, orders}; @@ -117,7 +117,8 @@ fn test_basic_order() { case.order_direction.opposite(), case.filled_amount, "user2", - ); + ) + .unwrap(); match case.order_direction { OrderDirection::Ask => { assert::pool_liquidity(&t, case.placed_amount - case.filled_amount, 0u8, case.name); @@ -155,3 +156,56 @@ fn test_basic_order() { assert::spot_price(&t, expected_bid_tick, expected_ask_tick, case.name); } } + +#[test] +fn test_cancelled_orders() { + let app = OsmosisTestApp::new(); + let cp = CosmwasmPool::new(&app); + let t = setup!(&app, "quote", "base"); + let amount_orders = 3; + + for i in 0..amount_orders { + orders::place_limit( + &t, + 0, + OrderDirection::Ask, + Uint128::from(100u128), + None, + "user1", + ) + .unwrap(); + orders::cancel_limit_success(&t, "user1", 0, i); + } + assert::pool_liquidity(&t, 0u8, 0u8, "cancelled orders"); + assert::pool_balance(&t, 0u8, 0u8, "cancelled orders"); + assert::tick_invariants(&t); + + orders::place_limit( + &t, + 0, + OrderDirection::Ask, + Uint128::from(100u128), + None, + "user1", + ) + .unwrap(); + assert::pool_liquidity(&t, 100u8, 0u8, "cancelled orders"); + assert::pool_balance(&t, 100u8, 0u8, "cancelled orders"); + assert::tick_invariants(&t); + + orders::place_market_success( + &cp, + &t, + OrderDirection::Bid, + Uint128::from(100u128), + "user1", + ) + .unwrap(); + assert::tick_invariants(&t); + assert::pool_liquidity(&t, 0u8, 0u8, "cancelled orders"); + assert::pool_balance(&t, 0u8, 100u8, "cancelled orders"); + assert::tick_invariants(&t); + + orders::claim(&t, "user1", 0, amount_orders).unwrap(); + assert::tick_invariants(&t); +} diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs index 52833d6..df22c52 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs @@ -61,10 +61,7 @@ macro_rules! setup { pub mod assert { use crate::{ - msg::{ - AllTicksResponse, DenomsResponse, GetTotalPoolLiquidityResponse, QueryMsg, - SpotPriceResponse, - }, + msg::{DenomsResponse, GetTotalPoolLiquidityResponse, QueryMsg, SpotPriceResponse}, tests::e2e::test_env::TestEnv, tick_math::tick_to_price, types::{OrderDirection, Orderbook}, @@ -206,15 +203,8 @@ pub mod assert { result } - pub fn tick_invariants(t: &mut TestEnv) { - let AllTicksResponse { ticks } = t - .contract - .query(&QueryMsg::AllTicks { - start_from: None, - end_at: None, - limit: None, - }) - .unwrap(); + pub fn tick_invariants(t: &TestEnv) { + let ticks = t.contract.collect_all_ticks(); let ticks_with_bid_amount = ticks.iter().filter(|tick| { !tick @@ -347,7 +337,7 @@ pub mod orders { order_direction: OrderDirection, quantity: impl Into + Clone, sender: &str, - ) { + ) -> RunnerExecuteResult { let quantity_u128: Uint128 = quantity.clone().into(); let DenomsResponse { base_denom, @@ -387,7 +377,6 @@ pub mod orders { )], || place_market(cp, t, order_direction, quantity, sender), ) - .unwrap(); } pub fn claim( diff --git a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs index 674278b..7e20381 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs @@ -1,7 +1,8 @@ use std::{collections::HashMap, path::PathBuf}; use crate::{ - msg::{DenomsResponse, ExecuteMsg, InstantiateMsg, QueryMsg}, + constants::{MAX_TICK, MIN_TICK}, + msg::{AllTicksResponse, DenomsResponse, ExecuteMsg, InstantiateMsg, QueryMsg, TickIdAndState}, ContractError, }; @@ -228,6 +229,26 @@ impl<'a> OrderbookContract<'a> { pub fn get_denoms(&self) -> DenomsResponse { self.query(&QueryMsg::Denoms {}).unwrap() } + + pub fn collect_all_ticks(&self) -> Vec { + let mut ticks = vec![]; + let mut min_tick = MIN_TICK; + while min_tick <= MAX_TICK { + let tick: AllTicksResponse = self + .query(&QueryMsg::AllTicks { + start_from: Some(min_tick), + end_at: Some(MAX_TICK), + limit: Some(300), + }) + .unwrap(); + if tick.ticks.is_empty() { + break; + } + ticks.extend(tick.ticks.clone()); + min_tick = tick.ticks.iter().max_by_key(|t| t.tick_id).unwrap().tick_id + 1; + } + ticks + } } pub fn _assert_contract_err(expected: ContractError, actual: RunnerError) { diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index 485f99d..b76c961 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -2,8 +2,7 @@ use std::str::FromStr; use crate::{ constants::{MAX_TICK, MIN_TICK}, error::ContractError, order::*, orderbook::*, state::*, sumtree::{ - node::{NodeType, TreeNode}, - tree::get_root_node, + node::{NodeType, TreeNode}, tree::get_root_node }, tests::{mock_querier::mock_dependencies_custom, test_utils::{decimal256_from_u128, place_multiple_limit_orders}}, types::{ @@ -4159,3 +4158,38 @@ fn test_maker_fee() { } +#[test] +fn test_cancelled_orders() { + let mut deps = mock_dependencies_custom(); + let sender = Addr::unchecked(DEFAULT_SENDER); + let env = mock_env(); + let info = mock_info(sender.as_str(), &[]); + + create_orderbook(deps.as_mut(), QUOTE_DENOM.to_string(), BASE_DENOM.to_string()).unwrap(); + + for i in 0..10 { + OrderOperation::PlaceLimit(LimitOrder::new(0, i, OrderDirection::Bid, sender.clone(), Uint128::from(100u128), Decimal256::zero(), None)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + if i % 3 != 0 { + OrderOperation::Cancel((0, i)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + } + + } + + OrderOperation::RunMarket(MarketOrder::new(Uint128::from(100u128).checked_mul(Uint128::from(4u128)).unwrap(), OrderDirection::Ask, sender.clone())).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + OrderOperation::PlaceLimit(LimitOrder::new(0, 10, OrderDirection::Bid, sender.clone(), Uint128::from(100u128), Decimal256::zero(), None)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + + + let err = OrderOperation::Claim((0, 10)).run(deps.as_mut(), env.clone(), info.clone()).unwrap_err(); + assert_eq!(err, ContractError::ZeroClaim); + + OrderOperation::RunMarket(MarketOrder::new(Uint128::from(100u128), OrderDirection::Ask, sender.clone())).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + + for i in 0..10 { + let j = i; + if j % 3 == 0 { + OrderOperation::Claim((0, j)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + } + } + + OrderOperation::Claim((0, 10)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); +} \ No newline at end of file diff --git a/contracts/sumtree-orderbook/src/tests/test_utils.rs b/contracts/sumtree-orderbook/src/tests/test_utils.rs index dc5209c..5618177 100644 --- a/contracts/sumtree-orderbook/src/tests/test_utils.rs +++ b/contracts/sumtree-orderbook/src/tests/test_utils.rs @@ -5,7 +5,7 @@ use cosmwasm_std::{ use crate::{ constants::{MAX_TICK, MIN_TICK}, error::ContractResult, - order::{cancel_limit, claim_order, place_limit, run_market_order}, + order::{cancel_limit, claim_limit, place_limit, run_market_order}, state::orders, types::{LimitOrder, MarketOrder, OrderDirection}, }; @@ -73,15 +73,10 @@ impl OrderOperation { Ok(()) } OrderOperation::Claim((tick_id, order_id)) => { - claim_order( - deps.storage, - info.sender.clone(), - env.contract.address, - tick_id, - order_id, - ) - .unwrap(); - Ok(()) + match claim_limit(deps, env, info, tick_id, order_id) { + Ok(_) => Ok(()), + Err(err) => Err(err), + } } OrderOperation::Cancel((tick_id, order_id)) => { let order = orders() diff --git a/contracts/sumtree-orderbook/src/tick.rs b/contracts/sumtree-orderbook/src/tick.rs index b974b09..18a1824 100644 --- a/contracts/sumtree-orderbook/src/tick.rs +++ b/contracts/sumtree-orderbook/src/tick.rs @@ -43,7 +43,7 @@ pub fn sync_tick( // If tick state for current order direction is already up to date, // skip the check. This saves us from walking the tree for both order directions // even though in most cases we will likely only need to sync one. - if tick_value.last_tick_sync_etas == target_etas { + if tick_value.last_tick_sync_etas >= target_etas { continue; } @@ -67,6 +67,7 @@ pub fn sync_tick( .effective_total_amount_swapped .checked_add(realized_since_last_sync)?; tick_value.cumulative_realized_cancels = new_cumulative_realized_cancels; + tick_value.last_tick_sync_etas = target_etas; // Defense in depth guardrail: ensure that tick sync does not push tick ETAS past CTT. ensure!( From 5e5ea367818912de112deee2be08d18c6f101251 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 17 Jun 2024 09:26:49 +0100 Subject: [PATCH 06/98] fix: added exit condition to tick sync to account for moving etas value --- contracts/sumtree-orderbook/src/order.rs | 30 ++++++++++++------- .../src/sumtree/test/test_tree.rs | 3 -- .../sumtree-orderbook/src/sumtree/tree.rs | 4 +-- .../src/tests/e2e/cases/test_fuzz.rs | 23 ++++++++------ .../tests/e2e/cases/test_orders_success.rs | 2 +- .../src/tests/e2e/cases/utils.rs | 8 +++-- .../sumtree-orderbook/src/tests/test_order.rs | 15 +--------- .../sumtree-orderbook/src/tests/test_tick.rs | 14 ++++----- contracts/sumtree-orderbook/src/tick.rs | 7 +++-- 9 files changed, 54 insertions(+), 52 deletions(-) diff --git a/contracts/sumtree-orderbook/src/order.rs b/contracts/sumtree-orderbook/src/order.rs index 097867e..8e6d654 100644 --- a/contracts/sumtree-orderbook/src/order.rs +++ b/contracts/sumtree-orderbook/src/order.rs @@ -225,6 +225,17 @@ pub fn cancel_limit( tree.save(deps.storage)?; + sync_tick( + deps.storage, + tick_id, + curr_tick_state + .get_values(OrderDirection::Bid) + .effective_total_amount_swapped, + curr_tick_state + .get_values(OrderDirection::Ask) + .effective_total_amount_swapped, + )?; + Ok(Response::new() .add_attributes(vec![ ("method", "cancelLimit"), @@ -629,19 +640,19 @@ pub(crate) fn claim_order( let bid_tick_values = tick_state.get_values(OrderDirection::Bid); let ask_tick_values = tick_state.get_values(OrderDirection::Ask); - let (bid_etas, ask_etas) = match order.order_direction { - OrderDirection::Bid => (order.etas, bid_tick_values.effective_total_amount_swapped), - OrderDirection::Ask => (ask_tick_values.effective_total_amount_swapped, order.etas), - }; - - sync_tick(storage, tick_id, bid_etas, ask_etas)?; + sync_tick( + storage, + tick_id, + bid_tick_values.effective_total_amount_swapped, + ask_tick_values.effective_total_amount_swapped, + )?; // Re-fetch tick post sync call let tick_state = TICK_STATE .may_load(storage, tick_id)? .ok_or(ContractError::InvalidTickId { tick_id })?; let tick_values = tick_state.get_values(order.order_direction); - println!("tick_values: {:?}, order: {:?}", tick_values, order); + // Early exit if nothing has been filled ensure!( tick_values.effective_total_amount_swapped > order.etas, @@ -653,7 +664,7 @@ pub(crate) fn claim_order( // we don't claim more than the order has available. let amount_filled_dec = tick_values .effective_total_amount_swapped - .checked_sub(tick_values.cumulative_realized_cancels)? + .checked_sub(order.etas)? .min(Decimal256::from_ratio(order.quantity, 1u128)); let amount_filled = Uint128::try_from(amount_filled_dec.to_uint_floor())?; @@ -710,9 +721,6 @@ pub(crate) fn claim_order( amount = amount.checked_sub(maker_fee_amount)?; } - // Cannot send a zero amount, may be zero'd out by rounding - ensure!(!amount.is_zero(), ContractError::ZeroClaim); - // Claimed amount always goes to the order owner let bank_msg = MsgSend256 { from_address: contract_address.to_string(), diff --git a/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs b/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs index 33faf82..9d6d9e5 100644 --- a/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs +++ b/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs @@ -117,7 +117,6 @@ fn test_get_prefix_sum_valid() { NodeType::leaf_uint256(20u128, 5u128), ], target_etas: Decimal256::from_ratio(20u128, 1u128), - // We expect the sum of the 2nd and 3rd nodes, so 7 + 5 expected_sum: Decimal256::from_ratio(12u128, 1u128), }, @@ -129,7 +128,6 @@ fn test_get_prefix_sum_valid() { NodeType::leaf_uint256(100u128, 30u128), ], target_etas: Decimal256::from_ratio(75u128, 1u128), - expected_sum: Decimal256::from_ratio(30u128, 1u128), }, TestPrefixSumCase { @@ -140,7 +138,6 @@ fn test_get_prefix_sum_valid() { NodeType::leaf_uint256(30u128, 10u128), ], target_etas: Decimal256::from_ratio(25u128, 1u128), - expected_sum: Decimal256::from_ratio(20u128, 1u128), }, TestPrefixSumCase { diff --git a/contracts/sumtree-orderbook/src/sumtree/tree.rs b/contracts/sumtree-orderbook/src/sumtree/tree.rs index ad3de7f..28dd4fe 100644 --- a/contracts/sumtree-orderbook/src/sumtree/tree.rs +++ b/contracts/sumtree-orderbook/src/sumtree/tree.rs @@ -63,6 +63,7 @@ pub fn get_prefix_sum( // Since the longest path this function can walk is from the root to a leaf, it runs in O(log(N)) time. Given // how it is able to terminate early using our sumtree's range properties, in many cases it will likely run // in much less. + fn prefix_sum_walk( storage: &dyn Storage, node: &TreeNode, @@ -73,8 +74,7 @@ fn prefix_sum_walk( if target_etas < node.get_min_range() { // If the target ETAS is below the root node's range, we can return zero early. return Ok(Decimal256::zero()); - } else if target_etas >= node.get_max_range() { - // If the target ETAS is above the root node's range, we can return the full sum early. + } else if target_etas >= node.get_max_range().checked_sub(node.get_value())? { return Ok(current_sum); } diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index 43601a3..cb65b46 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -19,22 +19,22 @@ use crate::{ #[test] fn test_order_fuzz_large_orders_small_range() { - run_fuzz(2000, (-10, 10), 0.2); + run_fuzz_linear(2000, (-10, 10), 0.2); } #[test] fn test_order_fuzz_small_orders_large_range() { - run_fuzz(100, (MIN_TICK, MAX_TICK), 0.2); + run_fuzz_linear(100, (MIN_TICK, MAX_TICK), 0.2); } #[test] fn test_order_fuzz_small_orders_small_range() { - run_fuzz(100, (-10, 0), 0.1); + run_fuzz_linear(100, (-10, 0), 0.1); } #[test] fn test_order_fuzz_large_cancelled_orders_small_range() { - run_fuzz(1000, (MIN_TICK, MIN_TICK + 20), 0.8); + run_fuzz_linear(1000, (MIN_TICK, MIN_TICK + 20), 0.8); } // #[test] @@ -42,7 +42,7 @@ fn test_order_fuzz_large_cancelled_orders_small_range() { // run_fuzz(3000, (-750, 750), 0.2); // } -fn run_fuzz(amount_limit_orders: u64, tick_range: (i64, i64), cancel_probability: f64) { +fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_probability: f64) { let seed: u64 = 123456789; let mut rng = StdRng::seed_from_u64(seed); @@ -131,14 +131,14 @@ fn run_fuzz(amount_limit_orders: u64, tick_range: (i64, i64), cancel_probability .query(&QueryMsg::GetTotalPoolLiquidity {}) .unwrap(); println!("Total remaining pool liquidity: {:?}", total_pool_liquidity); - + orders.reverse(); for (username, tick_id, order_id) in orders.iter() { t.add_account( "claimant", vec![ Coin::new(1, "base"), Coin::new(1, "quote"), - Coin::new(1000000000u128, "uosmo"), + Coin::new(1000000000000u128, "uosmo"), ], ); let order: LimitOrder = t @@ -174,8 +174,13 @@ fn run_fuzz(amount_limit_orders: u64, tick_range: (i64, i64), cancel_probability // We cannot verify how much to expect as tick is synced as part of the claim process // Hence orders::claim is used instead of orders::claim_success - match orders::claim(&t, sender, order.tick_id, order.order_id) { - Ok(_) => {} + match orders::claim_success(&t, sender, order.tick_id, order.order_id) { + Ok(res) => { + let gas_used = res.gas_info.gas_used; + if gas_used >= 200000 { + println!("gas_used: {}", res.gas_info.gas_used); + } + } Err(e) => { println!("Failed to claim order {}: {:?}", order.order_id, e); println!("contract_balance: {:?}", contract_balance); diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs index a8432d9..e51f540 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs @@ -142,7 +142,7 @@ fn test_basic_order() { assert::spot_price(&t, expected_bid_tick, expected_ask_tick, case.name); // Claim limit - orders::claim_success(&t, case.claimer, 0, 0); + orders::claim_success(&t, case.claimer, 0, 0).unwrap(); match case.order_direction { OrderDirection::Ask => { assert::pool_liquidity(&t, case.placed_amount - case.filled_amount, 0u8, case.name); diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs index df22c52..33ad3e9 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs @@ -392,7 +392,12 @@ pub mod orders { ) } - pub fn claim_success(t: &TestEnv, sender: &str, tick_id: i64, order_id: u64) { + pub fn claim_success( + t: &TestEnv, + sender: &str, + tick_id: i64, + order_id: u64, + ) -> RunnerExecuteResult { let order: LimitOrder = t .contract .query(&QueryMsg::Order { order_id, tick_id }) @@ -467,7 +472,6 @@ pub mod orders { .as_slice(), || claim(t, sender, tick_id, order_id), ) - .unwrap(); } pub fn cancel_limit( diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index b76c961..8d785a8 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -918,7 +918,6 @@ fn test_run_market_order() { continue; } - println!("{:?}", test.name); // Assert no error let response = response.unwrap(); @@ -4175,21 +4174,9 @@ fn test_cancelled_orders() { } - OrderOperation::RunMarket(MarketOrder::new(Uint128::from(100u128).checked_mul(Uint128::from(4u128)).unwrap(), OrderDirection::Ask, sender.clone())).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); OrderOperation::PlaceLimit(LimitOrder::new(0, 10, OrderDirection::Bid, sender.clone(), Uint128::from(100u128), Decimal256::zero(), None)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); - + OrderOperation::RunMarket(MarketOrder::new(Uint128::from(100u128).checked_mul(Uint128::from(5u128)).unwrap(), OrderDirection::Ask, sender.clone())).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); - let err = OrderOperation::Claim((0, 10)).run(deps.as_mut(), env.clone(), info.clone()).unwrap_err(); - assert_eq!(err, ContractError::ZeroClaim); - - OrderOperation::RunMarket(MarketOrder::new(Uint128::from(100u128), OrderDirection::Ask, sender.clone())).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); - - for i in 0..10 { - let j = i; - if j % 3 == 0 { - OrderOperation::Claim((0, j)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); - } - } OrderOperation::Claim((0, 10)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); } \ No newline at end of file diff --git a/contracts/sumtree-orderbook/src/tests/test_tick.rs b/contracts/sumtree-orderbook/src/tests/test_tick.rs index 63a7d45..c9ba35d 100644 --- a/contracts/sumtree-orderbook/src/tests/test_tick.rs +++ b/contracts/sumtree-orderbook/src/tests/test_tick.rs @@ -125,7 +125,7 @@ fn test_sync_tick() { NodeType::leaf_uint256(50u32, 12u32), NodeType::leaf_uint256(62u32, 10u32), NodeType::leaf_uint256(80u32, 28u32), - NodeType::leaf_uint256(128u32, 70u32), + NodeType::leaf_uint256(178u32, 70u32), ], unrealized_cancels_ask: vec![], @@ -133,7 +133,7 @@ fn test_sync_tick() { // Iteration 1: only node 1 is included // Iteration 2: first two nodes included // Iteration 3: first four nodes are incldued - new_bid_etas_per_sync: Decimal256::from_ratio(30u128, 1u128), + new_bid_etas_per_sync: Decimal256::from_ratio(20u128, 1u128), new_ask_etas_per_sync: Decimal256::zero(), num_syncs: 4, @@ -145,7 +145,7 @@ fn test_sync_tick() { // The new ETAS includes all the incremented amounts (3 * 30 each) which represent fills, // plus the amount of realized cancellations expected_new_bid_etas_post_sync: Decimal256::from_ratio( - (4u128 * 30u128) + 80u128, + (4u128 * 20u128) + 80u128, 1u128, ), expected_new_ask_etas_post_sync: Decimal256::zero(), @@ -244,7 +244,7 @@ fn test_sync_tick() { NodeType::leaf_uint256(50u32, 12u32), NodeType::leaf_uint256(62u32, 10u32), NodeType::leaf_uint256(80u32, 28u32), - NodeType::leaf_uint256(128u32, 70u32), + NodeType::leaf_uint256(178u32, 70u32), ], unrealized_cancels_bid: vec![], @@ -252,7 +252,7 @@ fn test_sync_tick() { // Iteration 1: only node 1 is included // Iteration 2: first two nodes included // Iteration 3: first four nodes are included - new_ask_etas_per_sync: Decimal256::from_ratio(30u128, 1u128), + new_ask_etas_per_sync: Decimal256::from_ratio(20u128, 1u128), new_bid_etas_per_sync: Decimal256::zero(), num_syncs: 4, @@ -264,7 +264,7 @@ fn test_sync_tick() { // The new ETAS includes all the incremented amounts (3 * 30 each) which represent fills, // plus the amount of realized cancellations expected_new_ask_etas_post_sync: Decimal256::from_ratio( - (4u128 * 30u128) + 80u128, + (4u128 * 20u128) + 80u128, 1u128, ), expected_new_bid_etas_post_sync: Decimal256::zero(), @@ -305,7 +305,7 @@ fn test_sync_tick() { // Multiple unrealized cancels for both bid and ask unrealized_cancels_bid: vec![ - NodeType::leaf_uint256(35u32, 25u32), + NodeType::leaf_uint256(50u32, 25u32), NodeType::leaf_uint256(10u32, 25u32), ], unrealized_cancels_ask: vec![ diff --git a/contracts/sumtree-orderbook/src/tick.rs b/contracts/sumtree-orderbook/src/tick.rs index 18a1824..d980725 100644 --- a/contracts/sumtree-orderbook/src/tick.rs +++ b/contracts/sumtree-orderbook/src/tick.rs @@ -43,7 +43,8 @@ pub fn sync_tick( // If tick state for current order direction is already up to date, // skip the check. This saves us from walking the tree for both order directions // even though in most cases we will likely only need to sync one. - if tick_value.last_tick_sync_etas >= target_etas { + + if tick_value.last_tick_sync_etas == target_etas { continue; } @@ -84,8 +85,8 @@ pub fn sync_tick( } // Write updated tick values to state - tick_state.set_values(OrderDirection::Bid, bid_values); - tick_state.set_values(OrderDirection::Ask, ask_values); + tick_state.set_values(OrderDirection::Bid, bid_values.clone()); + tick_state.set_values(OrderDirection::Ask, ask_values.clone()); TICK_STATE.save(storage, tick_id, &tick_state)?; Ok(()) From ee3d61af4361795f400e9e15c69a3fdeaf2f424b Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 17 Jun 2024 09:34:06 +0100 Subject: [PATCH 07/98] test: fixed test_get_prefix_sum_valid --- .../src/sumtree/test/test_tree.rs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs b/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs index 9d6d9e5..206f26a 100644 --- a/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs +++ b/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs @@ -21,7 +21,6 @@ fn test_get_prefix_sum_valid() { name: "Single node, target ETAS equal to node ETAS", nodes: vec![NodeType::leaf_uint256(10u128, 5u128)], target_etas: Decimal256::from_ratio(10u128, 1u128), - // We expect the full value of the node because the prefix // sum is intended to return "all nodes that overlap with // the target ETAS". @@ -34,14 +33,12 @@ fn test_get_prefix_sum_valid() { name: "Single node, target ETAS below node range", nodes: vec![NodeType::leaf_uint256(50u128, 20u128)], target_etas: Decimal256::from_ratio(25u128, 1u128), - expected_sum: Decimal256::zero(), }, TestPrefixSumCase { name: "Single node, target ETAS above node range", nodes: vec![NodeType::leaf_uint256(10u128, 10u128)], target_etas: Decimal256::from_ratio(30u128, 1u128), - expected_sum: Decimal256::from_ratio(10u128, 1u128), }, TestPrefixSumCase { @@ -52,8 +49,7 @@ fn test_get_prefix_sum_valid() { NodeType::leaf_uint256(35u128, 30u128), ], target_etas: Decimal256::from_ratio(20u128, 1u128), - - expected_sum: Decimal256::from_ratio(30u128, 1u128), + expected_sum: Decimal256::from_ratio(60u128, 1u128), }, TestPrefixSumCase { name: "Multiple nodes, target ETAS on boundary", @@ -94,8 +90,7 @@ fn test_get_prefix_sum_valid() { NodeType::leaf_uint256(20u128, 5u128), NodeType::leaf_uint256(10u128, 5u128), ], - target_etas: Decimal256::from_ratio(25u128, 1u128), - + target_etas: Decimal256::from_ratio(15u128, 1u128), expected_sum: Decimal256::from_ratio(10u128, 1u128), }, TestPrefixSumCase { @@ -105,7 +100,7 @@ fn test_get_prefix_sum_valid() { NodeType::leaf_uint256(10u128, 5u128), NodeType::leaf_uint256(20u128, 5u128), ], - target_etas: Decimal256::from_ratio(25u128, 1u128), + target_etas: Decimal256::from_ratio(15u128, 1u128), expected_sum: Decimal256::from_ratio(10u128, 1u128), }, @@ -116,7 +111,7 @@ fn test_get_prefix_sum_valid() { NodeType::leaf_uint256(10u128, 7u128), NodeType::leaf_uint256(20u128, 5u128), ], - target_etas: Decimal256::from_ratio(20u128, 1u128), + target_etas: Decimal256::from_ratio(15u128, 1u128), // We expect the sum of the 2nd and 3rd nodes, so 7 + 5 expected_sum: Decimal256::from_ratio(12u128, 1u128), }, @@ -127,7 +122,7 @@ fn test_get_prefix_sum_valid() { NodeType::leaf_uint256(50u128, 20u128), NodeType::leaf_uint256(100u128, 30u128), ], - target_etas: Decimal256::from_ratio(75u128, 1u128), + target_etas: Decimal256::from_ratio(60u128, 1u128), expected_sum: Decimal256::from_ratio(30u128, 1u128), }, TestPrefixSumCase { @@ -137,8 +132,8 @@ fn test_get_prefix_sum_valid() { NodeType::leaf_uint256(20u128, 10u128), NodeType::leaf_uint256(30u128, 10u128), ], - target_etas: Decimal256::from_ratio(25u128, 1u128), - expected_sum: Decimal256::from_ratio(20u128, 1u128), + target_etas: Decimal256::from_ratio(10u128, 1u128), + expected_sum: Decimal256::from_ratio(30u128, 1u128), }, TestPrefixSumCase { name: "Complex case with many nodes (shuffled, adjacent, spaced out)", From 225d219084fd713242c8e1f577b7a4eb16ad0f92 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 17 Jun 2024 09:40:57 +0100 Subject: [PATCH 08/98] test: fixed spot price query test --- .../src/sumtree/test/test_fuzz.rs | 2 +- .../src/sumtree/test/test_node.rs | 2 +- .../sumtree-orderbook/src/tests/test_query.rs | 17 ++++++++--------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs b/contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs index abbe438..9f05bd3 100644 --- a/contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs @@ -66,7 +66,7 @@ fn test_fuzz_insert() { // If the inserted node's start ETAS is <= the target ETAS, we add the node's amount // to our expected prefix sum. - if node.get_min_range() <= target_etas { + if node.get_max_range().checked_sub(node.get_value()).unwrap() <= target_etas { expected_prefix_sum = expected_prefix_sum.checked_add(node.get_value()).unwrap(); } diff --git a/contracts/sumtree-orderbook/src/sumtree/test/test_node.rs b/contracts/sumtree-orderbook/src/sumtree/test/test_node.rs index f8ee461..c1c0912 100644 --- a/contracts/sumtree-orderbook/src/sumtree/test/test_node.rs +++ b/contracts/sumtree-orderbook/src/sumtree/test/test_node.rs @@ -2736,7 +2736,7 @@ fn test_node_insert_large_quantity() { tree.insert(deps.as_mut().storage, &mut node).unwrap(); tree = get_root_node(deps.as_ref().storage, tick_id, direction).unwrap(); // Track insertions that fall below our target ETAS - if node.get_min_range() <= target_etas { + if node.get_min_range() <= target_etas || node.get_max_range().checked_sub(node.get_value()).unwrap() <= target_etas { expected_prefix_sum = expected_prefix_sum.checked_add(Decimal256::one()).unwrap(); } } diff --git a/contracts/sumtree-orderbook/src/tests/test_query.rs b/contracts/sumtree-orderbook/src/tests/test_query.rs index 0ffec63..05d964c 100644 --- a/contracts/sumtree-orderbook/src/tests/test_query.rs +++ b/contracts/sumtree-orderbook/src/tests/test_query.rs @@ -58,7 +58,7 @@ fn test_query_spot_price() { 1, OrderDirection::Ask, sender.clone(), - Uint128::one(), + Uint128::from(2u128), Decimal256::zero(), None, )), @@ -67,7 +67,7 @@ fn test_query_spot_price() { 2, OrderDirection::Ask, sender.clone(), - Uint128::one(), + Uint128::from(2u128), Decimal256::zero(), None, )), @@ -76,7 +76,7 @@ fn test_query_spot_price() { 3, OrderDirection::Ask, sender.clone(), - Uint128::one(), + Uint128::from(2u128), Decimal256::zero(), None, )), @@ -130,7 +130,7 @@ fn test_query_spot_price() { 1, OrderDirection::Ask, sender.clone(), - Uint128::one(), + Uint128::MAX, Decimal256::zero(), None, )), @@ -169,7 +169,7 @@ fn test_query_spot_price() { 1, OrderDirection::Bid, sender.clone(), - Uint128::one(), + Uint128::from(2u128), Decimal256::zero(), None, )), @@ -178,7 +178,7 @@ fn test_query_spot_price() { 2, OrderDirection::Bid, sender.clone(), - Uint128::one(), + Uint128::from(2u128), Decimal256::zero(), None, )), @@ -187,7 +187,7 @@ fn test_query_spot_price() { 3, OrderDirection::Bid, sender.clone(), - Uint128::one(), + Uint128::from(2u128), Decimal256::zero(), None, )), @@ -241,7 +241,7 @@ fn test_query_spot_price() { 1, OrderDirection::Bid, sender.clone(), - Uint128::one(), + Uint128::MAX, Decimal256::zero(), None, )), @@ -296,7 +296,6 @@ fn test_query_spot_price() { let mut deps = mock_dependencies_custom(); let env = mock_env(); let info = mock_info(sender.as_str(), &[]); - create_orderbook( deps.as_mut(), QUOTE_DENOM.to_string(), From 84ad5d5c62a694335cbfa15b5fbe69df93e8d68b Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 17 Jun 2024 09:55:16 +0100 Subject: [PATCH 09/98] test: fixed test_run_market_order --- contracts/sumtree-orderbook/src/order.rs | 1 - .../src/tests/e2e/cases/test_fuzz.rs | 40 +------------------ .../sumtree-orderbook/src/tests/test_order.rs | 12 +++--- .../sumtree-orderbook/src/tests/test_query.rs | 22 +++------- 4 files changed, 12 insertions(+), 63 deletions(-) diff --git a/contracts/sumtree-orderbook/src/order.rs b/contracts/sumtree-orderbook/src/order.rs index 8e6d654..28e315b 100644 --- a/contracts/sumtree-orderbook/src/order.rs +++ b/contracts/sumtree-orderbook/src/order.rs @@ -50,7 +50,6 @@ pub fn place_limit( tick_price, RoundingDirection::Down, )?; - ensure!( !amount_out.is_zero(), ContractError::InvalidQuantity { quantity } diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index cb65b46..0346b0e 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -153,48 +153,10 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob } else { username }; - let AllTicksResponse { ticks } = t - .contract - .query(&QueryMsg::AllTicks { - start_from: Some(order.tick_id), - end_at: None, - limit: Some(1), - }) - .unwrap(); - let tick = ticks.first().unwrap(); - let price = tick_to_price(tick.tick_id).unwrap(); - let value = amount_to_value( - order.order_direction, - order.quantity, - price, - RoundingDirection::Down, - ) - .unwrap(); - let contract_balance = Coins::try_from(t.get_balance(&t.contract.contract_addr)).unwrap(); // We cannot verify how much to expect as tick is synced as part of the claim process // Hence orders::claim is used instead of orders::claim_success - match orders::claim_success(&t, sender, order.tick_id, order.order_id) { - Ok(res) => { - let gas_used = res.gas_info.gas_used; - if gas_used >= 200000 { - println!("gas_used: {}", res.gas_info.gas_used); - } - } - Err(e) => { - println!("Failed to claim order {}: {:?}", order.order_id, e); - println!("contract_balance: {:?}", contract_balance); - println!( - "order etas: {}, price: {}, value: {}, tick etas: {}", - order.etas, - price, - value, - tick.tick_state - .get_values(order.order_direction) - .effective_total_amount_swapped - ); - } - } + orders::claim(&t, sender, order.tick_id, order.order_id).unwrap(); let maybe_order = t.contract.query::(&QueryMsg::Order { order_id: *order_id, diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index 8d785a8..3d21d34 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -711,7 +711,7 @@ fn test_run_market_order() { tick_bound: MAX_TICK, // Orders to fill against orders: generate_limit_orders( - &[-1500000, 40000000], + &[-1500000, 1500000], // 500 units of liquidity on each tick 5, default_quantity, @@ -721,19 +721,19 @@ fn test_run_market_order() { // implies 1000*0.85 = 850 units of output, but there is only 500 on the tick. // // So 500 gets filled at -1500000, corresponding to ~589 of the input (500/0.85). - // The remaining 1 unit is filled at tick 40,000,000 (price $50,000), which + // The remaining 1 unit is filled at tick 1500000 (price $2.5), which // corresponds to the remaining liquidity. // // Thus, the total expected output is 502. // // Note: this case does not cover rounding for input consumption since it overfills // the tick. - expected_output: Uint256::from_u128(1000), + expected_output: Uint256::from_u128(502), expected_tick_etas: vec![ (-1500000, decimal256_from_u128(Uint128::new(500))), - (40000000, decimal256_from_u128(Uint128::new(500))), + (1500000, decimal256_from_u128(Uint128::new(2))), ], - expected_tick_pointers: vec![(OrderDirection::Ask, 40000000)], + expected_tick_pointers: vec![(OrderDirection::Ask, 1500000)], expected_error: None, }, RunMarketOrderTestCase { @@ -856,7 +856,7 @@ fn test_run_market_order() { tick_bound: MAX_TICK, // Orders to fill against orders: generate_limit_orders( - &[40000000], + &[1500000], // Four limit orders with sufficient total liquidity to process the // full market order 4, diff --git a/contracts/sumtree-orderbook/src/tests/test_query.rs b/contracts/sumtree-orderbook/src/tests/test_query.rs index 05d964c..cfd10d4 100644 --- a/contracts/sumtree-orderbook/src/tests/test_query.rs +++ b/contracts/sumtree-orderbook/src/tests/test_query.rs @@ -665,34 +665,22 @@ fn test_total_pool_liquidity() { pre_operations: vec![ OrderOperation::PlaceLimitMulti(( // Increasingly spread ticks - vec![ - -1, - -2, - -3, - -5, - -8, - -13, - -21, - -34, - -55, - LARGE_NEGATIVE_TICK, - MIN_TICK, - ], + vec![-1, -2, -3, -5, -8, -13, -21, -34, -55, LARGE_NEGATIVE_TICK], 100, Uint128::from(50u128), OrderDirection::Bid, )), OrderOperation::PlaceLimitMulti(( // Increasingly spread ticks - vec![1, 2, 3, 5, 8, 13, 21, 34, 55, LARGE_POSITIVE_TICK, MAX_TICK], + vec![1, 2, 3, 5, 8, 13, 21, 34, 55, LARGE_POSITIVE_TICK], 100, Uint128::from(110u128), OrderDirection::Ask, )), ], - // Base: 11 ticks at 110*100 = 11000*11 = 121000 - // Quote: 11 ticks at 50*100 = 5000*11 = 55000 - expected_output: vec![coin(121000, BASE_DENOM), coin(55000, QUOTE_DENOM)], + // Base: 11 ticks at 110*100 = 11000*10 = 110000 + // Quote: 11 ticks at 50*100 = 5000*10 = 55000 + expected_output: vec![coin(110000, BASE_DENOM), coin(50000, QUOTE_DENOM)], expected_error: None, }, ]; From 55e061c1db608557e682ac75b0b00ee2b8ed4fc0 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 17 Jun 2024 09:55:44 +0100 Subject: [PATCH 10/98] chore: removed unused imports --- contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs | 2 +- contracts/sumtree-orderbook/src/tests/test_query.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index 0346b0e..af637f4 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -6,7 +6,7 @@ use rand::{rngs::StdRng, SeedableRng}; use super::utils::{assert, orders}; use crate::constants::{MAX_TICK, MIN_TICK}; -use crate::msg::{AllTicksResponse, CalcOutAmtGivenInResponse, QueryMsg, SpotPriceResponse}; +use crate::msg::{CalcOutAmtGivenInResponse, QueryMsg, SpotPriceResponse}; use crate::tests::e2e::modules::cosmwasm_pool::CosmwasmPool; use crate::tick_math::{amount_to_value, tick_to_price, RoundingDirection}; use crate::types::LimitOrder; diff --git a/contracts/sumtree-orderbook/src/tests/test_query.rs b/contracts/sumtree-orderbook/src/tests/test_query.rs index cfd10d4..a348608 100644 --- a/contracts/sumtree-orderbook/src/tests/test_query.rs +++ b/contracts/sumtree-orderbook/src/tests/test_query.rs @@ -5,7 +5,7 @@ use cosmwasm_std::{ }; use crate::{ - constants::{EXPECTED_SWAP_FEE, MAX_TICK, MIN_TICK}, + constants::EXPECTED_SWAP_FEE, orderbook::create_orderbook, query, state::IS_ACTIVE, From dcef0f36555386b22322c85acf7fd3cf8d67d7d6 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 17 Jun 2024 10:07:52 +0100 Subject: [PATCH 11/98] test: fixed failing e2e fuzz test --- contracts/sumtree-orderbook/Cargo.toml | 1 + contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs | 2 +- .../sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs | 7 ++++++- contracts/sumtree-orderbook/src/tests/e2e/mod.rs | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/contracts/sumtree-orderbook/Cargo.toml b/contracts/sumtree-orderbook/Cargo.toml index e194377..65c2aa9 100644 --- a/contracts/sumtree-orderbook/Cargo.toml +++ b/contracts/sumtree-orderbook/Cargo.toml @@ -31,6 +31,7 @@ rpath = false backtraces = ["cosmwasm-std/backtraces"] # use library feature to disable all instantiate/execute/query exports library = [] +skip-integration-tests = [] [package.metadata.scripts] optimize = """docker run --rm -v "$(pwd)":/code \ diff --git a/contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs b/contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs index 9f05bd3..9c36903 100644 --- a/contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs @@ -66,7 +66,7 @@ fn test_fuzz_insert() { // If the inserted node's start ETAS is <= the target ETAS, we add the node's amount // to our expected prefix sum. - if node.get_max_range().checked_sub(node.get_value()).unwrap() <= target_etas { + if node.get_min_range() <= target_etas.checked_add(expected_prefix_sum).unwrap() { expected_prefix_sum = expected_prefix_sum.checked_add(node.get_value()).unwrap(); } diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index af637f4..e82e7c3 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -17,6 +17,11 @@ use crate::{ types::OrderDirection, }; +// Tick Price = 2 +pub(crate) const LARGE_POSITIVE_TICK: i64 = 1000000; +// Tick Price = 0.5 +pub(crate) const LARGE_NEGATIVE_TICK: i64 = -5000000; + #[test] fn test_order_fuzz_large_orders_small_range() { run_fuzz_linear(2000, (-10, 10), 0.2); @@ -24,7 +29,7 @@ fn test_order_fuzz_large_orders_small_range() { #[test] fn test_order_fuzz_small_orders_large_range() { - run_fuzz_linear(100, (MIN_TICK, MAX_TICK), 0.2); + run_fuzz_linear(100, (LARGE_NEGATIVE_TICK, LARGE_POSITIVE_TICK), 0.2); } #[test] diff --git a/contracts/sumtree-orderbook/src/tests/e2e/mod.rs b/contracts/sumtree-orderbook/src/tests/e2e/mod.rs index 3286abc..9ff109b 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/mod.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/mod.rs @@ -1,4 +1,4 @@ -#![cfg(all(not(tarpaulin), not(feature = "skip-integration-test")))] +#![cfg(all(not(tarpaulin), not(feature = "skip-integration-tests")))] mod cases; mod modules; From 453b4d2b0225cd77b7baf315db2b0883bb68ff80 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 17 Jun 2024 10:12:04 +0100 Subject: [PATCH 12/98] test: added etas invariant check to ticks for e2e --- .../sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs | 2 +- contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index e82e7c3..d09d9e5 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -5,7 +5,7 @@ use rand::Rng; use rand::{rngs::StdRng, SeedableRng}; use super::utils::{assert, orders}; -use crate::constants::{MAX_TICK, MIN_TICK}; +use crate::constants::MIN_TICK; use crate::msg::{CalcOutAmtGivenInResponse, QueryMsg, SpotPriceResponse}; use crate::tests::e2e::modules::cosmwasm_pool::CosmwasmPool; use crate::tick_math::{amount_to_value, tick_to_price, RoundingDirection}; diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs index 33ad3e9..66d5eea 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs @@ -206,6 +206,15 @@ pub mod assert { pub fn tick_invariants(t: &TestEnv) { let ticks = t.contract.collect_all_ticks(); + assert!(ticks + .iter() + .all(|t| t.tick_state.ask_values.effective_total_amount_swapped + <= t.tick_state.ask_values.cumulative_total_value)); + assert!(ticks + .iter() + .all(|t| t.tick_state.bid_values.effective_total_amount_swapped + <= t.tick_state.bid_values.cumulative_total_value)); + let ticks_with_bid_amount = ticks.iter().filter(|tick| { !tick .tick_state From 49684e02fb8f43ebcd50103cc4020670e5073550 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 17 Jun 2024 10:26:59 +0100 Subject: [PATCH 13/98] chore: commented e2e linear fuzz test --- .../src/tests/e2e/cases/test_fuzz.rs | 126 +++++++++++------- .../src/tests/e2e/test_env.rs | 27 +++- 2 files changed, 100 insertions(+), 53 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index d09d9e5..5db2719 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -1,6 +1,7 @@ use cosmwasm_std::{Coin, Coins, Decimal, Uint256}; use cosmwasm_std::{Decimal256, Uint128}; use osmosis_test_tube::{Module, OsmosisTestApp}; +use rand::seq::SliceRandom; use rand::Rng; use rand::{rngs::StdRng, SeedableRng}; @@ -42,12 +43,19 @@ fn test_order_fuzz_large_cancelled_orders_small_range() { run_fuzz_linear(1000, (MIN_TICK, MIN_TICK + 20), 0.8); } +// This test takes a very long time to run // #[test] // fn test_order_fuzz_very_large_orders_no_bounds() { // run_fuzz(3000, (-750, 750), 0.2); // } +/// Runs a linear fuzz test with the following steps +/// 1. Place x amount of random limit orders in given tick range and cancel with provided probability +/// 2. For both directions fill the entire amount of liquidity available using market orders +/// 3. Claim orders in random order +/// 4. Assert that the orders were filled correctly fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_probability: f64) { + // -- Test Setup -- let seed: u64 = 123456789; let mut rng = StdRng::seed_from_u64(seed); @@ -55,6 +63,12 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob let cp = CosmwasmPool::new(&app); let mut t = setup!(app, "quote", "base"); let mut orders = vec![]; + + // -- System Under Test -- + + // Places the set amount of orders within the provided tick range + // Orders will be cancelled with a chance equal to the provided cancel_probability + // Tick state is verified after every order is placed (and cancelled) for i in 0..amount_limit_orders { let username = format!("user{}", i); let chosen_tick = place_random_order(&mut t, &mut rng, &username, tick_range); @@ -67,38 +81,26 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob assert::tick_invariants(&t); } + // For both directions fill the entire amount of liquidity available using market orders + // For certain cases it is not possible to fill the entire liquidity so a remainder of 1 may occur for order_direction in [OrderDirection::Bid, OrderDirection::Ask] { - let GetTotalPoolLiquidityResponse { - total_pool_liquidity, - } = t - .contract - .query(&QueryMsg::GetTotalPoolLiquidity {}) - .unwrap(); - let mut liquidity = if order_direction == OrderDirection::Bid { - Coins::try_from(total_pool_liquidity.clone()) - .unwrap() - .amount_of("base") - } else { - Coins::try_from(total_pool_liquidity.clone()) - .unwrap() - .amount_of("quote") - }; + // Determine the amount of liquidity for the given direction + let mut liquidity = t.contract.get_directional_liquidity(order_direction); let mut zero_amount_returns = 0; let mut user_id = 0; - while liquidity.gt(&Uint128::one()) { + + // While there is some fillable liquidity we want to place randomised market orders + while liquidity > 1u128 { let username = format!("user{}{}", order_direction, user_id); - let placed_amount = place_random_market( - &cp, - &mut t, - &mut rng, - &username, - order_direction, - liquidity.u128(), - ); + let placed_amount = + place_random_market(&cp, &mut t, &mut rng, &username, order_direction, liquidity); + // Increment the username of the order placer user_id += 1; if placed_amount == 0 { + // In the case that the last order cannot be filled we want an exit condition + // If there are 100 consecutive zero amount returns we will break zero_amount_returns += 1; if zero_amount_returns == 100 { break; @@ -106,21 +108,11 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob continue; } - let GetTotalPoolLiquidityResponse { - total_pool_liquidity, - } = t - .contract - .query(&QueryMsg::GetTotalPoolLiquidity {}) - .unwrap(); - liquidity = if order_direction == OrderDirection::Bid { - Coins::try_from(total_pool_liquidity.clone()) - .unwrap() - .amount_of("base") - } else { - Coins::try_from(total_pool_liquidity.clone()) - .unwrap() - .amount_of("quote") - }; + // Reset counter as order was placed + zero_amount_returns = 0; + + // Update the liquidity + liquidity = t.contract.get_directional_liquidity(order_direction); assert::tick_invariants(&t); } println!( @@ -136,8 +128,15 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob .query(&QueryMsg::GetTotalPoolLiquidity {}) .unwrap(); println!("Total remaining pool liquidity: {:?}", total_pool_liquidity); - orders.reverse(); + + // Shuffle the order of recorded orders (as liquidity is fully filled (except the possibility of a 1 remainder)) + // every order should be claimable and the order should not matter + orders.shuffle(&mut rng); + + let mut remainder_orders = 0; for (username, tick_id, order_id) in orders.iter() { + // If the order has a claim bounty we will use a separate sender to verify that the bounty is claimed correctly + // Otherwise we will use the original sender to verify that the order is claimed correctly t.add_account( "claimant", vec![ @@ -159,32 +158,44 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob username }; - // We cannot verify how much to expect as tick is synced as part of the claim process - // Hence orders::claim is used instead of orders::claim_success orders::claim(&t, sender, order.tick_id, order.order_id).unwrap(); + // For the situation that the order has the 1 remainder we record this for assertions let maybe_order = t.contract.query::(&QueryMsg::Order { order_id: *order_id, tick_id: *tick_id, }); if let Ok(order) = maybe_order { println!("order: {:?}", order); + remainder_orders += 1; } } + + // Assert orders were filled correctly + assert!( + remainder_orders <= 2, + "There should be at most 2 orders that have a remainder, received {}", + remainder_orders + ); } +/// Places a random limit order in the provided tick range using the provided username fn place_random_order( t: &mut TestEnv, rng: &mut StdRng, username: &str, tick_range: (i64, i64), ) -> i64 { + // Quantities are in magnitudes of u32 let quantity = Uint128::from(rng.gen::()); + // 50% chance to choose either direction let order_direction = if rng.gen_bool(0.5) { OrderDirection::Bid } else { OrderDirection::Ask }; + + // Get the appropriate denom for the chosen direction let DenomsResponse { base_denom, quote_denom, @@ -194,8 +205,11 @@ fn place_random_order( } else { &base_denom }; + // Select a random tick from the provided range let tick_id = rng.gen_range(tick_range.0..=tick_range.1); + // Convert the tick to a price let price = tick_to_price(tick_id).unwrap(); + // Calculate the minimum amount of the denom that can be bought at the given price let min = Uint128::try_from( amount_to_value( order_direction.opposite(), @@ -208,6 +222,7 @@ fn place_random_order( ) .unwrap(); + // Add the user account with the appropriate amount of the denom t.add_account( username, vec![ @@ -216,6 +231,7 @@ fn place_random_order( ], ); + // Give orders an 80% chance of having a randomised bounty (may be 0) let has_claim_bounty = rng.gen_bool(0.8); let claim_bounty = if has_claim_bounty { Some(Decimal256::percent(rng.gen_range(0..=1))) @@ -223,6 +239,7 @@ fn place_random_order( None }; + // Place the generated limit orders::place_limit( t, tick_id, @@ -233,18 +250,11 @@ fn place_random_order( ) .unwrap(); - // println!( - // "username: {}, sender: {}, tick_id: {}, order_direction: {}, quantity: {}, claim_bounty: {}", - // username, - // t.accounts[username].address(), - // tick_id, - // order_direction, - // quantity, - // claim_bounty.unwrap_or_default() - // ); + // Return the tick id to record the order tick_id } +/// Places a random market order in the provided tick range using the provided username with at most max value fn place_random_market( cp: &CosmwasmPool, t: &mut TestEnv, @@ -253,11 +263,13 @@ fn place_random_market( order_direction: OrderDirection, max: u128, ) -> u128 { + // Get the appropriate denom for the chosen direction let (token_in_denom, token_out_denom) = if order_direction == OrderDirection::Bid { ("quote", "base") } else { ("base", "quote") }; + // Get the spot price for the given denoms let SpotPriceResponse { spot_price } = t .contract .query(&QueryMsg::SpotPrice { @@ -265,6 +277,10 @@ fn place_random_market( quote_asset_denom: token_out_denom.to_string(), }) .unwrap(); + + // Determine how much liquidity is available for token in at the current spot price + // This only provides an estimate as the liquidity may be spread across multiple ticks + // Hence why it can be difficult to fill the ENTIRE liquidity let liquidity_at_price_u256 = amount_to_value( order_direction.opposite(), Uint128::from(max), @@ -273,8 +289,11 @@ fn place_random_market( ) .unwrap(); + // Select a random amount of the token in to swap let liquidity_at_price = Uint128::try_from(liquidity_at_price_u256).unwrap(); let amount = rng.gen_range(0..=liquidity_at_price.u128()); + + // Calculate the expected amount of token out let expected_out = t.contract .query::(&QueryMsg::CalcOutAmountGivenIn { @@ -282,10 +301,13 @@ fn place_random_market( token_out_denom: token_out_denom.to_string(), swap_fee: Decimal::zero(), }); + + // If the provided error cannot be filled then we return a 0 amount if amount == 0 || expected_out.is_err() || expected_out.unwrap().token_out.amount == "0" { return 0; } + // Generate the user account t.add_account( username, vec![ @@ -293,6 +315,8 @@ fn place_random_market( Coin::new(1000000000000000u128, "uosmo"), ], ); + + // Places the market order and ensures that funds are transferred correctly orders::place_market_success(cp, t, order_direction, amount, username).unwrap(); amount } diff --git a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs index 7e20381..c399188 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs @@ -2,11 +2,15 @@ use std::{collections::HashMap, path::PathBuf}; use crate::{ constants::{MAX_TICK, MIN_TICK}, - msg::{AllTicksResponse, DenomsResponse, ExecuteMsg, InstantiateMsg, QueryMsg, TickIdAndState}, + msg::{ + AllTicksResponse, DenomsResponse, ExecuteMsg, GetTotalPoolLiquidityResponse, + InstantiateMsg, QueryMsg, TickIdAndState, + }, + types::OrderDirection, ContractError, }; -use cosmwasm_std::{to_json_binary, Coin}; +use cosmwasm_std::{to_json_binary, Coin, Coins}; use osmosis_std::types::{ cosmos::bank::v1beta1::QueryAllBalancesRequest, cosmwasm::wasm::v1::MsgExecuteContractResponse, @@ -249,6 +253,25 @@ impl<'a> OrderbookContract<'a> { } ticks } + + pub fn get_directional_liquidity(&self, order_direction: OrderDirection) -> u128 { + let GetTotalPoolLiquidityResponse { + total_pool_liquidity, + } = self.query(&QueryMsg::GetTotalPoolLiquidity {}).unwrap(); + + // Determine the amount of liquidity for the given direction + let liquidity = if order_direction == OrderDirection::Bid { + Coins::try_from(total_pool_liquidity.clone()) + .unwrap() + .amount_of("base") + } else { + Coins::try_from(total_pool_liquidity.clone()) + .unwrap() + .amount_of("quote") + }; + + liquidity.u128() + } } pub fn _assert_contract_err(expected: ContractError, actual: RunnerError) { From d0dc7db1169306b6664f76b5134345ca5f6504da Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 17 Jun 2024 10:43:02 +0100 Subject: [PATCH 14/98] test: added more e2e tests and improved cancelled orders test --- .../src/tests/e2e/cases/test_fuzz.rs | 27 ++++++++++++------- .../sumtree-orderbook/src/tests/test_order.rs | 9 ++++--- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index 5db2719..af5eea8 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Coin, Coins, Decimal, Uint256}; +use cosmwasm_std::{Coin, Decimal, Uint256}; use cosmwasm_std::{Decimal256, Uint128}; use osmosis_test_tube::{Module, OsmosisTestApp}; use rand::seq::SliceRandom; @@ -24,30 +24,35 @@ pub(crate) const LARGE_POSITIVE_TICK: i64 = 1000000; pub(crate) const LARGE_NEGATIVE_TICK: i64 = -5000000; #[test] -fn test_order_fuzz_large_orders_small_range() { +fn test_order_fuzz_linear_large_orders_small_range() { run_fuzz_linear(2000, (-10, 10), 0.2); } #[test] -fn test_order_fuzz_small_orders_large_range() { +fn test_order_fuzz_linear_small_orders_large_range() { run_fuzz_linear(100, (LARGE_NEGATIVE_TICK, LARGE_POSITIVE_TICK), 0.2); } +// This test takes a VERY long time to run +// #[test] +// fn test_order_fuzz_linear_very_large_orders_large_range() { +// run_fuzz_linear(5000, (LARGE_NEGATIVE_TICK, LARGE_POSITIVE_TICK), 0.2); +// } + #[test] -fn test_order_fuzz_small_orders_small_range() { +fn test_order_fuzz_linear_small_orders_small_range() { run_fuzz_linear(100, (-10, 0), 0.1); } #[test] -fn test_order_fuzz_large_cancelled_orders_small_range() { +fn test_order_fuzz_linear_large_cancelled_orders_small_range() { run_fuzz_linear(1000, (MIN_TICK, MIN_TICK + 20), 0.8); } -// This test takes a very long time to run -// #[test] -// fn test_order_fuzz_very_large_orders_no_bounds() { -// run_fuzz(3000, (-750, 750), 0.2); -// } +#[test] +fn test_order_fuzz_linear_small_cancelled_orders_large_range() { + run_fuzz_linear(100, (LARGE_NEGATIVE_TICK, LARGE_POSITIVE_TICK), 0.8); +} /// Runs a linear fuzz test with the following steps /// 1. Place x amount of random limit orders in given tick range and cancel with provided probability @@ -171,6 +176,8 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob } } + // -- Post Test Assertions -- + // Assert orders were filled correctly assert!( remainder_orders <= 2, diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index 3d21d34..585b035 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -4175,8 +4175,11 @@ fn test_cancelled_orders() { } OrderOperation::PlaceLimit(LimitOrder::new(0, 10, OrderDirection::Bid, sender.clone(), Uint128::from(100u128), Decimal256::zero(), None)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); - OrderOperation::RunMarket(MarketOrder::new(Uint128::from(100u128).checked_mul(Uint128::from(5u128)).unwrap(), OrderDirection::Ask, sender.clone())).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + OrderOperation::RunMarket(MarketOrder::new(Uint128::from(100u128).checked_mul(Uint128::from(4u128)).unwrap(), OrderDirection::Ask, sender.clone())).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); - - OrderOperation::Claim((0, 10)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + // Second last order should be claimable + OrderOperation::Claim((0, 9)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + // Last order should NOT be claimable + let err = OrderOperation::Claim((0, 10)).run(deps.as_mut(), env.clone(), info.clone()).unwrap_err(); + assert_eq!(err, ContractError::ZeroClaim); } \ No newline at end of file From 6915386da61016a6032da8200da593985aae711d Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 17 Jun 2024 14:40:28 +0100 Subject: [PATCH 15/98] chore: fixed post merge issues --- contracts/sumtree-orderbook/src/contract.rs | 3 --- contracts/sumtree-orderbook/src/msg.rs | 3 --- contracts/sumtree-orderbook/src/state.rs | 2 +- .../src/tests/e2e/cases/test_fuzz.rs | 23 +++++++++++-------- .../tests/e2e/cases/test_orders_success.rs | 2 +- .../src/tests/e2e/cases/utils.rs | 9 ++++---- .../src/tests/e2e/test_env.rs | 22 ++++++++++++++---- 7 files changed, 37 insertions(+), 27 deletions(-) diff --git a/contracts/sumtree-orderbook/src/contract.rs b/contracts/sumtree-orderbook/src/contract.rs index 99a3e8b..19ed13c 100644 --- a/contracts/sumtree-orderbook/src/contract.rs +++ b/contracts/sumtree-orderbook/src/contract.rs @@ -143,9 +143,6 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> ContractResult { } => Ok(to_json_binary(&query::orders_by_owner( deps, owner, start_from, end_at, limit, )?)?), - QueryMsg::Order { tick_id, order_id } => { - Ok(to_json_binary(&query::order(deps, tick_id, order_id)?)?) - } QueryMsg::OrderbookState {} => Ok(to_json_binary(&query::orderbook_state(deps)?)?), QueryMsg::TicksById { tick_ids } => { Ok(to_json_binary(&query::ticks_by_id(deps, tick_ids)?)?) diff --git a/contracts/sumtree-orderbook/src/msg.rs b/contracts/sumtree-orderbook/src/msg.rs index 08249af..9c4d6bd 100644 --- a/contracts/sumtree-orderbook/src/msg.rs +++ b/contracts/sumtree-orderbook/src/msg.rs @@ -121,9 +121,6 @@ pub enum QueryMsg { limit: Option, }, - #[returns(crate::types::LimitOrder)] - Order { tick_id: i64, order_id: u64 }, - #[returns(crate::types::Orderbook)] OrderbookState {}, diff --git a/contracts/sumtree-orderbook/src/state.rs b/contracts/sumtree-orderbook/src/state.rs index c894841..7cac036 100644 --- a/contracts/sumtree-orderbook/src/state.rs +++ b/contracts/sumtree-orderbook/src/state.rs @@ -83,7 +83,7 @@ pub fn get_orders_by_owner( page_size: Option, ) -> StdResult> { let page_size = page_size.unwrap_or(DEFAULT_PAGE_SIZE) as usize; - let min = min.map(Bound::exclusive); + let min = min.map(Bound::inclusive); let max = max.map(Bound::inclusive); // Define the prefix iterator based on the filter diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index af5eea8..bb8b23a 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -1,6 +1,6 @@ -use cosmwasm_std::{Coin, Decimal, Uint256}; +use cosmwasm_std::{Addr, Coin, Decimal, Uint256}; use cosmwasm_std::{Decimal256, Uint128}; -use osmosis_test_tube::{Module, OsmosisTestApp}; +use osmosis_test_tube::{Account, Module, OsmosisTestApp}; use rand::seq::SliceRandom; use rand::Rng; use rand::{rngs::StdRng, SeedableRng}; @@ -152,9 +152,11 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob ); let order: LimitOrder = t .contract - .query(&QueryMsg::Order { - order_id: *order_id, - tick_id: *tick_id, + .query(&QueryMsg::OrdersByOwner { + owner: Addr::unchecked(t.accounts[username].address()), + start_from: Some((*tick_id, *order_id)), + end_at: None, + limit: Some(1), }) .unwrap(); let sender = if order.claim_bounty.is_some() { @@ -166,11 +168,12 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob orders::claim(&t, sender, order.tick_id, order.order_id).unwrap(); // For the situation that the order has the 1 remainder we record this for assertions - let maybe_order = t.contract.query::(&QueryMsg::Order { - order_id: *order_id, - tick_id: *tick_id, - }); - if let Ok(order) = maybe_order { + let maybe_order = t.contract.get_order( + t.accounts[username].address(), + order.tick_id, + order.order_id, + ); + if let Some(order) = maybe_order { println!("order: {:?}", order); remainder_orders += 1; } diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs index e51f540..58ebb72 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs @@ -142,7 +142,7 @@ fn test_basic_order() { assert::spot_price(&t, expected_bid_tick, expected_ask_tick, case.name); // Claim limit - orders::claim_success(&t, case.claimer, 0, 0).unwrap(); + orders::claim_success(&t, case.claimer, "user1", 0, 0).unwrap(); match case.order_direction { OrderDirection::Ask => { assert::pool_liquidity(&t, case.placed_amount - case.filled_amount, 0u8, case.name); diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs index 66d5eea..7aa87ab 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs @@ -260,7 +260,7 @@ pub mod orders { use osmosis_test_tube::{Account, OsmosisTestApp, RunnerExecuteResult}; use crate::{ - msg::{AllTicksResponse, CalcOutAmtGivenInResponse, DenomsResponse, ExecuteMsg, QueryMsg}, + msg::{CalcOutAmtGivenInResponse, DenomsResponse, ExecuteMsg, QueryMsg, TicksResponse}, tests::e2e::{modules::cosmwasm_pool::CosmwasmPool, test_env::TestEnv}, tick_math::{amount_to_value, tick_to_price, RoundingDirection}, types::{LimitOrder, OrderDirection}, @@ -404,14 +404,15 @@ pub mod orders { pub fn claim_success( t: &TestEnv, sender: &str, + owner: &str, tick_id: i64, order_id: u64, ) -> RunnerExecuteResult { let order: LimitOrder = t .contract - .query(&QueryMsg::Order { order_id, tick_id }) + .get_order(t.accounts[owner].address(), tick_id, order_id) .unwrap(); - let AllTicksResponse { ticks } = t + let TicksResponse { ticks } = t .contract .query(&QueryMsg::AllTicks { start_from: Some(order.tick_id), @@ -499,7 +500,7 @@ pub mod orders { pub fn cancel_limit_success(t: &TestEnv, sender: &str, tick_id: i64, order_id: u64) { let order: LimitOrder = t .contract - .query(&QueryMsg::Order { order_id, tick_id }) + .get_order(t.accounts[sender].address(), tick_id, order_id) .unwrap(); let order_direction = order.order_direction; let quantity = order.quantity; diff --git a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs index c399188..ffd6bc8 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs @@ -3,14 +3,14 @@ use std::{collections::HashMap, path::PathBuf}; use crate::{ constants::{MAX_TICK, MIN_TICK}, msg::{ - AllTicksResponse, DenomsResponse, ExecuteMsg, GetTotalPoolLiquidityResponse, - InstantiateMsg, QueryMsg, TickIdAndState, + DenomsResponse, ExecuteMsg, GetTotalPoolLiquidityResponse, InstantiateMsg, QueryMsg, + TickIdAndState, TicksResponse, }, - types::OrderDirection, + types::{LimitOrder, OrderDirection}, ContractError, }; -use cosmwasm_std::{to_json_binary, Coin, Coins}; +use cosmwasm_std::{to_json_binary, Addr, Coin, Coins}; use osmosis_std::types::{ cosmos::bank::v1beta1::QueryAllBalancesRequest, cosmwasm::wasm::v1::MsgExecuteContractResponse, @@ -238,7 +238,7 @@ impl<'a> OrderbookContract<'a> { let mut ticks = vec![]; let mut min_tick = MIN_TICK; while min_tick <= MAX_TICK { - let tick: AllTicksResponse = self + let tick: TicksResponse = self .query(&QueryMsg::AllTicks { start_from: Some(min_tick), end_at: Some(MAX_TICK), @@ -272,6 +272,18 @@ impl<'a> OrderbookContract<'a> { liquidity.u128() } + + pub fn get_order(&self, sender: String, tick_id: i64, order_id: u64) -> Option { + let orders: Vec = self + .query(&QueryMsg::OrdersByOwner { + owner: Addr::unchecked(sender), + start_from: Some((tick_id, order_id)), + end_at: None, + limit: Some(1), + }) + .unwrap(); + orders.first().cloned() + } } pub fn _assert_contract_err(expected: ContractError, actual: RunnerError) { From a185746b60ed8f7093d68af3cb3300723e4c9930 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 17 Jun 2024 22:30:15 +0100 Subject: [PATCH 16/98] test: fixed failing tests --- .../src/tests/e2e/cases/test_fuzz.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index bb8b23a..907f92b 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Addr, Coin, Decimal, Uint256}; +use cosmwasm_std::{Coin, Decimal, Uint256}; use cosmwasm_std::{Decimal256, Uint128}; use osmosis_test_tube::{Account, Module, OsmosisTestApp}; use rand::seq::SliceRandom; @@ -10,7 +10,6 @@ use crate::constants::MIN_TICK; use crate::msg::{CalcOutAmtGivenInResponse, QueryMsg, SpotPriceResponse}; use crate::tests::e2e::modules::cosmwasm_pool::CosmwasmPool; use crate::tick_math::{amount_to_value, tick_to_price, RoundingDirection}; -use crate::types::LimitOrder; use crate::{ msg::{DenomsResponse, GetTotalPoolLiquidityResponse}, setup, @@ -54,6 +53,11 @@ fn test_order_fuzz_linear_small_cancelled_orders_large_range() { run_fuzz_linear(100, (LARGE_NEGATIVE_TICK, LARGE_POSITIVE_TICK), 0.8); } +#[test] +fn test_order_fuzz_linear_large_all_cancelled_orders_small_range() { + run_fuzz_linear(1000, (-10, 10), 1.0); +} + /// Runs a linear fuzz test with the following steps /// 1. Place x amount of random limit orders in given tick range and cancel with provided probability /// 2. For both directions fill the entire amount of liquidity available using market orders @@ -150,14 +154,9 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob Coin::new(1000000000000u128, "uosmo"), ], ); - let order: LimitOrder = t + let order = t .contract - .query(&QueryMsg::OrdersByOwner { - owner: Addr::unchecked(t.accounts[username].address()), - start_from: Some((*tick_id, *order_id)), - end_at: None, - limit: Some(1), - }) + .get_order(t.accounts[username].address(), *tick_id, *order_id) .unwrap(); let sender = if order.claim_bounty.is_some() { "claimant" From 27c5a737abd4d70149203f261bb58f492e06bebb Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 17 Jun 2024 22:34:28 +0100 Subject: [PATCH 17/98] test: fixed claim success to use new cancels query --- .../src/tests/e2e/cases/test_fuzz.rs | 2 +- .../src/tests/e2e/cases/utils.rs | 25 +++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index 907f92b..3c3e9db 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -164,7 +164,7 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob username }; - orders::claim(&t, sender, order.tick_id, order.order_id).unwrap(); + orders::claim_success(&t, sender, username, order.tick_id, order.order_id).unwrap(); // For the situation that the order has the 1 remainder we record this for assertions let maybe_order = t.contract.get_order( diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs index 7aa87ab..285c647 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs @@ -260,7 +260,10 @@ pub mod orders { use osmosis_test_tube::{Account, OsmosisTestApp, RunnerExecuteResult}; use crate::{ - msg::{CalcOutAmtGivenInResponse, DenomsResponse, ExecuteMsg, QueryMsg, TicksResponse}, + msg::{ + CalcOutAmtGivenInResponse, DenomsResponse, ExecuteMsg, QueryMsg, TickUnrealizedCancels, + TickUnrealizedCancelsByIdResponse, TicksResponse, + }, tests::e2e::{modules::cosmwasm_pool::CosmwasmPool, test_env::TestEnv}, tick_math::{amount_to_value, tick_to_price, RoundingDirection}, types::{LimitOrder, OrderDirection}, @@ -422,11 +425,29 @@ pub mod orders { .unwrap(); let tick = ticks.first().unwrap().tick_state.clone(); let tick_values: crate::types::TickValues = tick.get_values(order.order_direction); + let TickUnrealizedCancelsByIdResponse { ticks } = t + .contract + .query(&QueryMsg::TickUnrealizedCancelsById { + tick_ids: vec![tick_id], + }) + .unwrap(); + let TickUnrealizedCancels { + unrealized_cancels, .. + } = ticks.first().unwrap(); + let cancelled_amount = match order.order_direction { + OrderDirection::Bid => unrealized_cancels.bid_unrealized_cancels, + OrderDirection::Ask => unrealized_cancels.ask_unrealized_cancels, + } + .checked_sub(tick_values.cumulative_realized_cancels) + .unwrap(); + let expected_amount_u256 = tick_values .effective_total_amount_swapped - .checked_sub(order.etas) + .checked_add(cancelled_amount) .unwrap() .to_uint_floor() + .checked_sub(Uint256::from(order.quantity.u128())) + .unwrap() .min(Uint256::from(order.quantity.u128())); let expected_amount = Uint128::try_from(expected_amount_u256).unwrap(); let price = tick_to_price(order.tick_id).unwrap(); From 8b8baba9b07758eb118dbd5efc721df20f859a0c Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 17 Jun 2024 22:37:53 +0100 Subject: [PATCH 18/98] tests: fixed failing tests --- contracts/sumtree-orderbook/src/tests/test_query.rs | 4 ++-- contracts/sumtree-orderbook/src/tests/test_state.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/test_query.rs b/contracts/sumtree-orderbook/src/tests/test_query.rs index 8b9d48e..70f2c35 100644 --- a/contracts/sumtree-orderbook/src/tests/test_query.rs +++ b/contracts/sumtree-orderbook/src/tests/test_query.rs @@ -1273,7 +1273,7 @@ fn test_orders_by_owner() { None, )], owner: Addr::unchecked("sender"), - start_from: Some((0, 0)), + start_from: Some((0, 1)), end_at: None, limit: None, expected_error: None, @@ -1593,7 +1593,7 @@ fn test_ticks_by_id() { cumulative_total_value: decimal256_from_u128(2u8), effective_total_amount_swapped: decimal256_from_u128(2u8), cumulative_realized_cancels: Decimal256::one(), - last_tick_sync_etas: Decimal256::zero(), + last_tick_sync_etas: Decimal256::one(), }, bid_values: TickValues::default(), }], diff --git a/contracts/sumtree-orderbook/src/tests/test_state.rs b/contracts/sumtree-orderbook/src/tests/test_state.rs index ff4a71d..2c3d4cc 100644 --- a/contracts/sumtree-orderbook/src/tests/test_state.rs +++ b/contracts/sumtree-orderbook/src/tests/test_state.rs @@ -242,7 +242,7 @@ fn test_get_orders_by_owner_with_pagination() { start_after = Some( owner_orders .last() - .map(|order| (order.tick_id, order.order_id)) + .map(|order| (order.tick_id, order.order_id + 1)) .unwrap(), ); } From 7f844f7236f2b217e26864da19116e0b4ecc5a15 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Tue, 18 Jun 2024 09:41:30 +0100 Subject: [PATCH 19/98] tests: fixed expected claim amount expected output --- .../src/tests/e2e/cases/utils.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs index 285c647..0cb2614 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs @@ -150,7 +150,7 @@ pub mod assert { } } - pub fn with_balance_changes( + pub fn balance_changes( t: &TestEnv, changes: &[(&str, Vec)], action: impl FnOnce() -> RunnerExecuteResult, @@ -269,7 +269,7 @@ pub mod orders { types::{LimitOrder, OrderDirection}, }; - use super::assert::with_balance_changes; + use super::assert; pub fn place_limit( t: &TestEnv, @@ -376,7 +376,7 @@ pub mod orders { }) .unwrap(); - with_balance_changes( + assert::balance_changes( t, &[( &t.accounts[sender].address(), @@ -445,10 +445,11 @@ pub mod orders { .effective_total_amount_swapped .checked_add(cancelled_amount) .unwrap() - .to_uint_floor() - .checked_sub(Uint256::from(order.quantity.u128())) + .checked_sub(order.etas) .unwrap() + .to_uint_floor() .min(Uint256::from(order.quantity.u128())); + let expected_amount = Uint128::try_from(expected_amount_u256).unwrap(); let price = tick_to_price(order.tick_id).unwrap(); let mut expected_received_u256 = amount_to_value( @@ -484,7 +485,7 @@ pub mod orders { quote_denom }; - with_balance_changes( + assert::balance_changes( t, [ ( @@ -535,7 +536,7 @@ pub mod orders { base_denom }; - with_balance_changes( + assert::balance_changes( t, &[( &t.accounts[sender].address(), From 421c2f5d4bbb579f12f72d19696138a4a33af674 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Tue, 18 Jun 2024 17:36:36 +0100 Subject: [PATCH 20/98] feat: added count to orders by owner query --- contracts/sumtree-orderbook/Cargo.toml | 2 +- contracts/sumtree-orderbook/src/msg.rs | 10 +++- contracts/sumtree-orderbook/src/query.rs | 23 ++++++-- .../src/tests/e2e/cases/test_fuzz.rs | 3 +- .../tests/e2e/cases/test_orders_success.rs | 4 +- .../src/tests/e2e/cases/utils.rs | 44 ++++++++++++--- .../src/tests/e2e/test_env.rs | 54 +++++++++++++++++-- .../sumtree-orderbook/src/tests/test_query.rs | 14 ++++- 8 files changed, 131 insertions(+), 23 deletions(-) diff --git a/contracts/sumtree-orderbook/Cargo.toml b/contracts/sumtree-orderbook/Cargo.toml index 65c2aa9..587ea8f 100644 --- a/contracts/sumtree-orderbook/Cargo.toml +++ b/contracts/sumtree-orderbook/Cargo.toml @@ -61,5 +61,5 @@ prost = { version = "0.11.2", default-features = false, features = [ [dev-dependencies] cw-multi-test = "0.18.0" +osmosis-test-tube = { version = "25.0.0", features = ["wasm-sudo"] } rand = "0.8.4" -osmosis-test-tube = "25.0.0" diff --git a/contracts/sumtree-orderbook/src/msg.rs b/contracts/sumtree-orderbook/src/msg.rs index 9c4d6bd..39b371c 100644 --- a/contracts/sumtree-orderbook/src/msg.rs +++ b/contracts/sumtree-orderbook/src/msg.rs @@ -1,4 +1,4 @@ -use crate::types::{OrderDirection, TickState}; +use crate::types::{LimitOrder, OrderDirection, TickState}; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Coin, Decimal, Decimal256, Uint128, Uint256}; use osmosis_std::types::cosmos::base::v1beta1::Coin as ProtoCoin; @@ -109,7 +109,7 @@ pub enum QueryMsg { #[returns(bool)] IsActive {}, - #[returns(Vec)] + #[returns(OrdersResponse)] OrdersByOwner { // The address of the order maker owner: Addr, @@ -201,6 +201,12 @@ pub struct TickUnrealizedCancelsByIdResponse { pub ticks: Vec, } +#[cw_serde] +pub struct OrdersResponse { + pub orders: Vec, + pub count: u64, +} + #[cw_serde] pub enum SudoMsg { /// SwapExactAmountIn swaps an exact amount of tokens in for as many tokens out as possible. diff --git a/contracts/sumtree-orderbook/src/query.rs b/contracts/sumtree-orderbook/src/query.rs index 649e785..56e34cc 100644 --- a/contracts/sumtree-orderbook/src/query.rs +++ b/contracts/sumtree-orderbook/src/query.rs @@ -8,11 +8,14 @@ use crate::{ error::ContractResult, msg::{ CalcOutAmtGivenInResponse, DenomsResponse, GetSwapFeeResponse, - GetTotalPoolLiquidityResponse, SpotPriceResponse, TickIdAndState, TickUnrealizedCancels, - TickUnrealizedCancelsByIdResponse, TickUnrealizedCancelsState, TicksResponse, + GetTotalPoolLiquidityResponse, OrdersResponse, SpotPriceResponse, TickIdAndState, + TickUnrealizedCancels, TickUnrealizedCancelsByIdResponse, TickUnrealizedCancelsState, + TicksResponse, }, order, - state::{get_directional_liquidity, get_orders_by_owner, IS_ACTIVE, ORDERBOOK, TICK_STATE}, + state::{ + get_directional_liquidity, get_orders_by_owner, orders, IS_ACTIVE, ORDERBOOK, TICK_STATE, + }, sudo::ensure_swap_fee, sumtree::tree::get_root_node, tick_math::tick_to_price, @@ -198,7 +201,13 @@ pub(crate) fn orders_by_owner( start_from: Option<(i64, u64)>, end_at: Option<(i64, u64)>, limit: Option, -) -> ContractResult> { +) -> ContractResult { + let count = orders() + .idx + .owner + .prefix(owner.clone()) + .keys(deps.storage, None, None, Order::Ascending) + .count(); let orders = get_orders_by_owner( deps.storage, FilterOwnerOrders::all(owner), @@ -206,7 +215,11 @@ pub(crate) fn orders_by_owner( end_at, limit, )?; - Ok(orders) + + Ok(OrdersResponse { + count: count as u64, + orders, + }) } pub(crate) fn denoms(deps: Deps) -> ContractResult { diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index 3c3e9db..4f09b0d 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -70,7 +70,8 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob let app = OsmosisTestApp::new(); let cp = CosmwasmPool::new(&app); - let mut t = setup!(app, "quote", "base"); + let mut t = setup!(&app, "quote", "base", 1); + let mut orders = vec![]; // -- System Under Test -- diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs index 58ebb72..82354ef 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs @@ -80,7 +80,7 @@ fn test_basic_order() { for case in cases { let app = OsmosisTestApp::new(); let cp = CosmwasmPool::new(&app); - let t = setup!(&app, "quote", "base"); + let t = setup!(&app, "quote", "base", 0); let (expected_bid_tick, expected_ask_tick) = if case.order_direction == OrderDirection::Ask { (MIN_TICK, case.tick_id) @@ -161,7 +161,7 @@ fn test_basic_order() { fn test_cancelled_orders() { let app = OsmosisTestApp::new(); let cp = CosmwasmPool::new(&app); - let t = setup!(&app, "quote", "base"); + let t = setup!(&app, "quote", "base", 0); let amount_orders = 3; for i in 0..amount_orders { diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs index 0cb2614..01521eb 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs @@ -1,12 +1,13 @@ #[macro_export] macro_rules! setup { - ($($app:expr, $quote_denom:expr, $base_denom:expr),* ) => {{ + ($($app:expr, $quote_denom:expr, $base_denom:expr, $maker_fee:expr),* ) => {{ $($app.init_account(&[ cosmwasm_std::Coin::new(1, $quote_denom), cosmwasm_std::Coin::new(1, $base_denom), ]) .unwrap(); + use osmosis_test_tube::Account; let t = $crate::tests::e2e::test_env::TestEnvBuilder::new() .with_account( "user1", @@ -22,6 +23,14 @@ macro_rules! setup { cosmwasm_std::Coin::new(2_000, $base_denom), ], ) + .with_account( + "contract_admin", + vec![], + ) + .with_account( + "maker_fee_recipient", + vec![], + ) .with_instantiate_msg($crate::msg::InstantiateMsg { base_denom: $base_denom.to_string(), quote_denom: $quote_denom.to_string(), @@ -55,6 +64,10 @@ macro_rules! setup { assert!(is_active); + t.contract.set_admin($app, cosmwasm_std::Addr::unchecked(&t.accounts["contract_admin"].address())); + t.contract + .set_maker_fee(&t.accounts["contract_admin"], Decimal256::percent($maker_fee), &t.accounts["maker_fee_recipient"]); + t)* }}; } @@ -459,19 +472,34 @@ pub mod orders { RoundingDirection::Down, ) .unwrap(); + let immut_expected_received_u256 = expected_received_u256; + let mut bounty_amount_256 = Uint256::zero(); if let Some(bounty) = order.claim_bounty { if order.owner != t.accounts[sender].address() { - bounty_amount_256 = Decimal256::from_ratio(expected_received_u256, Uint256::one()) - .checked_mul(bounty) - .unwrap() - .to_uint_floor(); + bounty_amount_256 = + Decimal256::from_ratio(immut_expected_received_u256, Uint256::one()) + .checked_mul(bounty) + .unwrap() + .to_uint_floor(); expected_received_u256 = expected_received_u256 .checked_sub(bounty_amount_256) .unwrap(); } } + let maker_fee = t.contract.get_maker_fee(); + let maker_fee_amount_u256 = + Decimal256::from_ratio(immut_expected_received_u256, Uint256::one()) + .checked_mul(maker_fee) + .unwrap() + .to_uint_floor(); + let maker_fee_amount = Uint128::try_from(maker_fee_amount_u256).unwrap(); + + expected_received_u256 = expected_received_u256 + .checked_sub(maker_fee_amount_u256) + .unwrap(); + let bounty_amount = Uint128::try_from(bounty_amount_256).unwrap(); let expected_received = Uint128::try_from(expected_received_u256).unwrap(); @@ -494,7 +522,11 @@ pub mod orders { ), ( &t.accounts[sender].address(), - vec![Coin::new(bounty_amount.u128(), expected_denom)], + vec![Coin::new(bounty_amount.u128(), expected_denom.clone())], + ), + ( + &t.accounts["maker_fee_recipient"].address(), + vec![Coin::new(maker_fee_amount.u128(), expected_denom)], ), ] .iter() diff --git a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs index ffd6bc8..0ea2e2a 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs @@ -3,14 +3,14 @@ use std::{collections::HashMap, path::PathBuf}; use crate::{ constants::{MAX_TICK, MIN_TICK}, msg::{ - DenomsResponse, ExecuteMsg, GetTotalPoolLiquidityResponse, InstantiateMsg, QueryMsg, - TickIdAndState, TicksResponse, + AuthExecuteMsg, AuthQueryMsg, DenomsResponse, ExecuteMsg, GetTotalPoolLiquidityResponse, + InstantiateMsg, QueryMsg, SudoMsg, TickIdAndState, TicksResponse, }, types::{LimitOrder, OrderDirection}, ContractError, }; -use cosmwasm_std::{to_json_binary, Addr, Coin, Coins}; +use cosmwasm_std::{to_json_binary, Addr, Coin, Coins, Decimal256}; use osmosis_std::types::{ cosmos::bank::v1beta1::QueryAllBalancesRequest, cosmwasm::wasm::v1::MsgExecuteContractResponse, @@ -190,12 +190,14 @@ impl<'a> OrderbookContract<'a> { code_id: _, } = cp.contract_info_by_pool_id(&ContractInfoByPoolIdRequest { pool_id })?; - Ok(Self { + let contract = Self { app, code_id, pool_id, contract_addr: contract_address, - }) + }; + + Ok(contract) } pub fn execute( @@ -230,6 +232,48 @@ impl<'a> OrderbookContract<'a> { .unwrap() } + pub fn set_admin(&self, app: &OsmosisTestApp, admin: Addr) { + app.wasm_sudo( + &self.contract_addr, + SudoMsg::TransferAdmin { new_admin: admin }, + ) + .unwrap(); + let admin: Option = self.query(&QueryMsg::Auth(AuthQueryMsg::Admin {})).unwrap(); + println!("admin_set: {:?}", admin); + } + + pub fn set_maker_fee( + &self, + signer: &SigningAccount, + maker_fee: Decimal256, + recipient: &SigningAccount, + ) { + let res = self + .execute( + &ExecuteMsg::Auth(AuthExecuteMsg::SetMakerFee { fee: maker_fee }), + &[], + signer, + ) + .unwrap(); + println!("events: {:?}", res.events); + + let admin: Option = self.query(&QueryMsg::Auth(AuthQueryMsg::Admin {})).unwrap(); + println!("admin: {:?}, signer: {:?}", admin, signer.address()); + self.execute( + &ExecuteMsg::Auth(AuthExecuteMsg::SetMakerFeeRecipient { + recipient: Addr::unchecked(recipient.address()), + }), + &[], + signer, + ) + .unwrap(); + } + + pub fn get_maker_fee(&self) -> Decimal256 { + let maker_fee: Decimal256 = self.query(&QueryMsg::GetMakerFee {}).unwrap(); + maker_fee + } + pub fn get_denoms(&self) -> DenomsResponse { self.query(&QueryMsg::Denoms {}).unwrap() } diff --git a/contracts/sumtree-orderbook/src/tests/test_query.rs b/contracts/sumtree-orderbook/src/tests/test_query.rs index 70f2c35..9477e5b 100644 --- a/contracts/sumtree-orderbook/src/tests/test_query.rs +++ b/contracts/sumtree-orderbook/src/tests/test_query.rs @@ -1155,6 +1155,7 @@ struct OrdersByOwnerTestCase { name: &'static str, pre_operations: Vec, expected_output: Vec, + expected_count: u64, owner: Addr, start_from: Option<(i64, u64)>, end_at: Option<(i64, u64)>, @@ -1172,6 +1173,7 @@ fn test_orders_by_owner() { name: "no orders", pre_operations: vec![], expected_output: vec![], + expected_count: 0, owner: Addr::unchecked("sender"), start_from: None, end_at: None, @@ -1198,6 +1200,7 @@ fn test_orders_by_owner() { Decimal256::zero(), None, )], + expected_count: 1, owner: Addr::unchecked("sender"), start_from: None, end_at: None, @@ -1235,6 +1238,7 @@ fn test_orders_by_owner() { Decimal256::zero(), None, )], + expected_count: 2, owner: Addr::unchecked("sender"), start_from: None, end_at: None, @@ -1272,6 +1276,7 @@ fn test_orders_by_owner() { Decimal256::zero(), None, )], + expected_count: 2, owner: Addr::unchecked("sender"), start_from: Some((0, 1)), end_at: None, @@ -1309,6 +1314,7 @@ fn test_orders_by_owner() { Decimal256::zero(), None, )], + expected_count: 2, owner: Addr::unchecked("sender"), start_from: None, end_at: Some((0, 0)), @@ -1385,6 +1391,7 @@ fn test_orders_by_owner() { None, ), ], + expected_count: 3, owner: Addr::unchecked("sender"), start_from: None, end_at: None, @@ -1439,10 +1446,15 @@ fn test_orders_by_owner() { ); }); assert_eq!( - res, test.expected_output, + res.orders, test.expected_output, "{}: output did not match", test.name ); + assert_eq!( + res.count, test.expected_count, + "{}: count did not match", + test.name + ); } } From 545f4d90c0cfad9da1e5c67f10309a5da7d917a7 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Tue, 18 Jun 2024 18:21:55 +0100 Subject: [PATCH 21/98] feat: added orders by tick id query --- contracts/sumtree-orderbook/src/contract.rs | 8 ++++++ contracts/sumtree-orderbook/src/msg.rs | 8 ++++++ contracts/sumtree-orderbook/src/query.rs | 31 ++++++++++++++++++++- 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/contracts/sumtree-orderbook/src/contract.rs b/contracts/sumtree-orderbook/src/contract.rs index 19ed13c..9ca9201 100644 --- a/contracts/sumtree-orderbook/src/contract.rs +++ b/contracts/sumtree-orderbook/src/contract.rs @@ -152,6 +152,14 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> ContractResult { QueryMsg::TickUnrealizedCancelsById { tick_ids } => Ok(to_json_binary( &query::ticks_unrealized_cancels_by_id(deps, tick_ids)?, )?), + QueryMsg::OrdersByTick { + tick_id, + start_from, + end_at, + limit, + } => Ok(to_json_binary(&query::orders_by_tick( + deps, tick_id, start_from, end_at, limit, + )?)?), // -- Auth Queries -- QueryMsg::Auth(msg) => Ok(to_json_binary(&auth::query(deps, msg)?)?), diff --git a/contracts/sumtree-orderbook/src/msg.rs b/contracts/sumtree-orderbook/src/msg.rs index 39b371c..a83a4d5 100644 --- a/contracts/sumtree-orderbook/src/msg.rs +++ b/contracts/sumtree-orderbook/src/msg.rs @@ -121,6 +121,14 @@ pub enum QueryMsg { limit: Option, }, + #[returns(OrdersResponse)] + OrdersByTick { + tick_id: i64, + start_from: Option, + end_at: Option, + limit: Option, + }, + #[returns(crate::types::Orderbook)] OrderbookState {}, diff --git a/contracts/sumtree-orderbook/src/query.rs b/contracts/sumtree-orderbook/src/query.rs index 56e34cc..0cf6da9 100644 --- a/contracts/sumtree-orderbook/src/query.rs +++ b/contracts/sumtree-orderbook/src/query.rs @@ -19,7 +19,7 @@ use crate::{ sudo::ensure_swap_fee, sumtree::tree::get_root_node, tick_math::tick_to_price, - types::{FilterOwnerOrders, LimitOrder, MarketOrder, OrderDirection, Orderbook, TickState}, + types::{FilterOwnerOrders, MarketOrder, OrderDirection, Orderbook, TickState}, ContractError, }; @@ -222,6 +222,35 @@ pub(crate) fn orders_by_owner( }) } +pub(crate) fn orders_by_tick( + deps: Deps, + tick_id: i64, + start_from: Option, + end_at: Option, + limit: Option, +) -> ContractResult { + let count = orders() + .prefix(tick_id) + .keys(deps.storage, None, None, Order::Ascending) + .count(); + let orders = orders() + .prefix(tick_id) + .range( + deps.storage, + start_from.map(Bound::inclusive), + end_at.map(Bound::inclusive), + Order::Ascending, + ) + .take(limit.unwrap_or(count as u64) as usize) + .map(|res| res.unwrap().1) + .collect(); + + Ok(OrdersResponse { + count: count as u64, + orders, + }) +} + pub(crate) fn denoms(deps: Deps) -> ContractResult { let orderbook = ORDERBOOK.load(deps.storage)?; Ok(DenomsResponse { From 756c320bc282b63732fd2d55d4a95317e29dee7d Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Wed, 19 Jun 2024 16:23:36 +0100 Subject: [PATCH 22/98] refactor: commented unused test env methods --- .../src/tests/e2e/cases/utils.rs | 8 ++++---- .../src/tests/e2e/test_env.rs | 20 ++++++++----------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs index 01521eb..4a7c013 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs @@ -7,7 +7,7 @@ macro_rules! setup { ]) .unwrap(); - use osmosis_test_tube::Account; + // use osmosis_test_tube::Account; let t = $crate::tests::e2e::test_env::TestEnvBuilder::new() .with_account( "user1", @@ -64,9 +64,9 @@ macro_rules! setup { assert!(is_active); - t.contract.set_admin($app, cosmwasm_std::Addr::unchecked(&t.accounts["contract_admin"].address())); - t.contract - .set_maker_fee(&t.accounts["contract_admin"], Decimal256::percent($maker_fee), &t.accounts["maker_fee_recipient"]); + // t.contract.set_admin($app, cosmwasm_std::Addr::unchecked(&t.accounts["contract_admin"].address())); + // t.contract + // .set_maker_fee(&t.accounts["contract_admin"], Decimal256::percent($maker_fee), &t.accounts["maker_fee_recipient"]); t)* }}; diff --git a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs index 0ea2e2a..25462b7 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs @@ -4,7 +4,7 @@ use crate::{ constants::{MAX_TICK, MIN_TICK}, msg::{ AuthExecuteMsg, AuthQueryMsg, DenomsResponse, ExecuteMsg, GetTotalPoolLiquidityResponse, - InstantiateMsg, QueryMsg, SudoMsg, TickIdAndState, TicksResponse, + InstantiateMsg, OrdersResponse, QueryMsg, SudoMsg, TickIdAndState, TicksResponse, }, types::{LimitOrder, OrderDirection}, ContractError, @@ -248,17 +248,13 @@ impl<'a> OrderbookContract<'a> { maker_fee: Decimal256, recipient: &SigningAccount, ) { - let res = self - .execute( - &ExecuteMsg::Auth(AuthExecuteMsg::SetMakerFee { fee: maker_fee }), - &[], - signer, - ) - .unwrap(); - println!("events: {:?}", res.events); + self.execute( + &ExecuteMsg::Auth(AuthExecuteMsg::SetMakerFee { fee: maker_fee }), + &[], + signer, + ) + .unwrap(); - let admin: Option = self.query(&QueryMsg::Auth(AuthQueryMsg::Admin {})).unwrap(); - println!("admin: {:?}, signer: {:?}", admin, signer.address()); self.execute( &ExecuteMsg::Auth(AuthExecuteMsg::SetMakerFeeRecipient { recipient: Addr::unchecked(recipient.address()), @@ -318,7 +314,7 @@ impl<'a> OrderbookContract<'a> { } pub fn get_order(&self, sender: String, tick_id: i64, order_id: u64) -> Option { - let orders: Vec = self + let OrdersResponse { orders, .. } = self .query(&QueryMsg::OrdersByOwner { owner: Addr::unchecked(sender), start_from: Some((tick_id, order_id)), From 97365e348e8f424672b6bc9514f04086df5b18e3 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Wed, 19 Jun 2024 18:40:08 +0100 Subject: [PATCH 23/98] test: added mixed fuzz tests --- .../src/tests/e2e/cases/test_fuzz.rs | 284 +++++++++++++++++- .../tests/e2e/cases/test_orders_success.rs | 2 +- .../src/tests/e2e/cases/utils.rs | 140 ++++----- .../src/tests/e2e/test_env.rs | 50 ++- 4 files changed, 395 insertions(+), 81 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index 4f09b0d..b95eedb 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use cosmwasm_std::{Coin, Decimal, Uint256}; use cosmwasm_std::{Decimal256, Uint128}; use osmosis_test_tube::{Account, Module, OsmosisTestApp}; @@ -58,6 +60,21 @@ fn test_order_fuzz_linear_large_all_cancelled_orders_small_range() { run_fuzz_linear(1000, (-10, 10), 1.0); } +#[test] +fn test_order_fuzz_linear_single_tick() { + run_fuzz_linear(1000, (0, 0), 0.2); +} + +#[test] +fn test_order_fuzz_mixed() { + run_fuzz_mixed(2500, (-20, 20)); +} + +#[test] +fn test_order_fuzz_single_tick() { + run_fuzz_mixed(1000, (0, 0)); +} + /// Runs a linear fuzz test with the following steps /// 1. Place x amount of random limit orders in given tick range and cancel with provided probability /// 2. For both directions fill the entire amount of liquidity available using market orders @@ -81,13 +98,15 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob // Tick state is verified after every order is placed (and cancelled) for i in 0..amount_limit_orders { let username = format!("user{}", i); - let chosen_tick = place_random_order(&mut t, &mut rng, &username, tick_range); + let chosen_tick = place_random_limit(&mut t, &mut rng, &username, tick_range); let is_cancelled = rng.gen_bool(cancel_probability); + if is_cancelled { - orders::cancel_limit_success(&t, &username, chosen_tick, i); + orders::cancel_limit_success(&t, &username, chosen_tick, i).unwrap(); } else { orders.push((username, chosen_tick, i)); } + assert::tick_invariants(&t); } @@ -189,8 +208,238 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob ); } +#[derive(Debug, Eq, PartialEq, Hash)] +enum MixedFuzzOperation { + PlaceLimit, + PlaceMarket, + CancelLimit, + Claim, +} + +impl MixedFuzzOperation { + /// Chooses a random fuzz operation + fn random(rng: &mut StdRng) -> Self { + let index: u32 = rng.gen_range(0..=100); + + if index < 25 { + Self::PlaceLimit + } else if index < 50 { + Self::PlaceMarket + } else if index < 75 { + Self::CancelLimit + } else { + Self::Claim + } + } + + /// Attempts to run the chosen fuzz operation + /// + /// Returns true if the operation was successful, false otherwise + #[allow(clippy::too_many_arguments)] + fn run( + &self, + t: &mut TestEnv, + cp: &CosmwasmPool, + rng: &mut StdRng, + iteration: u64, + orders: &mut HashMap, + order_count: &mut u64, + tick_bounds: (i64, i64), + ) -> Result { + let username = format!("user{}", iteration); + match self { + MixedFuzzOperation::PlaceLimit => { + // Determine tick range by finding the minimum and maximum tick ids of the orderbook + // Ticks are bounded by the provided tick range + // The concept is that ticks may randomly shift up and down until they reach the desired bounds + let all_ticks = t.contract.collect_all_ticks(); + let tick_range = ( + all_ticks + .iter() + .min_by_key(|f| f.tick_id) + .map(|f| f.tick_id) + .unwrap_or(0) + - 1.min(tick_bounds.1).max(tick_bounds.0), + all_ticks + .iter() + .max_by_key(|f| f.tick_id) + .map(|f| f.tick_id) + .unwrap_or(0) + + 1.min(tick_bounds.1).max(tick_bounds.0), + ); + + // Place the limit order + let tick_id = place_random_limit(t, rng, &username, tick_range); + // Record the order for claims/cancels + orders.insert(*order_count, (username, tick_id)); + *order_count += 1; + Ok(true) + } + MixedFuzzOperation::PlaceMarket => { + // Determine the market direction + let maybe_market_direction = get_random_market_direction(t, rng); + // May error if the orderbook has 0 liquidity for both directions + if maybe_market_direction.is_err() { + return Ok(false); + } + let market_direction = maybe_market_direction.unwrap(); + // Determine the maximum amount of the opposite direction that can be bought + let max_amount = t + .contract + .get_directional_liquidity(market_direction.opposite()); + // If nothing can be bought then we skip this operation + if max_amount == 0 { + return Ok(false); + } + + // Place the order + place_random_market(cp, t, rng, &username, market_direction, max_amount); + Ok(true) + } + MixedFuzzOperation::CancelLimit => { + // If there are no active orders skip the operation + if orders.is_empty() { + return Ok(false); + } + + // Determine the order to be cancelled + let order_ids = orders.keys().collect::>(); + let order_idx = rng.gen_range(0..order_ids.len()); + let order_id = *order_ids[order_idx]; + let (username, tick_id) = orders.get(&order_id).unwrap().clone(); + + // We cannot cancel an order if it is partially filled + let order = t + .contract + .get_order(t.accounts[&username].address(), tick_id, order_id) + .unwrap(); + let maybe_amount_claimable = t.contract.get_order_claimable_amount(order.clone()); + + // Determine if the order can be cancelled + if maybe_amount_claimable.is_err() { + return Ok(false); + } + let amount_claimable = maybe_amount_claimable.unwrap(); + if amount_claimable > 0 { + return Ok(false); + } + + // Remove the order once we know it is cancellable + orders.remove(&order_id).unwrap(); + // Cancel the order + orders::cancel_limit_success(t, &username, tick_id, order_id).unwrap(); + Ok(true) + } + MixedFuzzOperation::Claim => { + // If there are no active orders skip the operation + if orders.is_empty() { + return Ok(false); + } + + // Determine the order to be claimed + let order_ids = orders.keys().collect::>(); + let order_idx = rng.gen_range(0..order_ids.len()); + let order_id = *order_ids[order_idx]; + let (username, tick_id) = orders.get(&order_id).unwrap().clone(); + + // We cannot claim an order if it has nothing to be claimed + let order = t + .contract + .get_order(t.accounts[&username].address(), tick_id, order_id) + .unwrap(); + let maybe_amount_claimable = t.contract.get_order_claimable_amount(order.clone()); + + // Determine if the order can be claimed + if maybe_amount_claimable.is_err() { + return Ok(false); + } + let amount_claimable = maybe_amount_claimable.unwrap(); + if amount_claimable == 0 { + return Ok(false); + } + + let claimant = if order.claim_bounty.is_some() { + t.add_account("claimant", vec![Coin::new(1000000000000u128, "uosmo")]); + "claimant" + } else { + username.as_str() + }; + + // Remove the order once we know its claimable + orders.remove(&order_id).unwrap(); + // Claim the order + orders::claim_success(t, claimant, &username, tick_id, order_id).unwrap(); + Ok(true) + } + } + } +} + +/// Runs a fuzz test that randomly chooses between 4 operations: +/// 1. Place a Limit +/// 2. Place a Market +/// 3. Cancel a Limit +/// 4. Claim a Limit +/// +/// These operations are chosen at random and if they are an invalid operation they are skipped and a new operation is chosen. +/// Orders are placing in a tick range determined by the current tick bounds with the intent that ticks spread over time randomly to the desired tick bounds. +/// Expected errors are handled by skipping the operation and randomly choosing a new operation. Any errors returned are expected to be because of an issue in the orderbook. +fn run_fuzz_mixed(amount_of_orders: u64, tick_bounds: (i64, i64)) { + // -- Test Setup -- + let seed: u64 = 123456789; + let mut rng = StdRng::seed_from_u64(seed); + + let app = OsmosisTestApp::new(); + let cp = CosmwasmPool::new(&app); + let mut t = setup!(&app, "quote", "base", 1); + let mut orders: HashMap = HashMap::new(); + let mut order_count = 0; + + let mut oper_count: HashMap = HashMap::new(); + oper_count.insert(MixedFuzzOperation::PlaceLimit, 0); + oper_count.insert(MixedFuzzOperation::PlaceMarket, 0); + oper_count.insert(MixedFuzzOperation::CancelLimit, 0); + oper_count.insert(MixedFuzzOperation::Claim, 0); + + // -- System Under Test -- + for i in 0..amount_of_orders { + // Chooses an operation at random + let mut operation = MixedFuzzOperation::random(&mut rng); + + // We add an escape clause in the case that the test ever gets caught in an infinite loop + let mut repeated_failures = 0; + + // Repeat randomising operations until a successful one is chosen + while !operation + .run( + &mut t, + &cp, + &mut rng, + i, + &mut orders, + &mut order_count, + tick_bounds, + ) + .unwrap() + { + operation = MixedFuzzOperation::random(&mut rng); + repeated_failures += 1; + if repeated_failures > 100 { + panic!("Caught in loop"); + } + } + oper_count.entry(operation).and_modify(|c| *c += 1); + + // -- Post operation assertions -- + assert::tick_invariants(&t); + } + + // Assert every operation ran at least once successfully + assert!(oper_count.values().all(|c| *c > 0)); +} + /// Places a random limit order in the provided tick range using the provided username -fn place_random_order( +fn place_random_limit( t: &mut TestEnv, rng: &mut StdRng, username: &str, @@ -330,3 +579,32 @@ fn place_random_market( orders::place_market_success(cp, t, order_direction, amount, username).unwrap(); amount } + +/// Determines a random market direction based on the available liquidity for bids and asks. +/// Errors if both directions have no liquidity +/// +/// Chooses a direction if that direction is the only one with liquidity. +fn get_random_market_direction<'a>( + t: &TestEnv, + rng: &mut StdRng, +) -> Result { + let bid_liquidity = t.contract.get_directional_liquidity(OrderDirection::Bid); + let ask_liquidity = t.contract.get_directional_liquidity(OrderDirection::Ask); + if bid_liquidity == 0 && ask_liquidity == 0 { + return Err("No liquidity available to place market order"); + } + + let bid_probability = if bid_liquidity != 0 && ask_liquidity == 0 { + 1.0 + } else if bid_liquidity != 0 { + 0.5 + } else { + 0.0 + }; + + if rng.gen_bool(bid_probability) { + Ok(OrderDirection::Bid) + } else { + Ok(OrderDirection::Ask) + } +} diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs index 82354ef..ab3362d 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs @@ -174,7 +174,7 @@ fn test_cancelled_orders() { "user1", ) .unwrap(); - orders::cancel_limit_success(&t, "user1", 0, i); + orders::cancel_limit_success(&t, "user1", 0, i).unwrap(); } assert::pool_liquidity(&t, 0u8, 0u8, "cancelled orders"); assert::pool_balance(&t, 0u8, 0u8, "cancelled orders"); diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs index 4a7c013..f55e5e5 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs @@ -178,42 +178,48 @@ pub mod assert { }) .collect(); let result = action(); - let post_balances: Vec<(String, Coins)> = changes - .iter() - .map(|(sender, _)| { - ( - sender.to_string(), - Coins::try_from(t.get_balance(sender)).unwrap(), - ) - }) - .collect(); - for (sender, balance_change) in changes.iter().cloned() { - let pre_balance = pre_balances - .iter() - .find(|(s, _)| s == sender) - .unwrap() - .1 - .clone(); - let post_balance = post_balances - .iter() - .find(|(s, _)| s == sender) - .unwrap() - .1 - .clone(); - for coin in balance_change { - let pre_amount = pre_balance.amount_of(&coin.denom); - let post_amount = post_balance.amount_of(&coin.denom); - let change = post_amount.saturating_sub(pre_amount); - assert_eq!( - change, coin.amount, - "Did not receive expected amount change, expected: {}{}, got: {}{}", - coin.amount, coin.denom, change, coin.denom - ); + match result { + Ok(res) => { + let post_balances: Vec<(String, Coins)> = changes + .iter() + .map(|(sender, _)| { + ( + sender.to_string(), + Coins::try_from(t.get_balance(sender)).unwrap(), + ) + }) + .collect(); + + for (sender, balance_change) in changes.iter().cloned() { + let pre_balance = pre_balances + .iter() + .find(|(s, _)| s == sender) + .unwrap() + .1 + .clone(); + let post_balance = post_balances + .iter() + .find(|(s, _)| s == sender) + .unwrap() + .1 + .clone(); + for coin in balance_change { + let pre_amount = pre_balance.amount_of(&coin.denom); + let post_amount = post_balance.amount_of(&coin.denom); + let change = post_amount.saturating_sub(pre_amount); + assert_eq!( + change, coin.amount, + "Did not receive expected amount change, expected: {}{}, got: {}{}", + coin.amount, coin.denom, change, coin.denom + ); + } + } + + Ok(res) } + Err(e) => Err(e), } - - result } pub fn tick_invariants(t: &TestEnv) { @@ -270,13 +276,10 @@ pub mod orders { MsgSwapExactAmountIn, MsgSwapExactAmountInResponse, SwapAmountInRoute, }, }; - use osmosis_test_tube::{Account, OsmosisTestApp, RunnerExecuteResult}; + use osmosis_test_tube::{Account, OsmosisTestApp, RunnerError, RunnerExecuteResult}; use crate::{ - msg::{ - CalcOutAmtGivenInResponse, DenomsResponse, ExecuteMsg, QueryMsg, TickUnrealizedCancels, - TickUnrealizedCancelsByIdResponse, TicksResponse, - }, + msg::{CalcOutAmtGivenInResponse, DenomsResponse, ExecuteMsg, QueryMsg}, tests::e2e::{modules::cosmwasm_pool::CosmwasmPool, test_env::TestEnv}, tick_math::{amount_to_value, tick_to_price, RoundingDirection}, types::{LimitOrder, OrderDirection}, @@ -428,46 +431,20 @@ pub mod orders { .contract .get_order(t.accounts[owner].address(), tick_id, order_id) .unwrap(); - let TicksResponse { ticks } = t + let expected_amount = t .contract - .query(&QueryMsg::AllTicks { - start_from: Some(order.tick_id), - end_at: None, - limit: Some(1), - }) - .unwrap(); - let tick = ticks.first().unwrap().tick_state.clone(); - let tick_values: crate::types::TickValues = tick.get_values(order.order_direction); - let TickUnrealizedCancelsByIdResponse { ticks } = t - .contract - .query(&QueryMsg::TickUnrealizedCancelsById { - tick_ids: vec![tick_id], - }) - .unwrap(); - let TickUnrealizedCancels { - unrealized_cancels, .. - } = ticks.first().unwrap(); - let cancelled_amount = match order.order_direction { - OrderDirection::Bid => unrealized_cancels.bid_unrealized_cancels, - OrderDirection::Ask => unrealized_cancels.ask_unrealized_cancels, - } - .checked_sub(tick_values.cumulative_realized_cancels) - .unwrap(); - - let expected_amount_u256 = tick_values - .effective_total_amount_swapped - .checked_add(cancelled_amount) - .unwrap() - .checked_sub(order.etas) - .unwrap() - .to_uint_floor() - .min(Uint256::from(order.quantity.u128())); + .get_order_claimable_amount(order.clone()) + .map_err(RunnerError::GenericError)?; - let expected_amount = Uint128::try_from(expected_amount_u256).unwrap(); + if expected_amount == 0 { + return Err(RunnerError::GenericError( + "Cannot claim order: nothing to claim".to_string(), + )); + } let price = tick_to_price(order.tick_id).unwrap(); let mut expected_received_u256 = amount_to_value( order.order_direction, - expected_amount, + Uint128::from(expected_amount), price, RoundingDirection::Down, ) @@ -551,11 +528,25 @@ pub mod orders { ) } - pub fn cancel_limit_success(t: &TestEnv, sender: &str, tick_id: i64, order_id: u64) { + pub fn cancel_limit_success( + t: &TestEnv, + sender: &str, + tick_id: i64, + order_id: u64, + ) -> RunnerExecuteResult { let order: LimitOrder = t .contract .get_order(t.accounts[sender].address(), tick_id, order_id) .unwrap(); + let claimable = t + .contract + .get_order_claimable_amount(order.clone()) + .map_err(RunnerError::GenericError)?; + if claimable > 0 { + return Err(RunnerError::GenericError( + "Cannot cancel order: Order is partially filled".to_string(), + )); + } let order_direction = order.order_direction; let quantity = order.quantity; let DenomsResponse { @@ -576,6 +567,5 @@ pub mod orders { )], || cancel_limit(t, sender, tick_id, order_id), ) - .unwrap(); } } diff --git a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs index 25462b7..8782b61 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs @@ -4,13 +4,14 @@ use crate::{ constants::{MAX_TICK, MIN_TICK}, msg::{ AuthExecuteMsg, AuthQueryMsg, DenomsResponse, ExecuteMsg, GetTotalPoolLiquidityResponse, - InstantiateMsg, OrdersResponse, QueryMsg, SudoMsg, TickIdAndState, TicksResponse, + InstantiateMsg, OrdersResponse, QueryMsg, SudoMsg, TickIdAndState, TickUnrealizedCancels, + TickUnrealizedCancelsByIdResponse, TicksResponse, }, types::{LimitOrder, OrderDirection}, ContractError, }; -use cosmwasm_std::{to_json_binary, Addr, Coin, Coins, Decimal256}; +use cosmwasm_std::{to_json_binary, Addr, Coin, Coins, Decimal256, Uint128, Uint256}; use osmosis_std::types::{ cosmos::bank::v1beta1::QueryAllBalancesRequest, cosmwasm::wasm::v1::MsgExecuteContractResponse, @@ -265,6 +266,51 @@ impl<'a> OrderbookContract<'a> { .unwrap(); } + pub fn get_order_claimable_amount(&self, order: LimitOrder) -> Result { + let TicksResponse { ticks } = self + .query(&QueryMsg::AllTicks { + start_from: Some(order.tick_id), + end_at: None, + limit: Some(1), + }) + .unwrap(); + let tick = ticks.first().unwrap().tick_state.clone(); + let tick_values: crate::types::TickValues = tick.get_values(order.order_direction); + let TickUnrealizedCancelsByIdResponse { ticks } = self + .query(&QueryMsg::TickUnrealizedCancelsById { + tick_ids: vec![order.tick_id], + }) + .unwrap(); + let TickUnrealizedCancels { + unrealized_cancels, .. + } = ticks.first().unwrap(); + let cancelled_amount = match order.order_direction { + OrderDirection::Bid => unrealized_cancels.bid_unrealized_cancels, + OrderDirection::Ask => unrealized_cancels.ask_unrealized_cancels, + } + .checked_sub(tick_values.cumulative_realized_cancels) + .unwrap(); + + let synced_etas = tick_values + .effective_total_amount_swapped + .checked_add(cancelled_amount) + .unwrap(); + + if synced_etas < order.etas { + return Ok(0u128); + } + + let expected_amount_u256 = synced_etas + .checked_sub(order.etas) + .unwrap() + .to_uint_floor() + .min(Uint256::from(order.quantity.u128())); + + let expected_amount = Uint128::try_from(expected_amount_u256).unwrap(); + + Ok(expected_amount.u128()) + } + pub fn get_maker_fee(&self) -> Decimal256 { let maker_fee: Decimal256 = self.query(&QueryMsg::GetMakerFee {}).unwrap(); maker_fee From 08843047f435ea406a3ebc27fd877e6bcd8dcb30 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Fri, 21 Jun 2024 13:14:19 +0100 Subject: [PATCH 24/98] feat: added mixed fuzz tests --- .../src/tests/e2e/cases/test_fuzz.rs | 80 +++++++++++++++---- .../src/tests/e2e/cases/utils.rs | 36 +++++++-- .../src/tests/e2e/test_env.rs | 13 +-- .../sumtree-orderbook/src/tests/test_utils.rs | 7 +- 4 files changed, 98 insertions(+), 38 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index b95eedb..c460933 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -72,7 +72,7 @@ fn test_order_fuzz_mixed() { #[test] fn test_order_fuzz_single_tick() { - run_fuzz_mixed(1000, (0, 0)); + run_fuzz_mixed(2000, (0, 0)) } /// Runs a linear fuzz test with the following steps @@ -143,6 +143,7 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob // Update the liquidity liquidity = t.contract.get_directional_liquidity(order_direction); assert::tick_invariants(&t); + assert::has_liquidity(&t); } println!( "Placed {} market orders in {} direction", @@ -246,6 +247,7 @@ impl MixedFuzzOperation { order_count: &mut u64, tick_bounds: (i64, i64), ) -> Result { + println!("operation: {self:?}"); let username = format!("user{}", iteration); match self { MixedFuzzOperation::PlaceLimit => { @@ -313,13 +315,9 @@ impl MixedFuzzOperation { .contract .get_order(t.accounts[&username].address(), tick_id, order_id) .unwrap(); - let maybe_amount_claimable = t.contract.get_order_claimable_amount(order.clone()); + let amount_claimable = t.contract.get_order_claimable_amount(order.clone()); // Determine if the order can be cancelled - if maybe_amount_claimable.is_err() { - return Ok(false); - } - let amount_claimable = maybe_amount_claimable.unwrap(); if amount_claimable > 0 { return Ok(false); } @@ -347,14 +345,23 @@ impl MixedFuzzOperation { .contract .get_order(t.accounts[&username].address(), tick_id, order_id) .unwrap(); - let maybe_amount_claimable = t.contract.get_order_claimable_amount(order.clone()); + let amount_claimable = t.contract.get_order_claimable_amount(order.clone()); // Determine if the order can be claimed - if maybe_amount_claimable.is_err() { + if amount_claimable == 0 { return Ok(false); } - let amount_claimable = maybe_amount_claimable.unwrap(); - if amount_claimable == 0 { + + let price = tick_to_price(order.tick_id).unwrap(); + let expected_received_u256 = amount_to_value( + order.order_direction, + Uint128::from(amount_claimable), + price, + RoundingDirection::Down, + ) + .unwrap(); + + if expected_received_u256.is_zero() { return Ok(false); } @@ -368,8 +375,12 @@ impl MixedFuzzOperation { // Remove the order once we know its claimable orders.remove(&order_id).unwrap(); // Claim the order - orders::claim_success(t, claimant, &username, tick_id, order_id).unwrap(); - Ok(true) + match orders::claim_success(t, claimant, &username, tick_id, order_id) { + Ok(_) => Ok(true), + Err(e) => { + panic!("{e}") + } + } } } } @@ -408,7 +419,7 @@ fn run_fuzz_mixed(amount_of_orders: u64, tick_bounds: (i64, i64)) { // We add an escape clause in the case that the test ever gets caught in an infinite loop let mut repeated_failures = 0; - + println!("iteration: {}", i); // Repeat randomising operations until a successful one is chosen while !operation .run( @@ -432,6 +443,32 @@ fn run_fuzz_mixed(amount_of_orders: u64, tick_bounds: (i64, i64)) { // -- Post operation assertions -- assert::tick_invariants(&t); + assert::has_liquidity(&t); + } + + for (order_id, (username, tick_id)) in orders.clone().iter() { + match orders::claim_success(&t, username, username, *tick_id, *order_id) { + Ok(_) => { + continue; + } + Err(e) => println!("Error claiming order: {e}"), + } + + match orders::cancel_limit_success(&t, username, *tick_id, *order_id) { + Ok(_) => { + continue; + } + Err(e) => println!("Error cancelling order: {e}"), + } + + let order = t + .contract + .get_order(username.clone(), *tick_id, *order_id) + .unwrap(); + assert!( + order.placed_quantity != order.quantity, + "order could not be claimed or cancelled: {order:?}" + ); } // Assert every operation ran at least once successfully @@ -446,7 +483,7 @@ fn place_random_limit( tick_range: (i64, i64), ) -> i64 { // Quantities are in magnitudes of u32 - let quantity = Uint128::from(rng.gen::()); + let quantity = Uint128::from(rng.gen::()); // 50% chance to choose either direction let order_direction = if rng.gen_bool(0.5) { OrderDirection::Bid @@ -561,11 +598,22 @@ fn place_random_market( swap_fee: Decimal::zero(), }); - // If the provided error cannot be filled then we return a 0 amount - if amount == 0 || expected_out.is_err() || expected_out.unwrap().token_out.amount == "0" { + if let Ok(expected_out) = expected_out { + println!("expected_out: {expected_out:?}"); + let balance = t.get_balance(&t.contract.contract_addr); + if expected_out.token_out.amount == "0" { + return 0; + } + println!("balance: {balance:?}"); + } else if expected_out.is_err() || amount == 0 { return 0; } + // // If the provided error cannot be filled then we return a 0 amount + // if amount == 0 || expected_out.is_err() || expected_out.unwrap().token_out.amount == "0" { + // return 0; + // } + // Generate the user account t.add_account( username, diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs index f55e5e5..7d31cbe 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs @@ -163,6 +163,26 @@ pub mod assert { } } + pub fn has_liquidity(t: &TestEnv) { + let bid_liquidity = t.contract.get_directional_liquidity(OrderDirection::Bid); + let ask_liquidity = t.contract.get_directional_liquidity(OrderDirection::Ask); + let balance = Coins::try_from(t.get_balance(&t.contract.contract_addr)).unwrap(); + let bid_balance = balance.amount_of(&t.contract.get_denoms().base_denom); + let ask_balance = balance.amount_of(&t.contract.get_denoms().quote_denom); + assert!( + bid_liquidity <= bid_balance.u128(), + "invalid bid liquidity, expected: {}, got: {}", + bid_balance, + bid_liquidity + ); + assert!( + ask_liquidity <= ask_balance.u128(), + "invalid ask liquidity, expected: {}, got: {}", + ask_balance, + ask_liquidity + ); + } + pub fn balance_changes( t: &TestEnv, changes: &[(&str, Vec)], @@ -431,10 +451,7 @@ pub mod orders { .contract .get_order(t.accounts[owner].address(), tick_id, order_id) .unwrap(); - let expected_amount = t - .contract - .get_order_claimable_amount(order.clone()) - .map_err(RunnerError::GenericError)?; + let expected_amount = t.contract.get_order_claimable_amount(order.clone()); if expected_amount == 0 { return Err(RunnerError::GenericError( @@ -451,6 +468,12 @@ pub mod orders { .unwrap(); let immut_expected_received_u256 = expected_received_u256; + if immut_expected_received_u256.is_zero() { + return Err(RunnerError::GenericError( + "Cannot claim order: nothing to claim".to_string(), + )); + } + let mut bounty_amount_256 = Uint256::zero(); if let Some(bounty) = order.claim_bounty { if order.owner != t.accounts[sender].address() { @@ -538,10 +561,7 @@ pub mod orders { .contract .get_order(t.accounts[sender].address(), tick_id, order_id) .unwrap(); - let claimable = t - .contract - .get_order_claimable_amount(order.clone()) - .map_err(RunnerError::GenericError)?; + let claimable = t.contract.get_order_claimable_amount(order.clone()); if claimable > 0 { return Err(RunnerError::GenericError( "Cannot cancel order: Order is partially filled".to_string(), diff --git a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs index 8782b61..c6e3ff9 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs @@ -266,7 +266,7 @@ impl<'a> OrderbookContract<'a> { .unwrap(); } - pub fn get_order_claimable_amount(&self, order: LimitOrder) -> Result { + pub fn get_order_claimable_amount(&self, order: LimitOrder) -> u128 { let TicksResponse { ticks } = self .query(&QueryMsg::AllTicks { start_from: Some(order.tick_id), @@ -295,20 +295,13 @@ impl<'a> OrderbookContract<'a> { .effective_total_amount_swapped .checked_add(cancelled_amount) .unwrap(); - - if synced_etas < order.etas { - return Ok(0u128); - } - let expected_amount_u256 = synced_etas - .checked_sub(order.etas) - .unwrap() + .saturating_sub(order.etas) .to_uint_floor() .min(Uint256::from(order.quantity.u128())); let expected_amount = Uint128::try_from(expected_amount_u256).unwrap(); - - Ok(expected_amount.u128()) + expected_amount.u128() } pub fn get_maker_fee(&self) -> Decimal256 { diff --git a/contracts/sumtree-orderbook/src/tests/test_utils.rs b/contracts/sumtree-orderbook/src/tests/test_utils.rs index 5618177..b2c3209 100644 --- a/contracts/sumtree-orderbook/src/tests/test_utils.rs +++ b/contracts/sumtree-orderbook/src/tests/test_utils.rs @@ -33,8 +33,7 @@ impl OrderOperation { OrderDirection::Bid => MAX_TICK, OrderDirection::Ask => MIN_TICK, }; - run_market_order(deps.storage, env.contract.address, &mut order, tick_bound) - .unwrap(); + run_market_order(deps.storage, env.contract.address, &mut order, tick_bound)?; Ok(()) } OrderOperation::PlaceLimitMulti(( @@ -49,7 +48,7 @@ impl OrderOperation { quantity_per_order, direction, ); - place_multiple_limit_orders(&mut deps, env, info.sender.as_str(), orders).unwrap(); + place_multiple_limit_orders(&mut deps, env, info.sender.as_str(), orders)?; Ok(()) } OrderOperation::PlaceLimit(limit_order) => { @@ -83,7 +82,7 @@ impl OrderOperation { .load(deps.as_ref().storage, &(tick_id, order_id)) .unwrap(); let info = mock_info(order.owner.as_str(), &[]); - cancel_limit(deps, env, info, tick_id, order_id).unwrap(); + cancel_limit(deps, env, info, tick_id, order_id)?; Ok(()) } } From 50761af733c51ac4f715d7bbe4ab341045a50577 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Fri, 21 Jun 2024 13:14:57 +0100 Subject: [PATCH 25/98] feat: added syncing to cancelled limits and removed unused quantity restriction in place limit --- contracts/sumtree-orderbook/src/order.rs | 41 ++++++++++++------------ 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/contracts/sumtree-orderbook/src/order.rs b/contracts/sumtree-orderbook/src/order.rs index 28e315b..0db35f7 100644 --- a/contracts/sumtree-orderbook/src/order.rs +++ b/contracts/sumtree-orderbook/src/order.rs @@ -117,16 +117,13 @@ pub fn place_limit( ); let quant_dec256 = Decimal256::from_ratio(limit_order.quantity.u128(), Uint256::one()); - // Only save the order if not fully filled - if limit_order.quantity > Uint128::zero() { - // Save the order to the orderbook - orders().save(deps.storage, &(tick_id, order_id), &limit_order)?; + // Save the order to the orderbook + orders().save(deps.storage, &(tick_id, order_id), &limit_order)?; - tick_values.total_amount_of_liquidity = tick_values - .total_amount_of_liquidity - .checked_add(quant_dec256) - .unwrap(); - } + tick_values.total_amount_of_liquidity = tick_values + .total_amount_of_liquidity + .checked_add(quant_dec256) + .unwrap(); tick_values.cumulative_total_value = tick_values .cumulative_total_value @@ -168,6 +165,19 @@ pub fn cancel_limit( ensure_eq!(info.sender, order.owner, ContractError::Unauthorized {}); // Ensure the order has not been filled. + let tick_state = TICK_STATE.load(deps.storage, tick_id).unwrap_or_default(); + + sync_tick( + deps.storage, + tick_id, + tick_state + .get_values(OrderDirection::Bid) + .effective_total_amount_swapped, + tick_state + .get_values(OrderDirection::Ask) + .effective_total_amount_swapped, + )?; + let tick_state = TICK_STATE.load(deps.storage, tick_id).unwrap_or_default(); let tick_values = tick_state.get_values(order.order_direction); ensure!( @@ -214,27 +224,16 @@ pub fn cancel_limit( ); orders().remove(deps.storage, &(order.tick_id, order.order_id))?; - curr_tick_values.total_amount_of_liquidity = curr_tick_values .total_amount_of_liquidity .checked_sub(Decimal256::from_ratio(order.quantity, Uint256::one()))?; + curr_tick_state.set_values(order.order_direction, curr_tick_values); TICK_STATE.save(deps.storage, order.tick_id, &curr_tick_state)?; subtract_directional_liquidity(deps.storage, order.order_direction, quant_dec256)?; tree.save(deps.storage)?; - sync_tick( - deps.storage, - tick_id, - curr_tick_state - .get_values(OrderDirection::Bid) - .effective_total_amount_swapped, - curr_tick_state - .get_values(OrderDirection::Ask) - .effective_total_amount_swapped, - )?; - Ok(Response::new() .add_attributes(vec![ ("method", "cancelLimit"), From 721b82f458e5b392ea258b32d01a1fe603ead668 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Fri, 21 Jun 2024 13:17:32 +0100 Subject: [PATCH 26/98] refactor: improved error handling for sync_tick --- contracts/sumtree-orderbook/src/error.rs | 3 +++ contracts/sumtree-orderbook/src/tick.rs | 10 ++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/contracts/sumtree-orderbook/src/error.rs b/contracts/sumtree-orderbook/src/error.rs index 2feaeb1..25d52bd 100644 --- a/contracts/sumtree-orderbook/src/error.rs +++ b/contracts/sumtree-orderbook/src/error.rs @@ -88,6 +88,9 @@ pub enum ContractError { #[error("Invalid tick state: syncing tick pushed ETAS past CTT")] InvalidTickSync, + #[error("Invalid prefix sum: {error:?}")] + InvalidPrefixSum { error: Option }, + #[error("Zero Claim: Nothing to be claimed yet")] ZeroClaim, diff --git a/contracts/sumtree-orderbook/src/tick.rs b/contracts/sumtree-orderbook/src/tick.rs index d980725..3b9fcec 100644 --- a/contracts/sumtree-orderbook/src/tick.rs +++ b/contracts/sumtree-orderbook/src/tick.rs @@ -60,8 +60,14 @@ pub fn sync_tick( // Calculate the growth in realized cancels since previous sync. // This is equivalent to the amount we will need to add to the tick's ETAS. - let realized_since_last_sync = - new_cumulative_realized_cancels.checked_sub(old_cumulative_realized_cancels)?; + let realized_since_last_sync = new_cumulative_realized_cancels + .checked_sub(old_cumulative_realized_cancels) + .map_err(|_| ContractError::InvalidPrefixSum { + error: Some(format!( + "New prefix sum less than previous, previous: {}, new: {}", + old_cumulative_realized_cancels, new_cumulative_realized_cancels + )), + })?; // Update the tick state to represent new ETAS and new cumulative realized cancels. tick_value.effective_total_amount_swapped = tick_value From 4518498e0b652b04847bf1c97451b53e00e25bbe Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 24 Jun 2024 14:14:27 +0100 Subject: [PATCH 27/98] fix: potential fix for prefix sum algorithm [WIP] --- contracts/sumtree-orderbook/src/query.rs | 55 +++++-- .../src/sumtree/test/test_fuzz.rs | 4 +- .../src/sumtree/test/test_node.rs | 2 +- .../src/sumtree/test/test_tree.rs | 105 ++++++++++++- .../sumtree-orderbook/src/sumtree/tree.rs | 25 +++- .../src/tests/e2e/cases/test_fuzz.rs | 5 +- .../sumtree-orderbook/src/tests/test_order.rs | 139 +++++++++++++++++- contracts/sumtree-orderbook/src/tick.rs | 3 +- 8 files changed, 310 insertions(+), 28 deletions(-) diff --git a/contracts/sumtree-orderbook/src/query.rs b/contracts/sumtree-orderbook/src/query.rs index 0cf6da9..311135d 100644 --- a/contracts/sumtree-orderbook/src/query.rs +++ b/contracts/sumtree-orderbook/src/query.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -use cosmwasm_std::{coin, ensure, Addr, Coin, Decimal, Deps, Order, Uint128}; +use cosmwasm_std::{coin, ensure, Addr, Coin, Decimal, Decimal256, Deps, Order, Uint128}; use cw_storage_plus::Bound; use crate::{ @@ -17,7 +17,10 @@ use crate::{ get_directional_liquidity, get_orders_by_owner, orders, IS_ACTIVE, ORDERBOOK, TICK_STATE, }, sudo::ensure_swap_fee, - sumtree::tree::get_root_node, + sumtree::{ + node::{NodeType, TreeNode}, + tree::{get_prefix_sum, get_root_node}, + }, tick_math::tick_to_price, types::{FilterOwnerOrders, MarketOrder, OrderDirection, Orderbook, TickState}, ContractError, @@ -289,15 +292,47 @@ fn get_unrealized_cancels( tick_state: TickState, tick_id: i64, ) -> ContractResult { - let bid_unrealized_cancels = get_root_node(deps.storage, tick_id, OrderDirection::Bid).map_or( - tick_state.bid_values.cumulative_realized_cancels, - |ask_root| ask_root.get_value(), - ); + let bid_unrealized_cancels = if tick_state.bid_values.effective_total_amount_swapped + == tick_state.bid_values.last_tick_sync_etas + { + tick_state.bid_values.cumulative_realized_cancels + } else { + let bid_root_node = + get_root_node(deps.storage, tick_id, OrderDirection::Bid).unwrap_or(TreeNode::new( + tick_id, + OrderDirection::Bid, + 0, + NodeType::internal(Decimal256::zero(), (Decimal256::zero(), Decimal256::zero())), + )); + + get_prefix_sum( + deps.storage, + bid_root_node, + tick_state.bid_values.effective_total_amount_swapped, + tick_state.bid_values.cumulative_realized_cancels, + )? + }; - let ask_unrealized_cancels = get_root_node(deps.storage, tick_id, OrderDirection::Ask).map_or( - tick_state.ask_values.cumulative_realized_cancels, - |ask_root| ask_root.get_value(), - ); + let ask_unrealized_cancels = if tick_state.ask_values.effective_total_amount_swapped + == tick_state.ask_values.last_tick_sync_etas + { + tick_state.ask_values.cumulative_realized_cancels + } else { + let ask_root_node = + get_root_node(deps.storage, tick_id, OrderDirection::Ask).unwrap_or(TreeNode::new( + tick_id, + OrderDirection::Ask, + 0, + NodeType::internal(Decimal256::zero(), (Decimal256::zero(), Decimal256::zero())), + )); + + get_prefix_sum( + deps.storage, + ask_root_node, + tick_state.ask_values.effective_total_amount_swapped, + tick_state.ask_values.cumulative_realized_cancels, + )? + }; Ok(TickUnrealizedCancelsState { ask_unrealized_cancels, diff --git a/contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs b/contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs index 9c36903..ea5e0e4 100644 --- a/contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs @@ -74,7 +74,9 @@ fn test_fuzz_insert() { assert_sumtree_invariants(deps.as_ref(), &tree, &test_name); // Assert prefix sum correctness - let prefix_sum = get_prefix_sum(deps.as_ref().storage, tree, target_etas).unwrap(); + let prefix_sum = + get_prefix_sum(deps.as_ref().storage, tree, target_etas, Decimal256::zero()) + .unwrap(); assert_eq!( expected_prefix_sum, prefix_sum, "{test_name}: Expected prefix sum {expected_prefix_sum}, got {prefix_sum}. Target ETAS: {target_etas}", diff --git a/contracts/sumtree-orderbook/src/sumtree/test/test_node.rs b/contracts/sumtree-orderbook/src/sumtree/test/test_node.rs index c1c0912..70d6197 100644 --- a/contracts/sumtree-orderbook/src/sumtree/test/test_node.rs +++ b/contracts/sumtree-orderbook/src/sumtree/test/test_node.rs @@ -2751,7 +2751,7 @@ fn test_node_insert_large_quantity() { // Ensure prefix sum functions correctly let root_node = get_root_node(deps.as_mut().storage, tick_id, direction).unwrap(); - let prefix_sum = get_prefix_sum(deps.as_mut().storage, root_node, target_etas).unwrap(); + let prefix_sum = get_prefix_sum(deps.as_mut().storage, root_node, target_etas, Decimal256::zero()).unwrap(); assert_eq!(expected_prefix_sum, prefix_sum); } diff --git a/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs b/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs index 206f26a..9700f2d 100644 --- a/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs +++ b/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs @@ -1,5 +1,5 @@ use crate::sumtree::node::{generate_node_id, NodeType, TreeNode, NODES}; -use crate::sumtree::test::test_node::assert_internal_values; +use crate::sumtree::test::test_node::{assert_internal_values, print_tree}; use crate::sumtree::tree::{get_or_init_root_node, get_prefix_sum, get_root_node, TREE}; use crate::types::OrderDirection; use cosmwasm_std::Storage; @@ -175,7 +175,13 @@ fn test_get_prefix_sum_valid() { assert_internal_values(test.name, deps.as_ref(), internals, true); // System under test: get prefix sum - let prefix_sum = get_prefix_sum(deps.as_ref().storage, tree, test.target_etas).unwrap(); + let prefix_sum = get_prefix_sum( + deps.as_ref().storage, + tree, + test.target_etas, + Decimal256::zero(), + ) + .unwrap(); // Assert that the correct value was returned assert_eq!( @@ -195,6 +201,101 @@ fn test_get_prefix_sum_valid() { } } +struct TestInvariantPrefixCase { + name: &'static str, + nodes: Vec, + new_nodes: Vec, + target_etas: Decimal256, + expected_sum: Decimal256, +} + +#[test] +fn test_fuzz_invariant_prefix() { + let test_cases = vec![ + // TestInvariantPrefixCase { + // name: "Single node, target ETAS equal to node ETAS", + // nodes: vec![NodeType::leaf_uint256(10u128, 5u128)], + // new_nodes: vec![NodeType::leaf_uint256(20u128, 5u128)], + // target_etas: Decimal256::from_ratio(10u128, 1u128), + // expected_sum: Decimal256::from_ratio(5u128, 1u128), + // }, + TestInvariantPrefixCase { + name: "Single node, target ETAS equal to node ETAS", + nodes: vec![ + NodeType::leaf_uint256(4u128, 8u128), + NodeType::leaf_uint256(50u128, 5u128), + NodeType::leaf_uint256(152u128, 4u128), + ], + new_nodes: vec![NodeType::leaf_uint256(169u128, 2u128)], + target_etas: Decimal256::from_ratio(141u128, 1u128), + expected_sum: Decimal256::from_ratio(13u128, 1u128), + }, + TestInvariantPrefixCase { + name: "Multiple nodes, etas overlaps with highest node", + nodes: vec![ + NodeType::leaf_uint256(4u128, 8u128), + NodeType::leaf_uint256(50u128, 5u128), + NodeType::leaf_uint256(152u128, 4u128), + ], + new_nodes: vec![NodeType::leaf_uint256(156u128, 2u128)], + target_etas: Decimal256::from_ratio(152u128, 1u128), + expected_sum: Decimal256::from_ratio(17u128, 1u128), + }, + ]; + + let tick_id = 0; + let direction = OrderDirection::Ask; + + for test in test_cases { + let mut deps = mock_dependencies(); + + let mut tree = get_or_init_root_node(deps.as_mut().storage, tick_id, direction).unwrap(); + + for node in test.nodes { + tree = insert_and_refetch(deps.as_mut().storage, tick_id, direction, &node); + } + + let prefix_sum = get_prefix_sum( + deps.as_ref().storage, + tree.clone(), + test.target_etas, + Decimal256::zero(), + ) + .unwrap(); + print_tree("", test.name, &tree, &deps.as_ref()); + assert_eq!( + test.expected_sum, prefix_sum, + "{}: Expected prefix sum {}, got {}", + test.name, test.expected_sum, prefix_sum + ); + + let mut starting_etas = tree.get_max_range().checked_add(Decimal256::one()).unwrap(); + for node in test.new_nodes.iter() { + tree = insert_and_refetch(deps.as_mut().storage, tick_id, direction, node); + starting_etas = starting_etas + .checked_add(Decimal256::from_ratio(10u128, 1u128)) + .unwrap(); + println!("inserting {}", node); + print_tree("", test.name, &tree, &deps.as_ref()); + + let prefix_sum = get_prefix_sum( + deps.as_ref().storage, + tree.clone(), + test.target_etas, + Decimal256::zero(), + ) + .unwrap(); + assert!( + test.expected_sum <= prefix_sum, + "{}: Expected prefix sum {}, got {}", + test.name, + test.expected_sum, + prefix_sum + ); + } + } +} + // Inserts node into tree at ( tick_id, direction) and return the updated root pub fn insert_and_refetch( storage: &mut dyn Storage, diff --git a/contracts/sumtree-orderbook/src/sumtree/tree.rs b/contracts/sumtree-orderbook/src/sumtree/tree.rs index 28dd4fe..d890bd3 100644 --- a/contracts/sumtree-orderbook/src/sumtree/tree.rs +++ b/contracts/sumtree-orderbook/src/sumtree/tree.rs @@ -46,13 +46,14 @@ pub fn get_prefix_sum( storage: &dyn Storage, root_node: TreeNode, target_etas: Decimal256, + prev_sum: Decimal256, ) -> ContractResult { // We start from the root node's sum, which includes everything in the tree. // The prefix sum algorithm will chip away at this until we have the correct // prefux sum in O(log(N)) time. let starting_sum = TreeNode::get_value(&root_node); - prefix_sum_walk(storage, &root_node, starting_sum, target_etas) + prefix_sum_walk(storage, &root_node, starting_sum, target_etas, prev_sum) } // prefix_sum_walk is a recursive function that walks the sumtree to calculate the prefix sum below the given @@ -69,12 +70,13 @@ fn prefix_sum_walk( node: &TreeNode, mut current_sum: Decimal256, target_etas: Decimal256, + prev_sum: Decimal256, ) -> ContractResult { // Sanity check: target ETAS should be inside node's range. if target_etas < node.get_min_range() { // If the target ETAS is below the root node's range, we can return zero early. return Ok(Decimal256::zero()); - } else if target_etas >= node.get_max_range().checked_sub(node.get_value())? { + } else if target_etas >= node.get_max_range() { return Ok(current_sum); } @@ -95,6 +97,20 @@ fn prefix_sum_walk( let left_child = node.get_left(storage)?; let right_child = node.get_right(storage)?; + if left_child.is_some() && right_child.is_some() { + let left_child = left_child.clone().unwrap(); + let right_child = right_child.clone().unwrap(); + + let sum_at_node = current_sum.checked_sub(node.get_value())?; + let diff_at_node = prev_sum.saturating_sub(sum_at_node); + let unrealized_from_left = left_child.get_value().saturating_sub(diff_at_node); + let new_etas = target_etas.checked_add(unrealized_from_left)?; + + if new_etas >= right_child.get_min_range() { + return prefix_sum_walk(storage, &right_child, current_sum, new_etas, prev_sum); + } + } + // If the left child exists, we run the following logic: // * If target ETAS < left child's lower bound, exit early with zero // * Else if target ETAS <= upper bound, subtract right child sum from prefix sum and walk left @@ -118,7 +134,8 @@ fn prefix_sum_walk( current_sum = current_sum.checked_sub(right_sum)?; // Walk left recursively - current_sum = prefix_sum_walk(storage, &left_child, current_sum, target_etas)?; + current_sum = + prefix_sum_walk(storage, &left_child, current_sum, target_etas, prev_sum)?; return Ok(current_sum); } @@ -149,7 +166,7 @@ fn prefix_sum_walk( // to subtract from it yet. The right walk handles this update. // Walk right recursively - current_sum = prefix_sum_walk(storage, &right_child, current_sum, target_etas)?; + current_sum = prefix_sum_walk(storage, &right_child, current_sum, target_etas, prev_sum)?; Ok(current_sum) } else { diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index c460933..8c86508 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -72,7 +72,7 @@ fn test_order_fuzz_mixed() { #[test] fn test_order_fuzz_single_tick() { - run_fuzz_mixed(2000, (0, 0)) + run_fuzz_mixed(1000, (0, 0)); } /// Runs a linear fuzz test with the following steps @@ -599,12 +599,9 @@ fn place_random_market( }); if let Ok(expected_out) = expected_out { - println!("expected_out: {expected_out:?}"); - let balance = t.get_balance(&t.contract.contract_addr); if expected_out.token_out.amount == "0" { return 0; } - println!("balance: {balance:?}"); } else if expected_out.is_err() || amount == 0 { return 0; } diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index 585b035..7126ece 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -1,8 +1,8 @@ -use std::str::FromStr; +use std::{collections::HashMap, str::FromStr}; use crate::{ constants::{MAX_TICK, MIN_TICK}, error::ContractError, order::*, orderbook::*, state::*, sumtree::{ - node::{NodeType, TreeNode}, tree::get_root_node + node::{NodeType, TreeNode}, test::{test_fuzz::assert_sumtree_invariants, test_node::print_tree}, tree::{get_or_init_root_node, get_prefix_sum, get_root_node} }, tests::{mock_querier::mock_dependencies_custom, test_utils::{decimal256_from_u128, place_multiple_limit_orders}}, types::{ @@ -17,6 +17,7 @@ use cosmwasm_std::{ Decimal256, }; use cw_utils::PaymentError; +use rand::{rngs::StdRng, Rng, SeedableRng}; use super::{test_constants::{DEFAULT_OWNER, DEFAULT_SENDER, BASE_DENOM, QUOTE_DENOM, LARGE_POSITIVE_TICK, LARGE_NEGATIVE_TICK}, test_utils::{ format_test_name, generate_limit_orders, OrderOperation, @@ -4167,19 +4168,147 @@ fn test_cancelled_orders() { create_orderbook(deps.as_mut(), QUOTE_DENOM.to_string(), BASE_DENOM.to_string()).unwrap(); for i in 0..10 { - OrderOperation::PlaceLimit(LimitOrder::new(0, i, OrderDirection::Bid, sender.clone(), Uint128::from(100u128), Decimal256::zero(), None)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + OrderOperation::PlaceLimit(LimitOrder::new(0, i, OrderDirection::Bid, sender.clone(), Uint128::from(1u128), Decimal256::zero(), None)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); if i % 3 != 0 { OrderOperation::Cancel((0, i)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); } } - OrderOperation::PlaceLimit(LimitOrder::new(0, 10, OrderDirection::Bid, sender.clone(), Uint128::from(100u128), Decimal256::zero(), None)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); - OrderOperation::RunMarket(MarketOrder::new(Uint128::from(100u128).checked_mul(Uint128::from(4u128)).unwrap(), OrderDirection::Ask, sender.clone())).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + let root = get_or_init_root_node(deps.as_mut().storage, 0, OrderDirection::Bid).unwrap(); + print_tree("", "", &root, &deps.as_ref()); + + OrderOperation::PlaceLimit(LimitOrder::new(0, 10, OrderDirection::Bid, sender.clone(), Uint128::from(1u128), Decimal256::zero(), None)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + OrderOperation::RunMarket(MarketOrder::new(Uint128::from(1u128).checked_mul(Uint128::from(4u128)).unwrap(), OrderDirection::Ask, sender.clone())).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); // Second last order should be claimable OrderOperation::Claim((0, 9)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); // Last order should NOT be claimable let err = OrderOperation::Claim((0, 10)).run(deps.as_mut(), env.clone(), info.clone()).unwrap_err(); assert_eq!(err, ContractError::ZeroClaim); +} + +#[test] +fn test_random_orders_single_tick() { + let mut rng = StdRng::seed_from_u64(123456789); + let mut deps = mock_dependencies_custom(); + let env = mock_env(); + let info = mock_info(DEFAULT_SENDER, &[]); + + create_orderbook(deps.as_mut(), QUOTE_DENOM.to_string(), BASE_DENOM.to_string()).unwrap(); + + let oper_count = 3000; + let order_direction = OrderDirection::Bid; + let tick_id = 0; + let mut order_count = 0; + let mut placed_orders: HashMap = HashMap::new(); + + let sender = Addr::unchecked(DEFAULT_SENDER); + + for i in 0..=oper_count { + println!("i: {}", i); + let order_roll = rng.gen_range(0..=100); + if order_roll < 35 { + println!("placing limit"); + OrderOperation::PlaceLimit(LimitOrder::new(tick_id, order_count, order_direction, sender.clone(), Uint128::from(100u128), Decimal256::zero(), None)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + placed_orders.insert(order_count, true); + order_count += 1; + } else if order_roll < 60 { + println!("running market"); + match OrderOperation::RunMarket(MarketOrder::new(Uint128::from(50u128), order_direction.opposite(), sender.clone())).run(deps.as_mut(), env.clone(), info.clone()) { + Ok(_) => { + println!("successfully ran market order") + } + Err(e) => { + match e { + ContractError::InsufficientLiquidity => {} + _ => { + panic!("error: {:?}", e); + } + } + } + } + } else if order_roll < 80 { + println!("cancelling limit"); + let orders_clone = placed_orders.clone(); + let order_ids = orders_clone.keys().collect::>(); + + if order_ids.is_empty() { + continue; + } + let order_id = order_ids[rng.gen_range(0..order_ids.len())].clone(); + println!("order_id: {}", order_id); + let order = orders().load(deps.as_mut().storage, &(tick_id, order_id)).unwrap(); + let root = get_or_init_root_node(deps.as_mut().storage, tick_id, order.order_direction).unwrap(); + if !root.get_value().is_zero() { + println!("checking tree"); + assert_sumtree_invariants(deps.as_ref(), &root, "test_random_orders_single_tick"); + } + match OrderOperation::Cancel((tick_id, order_id)).run(deps.as_mut(), env.clone(), info.clone()) { + Ok(_) => { + println!("order cancelled"); + placed_orders.remove(&order_id); + } + Err(e) => { + match e { + ContractError::CancelFilledOrder => { + + } + _ => { + panic!("error: {:?}", e); + } + } + } + }; + } else { + println!("claiming limit"); + let orders_clone = placed_orders.clone(); + let order_ids = orders_clone.keys().collect::>(); + + if order_ids.is_empty() { + continue; + } + let order_id = order_ids[rng.gen_range(0..order_ids.len())].clone(); + println!("order_id: {}", order_id); + + let order = orders().load(deps.as_mut().storage, &(tick_id, order_id)).unwrap(); + let tick_state = TICK_STATE.load(deps.as_mut().storage, tick_id).unwrap(); + let tick_values = tick_state.get_values(order.order_direction); + let root = get_or_init_root_node(deps.as_mut().storage, tick_id, order.order_direction).unwrap(); + if !root.get_value().is_zero() { + println!("checking tree"); + assert_sumtree_invariants(deps.as_ref(), &root, "test_random_orders_single_tick"); + } + println!("target_etas: {}", tick_values.effective_total_amount_swapped); + let prefix_sum = get_prefix_sum(deps.as_ref().storage,root, tick_values.effective_total_amount_swapped, tick_values.cumulative_realized_cancels).unwrap(); + let cancelled_amount = prefix_sum.saturating_sub(tick_values.cumulative_realized_cancels); + let synced_etas = tick_values.effective_total_amount_swapped.checked_add(cancelled_amount).unwrap(); + + + let claimable_amount = synced_etas.saturating_sub(cancelled_amount).min(decimal256_from_u128(order.quantity)); + + println!("claimable_amount: {}", claimable_amount); + + if claimable_amount.is_zero() { + continue; + } + + match OrderOperation::Claim((tick_id, order_id)).run(deps.as_mut(), env.clone(), info.clone()) { + Ok(_) => { + println!("succesfully claimed limit"); + placed_orders.remove(&order_id); + } + Err(e) => { + match e { + ContractError::ZeroClaim => { + + } + _ => { + panic!("error: {:?}", e); + } + } + } + }; + } + } } \ No newline at end of file diff --git a/contracts/sumtree-orderbook/src/tick.rs b/contracts/sumtree-orderbook/src/tick.rs index 3b9fcec..62a4266 100644 --- a/contracts/sumtree-orderbook/src/tick.rs +++ b/contracts/sumtree-orderbook/src/tick.rs @@ -56,7 +56,8 @@ pub fn sync_tick( // Assuming `calculate_prefix_sum` is a function that calculates the prefix sum at the given ETAS. // This function needs to be implemented based on your sumtree structure and logic. - let new_cumulative_realized_cancels = get_prefix_sum(storage, tree, target_etas)?; + let new_cumulative_realized_cancels = + get_prefix_sum(storage, tree, target_etas, old_cumulative_realized_cancels)?; // Calculate the growth in realized cancels since previous sync. // This is equivalent to the amount we will need to add to the tick's ETAS. From 536c42f138d44e48c1ad341bb5cdb5cc9bbf52dc Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 24 Jun 2024 14:22:12 +0100 Subject: [PATCH 28/98] feat: added min order check when placing a limit --- contracts/sumtree-orderbook/src/order.rs | 29 ++++++++++++++++-------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/contracts/sumtree-orderbook/src/order.rs b/contracts/sumtree-orderbook/src/order.rs index bd6a298..a14504c 100644 --- a/contracts/sumtree-orderbook/src/order.rs +++ b/contracts/sumtree-orderbook/src/order.rs @@ -105,16 +105,27 @@ pub fn place_limit( ); let quant_dec256 = Decimal256::from_ratio(limit_order.quantity.u128(), Uint256::one()); - // Only save the order if not fully filled - if limit_order.quantity > Uint128::zero() { - // Save the order to the orderbook - orders().save(deps.storage, &(tick_id, order_id), &limit_order)?; - tick_values.total_amount_of_liquidity = tick_values - .total_amount_of_liquidity - .checked_add(quant_dec256) - .unwrap(); - } + // Ensure that the order returns a non-zero amount when being claimed + let tick_price = tick_to_price(tick_id)?; + let amount_out = amount_to_value( + order_direction, + quantity, + tick_price, + RoundingDirection::Down, + )?; + ensure!( + !amount_out.is_zero(), + ContractError::InvalidQuantity { quantity } + ); + + // Save the order to the orderbook + orders().save(deps.storage, &(tick_id, order_id), &limit_order)?; + + tick_values.total_amount_of_liquidity = tick_values + .total_amount_of_liquidity + .checked_add(quant_dec256) + .unwrap(); tick_values.cumulative_total_value = tick_values .cumulative_total_value From 5f4e85aae95307e457d6125679fa72f09ed30dd4 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 24 Jun 2024 14:25:48 +0100 Subject: [PATCH 29/98] test: fixed failing tests --- .../sumtree-orderbook/src/tests/test_order.rs | 13 +++---- .../sumtree-orderbook/src/tests/test_query.rs | 38 +++++++------------ 2 files changed, 19 insertions(+), 32 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index 485f99d..ed90061 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -712,7 +712,7 @@ fn test_run_market_order() { tick_bound: MAX_TICK, // Orders to fill against orders: generate_limit_orders( - &[-1500000, 40000000], + &[-1500000, 1500000], // 500 units of liquidity on each tick 5, default_quantity, @@ -722,19 +722,19 @@ fn test_run_market_order() { // implies 1000*0.85 = 850 units of output, but there is only 500 on the tick. // // So 500 gets filled at -1500000, corresponding to ~589 of the input (500/0.85). - // The remaining 1 unit is filled at tick 40,000,000 (price $50,000), which + // The remaining 1 unit is filled at tick 1500000 (price $2.5), which // corresponds to the remaining liquidity. // // Thus, the total expected output is 502. // // Note: this case does not cover rounding for input consumption since it overfills // the tick. - expected_output: Uint256::from_u128(1000), + expected_output: Uint256::from_u128(502), expected_tick_etas: vec![ (-1500000, decimal256_from_u128(Uint128::new(500))), - (40000000, decimal256_from_u128(Uint128::new(500))), + (1500000, decimal256_from_u128(Uint128::new(2))), ], - expected_tick_pointers: vec![(OrderDirection::Ask, 40000000)], + expected_tick_pointers: vec![(OrderDirection::Ask, 1500000)], expected_error: None, }, RunMarketOrderTestCase { @@ -857,7 +857,7 @@ fn test_run_market_order() { tick_bound: MAX_TICK, // Orders to fill against orders: generate_limit_orders( - &[40000000], + &[1500000], // Four limit orders with sufficient total liquidity to process the // full market order 4, @@ -919,7 +919,6 @@ fn test_run_market_order() { continue; } - println!("{:?}", test.name); // Assert no error let response = response.unwrap(); diff --git a/contracts/sumtree-orderbook/src/tests/test_query.rs b/contracts/sumtree-orderbook/src/tests/test_query.rs index 0ffec63..f5d84d1 100644 --- a/contracts/sumtree-orderbook/src/tests/test_query.rs +++ b/contracts/sumtree-orderbook/src/tests/test_query.rs @@ -58,7 +58,7 @@ fn test_query_spot_price() { 1, OrderDirection::Ask, sender.clone(), - Uint128::one(), + Uint128::from(2u128), Decimal256::zero(), None, )), @@ -67,7 +67,7 @@ fn test_query_spot_price() { 2, OrderDirection::Ask, sender.clone(), - Uint128::one(), + Uint128::from(2u128), Decimal256::zero(), None, )), @@ -76,7 +76,7 @@ fn test_query_spot_price() { 3, OrderDirection::Ask, sender.clone(), - Uint128::one(), + Uint128::from(2u128), Decimal256::zero(), None, )), @@ -130,7 +130,7 @@ fn test_query_spot_price() { 1, OrderDirection::Ask, sender.clone(), - Uint128::one(), + Uint128::MAX, Decimal256::zero(), None, )), @@ -169,7 +169,7 @@ fn test_query_spot_price() { 1, OrderDirection::Bid, sender.clone(), - Uint128::one(), + Uint128::from(2u128), Decimal256::zero(), None, )), @@ -178,7 +178,7 @@ fn test_query_spot_price() { 2, OrderDirection::Bid, sender.clone(), - Uint128::one(), + Uint128::from(2u128), Decimal256::zero(), None, )), @@ -187,7 +187,7 @@ fn test_query_spot_price() { 3, OrderDirection::Bid, sender.clone(), - Uint128::one(), + Uint128::from(2u128), Decimal256::zero(), None, )), @@ -241,7 +241,7 @@ fn test_query_spot_price() { 1, OrderDirection::Bid, sender.clone(), - Uint128::one(), + Uint128::MAX, Decimal256::zero(), None, )), @@ -666,34 +666,22 @@ fn test_total_pool_liquidity() { pre_operations: vec![ OrderOperation::PlaceLimitMulti(( // Increasingly spread ticks - vec![ - -1, - -2, - -3, - -5, - -8, - -13, - -21, - -34, - -55, - LARGE_NEGATIVE_TICK, - MIN_TICK, - ], + vec![-1, -2, -3, -5, -8, -13, -21, -34, -55, LARGE_NEGATIVE_TICK], 100, Uint128::from(50u128), OrderDirection::Bid, )), OrderOperation::PlaceLimitMulti(( // Increasingly spread ticks - vec![1, 2, 3, 5, 8, 13, 21, 34, 55, LARGE_POSITIVE_TICK, MAX_TICK], + vec![1, 2, 3, 5, 8, 13, 21, 34, 55, LARGE_POSITIVE_TICK], 100, Uint128::from(110u128), OrderDirection::Ask, )), ], - // Base: 11 ticks at 110*100 = 11000*11 = 121000 - // Quote: 11 ticks at 50*100 = 5000*11 = 55000 - expected_output: vec![coin(121000, BASE_DENOM), coin(55000, QUOTE_DENOM)], + // Base: 11 ticks at 110*100 = 11000*10 = 110000 + // Quote: 11 ticks at 50*100 = 5000*10 = 55000 + expected_output: vec![coin(110000, BASE_DENOM), coin(50000, QUOTE_DENOM)], expected_error: None, }, ]; From dbbb29862c5e51280f3134ca817d35df376b5330 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 24 Jun 2024 14:26:05 +0100 Subject: [PATCH 30/98] chore: removed unused imports --- contracts/sumtree-orderbook/src/tests/test_query.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/sumtree-orderbook/src/tests/test_query.rs b/contracts/sumtree-orderbook/src/tests/test_query.rs index f5d84d1..bdb6b53 100644 --- a/contracts/sumtree-orderbook/src/tests/test_query.rs +++ b/contracts/sumtree-orderbook/src/tests/test_query.rs @@ -5,7 +5,7 @@ use cosmwasm_std::{ }; use crate::{ - constants::{EXPECTED_SWAP_FEE, MAX_TICK, MIN_TICK}, + constants::EXPECTED_SWAP_FEE, orderbook::create_orderbook, query, state::IS_ACTIVE, From c812877eee84cb10ba0c9bcf47745a0ac2eb58bc Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 24 Jun 2024 14:29:48 +0100 Subject: [PATCH 31/98] test: added cases for min quantity --- .../sumtree-orderbook/src/tests/test_order.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index ed90061..53eb649 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -176,6 +176,24 @@ fn test_place_limit() { claim_bounty: None, expected_error: None, }, + PlaceLimitTestCase { + name: "invalid quantity: zero claim order ASK", + tick_id: LARGE_POSITIVE_TICK, + quantity: Uint128::one(), + sent: Uint128::one(), + order_direction: OrderDirection::Ask, + claim_bounty: Some(Decimal256::zero()), + expected_error: Some(ContractError::InvalidQuantity { quantity: Uint128::one() }), + }, + PlaceLimitTestCase { + name: "invalid quantity: zero claim order BID", + tick_id: LARGE_NEGATIVE_TICK, + quantity: Uint128::one(), + sent: Uint128::one(), + order_direction: OrderDirection::Bid, + claim_bounty: Some(Decimal256::zero()), + expected_error: Some(ContractError::InvalidQuantity { quantity: Uint128::one() }), + } ]; for test in test_cases { From cad9c4a9fa119a7da825b2a1a8760934de2b0976 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 24 Jun 2024 14:45:13 +0100 Subject: [PATCH 32/98] feat: improved sync tick error handling --- contracts/sumtree-orderbook/src/error.rs | 3 +++ contracts/sumtree-orderbook/src/tick.rs | 10 ++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/contracts/sumtree-orderbook/src/error.rs b/contracts/sumtree-orderbook/src/error.rs index 2feaeb1..25d52bd 100644 --- a/contracts/sumtree-orderbook/src/error.rs +++ b/contracts/sumtree-orderbook/src/error.rs @@ -88,6 +88,9 @@ pub enum ContractError { #[error("Invalid tick state: syncing tick pushed ETAS past CTT")] InvalidTickSync, + #[error("Invalid prefix sum: {error:?}")] + InvalidPrefixSum { error: Option }, + #[error("Zero Claim: Nothing to be claimed yet")] ZeroClaim, diff --git a/contracts/sumtree-orderbook/src/tick.rs b/contracts/sumtree-orderbook/src/tick.rs index b974b09..24f04e7 100644 --- a/contracts/sumtree-orderbook/src/tick.rs +++ b/contracts/sumtree-orderbook/src/tick.rs @@ -59,8 +59,14 @@ pub fn sync_tick( // Calculate the growth in realized cancels since previous sync. // This is equivalent to the amount we will need to add to the tick's ETAS. - let realized_since_last_sync = - new_cumulative_realized_cancels.checked_sub(old_cumulative_realized_cancels)?; + let realized_since_last_sync = new_cumulative_realized_cancels + .checked_sub(old_cumulative_realized_cancels) + .map_err(|_| ContractError::InvalidPrefixSum { + error: Some(format!( + "New prefix sum less than previous, previous: {}, new: {}", + old_cumulative_realized_cancels, new_cumulative_realized_cancels + )), + })?; // Update the tick state to represent new ETAS and new cumulative realized cancels. tick_value.effective_total_amount_swapped = tick_value From 561001657717961a928a509621ca26fee6cfe693 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 24 Jun 2024 14:49:57 +0100 Subject: [PATCH 33/98] fix: added case for when syncing the left node would cause the right node to sync in prefix sum --- .../src/sumtree/test/test_fuzz.rs | 4 +++- .../src/sumtree/test/test_node.rs | 4 +++- .../src/sumtree/test/test_tree.rs | 8 ++++++- .../sumtree-orderbook/src/sumtree/tree.rs | 23 ++++++++++++++++--- contracts/sumtree-orderbook/src/tick.rs | 3 ++- 5 files changed, 35 insertions(+), 7 deletions(-) diff --git a/contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs b/contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs index abbe438..047ded1 100644 --- a/contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs @@ -74,7 +74,9 @@ fn test_fuzz_insert() { assert_sumtree_invariants(deps.as_ref(), &tree, &test_name); // Assert prefix sum correctness - let prefix_sum = get_prefix_sum(deps.as_ref().storage, tree, target_etas).unwrap(); + let prefix_sum = + get_prefix_sum(deps.as_ref().storage, tree, target_etas, Decimal256::zero()) + .unwrap(); assert_eq!( expected_prefix_sum, prefix_sum, "{test_name}: Expected prefix sum {expected_prefix_sum}, got {prefix_sum}. Target ETAS: {target_etas}", diff --git a/contracts/sumtree-orderbook/src/sumtree/test/test_node.rs b/contracts/sumtree-orderbook/src/sumtree/test/test_node.rs index f8ee461..07dd789 100644 --- a/contracts/sumtree-orderbook/src/sumtree/test/test_node.rs +++ b/contracts/sumtree-orderbook/src/sumtree/test/test_node.rs @@ -2751,7 +2751,9 @@ fn test_node_insert_large_quantity() { // Ensure prefix sum functions correctly let root_node = get_root_node(deps.as_mut().storage, tick_id, direction).unwrap(); - let prefix_sum = get_prefix_sum(deps.as_mut().storage, root_node, target_etas).unwrap(); + let prefix_sum = + get_prefix_sum(deps.as_mut().storage, root_node, target_etas, Decimal256::zero()) + .unwrap(); assert_eq!(expected_prefix_sum, prefix_sum); } diff --git a/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs b/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs index 33faf82..d530902 100644 --- a/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs +++ b/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs @@ -183,7 +183,13 @@ fn test_get_prefix_sum_valid() { assert_internal_values(test.name, deps.as_ref(), internals, true); // System under test: get prefix sum - let prefix_sum = get_prefix_sum(deps.as_ref().storage, tree, test.target_etas).unwrap(); + let prefix_sum = get_prefix_sum( + deps.as_ref().storage, + tree, + test.target_etas, + Decimal256::zero(), + ) + .unwrap(); // Assert that the correct value was returned assert_eq!( diff --git a/contracts/sumtree-orderbook/src/sumtree/tree.rs b/contracts/sumtree-orderbook/src/sumtree/tree.rs index ad3de7f..5b06f17 100644 --- a/contracts/sumtree-orderbook/src/sumtree/tree.rs +++ b/contracts/sumtree-orderbook/src/sumtree/tree.rs @@ -46,13 +46,14 @@ pub fn get_prefix_sum( storage: &dyn Storage, root_node: TreeNode, target_etas: Decimal256, + prev_sum: Decimal256, ) -> ContractResult { // We start from the root node's sum, which includes everything in the tree. // The prefix sum algorithm will chip away at this until we have the correct // prefux sum in O(log(N)) time. let starting_sum = TreeNode::get_value(&root_node); - prefix_sum_walk(storage, &root_node, starting_sum, target_etas) + prefix_sum_walk(storage, &root_node, starting_sum, target_etas, prev_sum) } // prefix_sum_walk is a recursive function that walks the sumtree to calculate the prefix sum below the given @@ -68,6 +69,7 @@ fn prefix_sum_walk( node: &TreeNode, mut current_sum: Decimal256, target_etas: Decimal256, + prev_sum: Decimal256, ) -> ContractResult { // Sanity check: target ETAS should be inside node's range. if target_etas < node.get_min_range() { @@ -95,6 +97,20 @@ fn prefix_sum_walk( let left_child = node.get_left(storage)?; let right_child = node.get_right(storage)?; + if left_child.is_some() && right_child.is_some() { + let left_child = left_child.clone().unwrap(); + let right_child = right_child.clone().unwrap(); + + let sum_at_node = current_sum.checked_sub(node.get_value())?; + let diff_at_node = prev_sum.saturating_sub(sum_at_node); + let unrealized_from_left = left_child.get_value().saturating_sub(diff_at_node); + let new_etas = target_etas.checked_add(unrealized_from_left)?; + + if new_etas >= right_child.get_min_range() { + return prefix_sum_walk(storage, &right_child, current_sum, new_etas, prev_sum); + } + } + // If the left child exists, we run the following logic: // * If target ETAS < left child's lower bound, exit early with zero // * Else if target ETAS <= upper bound, subtract right child sum from prefix sum and walk left @@ -118,7 +134,8 @@ fn prefix_sum_walk( current_sum = current_sum.checked_sub(right_sum)?; // Walk left recursively - current_sum = prefix_sum_walk(storage, &left_child, current_sum, target_etas)?; + current_sum = + prefix_sum_walk(storage, &left_child, current_sum, target_etas, prev_sum)?; return Ok(current_sum); } @@ -149,7 +166,7 @@ fn prefix_sum_walk( // to subtract from it yet. The right walk handles this update. // Walk right recursively - current_sum = prefix_sum_walk(storage, &right_child, current_sum, target_etas)?; + current_sum = prefix_sum_walk(storage, &right_child, current_sum, target_etas, prev_sum)?; Ok(current_sum) } else { diff --git a/contracts/sumtree-orderbook/src/tick.rs b/contracts/sumtree-orderbook/src/tick.rs index 24f04e7..b3822bd 100644 --- a/contracts/sumtree-orderbook/src/tick.rs +++ b/contracts/sumtree-orderbook/src/tick.rs @@ -55,7 +55,8 @@ pub fn sync_tick( // Assuming `calculate_prefix_sum` is a function that calculates the prefix sum at the given ETAS. // This function needs to be implemented based on your sumtree structure and logic. - let new_cumulative_realized_cancels = get_prefix_sum(storage, tree, target_etas)?; + let new_cumulative_realized_cancels = + get_prefix_sum(storage, tree, target_etas, old_cumulative_realized_cancels)?; // Calculate the growth in realized cancels since previous sync. // This is equivalent to the amount we will need to add to the tick's ETAS. From 700043d85c7d977ca8bbb11a0d384e793553f25d Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 24 Jun 2024 15:22:06 +0100 Subject: [PATCH 34/98] test: fixed all failing tests --- .../src/sumtree/test/test_fuzz.rs | 10 ++-- .../src/sumtree/test/test_node.rs | 3 +- .../src/sumtree/test/test_tree.rs | 49 +++++++++---------- .../sumtree-orderbook/src/tests/test_tick.rs | 20 ++++---- 4 files changed, 42 insertions(+), 40 deletions(-) diff --git a/contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs b/contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs index 047ded1..6d40eaf 100644 --- a/contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs @@ -74,9 +74,13 @@ fn test_fuzz_insert() { assert_sumtree_invariants(deps.as_ref(), &tree, &test_name); // Assert prefix sum correctness - let prefix_sum = - get_prefix_sum(deps.as_ref().storage, tree, target_etas, Decimal256::zero()) - .unwrap(); + let prefix_sum = get_prefix_sum( + deps.as_ref().storage, + tree.clone(), + target_etas, + tree.get_value(), + ) + .unwrap(); assert_eq!( expected_prefix_sum, prefix_sum, "{test_name}: Expected prefix sum {expected_prefix_sum}, got {prefix_sum}. Target ETAS: {target_etas}", diff --git a/contracts/sumtree-orderbook/src/sumtree/test/test_node.rs b/contracts/sumtree-orderbook/src/sumtree/test/test_node.rs index 07dd789..8c5aeaf 100644 --- a/contracts/sumtree-orderbook/src/sumtree/test/test_node.rs +++ b/contracts/sumtree-orderbook/src/sumtree/test/test_node.rs @@ -2751,8 +2751,9 @@ fn test_node_insert_large_quantity() { // Ensure prefix sum functions correctly let root_node = get_root_node(deps.as_mut().storage, tick_id, direction).unwrap(); + // Use root node value as previous sum to use explicit prefix sum calculation with no overlaps let prefix_sum = - get_prefix_sum(deps.as_mut().storage, root_node, target_etas, Decimal256::zero()) + get_prefix_sum(deps.as_mut().storage, root_node.clone(), target_etas, root_node.get_value()) .unwrap(); assert_eq!(expected_prefix_sum, prefix_sum); } diff --git a/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs b/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs index d530902..3567cf3 100644 --- a/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs +++ b/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs @@ -9,6 +9,7 @@ struct TestPrefixSumCase { name: &'static str, nodes: Vec, target_etas: Decimal256, + prev_sum: Decimal256, expected_sum: Decimal256, } @@ -21,7 +22,7 @@ fn test_get_prefix_sum_valid() { name: "Single node, target ETAS equal to node ETAS", nodes: vec![NodeType::leaf_uint256(10u128, 5u128)], target_etas: Decimal256::from_ratio(10u128, 1u128), - + prev_sum: Decimal256::zero(), // We expect the full value of the node because the prefix // sum is intended to return "all nodes that overlap with // the target ETAS". @@ -34,14 +35,14 @@ fn test_get_prefix_sum_valid() { name: "Single node, target ETAS below node range", nodes: vec![NodeType::leaf_uint256(50u128, 20u128)], target_etas: Decimal256::from_ratio(25u128, 1u128), - + prev_sum: Decimal256::zero(), expected_sum: Decimal256::zero(), }, TestPrefixSumCase { name: "Single node, target ETAS above node range", nodes: vec![NodeType::leaf_uint256(10u128, 10u128)], target_etas: Decimal256::from_ratio(30u128, 1u128), - + prev_sum: Decimal256::zero(), expected_sum: Decimal256::from_ratio(10u128, 1u128), }, TestPrefixSumCase { @@ -49,10 +50,10 @@ fn test_get_prefix_sum_valid() { nodes: vec![ NodeType::leaf_uint256(5u128, 10u128), NodeType::leaf_uint256(15u128, 20u128), - NodeType::leaf_uint256(35u128, 30u128), + NodeType::leaf_uint256(45u128, 30u128), ], target_etas: Decimal256::from_ratio(20u128, 1u128), - + prev_sum: Decimal256::from_ratio(10u128, 1u128), expected_sum: Decimal256::from_ratio(30u128, 1u128), }, TestPrefixSumCase { @@ -62,7 +63,7 @@ fn test_get_prefix_sum_valid() { NodeType::leaf_uint256(5u128, 12u128), ], target_etas: Decimal256::from_ratio(5u128, 1u128), - + prev_sum: Decimal256::zero(), expected_sum: Decimal256::from_ratio(15u128, 1u128), }, TestPrefixSumCase { @@ -73,7 +74,7 @@ fn test_get_prefix_sum_valid() { NodeType::leaf_uint256(40u128, 30u128), ], target_etas: Decimal256::from_ratio(5u128, 1u128), - + prev_sum: Decimal256::zero(), expected_sum: Decimal256::zero(), }, TestPrefixSumCase { @@ -84,40 +85,40 @@ fn test_get_prefix_sum_valid() { NodeType::leaf_uint256(40u128, 30u128), ], target_etas: Decimal256::from_ratio(45u128, 1u128), - + prev_sum: Decimal256::zero(), expected_sum: Decimal256::from_ratio(60u128, 1u128), // Sum of all nodes }, TestPrefixSumCase { name: "Nodes inserted in reverse order", nodes: vec![ - NodeType::leaf_uint256(30u128, 10u128), + NodeType::leaf_uint256(40u128, 10u128), NodeType::leaf_uint256(20u128, 5u128), NodeType::leaf_uint256(10u128, 5u128), ], target_etas: Decimal256::from_ratio(25u128, 1u128), - + prev_sum: Decimal256::zero(), expected_sum: Decimal256::from_ratio(10u128, 1u128), }, TestPrefixSumCase { name: "Nodes inserted in shuffled order", nodes: vec![ - NodeType::leaf_uint256(30u128, 10u128), + NodeType::leaf_uint256(40u128, 10u128), NodeType::leaf_uint256(10u128, 5u128), NodeType::leaf_uint256(20u128, 5u128), ], target_etas: Decimal256::from_ratio(25u128, 1u128), - + prev_sum: Decimal256::zero(), expected_sum: Decimal256::from_ratio(10u128, 1u128), }, TestPrefixSumCase { name: "Nodes inserted in shuffled order, target ETAS at node lower bound", nodes: vec![ - NodeType::leaf_uint256(30u128, 11u128), + NodeType::leaf_uint256(35u128, 11u128), NodeType::leaf_uint256(10u128, 7u128), NodeType::leaf_uint256(20u128, 5u128), ], target_etas: Decimal256::from_ratio(20u128, 1u128), - + prev_sum: Decimal256::zero(), // We expect the sum of the 2nd and 3rd nodes, so 7 + 5 expected_sum: Decimal256::from_ratio(12u128, 1u128), }, @@ -129,7 +130,7 @@ fn test_get_prefix_sum_valid() { NodeType::leaf_uint256(100u128, 30u128), ], target_etas: Decimal256::from_ratio(75u128, 1u128), - + prev_sum: Decimal256::from_ratio(70u128, 1u128), expected_sum: Decimal256::from_ratio(30u128, 1u128), }, TestPrefixSumCase { @@ -139,9 +140,9 @@ fn test_get_prefix_sum_valid() { NodeType::leaf_uint256(20u128, 10u128), NodeType::leaf_uint256(30u128, 10u128), ], - target_etas: Decimal256::from_ratio(25u128, 1u128), - - expected_sum: Decimal256::from_ratio(20u128, 1u128), + target_etas: Decimal256::from_ratio(10u128, 1u128), + prev_sum: Decimal256::zero(), + expected_sum: Decimal256::from_ratio(30u128, 1u128), }, TestPrefixSumCase { name: "Complex case with many nodes (shuffled, adjacent, spaced out)", @@ -160,7 +161,8 @@ fn test_get_prefix_sum_valid() { ], // Target includes everything except the last two nodes target_etas: Decimal256::from_ratio(305u128, 1u128), - + // Previous sum at 4th last node max to prevent final two nodes being synced after previous nodes being synced + prev_sum: Decimal256::from_ratio(300u128, 1u128), // Sum of all nodes except the last two: // 5 + 19 + 4 + 10 + 9 + 20 + 50 + 40 + 29 = 186 expected_sum: Decimal256::from_ratio(186u128, 1u128), @@ -183,13 +185,8 @@ fn test_get_prefix_sum_valid() { assert_internal_values(test.name, deps.as_ref(), internals, true); // System under test: get prefix sum - let prefix_sum = get_prefix_sum( - deps.as_ref().storage, - tree, - test.target_etas, - Decimal256::zero(), - ) - .unwrap(); + let prefix_sum = + get_prefix_sum(deps.as_ref().storage, tree, test.target_etas, test.prev_sum).unwrap(); // Assert that the correct value was returned assert_eq!( diff --git a/contracts/sumtree-orderbook/src/tests/test_tick.rs b/contracts/sumtree-orderbook/src/tests/test_tick.rs index 63a7d45..f7b84a2 100644 --- a/contracts/sumtree-orderbook/src/tests/test_tick.rs +++ b/contracts/sumtree-orderbook/src/tests/test_tick.rs @@ -133,19 +133,19 @@ fn test_sync_tick() { // Iteration 1: only node 1 is included // Iteration 2: first two nodes included // Iteration 3: first four nodes are incldued - new_bid_etas_per_sync: Decimal256::from_ratio(30u128, 1u128), + new_bid_etas_per_sync: Decimal256::from_ratio(7u128, 1u128), new_ask_etas_per_sync: Decimal256::zero(), num_syncs: 4, - // By end of iteration 3, the amounts of the first four nodes should be included. + // By end of iteration 4, the amounts of the first four nodes should be included. // This is equal to 30 + 12 + 10 + 28 = 80 expected_cumulative_realized_bid: Decimal256::from_ratio(80u128, 1u128), expected_cumulative_realized_ask: Decimal256::zero(), - // The new ETAS includes all the incremented amounts (3 * 30 each) which represent fills, + // The new ETAS includes all the incremented amounts (4 * 7 each) which represent fills, // plus the amount of realized cancellations expected_new_bid_etas_post_sync: Decimal256::from_ratio( - (4u128 * 30u128) + 80u128, + (4u128 * 7u128) + 80u128, 1u128, ), expected_new_ask_etas_post_sync: Decimal256::zero(), @@ -252,7 +252,7 @@ fn test_sync_tick() { // Iteration 1: only node 1 is included // Iteration 2: first two nodes included // Iteration 3: first four nodes are included - new_ask_etas_per_sync: Decimal256::from_ratio(30u128, 1u128), + new_ask_etas_per_sync: Decimal256::from_ratio(7u128, 1u128), new_bid_etas_per_sync: Decimal256::zero(), num_syncs: 4, @@ -261,10 +261,10 @@ fn test_sync_tick() { expected_cumulative_realized_ask: Decimal256::from_ratio(80u128, 1u128), expected_cumulative_realized_bid: Decimal256::zero(), - // The new ETAS includes all the incremented amounts (3 * 30 each) which represent fills, + // The new ETAS includes all the incremented amounts (4 * 7 each) which represent fills, // plus the amount of realized cancellations expected_new_ask_etas_post_sync: Decimal256::from_ratio( - (4u128 * 30u128) + 80u128, + (4u128 * 7u128) + 80u128, 1u128, ), expected_new_bid_etas_post_sync: Decimal256::zero(), @@ -305,7 +305,7 @@ fn test_sync_tick() { // Multiple unrealized cancels for both bid and ask unrealized_cancels_bid: vec![ - NodeType::leaf_uint256(35u32, 25u32), + NodeType::leaf_uint256(45u32, 25u32), NodeType::leaf_uint256(10u32, 25u32), ], unrealized_cancels_ask: vec![ @@ -314,7 +314,7 @@ fn test_sync_tick() { ], // Increment tick ETAS by 10 for bid and 40 for ask per iteration for 2 iterations - new_bid_etas_per_sync: Decimal256::from_ratio(10u128, 1u128), + new_bid_etas_per_sync: Decimal256::from_ratio(5u128, 1u128), new_ask_etas_per_sync: Decimal256::from_ratio(40u128, 1u128), num_syncs: 2, @@ -325,7 +325,7 @@ fn test_sync_tick() { // The new ETAS includes all the incremented amounts which represent fills, // plus the amount of realized cancellations for both bid and ask expected_new_bid_etas_post_sync: Decimal256::from_ratio( - (2u128 * 10u128) + 25u128, + (2u128 * 5u128) + 25u128, 1u128, ), expected_new_ask_etas_post_sync: Decimal256::from_ratio( From 64bfa572f70c8ee37b82fe623e56cc6f732d99ba Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 24 Jun 2024 15:47:54 +0100 Subject: [PATCH 35/98] chore: commented new prefix sum condition --- contracts/sumtree-orderbook/src/sumtree/tree.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/contracts/sumtree-orderbook/src/sumtree/tree.rs b/contracts/sumtree-orderbook/src/sumtree/tree.rs index 5b06f17..75171b4 100644 --- a/contracts/sumtree-orderbook/src/sumtree/tree.rs +++ b/contracts/sumtree-orderbook/src/sumtree/tree.rs @@ -97,15 +97,26 @@ fn prefix_sum_walk( let left_child = node.get_left(storage)?; let right_child = node.get_right(storage)?; + // To prevent requiring a resync there needs to be a condition that covers the case that + // when realizing the left node the new ETAS is enough to realize the right node (to some extent) + // To cover this we can determine how much of the left node has been realized, using this we can then determine + // if realizing what is unrealized from the left node will result in a new ETAS that is enough to realize the + // right node (to some extent) if left_child.is_some() && right_child.is_some() { let left_child = left_child.clone().unwrap(); let right_child = right_child.clone().unwrap(); + // Calculate what the sum is before realizing the current node let sum_at_node = current_sum.checked_sub(node.get_value())?; + // Calculate how much of the node has been realized in a previous sync let diff_at_node = prev_sum.saturating_sub(sum_at_node); + // Calculate how much of the left node is unrealized let unrealized_from_left = left_child.get_value().saturating_sub(diff_at_node); + // Calculate the new ETAS after realizing what is unrealized from the left node let new_etas = target_etas.checked_add(unrealized_from_left)?; + // if the new ETAS is greater than or equal to the right child's min range, we can walk right + // as the left node MUST be realizable given the invariants of the sumtree mechanism if new_etas >= right_child.get_min_range() { return prefix_sum_walk(storage, &right_child, current_sum, new_etas, prev_sum); } From 5533a9cb1784e2aa7215bde562a8d77ded541958 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 24 Jun 2024 15:49:14 +0100 Subject: [PATCH 36/98] chore: small comment addition --- contracts/sumtree-orderbook/src/sumtree/tree.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/sumtree-orderbook/src/sumtree/tree.rs b/contracts/sumtree-orderbook/src/sumtree/tree.rs index 75171b4..680188d 100644 --- a/contracts/sumtree-orderbook/src/sumtree/tree.rs +++ b/contracts/sumtree-orderbook/src/sumtree/tree.rs @@ -90,13 +90,13 @@ fn prefix_sum_walk( return Ok(current_sum); } - // --- Attempt walk left --- - // We fetch both children here since we need to access both regardless of // whether we walk left or right. let left_child = node.get_left(storage)?; let right_child = node.get_right(storage)?; + // -- Resync Condition -- + // To prevent requiring a resync there needs to be a condition that covers the case that // when realizing the left node the new ETAS is enough to realize the right node (to some extent) // To cover this we can determine how much of the left node has been realized, using this we can then determine @@ -122,6 +122,8 @@ fn prefix_sum_walk( } } + // --- Attempt walk left --- + // If the left child exists, we run the following logic: // * If target ETAS < left child's lower bound, exit early with zero // * Else if target ETAS <= upper bound, subtract right child sum from prefix sum and walk left From 7685ed2e0657ac817cfca8a2f811ce84114c0bd8 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Tue, 25 Jun 2024 15:25:11 +0100 Subject: [PATCH 37/98] feat: added time restriction for testing --- .../sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index 8c86508..6da04b7 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::time::{Duration, SystemTime}; use cosmwasm_std::{Coin, Decimal, Uint256}; use cosmwasm_std::{Decimal256, Uint128}; @@ -67,12 +68,17 @@ fn test_order_fuzz_linear_single_tick() { #[test] fn test_order_fuzz_mixed() { - run_fuzz_mixed(2500, (-20, 20)); + let duration = Duration::from_secs(60); + let now = SystemTime::now(); + let end = now.checked_add(duration).unwrap(); + while SystemTime::now().le(&end) { + run_fuzz_mixed(2000, (-20, 20)); + } } #[test] fn test_order_fuzz_single_tick() { - run_fuzz_mixed(1000, (0, 0)); + run_fuzz_mixed(0000, (0, 0)); } /// Runs a linear fuzz test with the following steps From 5cdd161ece1ad63908b138a5151335a419c6e9e2 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Tue, 25 Jun 2024 15:35:03 +0100 Subject: [PATCH 38/98] refactor: altered prefix sum algorithm --- contracts/sumtree-orderbook/src/sumtree/tree.rs | 2 +- contracts/sumtree-orderbook/src/tick.rs | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/contracts/sumtree-orderbook/src/sumtree/tree.rs b/contracts/sumtree-orderbook/src/sumtree/tree.rs index d890bd3..57467b6 100644 --- a/contracts/sumtree-orderbook/src/sumtree/tree.rs +++ b/contracts/sumtree-orderbook/src/sumtree/tree.rs @@ -76,7 +76,7 @@ fn prefix_sum_walk( if target_etas < node.get_min_range() { // If the target ETAS is below the root node's range, we can return zero early. return Ok(Decimal256::zero()); - } else if target_etas >= node.get_max_range() { + } else if target_etas >= node.get_max_range().checked_sub(node.get_value())? { return Ok(current_sum); } diff --git a/contracts/sumtree-orderbook/src/tick.rs b/contracts/sumtree-orderbook/src/tick.rs index 62a4266..0c2bfbc 100644 --- a/contracts/sumtree-orderbook/src/tick.rs +++ b/contracts/sumtree-orderbook/src/tick.rs @@ -54,10 +54,15 @@ pub fn sync_tick( // Fetch sumtree for tick by order direction. If none exists, initialize one. let tree = get_or_init_root_node(storage, tick_id, direction)?; + let new_target_etas = target_etas.checked_sub(tick_value.cumulative_realized_cancels)?; // Assuming `calculate_prefix_sum` is a function that calculates the prefix sum at the given ETAS. // This function needs to be implemented based on your sumtree structure and logic. - let new_cumulative_realized_cancels = - get_prefix_sum(storage, tree, target_etas, old_cumulative_realized_cancels)?; + let new_cumulative_realized_cancels = get_prefix_sum( + storage, + tree, + new_target_etas, + old_cumulative_realized_cancels, + )?; // Calculate the growth in realized cancels since previous sync. // This is equivalent to the amount we will need to add to the tick's ETAS. From 26ae6ec971ccf4cc324a578a6677c05f5c630984 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Wed, 26 Jun 2024 14:36:46 +0100 Subject: [PATCH 39/98] fix: removed max range - value check for prefix sum --- contracts/sumtree-orderbook/src/sumtree/tree.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/sumtree-orderbook/src/sumtree/tree.rs b/contracts/sumtree-orderbook/src/sumtree/tree.rs index 57467b6..d890bd3 100644 --- a/contracts/sumtree-orderbook/src/sumtree/tree.rs +++ b/contracts/sumtree-orderbook/src/sumtree/tree.rs @@ -76,7 +76,7 @@ fn prefix_sum_walk( if target_etas < node.get_min_range() { // If the target ETAS is below the root node's range, we can return zero early. return Ok(Decimal256::zero()); - } else if target_etas >= node.get_max_range().checked_sub(node.get_value())? { + } else if target_etas >= node.get_max_range() { return Ok(current_sum); } From bccdac203fcb9faafc1464d8dccd5519cc3306d1 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Wed, 26 Jun 2024 16:12:17 +0100 Subject: [PATCH 40/98] feat: silently fail on zero amount claims --- contracts/sumtree-orderbook/src/order.rs | 34 ++-- .../sumtree-orderbook/src/tests/test_order.rs | 183 +++++++++++------- 2 files changed, 121 insertions(+), 96 deletions(-) diff --git a/contracts/sumtree-orderbook/src/order.rs b/contracts/sumtree-orderbook/src/order.rs index a14504c..a13685b 100644 --- a/contracts/sumtree-orderbook/src/order.rs +++ b/contracts/sumtree-orderbook/src/order.rs @@ -106,19 +106,6 @@ pub fn place_limit( let quant_dec256 = Decimal256::from_ratio(limit_order.quantity.u128(), Uint256::one()); - // Ensure that the order returns a non-zero amount when being claimed - let tick_price = tick_to_price(tick_id)?; - let amount_out = amount_to_value( - order_direction, - quantity, - tick_price, - RoundingDirection::Down, - )?; - ensure!( - !amount_out.is_zero(), - ContractError::InvalidQuantity { quantity } - ); - // Save the order to the orderbook orders().save(deps.storage, &(tick_id, order_id), &limit_order)?; @@ -680,9 +667,6 @@ pub(crate) fn claim_order( // Immutable amount to prevent bounty/maker fee calculations affecting each other let raw_amount = amount; - // Cannot send a zero amount, may be zero'd out by rounding - ensure!(!amount.is_zero(), ContractError::ZeroClaim); - let denom = orderbook.get_opposite_denom(&order.order_direction); // Send claim bounty to sender if applicable @@ -707,13 +691,17 @@ pub(crate) fn claim_order( amount = amount.checked_sub(maker_fee_amount)?; } - // Claimed amount always goes to the order owner - let bank_msg = MsgSend256 { - from_address: contract_address.to_string(), - to_address: order.owner.to_string(), - amount: vec![coin_u256(amount, &denom)], - }; - let mut bank_msg_vec = vec![SubMsg::reply_on_error(bank_msg, REPLY_ID_CLAIM)]; + let mut bank_msg_vec = vec![]; + // Silently fail on zero claim (dust amount) orders + if !amount.is_zero() { + // Claimed amount always goes to the order owner + let bank_msg = MsgSend256 { + from_address: contract_address.to_string(), + to_address: order.owner.to_string(), + amount: vec![coin_u256(amount, &denom)], + }; + bank_msg_vec.push(SubMsg::reply_on_error(bank_msg, REPLY_ID_CLAIM)); + } if !bounty.is_zero() { // Bounty always goes to the sender diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index 53eb649..eb0a27c 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -177,22 +177,22 @@ fn test_place_limit() { expected_error: None, }, PlaceLimitTestCase { - name: "invalid quantity: zero claim order ASK", + name: "zero claim order ASK", tick_id: LARGE_POSITIVE_TICK, quantity: Uint128::one(), sent: Uint128::one(), order_direction: OrderDirection::Ask, claim_bounty: Some(Decimal256::zero()), - expected_error: Some(ContractError::InvalidQuantity { quantity: Uint128::one() }), + expected_error: None, }, PlaceLimitTestCase { - name: "invalid quantity: zero claim order BID", + name: "zero claim order BID", tick_id: LARGE_NEGATIVE_TICK, quantity: Uint128::one(), sent: Uint128::one(), order_direction: OrderDirection::Bid, claim_bounty: Some(Decimal256::zero()), - expected_error: Some(ContractError::InvalidQuantity { quantity: Uint128::one() }), + expected_error: None, } ]; @@ -1581,13 +1581,10 @@ struct ClaimOrderTestCase { name: &'static str, operations: Vec, sender: Addr, - tick_id: i64, order_id: u64, - - expected_bank_msg: SubMsg, + expected_bank_msg: Option, expected_bounty_msg: Option, - expected_order_state: Option, expected_error: Option, } @@ -1620,14 +1617,14 @@ fn test_claim_order() { order_id: 0, tick_id: valid_tick_id, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(10u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: None, expected_error: None, @@ -1654,14 +1651,14 @@ fn test_claim_order() { order_id: 0, tick_id: valid_tick_id, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(10u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: None, expected_error: None, @@ -1687,14 +1684,14 @@ fn test_claim_order() { ], order_id: 0, tick_id: valid_tick_id, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(5u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: Some(LimitOrder::new( valid_tick_id, @@ -1734,14 +1731,14 @@ fn test_claim_order() { ], order_id: 0, tick_id: valid_tick_id, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(3u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: None, expected_error: None, @@ -1767,7 +1764,7 @@ fn test_claim_order() { ], order_id: 0, tick_id: valid_tick_id, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), @@ -1775,7 +1772,7 @@ fn test_claim_order() { amount: vec![coin_u256(Uint256::from(99u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), @@ -1816,7 +1813,7 @@ fn test_claim_order() { order_id: 0, tick_id: valid_tick_id, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), @@ -1824,7 +1821,7 @@ fn test_claim_order() { amount: vec![coin_u256(Uint256::from(299u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), @@ -1859,14 +1856,14 @@ fn test_claim_order() { ], order_id: 0, tick_id: LARGE_POSITIVE_TICK, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(5u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: None, expected_error: None, @@ -1894,14 +1891,14 @@ fn test_claim_order() { order_id: 0, tick_id: LARGE_POSITIVE_TICK, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(2u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: Some(LimitOrder::new( LARGE_POSITIVE_TICK, @@ -1944,14 +1941,14 @@ fn test_claim_order() { order_id: 0, tick_id: LARGE_POSITIVE_TICK, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(3u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: None, expected_error: None, @@ -1979,14 +1976,14 @@ fn test_claim_order() { order_id: 0, tick_id: LARGE_NEGATIVE_TICK, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(200u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: None, expected_error: None, @@ -2013,14 +2010,14 @@ fn test_claim_order() { order_id: 0, tick_id: LARGE_NEGATIVE_TICK, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(100u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: Some(LimitOrder::new( LARGE_NEGATIVE_TICK, @@ -2062,14 +2059,14 @@ fn test_claim_order() { order_id: 0, tick_id: LARGE_NEGATIVE_TICK, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(100u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: None, expected_error: None, @@ -2106,14 +2103,14 @@ fn test_claim_order() { order_id: 1, tick_id: valid_tick_id, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(100u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: None, expected_error: None, @@ -2143,14 +2140,14 @@ fn test_claim_order() { order_id: 0, tick_id: MIN_TICK, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(3_000_000_000_000u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: Some(LimitOrder::new( MIN_TICK, @@ -2186,14 +2183,14 @@ fn test_claim_order() { order_id: 0, tick_id: valid_tick_id, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(10u128), BASE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: None, expected_error: None, @@ -2220,14 +2217,14 @@ fn test_claim_order() { order_id: 0, tick_id: valid_tick_id, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(5u128), BASE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: Some(LimitOrder::new( valid_tick_id, @@ -2268,14 +2265,14 @@ fn test_claim_order() { order_id: 0, tick_id: valid_tick_id, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(3u128), BASE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: None, expected_error: None, @@ -2304,14 +2301,14 @@ fn test_claim_order() { order_id: 0, tick_id: LARGE_POSITIVE_TICK, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(20u128), BASE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: None, expected_error: None, @@ -2338,14 +2335,14 @@ fn test_claim_order() { order_id: 0, tick_id: LARGE_POSITIVE_TICK, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(10u128), BASE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: Some(LimitOrder::new( LARGE_POSITIVE_TICK, @@ -2387,14 +2384,14 @@ fn test_claim_order() { order_id: 0, tick_id: LARGE_POSITIVE_TICK, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(10u128), BASE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: None, expected_error: None, @@ -2422,14 +2419,14 @@ fn test_claim_order() { order_id: 0, tick_id: LARGE_NEGATIVE_TICK, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(50u128), BASE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: None, expected_error: None, @@ -2456,14 +2453,14 @@ fn test_claim_order() { order_id: 0, tick_id: LARGE_NEGATIVE_TICK, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(25u128), BASE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: Some(LimitOrder::new( LARGE_NEGATIVE_TICK, @@ -2505,14 +2502,14 @@ fn test_claim_order() { order_id: 0, tick_id: LARGE_NEGATIVE_TICK, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(25u128), BASE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: None, expected_error: None, @@ -2549,14 +2546,14 @@ fn test_claim_order() { order_id: 1, tick_id: valid_tick_id, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(100u128), BASE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: None, expected_error: None, @@ -2583,14 +2580,14 @@ fn test_claim_order() { order_id: 0, tick_id: 1, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(5u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: None, expected_error: Some(ContractError::InvalidTickId { tick_id: 1 }), @@ -2617,14 +2614,14 @@ fn test_claim_order() { order_id: 1, tick_id: valid_tick_id, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(5u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: None, expected_error: Some(ContractError::OrderNotFound { @@ -2650,14 +2647,14 @@ fn test_claim_order() { order_id: 0, tick_id: valid_tick_id, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(5u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: None, expected_error: Some(ContractError::OrderNotFound { @@ -2680,14 +2677,14 @@ fn test_claim_order() { order_id: 0, tick_id: valid_tick_id, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(5u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: None, expected_error: Some(ContractError::ZeroClaim), @@ -2718,14 +2715,14 @@ fn test_claim_order() { order_id: 1, tick_id: valid_tick_id, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(5u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: None, expected_error: Some(ContractError::ZeroClaim), @@ -2757,18 +2754,53 @@ fn test_claim_order() { order_id: 0, tick_id: valid_tick_id, - expected_bank_msg: SubMsg::reply_on_error( + expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), amount: vec![coin_u256(Uint256::from(5u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, - ), + )), expected_bounty_msg: None, expected_order_state: None, expected_error: Some(ContractError::ZeroClaim), }, + ClaimOrderTestCase { + name: "zero claim amount success case", + sender: sender.clone(), + operations: vec![ + OrderOperation::PlaceLimit(LimitOrder::new( + LARGE_NEGATIVE_TICK, + 0, + OrderDirection::Bid, + sender.clone(), + Uint128::from(1u128), + Decimal256::zero(), + None, + )), + OrderOperation::PlaceLimit(LimitOrder::new( + LARGE_NEGATIVE_TICK, + 1, + OrderDirection::Bid, + sender.clone(), + Uint128::from(1u128), + Decimal256::zero(), + None, + )), + OrderOperation::RunMarket(MarketOrder::new( + Uint128::from(1u128), + OrderDirection::Ask, + sender.clone(), + )), + ], + order_id: 0, + tick_id: LARGE_NEGATIVE_TICK, + expected_bank_msg:None, + expected_bounty_msg: None, + expected_order_state: None, + expected_error: None, + }, ]; for test in test_cases { @@ -2807,12 +2839,16 @@ fn test_claim_order() { let res = res.unwrap(); // Assert that the generated bank and bounty messages are as expected - assert_eq!( - res.messages[0], - test.expected_bank_msg, - "{}", + if let Some(bank_msg) = test.expected_bank_msg.clone() { + assert_eq!( + res.messages[0], + bank_msg, + "{}", format_test_name(test.name) ); + } else { + assert_eq!((res.messages).len(), 0, "{}", format_test_name(test.name)); + } if let Some(expected_bounty_msg) = test.expected_bounty_msg { // Bounty message expected @@ -2824,8 +2860,9 @@ fn test_claim_order() { format_test_name(test.name) ); } else { + let expected_message_len = if test.expected_bank_msg.is_some() { 1 } else { 0 }; // No bounty message expected - assert_eq!((res.messages).len(), 1, "{}", format_test_name(test.name)); + assert_eq!((res.messages).len(), expected_message_len, "{}", format_test_name(test.name)); } // Check order in state From ae2245c3e69e71306918189dd6a4b2942bdd321b Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Wed, 26 Jun 2024 16:31:56 +0100 Subject: [PATCH 41/98] refactor: stopped removing claimed orders in fuzz tests unless they are fully claimed --- .../src/tests/e2e/cases/test_fuzz.rs | 19 ++++++++++++++----- .../sumtree-orderbook/src/tests/test_order.rs | 1 - 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index 6da04b7..2fa5957 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -77,8 +77,8 @@ fn test_order_fuzz_mixed() { } #[test] -fn test_order_fuzz_single_tick() { - run_fuzz_mixed(0000, (0, 0)); +fn test_order_fuzz_mixed_single_tick() { + run_fuzz_mixed(1000, (0, 0)); } /// Runs a linear fuzz test with the following steps @@ -378,11 +378,20 @@ impl MixedFuzzOperation { username.as_str() }; - // Remove the order once we know its claimable - orders.remove(&order_id).unwrap(); // Claim the order match orders::claim_success(t, claimant, &username, tick_id, order_id) { - Ok(_) => Ok(true), + Ok(_) => { + let order = t.contract.get_order( + t.accounts[&username].address(), + tick_id, + order_id, + ); + if order.is_none() { + // Remove the order once we know its claimable + orders.remove(&order_id).unwrap(); + } + Ok(true) + } Err(e) => { panic!("{e}") } diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index d9ebfeb..dbb65ad 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -4231,7 +4231,6 @@ fn test_cancelled_orders() { } let root = get_or_init_root_node(deps.as_mut().storage, 0, OrderDirection::Bid).unwrap(); - print_tree("", "", &root, &deps.as_ref()); OrderOperation::PlaceLimit(LimitOrder::new(0, 10, OrderDirection::Bid, sender.clone(), Uint128::from(1u128), Decimal256::zero(), None)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); OrderOperation::RunMarket(MarketOrder::new(Uint128::from(1u128).checked_mul(Uint128::from(4u128)).unwrap(), OrderDirection::Ask, sender.clone())).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); From f35734a8d6982e2563ef4d98ab760e6304ee5f1e Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Wed, 26 Jun 2024 16:55:18 +0100 Subject: [PATCH 42/98] tests: fixed any failing tests --- contracts/sumtree-orderbook/src/order.rs | 12 --- .../src/sumtree/test/test_fuzz.rs | 2 +- .../src/sumtree/test/test_tree.rs | 97 +------------------ .../src/tests/e2e/cases/test_fuzz.rs | 24 +---- .../src/tests/e2e/test_env.rs | 16 ++- .../sumtree-orderbook/src/tests/test_order.rs | 20 ++-- 6 files changed, 23 insertions(+), 148 deletions(-) diff --git a/contracts/sumtree-orderbook/src/order.rs b/contracts/sumtree-orderbook/src/order.rs index ccba96b..37257a6 100644 --- a/contracts/sumtree-orderbook/src/order.rs +++ b/contracts/sumtree-orderbook/src/order.rs @@ -43,18 +43,6 @@ pub fn place_limit( ContractError::InvalidQuantity { quantity } ); - let tick_price = tick_to_price(tick_id)?; - let amount_out = amount_to_value( - order_direction, - quantity, - tick_price, - RoundingDirection::Down, - )?; - ensure!( - !amount_out.is_zero(), - ContractError::InvalidQuantity { quantity } - ); - // If applicable, ensure claim_bounty is between 0 and 0.01. // We set a conservative upper bound of 1% for claim bounties as a guardrail. if let Some(claim_bounty_value) = claim_bounty { diff --git a/contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs b/contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs index ec7a3ea..6d40eaf 100644 --- a/contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/sumtree/test/test_fuzz.rs @@ -66,7 +66,7 @@ fn test_fuzz_insert() { // If the inserted node's start ETAS is <= the target ETAS, we add the node's amount // to our expected prefix sum. - if node.get_min_range() <= target_etas.checked_add(expected_prefix_sum).unwrap() { + if node.get_min_range() <= target_etas { expected_prefix_sum = expected_prefix_sum.checked_add(node.get_value()).unwrap(); } diff --git a/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs b/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs index 73e3062..3567cf3 100644 --- a/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs +++ b/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs @@ -1,5 +1,5 @@ use crate::sumtree::node::{generate_node_id, NodeType, TreeNode, NODES}; -use crate::sumtree::test::test_node::{assert_internal_values, print_tree}; +use crate::sumtree::test::test_node::assert_internal_values; use crate::sumtree::tree::{get_or_init_root_node, get_prefix_sum, get_root_node, TREE}; use crate::types::OrderDirection; use cosmwasm_std::Storage; @@ -206,101 +206,6 @@ fn test_get_prefix_sum_valid() { } } -struct TestInvariantPrefixCase { - name: &'static str, - nodes: Vec, - new_nodes: Vec, - target_etas: Decimal256, - expected_sum: Decimal256, -} - -#[test] -fn test_fuzz_invariant_prefix() { - let test_cases = vec![ - // TestInvariantPrefixCase { - // name: "Single node, target ETAS equal to node ETAS", - // nodes: vec![NodeType::leaf_uint256(10u128, 5u128)], - // new_nodes: vec![NodeType::leaf_uint256(20u128, 5u128)], - // target_etas: Decimal256::from_ratio(10u128, 1u128), - // expected_sum: Decimal256::from_ratio(5u128, 1u128), - // }, - TestInvariantPrefixCase { - name: "Single node, target ETAS equal to node ETAS", - nodes: vec![ - NodeType::leaf_uint256(4u128, 8u128), - NodeType::leaf_uint256(50u128, 5u128), - NodeType::leaf_uint256(152u128, 4u128), - ], - new_nodes: vec![NodeType::leaf_uint256(169u128, 2u128)], - target_etas: Decimal256::from_ratio(141u128, 1u128), - expected_sum: Decimal256::from_ratio(13u128, 1u128), - }, - TestInvariantPrefixCase { - name: "Multiple nodes, etas overlaps with highest node", - nodes: vec![ - NodeType::leaf_uint256(4u128, 8u128), - NodeType::leaf_uint256(50u128, 5u128), - NodeType::leaf_uint256(152u128, 4u128), - ], - new_nodes: vec![NodeType::leaf_uint256(156u128, 2u128)], - target_etas: Decimal256::from_ratio(152u128, 1u128), - expected_sum: Decimal256::from_ratio(17u128, 1u128), - }, - ]; - - let tick_id = 0; - let direction = OrderDirection::Ask; - - for test in test_cases { - let mut deps = mock_dependencies(); - - let mut tree = get_or_init_root_node(deps.as_mut().storage, tick_id, direction).unwrap(); - - for node in test.nodes { - tree = insert_and_refetch(deps.as_mut().storage, tick_id, direction, &node); - } - - let prefix_sum = get_prefix_sum( - deps.as_ref().storage, - tree.clone(), - test.target_etas, - Decimal256::zero(), - ) - .unwrap(); - print_tree("", test.name, &tree, &deps.as_ref()); - assert_eq!( - test.expected_sum, prefix_sum, - "{}: Expected prefix sum {}, got {}", - test.name, test.expected_sum, prefix_sum - ); - - let mut starting_etas = tree.get_max_range().checked_add(Decimal256::one()).unwrap(); - for node in test.new_nodes.iter() { - tree = insert_and_refetch(deps.as_mut().storage, tick_id, direction, node); - starting_etas = starting_etas - .checked_add(Decimal256::from_ratio(10u128, 1u128)) - .unwrap(); - println!("inserting {}", node); - print_tree("", test.name, &tree, &deps.as_ref()); - - let prefix_sum = get_prefix_sum( - deps.as_ref().storage, - tree.clone(), - test.target_etas, - Decimal256::zero(), - ) - .unwrap(); - assert!( - test.expected_sum <= prefix_sum, - "{}: Expected prefix sum {}, got {}", - test.name, - test.expected_sum, - prefix_sum - ); - } - } -} - // Inserts node into tree at ( tick_id, direction) and return the updated root pub fn insert_and_refetch( storage: &mut dyn Storage, diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index 2fa5957..3af01c4 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::time::{Duration, SystemTime}; -use cosmwasm_std::{Coin, Decimal, Uint256}; +use cosmwasm_std::{Coin, Decimal}; use cosmwasm_std::{Decimal256, Uint128}; use osmosis_test_tube::{Account, Module, OsmosisTestApp}; use rand::seq::SliceRandom; @@ -63,7 +63,7 @@ fn test_order_fuzz_linear_large_all_cancelled_orders_small_range() { #[test] fn test_order_fuzz_linear_single_tick() { - run_fuzz_linear(1000, (0, 0), 0.2); + run_fuzz_linear(2000, (0, 0), 0.2); } #[test] @@ -78,7 +78,7 @@ fn test_order_fuzz_mixed() { #[test] fn test_order_fuzz_mixed_single_tick() { - run_fuzz_mixed(1000, (0, 0)); + run_fuzz_mixed(5000, (0, 0)); } /// Runs a linear fuzz test with the following steps @@ -518,26 +518,12 @@ fn place_random_limit( }; // Select a random tick from the provided range let tick_id = rng.gen_range(tick_range.0..=tick_range.1); - // Convert the tick to a price - let price = tick_to_price(tick_id).unwrap(); - // Calculate the minimum amount of the denom that can be bought at the given price - let min = Uint128::try_from( - amount_to_value( - order_direction.opposite(), - Uint128::one(), - price, - RoundingDirection::Up, - ) - .unwrap() - .min(Uint256::from(Uint128::MAX)), - ) - .unwrap(); // Add the user account with the appropriate amount of the denom t.add_account( username, vec![ - Coin::new(quantity.u128().max(min.u128()), expected_denom), + Coin::new(quantity.u128(), expected_denom), Coin::new(1000000000000000u128, "uosmo"), ], ); @@ -555,7 +541,7 @@ fn place_random_limit( t, tick_id, order_direction, - quantity.max(min), + quantity, claim_bounty, username, ) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs index c6e3ff9..73a5140 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs @@ -4,8 +4,8 @@ use crate::{ constants::{MAX_TICK, MIN_TICK}, msg::{ AuthExecuteMsg, AuthQueryMsg, DenomsResponse, ExecuteMsg, GetTotalPoolLiquidityResponse, - InstantiateMsg, OrdersResponse, QueryMsg, SudoMsg, TickIdAndState, TickUnrealizedCancels, - TickUnrealizedCancelsByIdResponse, TicksResponse, + GetUnrealizedCancelsResponse, InstantiateMsg, OrdersResponse, QueryMsg, SudoMsg, + TickIdAndState, TickUnrealizedCancels, TicksResponse, }, types::{LimitOrder, OrderDirection}, ContractError, @@ -233,7 +233,7 @@ impl<'a> OrderbookContract<'a> { .unwrap() } - pub fn set_admin(&self, app: &OsmosisTestApp, admin: Addr) { + pub fn _set_admin(&self, app: &OsmosisTestApp, admin: Addr) { app.wasm_sudo( &self.contract_addr, SudoMsg::TransferAdmin { new_admin: admin }, @@ -243,7 +243,7 @@ impl<'a> OrderbookContract<'a> { println!("admin_set: {:?}", admin); } - pub fn set_maker_fee( + pub fn _set_maker_fee( &self, signer: &SigningAccount, maker_fee: Decimal256, @@ -276,8 +276,8 @@ impl<'a> OrderbookContract<'a> { .unwrap(); let tick = ticks.first().unwrap().tick_state.clone(); let tick_values: crate::types::TickValues = tick.get_values(order.order_direction); - let TickUnrealizedCancelsByIdResponse { ticks } = self - .query(&QueryMsg::TickUnrealizedCancelsById { + let GetUnrealizedCancelsResponse { ticks } = self + .query(&QueryMsg::GetUnrealizedCancels { tick_ids: vec![order.tick_id], }) .unwrap(); @@ -287,9 +287,7 @@ impl<'a> OrderbookContract<'a> { let cancelled_amount = match order.order_direction { OrderDirection::Bid => unrealized_cancels.bid_unrealized_cancels, OrderDirection::Ask => unrealized_cancels.ask_unrealized_cancels, - } - .checked_sub(tick_values.cumulative_realized_cancels) - .unwrap(); + }; let synced_etas = tick_values .effective_total_amount_swapped diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index dbb65ad..b68fc42 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, str::FromStr}; use crate::{ constants::{MAX_TICK, MIN_TICK}, error::ContractError, order::*, orderbook::*, state::*, sumtree::{ - node::{NodeType, TreeNode}, test::{test_fuzz::assert_sumtree_invariants, test_node::print_tree}, tree::{get_or_init_root_node, get_prefix_sum, get_root_node} + node::{NodeType, TreeNode}, test::test_fuzz::assert_sumtree_invariants, tree::{get_or_init_root_node, get_prefix_sum, get_root_node} }, tests::{mock_querier::mock_dependencies_custom, test_utils::{decimal256_from_u128, place_multiple_limit_orders}}, types::{ @@ -4230,8 +4230,6 @@ fn test_cancelled_orders() { } - let root = get_or_init_root_node(deps.as_mut().storage, 0, OrderDirection::Bid).unwrap(); - OrderOperation::PlaceLimit(LimitOrder::new(0, 10, OrderDirection::Bid, sender.clone(), Uint128::from(1u128), Decimal256::zero(), None)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); OrderOperation::RunMarket(MarketOrder::new(Uint128::from(1u128).checked_mul(Uint128::from(4u128)).unwrap(), OrderDirection::Ask, sender.clone())).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); @@ -4290,18 +4288,18 @@ fn test_random_orders_single_tick() { if order_ids.is_empty() { continue; } - let order_id = order_ids[rng.gen_range(0..order_ids.len())].clone(); + let order_id = order_ids[rng.gen_range(0..order_ids.len())]; println!("order_id: {}", order_id); - let order = orders().load(deps.as_mut().storage, &(tick_id, order_id)).unwrap(); + let order = orders().load(deps.as_mut().storage, &(tick_id, *order_id)).unwrap(); let root = get_or_init_root_node(deps.as_mut().storage, tick_id, order.order_direction).unwrap(); if !root.get_value().is_zero() { println!("checking tree"); assert_sumtree_invariants(deps.as_ref(), &root, "test_random_orders_single_tick"); } - match OrderOperation::Cancel((tick_id, order_id)).run(deps.as_mut(), env.clone(), info.clone()) { + match OrderOperation::Cancel((tick_id, *order_id)).run(deps.as_mut(), env.clone(), info.clone()) { Ok(_) => { println!("order cancelled"); - placed_orders.remove(&order_id); + placed_orders.remove(order_id); } Err(e) => { match e { @@ -4322,10 +4320,10 @@ fn test_random_orders_single_tick() { if order_ids.is_empty() { continue; } - let order_id = order_ids[rng.gen_range(0..order_ids.len())].clone(); + let order_id = order_ids[rng.gen_range(0..order_ids.len())]; println!("order_id: {}", order_id); - let order = orders().load(deps.as_mut().storage, &(tick_id, order_id)).unwrap(); + let order = orders().load(deps.as_mut().storage, &(tick_id, *order_id)).unwrap(); let tick_state = TICK_STATE.load(deps.as_mut().storage, tick_id).unwrap(); let tick_values = tick_state.get_values(order.order_direction); let root = get_or_init_root_node(deps.as_mut().storage, tick_id, order.order_direction).unwrap(); @@ -4347,10 +4345,10 @@ fn test_random_orders_single_tick() { continue; } - match OrderOperation::Claim((tick_id, order_id)).run(deps.as_mut(), env.clone(), info.clone()) { + match OrderOperation::Claim((tick_id, *order_id)).run(deps.as_mut(), env.clone(), info.clone()) { Ok(_) => { println!("succesfully claimed limit"); - placed_orders.remove(&order_id); + placed_orders.remove(order_id); } Err(e) => { match e { From e7eafb0834b211700177ee4d5dcca0b0b8a80665 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Thu, 27 Jun 2024 14:13:41 +0100 Subject: [PATCH 43/98] Update contracts/sumtree-orderbook/src/sumtree/tree.rs Co-authored-by: Alpo <62043214+AlpinYukseloglu@users.noreply.github.com> --- contracts/sumtree-orderbook/src/sumtree/tree.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/contracts/sumtree-orderbook/src/sumtree/tree.rs b/contracts/sumtree-orderbook/src/sumtree/tree.rs index 680188d..efe4190 100644 --- a/contracts/sumtree-orderbook/src/sumtree/tree.rs +++ b/contracts/sumtree-orderbook/src/sumtree/tree.rs @@ -97,11 +97,16 @@ fn prefix_sum_walk( // -- Resync Condition -- - // To prevent requiring a resync there needs to be a condition that covers the case that - // when realizing the left node the new ETAS is enough to realize the right node (to some extent) - // To cover this we can determine how much of the left node has been realized, using this we can then determine - // if realizing what is unrealized from the left node will result in a new ETAS that is enough to realize the - // right node (to some extent) + // To prevent requiring one or multiple resyncs, we add an optimization step that dynamically batches multiple + // syncs into one when applicable. This is achieved through an early check that is triggered in the case where + // all orders up to a certain point are either filled or canceled, as in this context we do not care about the order + // in which cancels are realized and can simply "batch realize" all of them. + // + // This case is characterized by when the amount filled on the current tick + the unrealized cancellations in the + // left child of a node pushes ETAS into the range of the right child node (i.e. where the next order in line, after + // the sync is complete, is guaranteed to be another cancel). In this case, we count the left child as fully realized + // and roll the newly realized portion into the target ETAS. This is functionally equivalent to batching multiple + // syncs into one. if left_child.is_some() && right_child.is_some() { let left_child = left_child.clone().unwrap(); let right_child = right_child.clone().unwrap(); From f43f41f5e8c32e4b94fb29dd1a27ded56a9c52e4 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Thu, 27 Jun 2024 14:13:49 +0100 Subject: [PATCH 44/98] Update contracts/sumtree-orderbook/src/sumtree/tree.rs Co-authored-by: Alpo <62043214+AlpinYukseloglu@users.noreply.github.com> --- contracts/sumtree-orderbook/src/sumtree/tree.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contracts/sumtree-orderbook/src/sumtree/tree.rs b/contracts/sumtree-orderbook/src/sumtree/tree.rs index efe4190..84e69d5 100644 --- a/contracts/sumtree-orderbook/src/sumtree/tree.rs +++ b/contracts/sumtree-orderbook/src/sumtree/tree.rs @@ -111,7 +111,11 @@ fn prefix_sum_walk( let left_child = left_child.clone().unwrap(); let right_child = right_child.clone().unwrap(); - // Calculate what the sum is before realizing the current node + // `sum_at_node` corresponds to everything to the left and in the current node. + // We don't know which component of the current node, if any, will be included, so we remove the whole thing. + // + // Sanity check: for the root node, this will be 0, since the "current node" is the root and includes the whole + // tree (so when it is removed, there is nothing left) let sum_at_node = current_sum.checked_sub(node.get_value())?; // Calculate how much of the node has been realized in a previous sync let diff_at_node = prev_sum.saturating_sub(sum_at_node); From dd196a0e06567c47e8ebded78711b7832c258ede Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Thu, 27 Jun 2024 14:13:56 +0100 Subject: [PATCH 45/98] Update contracts/sumtree-orderbook/src/sumtree/tree.rs Co-authored-by: Alpo <62043214+AlpinYukseloglu@users.noreply.github.com> --- contracts/sumtree-orderbook/src/sumtree/tree.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/contracts/sumtree-orderbook/src/sumtree/tree.rs b/contracts/sumtree-orderbook/src/sumtree/tree.rs index 84e69d5..8b6b8e4 100644 --- a/contracts/sumtree-orderbook/src/sumtree/tree.rs +++ b/contracts/sumtree-orderbook/src/sumtree/tree.rs @@ -117,7 +117,13 @@ fn prefix_sum_walk( // Sanity check: for the root node, this will be 0, since the "current node" is the root and includes the whole // tree (so when it is removed, there is nothing left) let sum_at_node = current_sum.checked_sub(node.get_value())?; - // Calculate how much of the node has been realized in a previous sync + // Calculate the amount of cumulative realized cancellations *below the current node* at the end of the + // previous sync. + // + // Recall that `prev_sum` is the cumulative *global* amount that was realized at the end of the previous sync. + // + // Concretely, if this value is ever nonzero for a node, the amount corresponds exactly to the amount realized + // below the node at the end of the previous sync. In all other cases, it will snap to zero due to the saturating sub. let diff_at_node = prev_sum.saturating_sub(sum_at_node); // Calculate how much of the left node is unrealized let unrealized_from_left = left_child.get_value().saturating_sub(diff_at_node); From cfb29aba64eb2e883da61912d997adc22fea2b5f Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Thu, 27 Jun 2024 14:14:05 +0100 Subject: [PATCH 46/98] Update contracts/sumtree-orderbook/src/sumtree/tree.rs Co-authored-by: Alpo <62043214+AlpinYukseloglu@users.noreply.github.com> --- contracts/sumtree-orderbook/src/sumtree/tree.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/contracts/sumtree-orderbook/src/sumtree/tree.rs b/contracts/sumtree-orderbook/src/sumtree/tree.rs index 8b6b8e4..98b96e1 100644 --- a/contracts/sumtree-orderbook/src/sumtree/tree.rs +++ b/contracts/sumtree-orderbook/src/sumtree/tree.rs @@ -125,7 +125,15 @@ fn prefix_sum_walk( // Concretely, if this value is ever nonzero for a node, the amount corresponds exactly to the amount realized // below the node at the end of the previous sync. In all other cases, it will snap to zero due to the saturating sub. let diff_at_node = prev_sum.saturating_sub(sum_at_node); - // Calculate how much of the left node is unrealized + // `unrealized_from_left` is the amount in the left child that remained unrealized at the end of the previous + // sync. + // + // If the left child is fully realized then the subtraction here will either be zero (if none of the right child is + // realized) or negative (if some of the right child is realized), since `diff_at_node` = amount realized below left + // and below right children. Saturating sub will ensure both of these cases snap to zero. + // + // Thus, if this value is ever nonzero, it means that the left child had some unrealized cancels in it, and the + // amount corresponds exactly to `unrealized_from_left`. let unrealized_from_left = left_child.get_value().saturating_sub(diff_at_node); // Calculate the new ETAS after realizing what is unrealized from the left node let new_etas = target_etas.checked_add(unrealized_from_left)?; From 801d28e98ccab5617ba6c471c292cfac5771fb1c Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Thu, 27 Jun 2024 14:14:12 +0100 Subject: [PATCH 47/98] Update contracts/sumtree-orderbook/src/sumtree/tree.rs Co-authored-by: Alpo <62043214+AlpinYukseloglu@users.noreply.github.com> --- contracts/sumtree-orderbook/src/sumtree/tree.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contracts/sumtree-orderbook/src/sumtree/tree.rs b/contracts/sumtree-orderbook/src/sumtree/tree.rs index 98b96e1..63affe4 100644 --- a/contracts/sumtree-orderbook/src/sumtree/tree.rs +++ b/contracts/sumtree-orderbook/src/sumtree/tree.rs @@ -136,6 +136,11 @@ fn prefix_sum_walk( // amount corresponds exactly to `unrealized_from_left`. let unrealized_from_left = left_child.get_value().saturating_sub(diff_at_node); // Calculate the new ETAS after realizing what is unrealized from the left node + // + // Instead of doing this manually (and expensively) by triggering a resync, we simply add `unrealized_from_left` + // to the running target ETAS value. This is functionally equivalent to simulating realizing the + // remainder of the left child. As a sanity check, if it was already realized, then `unrealized_from_left` + // would be zero and the target ETAS would remain unchanged. let new_etas = target_etas.checked_add(unrealized_from_left)?; // if the new ETAS is greater than or equal to the right child's min range, we can walk right From 03a137cccbd9de060ddfa9be4be82ec4aeefdbcb Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Thu, 27 Jun 2024 14:14:22 +0100 Subject: [PATCH 48/98] Update contracts/sumtree-orderbook/src/sumtree/tree.rs Co-authored-by: Alpo <62043214+AlpinYukseloglu@users.noreply.github.com> --- contracts/sumtree-orderbook/src/sumtree/tree.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contracts/sumtree-orderbook/src/sumtree/tree.rs b/contracts/sumtree-orderbook/src/sumtree/tree.rs index 63affe4..7d2c8d1 100644 --- a/contracts/sumtree-orderbook/src/sumtree/tree.rs +++ b/contracts/sumtree-orderbook/src/sumtree/tree.rs @@ -143,8 +143,12 @@ fn prefix_sum_walk( // would be zero and the target ETAS would remain unchanged. let new_etas = target_etas.checked_add(unrealized_from_left)?; - // if the new ETAS is greater than or equal to the right child's min range, we can walk right + // If the new ETAS is greater than or equal to the right child's min range, we can walk right // as the left node MUST be realizable given the invariants of the sumtree mechanism + // + // Intuition: If all swaps and cancels from the left child put us in the cancel territory of the right side, then we + // don't care about the ordering of the swaps and cancels on the left. We know that all the cancels need to be + // realized, so we batch realize them by adding *their unrealized portion* to our target ETAS and walking right. if new_etas >= right_child.get_min_range() { return prefix_sum_walk(storage, &right_child, current_sum, new_etas, prev_sum); } From c390f7e2479f25ad8ca4fdda43d26df0250e10d5 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Thu, 27 Jun 2024 14:20:27 +0100 Subject: [PATCH 49/98] fix: add required input to prefix sum in get_unrealized_cancels --- contracts/sumtree-orderbook/src/query.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/sumtree-orderbook/src/query.rs b/contracts/sumtree-orderbook/src/query.rs index 4384841..06683bf 100644 --- a/contracts/sumtree-orderbook/src/query.rs +++ b/contracts/sumtree-orderbook/src/query.rs @@ -8,8 +8,8 @@ use crate::{ error::ContractResult, msg::{ CalcOutAmtGivenInResponse, DenomsResponse, GetSwapFeeResponse, - GetTotalPoolLiquidityResponse, GetUnrealizedCancelsResponse, OrdersResponse, SpotPriceResponse, - TickIdAndState, TickUnrealizedCancels, TicksResponse, UnrealizedCancels, + GetTotalPoolLiquidityResponse, GetUnrealizedCancelsResponse, OrdersResponse, + SpotPriceResponse, TickIdAndState, TickUnrealizedCancels, TicksResponse, UnrealizedCancels, }, order, state::{ @@ -252,6 +252,7 @@ fn get_unrealized_cancels( deps.storage, root_node, tick_values.effective_total_amount_swapped, + tick_values.cumulative_realized_cancels, )?; total_realized_cancels.saturating_sub(tick_values.cumulative_realized_cancels) From 6637ec7908ca8366f18214b3b92f24ca5c67e97b Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Thu, 27 Jun 2024 14:25:27 +0100 Subject: [PATCH 50/98] fix: fixed spot price calculation to account for direction --- contracts/sumtree-orderbook/src/query.rs | 10 ++++++---- contracts/sumtree-orderbook/src/tests/test_query.rs | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/contracts/sumtree-orderbook/src/query.rs b/contracts/sumtree-orderbook/src/query.rs index 4384841..9b74378 100644 --- a/contracts/sumtree-orderbook/src/query.rs +++ b/contracts/sumtree-orderbook/src/query.rs @@ -8,8 +8,8 @@ use crate::{ error::ContractResult, msg::{ CalcOutAmtGivenInResponse, DenomsResponse, GetSwapFeeResponse, - GetTotalPoolLiquidityResponse, GetUnrealizedCancelsResponse, OrdersResponse, SpotPriceResponse, - TickIdAndState, TickUnrealizedCancels, TicksResponse, UnrealizedCancels, + GetTotalPoolLiquidityResponse, GetUnrealizedCancelsResponse, OrdersResponse, + SpotPriceResponse, TickIdAndState, TickUnrealizedCancels, TicksResponse, UnrealizedCancels, }, order, state::{ @@ -17,7 +17,7 @@ use crate::{ }, sudo::ensure_swap_fee, sumtree::tree::{get_prefix_sum, get_root_node}, - tick_math::tick_to_price, + tick_math::{amount_to_value, tick_to_price, RoundingDirection}, types::{FilterOwnerOrders, LimitOrder, MarketOrder, OrderDirection, TickState}, ContractError, }; @@ -59,8 +59,10 @@ pub(crate) fn spot_price( // Generate spot price based on current active tick for desired order direction let price = tick_to_price(next_tick)?; + let spot_price = amount_to_value(direction, Uint128::one(), price, RoundingDirection::Down)?; + Ok(SpotPriceResponse { - spot_price: Decimal::from_str(&price.to_string())?, + spot_price: Decimal::from_str(&spot_price.to_string())?, }) } diff --git a/contracts/sumtree-orderbook/src/tests/test_query.rs b/contracts/sumtree-orderbook/src/tests/test_query.rs index e2d3732..4c1a675 100644 --- a/contracts/sumtree-orderbook/src/tests/test_query.rs +++ b/contracts/sumtree-orderbook/src/tests/test_query.rs @@ -253,7 +253,7 @@ fn test_query_spot_price() { ], base_denom: QUOTE_DENOM.to_string(), quote_denom: BASE_DENOM.to_string(), - expected_price: Decimal::percent(50), + expected_price: Decimal::percent(200), expected_error: None, }, SpotPriceTestCase { From 9d6f82aa5eeb5dfddc79153d8798a6e39be13a3b Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Thu, 27 Jun 2024 15:02:23 +0100 Subject: [PATCH 51/98] fix: inverted base/quote denoms for spot price to be correct --- contracts/sumtree-orderbook/src/query.rs | 11 ++- .../sumtree-orderbook/src/tests/test_query.rs | 88 ++++++++++++++----- 2 files changed, 74 insertions(+), 25 deletions(-) diff --git a/contracts/sumtree-orderbook/src/query.rs b/contracts/sumtree-orderbook/src/query.rs index 9b74378..9b7511f 100644 --- a/contracts/sumtree-orderbook/src/query.rs +++ b/contracts/sumtree-orderbook/src/query.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -use cosmwasm_std::{coin, ensure, Addr, Coin, Decimal, Decimal256, Deps, Order, Uint128}; +use cosmwasm_std::{coin, ensure, Addr, Coin, Decimal, Decimal256, Deps, Fraction, Order, Uint128}; use cw_storage_plus::Bound; use crate::{ @@ -17,7 +17,7 @@ use crate::{ }, sudo::ensure_swap_fee, sumtree::tree::{get_prefix_sum, get_root_node}, - tick_math::{amount_to_value, tick_to_price, RoundingDirection}, + tick_math::tick_to_price, types::{FilterOwnerOrders, LimitOrder, MarketOrder, OrderDirection, TickState}, ContractError, }; @@ -48,7 +48,7 @@ pub(crate) fn spot_price( // Fetch orderbook to retrieve tick info let orderbook = ORDERBOOK.load(deps.storage)?; // Determine the order direction by denom pairing - let direction = orderbook.direction_from_pair(quote_asset_denom, base_asset_denom)?; + let direction = orderbook.direction_from_pair(base_asset_denom, quote_asset_denom)?; // Determine next tick based on desired order direction let next_tick = match direction { @@ -59,7 +59,10 @@ pub(crate) fn spot_price( // Generate spot price based on current active tick for desired order direction let price = tick_to_price(next_tick)?; - let spot_price = amount_to_value(direction, Uint128::one(), price, RoundingDirection::Down)?; + let spot_price = match direction { + OrderDirection::Ask => price.inv().unwrap(), + OrderDirection::Bid => price, + }; Ok(SpotPriceResponse { spot_price: Decimal::from_str(&spot_price.to_string())?, diff --git a/contracts/sumtree-orderbook/src/tests/test_query.rs b/contracts/sumtree-orderbook/src/tests/test_query.rs index 4c1a675..2941a86 100644 --- a/contracts/sumtree-orderbook/src/tests/test_query.rs +++ b/contracts/sumtree-orderbook/src/tests/test_query.rs @@ -5,7 +5,7 @@ use cosmwasm_std::{ }; use crate::{ - constants::EXPECTED_SWAP_FEE, + constants::{EXPECTED_SWAP_FEE, MIN_TICK}, orderbook::create_orderbook, query, state::IS_ACTIVE, @@ -45,8 +45,8 @@ fn test_query_spot_price() { Decimal256::zero(), None, ))], - base_denom: BASE_DENOM.to_string(), - quote_denom: QUOTE_DENOM.to_string(), + base_denom: QUOTE_DENOM.to_string(), + quote_denom: BASE_DENOM.to_string(), expected_price: Decimal::one(), expected_error: None, }, @@ -81,8 +81,8 @@ fn test_query_spot_price() { None, )), ], - base_denom: BASE_DENOM.to_string(), - quote_denom: QUOTE_DENOM.to_string(), + base_denom: QUOTE_DENOM.to_string(), + quote_denom: BASE_DENOM.to_string(), expected_price: Decimal::one(), expected_error: None, }, @@ -108,8 +108,8 @@ fn test_query_spot_price() { None, )), ], - base_denom: BASE_DENOM.to_string(), - quote_denom: QUOTE_DENOM.to_string(), + base_denom: QUOTE_DENOM.to_string(), + quote_denom: BASE_DENOM.to_string(), expected_price: Decimal::one(), expected_error: None, }, @@ -140,8 +140,8 @@ fn test_query_spot_price() { sender.clone(), )), ], - base_denom: BASE_DENOM.to_string(), - quote_denom: QUOTE_DENOM.to_string(), + base_denom: QUOTE_DENOM.to_string(), + quote_denom: BASE_DENOM.to_string(), expected_price: Decimal::percent(200), expected_error: None, }, @@ -156,8 +156,8 @@ fn test_query_spot_price() { Decimal256::zero(), None, ))], - base_denom: QUOTE_DENOM.to_string(), - quote_denom: BASE_DENOM.to_string(), + base_denom: BASE_DENOM.to_string(), + quote_denom: QUOTE_DENOM.to_string(), expected_price: Decimal::one(), expected_error: None, }, @@ -192,8 +192,8 @@ fn test_query_spot_price() { None, )), ], - base_denom: QUOTE_DENOM.to_string(), - quote_denom: BASE_DENOM.to_string(), + base_denom: BASE_DENOM.to_string(), + quote_denom: QUOTE_DENOM.to_string(), expected_price: Decimal::one(), expected_error: None, }, @@ -219,8 +219,8 @@ fn test_query_spot_price() { None, )), ], - base_denom: QUOTE_DENOM.to_string(), - quote_denom: BASE_DENOM.to_string(), + base_denom: BASE_DENOM.to_string(), + quote_denom: QUOTE_DENOM.to_string(), expected_price: Decimal::one(), expected_error: None, }, @@ -251,11 +251,57 @@ fn test_query_spot_price() { sender.clone(), )), ], - base_denom: QUOTE_DENOM.to_string(), - quote_denom: BASE_DENOM.to_string(), + base_denom: BASE_DENOM.to_string(), + quote_denom: QUOTE_DENOM.to_string(), expected_price: Decimal::percent(200), expected_error: None, }, + SpotPriceTestCase { + name: "ASK: large positive tick", + pre_operations: vec![ + OrderOperation::PlaceLimit(LimitOrder::new( + LARGE_POSITIVE_TICK, + 0, + OrderDirection::Bid, + sender.clone(), + Uint128::MAX, + Decimal256::zero(), + None, + )), + OrderOperation::RunMarket(MarketOrder::new( + Uint128::from(100u128), + OrderDirection::Ask, + sender.clone(), + )), + ], + base_denom: BASE_DENOM.to_string(), + quote_denom: QUOTE_DENOM.to_string(), + expected_price: Decimal::percent(50), + expected_error: None, + }, + SpotPriceTestCase { + name: "ASK: max tick", + pre_operations: vec![ + OrderOperation::PlaceLimit(LimitOrder::new( + MIN_TICK, + 0, + OrderDirection::Bid, + sender.clone(), + Uint128::MAX, + Decimal256::zero(), + None, + )), + OrderOperation::RunMarket(MarketOrder::new( + Uint128::from(100u128), + OrderDirection::Ask, + sender.clone(), + )), + ], + base_denom: BASE_DENOM.to_string(), + quote_denom: QUOTE_DENOM.to_string(), + expected_price: Decimal::from_ratio(1000000000000u128, 1u128), + expected_error: None, + }, SpotPriceTestCase { name: "invalid: duplicate denom", pre_operations: vec![], @@ -274,8 +320,8 @@ fn test_query_spot_price() { quote_denom: QUOTE_DENOM.to_string(), expected_price: Decimal::percent(50), expected_error: Some(ContractError::InvalidPair { - token_in_denom: QUOTE_DENOM.to_string(), - token_out_denom: "notadenom".to_string(), + token_out_denom: QUOTE_DENOM.to_string(), + token_in_denom: "notadenom".to_string(), }), }, SpotPriceTestCase { @@ -285,8 +331,8 @@ fn test_query_spot_price() { quote_denom: "notadenom".to_string(), expected_price: Decimal::percent(50), expected_error: Some(ContractError::InvalidPair { - token_out_denom: BASE_DENOM.to_string(), - token_in_denom: "notadenom".to_string(), + token_in_denom: BASE_DENOM.to_string(), + token_out_denom: "notadenom".to_string(), }), }, ]; From 6c3636d7ecfb3a6997b21e6e4f145642dcf85ec2 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Thu, 27 Jun 2024 15:37:09 +0100 Subject: [PATCH 52/98] test: added more cases --- .../sumtree-orderbook/src/tests/test_query.rs | 79 +++++++++++++++---- 1 file changed, 62 insertions(+), 17 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/test_query.rs b/contracts/sumtree-orderbook/src/tests/test_query.rs index 2941a86..8eed520 100644 --- a/contracts/sumtree-orderbook/src/tests/test_query.rs +++ b/contracts/sumtree-orderbook/src/tests/test_query.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use cosmwasm_std::{ coin, testing::{mock_env, mock_info}, @@ -5,7 +7,7 @@ use cosmwasm_std::{ }; use crate::{ - constants::{EXPECTED_SWAP_FEE, MIN_TICK}, + constants::{EXPECTED_SWAP_FEE, MAX_TICK, MIN_TICK}, orderbook::create_orderbook, query, state::IS_ACTIVE, @@ -145,6 +147,38 @@ fn test_query_spot_price() { expected_price: Decimal::percent(200), expected_error: None, }, + SpotPriceTestCase { + name: "BID: max tick", + pre_operations: vec![OrderOperation::PlaceLimit(LimitOrder::new( + MAX_TICK, + 0, + OrderDirection::Ask, + sender.clone(), + Uint128::one(), + Decimal256::zero(), + None, + ))], + base_denom: QUOTE_DENOM.to_string(), + quote_denom: BASE_DENOM.to_string(), + expected_price: Decimal::from_ratio(340282300000000000000u128, 1u128), + expected_error: None, + }, + SpotPriceTestCase { + name: "BID: min tick", + pre_operations: vec![OrderOperation::PlaceLimit(LimitOrder::new( + MIN_TICK, + 0, + OrderDirection::Ask, + sender.clone(), + Uint128::one(), + Decimal256::zero(), + None, + ))], + base_denom: QUOTE_DENOM.to_string(), + quote_denom: BASE_DENOM.to_string(), + expected_price: Decimal::from_str("0.000000000001").unwrap(), + expected_error: None, + }, SpotPriceTestCase { name: "ASK: basic price 1 query", pre_operations: vec![OrderOperation::PlaceLimit(LimitOrder::new( @@ -281,22 +315,33 @@ fn test_query_spot_price() { }, SpotPriceTestCase { name: "ASK: max tick", - pre_operations: vec![ - OrderOperation::PlaceLimit(LimitOrder::new( - MIN_TICK, - 0, - OrderDirection::Bid, - sender.clone(), - Uint128::MAX, - Decimal256::zero(), - None, - )), - OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(100u128), - OrderDirection::Ask, - sender.clone(), - )), - ], + pre_operations: vec![OrderOperation::PlaceLimit(LimitOrder::new( + MAX_TICK, + 0, + OrderDirection::Bid, + sender.clone(), + Uint128::MAX, + Decimal256::zero(), + None, + ))], + base_denom: BASE_DENOM.to_string(), + quote_denom: QUOTE_DENOM.to_string(), + // At max tick the price is 2.9387365e-21 which is outside the range of the `Decimal` type + // As such the returned price is zero + expected_price: Decimal::zero(), + expected_error: None, + }, + SpotPriceTestCase { + name: "ASK: min tick", + pre_operations: vec![OrderOperation::PlaceLimit(LimitOrder::new( + MIN_TICK, + 0, + OrderDirection::Bid, + sender.clone(), + Uint128::MAX, + Decimal256::zero(), + None, + ))], base_denom: BASE_DENOM.to_string(), quote_denom: QUOTE_DENOM.to_string(), expected_price: Decimal::from_ratio(1000000000000u128, 1u128), From 06ea434219a60f676d2d0a154daa51b41e4a255a Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Thu, 27 Jun 2024 19:22:25 +0100 Subject: [PATCH 53/98] test: added more prefix sum test cases for batch realization --- .../src/sumtree/test/test_tree.rs | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs b/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs index 3567cf3..74b7086 100644 --- a/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs +++ b/contracts/sumtree-orderbook/src/sumtree/test/test_tree.rs @@ -167,6 +167,96 @@ fn test_get_prefix_sum_valid() { // 5 + 19 + 4 + 10 + 9 + 20 + 50 + 40 + 29 = 186 expected_sum: Decimal256::from_ratio(186u128, 1u128), }, + TestPrefixSumCase { + name: "batch realization case: cancels are back to back", + nodes: vec![ + NodeType::leaf_uint256(10u128, 10u128), + NodeType::leaf_uint256(20u128, 10u128), + NodeType::leaf_uint256(30u128, 10u128), + ], + target_etas: Decimal256::from_ratio(10u128, 1u128), + prev_sum: Decimal256::zero(), + expected_sum: Decimal256::from_ratio(30u128, 1u128), + }, + TestPrefixSumCase { + name: "batch realization case: gap between cancels that is filled", + nodes: vec![ + NodeType::leaf_uint256(10u128, 10u128), + NodeType::leaf_uint256(30u128, 10u128), + NodeType::leaf_uint256(40u128, 10u128), + ], + // Gap from 0-10 + Gap from 20-30 = 20 + target_etas: Decimal256::from_ratio(20u128, 1u128), + prev_sum: Decimal256::zero(), + expected_sum: Decimal256::from_ratio(30u128, 1u128), + }, + TestPrefixSumCase { + name: "batch realization case: partially realized left node", + nodes: vec![ + NodeType::leaf_uint256(10u128, 10u128), + NodeType::leaf_uint256(20u128, 10u128), + NodeType::leaf_uint256(30u128, 10u128), + NodeType::leaf_uint256(40u128, 10u128), + ], + target_etas: Decimal256::from_ratio(20u128, 1u128), + prev_sum: Decimal256::from_ratio(10u128, 1u128), + expected_sum: Decimal256::from_ratio(40u128, 1u128), + }, + TestPrefixSumCase { + name: "batch realization case: fully realized left node", + nodes: vec![ + NodeType::leaf_uint256(10u128, 10u128), + NodeType::leaf_uint256(20u128, 10u128), + NodeType::leaf_uint256(30u128, 10u128), + NodeType::leaf_uint256(40u128, 10u128), + ], + target_etas: Decimal256::from_ratio(20u128, 1u128), + prev_sum: Decimal256::from_ratio(20u128, 1u128), + // Left node is fully realized and ETAS is < right node min + // Hence only left node is included in prefix sum and 20 is returned + expected_sum: Decimal256::from_ratio(20u128, 1u128), + }, + TestPrefixSumCase { + name: "batch realization case: fully realized left node with gap", + nodes: vec![ + NodeType::leaf_uint256(10u128, 10u128), + NodeType::leaf_uint256(30u128, 10u128), + NodeType::leaf_uint256(50u128, 10u128), + NodeType::leaf_uint256(60u128, 10u128), + ], + target_etas: Decimal256::from_ratio(30u128, 1u128), + prev_sum: Decimal256::from_ratio(20u128, 1u128), + // Left node is fully realized and ETAS is < right node min + // Hence only left node is included in prefix sum and 20 is returned + expected_sum: Decimal256::from_ratio(20u128, 1u128), + }, + TestPrefixSumCase { + name: "batch realization case: partially realized left node with gap", + nodes: vec![ + NodeType::leaf_uint256(10u128, 10u128), + NodeType::leaf_uint256(30u128, 10u128), + NodeType::leaf_uint256(40u128, 10u128), + NodeType::leaf_uint256(50u128, 10u128), + ], + target_etas: Decimal256::from_ratio(30u128, 1u128), + prev_sum: Decimal256::from_ratio(10u128, 1u128), + // Left node is partially realized and realizing the remaining amount makes the target ETAS > 40 which causes both right leaves to be realized + expected_sum: Decimal256::from_ratio(40u128, 1u128), + }, + TestPrefixSumCase { + name: "batch realization case: partially realized left node with gap does not cause overlap", + nodes: vec![ + NodeType::leaf_uint256(10u128, 10u128), + NodeType::leaf_uint256(30u128, 10u128), + NodeType::leaf_uint256(50u128, 10u128), + NodeType::leaf_uint256(60u128, 10u128), + ], + target_etas: Decimal256::from_ratio(30u128, 1u128), + prev_sum: Decimal256::from_ratio(10u128, 1u128), + // Left node is partially realized and ETAS + Remaining Unrealized on Left is < right node min + // Hence only left node is included in prefix sum and 20 is returned + expected_sum: Decimal256::from_ratio(20u128, 1u128), + }, ]; for test in test_cases { From 6f9e3311a685aeabc5f6f8aeb1646e6aadd3e352 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Thu, 27 Jun 2024 19:53:11 +0100 Subject: [PATCH 54/98] test: added batch realization test cases --- .../sumtree-orderbook/src/tests/test_tick.rs | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/contracts/sumtree-orderbook/src/tests/test_tick.rs b/contracts/sumtree-orderbook/src/tests/test_tick.rs index f7b84a2..b6e6a00 100644 --- a/contracts/sumtree-orderbook/src/tests/test_tick.rs +++ b/contracts/sumtree-orderbook/src/tests/test_tick.rs @@ -333,6 +333,114 @@ fn test_sync_tick() { 1u128, ), }, + SyncTickTestCase { + name: "Batch realization: simple case", + // Tick with: + // * 100 units of available liquidity for bid + // * 50 units of unrealized cancellations for bid + initial_tick_bid_values: build_tick_values(100, 50), + initial_tick_ask_values: build_tick_values(0, 0), + + // Multiple unrealized cancels for both bid and ask + unrealized_cancels_bid: vec![ + NodeType::leaf_uint256(45u32, 25u32), + NodeType::leaf_uint256(10u32, 25u32), + ], + unrealized_cancels_ask: vec![], + + // Increment tick ETAS by 10 for bid and 40 for ask per iteration for 2 iterations + new_bid_etas_per_sync: Decimal256::from_ratio(10u128, 1u128), + new_ask_etas_per_sync: Decimal256::zero(), + num_syncs: 2, + + // By end of iteration 2 both bid nodes should be included. + expected_cumulative_realized_bid: Decimal256::from_ratio(50u128, 1u128), + expected_cumulative_realized_ask: Decimal256::zero(), + + // The new ETAS includes all the incremented amounts which represent fills, + // plus the amount of realized cancellations for both bid and ask + expected_new_bid_etas_post_sync: Decimal256::from_ratio( + (2u128 * 10u128) + 50u128, + 1u128, + ), + expected_new_ask_etas_post_sync: Decimal256::zero(), + }, + SyncTickTestCase { + name: "Batch realization: all but most right", + // Tick with: + // * 200 units of available liquidity for bid + // * 78 units of unrealized cancellations for bid + initial_tick_bid_values: build_tick_values(200, 78), + initial_tick_ask_values: build_tick_values(0, 0), + + // Multiple unrealized cancels for both bid and ask + unrealized_cancels_bid: vec![ + NodeType::leaf_uint256(180u32, 3u32), + NodeType::leaf_uint256(130u32, 15u32), + NodeType::leaf_uint256(45u32, 25u32), + NodeType::leaf_uint256(90u32, 10u32), + NodeType::leaf_uint256(10u32, 25u32), + ], + unrealized_cancels_ask: vec![], + + // Increment tick ETAS by 20 for bid per iteration for 4 iterations + new_bid_etas_per_sync: Decimal256::from_ratio(20u128, 1u128), + new_ask_etas_per_sync: Decimal256::zero(), + num_syncs: 4, + + // By end of iteration 4 there should be enough liquidity to realize everything except the last cancellation + // Hence total realized becomes 25+10+25+15 = 75 + expected_cumulative_realized_bid: Decimal256::from_ratio(75u128, 1u128), + expected_cumulative_realized_ask: Decimal256::zero(), + + // The new ETAS includes all the incremented amounts which represent fills, + // plus the amount of realized cancellations for both bid and ask + // 20+20+20+20 = 80 + 75 = 155 + expected_new_bid_etas_post_sync: Decimal256::from_ratio( + (4u128 * 20u128) + 75u128, + 1u128, + ), + expected_new_ask_etas_post_sync: Decimal256::zero(), + }, + SyncTickTestCase { + name: "Batch realization: chained batch claim (worst case for non-optimized)", + // Tick with: + // * 10 units of available liquidity for bid + // * 10 units of unrealized cancellations for bid + initial_tick_bid_values: build_tick_values(10, 10), + initial_tick_ask_values: build_tick_values(0, 0), + + // Multiple unrealized cancels for both bid and ask + unrealized_cancels_bid: vec![ + NodeType::leaf_uint256(1u32, 1u32), + NodeType::leaf_uint256(2u32, 1u32), + NodeType::leaf_uint256(3u32, 1u32), + NodeType::leaf_uint256(4u32, 1u32), + NodeType::leaf_uint256(5u32, 1u32), + NodeType::leaf_uint256(6u32, 1u32), + NodeType::leaf_uint256(7u32, 1u32), + NodeType::leaf_uint256(8u32, 1u32), + NodeType::leaf_uint256(9u32, 1u32), + NodeType::leaf_uint256(10u32, 1u32), + ], + unrealized_cancels_ask: vec![], + + // Increment tick ETAS by 1 for bid per iteration for 1 iteration + new_bid_etas_per_sync: Decimal256::from_ratio(1u128, 1u128), + new_ask_etas_per_sync: Decimal256::zero(), + num_syncs: 1, + + // The first iteration should realize all the cancellations + // In a naiive approach we would require multiple resyncs to realize all cancellations + // Here all should be realized on first pass + expected_cumulative_realized_bid: Decimal256::from_ratio(10u128, 1u128), + expected_cumulative_realized_ask: Decimal256::zero(), + + // The new ETAS includes all the incremented amounts which represent fills, + // plus the amount of realized cancellations for both bid and ask + expected_new_bid_etas_post_sync: Decimal256::from_ratio(1u128 + 10u128, 1u128), + expected_new_ask_etas_post_sync: Decimal256::zero(), + }, ]; for test in test_cases { @@ -361,6 +469,12 @@ fn test_sync_tick() { // --- System under test --- for _ in 0..test.num_syncs { + let TickState { + bid_values: pre_bid_tick_values, + ask_values: pre_ask_tick_values, + } = TICK_STATE + .load(deps.as_ref().storage, default_tick_id) + .unwrap(); // Increment tick ETAS for each step let (updated_bid_etas, updated_ask_etas) = increment_tick_etas( deps.as_mut().storage, @@ -378,6 +492,26 @@ fn test_sync_tick() { updated_ask_etas, ) .unwrap(); + + let TickState { + bid_values: post_bid_tick_values, + ask_values: post_ask_tick_values, + } = TICK_STATE + .load(deps.as_ref().storage, default_tick_id) + .unwrap(); + + assert!( + pre_bid_tick_values.effective_total_amount_swapped + <= post_bid_tick_values.effective_total_amount_swapped, + "ETAS decreased for bid on new sync: {}", + test.name + ); + assert!( + pre_ask_tick_values.effective_total_amount_swapped + <= post_ask_tick_values.effective_total_amount_swapped, + "ETAS decreased for ask on new sync: {}", + test.name + ); } // --- Assertions --- From c896369613f0e28fe292072f56edc9348c7b0964 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Thu, 27 Jun 2024 19:56:33 +0100 Subject: [PATCH 55/98] chore: removed redundant check --- .../sumtree-orderbook/src/tests/test_tick.rs | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/test_tick.rs b/contracts/sumtree-orderbook/src/tests/test_tick.rs index b6e6a00..61712f5 100644 --- a/contracts/sumtree-orderbook/src/tests/test_tick.rs +++ b/contracts/sumtree-orderbook/src/tests/test_tick.rs @@ -469,12 +469,6 @@ fn test_sync_tick() { // --- System under test --- for _ in 0..test.num_syncs { - let TickState { - bid_values: pre_bid_tick_values, - ask_values: pre_ask_tick_values, - } = TICK_STATE - .load(deps.as_ref().storage, default_tick_id) - .unwrap(); // Increment tick ETAS for each step let (updated_bid_etas, updated_ask_etas) = increment_tick_etas( deps.as_mut().storage, @@ -492,26 +486,6 @@ fn test_sync_tick() { updated_ask_etas, ) .unwrap(); - - let TickState { - bid_values: post_bid_tick_values, - ask_values: post_ask_tick_values, - } = TICK_STATE - .load(deps.as_ref().storage, default_tick_id) - .unwrap(); - - assert!( - pre_bid_tick_values.effective_total_amount_swapped - <= post_bid_tick_values.effective_total_amount_swapped, - "ETAS decreased for bid on new sync: {}", - test.name - ); - assert!( - pre_ask_tick_values.effective_total_amount_swapped - <= post_ask_tick_values.effective_total_amount_swapped, - "ETAS decreased for ask on new sync: {}", - test.name - ); } // --- Assertions --- From e07de7ba5ac82033fe61e6f22dba650bb290bb1a Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Thu, 27 Jun 2024 20:01:47 +0100 Subject: [PATCH 56/98] test: added cancelled orders test case --- .../sumtree-orderbook/src/tests/test_order.rs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index bb1db3f..4a5ec59 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -4227,3 +4227,30 @@ fn test_maker_fee() { } + +#[test] +fn test_cancelled_orders() { + let mut deps = mock_dependencies_custom(); + let sender = Addr::unchecked(DEFAULT_SENDER); + let env = mock_env(); + let info = mock_info(sender.as_str(), &[]); + + create_orderbook(deps.as_mut(), QUOTE_DENOM.to_string(), BASE_DENOM.to_string()).unwrap(); + + for i in 0..10 { + OrderOperation::PlaceLimit(LimitOrder::new(0, i, OrderDirection::Bid, sender.clone(), Uint128::from(1u128), Decimal256::zero(), None)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + if i % 3 != 0 { + OrderOperation::Cancel((0, i)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + } + + } + + OrderOperation::PlaceLimit(LimitOrder::new(0, 10, OrderDirection::Bid, sender.clone(), Uint128::from(1u128), Decimal256::zero(), None)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + OrderOperation::RunMarket(MarketOrder::new(Uint128::from(1u128).checked_mul(Uint128::from(4u128)).unwrap(), OrderDirection::Ask, sender.clone())).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + + // Second last order should be claimable + OrderOperation::Claim((0, 9)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + // Last order should NOT be claimable + let err = OrderOperation::Claim((0, 10)).run(deps.as_mut(), env.clone(), info.clone()).unwrap_err(); + assert_eq!(err, ContractError::ZeroClaim); +} From 32d867a1cf9dd027f596adc9c776d99d68a1f686 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Fri, 28 Jun 2024 13:18:13 +0100 Subject: [PATCH 57/98] test: fixed failing test, added last_tick_sync_etas and added cancelled orders test case --- .../sumtree-orderbook/src/sumtree/tree.rs | 10 +++--- .../sumtree-orderbook/src/tests/test_order.rs | 32 ++++++++++++++++--- .../sumtree-orderbook/src/tests/test_query.rs | 2 +- .../sumtree-orderbook/src/tests/test_utils.rs | 5 ++- contracts/sumtree-orderbook/src/tick.rs | 1 + 5 files changed, 36 insertions(+), 14 deletions(-) diff --git a/contracts/sumtree-orderbook/src/sumtree/tree.rs b/contracts/sumtree-orderbook/src/sumtree/tree.rs index 7d2c8d1..dc93a99 100644 --- a/contracts/sumtree-orderbook/src/sumtree/tree.rs +++ b/contracts/sumtree-orderbook/src/sumtree/tree.rs @@ -103,7 +103,7 @@ fn prefix_sum_walk( // in which cancels are realized and can simply "batch realize" all of them. // // This case is characterized by when the amount filled on the current tick + the unrealized cancellations in the - // left child of a node pushes ETAS into the range of the right child node (i.e. where the next order in line, after + // left child of a node pushes ETAS into the range of the right child node (i.e. where the next order in line, after // the sync is complete, is guaranteed to be another cancel). In this case, we count the left child as fully realized // and roll the newly realized portion into the target ETAS. This is functionally equivalent to batching multiple // syncs into one. @@ -113,15 +113,15 @@ fn prefix_sum_walk( // `sum_at_node` corresponds to everything to the left and in the current node. // We don't know which component of the current node, if any, will be included, so we remove the whole thing. - // + // // Sanity check: for the root node, this will be 0, since the "current node" is the root and includes the whole // tree (so when it is removed, there is nothing left) let sum_at_node = current_sum.checked_sub(node.get_value())?; // Calculate the amount of cumulative realized cancellations *below the current node* at the end of the // previous sync. // - // Recall that `prev_sum` is the cumulative *global* amount that was realized at the end of the previous sync. - // + // Recall that `prev_sum` is the cumulative *global* amount that was realized at the end of the previous sync. + // // Concretely, if this value is ever nonzero for a node, the amount corresponds exactly to the amount realized // below the node at the end of the previous sync. In all other cases, it will snap to zero due to the saturating sub. let diff_at_node = prev_sum.saturating_sub(sum_at_node); @@ -131,7 +131,7 @@ fn prefix_sum_walk( // If the left child is fully realized then the subtraction here will either be zero (if none of the right child is // realized) or negative (if some of the right child is realized), since `diff_at_node` = amount realized below left // and below right children. Saturating sub will ensure both of these cases snap to zero. - // + // // Thus, if this value is ever nonzero, it means that the left child had some unrealized cancels in it, and the // amount corresponds exactly to `unrealized_from_left`. let unrealized_from_left = left_child.get_value().saturating_sub(diff_at_node); diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index 4a5ec59..fbeb803 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -2,8 +2,7 @@ use std::str::FromStr; use crate::{ constants::{MAX_TICK, MIN_TICK}, error::ContractError, order::*, orderbook::*, state::*, sumtree::{ - node::{NodeType, TreeNode}, - tree::get_root_node, + node::{NodeType, TreeNode},tree::get_root_node }, tests::{mock_querier::mock_dependencies_custom, test_utils::{decimal256_from_u128, place_multiple_limit_orders}}, types::{ @@ -4237,6 +4236,8 @@ fn test_cancelled_orders() { create_orderbook(deps.as_mut(), QUOTE_DENOM.to_string(), BASE_DENOM.to_string()).unwrap(); + // Place 10 orders and cancel every order that is not evenly divisible by 3 + // This leaves orders 0, 3, 6 and 9 uncancelled for i in 0..10 { OrderOperation::PlaceLimit(LimitOrder::new(0, i, OrderDirection::Bid, sender.clone(), Uint128::from(1u128), Decimal256::zero(), None)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); if i % 3 != 0 { @@ -4245,12 +4246,33 @@ fn test_cancelled_orders() { } + // Tree after cancelling every order that is not evenly divisible by 3: + // + // 5: 6 1-9 + // ┌────────────────────────────────────────────────────────────────────┐ + // 1: 2 1-3 9: 4 4-9 + // ┌────────────────────────────────┐ ┌────────────────────────────────┐ + // 2: 1 1 3: 2 1 7: 2 4-6 11: 2 7-9 + // ┌────────────────┐ ┌────────────────┐ + // 4: 4 1 6: 5 1 8: 7 1 10: 8 1 + + // Last order should be unclaimable + let err = OrderOperation::Claim((0, 9)).run(deps.as_mut(), env.clone(), info.clone()).unwrap_err(); + assert_eq!(err, ContractError::ZeroClaim); + OrderOperation::PlaceLimit(LimitOrder::new(0, 10, OrderDirection::Bid, sender.clone(), Uint128::from(1u128), Decimal256::zero(), None)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); OrderOperation::RunMarket(MarketOrder::new(Uint128::from(1u128).checked_mul(Uint128::from(4u128)).unwrap(), OrderDirection::Ask, sender.clone())).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); - // Second last order should be claimable - OrderOperation::Claim((0, 9)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); - // Last order should NOT be claimable + // Order of checks should not matter + for id in [9, 3, 6, 0] { + // All non-cancelled orders should be claimable and uncancellable + let err = OrderOperation::Cancel((0, id)).run(deps.as_mut(), env.clone(), info.clone()).unwrap_err(); + assert_eq!(err, ContractError::CancelFilledOrder); + OrderOperation::Claim((0, id)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + } + + // Last order should NOT be claimable and can be cancelled let err = OrderOperation::Claim((0, 10)).run(deps.as_mut(), env.clone(), info.clone()).unwrap_err(); assert_eq!(err, ContractError::ZeroClaim); + OrderOperation::Cancel((0, 10)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); } diff --git a/contracts/sumtree-orderbook/src/tests/test_query.rs b/contracts/sumtree-orderbook/src/tests/test_query.rs index e2d3732..7814e21 100644 --- a/contracts/sumtree-orderbook/src/tests/test_query.rs +++ b/contracts/sumtree-orderbook/src/tests/test_query.rs @@ -1932,7 +1932,7 @@ fn test_ticks_by_id() { cumulative_total_value: decimal256_from_u128(2u8), effective_total_amount_swapped: decimal256_from_u128(2u8), cumulative_realized_cancels: Decimal256::one(), - last_tick_sync_etas: Decimal256::zero(), + last_tick_sync_etas: Decimal256::one(), }, bid_values: TickValues::default(), }], diff --git a/contracts/sumtree-orderbook/src/tests/test_utils.rs b/contracts/sumtree-orderbook/src/tests/test_utils.rs index d74660c..157ec14 100644 --- a/contracts/sumtree-orderbook/src/tests/test_utils.rs +++ b/contracts/sumtree-orderbook/src/tests/test_utils.rs @@ -79,8 +79,7 @@ impl OrderOperation { env.contract.address, tick_id, order_id, - ) - .unwrap(); + )?; Ok(()) } OrderOperation::Cancel((tick_id, order_id)) => { @@ -88,7 +87,7 @@ impl OrderOperation { .load(deps.as_ref().storage, &(tick_id, order_id)) .unwrap(); let info = mock_info(order.owner.as_str(), &[]); - cancel_limit(deps, env, info, tick_id, order_id).unwrap(); + cancel_limit(deps, env, info, tick_id, order_id)?; Ok(()) } } diff --git a/contracts/sumtree-orderbook/src/tick.rs b/contracts/sumtree-orderbook/src/tick.rs index b3822bd..e5b92a1 100644 --- a/contracts/sumtree-orderbook/src/tick.rs +++ b/contracts/sumtree-orderbook/src/tick.rs @@ -74,6 +74,7 @@ pub fn sync_tick( .effective_total_amount_swapped .checked_add(realized_since_last_sync)?; tick_value.cumulative_realized_cancels = new_cumulative_realized_cancels; + tick_value.last_tick_sync_etas = target_etas; // Defense in depth guardrail: ensure that tick sync does not push tick ETAS past CTT. ensure!( From f4bd03e4083dca5f78fe498f263c26385b7f940a Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Fri, 28 Jun 2024 13:51:28 +0100 Subject: [PATCH 58/98] test: added additional checks for mutating state in test_sync_tick --- .../sumtree-orderbook/src/tests/test_tick.rs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/contracts/sumtree-orderbook/src/tests/test_tick.rs b/contracts/sumtree-orderbook/src/tests/test_tick.rs index 61712f5..9cce472 100644 --- a/contracts/sumtree-orderbook/src/tests/test_tick.rs +++ b/contracts/sumtree-orderbook/src/tests/test_tick.rs @@ -446,7 +446,9 @@ fn test_sync_tick() { for test in test_cases { // --- Setup --- + // Create two dependency objects to allow tracking of a mutating state (closer to actual implementation) let mut deps = mock_dependencies(); + let mut mutable_tick_deps = mock_dependencies(); // Create and save default tick state let mut tick_state = TickState::default(); @@ -455,6 +457,13 @@ fn test_sync_tick() { TICK_STATE .save(deps.as_mut().storage, default_tick_id, &tick_state) .unwrap(); + TICK_STATE + .save( + mutable_tick_deps.as_mut().storage, + default_tick_id, + &tick_state, + ) + .unwrap(); // Insert specified nodes into tree for (unrealized_cancels, direction) in [ @@ -463,12 +472,41 @@ fn test_sync_tick() { ] { for node in unrealized_cancels.iter() { insert_and_refetch(deps.as_mut().storage, default_tick_id, direction, node); + insert_and_refetch( + mutable_tick_deps.as_mut().storage, + default_tick_id, + direction, + node, + ); } } // --- System under test --- for _ in 0..test.num_syncs { + // Sync both the mutable state and static state independently + let mut mutable_tick = TICK_STATE + .load(deps.as_mut().storage, default_tick_id) + .unwrap(); + let (updated_bid_etas, updated_ask_etas) = increment_tick_etas( + deps.as_mut().storage, + default_tick_id, + &mut mutable_tick, + test.new_bid_etas_per_sync, + test.new_ask_etas_per_sync, + ); + // Run sync + sync_tick( + mutable_tick_deps.as_mut().storage, + default_tick_id, + updated_bid_etas, + updated_ask_etas, + ) + .unwrap(); + let mutable_tick_post_sync = TICK_STATE + .load(mutable_tick_deps.as_ref().storage, default_tick_id) + .unwrap(); + // Increment tick ETAS for each step let (updated_bid_etas, updated_ask_etas) = increment_tick_etas( deps.as_mut().storage, @@ -486,6 +524,24 @@ fn test_sync_tick() { updated_ask_etas, ) .unwrap(); + let tick_state_post_sync = TICK_STATE + .load(deps.as_ref().storage, default_tick_id) + .unwrap(); + + // As adding to the ETAS each iteration is effectively simulating a market order for the increment no matter if the state is mutating or not + // both states should end up with the same realized cancels + for direction in [OrderDirection::Ask, OrderDirection::Bid] { + let mutable_tick_value = mutable_tick_post_sync.get_values(direction); + let tick_state_value = tick_state_post_sync.get_values(direction); + + assert_eq!( + mutable_tick_value.cumulative_realized_cancels, + tick_state_value.cumulative_realized_cancels, + "{}: Cumulative realized cancels did not match for direction: {}", + test.name, + direction + ); + } } // --- Assertions --- From ba5392ed82fb72542db434a063ca05e8afa7f11b Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Fri, 28 Jun 2024 14:19:02 +0100 Subject: [PATCH 59/98] test: extended cancelled orders test case --- .../sumtree-orderbook/src/tests/test_order.rs | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index fbeb803..ede9aa7 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -4239,7 +4239,7 @@ fn test_cancelled_orders() { // Place 10 orders and cancel every order that is not evenly divisible by 3 // This leaves orders 0, 3, 6 and 9 uncancelled for i in 0..10 { - OrderOperation::PlaceLimit(LimitOrder::new(0, i, OrderDirection::Bid, sender.clone(), Uint128::from(1u128), Decimal256::zero(), None)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + OrderOperation::PlaceLimit(LimitOrder::new(0, i, OrderDirection::Bid, sender.clone(), Uint128::from(2u128), Decimal256::zero(), None)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); if i % 3 != 0 { OrderOperation::Cancel((0, i)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); } @@ -4260,8 +4260,10 @@ fn test_cancelled_orders() { let err = OrderOperation::Claim((0, 9)).run(deps.as_mut(), env.clone(), info.clone()).unwrap_err(); assert_eq!(err, ContractError::ZeroClaim); - OrderOperation::PlaceLimit(LimitOrder::new(0, 10, OrderDirection::Bid, sender.clone(), Uint128::from(1u128), Decimal256::zero(), None)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); - OrderOperation::RunMarket(MarketOrder::new(Uint128::from(1u128).checked_mul(Uint128::from(4u128)).unwrap(), OrderDirection::Ask, sender.clone())).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + OrderOperation::PlaceLimit(LimitOrder::new(0, 10, OrderDirection::Bid, sender.clone(), Uint128::from(2u128), Decimal256::zero(), None)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + + // Fill half of the final order + OrderOperation::RunMarket(MarketOrder::new(Uint128::from(1u128).checked_mul(Uint128::from(7u128)).unwrap(), OrderDirection::Ask, sender.clone())).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); // Order of checks should not matter for id in [9, 3, 6, 0] { @@ -4269,10 +4271,31 @@ fn test_cancelled_orders() { let err = OrderOperation::Cancel((0, id)).run(deps.as_mut(), env.clone(), info.clone()).unwrap_err(); assert_eq!(err, ContractError::CancelFilledOrder); OrderOperation::Claim((0, id)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + + // Last order should be cancellable after being claimed + if id == 9 { + OrderOperation::Cancel((0, id)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + } } // Last order should NOT be claimable and can be cancelled let err = OrderOperation::Claim((0, 10)).run(deps.as_mut(), env.clone(), info.clone()).unwrap_err(); assert_eq!(err, ContractError::ZeroClaim); OrderOperation::Cancel((0, 10)).run(deps.as_mut(), env.clone(), info.clone()).unwrap(); + + // All orders should have been claimed or cancelled + for i in 0..10 { + let order = orders().may_load(deps.as_mut().storage, &(0, i)).unwrap(); + assert!(order.is_none(), "Order {} should have been removed", i); + } + + let tick_state = TICK_STATE.load(deps.as_mut().storage, 0).unwrap(); + // Cancels from orders 1, 2, 4, 5, 7 and 8 should have been realized + assert_eq!(tick_state.get_values(OrderDirection::Bid).cumulative_realized_cancels, Decimal256::from_ratio(12u64, 1u64)); + // ETAS should be cumulative realized cancels (12) + amount sold (7) = 19 + assert_eq!(tick_state.get_values(OrderDirection::Bid).effective_total_amount_swapped, Decimal256::from_ratio(19u64, 1u64)); + assert_eq!(tick_state.get_values(OrderDirection::Bid).last_tick_sync_etas, Decimal256::from_ratio(19u64, 1u64)); + // No orders should be left + assert_eq!(tick_state.get_values(OrderDirection::Bid).total_amount_of_liquidity, Decimal256::zero()); + assert_eq!(tick_state.get_values(OrderDirection::Bid).cumulative_total_value, Decimal256::from_ratio(22u128, 1u128)); } From dbbe47f4bae781fae6b843a8f1d33f2413adfe2d Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Fri, 28 Jun 2024 14:22:06 +0100 Subject: [PATCH 60/98] chore: fixed tree comment in cancelled orders --- .../sumtree-orderbook/src/tests/test_order.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index ede9aa7..dfa3dfc 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use crate::{ constants::{MAX_TICK, MIN_TICK}, error::ContractError, order::*, orderbook::*, state::*, sumtree::{ - node::{NodeType, TreeNode},tree::get_root_node + node::{NodeType, TreeNode}, test::test_node::print_tree, tree::{get_or_init_root_node, get_root_node} }, tests::{mock_querier::mock_dependencies_custom, test_utils::{decimal256_from_u128, place_multiple_limit_orders}}, types::{ @@ -4248,13 +4248,13 @@ fn test_cancelled_orders() { // Tree after cancelling every order that is not evenly divisible by 3: // - // 5: 6 1-9 - // ┌────────────────────────────────────────────────────────────────────┐ - // 1: 2 1-3 9: 4 4-9 - // ┌────────────────────────────────┐ ┌────────────────────────────────┐ - // 2: 1 1 3: 2 1 7: 2 4-6 11: 2 7-9 + // 5: 12 2-18 + // ┌────────────────────────────────────────────────────────────────────┐ + // 1: 4 2-6 9: 8 8-18 + // ┌────────────────────────────────┐ ┌────────────────────────────────┐ + // 2: 2 2 3: 4 2 7: 4 8-12 11: 4 14-18 // ┌────────────────┐ ┌────────────────┐ - // 4: 4 1 6: 5 1 8: 7 1 10: 8 1 + // 4: 8 2 6: 10 2 8: 14 2 10: 16 2 // Last order should be unclaimable let err = OrderOperation::Claim((0, 9)).run(deps.as_mut(), env.clone(), info.clone()).unwrap_err(); From 05e8fa16a776d8f920b004bb1232b627f1c6eac5 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Fri, 28 Jun 2024 14:31:25 +0100 Subject: [PATCH 61/98] fix: fixed compiler errors and failing tests --- contracts/sumtree-orderbook/src/contract.rs | 8 ----- contracts/sumtree-orderbook/src/query.rs | 29 ------------------- .../sumtree-orderbook/src/tests/test_order.rs | 6 ++-- .../sumtree-orderbook/src/tests/test_query.rs | 2 +- 4 files changed, 3 insertions(+), 42 deletions(-) diff --git a/contracts/sumtree-orderbook/src/contract.rs b/contracts/sumtree-orderbook/src/contract.rs index 3a3b0e6..aec8330 100644 --- a/contracts/sumtree-orderbook/src/contract.rs +++ b/contracts/sumtree-orderbook/src/contract.rs @@ -160,14 +160,6 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> ContractResult { QueryMsg::GetUnrealizedCancels { tick_ids } => Ok(to_json_binary( &query::ticks_unrealized_cancels_by_id(deps, tick_ids)?, )?), - QueryMsg::OrdersByTick { - tick_id, - start_from, - end_at, - limit, - } => Ok(to_json_binary(&query::orders_by_tick( - deps, tick_id, start_from, end_at, limit, - )?)?), // -- Auth Queries -- QueryMsg::Auth(msg) => Ok(to_json_binary(&auth::query(deps, msg)?)?), diff --git a/contracts/sumtree-orderbook/src/query.rs b/contracts/sumtree-orderbook/src/query.rs index 7d90f62..feb207f 100644 --- a/contracts/sumtree-orderbook/src/query.rs +++ b/contracts/sumtree-orderbook/src/query.rs @@ -221,35 +221,6 @@ pub(crate) fn orders_by_owner( }) } -pub(crate) fn orders_by_tick( - deps: Deps, - tick_id: i64, - start_from: Option, - end_at: Option, - limit: Option, -) -> ContractResult { - let count = orders() - .prefix(tick_id) - .keys(deps.storage, None, None, Order::Ascending) - .count(); - let orders = orders() - .prefix(tick_id) - .range( - deps.storage, - start_from.map(Bound::inclusive), - end_at.map(Bound::inclusive), - Order::Ascending, - ) - .take(limit.unwrap_or(count as u64) as usize) - .map(|res| res.unwrap().1) - .collect(); - - Ok(OrdersResponse { - count: count as u64, - orders, - }) -} - pub(crate) fn denoms(deps: Deps) -> ContractResult { let orderbook = ORDERBOOK.load(deps.storage)?; Ok(DenomsResponse { diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index bfd5d2f..0d37797 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -1,8 +1,8 @@ -use std::{collections::HashMap, str::FromStr}; +use std::str::FromStr; use crate::{ constants::{MAX_TICK, MIN_TICK}, error::ContractError, order::*, orderbook::*, state::*, sumtree::{ - node::{NodeType, TreeNode}, test::test_fuzz::assert_sumtree_invariants, tree::{get_or_init_root_node, get_prefix_sum, get_root_node} + node::{NodeType, TreeNode}, tree::get_root_node }, tests::{mock_querier::mock_dependencies_custom, test_utils::{decimal256_from_u128, place_multiple_limit_orders}}, types::{ @@ -17,8 +17,6 @@ use cosmwasm_std::{ Decimal256, }; use cw_utils::PaymentError; -use rand::{rngs::StdRng, Rng, SeedableRng}; - use super::{test_constants::{DEFAULT_OWNER, DEFAULT_SENDER, BASE_DENOM, QUOTE_DENOM, LARGE_POSITIVE_TICK, LARGE_NEGATIVE_TICK}, test_utils::{ format_test_name, generate_limit_orders, OrderOperation, }}; diff --git a/contracts/sumtree-orderbook/src/tests/test_query.rs b/contracts/sumtree-orderbook/src/tests/test_query.rs index 18f5417..f519607 100644 --- a/contracts/sumtree-orderbook/src/tests/test_query.rs +++ b/contracts/sumtree-orderbook/src/tests/test_query.rs @@ -1446,7 +1446,7 @@ fn test_orders_by_owner() { ); }); assert_eq!( - res, + res.orders, test.expected_output .iter() .map(|o| o.clone().with_placed_at(env.block.time)) From 008a24bc91202a4a863c130f060748fdf9a1530a Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Sun, 30 Jun 2024 10:09:50 +0100 Subject: [PATCH 62/98] test: added more fuzz test assertions --- .../src/tests/e2e/cases/test_fuzz.rs | 45 ++++++++++------ .../src/tests/e2e/cases/utils.rs | 52 ++++++++++++++++++- .../src/tests/e2e/test_env.rs | 26 ++++++++-- 3 files changed, 104 insertions(+), 19 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index 3af01c4..b1e0e60 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -68,12 +68,21 @@ fn test_order_fuzz_linear_single_tick() { #[test] fn test_order_fuzz_mixed() { - let duration = Duration::from_secs(60); + let duration = Duration::from_secs(60 * 3); let now = SystemTime::now(); let end = now.checked_add(duration).unwrap(); + + let oper_per_iteration = 1000; + let mut oper_count = 0; + let mut iterations = 0; while SystemTime::now().le(&end) { - run_fuzz_mixed(2000, (-20, 20)); + run_fuzz_mixed(oper_per_iteration, (-20, 20)); + + oper_count += oper_per_iteration; + iterations += 1; } + println!("operations: {}", oper_count); + println!("iterations: {}", iterations); } #[test] @@ -253,7 +262,7 @@ impl MixedFuzzOperation { order_count: &mut u64, tick_bounds: (i64, i64), ) -> Result { - println!("operation: {self:?}"); + // println!("operation: {self:?}"); let username = format!("user{}", iteration); match self { MixedFuzzOperation::PlaceLimit => { @@ -328,10 +337,10 @@ impl MixedFuzzOperation { return Ok(false); } - // Remove the order once we know it is cancellable - orders.remove(&order_id).unwrap(); // Cancel the order orders::cancel_limit_success(t, &username, tick_id, order_id).unwrap(); + // Remove the order once we know it is cancellable + orders.remove(&order_id).unwrap(); Ok(true) } MixedFuzzOperation::Claim => { @@ -434,7 +443,7 @@ fn run_fuzz_mixed(amount_of_orders: u64, tick_bounds: (i64, i64)) { // We add an escape clause in the case that the test ever gets caught in an infinite loop let mut repeated_failures = 0; - println!("iteration: {}", i); + // println!("iteration: {}", i); // Repeat randomising operations until a successful one is chosen while !operation .run( @@ -464,30 +473,36 @@ fn run_fuzz_mixed(amount_of_orders: u64, tick_bounds: (i64, i64)) { for (order_id, (username, tick_id)) in orders.clone().iter() { match orders::claim_success(&t, username, username, *tick_id, *order_id) { Ok(_) => { - continue; + let order = t.contract.get_order( + t.accounts[username.as_str()].address(), + *tick_id, + *order_id, + ); + if order.is_none() { + continue; + } } - Err(e) => println!("Error claiming order: {e}"), + Err(e) => {} } match orders::cancel_limit_success(&t, username, *tick_id, *order_id) { Ok(_) => { continue; } - Err(e) => println!("Error cancelling order: {e}"), + Err(e) => {} } - let order = t - .contract - .get_order(username.clone(), *tick_id, *order_id) - .unwrap(); + let order = t.contract.get_order(username.clone(), *tick_id, *order_id); assert!( - order.placed_quantity != order.quantity, - "order could not be claimed or cancelled: {order:?}" + order.is_none(), + "order was not cleaned from state: {order:?}" ); } // Assert every operation ran at least once successfully assert!(oper_count.values().all(|c| *c > 0)); + assert::no_remaining_orders(&t); + assert::clean_ticks(&t); } /// Places a random limit order in the provided tick range using the provided username diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs index 7d31cbe..d5bf195 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs @@ -74,7 +74,10 @@ macro_rules! setup { pub mod assert { use crate::{ - msg::{DenomsResponse, GetTotalPoolLiquidityResponse, QueryMsg, SpotPriceResponse}, + msg::{ + DenomsResponse, GetTotalPoolLiquidityResponse, GetUnrealizedCancelsResponse, QueryMsg, + SpotPriceResponse, + }, tests::e2e::test_env::TestEnv, tick_math::tick_to_price, types::{OrderDirection, Orderbook}, @@ -283,6 +286,53 @@ pub mod assert { assert!(next_bid_tick >= max_tick_with_bid.tick_id); } } + + pub fn no_remaining_orders(t: &TestEnv) { + let all_orders = t.contract.collect_all_orders(); + assert_eq!(all_orders.len(), 0); + } + + pub fn clean_ticks(t: &TestEnv) { + let all_ticks = t.contract.collect_all_ticks(); + for tick in all_ticks { + let GetUnrealizedCancelsResponse { ticks } = t + .contract + .query(&QueryMsg::GetUnrealizedCancels { + tick_ids: vec![tick.tick_id], + }) + .unwrap(); + let unrealized_cancels_state = ticks.first().unwrap(); + for direction in [OrderDirection::Ask, OrderDirection::Bid] { + let values = tick.tick_state.get_values(direction); + assert!( + values.total_amount_of_liquidity.is_zero(), + "tick {} has liquidity", + tick.tick_id + ); + + let unrealized_cancels = match direction { + OrderDirection::Ask => { + unrealized_cancels_state + .unrealized_cancels + .ask_unrealized_cancels + } + OrderDirection::Bid => { + unrealized_cancels_state + .unrealized_cancels + .bid_unrealized_cancels + } + }; + + assert_eq!( + values + .effective_total_amount_swapped + .checked_add(unrealized_cancels) + .unwrap(), + values.cumulative_total_value + ); + } + } + } } pub mod orders { diff --git a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs index 73a5140..922ad4c 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs @@ -331,6 +331,25 @@ impl<'a> OrderbookContract<'a> { ticks } + pub fn collect_all_orders(&self) -> Vec { + let ticks = self.collect_all_ticks(); + + let mut all_orders: Vec = vec![]; + for tick in ticks { + let orders: OrdersResponse = self + .query(&QueryMsg::OrdersByTick { + tick_id: tick.tick_id, + start_from: None, + end_at: None, + limit: None, + }) + .unwrap(); + all_orders.extend(orders.orders.clone()); + } + + all_orders + } + pub fn get_directional_liquidity(&self, order_direction: OrderDirection) -> u128 { let GetTotalPoolLiquidityResponse { total_pool_liquidity, @@ -354,12 +373,13 @@ impl<'a> OrderbookContract<'a> { let OrdersResponse { orders, .. } = self .query(&QueryMsg::OrdersByOwner { owner: Addr::unchecked(sender), - start_from: Some((tick_id, order_id)), + start_from: None, end_at: None, - limit: Some(1), + limit: None, }) .unwrap(); - orders.first().cloned() + let order = orders.iter().find(|o| o.order_id == order_id).cloned(); + order } } From ecea399ab451ad2e6dd66acc37f97b4ba62526af Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Sun, 30 Jun 2024 10:28:24 +0100 Subject: [PATCH 63/98] chore: commented more of test_fuzz --- .../src/tests/e2e/cases/test_fuzz.rs | 46 +++++++++---------- .../src/tests/e2e/test_env.rs | 7 ++- .../sumtree-orderbook/src/tests/test_order.rs | 2 +- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index b1e0e60..5d86060 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -87,7 +87,7 @@ fn test_order_fuzz_mixed() { #[test] fn test_order_fuzz_mixed_single_tick() { - run_fuzz_mixed(5000, (0, 0)); + run_fuzz_mixed(1000, (0, 0)); } /// Runs a linear fuzz test with the following steps @@ -131,7 +131,9 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob // Determine the amount of liquidity for the given direction let mut liquidity = t.contract.get_directional_liquidity(order_direction); + // A counter to track the number of zero amount returns let mut zero_amount_returns = 0; + // A counter to track the current user ID let mut user_id = 0; // While there is some fillable liquidity we want to place randomised market orders @@ -427,9 +429,13 @@ fn run_fuzz_mixed(amount_of_orders: u64, tick_bounds: (i64, i64)) { let app = OsmosisTestApp::new(); let cp = CosmwasmPool::new(&app); let mut t = setup!(&app, "quote", "base", 1); + + // A record of the orders placed to allow for simpler management of cancellations and claims let mut orders: HashMap = HashMap::new(); + // A count of the orders placed to track the current order ID let mut order_count = 0; + // Record how many times each operation is chosen for assertion post test let mut oper_count: HashMap = HashMap::new(); oper_count.insert(MixedFuzzOperation::PlaceLimit, 0); oper_count.insert(MixedFuzzOperation::PlaceMarket, 0); @@ -471,27 +477,22 @@ fn run_fuzz_mixed(amount_of_orders: u64, tick_bounds: (i64, i64)) { } for (order_id, (username, tick_id)) in orders.clone().iter() { - match orders::claim_success(&t, username, username, *tick_id, *order_id) { - Ok(_) => { - let order = t.contract.get_order( - t.accounts[username.as_str()].address(), - *tick_id, - *order_id, - ); - if order.is_none() { - continue; - } - } - Err(e) => {} + let _ = orders::claim_success(&t, username, username, *tick_id, *order_id); + + // Order may be cleared by fully claiming, in which case we want to continue to the next order + if t.contract + .get_order(t.accounts[username.as_str()].address(), *tick_id, *order_id) + .is_none() + { + continue; } - match orders::cancel_limit_success(&t, username, *tick_id, *order_id) { - Ok(_) => { - continue; - } - Err(e) => {} + // If cancelling is a success we can continue to the next order + if orders::cancel_limit_success(&t, username, *tick_id, *order_id).is_ok() { + continue; } + // If an order cannot be claimed or cancelled something has gone wrong let order = t.contract.get_order(username.clone(), *tick_id, *order_id); assert!( order.is_none(), @@ -499,6 +500,8 @@ fn run_fuzz_mixed(amount_of_orders: u64, tick_bounds: (i64, i64)) { ); } + // -- Post test assertions -- + // Assert every operation ran at least once successfully assert!(oper_count.values().all(|c| *c > 0)); assert::no_remaining_orders(&t); @@ -622,11 +625,6 @@ fn place_random_market( return 0; } - // // If the provided error cannot be filled then we return a 0 amount - // if amount == 0 || expected_out.is_err() || expected_out.unwrap().token_out.amount == "0" { - // return 0; - // } - // Generate the user account t.add_account( username, @@ -638,6 +636,8 @@ fn place_random_market( // Places the market order and ensures that funds are transferred correctly orders::place_market_success(cp, t, order_direction, amount, username).unwrap(); + + // We return the amount placed for recording amount } diff --git a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs index 922ad4c..dba6248 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs @@ -373,13 +373,12 @@ impl<'a> OrderbookContract<'a> { let OrdersResponse { orders, .. } = self .query(&QueryMsg::OrdersByOwner { owner: Addr::unchecked(sender), - start_from: None, + start_from: Some((tick_id, order_id)), end_at: None, - limit: None, + limit: Some(1), }) .unwrap(); - let order = orders.iter().find(|o| o.order_id == order_id).cloned(); - order + orders.first().cloned() } } diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index d5b2f90..3f2fb17 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use crate::{ constants::{MAX_TICK, MIN_TICK}, error::ContractError, order::*, orderbook::*, state::*, sumtree::{ - node::{NodeType, TreeNode}, test::test_node::print_tree, tree::{get_or_init_root_node, get_root_node} + node::{NodeType, TreeNode}, tree::get_root_node }, tests::{mock_querier::mock_dependencies_custom, test_utils::{decimal256_from_u128, place_multiple_limit_orders}}, types::{ From 67850a1d8d8f95faf7e340ff5e001cc78239f41f Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Sun, 30 Jun 2024 10:54:46 +0100 Subject: [PATCH 64/98] refactor: cleaned up utils for e2e --- .../src/tests/e2e/cases/test_fuzz.rs | 17 +- .../tests/e2e/cases/test_orders_success.rs | 8 +- .../src/tests/e2e/cases/utils.rs | 201 +++++++++++------- 3 files changed, 133 insertions(+), 93 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index 5d86060..de2e18d 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -68,7 +68,7 @@ fn test_order_fuzz_linear_single_tick() { #[test] fn test_order_fuzz_mixed() { - let duration = Duration::from_secs(60 * 3); + let duration = Duration::from_secs(60 * 1); let now = SystemTime::now(); let end = now.checked_add(duration).unwrap(); @@ -117,7 +117,7 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob let is_cancelled = rng.gen_bool(cancel_probability); if is_cancelled { - orders::cancel_limit_success(&t, &username, chosen_tick, i).unwrap(); + orders::cancel_limit_and_assert_balance(&t, &username, chosen_tick, i).unwrap(); } else { orders.push((username, chosen_tick, i)); } @@ -202,7 +202,8 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob username }; - orders::claim_success(&t, sender, username, order.tick_id, order.order_id).unwrap(); + orders::claim_and_assert_balance(&t, sender, username, order.tick_id, order.order_id) + .unwrap(); // For the situation that the order has the 1 remainder we record this for assertions let maybe_order = t.contract.get_order( @@ -340,7 +341,7 @@ impl MixedFuzzOperation { } // Cancel the order - orders::cancel_limit_success(t, &username, tick_id, order_id).unwrap(); + orders::cancel_limit_and_assert_balance(t, &username, tick_id, order_id).unwrap(); // Remove the order once we know it is cancellable orders.remove(&order_id).unwrap(); Ok(true) @@ -390,7 +391,7 @@ impl MixedFuzzOperation { }; // Claim the order - match orders::claim_success(t, claimant, &username, tick_id, order_id) { + match orders::claim_and_assert_balance(t, claimant, &username, tick_id, order_id) { Ok(_) => { let order = t.contract.get_order( t.accounts[&username].address(), @@ -477,7 +478,7 @@ fn run_fuzz_mixed(amount_of_orders: u64, tick_bounds: (i64, i64)) { } for (order_id, (username, tick_id)) in orders.clone().iter() { - let _ = orders::claim_success(&t, username, username, *tick_id, *order_id); + let _ = orders::claim_and_assert_balance(&t, username, username, *tick_id, *order_id); // Order may be cleared by fully claiming, in which case we want to continue to the next order if t.contract @@ -488,7 +489,7 @@ fn run_fuzz_mixed(amount_of_orders: u64, tick_bounds: (i64, i64)) { } // If cancelling is a success we can continue to the next order - if orders::cancel_limit_success(&t, username, *tick_id, *order_id).is_ok() { + if orders::cancel_limit_and_assert_balance(&t, username, *tick_id, *order_id).is_ok() { continue; } @@ -635,7 +636,7 @@ fn place_random_market( ); // Places the market order and ensures that funds are transferred correctly - orders::place_market_success(cp, t, order_direction, amount, username).unwrap(); + orders::place_market_and_assert_balance(cp, t, order_direction, amount, username).unwrap(); // We return the amount placed for recording amount diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs index ab3362d..e9384d6 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs @@ -111,7 +111,7 @@ fn test_basic_order() { assert::spot_price(&t, expected_bid_tick, expected_ask_tick, case.name); // Fill limit order - orders::place_market_success( + orders::place_market_and_assert_balance( &cp, &t, case.order_direction.opposite(), @@ -142,7 +142,7 @@ fn test_basic_order() { assert::spot_price(&t, expected_bid_tick, expected_ask_tick, case.name); // Claim limit - orders::claim_success(&t, case.claimer, "user1", 0, 0).unwrap(); + orders::claim_and_assert_balance(&t, case.claimer, "user1", 0, 0).unwrap(); match case.order_direction { OrderDirection::Ask => { assert::pool_liquidity(&t, case.placed_amount - case.filled_amount, 0u8, case.name); @@ -174,7 +174,7 @@ fn test_cancelled_orders() { "user1", ) .unwrap(); - orders::cancel_limit_success(&t, "user1", 0, i).unwrap(); + orders::cancel_limit_and_assert_balance(&t, "user1", 0, i).unwrap(); } assert::pool_liquidity(&t, 0u8, 0u8, "cancelled orders"); assert::pool_balance(&t, 0u8, 0u8, "cancelled orders"); @@ -193,7 +193,7 @@ fn test_cancelled_orders() { assert::pool_balance(&t, 100u8, 0u8, "cancelled orders"); assert::tick_invariants(&t); - orders::place_market_success( + orders::place_market_and_assert_balance( &cp, &t, OrderDirection::Bid, diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs index d5bf195..56305d4 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs @@ -1,3 +1,4 @@ +/// Sets up the testing environment for the orderbook #[macro_export] macro_rules! setup { ($($app:expr, $quote_denom:expr, $base_denom:expr, $maker_fee:expr),* ) => {{ @@ -64,6 +65,7 @@ macro_rules! setup { assert!(is_active); + // NOTE: wasm_sudo does not currently maintain state so these calls will not work // t.contract.set_admin($app, cosmwasm_std::Addr::unchecked(&t.accounts["contract_admin"].address())); // t.contract // .set_maker_fee(&t.accounts["contract_admin"], Decimal256::percent($maker_fee), &t.accounts["maker_fee_recipient"]); @@ -72,6 +74,8 @@ macro_rules! setup { }}; } +// -- Assertions -- +// Assertions about current state pub mod assert { use crate::{ msg::{ @@ -85,6 +89,9 @@ pub mod assert { use cosmwasm_std::{Coin, Coins}; use osmosis_test_tube::{cosmrs::proto::prost::Message, RunnerExecuteResult}; + // -- Contract State Assertions + + /// Asserts that the orderbook's current liquidity matches what is provided pub fn pool_liquidity( t: &TestEnv, base_liquidity: impl Into, @@ -112,6 +119,7 @@ pub mod assert { ); } + /// Asserts that the contract's balance matches what is provided pub fn pool_balance( t: &TestEnv, base_liquidity: impl Into, @@ -136,6 +144,7 @@ pub mod assert { ); } + /// Asserts that the orderbook spot price matches what is provided pub fn spot_price(t: &TestEnv, bid_tick: i64, ask_tick: i64, label: &str) { let bid_price = tick_to_price(bid_tick).unwrap(); let ask_price = tick_to_price(ask_tick).unwrap(); @@ -166,12 +175,16 @@ pub mod assert { } } + /// Asserts that the contract balance is greater than or equal to what is recorded in the orderbook directional liquidity state + /// If this assertion is ever false then the orderbook is "out of balance" and cannot provide liquidity for future orders pub fn has_liquidity(t: &TestEnv) { let bid_liquidity = t.contract.get_directional_liquidity(OrderDirection::Bid); let ask_liquidity = t.contract.get_directional_liquidity(OrderDirection::Ask); + let balance = Coins::try_from(t.get_balance(&t.contract.contract_addr)).unwrap(); let bid_balance = balance.amount_of(&t.contract.get_denoms().base_denom); let ask_balance = balance.amount_of(&t.contract.get_denoms().quote_denom); + assert!( bid_liquidity <= bid_balance.u128(), "invalid bid liquidity, expected: {}, got: {}", @@ -186,65 +199,12 @@ pub mod assert { ); } - pub fn balance_changes( - t: &TestEnv, - changes: &[(&str, Vec)], - action: impl FnOnce() -> RunnerExecuteResult, - ) -> RunnerExecuteResult { - let pre_balances: Vec<(String, Coins)> = changes - .iter() - .map(|(sender, _)| { - ( - sender.to_string(), - Coins::try_from(t.get_balance(sender)).unwrap(), - ) - }) - .collect(); - let result = action(); - - match result { - Ok(res) => { - let post_balances: Vec<(String, Coins)> = changes - .iter() - .map(|(sender, _)| { - ( - sender.to_string(), - Coins::try_from(t.get_balance(sender)).unwrap(), - ) - }) - .collect(); - - for (sender, balance_change) in changes.iter().cloned() { - let pre_balance = pre_balances - .iter() - .find(|(s, _)| s == sender) - .unwrap() - .1 - .clone(); - let post_balance = post_balances - .iter() - .find(|(s, _)| s == sender) - .unwrap() - .1 - .clone(); - for coin in balance_change { - let pre_amount = pre_balance.amount_of(&coin.denom); - let post_amount = post_balance.amount_of(&coin.denom); - let change = post_amount.saturating_sub(pre_amount); - assert_eq!( - change, coin.amount, - "Did not receive expected amount change, expected: {}{}, got: {}{}", - coin.amount, coin.denom, change, coin.denom - ); - } - } - - Ok(res) - } - Err(e) => Err(e), - } - } - + /// Assertions about tick state + /// 1. All ticks have a cumulative value that is greater than or equal to the effective total amount swapped + /// 2. The next ask tick is less than or equal to the minimum tick with an ask amount + /// 3. The next bid tick is greater than or equal to the maximum tick with a bid amount + /// + /// This assertion can be run mid test as it must always be true pub fn tick_invariants(t: &TestEnv) { let ticks = t.contract.collect_all_ticks(); @@ -287,11 +247,15 @@ pub mod assert { } } + /// Asserts that there are no remaining orders in the orderbook pub fn no_remaining_orders(t: &TestEnv) { let all_orders = t.contract.collect_all_orders(); assert_eq!(all_orders.len(), 0); } + /// Asserts that all ticks are fully synced + /// + /// **Should be run AFTER a fuzz test** pub fn clean_ticks(t: &TestEnv) { let all_ticks = t.contract.collect_all_ticks(); for tick in all_ticks { @@ -323,6 +287,10 @@ pub mod assert { } }; + // As a tick may not be fully synced due to the last order being a cancellation rather than a claim + // we check that if the tick was fully synced then ETAS == CTT must be true + // In the case that the tick was already synced then unrealized cancels is 0 and we are doing a direct + // ETAS == CTT comparison assert_eq!( values .effective_total_amount_swapped @@ -333,8 +301,73 @@ pub mod assert { } } } + + // -- Balance Assertions -- + + /// An assertion that records balances before an action and compares the balances after the provided action + /// Comparisons are only done for the vector of addresses provided in the second parameter + pub fn balance_changes( + t: &TestEnv, + changes: &[(&str, Vec)], + action: impl FnOnce() -> RunnerExecuteResult, + ) -> RunnerExecuteResult { + // Record balances before the action + let pre_balances: Vec<(String, Coins)> = changes + .iter() + .map(|(sender, _)| { + ( + sender.to_string(), + Coins::try_from(t.get_balance(sender)).unwrap(), + ) + }) + .collect(); + + // Run the action + let result = action()?; + + // Check balances after running the action + let post_balances: Vec<(String, Coins)> = changes + .iter() + .map(|(sender, _)| { + ( + sender.to_string(), + Coins::try_from(t.get_balance(sender)).unwrap(), + ) + }) + .collect(); + + // Check all expected balance changes + for (sender, balance_change) in changes.iter().cloned() { + let pre_balance = pre_balances + .iter() + .find(|(s, _)| s == sender) + .unwrap() + .1 + .clone(); + let post_balance = post_balances + .iter() + .find(|(s, _)| s == sender) + .unwrap() + .1 + .clone(); + + for coin in balance_change { + let pre_amount = pre_balance.amount_of(&coin.denom); + let post_amount = post_balance.amount_of(&coin.denom); + let change = post_amount.saturating_sub(pre_amount); + assert_eq!( + change, coin.amount, + "Did not receive expected amount change, expected: {}{}, got: {}{}", + coin.amount, coin.denom, change, coin.denom + ); + } + } + + Ok(result) + } } +/// Utili functions for interacting with the orderbook pub mod orders { use std::str::FromStr; @@ -346,7 +379,7 @@ pub mod orders { MsgSwapExactAmountIn, MsgSwapExactAmountInResponse, SwapAmountInRoute, }, }; - use osmosis_test_tube::{Account, OsmosisTestApp, RunnerError, RunnerExecuteResult}; + use osmosis_test_tube::{Account, OsmosisTestApp, RunnerExecuteResult}; use crate::{ msg::{CalcOutAmtGivenInResponse, DenomsResponse, ExecuteMsg, QueryMsg}, @@ -429,7 +462,10 @@ pub mod orders { ) } - pub fn place_market_success( + /// Places a market order and asserts that the sender's balance changes correctly + /// + /// Note: this check has some circularity to it as the expected out depends on the `CalcOutAmtGivenInResponse` + pub fn place_market_and_assert_balance( cp: &CosmwasmPool, t: &TestEnv, order_direction: OrderDirection, @@ -464,6 +500,7 @@ pub mod orders { assert::balance_changes( t, + // Users receives expected amount out in token out denom &[( &t.accounts[sender].address(), vec![Coin::new( @@ -490,7 +527,10 @@ pub mod orders { ) } - pub fn claim_success( + /// Claims a given order using the provided sender account name + /// + /// Asserts that the sender and order owner's balances change correctly + pub fn claim_and_assert_balance( t: &TestEnv, sender: &str, owner: &str, @@ -501,13 +541,11 @@ pub mod orders { .contract .get_order(t.accounts[owner].address(), tick_id, order_id) .unwrap(); + + // Get how much is expected out given the current tick state (accounts for unrealized cancels) let expected_amount = t.contract.get_order_claimable_amount(order.clone()); - if expected_amount == 0 { - return Err(RunnerError::GenericError( - "Cannot claim order: nothing to claim".to_string(), - )); - } + // Convert the expected amount to the price of the order let price = tick_to_price(order.tick_id).unwrap(); let mut expected_received_u256 = amount_to_value( order.order_direction, @@ -516,14 +554,10 @@ pub mod orders { RoundingDirection::Down, ) .unwrap(); + // Create immutable expected received for calculating claim and maker fees let immut_expected_received_u256 = expected_received_u256; - if immut_expected_received_u256.is_zero() { - return Err(RunnerError::GenericError( - "Cannot claim order: nothing to claim".to_string(), - )); - } - + // Calculate the bounty amount if there is one let mut bounty_amount_256 = Uint256::zero(); if let Some(bounty) = order.claim_bounty { if order.owner != t.accounts[sender].address() { @@ -532,12 +566,15 @@ pub mod orders { .checked_mul(bounty) .unwrap() .to_uint_floor(); + // Subtract the bounty from the expected received expected_received_u256 = expected_received_u256 .checked_sub(bounty_amount_256) .unwrap(); } } + // Calculate the maker fee + // May be zero let maker_fee = t.contract.get_maker_fee(); let maker_fee_amount_u256 = Decimal256::from_ratio(immut_expected_received_u256, Uint256::one()) @@ -546,6 +583,7 @@ pub mod orders { .to_uint_floor(); let maker_fee_amount = Uint128::try_from(maker_fee_amount_u256).unwrap(); + // Subtract the maker fee from the expected received expected_received_u256 = expected_received_u256 .checked_sub(maker_fee_amount_u256) .unwrap(); @@ -566,20 +604,24 @@ pub mod orders { assert::balance_changes( t, [ + // Assert owner receives amount - maker fee - claim bounty ( order.owner.as_str(), vec![Coin::new(expected_received.u128(), expected_denom.clone())], ), + // Assert sender receives bounty (will be 0 if the sender is the owner) ( &t.accounts[sender].address(), vec![Coin::new(bounty_amount.u128(), expected_denom.clone())], ), + // Assert maker fee recipient receives maker fee ( &t.accounts["maker_fee_recipient"].address(), vec![Coin::new(maker_fee_amount.u128(), expected_denom)], ), ] .iter() + // Remove any 0 checks .filter(|x| x.1.iter().all(|y| !y.amount.is_zero())) .cloned() .collect::)>>() @@ -601,22 +643,18 @@ pub mod orders { ) } - pub fn cancel_limit_success( + /// Cancels a limit order and asserts that the owner receives back the remaining order quantity (may be partially filled) + pub fn cancel_limit_and_assert_balance( t: &TestEnv, sender: &str, tick_id: i64, order_id: u64, ) -> RunnerExecuteResult { - let order: LimitOrder = t + let order = t .contract .get_order(t.accounts[sender].address(), tick_id, order_id) .unwrap(); - let claimable = t.contract.get_order_claimable_amount(order.clone()); - if claimable > 0 { - return Err(RunnerError::GenericError( - "Cannot cancel order: Order is partially filled".to_string(), - )); - } + let order_direction = order.order_direction; let quantity = order.quantity; let DenomsResponse { @@ -631,6 +669,7 @@ pub mod orders { assert::balance_changes( t, + // Assert owner receives back the remaining order quantity &[( &t.accounts[sender].address(), vec![Coin::new(quantity.u128(), token_in_denom)], From 932708652f25fcd57c04438ca17a07b15b504d02 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Sun, 30 Jun 2024 11:04:06 +0100 Subject: [PATCH 65/98] refactor: added run_for_duration to fuzz testing --- .../src/tests/e2e/cases/test_fuzz.rs | 46 +++++++++++++------ .../src/tests/e2e/cases/utils.rs | 3 +- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index de2e18d..44bd482 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -25,6 +25,30 @@ pub(crate) const LARGE_POSITIVE_TICK: i64 = 1000000; // Tick Price = 0.5 pub(crate) const LARGE_NEGATIVE_TICK: i64 = -5000000; +// Loops over a provided action for the provided duration +// Tracks the number of operations and iterations +// Duration is in seconds +fn run_for_duration( + duration: u64, + count_per_iteration: u64, + action: impl FnOnce(u64) + std::marker::Copy, +) { + let duration = Duration::from_secs(duration); + let now = SystemTime::now(); + let end = now.checked_add(duration).unwrap(); + + let mut oper_count = 0; + let mut iterations = 0; + while SystemTime::now().le(&end) { + action(count_per_iteration); + + oper_count += count_per_iteration; + iterations += 1; + } + println!("operations: {}", oper_count); + println!("iterations: {}", iterations); +} + #[test] fn test_order_fuzz_linear_large_orders_small_range() { run_fuzz_linear(2000, (-10, 10), 0.2); @@ -68,26 +92,20 @@ fn test_order_fuzz_linear_single_tick() { #[test] fn test_order_fuzz_mixed() { - let duration = Duration::from_secs(60 * 1); - let now = SystemTime::now(); - let end = now.checked_add(duration).unwrap(); - let oper_per_iteration = 1000; - let mut oper_count = 0; - let mut iterations = 0; - while SystemTime::now().le(&end) { - run_fuzz_mixed(oper_per_iteration, (-20, 20)); - oper_count += oper_per_iteration; - iterations += 1; - } - println!("operations: {}", oper_count); - println!("iterations: {}", iterations); + run_for_duration(60, oper_per_iteration, |count| { + run_fuzz_mixed(count, (-20, 20)); + }); } #[test] fn test_order_fuzz_mixed_single_tick() { - run_fuzz_mixed(1000, (0, 0)); + let oper_per_iteration = 1000; + + run_for_duration(60, oper_per_iteration, |count| { + run_fuzz_mixed(count, (0, 0)); + }); } /// Runs a linear fuzz test with the following steps diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs index 56305d4..41db60f 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs @@ -8,7 +8,7 @@ macro_rules! setup { ]) .unwrap(); - // use osmosis_test_tube::Account; + let t = $crate::tests::e2e::test_env::TestEnvBuilder::new() .with_account( "user1", @@ -66,6 +66,7 @@ macro_rules! setup { assert!(is_active); // NOTE: wasm_sudo does not currently maintain state so these calls will not work + // use osmosis_test_tube::Account; // t.contract.set_admin($app, cosmwasm_std::Addr::unchecked(&t.accounts["contract_admin"].address())); // t.contract // .set_maker_fee(&t.accounts["contract_admin"], Decimal256::percent($maker_fee), &t.accounts["maker_fee_recipient"]); From 4c7b7599455a215a4627d592654d4c452b57e29d Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Sun, 30 Jun 2024 11:09:26 +0100 Subject: [PATCH 66/98] test: added more comments and adjusted assertions for linear fuzz --- .../src/tests/e2e/cases/test_fuzz.rs | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index 44bd482..55764fb 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -126,6 +126,8 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob // -- System Under Test -- + // -- Step 1: Place Limits -- + // Places the set amount of orders within the provided tick range // Orders will be cancelled with a chance equal to the provided cancel_probability // Tick state is verified after every order is placed (and cancelled) @@ -143,6 +145,8 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob assert::tick_invariants(&t); } + // -- Step 2: Place Market Orders -- + // For both directions fill the entire amount of liquidity available using market orders // For certain cases it is not possible to fill the entire liquidity so a remainder of 1 may occur for order_direction in [OrderDirection::Bid, OrderDirection::Ask] { @@ -194,11 +198,12 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob .unwrap(); println!("Total remaining pool liquidity: {:?}", total_pool_liquidity); + // -- Step 3: Claim Orders -- + // Shuffle the order of recorded orders (as liquidity is fully filled (except the possibility of a 1 remainder)) // every order should be claimable and the order should not matter orders.shuffle(&mut rng); - let mut remainder_orders = 0; for (username, tick_id, order_id) in orders.iter() { // If the order has a claim bounty we will use a separate sender to verify that the bounty is claimed correctly // Otherwise we will use the original sender to verify that the order is claimed correctly @@ -230,19 +235,14 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob order.order_id, ); if let Some(order) = maybe_order { - println!("order: {:?}", order); - remainder_orders += 1; + orders::cancel_limit_and_assert_balance(&t, username, order.tick_id, order.order_id) + .unwrap(); } } // -- Post Test Assertions -- - - // Assert orders were filled correctly - assert!( - remainder_orders <= 2, - "There should be at most 2 orders that have a remainder, received {}", - remainder_orders - ); + assert::clean_ticks(&t); + assert::no_remaining_orders(&t); } #[derive(Debug, Eq, PartialEq, Hash)] @@ -522,7 +522,10 @@ fn run_fuzz_mixed(amount_of_orders: u64, tick_bounds: (i64, i64)) { // -- Post test assertions -- // Assert every operation ran at least once successfully - assert!(oper_count.values().all(|c| *c > 0)); + assert!( + oper_count.values().all(|c| *c > 0), + "not all operations were used" + ); assert::no_remaining_orders(&t); assert::clean_ticks(&t); } From 225def7aefe1017aba6aade98d30d842293649b8 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Sun, 30 Jun 2024 11:28:38 +0100 Subject: [PATCH 67/98] test: fixed failing tests --- .../src/tests/e2e/cases/test_fuzz.rs | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index 55764fb..2318dc7 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -10,9 +10,10 @@ use rand::{rngs::StdRng, SeedableRng}; use super::utils::{assert, orders}; use crate::constants::MIN_TICK; -use crate::msg::{CalcOutAmtGivenInResponse, QueryMsg, SpotPriceResponse}; +use crate::msg::{CalcOutAmtGivenInResponse, QueryMsg}; use crate::tests::e2e::modules::cosmwasm_pool::CosmwasmPool; use crate::tick_math::{amount_to_value, tick_to_price, RoundingDirection}; +use crate::types::Orderbook; use crate::{ msg::{DenomsResponse, GetTotalPoolLiquidityResponse}, setup, @@ -94,16 +95,16 @@ fn test_order_fuzz_linear_single_tick() { fn test_order_fuzz_mixed() { let oper_per_iteration = 1000; - run_for_duration(60, oper_per_iteration, |count| { + run_for_duration(10, oper_per_iteration, |count| { run_fuzz_mixed(count, (-20, 20)); }); } #[test] fn test_order_fuzz_mixed_single_tick() { - let oper_per_iteration = 1000; + let oper_per_iteration = 13000; - run_for_duration(60, oper_per_iteration, |count| { + run_for_duration(60 * 60 * 2, oper_per_iteration, |count| { run_fuzz_mixed(count, (0, 0)); }); } @@ -331,8 +332,10 @@ impl MixedFuzzOperation { } // Place the order - place_random_market(cp, t, rng, &username, market_direction, max_amount); - Ok(true) + let amount = + place_random_market(cp, t, rng, &username, market_direction, max_amount); + + Ok(amount != 0) } MixedFuzzOperation::CancelLimit => { // If there are no active orders skip the operation @@ -606,14 +609,20 @@ fn place_random_market( } else { ("base", "quote") }; - // Get the spot price for the given denoms - let SpotPriceResponse { spot_price } = t - .contract - .query(&QueryMsg::SpotPrice { - base_asset_denom: token_in_denom.to_string(), - quote_asset_denom: token_out_denom.to_string(), - }) - .unwrap(); + + // Get the next tick to determine liquidity + let Orderbook { + next_ask_tick, + next_bid_tick, + .. + } = t.contract.query(&QueryMsg::OrderbookState {}).unwrap(); + let next_tick = if order_direction == OrderDirection::Bid { + next_ask_tick + } else { + next_bid_tick + }; + + let price = tick_to_price(next_tick).unwrap(); // Determine how much liquidity is available for token in at the current spot price // This only provides an estimate as the liquidity may be spread across multiple ticks @@ -621,7 +630,7 @@ fn place_random_market( let liquidity_at_price_u256 = amount_to_value( order_direction.opposite(), Uint128::from(max), - Decimal256::from(spot_price), + price, RoundingDirection::Up, ) .unwrap(); From 1c575e42505b0e27fdde0bf9d5a6b6924d0458a4 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Sun, 30 Jun 2024 14:28:40 +0100 Subject: [PATCH 68/98] fix: skip ticks with no liquidity --- contracts/sumtree-orderbook/src/order.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/sumtree-orderbook/src/order.rs b/contracts/sumtree-orderbook/src/order.rs index 0501c74..7122523 100644 --- a/contracts/sumtree-orderbook/src/order.rs +++ b/contracts/sumtree-orderbook/src/order.rs @@ -534,6 +534,10 @@ pub(crate) fn run_market_order_internal( break; } + if current_tick_values.total_amount_of_liquidity.is_zero() { + continue; + } + // Update current tick pointer as we visit ticks that contribute to filling the order match order.order_direction.opposite() { OrderDirection::Ask => orderbook.next_ask_tick = current_tick_id, From cfa55aaca8ab88abe1c46a0215239d4475700632 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Sun, 30 Jun 2024 14:28:50 +0100 Subject: [PATCH 69/98] test: fix market order generation --- .../src/tests/e2e/cases/test_fuzz.rs | 74 +++++++------------ .../src/tests/e2e/test_env.rs | 37 +++++++++- 2 files changed, 63 insertions(+), 48 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index 2318dc7..70f658e 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -52,7 +52,10 @@ fn run_for_duration( #[test] fn test_order_fuzz_linear_large_orders_small_range() { - run_fuzz_linear(2000, (-10, 10), 0.2); + let oper_per_iteration = 10000; + run_for_duration(60 * 60 * 2, oper_per_iteration, |count| { + run_fuzz_linear(count, (-10, 10), 0.2); + }); } #[test] @@ -66,29 +69,32 @@ fn test_order_fuzz_linear_small_orders_large_range() { // run_fuzz_linear(5000, (LARGE_NEGATIVE_TICK, LARGE_POSITIVE_TICK), 0.2); // } -#[test] -fn test_order_fuzz_linear_small_orders_small_range() { - run_fuzz_linear(100, (-10, 0), 0.1); -} +// #[test] +// fn test_order_fuzz_linear_small_orders_small_range() { +// run_fuzz_linear(100, (-10, 0), 0.1); +// } #[test] fn test_order_fuzz_linear_large_cancelled_orders_small_range() { run_fuzz_linear(1000, (MIN_TICK, MIN_TICK + 20), 0.8); } -#[test] -fn test_order_fuzz_linear_small_cancelled_orders_large_range() { - run_fuzz_linear(100, (LARGE_NEGATIVE_TICK, LARGE_POSITIVE_TICK), 0.8); -} +// #[test] +// fn test_order_fuzz_linear_small_cancelled_orders_large_range() { +// run_fuzz_linear(100, (LARGE_NEGATIVE_TICK, LARGE_POSITIVE_TICK), 0.8); +// } -#[test] -fn test_order_fuzz_linear_large_all_cancelled_orders_small_range() { - run_fuzz_linear(1000, (-10, 10), 1.0); -} +// #[test] +// fn test_order_fuzz_linear_large_all_cancelled_orders_small_range() { +// run_fuzz_linear(1000, (-10, 10), 1.0); +// } #[test] fn test_order_fuzz_linear_single_tick() { - run_fuzz_linear(2000, (0, 0), 0.2); + let oper_per_iteration = 1000; + run_for_duration(10, oper_per_iteration, |count| { + run_fuzz_linear(count, (0, 0), 0.2); + }); } #[test] @@ -102,9 +108,9 @@ fn test_order_fuzz_mixed() { #[test] fn test_order_fuzz_mixed_single_tick() { - let oper_per_iteration = 13000; + let oper_per_iteration = 1000; - run_for_duration(60 * 60 * 2, oper_per_iteration, |count| { + run_for_duration(10, oper_per_iteration, |count| { run_fuzz_mixed(count, (0, 0)); }); } @@ -163,7 +169,7 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob while liquidity > 1u128 { let username = format!("user{}{}", order_direction, user_id); let placed_amount = - place_random_market(&cp, &mut t, &mut rng, &username, order_direction, liquidity); + place_random_market(&cp, &mut t, &mut rng, &username, order_direction); // Increment the username of the order placer user_id += 1; @@ -332,8 +338,7 @@ impl MixedFuzzOperation { } // Place the order - let amount = - place_random_market(cp, t, rng, &username, market_direction, max_amount); + let amount = place_random_market(cp, t, rng, &username, market_direction); Ok(amount != 0) } @@ -601,7 +606,6 @@ fn place_random_market( rng: &mut StdRng, username: &str, order_direction: OrderDirection, - max: u128, ) -> u128 { // Get the appropriate denom for the chosen direction let (token_in_denom, token_out_denom) = if order_direction == OrderDirection::Bid { @@ -610,34 +614,10 @@ fn place_random_market( ("base", "quote") }; - // Get the next tick to determine liquidity - let Orderbook { - next_ask_tick, - next_bid_tick, - .. - } = t.contract.query(&QueryMsg::OrderbookState {}).unwrap(); - let next_tick = if order_direction == OrderDirection::Bid { - next_ask_tick - } else { - next_bid_tick - }; - - let price = tick_to_price(next_tick).unwrap(); - - // Determine how much liquidity is available for token in at the current spot price - // This only provides an estimate as the liquidity may be spread across multiple ticks - // Hence why it can be difficult to fill the ENTIRE liquidity - let liquidity_at_price_u256 = amount_to_value( - order_direction.opposite(), - Uint128::from(max), - price, - RoundingDirection::Up, - ) - .unwrap(); - // Select a random amount of the token in to swap - let liquidity_at_price = Uint128::try_from(liquidity_at_price_u256).unwrap(); - let amount = rng.gen_range(0..=liquidity_at_price.u128()); + // let liquidity_at_price = Uint128::try_from(liquidity_at_price_u256).unwrap(); + let max_amount = t.contract.get_max_market_amount(order_direction); + let amount = rng.gen_range(0..=max_amount); // Calculate the expected amount of token out let expected_out = diff --git a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs index dba6248..27a075b 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, path::PathBuf}; +use std::{collections::HashMap, path::PathBuf, str::FromStr}; use crate::{ constants::{MAX_TICK, MIN_TICK}, @@ -7,6 +7,8 @@ use crate::{ GetUnrealizedCancelsResponse, InstantiateMsg, OrdersResponse, QueryMsg, SudoMsg, TickIdAndState, TickUnrealizedCancels, TicksResponse, }, + tests::test_utils::decimal256_from_u128, + tick_math::{amount_to_value, tick_to_price, RoundingDirection}, types::{LimitOrder, OrderDirection}, ContractError, }; @@ -380,6 +382,39 @@ impl<'a> OrderbookContract<'a> { .unwrap(); orders.first().cloned() } + + pub fn get_max_market_amount(&self, direction: OrderDirection) -> u128 { + let mut max_amount: u128 = 0; + let ticks = self.collect_all_ticks(); + for tick in ticks { + let value = tick.tick_state.get_values(direction.opposite()); + if value.total_amount_of_liquidity.is_zero() { + continue; + } + + let price = tick_to_price(tick.tick_id).unwrap(); + let amount_of_liquidity = Uint128::from_str( + &(value + .total_amount_of_liquidity + .min(decimal256_from_u128(u128::MAX))) + .to_string(), + ) + .unwrap(); + let amount_u256 = amount_to_value( + direction.opposite(), + amount_of_liquidity, + price, + RoundingDirection::Up, + ) + .unwrap(); + let amount = + Uint128::from_str(&(amount_u256.min(Uint256::from_u128(u128::MAX))).to_string()) + .unwrap(); + + max_amount += amount.u128(); + } + max_amount + } } pub fn _assert_contract_err(expected: ContractError, actual: RunnerError) { From b5197c2e3cedec68fe09fb37ea7bdf3ab33a1a0b Mon Sep 17 00:00:00 2001 From: alpo Date: Sun, 30 Jun 2024 15:30:03 -0700 Subject: [PATCH 70/98] fix price computation and recompute base market order test cases --- .../sumtree-orderbook/src/tests/test_order.rs | 78 ++++++++++--------- contracts/sumtree-orderbook/src/tick_math.rs | 4 +- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index dfa3dfc..1c74366 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -655,21 +655,23 @@ fn test_run_market_order() { orders: generate_limit_orders( &[-1500000], // 1000 units of liquidity total - 10, + 12, default_quantity, OrderDirection::Ask, ), // Bidding 1000 units of input into tick -1500000, which corresponds to $0.85, - // implies 1000*0.85 = 850 units of output. - expected_output: Uint256::from_u128(850), - expected_tick_etas: vec![(-1500000, decimal256_from_u128(Uint128::new(850)))], + // implies 1000 / 0.85 = 1176 units of output. As a sanity check, a bid is a + // buy on the other asset, so we should expect >1000 output due to the price + // being below $1. + expected_output: Uint256::from_u128(1176), + expected_tick_etas: vec![(-1500000, decimal256_from_u128(Uint128::new(1176)))], expected_tick_pointers: vec![(OrderDirection::Ask, -1500000)], expected_error: None, }, RunMarketOrderTestCase { name: "happy path bid at positive tick", placed_order: MarketOrder::new( - Uint128::new(1000), + Uint128::new(100_000), OrderDirection::Bid, Addr::unchecked(DEFAULT_SENDER), ), @@ -680,15 +682,15 @@ fn test_run_market_order() { // Two orders with sufficient total liquidity to process the // full market order 2, - Uint128::new(25_000_000), + Uint128::new(1), OrderDirection::Ask, ), - // Bidding 1000 units of input into tick 40,000,000, which corresponds to a + // Bidding 100,000 units of input into tick 40,000,000, which corresponds to a // price of $50000 (from tick math test cases). // - // This implies 1000*50000 = 50,000,000 units of output. - expected_output: Uint256::from_u128(50_000_000), - expected_tick_etas: vec![(40000000, decimal256_from_u128(Uint128::new(50_000_000)))], + // This implies 100,000 / 50,000 = 2 units of output. + expected_output: Uint256::from_u128(2), + expected_tick_etas: vec![(40000000, decimal256_from_u128(Uint128::new(2)))], expected_tick_pointers: vec![(OrderDirection::Ask, 40000000)], expected_error: None, }, @@ -706,23 +708,23 @@ fn test_run_market_order() { // Four limit orders with sufficient total liquidity to process the // full market order 4, - Uint128::new(3), + Uint128::new(20_250), OrderDirection::Ask, ), // Bidding 1000 units of input into tick -17765433, which corresponds to a // price of $0.012345670000000000 (from tick math test cases). // - // This implies 1000*0.012345670000000000 = 12.34567 units of output, + // This implies 1000 / 0.012345670000000000 = 81,000 units of output, // truncated to 12 units. - expected_output: Uint256::from_u128(12), - expected_tick_etas: vec![(-17765433, decimal256_from_u128(Uint128::new(12)))], + expected_output: Uint256::from_u128(81_000), + expected_tick_etas: vec![(-17765433, decimal256_from_u128(Uint128::new(81_000)))], expected_tick_pointers: vec![(OrderDirection::Ask, -17765433)], expected_error: None, }, RunMarketOrderTestCase { name: "bid across multiple ticks", placed_order: MarketOrder::new( - Uint128::new(589 + 1), + Uint128::new(510 + 3), OrderDirection::Bid, Addr::unchecked(DEFAULT_SENDER), ), @@ -730,26 +732,26 @@ fn test_run_market_order() { // Orders to fill against orders: generate_limit_orders( &[-1500000, 1500000], - // 500 units of liquidity on each tick - 5, + // 600 units of liquidity on each tick + 6, default_quantity, OrderDirection::Ask, ), // Bidding 1000 units of input into tick -1500000, which corresponds to $0.85, - // implies 1000*0.85 = 850 units of output, but there is only 500 on the tick. + // implies 1000 / 0.85 = 1176 units of output, but there is only 600 on the tick. // - // So 500 gets filled at -1500000, corresponding to ~589 of the input (500/0.85). - // The remaining 1 unit is filled at tick 1500000 (price $2.5), which - // corresponds to the remaining liquidity. + // So 600 gets filled at -1500000, corresponding to ~510 of the input (600 * 0.85). + // The remaining 3 units of input is filled at tick 1500000 (price $2.5), which + // corresponds to the remaining liquidity (3 / 2.5 = 1.2 -> truncated to 1). // - // Thus, the total expected output is 502. + // Thus, the total expected output is 600 + 1. // // Note: this case does not cover rounding for input consumption since it overfills // the tick. - expected_output: Uint256::from_u128(502), + expected_output: Uint256::from_u128(601), expected_tick_etas: vec![ - (-1500000, decimal256_from_u128(Uint128::new(500))), - (1500000, decimal256_from_u128(Uint128::new(2))), + (-1500000, decimal256_from_u128(Uint128::new(600))), + (1500000, decimal256_from_u128(Uint128::new(1))), ], expected_tick_pointers: vec![(OrderDirection::Ask, 1500000)], expected_error: None, @@ -757,7 +759,7 @@ fn test_run_market_order() { RunMarketOrderTestCase { name: "happy path ask at positive tick", placed_order: MarketOrder::new( - Uint128::new(100000), + Uint128::new(100), OrderDirection::Ask, Addr::unchecked(DEFAULT_SENDER), ), @@ -767,16 +769,16 @@ fn test_run_market_order() { &[40000000], // Two orders with sufficient total liquidity to process the // full market order - 2, - Uint128::new(1), + 5, + Uint128::new(1_000_000), OrderDirection::Bid, ), - // Asking 100,000 units of input into tick 40,000,000, which corresponds to a - // price of $1/50000 (from tick math test cases). + // Asking 100 units of input into tick 40,000,000, which corresponds to a + // price of $50,000 (from tick math test cases). // - // This implies 100,000/50000 = 2 units of output. - expected_output: Uint256::from_u128(2), - expected_tick_etas: vec![(40000000, decimal256_from_u128(Uint128::new(2)))], + // This implies 100 * 50,000 = 5,000,000 units of output. + expected_output: Uint256::from_u128(5_000_000), + expected_tick_etas: vec![(40000000, decimal256_from_u128(Uint128::new(5_000_000)))], expected_tick_pointers: vec![(OrderDirection::Bid, 40000000)], expected_error: None, }, @@ -794,16 +796,16 @@ fn test_run_market_order() { // Two orders with sufficient total liquidity to process the // full market order 2, - Uint128::new(50_000), + Uint128::new(10), OrderDirection::Bid, ), // The order asks with 1000 units of input into tick -17765433, which corresponds // to a price of $0.012345670000000000 (from tick math test cases). // - // This implies 1000 / 0.012345670000000000 = 81,000.059 units of output, - // which gets truncated to 81,000 units. - expected_output: Uint256::from_u128(81_000), - expected_tick_etas: vec![(-17765433, decimal256_from_u128(Uint128::new(81_000)))], + // This implies 1000 * 0.012345670000000000 = 12.34567 units of output, + // which gets truncated to 12 units. + expected_output: Uint256::from_u128(12), + expected_tick_etas: vec![(-17765433, decimal256_from_u128(Uint128::new(12)))], expected_tick_pointers: vec![(OrderDirection::Bid, -17765433)], expected_error: None, }, diff --git a/contracts/sumtree-orderbook/src/tick_math.rs b/contracts/sumtree-orderbook/src/tick_math.rs index 15f05e4..06db154 100644 --- a/contracts/sumtree-orderbook/src/tick_math.rs +++ b/contracts/sumtree-orderbook/src/tick_math.rs @@ -146,7 +146,7 @@ pub fn amount_to_value( return Ok(Uint256::zero()); } match order { - OrderDirection::Bid => multiply_by_price(amount, price, rounding_direction), - OrderDirection::Ask => divide_by_price(amount, price, rounding_direction), + OrderDirection::Bid => divide_by_price(amount, price, rounding_direction), + OrderDirection::Ask => multiply_by_price(amount, price, rounding_direction), } } From 1a4c2e0a64d6f5d616f3b373e8883f79fdeb8a2d Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 1 Jul 2024 09:48:07 +0100 Subject: [PATCH 71/98] fix: corrected price calculation and related e2e tests --- contracts/sumtree-orderbook/src/order.rs | 9 +- contracts/sumtree-orderbook/src/query.rs | 4 +- .../src/tests/e2e/cases/test_fuzz.rs | 119 +++++++++++------- .../tests/e2e/cases/test_orders_success.rs | 51 +++++++- .../src/tests/e2e/cases/utils.rs | 70 +++++++++-- .../src/tests/e2e/test_env.rs | 8 +- contracts/sumtree-orderbook/src/tick_math.rs | 4 +- 7 files changed, 201 insertions(+), 64 deletions(-) diff --git a/contracts/sumtree-orderbook/src/order.rs b/contracts/sumtree-orderbook/src/order.rs index 7122523..ea6ad9b 100644 --- a/contracts/sumtree-orderbook/src/order.rs +++ b/contracts/sumtree-orderbook/src/order.rs @@ -516,6 +516,11 @@ pub(crate) fn run_market_order_internal( let current_tick_id = maybe_current_tick?; let mut current_tick = TICK_STATE.load(storage, current_tick_id)?; let mut current_tick_values = current_tick.get_values(order.order_direction.opposite()); + + if current_tick_values.total_amount_of_liquidity.is_zero() { + continue; + } + let tick_price = tick_to_price(current_tick_id)?; last_tick_price = tick_price; @@ -534,10 +539,6 @@ pub(crate) fn run_market_order_internal( break; } - if current_tick_values.total_amount_of_liquidity.is_zero() { - continue; - } - // Update current tick pointer as we visit ticks that contribute to filling the order match order.order_direction.opposite() { OrderDirection::Ask => orderbook.next_ask_tick = current_tick_id, diff --git a/contracts/sumtree-orderbook/src/query.rs b/contracts/sumtree-orderbook/src/query.rs index c867e05..6e951a4 100644 --- a/contracts/sumtree-orderbook/src/query.rs +++ b/contracts/sumtree-orderbook/src/query.rs @@ -60,8 +60,8 @@ pub(crate) fn spot_price( let price = tick_to_price(next_tick)?; let spot_price = match direction { - OrderDirection::Ask => price.inv().unwrap(), - OrderDirection::Bid => price, + OrderDirection::Ask => price, + OrderDirection::Bid => price.inv().unwrap(), }; Ok(SpotPriceResponse { diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index 70f658e..5e2aaee 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -9,11 +9,10 @@ use rand::Rng; use rand::{rngs::StdRng, SeedableRng}; use super::utils::{assert, orders}; -use crate::constants::MIN_TICK; +use crate::constants::{MAX_TICK, MIN_TICK}; use crate::msg::{CalcOutAmtGivenInResponse, QueryMsg}; use crate::tests::e2e::modules::cosmwasm_pool::CosmwasmPool; use crate::tick_math::{amount_to_value, tick_to_price, RoundingDirection}; -use crate::types::Orderbook; use crate::{ msg::{DenomsResponse, GetTotalPoolLiquidityResponse}, setup, @@ -52,15 +51,18 @@ fn run_for_duration( #[test] fn test_order_fuzz_linear_large_orders_small_range() { - let oper_per_iteration = 10000; - run_for_duration(60 * 60 * 2, oper_per_iteration, |count| { + let oper_per_iteration = 1000; + run_for_duration(60, oper_per_iteration, |count| { run_fuzz_linear(count, (-10, 10), 0.2); }); } #[test] fn test_order_fuzz_linear_small_orders_large_range() { - run_fuzz_linear(100, (LARGE_NEGATIVE_TICK, LARGE_POSITIVE_TICK), 0.2); + let oper_per_iteration = 100; + run_for_duration(60, oper_per_iteration, |count| { + run_fuzz_linear(count, (LARGE_NEGATIVE_TICK, LARGE_POSITIVE_TICK), 0.2); + }); } // This test takes a VERY long time to run @@ -69,25 +71,37 @@ fn test_order_fuzz_linear_small_orders_large_range() { // run_fuzz_linear(5000, (LARGE_NEGATIVE_TICK, LARGE_POSITIVE_TICK), 0.2); // } -// #[test] -// fn test_order_fuzz_linear_small_orders_small_range() { -// run_fuzz_linear(100, (-10, 0), 0.1); -// } +#[test] +fn test_order_fuzz_linear_small_orders_small_range() { + let oper_per_iteration = 100; + run_for_duration(60, oper_per_iteration, |count| { + run_fuzz_linear(count, (-10, 0), 0.1); + }); +} #[test] fn test_order_fuzz_linear_large_cancelled_orders_small_range() { - run_fuzz_linear(1000, (MIN_TICK, MIN_TICK + 20), 0.8); + let oper_per_iteration = 1000; + run_for_duration(60, oper_per_iteration, |count| { + run_fuzz_linear(count, (MIN_TICK, MIN_TICK + 20), 0.8); + }); } -// #[test] -// fn test_order_fuzz_linear_small_cancelled_orders_large_range() { -// run_fuzz_linear(100, (LARGE_NEGATIVE_TICK, LARGE_POSITIVE_TICK), 0.8); -// } +#[test] +fn test_order_fuzz_linear_small_cancelled_orders_large_range() { + let oper_per_iteration = 100; + run_for_duration(60, oper_per_iteration, |count| { + run_fuzz_linear(count, (LARGE_NEGATIVE_TICK, LARGE_POSITIVE_TICK), 0.8); + }); +} -// #[test] -// fn test_order_fuzz_linear_large_all_cancelled_orders_small_range() { -// run_fuzz_linear(1000, (-10, 10), 1.0); -// } +#[test] +fn test_order_fuzz_linear_large_all_cancelled_orders_small_range() { + let oper_per_iteration = 1000; + run_for_duration(60, oper_per_iteration, |count| { + run_fuzz_linear(count, (-10, 10), 1.0); + }); +} #[test] fn test_order_fuzz_linear_single_tick() { @@ -100,8 +114,7 @@ fn test_order_fuzz_linear_single_tick() { #[test] fn test_order_fuzz_mixed() { let oper_per_iteration = 1000; - - run_for_duration(10, oper_per_iteration, |count| { + run_for_duration(60, oper_per_iteration, |count| { run_fuzz_mixed(count, (-20, 20)); }); } @@ -110,11 +123,47 @@ fn test_order_fuzz_mixed() { fn test_order_fuzz_mixed_single_tick() { let oper_per_iteration = 1000; - run_for_duration(10, oper_per_iteration, |count| { + run_for_duration(60, oper_per_iteration, |count| { run_fuzz_mixed(count, (0, 0)); }); } +#[test] +fn test_order_fuzz_mixed_large_negative_tick_range() { + let oper_per_iteration = 1000; + + run_for_duration(30, oper_per_iteration, |count| { + run_fuzz_mixed(count, (LARGE_NEGATIVE_TICK, LARGE_NEGATIVE_TICK + 10)); + }); +} + +#[test] +fn test_order_fuzz_mixed_large_positive_tick_range() { + let oper_per_iteration = 1000; + + run_for_duration(30, oper_per_iteration, |count| { + run_fuzz_mixed(count, (LARGE_POSITIVE_TICK - 10, LARGE_POSITIVE_TICK)); + }); +} + +#[test] +fn test_order_fuzz_mixed_min_tick() { + let oper_per_iteration = 1000; + + run_for_duration(30, oper_per_iteration, |count| { + run_fuzz_mixed(count, (MIN_TICK, MIN_TICK + 10)); + }); +} + +#[test] +fn test_order_fuzz_large_tick_range() { + let oper_per_iteration = 1000; + + run_for_duration(30, oper_per_iteration, |count| { + run_fuzz_mixed(count, (MIN_TICK, MAX_TICK)); + }); +} + /// Runs a linear fuzz test with the following steps /// 1. Place x amount of random limit orders in given tick range and cancel with provided probability /// 2. For both directions fill the entire amount of liquidity available using market orders @@ -294,27 +343,8 @@ impl MixedFuzzOperation { let username = format!("user{}", iteration); match self { MixedFuzzOperation::PlaceLimit => { - // Determine tick range by finding the minimum and maximum tick ids of the orderbook - // Ticks are bounded by the provided tick range - // The concept is that ticks may randomly shift up and down until they reach the desired bounds - let all_ticks = t.contract.collect_all_ticks(); - let tick_range = ( - all_ticks - .iter() - .min_by_key(|f| f.tick_id) - .map(|f| f.tick_id) - .unwrap_or(0) - - 1.min(tick_bounds.1).max(tick_bounds.0), - all_ticks - .iter() - .max_by_key(|f| f.tick_id) - .map(|f| f.tick_id) - .unwrap_or(0) - + 1.min(tick_bounds.1).max(tick_bounds.0), - ); - // Place the limit order - let tick_id = place_random_limit(t, rng, &username, tick_range); + let tick_id = place_random_limit(t, rng, &username, tick_bounds); // Record the order for claims/cancels orders.insert(*order_count, (username, tick_id)); *order_count += 1; @@ -619,6 +649,10 @@ fn place_random_market( let max_amount = t.contract.get_max_market_amount(order_direction); let amount = rng.gen_range(0..=max_amount); + if amount == 0 { + return 0; + } + // Calculate the expected amount of token out let expected_out = t.contract @@ -632,7 +666,8 @@ fn place_random_market( if expected_out.token_out.amount == "0" { return 0; } - } else if expected_out.is_err() || amount == 0 { + } else if expected_out.is_err() { + println!("expected_out: {:?}", expected_out); return 0; } diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs index e9384d6..9d56de1 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs @@ -1,7 +1,10 @@ -use cosmwasm_std::{Decimal256, Uint128}; +use cosmwasm_std::{coin, Decimal256, Uint128}; use osmosis_test_tube::{Module, OsmosisTestApp}; -use super::utils::{assert, orders}; +use super::{ + test_fuzz::LARGE_NEGATIVE_TICK, + utils::{assert, orders}, +}; use crate::{ constants::{MAX_TICK, MIN_TICK}, setup, @@ -209,3 +212,47 @@ fn test_cancelled_orders() { orders::claim(&t, "user1", 0, amount_orders).unwrap(); assert::tick_invariants(&t); } + +#[test] +fn test_tick_extremes() { + let app = OsmosisTestApp::new(); + let cp = CosmwasmPool::new(&app); + let mut t = setup!(&app, "quote", "base", 0); + + t.add_account( + "newuser", + vec![ + coin(u128::MAX, "base"), + coin(u128::MAX, "quote"), + coin(u128::MAX, "uosmo"), + ], + ); + + orders::place_limit( + &t, + MIN_TICK, + OrderDirection::Ask, + Uint128::MAX.checked_div(Uint128::from(2u128)).unwrap(), + None, + "newuser", + ) + .unwrap(); + orders::place_limit( + &t, + LARGE_NEGATIVE_TICK, + OrderDirection::Ask, + Uint128::MAX.checked_div(Uint128::from(2u128)).unwrap(), + None, + "newuser", + ) + .unwrap(); + + orders::place_market_and_assert_balance( + &cp, + &t, + OrderDirection::Bid, + Uint128::from(2u128), + "newuser", + ) + .unwrap(); +} diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs index 41db60f..4ca28ba 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs @@ -78,16 +78,18 @@ macro_rules! setup { // -- Assertions -- // Assertions about current state pub mod assert { + use std::str::FromStr; + use crate::{ msg::{ DenomsResponse, GetTotalPoolLiquidityResponse, GetUnrealizedCancelsResponse, QueryMsg, SpotPriceResponse, }, tests::e2e::test_env::TestEnv, - tick_math::tick_to_price, + tick_math::{amount_to_value, tick_to_price, RoundingDirection}, types::{OrderDirection, Orderbook}, }; - use cosmwasm_std::{Coin, Coins}; + use cosmwasm_std::{Coin, Coins, Fraction, Uint128}; use osmosis_test_tube::{cosmrs::proto::prost::Message, RunnerExecuteResult}; // -- Contract State Assertions @@ -155,8 +157,8 @@ pub mod assert { } = t.contract.get_denoms(); for (base_denom, quote_denom, price, direction) in [ - (base_denom.clone(), quote_denom.clone(), ask_price, "ask"), - (quote_denom, base_denom, bid_price, "bid"), + (base_denom.clone(), quote_denom.clone(), bid_price, "ask"), + (quote_denom, base_denom, ask_price.inv().unwrap(), "bid"), ] { let SpotPriceResponse { spot_price } = t .contract @@ -219,18 +221,60 @@ pub mod assert { <= t.tick_state.bid_values.cumulative_total_value)); let ticks_with_bid_amount = ticks.iter().filter(|tick| { - !tick + if tick .tick_state .get_values(OrderDirection::Bid) .total_amount_of_liquidity .is_zero() + { + return false; + } + + let price = tick_to_price(tick.tick_id).unwrap(); + let amount_of_liquidity = Uint128::from_str( + &tick + .tick_state + .get_values(OrderDirection::Bid) + .total_amount_of_liquidity + .to_string(), + ) + .unwrap(); + let fillable_amount = amount_to_value( + OrderDirection::Bid, + amount_of_liquidity, + price, + RoundingDirection::Down, + ) + .unwrap(); + !fillable_amount.is_zero() }); let ticks_with_ask_amount = ticks.iter().filter(|tick| { - !tick + if tick .tick_state .get_values(OrderDirection::Ask) .total_amount_of_liquidity .is_zero() + { + return false; + } + + let price = tick_to_price(tick.tick_id).unwrap(); + let amount_of_liquidity = Uint128::from_str( + &tick + .tick_state + .get_values(OrderDirection::Ask) + .total_amount_of_liquidity + .to_string(), + ) + .unwrap(); + let fillable_amount = amount_to_value( + OrderDirection::Ask, + amount_of_liquidity, + price, + RoundingDirection::Down, + ) + .unwrap(); + !fillable_amount.is_zero() }); let max_tick_with_bid = ticks_with_bid_amount.max_by_key(|tick| tick.tick_id); let min_tick_with_ask = ticks_with_ask_amount.min_by_key(|tick| tick.tick_id); @@ -241,10 +285,20 @@ pub mod assert { .. } = t.contract.query(&QueryMsg::OrderbookState {}).unwrap(); if let Some(min_tick_with_ask) = min_tick_with_ask { - assert!(next_ask_tick <= min_tick_with_ask.tick_id); + assert!( + next_ask_tick <= min_tick_with_ask.tick_id, + "ASK TICK: got: {}, expected: {}", + next_ask_tick, + min_tick_with_ask.tick_id + ); } if let Some(max_tick_with_bid) = max_tick_with_bid { - assert!(next_bid_tick >= max_tick_with_bid.tick_id); + assert!( + next_bid_tick >= max_tick_with_bid.tick_id, + "BID TICK: got: {}, expected: {}", + next_bid_tick, + max_tick_with_bid.tick_id + ); } } diff --git a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs index 27a075b..8c39683 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs @@ -384,7 +384,7 @@ impl<'a> OrderbookContract<'a> { } pub fn get_max_market_amount(&self, direction: OrderDirection) -> u128 { - let mut max_amount: u128 = 0; + let mut max_amount: Uint128 = Uint128::zero(); let ticks = self.collect_all_ticks(); for tick in ticks { let value = tick.tick_state.get_values(direction.opposite()); @@ -404,16 +404,16 @@ impl<'a> OrderbookContract<'a> { direction.opposite(), amount_of_liquidity, price, - RoundingDirection::Up, + RoundingDirection::Down, ) .unwrap(); let amount = Uint128::from_str(&(amount_u256.min(Uint256::from_u128(u128::MAX))).to_string()) .unwrap(); - max_amount += amount.u128(); + max_amount = max_amount.saturating_add(amount); } - max_amount + max_amount.u128() } } diff --git a/contracts/sumtree-orderbook/src/tick_math.rs b/contracts/sumtree-orderbook/src/tick_math.rs index 15f05e4..b8e8e3b 100644 --- a/contracts/sumtree-orderbook/src/tick_math.rs +++ b/contracts/sumtree-orderbook/src/tick_math.rs @@ -146,7 +146,7 @@ pub fn amount_to_value( return Ok(Uint256::zero()); } match order { - OrderDirection::Bid => multiply_by_price(amount, price, rounding_direction), - OrderDirection::Ask => divide_by_price(amount, price, rounding_direction), + OrderDirection::Ask => multiply_by_price(amount, price, rounding_direction), + OrderDirection::Bid => divide_by_price(amount, price, rounding_direction), } } From 7628b9b2d0c4c97eefcdd668ebb55f2b649e8469 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 1 Jul 2024 09:59:11 +0100 Subject: [PATCH 72/98] fix: fixed spot price query and test --- contracts/sumtree-orderbook/src/query.rs | 4 ++-- .../sumtree-orderbook/src/tests/test_query.rs | 20 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/contracts/sumtree-orderbook/src/query.rs b/contracts/sumtree-orderbook/src/query.rs index b0092c9..8028f19 100644 --- a/contracts/sumtree-orderbook/src/query.rs +++ b/contracts/sumtree-orderbook/src/query.rs @@ -60,8 +60,8 @@ pub(crate) fn spot_price( let price = tick_to_price(next_tick)?; let spot_price = match direction { - OrderDirection::Ask => price.inv().unwrap(), - OrderDirection::Bid => price, + OrderDirection::Ask => price, + OrderDirection::Bid => price.inv().unwrap(), }; Ok(SpotPriceResponse { diff --git a/contracts/sumtree-orderbook/src/tests/test_query.rs b/contracts/sumtree-orderbook/src/tests/test_query.rs index 5faba40..c193729 100644 --- a/contracts/sumtree-orderbook/src/tests/test_query.rs +++ b/contracts/sumtree-orderbook/src/tests/test_query.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use cosmwasm_std::{ coin, testing::{mock_env, mock_info}, - Addr, Coin, Decimal, Decimal256, Uint128, + Addr, Coin, Decimal, Decimal256, Fraction, Uint128, }; use crate::{ @@ -137,14 +137,14 @@ fn test_query_spot_price() { None, )), OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(2u128), + Uint128::from(3u128), OrderDirection::Bid, sender.clone(), )), ], base_denom: QUOTE_DENOM.to_string(), quote_denom: BASE_DENOM.to_string(), - expected_price: Decimal::percent(200), + expected_price: Decimal::percent(200).inv().unwrap(), expected_error: None, }, SpotPriceTestCase { @@ -160,7 +160,7 @@ fn test_query_spot_price() { ))], base_denom: QUOTE_DENOM.to_string(), quote_denom: BASE_DENOM.to_string(), - expected_price: Decimal::from_ratio(340282300000000000000u128, 1u128), + expected_price: Decimal::from_ratio(1u128, 340282300000000000000u128), expected_error: None, }, SpotPriceTestCase { @@ -176,7 +176,7 @@ fn test_query_spot_price() { ))], base_denom: QUOTE_DENOM.to_string(), quote_denom: BASE_DENOM.to_string(), - expected_price: Decimal::from_str("0.000000000001").unwrap(), + expected_price: Decimal::from_str("1000000000000").unwrap(), expected_error: None, }, SpotPriceTestCase { @@ -280,14 +280,14 @@ fn test_query_spot_price() { None, )), OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(2u128), + Uint128::from(3u128), OrderDirection::Ask, sender.clone(), )), ], base_denom: BASE_DENOM.to_string(), quote_denom: QUOTE_DENOM.to_string(), - expected_price: Decimal::percent(200), + expected_price: Decimal::percent(50), expected_error: None, }, SpotPriceTestCase { @@ -310,7 +310,7 @@ fn test_query_spot_price() { ], base_denom: BASE_DENOM.to_string(), quote_denom: QUOTE_DENOM.to_string(), - expected_price: Decimal::percent(50), + expected_price: Decimal::percent(200), expected_error: None, }, SpotPriceTestCase { @@ -328,7 +328,7 @@ fn test_query_spot_price() { quote_denom: QUOTE_DENOM.to_string(), // At max tick the price is 2.9387365e-21 which is outside the range of the `Decimal` type // As such the returned price is zero - expected_price: Decimal::zero(), + expected_price: Decimal::from_str("340282300000000000000").unwrap(), expected_error: None, }, SpotPriceTestCase { @@ -344,7 +344,7 @@ fn test_query_spot_price() { ))], base_denom: BASE_DENOM.to_string(), quote_denom: QUOTE_DENOM.to_string(), - expected_price: Decimal::from_ratio(1000000000000u128, 1u128), + expected_price: Decimal::from_ratio(1u128, 1000000000000u128), expected_error: None, }, SpotPriceTestCase { From a2d6b6f566c2ba78cac1b48cd74af714ce53ca20 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 1 Jul 2024 10:01:51 +0100 Subject: [PATCH 73/98] test: fixed calc_out_amount_given_in test --- contracts/sumtree-orderbook/src/tests/test_query.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/test_query.rs b/contracts/sumtree-orderbook/src/tests/test_query.rs index c193729..10b76b1 100644 --- a/contracts/sumtree-orderbook/src/tests/test_query.rs +++ b/contracts/sumtree-orderbook/src/tests/test_query.rs @@ -517,8 +517,8 @@ fn test_calc_out_amount_given_in() { token_in: coin(150, QUOTE_DENOM), token_out_denom: BASE_DENOM, swap_fee: EXPECTED_SWAP_FEE, - // Output: 100*1 (tick: 0) + 50*2 (tick: LARGE_POSITIVE_TICK) = 200 - expected_output: coin_u256(200u128, BASE_DENOM), + // Output: 100*1 (tick: 0) + 50/2 (tick: LARGE_POSITIVE_TICK) = 125 + expected_output: coin_u256(125u128, BASE_DENOM), expected_error: None, }, CalcOutAmountGivenInTestCase { @@ -572,7 +572,7 @@ fn test_calc_out_amount_given_in() { 0, OrderDirection::Bid, sender.clone(), - Uint128::from(25u128), + Uint128::from(100u128), Decimal256::percent(0), None, )), @@ -598,8 +598,8 @@ fn test_calc_out_amount_given_in() { token_in: coin(150, BASE_DENOM), token_out_denom: QUOTE_DENOM, swap_fee: EXPECTED_SWAP_FEE, - // Output: 25 at 0.5 tick price + 100 at 1 tick price = 125 - expected_output: coin_u256(125u128, QUOTE_DENOM), + // Output: 50/0.5 at LARGE_POSITIVE_TICK tick price + 100 at 1 tick price = 200 + expected_output: coin_u256(200u128, QUOTE_DENOM), expected_error: None, }, CalcOutAmountGivenInTestCase { From fd05e99ff12f0acb46046c479e9a2d1118be9cee Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 1 Jul 2024 10:09:31 +0100 Subject: [PATCH 74/98] test: fixed run_market_order_moving_tick --- contracts/sumtree-orderbook/src/tests/test_order.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index 1c74366..8edf761 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -1054,7 +1054,8 @@ fn test_run_market_order_moving_tick() { )), // Fill all limits on tick 0 and 50% of tick 1, leaving tick 0 empty and forcing positive movement OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(15u128), + // We provide 16 here as rounding on the second tick causes this to fill 5 on the second tick + Uint128::from(16u128), OrderDirection::Bid, Addr::unchecked("buyer"), )), @@ -1295,7 +1296,8 @@ fn test_run_market_order_moving_tick() { )), // Fill entire first tick and 50% of next tick to force negative movement OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(15u128), + // We provide 16 here as rounding on the second tick causes this to fill 5 on the second tick + Uint128::from(16u128), OrderDirection::Ask, Addr::unchecked("buyer"), )), @@ -1372,7 +1374,8 @@ fn test_run_market_order_moving_tick() { )), // Fill entire first tick and 50% of next tick to force negative movement OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(15u128), + // We provide 16 here as rounding on the second tick causes this to fill 5 on the second tick + Uint128::from(16u128), OrderDirection::Ask, Addr::unchecked("buyer"), )), @@ -1469,7 +1472,8 @@ fn test_run_market_order_moving_tick() { )), // Fill entire first tick and 50% of second tick to force positive movement OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(15u128), + // We provide 16 here as rounding on the second tick causes this to fill 5 on the second tick + Uint128::from(16u128), OrderDirection::Bid, Addr::unchecked("buyer"), )), From 9924ca1de0e62f1b3129b2983c109013143551dd Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 1 Jul 2024 10:24:20 +0100 Subject: [PATCH 75/98] test: fixed test_claim_order --- .../sumtree-orderbook/src/tests/test_order.rs | 97 +++++++++---------- 1 file changed, 45 insertions(+), 52 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index 8edf761..4aea2a2 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -1853,8 +1853,8 @@ fn test_claim_order() { None, )), OrderOperation::RunMarket(MarketOrder::new( - // Tick price is 2, 2*5 = 10 - Uint128::from(5u128), + // Tick price is 2, 2*10 = 20 + Uint128::from(20u128), OrderDirection::Bid, Addr::unchecked("buyer"), )), @@ -1865,7 +1865,7 @@ fn test_claim_order() { MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), - amount: vec![coin_u256(Uint256::from(5u128), QUOTE_DENOM)], + amount: vec![coin_u256(Uint256::from(20u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, )), @@ -1887,8 +1887,8 @@ fn test_claim_order() { None, )), OrderOperation::RunMarket(MarketOrder::new( - // Tick price is 2, 2*2 = 4 - Uint128::from(2u128), + // Tick price is 2, 2*4 = 8 + Uint128::from(8u128), OrderDirection::Bid, Addr::unchecked("buyer"), )), @@ -1900,7 +1900,7 @@ fn test_claim_order() { MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), - amount: vec![coin_u256(Uint256::from(2u128), QUOTE_DENOM)], + amount: vec![coin_u256(Uint256::from(8u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, )), @@ -1930,15 +1930,16 @@ fn test_claim_order() { None, )), OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(2u128), + // Tick price is 2, 2*6 = 12 + Uint128::from(12u128), OrderDirection::Bid, Addr::unchecked("buyer"), )), // Claim the first partial fill OrderOperation::Claim((LARGE_POSITIVE_TICK, 0)), OrderOperation::RunMarket(MarketOrder::new( - // Tick price is 2, 2*3 = 6 - Uint128::from(3u128), + // Tick price is 2, 2*4 = 8 + Uint128::from(8u128), OrderDirection::Bid, Addr::unchecked("buyer"), )), @@ -1950,7 +1951,7 @@ fn test_claim_order() { MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), - amount: vec![coin_u256(Uint256::from(3u128), QUOTE_DENOM)], + amount: vec![coin_u256(Uint256::from(8u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, )), @@ -1973,7 +1974,7 @@ fn test_claim_order() { None, )), OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(200u128), + Uint128::from(50u128), OrderDirection::Bid, Addr::unchecked("buyer"), )), @@ -1985,7 +1986,7 @@ fn test_claim_order() { MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), - amount: vec![coin_u256(Uint256::from(200u128), QUOTE_DENOM)], + amount: vec![coin_u256(Uint256::from(50u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, )), @@ -2007,7 +2008,7 @@ fn test_claim_order() { None, )), OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(100u128), + Uint128::from(25u128), OrderDirection::Bid, Addr::unchecked("buyer"), )), @@ -2019,7 +2020,7 @@ fn test_claim_order() { MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), - amount: vec![coin_u256(Uint256::from(100u128), QUOTE_DENOM)], + amount: vec![coin_u256(Uint256::from(25u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, )), @@ -2049,14 +2050,14 @@ fn test_claim_order() { None, )), OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(100u128), + Uint128::from(25u128), OrderDirection::Bid, Addr::unchecked("buyer"), )), // Claim the first partial fill OrderOperation::Claim((LARGE_NEGATIVE_TICK, 0)), OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(100u128), + Uint128::from(25u128), OrderDirection::Bid, Addr::unchecked("buyer"), )), @@ -2068,7 +2069,7 @@ fn test_claim_order() { MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), - amount: vec![coin_u256(Uint256::from(100u128), QUOTE_DENOM)], + amount: vec![coin_u256(Uint256::from(25u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, )), @@ -2121,7 +2122,7 @@ fn test_claim_order() { expected_error: None, }, ClaimOrderTestCase { - name: "ASK: valid basic full claim at MIN_TICK", + name: "ASK: valid basic partial claim at MIN_TICK", sender: sender.clone(), operations: vec![ OrderOperation::PlaceLimit(LimitOrder::new( @@ -2129,7 +2130,7 @@ fn test_claim_order() { 0, OrderDirection::Ask, sender.clone(), - Uint128::from(10u128), + Uint128::from(3_000_000_000_000u128), Decimal256::zero(), None, )), @@ -2137,7 +2138,7 @@ fn test_claim_order() { // Tick price is 0.000000000001, so 3_333_333_333_333 * 0.000000000001 = 3.33333333333 // We expect this to get truncated to 3, as order outputs should always be rounding // in favor of the orderbook. - Uint128::from(3_000_000_000_000u128), + Uint128::from(3u128), OrderDirection::Bid, Addr::unchecked("buyer"), )), @@ -2149,20 +2150,12 @@ fn test_claim_order() { MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), - amount: vec![coin_u256(Uint256::from(3_000_000_000_000u128), QUOTE_DENOM)], + amount: vec![coin_u256(Uint256::from(3u128), QUOTE_DENOM)], }, REPLY_ID_CLAIM, )), expected_bounty_msg: None, - expected_order_state: Some(LimitOrder::new( - MIN_TICK, - 0, - OrderDirection::Ask, - sender.clone(), - Uint128::from(7u128), - decimal256_from_u128(3u128), - None, - ).with_placed_quantity(10u128)), + expected_order_state: None, expected_error: None, }, // A tick id of 0 operates on a tick price of 1 @@ -2298,7 +2291,7 @@ fn test_claim_order() { )), OrderOperation::RunMarket(MarketOrder::new( // Tick price is 2, 2*5 = 10 - Uint128::from(20u128), + Uint128::from(5u128), OrderDirection::Ask, Addr::unchecked("buyer"), )), @@ -2310,7 +2303,7 @@ fn test_claim_order() { MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), - amount: vec![coin_u256(Uint256::from(20u128), BASE_DENOM)], + amount: vec![coin_u256(Uint256::from(5u128), BASE_DENOM)], }, REPLY_ID_CLAIM, )), @@ -2332,7 +2325,7 @@ fn test_claim_order() { None, )), OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(10u128), + Uint128::from(2u128), OrderDirection::Ask, Addr::unchecked("buyer"), )), @@ -2344,7 +2337,7 @@ fn test_claim_order() { MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), - amount: vec![coin_u256(Uint256::from(10u128), BASE_DENOM)], + amount: vec![coin_u256(Uint256::from(2u128), BASE_DENOM)], }, REPLY_ID_CLAIM, )), @@ -2354,8 +2347,8 @@ fn test_claim_order() { 0, OrderDirection::Bid, sender.clone(), - Uint128::from(5u128), - decimal256_from_u128(5u128), + Uint128::from(6u128), + decimal256_from_u128(4u128), None, ).with_placed_quantity(10u128)), expected_error: None, @@ -2374,14 +2367,14 @@ fn test_claim_order() { None, )), OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(10u128), + Uint128::from(2u128), OrderDirection::Ask, Addr::unchecked("buyer"), )), // Claim the first partial fill OrderOperation::Claim((LARGE_POSITIVE_TICK, 0)), OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(10u128), + Uint128::from(3u128), OrderDirection::Ask, Addr::unchecked("buyer"), )), @@ -2393,7 +2386,7 @@ fn test_claim_order() { MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), - amount: vec![coin_u256(Uint256::from(10u128), BASE_DENOM)], + amount: vec![coin_u256(Uint256::from(3u128), BASE_DENOM)], }, REPLY_ID_CLAIM, )), @@ -2416,7 +2409,7 @@ fn test_claim_order() { None, )), OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(50u128), + Uint128::from(200u128), OrderDirection::Ask, Addr::unchecked("buyer"), )), @@ -2428,7 +2421,7 @@ fn test_claim_order() { MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), - amount: vec![coin_u256(Uint256::from(50u128), BASE_DENOM)], + amount: vec![coin_u256(Uint256::from(200u128), BASE_DENOM)], }, REPLY_ID_CLAIM, )), @@ -2450,7 +2443,7 @@ fn test_claim_order() { None, )), OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(25u128), + Uint128::from(50u128), OrderDirection::Ask, Addr::unchecked("buyer"), )), @@ -2462,7 +2455,7 @@ fn test_claim_order() { MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), - amount: vec![coin_u256(Uint256::from(25u128), BASE_DENOM)], + amount: vec![coin_u256(Uint256::from(50u128), BASE_DENOM)], }, REPLY_ID_CLAIM, )), @@ -2472,8 +2465,8 @@ fn test_claim_order() { 0, OrderDirection::Bid, sender.clone(), - Uint128::from(50u128), - decimal256_from_u128(50u128), + Uint128::from(75u128), + decimal256_from_u128(25u128), None, ).with_placed_quantity(100u128)), expected_error: None, @@ -2492,14 +2485,14 @@ fn test_claim_order() { None, )), OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(25u128), + Uint128::from(100u128), OrderDirection::Ask, Addr::unchecked("buyer"), )), // Claim the first partial fill OrderOperation::Claim((LARGE_NEGATIVE_TICK, 0)), OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(25u128), + Uint128::from(100u128), OrderDirection::Ask, Addr::unchecked("buyer"), )), @@ -2511,7 +2504,7 @@ fn test_claim_order() { MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: sender.to_string(), - amount: vec![coin_u256(Uint256::from(25u128), BASE_DENOM)], + amount: vec![coin_u256(Uint256::from(100u128), BASE_DENOM)], }, REPLY_ID_CLAIM, )), @@ -2778,7 +2771,7 @@ fn test_claim_order() { OrderOperation::PlaceLimit(LimitOrder::new( LARGE_NEGATIVE_TICK, 0, - OrderDirection::Bid, + OrderDirection::Ask, sender.clone(), Uint128::from(1u128), Decimal256::zero(), @@ -2787,7 +2780,7 @@ fn test_claim_order() { OrderOperation::PlaceLimit(LimitOrder::new( LARGE_NEGATIVE_TICK, 1, - OrderDirection::Bid, + OrderDirection::Ask, sender.clone(), Uint128::from(1u128), Decimal256::zero(), @@ -2795,7 +2788,7 @@ fn test_claim_order() { )), OrderOperation::RunMarket(MarketOrder::new( Uint128::from(1u128), - OrderDirection::Ask, + OrderDirection::Bid, sender.clone(), )), ], @@ -2819,7 +2812,7 @@ fn test_claim_order() { BASE_DENOM.to_string(), ) .unwrap(); - + println!("name: {}", test.name); // Run setup operations for operation in test.operations { operation From d7eb87fa32c923b6456477caf12db6aa77b38e9e Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 1 Jul 2024 10:31:55 +0100 Subject: [PATCH 76/98] test: fixed test_direcitonal_liquidity --- .../sumtree-orderbook/src/tests/test_order.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index 4aea2a2..871ffe8 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -3940,15 +3940,15 @@ fn test_directional_liquidity() { Decimal256::zero(), None, )), - // Filling Ask at 0.5 price = 100 units of opposite denom + // Partial filling Bid at 2 price = 50 units of opposite denom OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(200u128), + Uint128::from(50u128), OrderDirection::Ask, sender.clone(), )), - // Filling Bid at 0.5 price = 100 units of opposite denom + // Filling Ask at 2 price = 200 units of opposite denom OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(50u128), + Uint128::from(200u128), OrderDirection::Bid, sender.clone(), )), @@ -3979,21 +3979,21 @@ fn test_directional_liquidity() { Decimal256::zero(), None, )), - // Filling Ask at 0.5 price = 200 units of opposite denom + // Filling Bid at 0.5 price = 400 units of opposite denom OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(100u128), + Uint128::from(400u128), OrderDirection::Ask, sender.clone(), )), - // Filling Bid at 0.5 price = 25 units of opposite denom + // Partial flling Ask at 0.5 price = 25 units of opposite denom OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(50u128), + Uint128::from(25u128), OrderDirection::Bid, sender, )), ], expected_liquidity: ( - (OrderDirection::Ask, decimal256_from_u128(75u128)), + (OrderDirection::Ask, decimal256_from_u128(50u128)), (OrderDirection::Bid, decimal256_from_u128(0u128)), ), }, From 44dc901c54953df6385a651a130f812cb6179d9e Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 1 Jul 2024 10:37:22 +0100 Subject: [PATCH 77/98] test: fixed test_batch_claim --- contracts/sumtree-orderbook/src/tests/test_order.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index 871ffe8..0f94879 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -3620,7 +3620,7 @@ fn test_batch_claim_order() { owner.clone(), )), OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(50u128), + Uint128::from(51u128), OrderDirection::Bid, owner.clone(), )), @@ -3648,7 +3648,7 @@ fn test_batch_claim_order() { MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: owner.to_string(), - amount: vec![coin_u256(49u128, QUOTE_DENOM)], + amount: vec![coin_u256(50u128, QUOTE_DENOM)], }, REPLY_ID_CLAIM, ), @@ -3696,7 +3696,7 @@ fn test_batch_claim_order() { owner.clone(), )), OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(50u128), + Uint128::from(51u128), OrderDirection::Bid, owner.clone(), )), @@ -3716,7 +3716,7 @@ fn test_batch_claim_order() { MsgSend256 { from_address: "cosmos2contract".to_string(), to_address: owner.to_string(), - amount: vec![coin_u256(49u128, QUOTE_DENOM)], + amount: vec![coin_u256(50u128, QUOTE_DENOM)], }, REPLY_ID_CLAIM, ), From 100006d31118bd6b6b20c70cb1662e0c79c8bf14 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 1 Jul 2024 10:41:34 +0100 Subject: [PATCH 78/98] chore: removed unused imports --- contracts/sumtree-orderbook/src/tests/test_order.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index 0f94879..3a67865 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use crate::{ constants::{MAX_TICK, MIN_TICK}, error::ContractError, order::*, orderbook::*, state::*, sumtree::{ - node::{NodeType, TreeNode}, test::test_node::print_tree, tree::{get_or_init_root_node, get_root_node} + node::{NodeType, TreeNode}, tree::get_root_node }, tests::{mock_querier::mock_dependencies_custom, test_utils::{decimal256_from_u128, place_multiple_limit_orders}}, types::{ From 2a1178f4f04f628bb69b843d5e235c6acf0b6f07 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 1 Jul 2024 10:50:45 +0100 Subject: [PATCH 79/98] chore: commented test_claim_order --- .../sumtree-orderbook/src/tests/test_order.rs | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index 3a67865..c3849e5 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -1887,6 +1887,7 @@ fn test_claim_order() { None, )), OrderOperation::RunMarket(MarketOrder::new( + // Filling 4/10 of the Ask order // Tick price is 2, 2*4 = 8 Uint128::from(8u128), OrderDirection::Bid, @@ -1930,6 +1931,7 @@ fn test_claim_order() { None, )), OrderOperation::RunMarket(MarketOrder::new( + // Filling 6/10 of the Ask order // Tick price is 2, 2*6 = 12 Uint128::from(12u128), OrderDirection::Bid, @@ -1938,6 +1940,7 @@ fn test_claim_order() { // Claim the first partial fill OrderOperation::Claim((LARGE_POSITIVE_TICK, 0)), OrderOperation::RunMarket(MarketOrder::new( + // Filling 4/10 of the Ask order (full fill) // Tick price is 2, 2*4 = 8 Uint128::from(8u128), OrderDirection::Bid, @@ -1974,6 +1977,8 @@ fn test_claim_order() { None, )), OrderOperation::RunMarket(MarketOrder::new( + // Full filling ask order + // Tick price is 0.5, 0.5*100 = 50 Uint128::from(50u128), OrderDirection::Bid, Addr::unchecked("buyer"), @@ -2008,6 +2013,8 @@ fn test_claim_order() { None, )), OrderOperation::RunMarket(MarketOrder::new( + // Filling 50/100 of the Ask order + // Tick price is 0.5, 0.5*50 = 25 Uint128::from(25u128), OrderDirection::Bid, Addr::unchecked("buyer"), @@ -2050,6 +2057,8 @@ fn test_claim_order() { None, )), OrderOperation::RunMarket(MarketOrder::new( + // Filling 50/100 of the Ask order + // Tick price is 0.5, 0.5*50 = 25 Uint128::from(25u128), OrderDirection::Bid, Addr::unchecked("buyer"), @@ -2057,6 +2066,8 @@ fn test_claim_order() { // Claim the first partial fill OrderOperation::Claim((LARGE_NEGATIVE_TICK, 0)), OrderOperation::RunMarket(MarketOrder::new( + // Filling 50/100 of the Ask order (full fill) + // Tick price is 0.5, 0.5*50 = 25 Uint128::from(25u128), OrderDirection::Bid, Addr::unchecked("buyer"), @@ -2101,7 +2112,9 @@ fn test_claim_order() { )), OrderOperation::Cancel((valid_tick_id, 0)), OrderOperation::RunMarket(MarketOrder::new( - Uint128::from(100u128), + // Filling 100/100 of the Ask order + // Tick price is 0.5, 0.5*100 = 50 + Uint128::from(50u128), OrderDirection::Bid, Addr::unchecked("buyer"), )), @@ -2144,7 +2157,6 @@ fn test_claim_order() { )), ], order_id: 0, - tick_id: MIN_TICK, expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { @@ -2179,7 +2191,6 @@ fn test_claim_order() { )), ], order_id: 0, - tick_id: valid_tick_id, expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { @@ -2290,7 +2301,8 @@ fn test_claim_order() { None, )), OrderOperation::RunMarket(MarketOrder::new( - // Tick price is 2, 2*5 = 10 + // Filling 10/10 of the Bid order + // Tick price is 2, 10/2 = 5 Uint128::from(5u128), OrderDirection::Ask, Addr::unchecked("buyer"), @@ -2325,13 +2337,14 @@ fn test_claim_order() { None, )), OrderOperation::RunMarket(MarketOrder::new( + // Filling 4/10 of the Bid order + // Tick price is 2, 4/2 = 2 Uint128::from(2u128), OrderDirection::Ask, Addr::unchecked("buyer"), )), ], order_id: 0, - tick_id: LARGE_POSITIVE_TICK, expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { @@ -2367,6 +2380,8 @@ fn test_claim_order() { None, )), OrderOperation::RunMarket(MarketOrder::new( + // Filling 4/10 of the Bid Order + // Tick price is 2, 4/2 = 2 Uint128::from(2u128), OrderDirection::Ask, Addr::unchecked("buyer"), @@ -2374,13 +2389,14 @@ fn test_claim_order() { // Claim the first partial fill OrderOperation::Claim((LARGE_POSITIVE_TICK, 0)), OrderOperation::RunMarket(MarketOrder::new( + // Filling 6/10 of the Bid order (full fill) + // Tick price is 2, 6/2 = 3 Uint128::from(3u128), OrderDirection::Ask, Addr::unchecked("buyer"), )), ], order_id: 0, - tick_id: LARGE_POSITIVE_TICK, expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { @@ -2409,13 +2425,14 @@ fn test_claim_order() { None, )), OrderOperation::RunMarket(MarketOrder::new( + // Full filling the bid order + // Tick price is 0.5 so 100/0.5 = 200 Uint128::from(200u128), OrderDirection::Ask, Addr::unchecked("buyer"), )), ], order_id: 0, - tick_id: LARGE_NEGATIVE_TICK, expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { @@ -2443,13 +2460,14 @@ fn test_claim_order() { None, )), OrderOperation::RunMarket(MarketOrder::new( + // Filling 25/100 of the Bid order + // Tick price is 0.5 so 25/0.5 = 50 Uint128::from(50u128), OrderDirection::Ask, Addr::unchecked("buyer"), )), ], order_id: 0, - tick_id: LARGE_NEGATIVE_TICK, expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { @@ -2485,6 +2503,8 @@ fn test_claim_order() { None, )), OrderOperation::RunMarket(MarketOrder::new( + // Filling 50/100 of the Bid order + // Tick price is 0.5 so 50/0.5 = 100 Uint128::from(100u128), OrderDirection::Ask, Addr::unchecked("buyer"), @@ -2492,13 +2512,14 @@ fn test_claim_order() { // Claim the first partial fill OrderOperation::Claim((LARGE_NEGATIVE_TICK, 0)), OrderOperation::RunMarket(MarketOrder::new( + // Filling 50/100 of the Bid order (full fill) + // Tick price is 0.5 so 50/0.5 = 100 Uint128::from(100u128), OrderDirection::Ask, Addr::unchecked("buyer"), )), ], order_id: 0, - tick_id: LARGE_NEGATIVE_TICK, expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { @@ -2542,7 +2563,6 @@ fn test_claim_order() { )), ], order_id: 1, - tick_id: valid_tick_id, expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { From 37d5aace3a30d4a556f6e47ae4dbee1dbc80fa9d Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 1 Jul 2024 10:56:34 +0100 Subject: [PATCH 80/98] fix: failing test --- contracts/sumtree-orderbook/src/tests/test_order.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index e0fb180..7e6d4aa 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -2112,8 +2112,8 @@ fn test_claim_order() { OrderOperation::Cancel((valid_tick_id, 0)), OrderOperation::RunMarket(MarketOrder::new( // Filling 100/100 of the Ask order - // Tick price is 0.5, 0.5*100 = 50 - Uint128::from(50u128), + // Tick price is 1, 1*100 = 100 + Uint128::from(100u128), OrderDirection::Bid, Addr::unchecked("buyer"), )), From f609198166218bad0c359208df2010e09e6648d9 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Mon, 1 Jul 2024 13:50:08 +0100 Subject: [PATCH 81/98] test: adjusted tick bounds for e2e --- .../src/tests/e2e/cases/test_fuzz.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index 5e2aaee..b6551e0 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -9,7 +9,7 @@ use rand::Rng; use rand::{rngs::StdRng, SeedableRng}; use super::utils::{assert, orders}; -use crate::constants::{MAX_TICK, MIN_TICK}; +use crate::constants::MIN_TICK; use crate::msg::{CalcOutAmtGivenInResponse, QueryMsg}; use crate::tests::e2e::modules::cosmwasm_pool::CosmwasmPool; use crate::tick_math::{amount_to_value, tick_to_price, RoundingDirection}; @@ -20,10 +20,10 @@ use crate::{ types::OrderDirection, }; -// Tick Price = 2 -pub(crate) const LARGE_POSITIVE_TICK: i64 = 1000000; -// Tick Price = 0.5 -pub(crate) const LARGE_NEGATIVE_TICK: i64 = -5000000; +// Tick Price = 100000 +pub(crate) const LARGE_POSITIVE_TICK: i64 = 45000000; +// Tick Price = 0.00001 +pub(crate) const LARGE_NEGATIVE_TICK: i64 = -45000000; // Loops over a provided action for the provided duration // Tracks the number of operations and iterations @@ -160,7 +160,7 @@ fn test_order_fuzz_large_tick_range() { let oper_per_iteration = 1000; run_for_duration(30, oper_per_iteration, |count| { - run_fuzz_mixed(count, (MIN_TICK, MAX_TICK)); + run_fuzz_mixed(count, (MIN_TICK, LARGE_POSITIVE_TICK)); }); } From c81cf3bd9355abb215641c9c68739f1a6fd6a253 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Tue, 2 Jul 2024 20:22:17 +0100 Subject: [PATCH 82/98] test: added run time for all e2e tests and decrement market value assertion --- .../src/tests/e2e/cases/test_fuzz.rs | 43 ++++++++++------ .../src/tests/e2e/cases/utils.rs | 49 +++++++++++++++++-- 2 files changed, 73 insertions(+), 19 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index b6551e0..d3957c5 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -21,9 +21,10 @@ use crate::{ }; // Tick Price = 100000 -pub(crate) const LARGE_POSITIVE_TICK: i64 = 45000000; +pub(crate) const LARGE_POSITIVE_TICK: i64 = 4500000; // Tick Price = 0.00001 -pub(crate) const LARGE_NEGATIVE_TICK: i64 = -45000000; +pub(crate) const LARGE_NEGATIVE_TICK: i64 = -4500000; +// pub(crate) const LARGE_NEGATIVE_TICK: i64 = -5000000; // Loops over a provided action for the provided duration // Tracks the number of operations and iterations @@ -52,15 +53,15 @@ fn run_for_duration( #[test] fn test_order_fuzz_linear_large_orders_small_range() { let oper_per_iteration = 1000; - run_for_duration(60, oper_per_iteration, |count| { + run_for_duration(60 * 60, oper_per_iteration, |count| { run_fuzz_linear(count, (-10, 10), 0.2); }); } #[test] fn test_order_fuzz_linear_small_orders_large_range() { - let oper_per_iteration = 100; - run_for_duration(60, oper_per_iteration, |count| { + let oper_per_iteration = 1000; + run_for_duration(60 * 5, oper_per_iteration, |count| { run_fuzz_linear(count, (LARGE_NEGATIVE_TICK, LARGE_POSITIVE_TICK), 0.2); }); } @@ -74,7 +75,7 @@ fn test_order_fuzz_linear_small_orders_large_range() { #[test] fn test_order_fuzz_linear_small_orders_small_range() { let oper_per_iteration = 100; - run_for_duration(60, oper_per_iteration, |count| { + run_for_duration(60 * 60, oper_per_iteration, |count| { run_fuzz_linear(count, (-10, 0), 0.1); }); } @@ -82,7 +83,7 @@ fn test_order_fuzz_linear_small_orders_small_range() { #[test] fn test_order_fuzz_linear_large_cancelled_orders_small_range() { let oper_per_iteration = 1000; - run_for_duration(60, oper_per_iteration, |count| { + run_for_duration(60 * 60, oper_per_iteration, |count| { run_fuzz_linear(count, (MIN_TICK, MIN_TICK + 20), 0.8); }); } @@ -90,7 +91,7 @@ fn test_order_fuzz_linear_large_cancelled_orders_small_range() { #[test] fn test_order_fuzz_linear_small_cancelled_orders_large_range() { let oper_per_iteration = 100; - run_for_duration(60, oper_per_iteration, |count| { + run_for_duration(60 * 60, oper_per_iteration, |count| { run_fuzz_linear(count, (LARGE_NEGATIVE_TICK, LARGE_POSITIVE_TICK), 0.8); }); } @@ -98,14 +99,14 @@ fn test_order_fuzz_linear_small_cancelled_orders_large_range() { #[test] fn test_order_fuzz_linear_large_all_cancelled_orders_small_range() { let oper_per_iteration = 1000; - run_for_duration(60, oper_per_iteration, |count| { + run_for_duration(60 * 60, oper_per_iteration, |count| { run_fuzz_linear(count, (-10, 10), 1.0); }); } #[test] fn test_order_fuzz_linear_single_tick() { - let oper_per_iteration = 1000; + let oper_per_iteration = 100; run_for_duration(10, oper_per_iteration, |count| { run_fuzz_linear(count, (0, 0), 0.2); }); @@ -114,7 +115,7 @@ fn test_order_fuzz_linear_single_tick() { #[test] fn test_order_fuzz_mixed() { let oper_per_iteration = 1000; - run_for_duration(60, oper_per_iteration, |count| { + run_for_duration(60 * 60, oper_per_iteration, |count| { run_fuzz_mixed(count, (-20, 20)); }); } @@ -123,7 +124,7 @@ fn test_order_fuzz_mixed() { fn test_order_fuzz_mixed_single_tick() { let oper_per_iteration = 1000; - run_for_duration(60, oper_per_iteration, |count| { + run_for_duration(60 * 60, oper_per_iteration, |count| { run_fuzz_mixed(count, (0, 0)); }); } @@ -132,7 +133,7 @@ fn test_order_fuzz_mixed_single_tick() { fn test_order_fuzz_mixed_large_negative_tick_range() { let oper_per_iteration = 1000; - run_for_duration(30, oper_per_iteration, |count| { + run_for_duration(60 * 60, oper_per_iteration, |count| { run_fuzz_mixed(count, (LARGE_NEGATIVE_TICK, LARGE_NEGATIVE_TICK + 10)); }); } @@ -141,7 +142,7 @@ fn test_order_fuzz_mixed_large_negative_tick_range() { fn test_order_fuzz_mixed_large_positive_tick_range() { let oper_per_iteration = 1000; - run_for_duration(30, oper_per_iteration, |count| { + run_for_duration(60 * 60, oper_per_iteration, |count| { run_fuzz_mixed(count, (LARGE_POSITIVE_TICK - 10, LARGE_POSITIVE_TICK)); }); } @@ -150,7 +151,7 @@ fn test_order_fuzz_mixed_large_positive_tick_range() { fn test_order_fuzz_mixed_min_tick() { let oper_per_iteration = 1000; - run_for_duration(30, oper_per_iteration, |count| { + run_for_duration(60 * 60, oper_per_iteration, |count| { run_fuzz_mixed(count, (MIN_TICK, MIN_TICK + 10)); }); } @@ -159,7 +160,7 @@ fn test_order_fuzz_mixed_min_tick() { fn test_order_fuzz_large_tick_range() { let oper_per_iteration = 1000; - run_for_duration(30, oper_per_iteration, |count| { + run_for_duration(60 * 60, oper_per_iteration, |count| { run_fuzz_mixed(count, (MIN_TICK, LARGE_POSITIVE_TICK)); }); } @@ -214,6 +215,9 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob // A counter to track the current user ID let mut user_id = 0; + let mut previous_expected_out = + assert::decrementing_market_order_output(&t, u128::MAX, 10000000u128, order_direction); + // While there is some fillable liquidity we want to place randomised market orders while liquidity > 1u128 { let username = format!("user{}{}", order_direction, user_id); @@ -239,6 +243,13 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob liquidity = t.contract.get_directional_liquidity(order_direction); assert::tick_invariants(&t); assert::has_liquidity(&t); + + previous_expected_out = assert::decrementing_market_order_output( + &t, + previous_expected_out.u128(), + 100u128, + order_direction, + ); } println!( "Placed {} market orders in {} direction", diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs index 4ca28ba..fcb1288 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs @@ -82,14 +82,14 @@ pub mod assert { use crate::{ msg::{ - DenomsResponse, GetTotalPoolLiquidityResponse, GetUnrealizedCancelsResponse, QueryMsg, - SpotPriceResponse, + CalcOutAmtGivenInResponse, DenomsResponse, GetTotalPoolLiquidityResponse, + GetUnrealizedCancelsResponse, QueryMsg, SpotPriceResponse, }, tests::e2e::test_env::TestEnv, tick_math::{amount_to_value, tick_to_price, RoundingDirection}, types::{OrderDirection, Orderbook}, }; - use cosmwasm_std::{Coin, Coins, Fraction, Uint128}; + use cosmwasm_std::{Coin, Coins, Decimal, Fraction, Uint128}; use osmosis_test_tube::{cosmrs::proto::prost::Message, RunnerExecuteResult}; // -- Contract State Assertions @@ -302,6 +302,49 @@ pub mod assert { } } + pub fn decrementing_market_order_output( + t: &TestEnv, + previous_market_value: u128, + amount_to_run: u128, + direction: OrderDirection, + ) -> Uint128 { + let DenomsResponse { + quote_denom, + base_denom, + } = t.contract.get_denoms(); + + let token_in_denom = if direction == OrderDirection::Bid { + quote_denom.clone() + } else { + base_denom.clone() + }; + let token_out_denom = if direction == OrderDirection::Bid { + base_denom + } else { + quote_denom + }; + + let maybe_expected_output = t.contract.query(&QueryMsg::CalcOutAmountGivenIn { + token_in: Coin::new(amount_to_run, token_in_denom), + token_out_denom, + swap_fee: Decimal::zero(), + }); + + let expected_output = maybe_expected_output + .map_or(Uint128::zero(), |r: CalcOutAmtGivenInResponse| { + Uint128::from_str(&r.token_out.amount).unwrap() + }); + + assert!( + expected_output.u128() <= previous_market_value, + "subsequent market orders increased unexpectedly, got: {}, expected: {}", + expected_output, + previous_market_value + ); + + expected_output + } + /// Asserts that there are no remaining orders in the orderbook pub fn no_remaining_orders(t: &TestEnv) { let all_orders = t.contract.collect_all_orders(); From 2dd35e4f8d55e6df2047747909ca67a83cd8b526 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Tue, 2 Jul 2024 21:01:35 +0100 Subject: [PATCH 83/98] test: added test to ensure market price deteriorates as expected --- .../src/tests/e2e/cases/mod.rs | 2 +- ...{test_orders_success.rs => test_orders.rs} | 64 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) rename contracts/sumtree-orderbook/src/tests/e2e/cases/{test_orders_success.rs => test_orders.rs} (74%) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/mod.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/mod.rs index 95a9452..79d220e 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/mod.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/mod.rs @@ -1,3 +1,3 @@ mod test_fuzz; -mod test_orders_success; +mod test_orders; mod utils; diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders.rs similarity index 74% rename from contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs rename to contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders.rs index 9d56de1..31dad30 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders_success.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders.rs @@ -1,3 +1,5 @@ +use std::fmt::format; + use cosmwasm_std::{coin, Decimal256, Uint128}; use osmosis_test_tube::{Module, OsmosisTestApp}; @@ -256,3 +258,65 @@ fn test_tick_extremes() { ) .unwrap(); } + +/// This test ensures that as ticks are iterated and filled the price is getting worse (as it starts at the best possible price an works towards the worst) +#[test] +fn test_decrementing_market_value() { + let app = OsmosisTestApp::new(); + let cp = CosmwasmPool::new(&app); + let mut t = setup!(&app, "quote", "base", 0); + + // We want a relatively large market amount to ensure tick moves without a large amount of market orders + let market_amount = 1000u128; + // We want a relatively small limit amount to help move the tick + let limit_amount = 10u128; + // We want a relatively large tick step to ensure price is shifting between ticks + let tick_step = 10000; + let (min_tick, max_tick) = (-4500000, 4500000); + + // Ensure this is true for both directions + for direction in [OrderDirection::Ask, OrderDirection::Bid] { + // Place a limit order at each tick step between the min and max tick + for tick in (min_tick..max_tick).step_by(tick_step as usize) { + // We don't care who places the order as we are only checking expected output + let username = format!("user{}", tick); + t.add_account( + &username, + vec![ + coin(1000000, "base"), + coin(1000000, "quote"), + coin(10000000000, "uosmo"), + ], + ); + orders::place_limit(&t, tick, direction, limit_amount, None, &username).unwrap(); + } + + // Record the current expected output for the first market order + let mut prev_expected_output = assert::decrementing_market_order_output( + &t, + u128::MAX - 1, + market_amount, + direction.opposite(), + ); + + // If this is zero then the setup amounts do not work + assert!(!prev_expected_output.is_zero()); + + let mut iterations = 0; + // Place orders while possible (this should loop at least once due to the expected output from above being non-zero) + while orders::place_market(&cp, &t, direction.opposite(), market_amount, "user1").is_ok() { + iterations += 1; + // Assert the expected output is getting worse + prev_expected_output = assert::decrementing_market_order_output( + &t, + prev_expected_output.u128(), + market_amount, + direction.opposite(), + ); + // Ensure ticks are correct as we iterate + assert::tick_invariants(&t); + } + + assert!(iterations > 0, "no market orders were placed"); + } +} From bd2491bbbdd76e0d1ff851d73313ecfe6b2ae75f Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Wed, 3 Jul 2024 10:36:23 +0100 Subject: [PATCH 84/98] test: removed unused test --- .../src/tests/e2e/cases/test_fuzz.rs | 44 ++++++++-------- .../src/tests/e2e/cases/test_orders.rs | 51 +------------------ 2 files changed, 23 insertions(+), 72 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index d3957c5..1f33156 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -53,15 +53,15 @@ fn run_for_duration( #[test] fn test_order_fuzz_linear_large_orders_small_range() { let oper_per_iteration = 1000; - run_for_duration(60 * 60, oper_per_iteration, |count| { + run_for_duration(60, oper_per_iteration, |count| { run_fuzz_linear(count, (-10, 10), 0.2); }); } #[test] fn test_order_fuzz_linear_small_orders_large_range() { - let oper_per_iteration = 1000; - run_for_duration(60 * 5, oper_per_iteration, |count| { + let oper_per_iteration = 2000; + run_for_duration(60, oper_per_iteration, |count| { run_fuzz_linear(count, (LARGE_NEGATIVE_TICK, LARGE_POSITIVE_TICK), 0.2); }); } @@ -75,15 +75,15 @@ fn test_order_fuzz_linear_small_orders_large_range() { #[test] fn test_order_fuzz_linear_small_orders_small_range() { let oper_per_iteration = 100; - run_for_duration(60 * 60, oper_per_iteration, |count| { + run_for_duration(60, oper_per_iteration, |count| { run_fuzz_linear(count, (-10, 0), 0.1); }); } #[test] fn test_order_fuzz_linear_large_cancelled_orders_small_range() { - let oper_per_iteration = 1000; - run_for_duration(60 * 60, oper_per_iteration, |count| { + let oper_per_iteration = 2000; + run_for_duration(60, oper_per_iteration, |count| { run_fuzz_linear(count, (MIN_TICK, MIN_TICK + 20), 0.8); }); } @@ -91,22 +91,22 @@ fn test_order_fuzz_linear_large_cancelled_orders_small_range() { #[test] fn test_order_fuzz_linear_small_cancelled_orders_large_range() { let oper_per_iteration = 100; - run_for_duration(60 * 60, oper_per_iteration, |count| { + run_for_duration(60, oper_per_iteration, |count| { run_fuzz_linear(count, (LARGE_NEGATIVE_TICK, LARGE_POSITIVE_TICK), 0.8); }); } #[test] fn test_order_fuzz_linear_large_all_cancelled_orders_small_range() { - let oper_per_iteration = 1000; - run_for_duration(60 * 60, oper_per_iteration, |count| { + let oper_per_iteration = 2000; + run_for_duration(60, oper_per_iteration, |count| { run_fuzz_linear(count, (-10, 10), 1.0); }); } #[test] fn test_order_fuzz_linear_single_tick() { - let oper_per_iteration = 100; + let oper_per_iteration = 2000; run_for_duration(10, oper_per_iteration, |count| { run_fuzz_linear(count, (0, 0), 0.2); }); @@ -114,53 +114,53 @@ fn test_order_fuzz_linear_single_tick() { #[test] fn test_order_fuzz_mixed() { - let oper_per_iteration = 1000; - run_for_duration(60 * 60, oper_per_iteration, |count| { + let oper_per_iteration = 2000; + run_for_duration(10, oper_per_iteration, |count| { run_fuzz_mixed(count, (-20, 20)); }); } #[test] fn test_order_fuzz_mixed_single_tick() { - let oper_per_iteration = 1000; + let oper_per_iteration = 2000; - run_for_duration(60 * 60, oper_per_iteration, |count| { + run_for_duration(10, oper_per_iteration, |count| { run_fuzz_mixed(count, (0, 0)); }); } #[test] fn test_order_fuzz_mixed_large_negative_tick_range() { - let oper_per_iteration = 1000; + let oper_per_iteration = 2000; - run_for_duration(60 * 60, oper_per_iteration, |count| { + run_for_duration(60, oper_per_iteration, |count| { run_fuzz_mixed(count, (LARGE_NEGATIVE_TICK, LARGE_NEGATIVE_TICK + 10)); }); } #[test] fn test_order_fuzz_mixed_large_positive_tick_range() { - let oper_per_iteration = 1000; + let oper_per_iteration = 2000; - run_for_duration(60 * 60, oper_per_iteration, |count| { + run_for_duration(60, oper_per_iteration, |count| { run_fuzz_mixed(count, (LARGE_POSITIVE_TICK - 10, LARGE_POSITIVE_TICK)); }); } #[test] fn test_order_fuzz_mixed_min_tick() { - let oper_per_iteration = 1000; + let oper_per_iteration = 2000; - run_for_duration(60 * 60, oper_per_iteration, |count| { + run_for_duration(60, oper_per_iteration, |count| { run_fuzz_mixed(count, (MIN_TICK, MIN_TICK + 10)); }); } #[test] fn test_order_fuzz_large_tick_range() { - let oper_per_iteration = 1000; + let oper_per_iteration = 2000; - run_for_duration(60 * 60, oper_per_iteration, |count| { + run_for_duration(60, oper_per_iteration, |count| { run_fuzz_mixed(count, (MIN_TICK, LARGE_POSITIVE_TICK)); }); } diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders.rs index 31dad30..d1dba1e 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders.rs @@ -1,12 +1,7 @@ -use std::fmt::format; - use cosmwasm_std::{coin, Decimal256, Uint128}; use osmosis_test_tube::{Module, OsmosisTestApp}; -use super::{ - test_fuzz::LARGE_NEGATIVE_TICK, - utils::{assert, orders}, -}; +use super::utils::{assert, orders}; use crate::{ constants::{MAX_TICK, MIN_TICK}, setup, @@ -215,50 +210,6 @@ fn test_cancelled_orders() { assert::tick_invariants(&t); } -#[test] -fn test_tick_extremes() { - let app = OsmosisTestApp::new(); - let cp = CosmwasmPool::new(&app); - let mut t = setup!(&app, "quote", "base", 0); - - t.add_account( - "newuser", - vec![ - coin(u128::MAX, "base"), - coin(u128::MAX, "quote"), - coin(u128::MAX, "uosmo"), - ], - ); - - orders::place_limit( - &t, - MIN_TICK, - OrderDirection::Ask, - Uint128::MAX.checked_div(Uint128::from(2u128)).unwrap(), - None, - "newuser", - ) - .unwrap(); - orders::place_limit( - &t, - LARGE_NEGATIVE_TICK, - OrderDirection::Ask, - Uint128::MAX.checked_div(Uint128::from(2u128)).unwrap(), - None, - "newuser", - ) - .unwrap(); - - orders::place_market_and_assert_balance( - &cp, - &t, - OrderDirection::Bid, - Uint128::from(2u128), - "newuser", - ) - .unwrap(); -} - /// This test ensures that as ticks are iterated and filled the price is getting worse (as it starts at the best possible price an works towards the worst) #[test] fn test_decrementing_market_value() { From ff9fe52cf185c3d89ec6477cc326cf019284e1a5 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Wed, 3 Jul 2024 10:38:57 +0100 Subject: [PATCH 85/98] chore: removed print lines --- contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs | 4 +--- contracts/sumtree-orderbook/src/tests/e2e/test_env.rs | 2 -- contracts/sumtree-orderbook/src/tests/test_order.rs | 1 - 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index 1f33156..398928a 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -350,7 +350,6 @@ impl MixedFuzzOperation { order_count: &mut u64, tick_bounds: (i64, i64), ) -> Result { - // println!("operation: {self:?}"); let username = format!("user{}", iteration); match self { MixedFuzzOperation::PlaceLimit => { @@ -517,7 +516,7 @@ fn run_fuzz_mixed(amount_of_orders: u64, tick_bounds: (i64, i64)) { // We add an escape clause in the case that the test ever gets caught in an infinite loop let mut repeated_failures = 0; - // println!("iteration: {}", i); + // Repeat randomising operations until a successful one is chosen while !operation .run( @@ -678,7 +677,6 @@ fn place_random_market( return 0; } } else if expected_out.is_err() { - println!("expected_out: {:?}", expected_out); return 0; } diff --git a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs index 8c39683..2943923 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs @@ -241,8 +241,6 @@ impl<'a> OrderbookContract<'a> { SudoMsg::TransferAdmin { new_admin: admin }, ) .unwrap(); - let admin: Option = self.query(&QueryMsg::Auth(AuthQueryMsg::Admin {})).unwrap(); - println!("admin_set: {:?}", admin); } pub fn _set_maker_fee( diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index 7e6d4aa..40d50e5 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -2831,7 +2831,6 @@ fn test_claim_order() { BASE_DENOM.to_string(), ) .unwrap(); - println!("name: {}", test.name); // Run setup operations for operation in test.operations { operation From 2c7e372d9575d272e23154ad9f49ba59c89a7269 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Wed, 3 Jul 2024 10:44:47 +0100 Subject: [PATCH 86/98] chore: removed unused code/empty lines --- contracts/sumtree-orderbook/src/order.rs | 14 -------------- .../src/sumtree/test/test_node.rs | 2 +- .../sumtree-orderbook/src/tests/e2e/test_env.rs | 2 +- contracts/sumtree-orderbook/src/tick.rs | 1 - 4 files changed, 2 insertions(+), 17 deletions(-) diff --git a/contracts/sumtree-orderbook/src/order.rs b/contracts/sumtree-orderbook/src/order.rs index ea6ad9b..9313326 100644 --- a/contracts/sumtree-orderbook/src/order.rs +++ b/contracts/sumtree-orderbook/src/order.rs @@ -166,20 +166,6 @@ pub fn cancel_limit( .effective_total_amount_swapped, )?; - // Ensure the order has not been filled. - let tick_state = TICK_STATE.load(deps.storage, tick_id).unwrap_or_default(); - - sync_tick( - deps.storage, - tick_id, - tick_state - .get_values(OrderDirection::Bid) - .effective_total_amount_swapped, - tick_state - .get_values(OrderDirection::Ask) - .effective_total_amount_swapped, - )?; - let tick_state = TICK_STATE.load(deps.storage, tick_id).unwrap_or_default(); let tick_values = tick_state.get_values(order.order_direction); ensure!( diff --git a/contracts/sumtree-orderbook/src/sumtree/test/test_node.rs b/contracts/sumtree-orderbook/src/sumtree/test/test_node.rs index 8512f9d..8c5aeaf 100644 --- a/contracts/sumtree-orderbook/src/sumtree/test/test_node.rs +++ b/contracts/sumtree-orderbook/src/sumtree/test/test_node.rs @@ -2736,7 +2736,7 @@ fn test_node_insert_large_quantity() { tree.insert(deps.as_mut().storage, &mut node).unwrap(); tree = get_root_node(deps.as_ref().storage, tick_id, direction).unwrap(); // Track insertions that fall below our target ETAS - if node.get_min_range() <= target_etas || node.get_max_range().checked_sub(node.get_value()).unwrap() <= target_etas { + if node.get_min_range() <= target_etas { expected_prefix_sum = expected_prefix_sum.checked_add(Decimal256::one()).unwrap(); } } diff --git a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs index 2943923..413beaa 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, path::PathBuf, str::FromStr}; use crate::{ constants::{MAX_TICK, MIN_TICK}, msg::{ - AuthExecuteMsg, AuthQueryMsg, DenomsResponse, ExecuteMsg, GetTotalPoolLiquidityResponse, + AuthExecuteMsg, DenomsResponse, ExecuteMsg, GetTotalPoolLiquidityResponse, GetUnrealizedCancelsResponse, InstantiateMsg, OrdersResponse, QueryMsg, SudoMsg, TickIdAndState, TickUnrealizedCancels, TicksResponse, }, diff --git a/contracts/sumtree-orderbook/src/tick.rs b/contracts/sumtree-orderbook/src/tick.rs index 62a4266..89df3f7 100644 --- a/contracts/sumtree-orderbook/src/tick.rs +++ b/contracts/sumtree-orderbook/src/tick.rs @@ -43,7 +43,6 @@ pub fn sync_tick( // If tick state for current order direction is already up to date, // skip the check. This saves us from walking the tree for both order directions // even though in most cases we will likely only need to sync one. - if tick_value.last_tick_sync_etas == target_etas { continue; } From da338b19240a59d3940118c09be63d9115dacedc Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Wed, 3 Jul 2024 10:48:25 +0100 Subject: [PATCH 87/98] chore: further cleanup --- contracts/sumtree-orderbook/src/order.rs | 3 +-- contracts/sumtree-orderbook/src/sumtree/tree.rs | 1 + contracts/sumtree-orderbook/src/tests/test_tick.rs | 4 ++-- contracts/sumtree-orderbook/src/tests/test_utils.rs | 6 ++---- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/contracts/sumtree-orderbook/src/order.rs b/contracts/sumtree-orderbook/src/order.rs index 9313326..c97ec62 100644 --- a/contracts/sumtree-orderbook/src/order.rs +++ b/contracts/sumtree-orderbook/src/order.rs @@ -111,8 +111,7 @@ pub fn place_limit( tick_values.total_amount_of_liquidity = tick_values .total_amount_of_liquidity - .checked_add(quant_dec256) - .unwrap(); + .checked_add(quant_dec256)?; tick_values.cumulative_total_value = tick_values .cumulative_total_value diff --git a/contracts/sumtree-orderbook/src/sumtree/tree.rs b/contracts/sumtree-orderbook/src/sumtree/tree.rs index 95beaed..7be0311 100644 --- a/contracts/sumtree-orderbook/src/sumtree/tree.rs +++ b/contracts/sumtree-orderbook/src/sumtree/tree.rs @@ -77,6 +77,7 @@ fn prefix_sum_walk( // If the target ETAS is below the root node's range, we can return zero early. return Ok(Decimal256::zero()); } else if target_etas >= node.get_max_range() { + // If the target ETAS is above the root node's range, we can return the full sum early. return Ok(current_sum); } diff --git a/contracts/sumtree-orderbook/src/tests/test_tick.rs b/contracts/sumtree-orderbook/src/tests/test_tick.rs index 385df7f..9cce472 100644 --- a/contracts/sumtree-orderbook/src/tests/test_tick.rs +++ b/contracts/sumtree-orderbook/src/tests/test_tick.rs @@ -125,7 +125,7 @@ fn test_sync_tick() { NodeType::leaf_uint256(50u32, 12u32), NodeType::leaf_uint256(62u32, 10u32), NodeType::leaf_uint256(80u32, 28u32), - NodeType::leaf_uint256(178u32, 70u32), + NodeType::leaf_uint256(128u32, 70u32), ], unrealized_cancels_ask: vec![], @@ -244,7 +244,7 @@ fn test_sync_tick() { NodeType::leaf_uint256(50u32, 12u32), NodeType::leaf_uint256(62u32, 10u32), NodeType::leaf_uint256(80u32, 28u32), - NodeType::leaf_uint256(178u32, 70u32), + NodeType::leaf_uint256(128u32, 70u32), ], unrealized_cancels_bid: vec![], diff --git a/contracts/sumtree-orderbook/src/tests/test_utils.rs b/contracts/sumtree-orderbook/src/tests/test_utils.rs index 5d2149c..6df60e7 100644 --- a/contracts/sumtree-orderbook/src/tests/test_utils.rs +++ b/contracts/sumtree-orderbook/src/tests/test_utils.rs @@ -72,10 +72,8 @@ impl OrderOperation { Ok(()) } OrderOperation::Claim((tick_id, order_id)) => { - match claim_limit(deps, env, info, tick_id, order_id) { - Ok(_) => Ok(()), - Err(err) => Err(err), - } + claim_limit(deps, env, info, tick_id, order_id)?; + Ok(()) } OrderOperation::Cancel((tick_id, order_id)) => { let order = orders() From 16d6cef18bb833384a0aee1d963d91d8663f3719 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Wed, 3 Jul 2024 10:59:36 +0100 Subject: [PATCH 88/98] refactor: rewrote tick invariants assertion --- .../src/tests/e2e/cases/utils.rs | 162 ++++++++---------- 1 file changed, 75 insertions(+), 87 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs index fcb1288..cb8e526 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs @@ -83,7 +83,7 @@ pub mod assert { use crate::{ msg::{ CalcOutAmtGivenInResponse, DenomsResponse, GetTotalPoolLiquidityResponse, - GetUnrealizedCancelsResponse, QueryMsg, SpotPriceResponse, + GetUnrealizedCancelsResponse, QueryMsg, SpotPriceResponse, TickIdAndState, }, tests::e2e::test_env::TestEnv, tick_math::{amount_to_value, tick_to_price, RoundingDirection}, @@ -202,6 +202,38 @@ pub mod assert { ); } + /// Determines if the provided tick has fillable liquidity for the given direction + /// + /// Fillable liquidity means that after calculating amount to value the amount of liquidity is still non-zero + fn has_fillable_liqudity(tick: &TickIdAndState, direction: OrderDirection) -> bool { + if tick + .tick_state + .get_values(direction) + .total_amount_of_liquidity + .is_zero() + { + return false; + } + + let price = tick_to_price(tick.tick_id).unwrap(); + let amount_of_liquidity = Uint128::from_str( + &tick + .tick_state + .get_values(direction) + .total_amount_of_liquidity + .to_string(), + ) + .unwrap(); + let fillable_amount = amount_to_value( + direction, + amount_of_liquidity, + price, + RoundingDirection::Down, + ) + .unwrap(); + !fillable_amount.is_zero() + } + /// Assertions about tick state /// 1. All ticks have a cumulative value that is greater than or equal to the effective total amount swapped /// 2. The next ask tick is less than or equal to the minimum tick with an ask amount @@ -209,96 +241,52 @@ pub mod assert { /// /// This assertion can be run mid test as it must always be true pub fn tick_invariants(t: &TestEnv) { - let ticks = t.contract.collect_all_ticks(); - - assert!(ticks - .iter() - .all(|t| t.tick_state.ask_values.effective_total_amount_swapped - <= t.tick_state.ask_values.cumulative_total_value)); - assert!(ticks - .iter() - .all(|t| t.tick_state.bid_values.effective_total_amount_swapped - <= t.tick_state.bid_values.cumulative_total_value)); - - let ticks_with_bid_amount = ticks.iter().filter(|tick| { - if tick - .tick_state - .get_values(OrderDirection::Bid) - .total_amount_of_liquidity - .is_zero() - { - return false; - } - - let price = tick_to_price(tick.tick_id).unwrap(); - let amount_of_liquidity = Uint128::from_str( - &tick - .tick_state - .get_values(OrderDirection::Bid) - .total_amount_of_liquidity - .to_string(), - ) - .unwrap(); - let fillable_amount = amount_to_value( - OrderDirection::Bid, - amount_of_liquidity, - price, - RoundingDirection::Down, - ) - .unwrap(); - !fillable_amount.is_zero() - }); - let ticks_with_ask_amount = ticks.iter().filter(|tick| { - if tick - .tick_state - .get_values(OrderDirection::Ask) - .total_amount_of_liquidity - .is_zero() - { - return false; - } - - let price = tick_to_price(tick.tick_id).unwrap(); - let amount_of_liquidity = Uint128::from_str( - &tick - .tick_state - .get_values(OrderDirection::Ask) - .total_amount_of_liquidity - .to_string(), - ) - .unwrap(); - let fillable_amount = amount_to_value( - OrderDirection::Ask, - amount_of_liquidity, - price, - RoundingDirection::Down, - ) - .unwrap(); - !fillable_amount.is_zero() - }); - let max_tick_with_bid = ticks_with_bid_amount.max_by_key(|tick| tick.tick_id); - let min_tick_with_ask = ticks_with_ask_amount.min_by_key(|tick| tick.tick_id); - let Orderbook { next_ask_tick, next_bid_tick, .. } = t.contract.query(&QueryMsg::OrderbookState {}).unwrap(); - if let Some(min_tick_with_ask) = min_tick_with_ask { - assert!( - next_ask_tick <= min_tick_with_ask.tick_id, - "ASK TICK: got: {}, expected: {}", - next_ask_tick, - min_tick_with_ask.tick_id - ); - } - if let Some(max_tick_with_bid) = max_tick_with_bid { - assert!( - next_bid_tick >= max_tick_with_bid.tick_id, - "BID TICK: got: {}, expected: {}", - next_bid_tick, - max_tick_with_bid.tick_id - ); + + let ticks = t.contract.collect_all_ticks(); + for direction in [OrderDirection::Bid, OrderDirection::Ask] { + // Assert every tick has a cumulative value that is greater than or equal to the effective total amount swapped + assert!(ticks.iter().all(|t| t + .tick_state + .get_values(direction) + .effective_total_amount_swapped + <= t.tick_state.get_values(direction).cumulative_total_value)); + + // Get all ticks with fillable liquidity for the given direction + let ticks_with_liquidity = ticks.iter().filter(|t| has_fillable_liqudity(t, direction)); + + // Determine the max/min tick with liquidity for the given direction + let boundary_tick_with_liquidity = match direction { + OrderDirection::Bid => ticks_with_liquidity.max_by_key(|tick| tick.tick_id), + OrderDirection::Ask => ticks_with_liquidity.min_by_key(|tick| tick.tick_id), + }; + + // If the given direction has at least one tick and a boundary exists we compare this with what is + // stored in the orderbook to ensure that tick pointers are correctly updated for the current orderbook state + if let Some(boundary_tick) = boundary_tick_with_liquidity { + match direction { + OrderDirection::Bid => { + assert!( + boundary_tick.tick_id <= next_bid_tick, + "BID TICK: got: {}, expected: {}", + next_bid_tick, + boundary_tick.tick_id + ); + } + OrderDirection::Ask => { + assert!( + boundary_tick.tick_id >= next_ask_tick, + "ASK TICK: got: {}, expected: {}", + next_ask_tick, + boundary_tick.tick_id + ); + } + } + } } } @@ -465,7 +453,7 @@ pub mod assert { } } -/// Utili functions for interacting with the orderbook +/// Utility functions for interacting with the orderbook pub mod orders { use std::str::FromStr; From 25d255efde7127bcecead99a14e394ab9d5bc3f9 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Wed, 3 Jul 2024 11:04:26 +0100 Subject: [PATCH 89/98] chore: util function scoping and additional comments --- .../src/tests/e2e/cases/utils.rs | 64 +++++++++---------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs index cb8e526..27499cd 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs @@ -2,13 +2,15 @@ #[macro_export] macro_rules! setup { ($($app:expr, $quote_denom:expr, $base_denom:expr, $maker_fee:expr),* ) => {{ + // -- Setup -- + // Ensure both denoms are present in the app $($app.init_account(&[ cosmwasm_std::Coin::new(1, $quote_denom), cosmwasm_std::Coin::new(1, $base_denom), ]) .unwrap(); - + // Create two user accounts, an account for contract admin and one to be the recipient for the marker fee let t = $crate::tests::e2e::test_env::TestEnvBuilder::new() .with_account( "user1", @@ -38,6 +40,7 @@ macro_rules! setup { }) .build(&$app); + // -- Assert Contract State -- let $crate::msg::DenomsResponse { quote_denom, base_denom, @@ -52,7 +55,6 @@ macro_rules! setup { .contract .query(&$crate::msg::QueryMsg::GetTotalPoolLiquidity {}) .unwrap(); - assert_eq!( total_pool_liquidity, vec![ @@ -62,7 +64,6 @@ macro_rules! setup { ); let is_active: bool = t.contract.query(&$crate::msg::QueryMsg::IsActive {}).unwrap(); - assert!(is_active); // NOTE: wasm_sudo does not currently maintain state so these calls will not work @@ -95,7 +96,7 @@ pub mod assert { // -- Contract State Assertions /// Asserts that the orderbook's current liquidity matches what is provided - pub fn pool_liquidity( + pub(crate) fn pool_liquidity( t: &TestEnv, base_liquidity: impl Into, quote_liquidity: impl Into, @@ -123,7 +124,7 @@ pub mod assert { } /// Asserts that the contract's balance matches what is provided - pub fn pool_balance( + pub(crate) fn pool_balance( t: &TestEnv, base_liquidity: impl Into, quote_liquidity: impl Into, @@ -148,7 +149,7 @@ pub mod assert { } /// Asserts that the orderbook spot price matches what is provided - pub fn spot_price(t: &TestEnv, bid_tick: i64, ask_tick: i64, label: &str) { + pub(crate) fn spot_price(t: &TestEnv, bid_tick: i64, ask_tick: i64, label: &str) { let bid_price = tick_to_price(bid_tick).unwrap(); let ask_price = tick_to_price(ask_tick).unwrap(); let DenomsResponse { @@ -180,7 +181,7 @@ pub mod assert { /// Asserts that the contract balance is greater than or equal to what is recorded in the orderbook directional liquidity state /// If this assertion is ever false then the orderbook is "out of balance" and cannot provide liquidity for future orders - pub fn has_liquidity(t: &TestEnv) { + pub(crate) fn has_liquidity(t: &TestEnv) { let bid_liquidity = t.contract.get_directional_liquidity(OrderDirection::Bid); let ask_liquidity = t.contract.get_directional_liquidity(OrderDirection::Ask); @@ -240,7 +241,7 @@ pub mod assert { /// 3. The next bid tick is greater than or equal to the maximum tick with a bid amount /// /// This assertion can be run mid test as it must always be true - pub fn tick_invariants(t: &TestEnv) { + pub(crate) fn tick_invariants(t: &TestEnv) { let Orderbook { next_ask_tick, next_bid_tick, @@ -290,7 +291,7 @@ pub mod assert { } } - pub fn decrementing_market_order_output( + pub(crate) fn decrementing_market_order_output( t: &TestEnv, previous_market_value: u128, amount_to_run: u128, @@ -334,7 +335,7 @@ pub mod assert { } /// Asserts that there are no remaining orders in the orderbook - pub fn no_remaining_orders(t: &TestEnv) { + pub(crate) fn no_remaining_orders(t: &TestEnv) { let all_orders = t.contract.collect_all_orders(); assert_eq!(all_orders.len(), 0); } @@ -342,7 +343,7 @@ pub mod assert { /// Asserts that all ticks are fully synced /// /// **Should be run AFTER a fuzz test** - pub fn clean_ticks(t: &TestEnv) { + pub(crate) fn clean_ticks(t: &TestEnv) { let all_ticks = t.contract.collect_all_ticks(); for tick in all_ticks { let GetUnrealizedCancelsResponse { ticks } = t @@ -392,7 +393,7 @@ pub mod assert { /// An assertion that records balances before an action and compares the balances after the provided action /// Comparisons are only done for the vector of addresses provided in the second parameter - pub fn balance_changes( + pub(crate) fn balance_changes( t: &TestEnv, changes: &[(&str, Vec)], action: impl FnOnce() -> RunnerExecuteResult, @@ -476,7 +477,7 @@ pub mod orders { use super::assert; - pub fn place_limit( + pub(crate) fn place_limit( t: &TestEnv, tick_id: i64, order_direction: OrderDirection, @@ -509,7 +510,7 @@ pub mod orders { ) } - pub fn place_market( + pub(crate) fn place_market( cp: &CosmwasmPool, t: &TestEnv, order_direction: OrderDirection, @@ -523,15 +524,11 @@ pub mod orders { quote_denom, } = t.contract.query(&QueryMsg::Denoms {}).unwrap(); - let token_out_denom = if order_direction == OrderDirection::Bid { - base_denom.clone() + // Determine denom ordering based on order direction + let (token_in_denom, token_out_denom) = if order_direction == OrderDirection::Bid { + (quote_denom.clone(), base_denom.clone()) } else { - quote_denom.clone() - }; - let token_in_denom = if order_direction == OrderDirection::Bid { - quote_denom - } else { - base_denom + (base_denom.clone(), quote_denom.clone()) }; cp.swap_exact_amount_in( @@ -551,7 +548,7 @@ pub mod orders { /// Places a market order and asserts that the sender's balance changes correctly /// /// Note: this check has some circularity to it as the expected out depends on the `CalcOutAmtGivenInResponse` - pub fn place_market_and_assert_balance( + pub(crate) fn place_market_and_assert_balance( cp: &CosmwasmPool, t: &TestEnv, order_direction: OrderDirection, @@ -564,17 +561,14 @@ pub mod orders { quote_denom, } = t.contract.query(&QueryMsg::Denoms {}).unwrap(); - let token_out_denom = if order_direction == OrderDirection::Bid { - base_denom.clone() - } else { - quote_denom.clone() - }; - let token_in_denom = if order_direction == OrderDirection::Bid { - quote_denom + // Determine denom ordering based on order direction + let (token_in_denom, token_out_denom) = if order_direction == OrderDirection::Bid { + (quote_denom.clone(), base_denom.clone()) } else { - base_denom + (base_denom.clone(), quote_denom.clone()) }; + // DEV NOTE: is there a way to remove circular dependency for output expectancy? let CalcOutAmtGivenInResponse { token_out } = t .contract .query(&QueryMsg::CalcOutAmountGivenIn { @@ -600,7 +594,7 @@ pub mod orders { ) } - pub fn claim( + pub(crate) fn claim( t: &TestEnv, sender: &str, tick_id: i64, @@ -616,7 +610,7 @@ pub mod orders { /// Claims a given order using the provided sender account name /// /// Asserts that the sender and order owner's balances change correctly - pub fn claim_and_assert_balance( + pub(crate) fn claim_and_assert_balance( t: &TestEnv, sender: &str, owner: &str, @@ -716,7 +710,7 @@ pub mod orders { ) } - pub fn cancel_limit( + pub(crate) fn cancel_limit( t: &TestEnv, sender: &str, tick_id: i64, @@ -730,7 +724,7 @@ pub mod orders { } /// Cancels a limit order and asserts that the owner receives back the remaining order quantity (may be partially filled) - pub fn cancel_limit_and_assert_balance( + pub(crate) fn cancel_limit_and_assert_balance( t: &TestEnv, sender: &str, tick_id: i64, From 64c2f4d1dc31053cbb59f2c1f40914abd6e57f2c Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Wed, 3 Jul 2024 11:05:35 +0100 Subject: [PATCH 90/98] chore: remove unused variable --- contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index 398928a..23f9d34 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -24,7 +24,6 @@ use crate::{ pub(crate) const LARGE_POSITIVE_TICK: i64 = 4500000; // Tick Price = 0.00001 pub(crate) const LARGE_NEGATIVE_TICK: i64 = -4500000; -// pub(crate) const LARGE_NEGATIVE_TICK: i64 = -5000000; // Loops over a provided action for the provided duration // Tracks the number of operations and iterations From b3f60d3b357aff001399a8ffdd87313353dfc3c2 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Wed, 3 Jul 2024 11:13:34 +0100 Subject: [PATCH 91/98] chore: refactored out given in querys for e2e --- .../src/tests/e2e/cases/test_fuzz.rs | 20 +++---- .../src/tests/e2e/cases/utils.rs | 60 +++++-------------- .../src/tests/e2e/test_env.rs | 30 ++++++++-- 3 files changed, 46 insertions(+), 64 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index 23f9d34..a7731ef 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::time::{Duration, SystemTime}; -use cosmwasm_std::{Coin, Decimal}; +use cosmwasm_std::Coin; use cosmwasm_std::{Decimal256, Uint128}; use osmosis_test_tube::{Account, Module, OsmosisTestApp}; use rand::seq::SliceRandom; @@ -10,7 +10,7 @@ use rand::{rngs::StdRng, SeedableRng}; use super::utils::{assert, orders}; use crate::constants::MIN_TICK; -use crate::msg::{CalcOutAmtGivenInResponse, QueryMsg}; +use crate::msg::QueryMsg; use crate::tests::e2e::modules::cosmwasm_pool::CosmwasmPool; use crate::tick_math::{amount_to_value, tick_to_price, RoundingDirection}; use crate::{ @@ -647,10 +647,10 @@ fn place_random_market( order_direction: OrderDirection, ) -> u128 { // Get the appropriate denom for the chosen direction - let (token_in_denom, token_out_denom) = if order_direction == OrderDirection::Bid { - ("quote", "base") + let token_in_denom = if order_direction == OrderDirection::Bid { + "quote" } else { - ("base", "quote") + "base" }; // Select a random amount of the token in to swap @@ -663,16 +663,10 @@ fn place_random_market( } // Calculate the expected amount of token out - let expected_out = - t.contract - .query::(&QueryMsg::CalcOutAmountGivenIn { - token_in: Coin::new(amount, token_in_denom.to_string()), - token_out_denom: token_out_denom.to_string(), - swap_fee: Decimal::zero(), - }); + let expected_out = t.contract.get_out_given_in(order_direction, amount); if let Ok(expected_out) = expected_out { - if expected_out.token_out.amount == "0" { + if expected_out.amount == "0" { return 0; } } else if expected_out.is_err() { diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs index 27499cd..d83d828 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs @@ -83,14 +83,14 @@ pub mod assert { use crate::{ msg::{ - CalcOutAmtGivenInResponse, DenomsResponse, GetTotalPoolLiquidityResponse, - GetUnrealizedCancelsResponse, QueryMsg, SpotPriceResponse, TickIdAndState, + DenomsResponse, GetTotalPoolLiquidityResponse, GetUnrealizedCancelsResponse, QueryMsg, + SpotPriceResponse, TickIdAndState, }, tests::e2e::test_env::TestEnv, tick_math::{amount_to_value, tick_to_price, RoundingDirection}, types::{OrderDirection, Orderbook}, }; - use cosmwasm_std::{Coin, Coins, Decimal, Fraction, Uint128}; + use cosmwasm_std::{Coin, Coins, Fraction, Uint128}; use osmosis_test_tube::{cosmrs::proto::prost::Message, RunnerExecuteResult}; // -- Contract State Assertions @@ -291,39 +291,21 @@ pub mod assert { } } + // Asserts that a new market order will return a lower or equal expected amount that a previous market expected output pub(crate) fn decrementing_market_order_output( t: &TestEnv, previous_market_value: u128, amount_to_run: u128, direction: OrderDirection, ) -> Uint128 { - let DenomsResponse { - quote_denom, - base_denom, - } = t.contract.get_denoms(); - - let token_in_denom = if direction == OrderDirection::Bid { - quote_denom.clone() - } else { - base_denom.clone() - }; - let token_out_denom = if direction == OrderDirection::Bid { - base_denom - } else { - quote_denom - }; - - let maybe_expected_output = t.contract.query(&QueryMsg::CalcOutAmountGivenIn { - token_in: Coin::new(amount_to_run, token_in_denom), - token_out_denom, - swap_fee: Decimal::zero(), - }); + // Calculate the expected output for a market order of the given amount + let maybe_expected_output = t.contract.get_out_given_in(direction, amount_to_run); + // If the expected output errors we return zero let expected_output = maybe_expected_output - .map_or(Uint128::zero(), |r: CalcOutAmtGivenInResponse| { - Uint128::from_str(&r.token_out.amount).unwrap() - }); + .map_or(Uint128::zero(), |r| Uint128::from_str(&r.amount).unwrap()); + // Assert that the expected output is less than or equal to the previous market value assert!( expected_output.u128() <= previous_market_value, "subsequent market orders increased unexpectedly, got: {}, expected: {}", @@ -331,6 +313,7 @@ pub mod assert { previous_market_value ); + // Return the expected output expected_output } @@ -458,7 +441,7 @@ pub mod assert { pub mod orders { use std::str::FromStr; - use cosmwasm_std::{coins, Coin, Decimal, Decimal256, Uint128, Uint256}; + use cosmwasm_std::{coins, Coin, Decimal256, Uint128, Uint256}; use osmosis_std::types::{ cosmwasm::wasm::v1::MsgExecuteContractResponse, @@ -469,7 +452,7 @@ pub mod orders { use osmosis_test_tube::{Account, OsmosisTestApp, RunnerExecuteResult}; use crate::{ - msg::{CalcOutAmtGivenInResponse, DenomsResponse, ExecuteMsg, QueryMsg}, + msg::{DenomsResponse, ExecuteMsg, QueryMsg}, tests::e2e::{modules::cosmwasm_pool::CosmwasmPool, test_env::TestEnv}, tick_math::{amount_to_value, tick_to_price, RoundingDirection}, types::{LimitOrder, OrderDirection}, @@ -556,26 +539,11 @@ pub mod orders { sender: &str, ) -> RunnerExecuteResult { let quantity_u128: Uint128 = quantity.clone().into(); - let DenomsResponse { - base_denom, - quote_denom, - } = t.contract.query(&QueryMsg::Denoms {}).unwrap(); - - // Determine denom ordering based on order direction - let (token_in_denom, token_out_denom) = if order_direction == OrderDirection::Bid { - (quote_denom.clone(), base_denom.clone()) - } else { - (base_denom.clone(), quote_denom.clone()) - }; // DEV NOTE: is there a way to remove circular dependency for output expectancy? - let CalcOutAmtGivenInResponse { token_out } = t + let token_out = t .contract - .query(&QueryMsg::CalcOutAmountGivenIn { - token_in: Coin::new(quantity_u128.u128(), token_in_denom.clone()), - token_out_denom, - swap_fee: Decimal::zero(), - }) + .get_out_given_in(order_direction, quantity_u128.u128()) .unwrap(); assert::balance_changes( diff --git a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs index 413beaa..48cd63e 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs @@ -3,9 +3,9 @@ use std::{collections::HashMap, path::PathBuf, str::FromStr}; use crate::{ constants::{MAX_TICK, MIN_TICK}, msg::{ - AuthExecuteMsg, DenomsResponse, ExecuteMsg, GetTotalPoolLiquidityResponse, - GetUnrealizedCancelsResponse, InstantiateMsg, OrdersResponse, QueryMsg, SudoMsg, - TickIdAndState, TickUnrealizedCancels, TicksResponse, + AuthExecuteMsg, CalcOutAmtGivenInResponse, DenomsResponse, ExecuteMsg, + GetTotalPoolLiquidityResponse, GetUnrealizedCancelsResponse, InstantiateMsg, + OrdersResponse, QueryMsg, SudoMsg, TickIdAndState, TickUnrealizedCancels, TicksResponse, }, tests::test_utils::decimal256_from_u128, tick_math::{amount_to_value, tick_to_price, RoundingDirection}, @@ -13,9 +13,9 @@ use crate::{ ContractError, }; -use cosmwasm_std::{to_json_binary, Addr, Coin, Coins, Decimal256, Uint128, Uint256}; +use cosmwasm_std::{to_json_binary, Addr, Coin, Coins, Decimal, Decimal256, Uint128, Uint256}; use osmosis_std::types::{ - cosmos::bank::v1beta1::QueryAllBalancesRequest, + cosmos::{bank::v1beta1::QueryAllBalancesRequest, base::v1beta1::Coin as ProtoCoin}, cosmwasm::wasm::v1::MsgExecuteContractResponse, osmosis::cosmwasmpool::v1beta1::{ ContractInfoByPoolIdRequest, ContractInfoByPoolIdResponse, MsgCreateCosmWasmPool, @@ -413,6 +413,26 @@ impl<'a> OrderbookContract<'a> { } max_amount.u128() } + + // Calculate the expected output for a given input amount/direction using the CosmWasm pool query + pub(crate) fn get_out_given_in( + &self, + direction: OrderDirection, + amount: impl Into, + ) -> RunnerResult { + let (token_in_denom, token_out_denom) = if direction == OrderDirection::Bid { + (self.get_denoms().quote_denom, self.get_denoms().base_denom) + } else { + (self.get_denoms().base_denom, self.get_denoms().quote_denom) + }; + + self.query(&QueryMsg::CalcOutAmountGivenIn { + token_in: Coin::new(amount.into(), token_in_denom), + token_out_denom, + swap_fee: Decimal::zero(), + }) + .map(|r: CalcOutAmtGivenInResponse| r.token_out) + } } pub fn _assert_contract_err(expected: ContractError, actual: RunnerError) { From 523f283f5860358088be39f3a2a9d76a09769542 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Wed, 3 Jul 2024 11:22:30 +0100 Subject: [PATCH 92/98] chore: cleanup test_env --- .../src/tests/e2e/test_env.rs | 264 +++++++++--------- 1 file changed, 127 insertions(+), 137 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs index 48cd63e..d09185f 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/test_env.rs @@ -10,7 +10,6 @@ use crate::{ tests::test_utils::decimal256_from_u128, tick_math::{amount_to_value, tick_to_price, RoundingDirection}, types::{LimitOrder, OrderDirection}, - ContractError, }; use cosmwasm_std::{to_json_binary, Addr, Coin, Coins, Decimal, Decimal256, Uint128, Uint256}; @@ -42,28 +41,12 @@ pub struct TestEnv<'a> { } impl<'a> TestEnv<'a> { - pub fn add_account(&mut self, username: &str, balance: Vec) { + pub(crate) fn add_account(&mut self, username: &str, balance: Vec) { let account = self.app.init_account(&balance).unwrap(); self.accounts.insert(username.to_string(), account); } - pub fn _assert_account_balances( - &self, - account: &str, - expected_balances: Vec, - ignore_denoms: Vec<&str>, - ) { - let account_balances: Vec = self - ._get_account_balance(account) - .iter() - .filter(|coin| !ignore_denoms.contains(&coin.denom.as_str())) - .cloned() - .collect(); - - assert_eq!(account_balances, expected_balances); - } - - pub fn assert_contract_balances(&self, expected_balances: &[Coin], label: &str) { + pub(crate) fn assert_contract_balances(&self, expected_balances: &[Coin], label: &str) { let contract_balances: Vec = self.get_balance(&self.contract.contract_addr); assert_eq!( @@ -73,7 +56,7 @@ impl<'a> TestEnv<'a> { ); } - pub fn get_balance(&self, address: &str) -> Vec { + pub(crate) fn get_balance(&self, address: &str) -> Vec { let account_balances: Vec = Bank::new(self.app) .query_all_balances(&QueryAllBalancesRequest { address: address.to_string(), @@ -87,12 +70,6 @@ impl<'a> TestEnv<'a> { account_balances } - - pub fn _get_account_balance(&self, account: &str) -> Vec { - let account = self.accounts.get(account).unwrap(); - - self.get_balance(&account.address()) - } } pub struct TestEnvBuilder { @@ -101,23 +78,23 @@ pub struct TestEnvBuilder { } impl TestEnvBuilder { - pub fn new() -> Self { + pub(crate) fn new() -> Self { Self { account_balances: HashMap::new(), instantiate_msg: None, } } - pub fn with_instantiate_msg(mut self, msg: InstantiateMsg) -> Self { + pub(crate) fn with_instantiate_msg(mut self, msg: InstantiateMsg) -> Self { self.instantiate_msg = Some(msg); self } - pub fn with_account(mut self, account: &str, balance: Vec) -> Self { + pub(crate) fn with_account(mut self, account: &str, balance: Vec) -> Self { self.account_balances.insert(account.to_string(), balance); self } - pub fn build(self, app: &'_ OsmosisTestApp) -> TestEnv<'_> { + pub(crate) fn build(self, app: &'_ OsmosisTestApp) -> TestEnv<'_> { let accounts: HashMap<_, _> = self .account_balances .into_iter() @@ -157,7 +134,7 @@ pub struct OrderbookContract<'a> { } impl<'a> OrderbookContract<'a> { - pub fn deploy( + pub(crate) fn deploy( app: &'a OsmosisTestApp, instantiate_msg: &InstantiateMsg, signer: &SigningAccount, @@ -203,7 +180,7 @@ impl<'a> OrderbookContract<'a> { Ok(contract) } - pub fn execute( + pub(crate) fn execute( &self, msg: &ExecuteMsg, funds: &[Coin], @@ -213,7 +190,7 @@ impl<'a> OrderbookContract<'a> { wasm.execute(&self.contract_addr, msg, funds, signer) } - pub fn query(&self, msg: &QueryMsg) -> RunnerResult + pub(crate) fn query(&self, msg: &QueryMsg) -> RunnerResult where Res: ?Sized + DeserializeOwned, { @@ -221,7 +198,7 @@ impl<'a> OrderbookContract<'a> { wasm.query(&self.contract_addr, msg) } - pub fn get_wasm_byte_code() -> Vec { + pub(crate) fn get_wasm_byte_code() -> Vec { let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); std::fs::read( manifest_path @@ -235,7 +212,8 @@ impl<'a> OrderbookContract<'a> { .unwrap() } - pub fn _set_admin(&self, app: &OsmosisTestApp, admin: Addr) { + // -- Admin Methods -- + pub(crate) fn _set_admin(&self, app: &OsmosisTestApp, admin: Addr) { app.wasm_sudo( &self.contract_addr, SudoMsg::TransferAdmin { new_admin: admin }, @@ -243,7 +221,7 @@ impl<'a> OrderbookContract<'a> { .unwrap(); } - pub fn _set_maker_fee( + pub(crate) fn _set_maker_fee( &self, signer: &SigningAccount, maker_fee: Decimal256, @@ -266,52 +244,74 @@ impl<'a> OrderbookContract<'a> { .unwrap(); } - pub fn get_order_claimable_amount(&self, order: LimitOrder) -> u128 { - let TicksResponse { ticks } = self - .query(&QueryMsg::AllTicks { - start_from: Some(order.tick_id), - end_at: None, - limit: Some(1), - }) - .unwrap(); - let tick = ticks.first().unwrap().tick_state.clone(); - let tick_values: crate::types::TickValues = tick.get_values(order.order_direction); - let GetUnrealizedCancelsResponse { ticks } = self - .query(&QueryMsg::GetUnrealizedCancels { - tick_ids: vec![order.tick_id], - }) - .unwrap(); - let TickUnrealizedCancels { - unrealized_cancels, .. - } = ticks.first().unwrap(); - let cancelled_amount = match order.order_direction { - OrderDirection::Bid => unrealized_cancels.bid_unrealized_cancels, - OrderDirection::Ask => unrealized_cancels.ask_unrealized_cancels, - }; - - let synced_etas = tick_values - .effective_total_amount_swapped - .checked_add(cancelled_amount) - .unwrap(); - let expected_amount_u256 = synced_etas - .saturating_sub(order.etas) - .to_uint_floor() - .min(Uint256::from(order.quantity.u128())); - - let expected_amount = Uint128::try_from(expected_amount_u256).unwrap(); - expected_amount.u128() - } + // -- Queries -- - pub fn get_maker_fee(&self) -> Decimal256 { + pub(crate) fn get_maker_fee(&self) -> Decimal256 { let maker_fee: Decimal256 = self.query(&QueryMsg::GetMakerFee {}).unwrap(); maker_fee } - pub fn get_denoms(&self) -> DenomsResponse { + pub(crate) fn get_denoms(&self) -> DenomsResponse { self.query(&QueryMsg::Denoms {}).unwrap() } - pub fn collect_all_ticks(&self) -> Vec { + // Calculate the expected output for a given input amount/direction using the CosmWasm pool query + pub(crate) fn get_out_given_in( + &self, + direction: OrderDirection, + amount: impl Into, + ) -> RunnerResult { + let (token_in_denom, token_out_denom) = if direction == OrderDirection::Bid { + (self.get_denoms().quote_denom, self.get_denoms().base_denom) + } else { + (self.get_denoms().base_denom, self.get_denoms().quote_denom) + }; + + self.query(&QueryMsg::CalcOutAmountGivenIn { + token_in: Coin::new(amount.into(), token_in_denom), + token_out_denom, + swap_fee: Decimal::zero(), + }) + .map(|r: CalcOutAmtGivenInResponse| r.token_out) + } + + pub(crate) fn get_directional_liquidity(&self, order_direction: OrderDirection) -> u128 { + let GetTotalPoolLiquidityResponse { + total_pool_liquidity, + } = self.query(&QueryMsg::GetTotalPoolLiquidity {}).unwrap(); + + // Determine the amount of liquidity for the given direction + let liquidity = if order_direction == OrderDirection::Bid { + Coins::try_from(total_pool_liquidity.clone()) + .unwrap() + .amount_of("base") + } else { + Coins::try_from(total_pool_liquidity.clone()) + .unwrap() + .amount_of("quote") + }; + + liquidity.u128() + } + + pub(crate) fn get_order( + &self, + sender: String, + tick_id: i64, + order_id: u64, + ) -> Option { + let OrdersResponse { orders, .. } = self + .query(&QueryMsg::OrdersByOwner { + owner: Addr::unchecked(sender), + start_from: Some((tick_id, order_id)), + end_at: None, + limit: Some(1), + }) + .unwrap(); + orders.first().cloned() + } + + pub(crate) fn collect_all_ticks(&self) -> Vec { let mut ticks = vec![]; let mut min_tick = MIN_TICK; while min_tick <= MAX_TICK { @@ -326,12 +326,13 @@ impl<'a> OrderbookContract<'a> { break; } ticks.extend(tick.ticks.clone()); + // Determine the next tick to start at for the next query loop min_tick = tick.ticks.iter().max_by_key(|t| t.tick_id).unwrap().tick_id + 1; } ticks } - pub fn collect_all_orders(&self) -> Vec { + pub(crate) fn collect_all_orders(&self) -> Vec { let ticks = self.collect_all_ticks(); let mut all_orders: Vec = vec![]; @@ -350,42 +351,17 @@ impl<'a> OrderbookContract<'a> { all_orders } - pub fn get_directional_liquidity(&self, order_direction: OrderDirection) -> u128 { - let GetTotalPoolLiquidityResponse { - total_pool_liquidity, - } = self.query(&QueryMsg::GetTotalPoolLiquidity {}).unwrap(); - - // Determine the amount of liquidity for the given direction - let liquidity = if order_direction == OrderDirection::Bid { - Coins::try_from(total_pool_liquidity.clone()) - .unwrap() - .amount_of("base") - } else { - Coins::try_from(total_pool_liquidity.clone()) - .unwrap() - .amount_of("quote") - }; - - liquidity.u128() - } - - pub fn get_order(&self, sender: String, tick_id: i64, order_id: u64) -> Option { - let OrdersResponse { orders, .. } = self - .query(&QueryMsg::OrdersByOwner { - owner: Addr::unchecked(sender), - start_from: Some((tick_id, order_id)), - end_at: None, - limit: Some(1), - }) - .unwrap(); - orders.first().cloned() - } - - pub fn get_max_market_amount(&self, direction: OrderDirection) -> u128 { + /// Calculates the max amount for a market order that can be placed + /// by iterating over all the ticks, calculating their liquidity and summing + /// + /// The amount caps at `u128::MAX` + pub(crate) fn get_max_market_amount(&self, direction: OrderDirection) -> u128 { let mut max_amount: Uint128 = Uint128::zero(); let ticks = self.collect_all_ticks(); for tick in ticks { let value = tick.tick_state.get_values(direction.opposite()); + + // If the tick has no liquidity we can skip this tick if value.total_amount_of_liquidity.is_zero() { continue; } @@ -414,37 +390,51 @@ impl<'a> OrderbookContract<'a> { max_amount.u128() } - // Calculate the expected output for a given input amount/direction using the CosmWasm pool query - pub(crate) fn get_out_given_in( - &self, - direction: OrderDirection, - amount: impl Into, - ) -> RunnerResult { - let (token_in_denom, token_out_denom) = if direction == OrderDirection::Bid { - (self.get_denoms().quote_denom, self.get_denoms().base_denom) - } else { - (self.get_denoms().base_denom, self.get_denoms().quote_denom) + /// Calculates how much is available for claim for a given order + /// + /// The amount that is claimable is dependent on a tick sync. + /// To account for this we first fetch the amount of unrealized cancels for the tick + /// and add it to the current ETAS before computing the difference. + pub(crate) fn get_order_claimable_amount(&self, order: LimitOrder) -> u128 { + let TicksResponse { ticks } = self + .query(&QueryMsg::AllTicks { + start_from: Some(order.tick_id), + end_at: None, + limit: Some(1), + }) + .unwrap(); + + // Get current tick values + let tick = ticks.first().unwrap().tick_state.clone(); + let tick_values = tick.get_values(order.order_direction); + + // Get the current unrealized cancels for the tick + let GetUnrealizedCancelsResponse { ticks } = self + .query(&QueryMsg::GetUnrealizedCancels { + tick_ids: vec![order.tick_id], + }) + .unwrap(); + let TickUnrealizedCancels { + unrealized_cancels, .. + } = ticks.first().unwrap(); + let cancelled_amount = match order.order_direction { + OrderDirection::Bid => unrealized_cancels.bid_unrealized_cancels, + OrderDirection::Ask => unrealized_cancels.ask_unrealized_cancels, }; - self.query(&QueryMsg::CalcOutAmountGivenIn { - token_in: Coin::new(amount.into(), token_in_denom), - token_out_denom, - swap_fee: Decimal::zero(), - }) - .map(|r: CalcOutAmtGivenInResponse| r.token_out) - } -} + // Add unrealized cancels to the current ETAS + let synced_etas = tick_values + .effective_total_amount_swapped + .checked_add(cancelled_amount) + .unwrap(); -pub fn _assert_contract_err(expected: ContractError, actual: RunnerError) { - match actual { - RunnerError::ExecuteError { msg } => { - if !msg.contains(&expected.to_string()) { - panic!( - "assertion failed:\n\n must contain \t: \"{}\",\n actual \t: \"{}\"\n", - expected, msg - ) - } - } - _ => panic!("unexpected error, expect execute error but got: {}", actual), - }; + // Compute the expected amount as if the tick had been synced + let expected_amount_u256 = synced_etas + .saturating_sub(order.etas) + .to_uint_floor() + .min(Uint256::from(order.quantity.u128())); + + let expected_amount = Uint128::try_from(expected_amount_u256).unwrap(); + expected_amount.u128() + } } From a5ca8b24f43e58748f8aaf074c940cf652010b0b Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Wed, 3 Jul 2024 12:07:10 +0100 Subject: [PATCH 93/98] test: rearranged e2e files and refactored order cleanup for fuzz tests --- .../src/tests/e2e/cases/mod.rs | 1 - .../src/tests/e2e/cases/test_fuzz.rs | 180 ++++++++++-------- .../src/tests/e2e/cases/test_orders.rs | 2 +- .../sumtree-orderbook/src/tests/e2e/mod.rs | 1 + .../src/tests/e2e/{cases => }/utils.rs | 5 +- 5 files changed, 101 insertions(+), 88 deletions(-) rename contracts/sumtree-orderbook/src/tests/e2e/{cases => }/utils.rs (99%) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/mod.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/mod.rs index 79d220e..0eb7047 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/mod.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/mod.rs @@ -1,3 +1,2 @@ mod test_fuzz; mod test_orders; -mod utils; diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index a7731ef..620463c 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -1,22 +1,20 @@ use std::collections::HashMap; use std::time::{Duration, SystemTime}; -use cosmwasm_std::Coin; -use cosmwasm_std::{Decimal256, Uint128}; +use cosmwasm_std::{Coin, Decimal256, Uint128}; use osmosis_test_tube::{Account, Module, OsmosisTestApp}; +use rand::rngs::StdRng; use rand::seq::SliceRandom; -use rand::Rng; -use rand::{rngs::StdRng, SeedableRng}; - -use super::utils::{assert, orders}; -use crate::constants::MIN_TICK; -use crate::msg::QueryMsg; -use crate::tests::e2e::modules::cosmwasm_pool::CosmwasmPool; -use crate::tick_math::{amount_to_value, tick_to_price, RoundingDirection}; +use rand::{Rng, SeedableRng}; + +use super::super::utils::*; use crate::{ - msg::{DenomsResponse, GetTotalPoolLiquidityResponse}, + constants::MIN_TICK, + msg::{DenomsResponse, GetTotalPoolLiquidityResponse, QueryMsg}, setup, + tests::e2e::modules::cosmwasm_pool::CosmwasmPool, tests::e2e::test_env::TestEnv, + tick_math::{amount_to_value, tick_to_price, RoundingDirection}, types::OrderDirection, }; @@ -105,8 +103,8 @@ fn test_order_fuzz_linear_large_all_cancelled_orders_small_range() { #[test] fn test_order_fuzz_linear_single_tick() { - let oper_per_iteration = 2000; - run_for_duration(10, oper_per_iteration, |count| { + let oper_per_iteration = 1000; + run_for_duration(60, oper_per_iteration, |count| { run_fuzz_linear(count, (0, 0), 0.2); }); } @@ -114,7 +112,7 @@ fn test_order_fuzz_linear_single_tick() { #[test] fn test_order_fuzz_mixed() { let oper_per_iteration = 2000; - run_for_duration(10, oper_per_iteration, |count| { + run_for_duration(60, oper_per_iteration, |count| { run_fuzz_mixed(count, (-20, 20)); }); } @@ -123,7 +121,7 @@ fn test_order_fuzz_mixed() { fn test_order_fuzz_mixed_single_tick() { let oper_per_iteration = 2000; - run_for_duration(10, oper_per_iteration, |count| { + run_for_duration(60, oper_per_iteration, |count| { run_fuzz_mixed(count, (0, 0)); }); } @@ -178,7 +176,7 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob let cp = CosmwasmPool::new(&app); let mut t = setup!(&app, "quote", "base", 1); - let mut orders = vec![]; + let mut orders: HashMap = HashMap::new(); // -- System Under Test -- @@ -195,13 +193,13 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob if is_cancelled { orders::cancel_limit_and_assert_balance(&t, &username, chosen_tick, i).unwrap(); } else { - orders.push((username, chosen_tick, i)); + orders.insert(i, (username, chosen_tick)); } assert::tick_invariants(&t); } - // -- Step 2: Place Market Orders -- + // -- Step 2: Place Market Orders & Fill Liquidity -- // For both directions fill the entire amount of liquidity available using market orders // For certain cases it is not possible to fill the entire liquidity so a remainder of 1 may occur @@ -214,6 +212,8 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob // A counter to track the current user ID let mut user_id = 0; + // Record starting expected output + // Provide max as previous to ensure we start with a valid expected output let mut previous_expected_out = assert::decrementing_market_order_output(&t, u128::MAX, 10000000u128, order_direction); @@ -223,8 +223,6 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob let placed_amount = place_random_market(&cp, &mut t, &mut rng, &username, order_direction); - // Increment the username of the order placer - user_id += 1; if placed_amount == 0 { // In the case that the last order cannot be filled we want an exit condition // If there are 100 consecutive zero amount returns we will break @@ -235,6 +233,9 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob continue; } + // Increment the username of the order placer + user_id += 1; + // Reset counter as order was placed zero_amount_returns = 0; @@ -264,47 +265,20 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob .unwrap(); println!("Total remaining pool liquidity: {:?}", total_pool_liquidity); - // -- Step 3: Claim Orders -- + // -- Step 3: Claim & Cancel Orders -- - // Shuffle the order of recorded orders (as liquidity is fully filled (except the possibility of a 1 remainder)) - // every order should be claimable and the order should not matter - orders.shuffle(&mut rng); - - for (username, tick_id, order_id) in orders.iter() { - // If the order has a claim bounty we will use a separate sender to verify that the bounty is claimed correctly - // Otherwise we will use the original sender to verify that the order is claimed correctly - t.add_account( - "claimant", - vec![ - Coin::new(1, "base"), - Coin::new(1, "quote"), - Coin::new(1000000000000u128, "uosmo"), - ], - ); - let order = t - .contract - .get_order(t.accounts[username].address(), *tick_id, *order_id) - .unwrap(); - let sender = if order.claim_bounty.is_some() { - "claimant" - } else { - username - }; - - orders::claim_and_assert_balance(&t, sender, username, order.tick_id, order.order_id) - .unwrap(); - - // For the situation that the order has the 1 remainder we record this for assertions - let maybe_order = t.contract.get_order( - t.accounts[username].address(), - order.tick_id, - order.order_id, - ); - if let Some(order) = maybe_order { - orders::cancel_limit_and_assert_balance(&t, username, order.tick_id, order.order_id) - .unwrap(); - } - } + // Attempt to claim & cancel all limit orders + let (bid_unclaimable_amount, ask_unclaimable_amount) = + clear_remaining_orders(&mut t, &mut rng, &mut orders); + // At most one order should remain in each direction + assert!( + bid_unclaimable_amount <= 1, + "bid_unclaimable_amount is greater than 1" + ); + assert!( + ask_unclaimable_amount <= 1, + "ask_unclaimable_amount is greater than 1" + ); // -- Post Test Assertions -- assert::clean_ticks(&t); @@ -542,29 +516,7 @@ fn run_fuzz_mixed(amount_of_orders: u64, tick_bounds: (i64, i64)) { assert::has_liquidity(&t); } - for (order_id, (username, tick_id)) in orders.clone().iter() { - let _ = orders::claim_and_assert_balance(&t, username, username, *tick_id, *order_id); - - // Order may be cleared by fully claiming, in which case we want to continue to the next order - if t.contract - .get_order(t.accounts[username.as_str()].address(), *tick_id, *order_id) - .is_none() - { - continue; - } - - // If cancelling is a success we can continue to the next order - if orders::cancel_limit_and_assert_balance(&t, username, *tick_id, *order_id).is_ok() { - continue; - } - - // If an order cannot be claimed or cancelled something has gone wrong - let order = t.contract.get_order(username.clone(), *tick_id, *order_id); - assert!( - order.is_none(), - "order was not cleaned from state: {order:?}" - ); - } + clear_remaining_orders(&mut t, &mut rng, &mut orders); // -- Post test assertions -- @@ -654,7 +606,6 @@ fn place_random_market( }; // Select a random amount of the token in to swap - // let liquidity_at_price = Uint128::try_from(liquidity_at_price_u256).unwrap(); let max_amount = t.contract.get_max_market_amount(order_direction); let amount = rng.gen_range(0..=max_amount); @@ -717,3 +668,64 @@ fn get_random_market_direction<'a>( Ok(OrderDirection::Ask) } } + +/// Attempts to clear the remaining orders by first attempting to claim the order. +/// If the order is claimed but not removed (not fully filled) it is then cancelled. +/// +/// If neither/both succeed and the order is still in state then something is wrong. +fn clear_remaining_orders( + t: &mut TestEnv, + rng: &mut StdRng, + orders: &mut HashMap, +) -> (u64, u64) { + // Shuffle the order of recorded orders (as liquidity is fully filled (except the possibility of a 1 remainder)) + // every order should be claimable and the order should not matter + let mut orders_vec = orders.iter().collect::>(); + orders_vec.shuffle(rng); + + // We track how many orders were not fully claimable for future assertions + let mut bid_unclaimable_amount = 0; + let mut ask_unclaimable_amount = 0; + + for (order_id, (username, tick_id)) in orders.clone().iter() { + let maybe_order = t + .contract + .get_order(t.accounts[username].address(), *tick_id, *order_id); + + if maybe_order.is_none() { + continue; + } + + let _ = orders::claim_and_assert_balance(t, username, username, *tick_id, *order_id); + + // Order may be cleared by fully claiming, in which case we want to continue to the next order + if t.contract + .get_order(t.accounts[username.as_str()].address(), *tick_id, *order_id) + .is_none() + { + continue; + } + + let order = maybe_order.unwrap(); + match order.order_direction { + OrderDirection::Bid => bid_unclaimable_amount += 1, + OrderDirection::Ask => ask_unclaimable_amount += 1, + } + + // If cancelling is a success we can continue to the next order + if orders::cancel_limit_and_assert_balance(t, username, *tick_id, *order_id).is_ok() { + continue; + } + + // If an order cannot be claimed or cancelled something has gone wrong + let order = t + .contract + .get_order(t.accounts[username].address(), *tick_id, *order_id); + assert!( + order.is_none(), + "order was not cleaned from state: {order:?}" + ); + } + + (bid_unclaimable_amount, ask_unclaimable_amount) +} diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders.rs index d1dba1e..74d8a0d 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_orders.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{coin, Decimal256, Uint128}; use osmosis_test_tube::{Module, OsmosisTestApp}; -use super::utils::{assert, orders}; +use super::super::utils::*; use crate::{ constants::{MAX_TICK, MIN_TICK}, setup, diff --git a/contracts/sumtree-orderbook/src/tests/e2e/mod.rs b/contracts/sumtree-orderbook/src/tests/e2e/mod.rs index 9ff109b..2a95497 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/mod.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/mod.rs @@ -3,3 +3,4 @@ mod cases; mod modules; mod test_env; +mod utils; diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs b/contracts/sumtree-orderbook/src/tests/e2e/utils.rs similarity index 99% rename from contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs rename to contracts/sumtree-orderbook/src/tests/e2e/utils.rs index d83d828..224f6e2 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/utils.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/utils.rs @@ -340,8 +340,9 @@ pub mod assert { let values = tick.tick_state.get_values(direction); assert!( values.total_amount_of_liquidity.is_zero(), - "tick {} has liquidity", - tick.tick_id + "tick {} has {} liquidity", + tick.tick_id, + values.total_amount_of_liquidity ); let unrealized_cancels = match direction { From d49afab9d6445b9bdb4dce2404d87bd9bad75ae5 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Wed, 3 Jul 2024 12:14:14 +0100 Subject: [PATCH 94/98] test: fixed failing test --- contracts/sumtree-orderbook/src/tests/test_order.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/test_order.rs b/contracts/sumtree-orderbook/src/tests/test_order.rs index c3849e5..46783e1 100644 --- a/contracts/sumtree-orderbook/src/tests/test_order.rs +++ b/contracts/sumtree-orderbook/src/tests/test_order.rs @@ -2113,14 +2113,12 @@ fn test_claim_order() { OrderOperation::Cancel((valid_tick_id, 0)), OrderOperation::RunMarket(MarketOrder::new( // Filling 100/100 of the Ask order - // Tick price is 0.5, 0.5*100 = 50 - Uint128::from(50u128), + Uint128::from(100u128), OrderDirection::Bid, Addr::unchecked("buyer"), )), ], order_id: 1, - tick_id: valid_tick_id, expected_bank_msg: Some(SubMsg::reply_on_error( MsgSend256 { From 030176e132ee8efdeb6b855bb24a6a4fafe3c802 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Wed, 3 Jul 2024 12:18:13 +0100 Subject: [PATCH 95/98] chore: claim fuzz operation cleanup --- .../src/tests/e2e/cases/test_fuzz.rs | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index 620463c..f1114b1 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -430,23 +430,16 @@ impl MixedFuzzOperation { }; // Claim the order - match orders::claim_and_assert_balance(t, claimant, &username, tick_id, order_id) { - Ok(_) => { - let order = t.contract.get_order( - t.accounts[&username].address(), - tick_id, - order_id, - ); - if order.is_none() { - // Remove the order once we know its claimable - orders.remove(&order_id).unwrap(); - } - Ok(true) - } - Err(e) => { - panic!("{e}") - } + orders::claim_and_assert_balance(t, claimant, &username, tick_id, order_id) + .unwrap(); + let order = + t.contract + .get_order(t.accounts[&username].address(), tick_id, order_id); + if order.is_none() { + // Remove the order once we know its claimable + orders.remove(&order_id).unwrap(); } + Ok(true) } } } From ad963eaaf79d81f57ea53855e287a51443ea1c18 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Wed, 3 Jul 2024 12:37:47 +0100 Subject: [PATCH 96/98] chore: final cleanup --- .../src/tests/e2e/cases/test_fuzz.rs | 28 +++++++------------ .../sumtree-orderbook/src/tests/e2e/utils.rs | 17 ++++++++++- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index f1114b1..c025c24 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -8,13 +8,13 @@ use rand::seq::SliceRandom; use rand::{Rng, SeedableRng}; use super::super::utils::*; +use crate::ContractError; use crate::{ constants::MIN_TICK, msg::{DenomsResponse, GetTotalPoolLiquidityResponse, QueryMsg}, setup, tests::e2e::modules::cosmwasm_pool::CosmwasmPool, tests::e2e::test_env::TestEnv, - tick_math::{amount_to_value, tick_to_price, RoundingDirection}, types::OrderDirection, }; @@ -269,7 +269,7 @@ fn run_fuzz_linear(amount_limit_orders: u64, tick_range: (i64, i64), cancel_prob // Attempt to claim & cancel all limit orders let (bid_unclaimable_amount, ask_unclaimable_amount) = - clear_remaining_orders(&mut t, &mut rng, &mut orders); + clear_remaining_orders(&mut t, &mut rng, &orders); // At most one order should remain in each direction assert!( bid_unclaimable_amount <= 1, @@ -376,6 +376,8 @@ impl MixedFuzzOperation { // Determine if the order can be cancelled if amount_claimable > 0 { + let res = orders::cancel_limit(t, &username, tick_id, order_id).unwrap_err(); + assert::contract_err(ContractError::CancelFilledOrder, res); return Ok(false); } @@ -406,19 +408,8 @@ impl MixedFuzzOperation { // Determine if the order can be claimed if amount_claimable == 0 { - return Ok(false); - } - - let price = tick_to_price(order.tick_id).unwrap(); - let expected_received_u256 = amount_to_value( - order.order_direction, - Uint128::from(amount_claimable), - price, - RoundingDirection::Down, - ) - .unwrap(); - - if expected_received_u256.is_zero() { + let res = orders::claim(t, &username, tick_id, order_id).unwrap_err(); + assert::contract_err(ContractError::ZeroClaim, res); return Ok(false); } @@ -509,7 +500,8 @@ fn run_fuzz_mixed(amount_of_orders: u64, tick_bounds: (i64, i64)) { assert::has_liquidity(&t); } - clear_remaining_orders(&mut t, &mut rng, &mut orders); + // Attempt to claim/cancel all remaining orders + clear_remaining_orders(&mut t, &mut rng, &orders); // -- Post test assertions -- @@ -669,7 +661,7 @@ fn get_random_market_direction<'a>( fn clear_remaining_orders( t: &mut TestEnv, rng: &mut StdRng, - orders: &mut HashMap, + orders: &HashMap, ) -> (u64, u64) { // Shuffle the order of recorded orders (as liquidity is fully filled (except the possibility of a 1 remainder)) // every order should be claimable and the order should not matter @@ -680,7 +672,7 @@ fn clear_remaining_orders( let mut bid_unclaimable_amount = 0; let mut ask_unclaimable_amount = 0; - for (order_id, (username, tick_id)) in orders.clone().iter() { + for (order_id, (username, tick_id)) in orders_vec.iter().cloned() { let maybe_order = t .contract .get_order(t.accounts[username].address(), *tick_id, *order_id); diff --git a/contracts/sumtree-orderbook/src/tests/e2e/utils.rs b/contracts/sumtree-orderbook/src/tests/e2e/utils.rs index 224f6e2..5e890d0 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/utils.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/utils.rs @@ -89,9 +89,10 @@ pub mod assert { tests::e2e::test_env::TestEnv, tick_math::{amount_to_value, tick_to_price, RoundingDirection}, types::{OrderDirection, Orderbook}, + ContractError, }; use cosmwasm_std::{Coin, Coins, Fraction, Uint128}; - use osmosis_test_tube::{cosmrs::proto::prost::Message, RunnerExecuteResult}; + use osmosis_test_tube::{cosmrs::proto::prost::Message, RunnerError, RunnerExecuteResult}; // -- Contract State Assertions @@ -436,6 +437,20 @@ pub mod assert { Ok(result) } + + pub(crate) fn contract_err(expected: ContractError, actual: RunnerError) { + match actual { + RunnerError::ExecuteError { msg } => { + if !msg.contains(&expected.to_string()) { + panic!( + "assertion failed:\n\n must contain \t: \"{}\",\n actual \t: \"{}\"\n", + expected, msg + ) + } + } + _ => panic!("unexpected error, expect execute error but got: {}", actual), + }; + } } /// Utility functions for interacting with the orderbook From 50343e6f99241ecb180e174324255fbe58316330 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Wed, 3 Jul 2024 12:50:39 +0100 Subject: [PATCH 97/98] test: reduced fuzz test duration --- .../src/tests/e2e/cases/test_fuzz.rs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index c025c24..5b21aa0 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -50,7 +50,7 @@ fn run_for_duration( #[test] fn test_order_fuzz_linear_large_orders_small_range() { let oper_per_iteration = 1000; - run_for_duration(60, oper_per_iteration, |count| { + run_for_duration(30, oper_per_iteration, |count| { run_fuzz_linear(count, (-10, 10), 0.2); }); } @@ -58,7 +58,7 @@ fn test_order_fuzz_linear_large_orders_small_range() { #[test] fn test_order_fuzz_linear_small_orders_large_range() { let oper_per_iteration = 2000; - run_for_duration(60, oper_per_iteration, |count| { + run_for_duration(30, oper_per_iteration, |count| { run_fuzz_linear(count, (LARGE_NEGATIVE_TICK, LARGE_POSITIVE_TICK), 0.2); }); } @@ -72,7 +72,7 @@ fn test_order_fuzz_linear_small_orders_large_range() { #[test] fn test_order_fuzz_linear_small_orders_small_range() { let oper_per_iteration = 100; - run_for_duration(60, oper_per_iteration, |count| { + run_for_duration(30, oper_per_iteration, |count| { run_fuzz_linear(count, (-10, 0), 0.1); }); } @@ -80,7 +80,7 @@ fn test_order_fuzz_linear_small_orders_small_range() { #[test] fn test_order_fuzz_linear_large_cancelled_orders_small_range() { let oper_per_iteration = 2000; - run_for_duration(60, oper_per_iteration, |count| { + run_for_duration(30, oper_per_iteration, |count| { run_fuzz_linear(count, (MIN_TICK, MIN_TICK + 20), 0.8); }); } @@ -88,7 +88,7 @@ fn test_order_fuzz_linear_large_cancelled_orders_small_range() { #[test] fn test_order_fuzz_linear_small_cancelled_orders_large_range() { let oper_per_iteration = 100; - run_for_duration(60, oper_per_iteration, |count| { + run_for_duration(30, oper_per_iteration, |count| { run_fuzz_linear(count, (LARGE_NEGATIVE_TICK, LARGE_POSITIVE_TICK), 0.8); }); } @@ -96,7 +96,7 @@ fn test_order_fuzz_linear_small_cancelled_orders_large_range() { #[test] fn test_order_fuzz_linear_large_all_cancelled_orders_small_range() { let oper_per_iteration = 2000; - run_for_duration(60, oper_per_iteration, |count| { + run_for_duration(30, oper_per_iteration, |count| { run_fuzz_linear(count, (-10, 10), 1.0); }); } @@ -104,7 +104,7 @@ fn test_order_fuzz_linear_large_all_cancelled_orders_small_range() { #[test] fn test_order_fuzz_linear_single_tick() { let oper_per_iteration = 1000; - run_for_duration(60, oper_per_iteration, |count| { + run_for_duration(30, oper_per_iteration, |count| { run_fuzz_linear(count, (0, 0), 0.2); }); } @@ -112,7 +112,7 @@ fn test_order_fuzz_linear_single_tick() { #[test] fn test_order_fuzz_mixed() { let oper_per_iteration = 2000; - run_for_duration(60, oper_per_iteration, |count| { + run_for_duration(30, oper_per_iteration, |count| { run_fuzz_mixed(count, (-20, 20)); }); } @@ -121,7 +121,7 @@ fn test_order_fuzz_mixed() { fn test_order_fuzz_mixed_single_tick() { let oper_per_iteration = 2000; - run_for_duration(60, oper_per_iteration, |count| { + run_for_duration(30, oper_per_iteration, |count| { run_fuzz_mixed(count, (0, 0)); }); } @@ -130,7 +130,7 @@ fn test_order_fuzz_mixed_single_tick() { fn test_order_fuzz_mixed_large_negative_tick_range() { let oper_per_iteration = 2000; - run_for_duration(60, oper_per_iteration, |count| { + run_for_duration(30, oper_per_iteration, |count| { run_fuzz_mixed(count, (LARGE_NEGATIVE_TICK, LARGE_NEGATIVE_TICK + 10)); }); } @@ -139,7 +139,7 @@ fn test_order_fuzz_mixed_large_negative_tick_range() { fn test_order_fuzz_mixed_large_positive_tick_range() { let oper_per_iteration = 2000; - run_for_duration(60, oper_per_iteration, |count| { + run_for_duration(30, oper_per_iteration, |count| { run_fuzz_mixed(count, (LARGE_POSITIVE_TICK - 10, LARGE_POSITIVE_TICK)); }); } @@ -148,7 +148,7 @@ fn test_order_fuzz_mixed_large_positive_tick_range() { fn test_order_fuzz_mixed_min_tick() { let oper_per_iteration = 2000; - run_for_duration(60, oper_per_iteration, |count| { + run_for_duration(30, oper_per_iteration, |count| { run_fuzz_mixed(count, (MIN_TICK, MIN_TICK + 10)); }); } @@ -157,7 +157,7 @@ fn test_order_fuzz_mixed_min_tick() { fn test_order_fuzz_large_tick_range() { let oper_per_iteration = 2000; - run_for_duration(60, oper_per_iteration, |count| { + run_for_duration(30, oper_per_iteration, |count| { run_fuzz_mixed(count, (MIN_TICK, LARGE_POSITIVE_TICK)); }); } From 5c1911ad80cefc28b8a793e126291de48546943b Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Wed, 3 Jul 2024 12:59:01 +0100 Subject: [PATCH 98/98] test: reduced fuzz duration oper count --- .../src/tests/e2e/cases/test_fuzz.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs index 5b21aa0..8b6599d 100644 --- a/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs +++ b/contracts/sumtree-orderbook/src/tests/e2e/cases/test_fuzz.rs @@ -57,7 +57,7 @@ fn test_order_fuzz_linear_large_orders_small_range() { #[test] fn test_order_fuzz_linear_small_orders_large_range() { - let oper_per_iteration = 2000; + let oper_per_iteration = 1000; run_for_duration(30, oper_per_iteration, |count| { run_fuzz_linear(count, (LARGE_NEGATIVE_TICK, LARGE_POSITIVE_TICK), 0.2); }); @@ -79,7 +79,7 @@ fn test_order_fuzz_linear_small_orders_small_range() { #[test] fn test_order_fuzz_linear_large_cancelled_orders_small_range() { - let oper_per_iteration = 2000; + let oper_per_iteration = 1000; run_for_duration(30, oper_per_iteration, |count| { run_fuzz_linear(count, (MIN_TICK, MIN_TICK + 20), 0.8); }); @@ -95,7 +95,7 @@ fn test_order_fuzz_linear_small_cancelled_orders_large_range() { #[test] fn test_order_fuzz_linear_large_all_cancelled_orders_small_range() { - let oper_per_iteration = 2000; + let oper_per_iteration = 1000; run_for_duration(30, oper_per_iteration, |count| { run_fuzz_linear(count, (-10, 10), 1.0); }); @@ -111,7 +111,7 @@ fn test_order_fuzz_linear_single_tick() { #[test] fn test_order_fuzz_mixed() { - let oper_per_iteration = 2000; + let oper_per_iteration = 1000; run_for_duration(30, oper_per_iteration, |count| { run_fuzz_mixed(count, (-20, 20)); }); @@ -119,7 +119,7 @@ fn test_order_fuzz_mixed() { #[test] fn test_order_fuzz_mixed_single_tick() { - let oper_per_iteration = 2000; + let oper_per_iteration = 1000; run_for_duration(30, oper_per_iteration, |count| { run_fuzz_mixed(count, (0, 0)); @@ -128,7 +128,7 @@ fn test_order_fuzz_mixed_single_tick() { #[test] fn test_order_fuzz_mixed_large_negative_tick_range() { - let oper_per_iteration = 2000; + let oper_per_iteration = 1000; run_for_duration(30, oper_per_iteration, |count| { run_fuzz_mixed(count, (LARGE_NEGATIVE_TICK, LARGE_NEGATIVE_TICK + 10)); @@ -137,7 +137,7 @@ fn test_order_fuzz_mixed_large_negative_tick_range() { #[test] fn test_order_fuzz_mixed_large_positive_tick_range() { - let oper_per_iteration = 2000; + let oper_per_iteration = 1000; run_for_duration(30, oper_per_iteration, |count| { run_fuzz_mixed(count, (LARGE_POSITIVE_TICK - 10, LARGE_POSITIVE_TICK)); @@ -146,7 +146,7 @@ fn test_order_fuzz_mixed_large_positive_tick_range() { #[test] fn test_order_fuzz_mixed_min_tick() { - let oper_per_iteration = 2000; + let oper_per_iteration = 1000; run_for_duration(30, oper_per_iteration, |count| { run_fuzz_mixed(count, (MIN_TICK, MIN_TICK + 10)); @@ -155,7 +155,7 @@ fn test_order_fuzz_mixed_min_tick() { #[test] fn test_order_fuzz_large_tick_range() { - let oper_per_iteration = 2000; + let oper_per_iteration = 1000; run_for_duration(30, oper_per_iteration, |count| { run_fuzz_mixed(count, (MIN_TICK, LARGE_POSITIVE_TICK));