diff --git a/Cargo.lock b/Cargo.lock index b115e085..7eb52179 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,18 @@ dependencies = [ "cw-storage-plus 0.15.1", ] +[[package]] +name = "astroport" +version = "2.0.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 0.15.1", + "cw20 0.15.1", + "itertools", + "uint", +] + [[package]] name = "astroport" version = "2.1.0" @@ -48,13 +60,13 @@ dependencies = [ name = "astroport-auction" version = "1.0.0" dependencies = [ - "astroport", + "astroport 2.1.0", "astroport-lockdrop", "astroport-periphery", "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", - "cw-multi-test", + "cw-multi-test 0.13.4", "cw-storage-plus 0.15.1", "cw2 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)", "cw20 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -62,15 +74,34 @@ dependencies = [ "serde", ] +[[package]] +name = "astroport-factory" +version = "1.3.0" +dependencies = [ + "anyhow", + "astroport 2.0.0", + "astroport-pair", + "astroport-token", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test 0.15.1", + "cw-storage-plus 0.15.1", + "cw2 0.15.1", + "cw20 0.15.1", + "itertools", + "protobuf", + "thiserror", +] + [[package]] name = "astroport-lockdrop" version = "1.2.0" dependencies = [ - "astroport", + "astroport 2.1.0", "astroport-periphery", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", + "cw-multi-test 0.13.4", "cw-storage-plus 0.15.1", "cw2 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)", "cw20 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -80,11 +111,72 @@ dependencies = [ "terraswap", ] +[[package]] +name = "astroport-oracle" +version = "2.0.0" +dependencies = [ + "anyhow", + "astroport 2.0.0", + "astroport-factory", + "astroport-pair", + "astroport-pair-stable", + "astroport-token", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test 0.15.1", + "cw-storage-plus 0.15.1", + "cw2 0.15.1", + "cw20 0.15.1", + "itertools", + "thiserror", +] + +[[package]] +name = "astroport-pair" +version = "1.0.0" +dependencies = [ + "astroport 2.0.0", + "astroport-factory", + "astroport-token", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test 0.15.1", + "cw-storage-plus 0.15.1", + "cw2 0.15.1", + "cw20 0.15.1", + "integer-sqrt", + "proptest", + "protobuf", + "thiserror", +] + +[[package]] +name = "astroport-pair-stable" +version = "2.0.0" +dependencies = [ + "anyhow", + "astroport 2.0.0", + "astroport-factory", + "astroport-token", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test 0.15.1", + "cw-storage-plus 0.15.1", + "cw2 0.15.1", + "cw20 0.15.1", + "derivative", + "itertools", + "proptest", + "protobuf", + "sim", + "thiserror", +] + [[package]] name = "astroport-periphery" version = "1.1.0" dependencies = [ - "astroport", + "astroport 2.1.0", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -94,6 +186,25 @@ dependencies = [ "terraswap", ] +[[package]] +name = "astroport-token" +version = "1.0.0" +dependencies = [ + "astroport 2.0.0", + "cosmwasm-schema", + "cosmwasm-std", + "cw2 0.15.1", + "cw20 0.15.1", + "cw20-base 0.15.1", + "snafu", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "base16ct" version = "0.1.1" @@ -112,6 +223,27 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "block-buffer" version = "0.9.0" @@ -142,6 +274,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + [[package]] name = "cfg-if" version = "1.0.0" @@ -246,7 +384,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", - "cw-multi-test", + "cw-multi-test 0.13.4", "cw-storage-plus 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)", "cw-utils 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)", "cw2 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -285,6 +423,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "curve25519-dalek" version = "3.2.0" @@ -317,6 +465,25 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cw-multi-test" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e81b4a7821d5eeba0d23f737c16027b39a600742ca8c32eb980895ffd270f4" +dependencies = [ + "anyhow", + "cosmwasm-std", + "cosmwasm-storage", + "cw-storage-plus 0.15.1", + "cw-utils 0.15.1", + "derivative", + "itertools", + "prost", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "cw-storage-plus" version = "0.13.4" @@ -490,6 +657,24 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cw20-base" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0909c56d0c14601fbdc69382189799482799dcad87587926aec1f3aa321abc41" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 0.15.1", + "cw-utils 0.15.1", + "cw2 0.15.1", + "cw20 0.15.1", + "schemars", + "semver", + "serde", + "thiserror", +] + [[package]] name = "der" version = "0.6.1" @@ -531,6 +716,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "dyn-clone" version = "1.0.11" @@ -590,6 +781,36 @@ dependencies = [ "zeroize", ] +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "ff" version = "0.12.1" @@ -600,6 +821,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "forward_ref" version = "1.0.0" @@ -627,6 +854,17 @@ dependencies = [ "wasi", ] +[[package]] +name = "ghost" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41973d4c45f7a35af8753ba3457cc99d406d863941fd7f52663cff54a5ab99b3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "group" version = "0.12.1" @@ -662,6 +900,79 @@ dependencies = [ "digest 0.10.6", ] +[[package]] +name = "indoc" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47741a8bc60fb26eb8d6e0238bbb26d8575ff623fdc97b1a2c00c050b9684ed8" +dependencies = [ + "indoc-impl", + "proc-macro-hack", +] + +[[package]] +name = "indoc-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce046d161f000fffde5f432a0d034d0341dc152643b2598ed5bfce44c4f3a8f0" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", + "unindent", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "integer-sqrt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +dependencies = [ + "num-traits", +] + +[[package]] +name = "inventory" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0eb5160c60ba1e809707918ee329adb99d222888155835c6feedba19f6c3fd4" +dependencies = [ + "ctor", + "ghost", + "inventory-impl", +] + +[[package]] +name = "inventory-impl" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e41b53715c6f0c4be49510bb82dee2c1e51c8586d885abe65396e82ed518548" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" +dependencies = [ + "libc", + "windows-sys 0.45.0", +] + [[package]] name = "itertools" version = "0.10.5" @@ -689,12 +1000,50 @@ dependencies = [ "sha2 0.10.6", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +[[package]] +name = "libm" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" + +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", + "libm", +] + [[package]] name = "once_cell" version = "1.17.1" @@ -707,6 +1056,50 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "paste" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880" +dependencies = [ + "paste-impl", + "proc-macro-hack", +] + +[[package]] +name = "paste-impl" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6" +dependencies = [ + "proc-macro-hack", +] + [[package]] name = "pkcs8" version = "0.9.0" @@ -717,6 +1110,18 @@ dependencies = [ "spki", ] +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.51" @@ -726,6 +1131,27 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29f1b898011ce9595050a68e60f90bad083ff2987a695a42357134c8381fba70" +dependencies = [ + "bit-set", + "bitflags", + "byteorder", + "lazy_static", + "num-traits", + "quick-error 2.0.1", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + [[package]] name = "prost" version = "0.9.0" @@ -758,6 +1184,56 @@ dependencies = [ "bytes", ] +[[package]] +name = "pyo3" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf6bbbe8f70d179260b3728e5d04eb012f4f0c7988e58c11433dd689cecaa72e" +dependencies = [ + "ctor", + "indoc", + "inventory", + "libc", + "parking_lot", + "paste", + "pyo3cls", + "unindent", +] + +[[package]] +name = "pyo3-derive-backend" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ecd0eb6ed7b3d9965b4f4370b5b9e99e3e5e8742000e1c452c018f8c2a322f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pyo3cls" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d344fdaa6a834a06dd1720ff104ea12fe101dad2e8db89345af9db74c0bb11a0" +dependencies = [ + "pyo3-derive-backend", + "quote", + "syn", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.23" @@ -767,6 +1243,27 @@ 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" @@ -782,6 +1279,30 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + [[package]] name = "rfc6979" version = "0.3.1" @@ -793,6 +1314,32 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustix" +version = "0.36.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.45.0", +] + +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error 1.2.3", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.12" @@ -823,6 +1370,12 @@ dependencies = [ "syn", ] +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "sec1" version = "0.3.0" @@ -928,6 +1481,41 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "sim" +version = "0.1.0" +source = "git+https://github.com/astroport-fi/astroport-sims.git?rev=6869900d4db9061ff8f0c1bfd7c7560056f7e74f#6869900d4db9061ff8f0c1bfd7c7560056f7e74f" +dependencies = [ + "pyo3", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "snafu" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab12d3c261b2308b0d80c26fffb58d17eba81a4be97890101f416b478c79ca7" +dependencies = [ + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1508efa03c362e23817f96cde18abed596a25219a8b2c66e8db33c03543d315b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "spki" version = "0.6.0" @@ -961,6 +1549,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.42.0", +] + [[package]] name = "terraswap" version = "2.8.0" @@ -1013,24 +1614,148 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" +[[package]] +name = "unindent" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c" + [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" + [[package]] name = "zeroize" version = "1.5.7" diff --git a/Cargo.toml b/Cargo.toml index e5d5f8f4..2bde33ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["contracts/*"] +members = ["contracts/auction", "contracts/lockdrop", "contracts/credits","contracts/astroport/*"] [profile.release] rpath = false diff --git a/contracts/astroport/README.md b/contracts/astroport/README.md new file mode 100644 index 00000000..da2e46e9 --- /dev/null +++ b/contracts/astroport/README.md @@ -0,0 +1,4 @@ +# Astroport + +This is [Astroport Core contracts](https://github.com/astroport-fi/astroport-core/tree/bedca9e7736d76f7a57a25fa4d1d850696cd03c5) modified for Neutron purposes. +Original repo commit hash: bedca9e7736d76f7a57a25fa4d1d850696cd03c5 diff --git a/contracts/astroport/factory/.cargo/config b/contracts/astroport/factory/.cargo/config new file mode 100644 index 00000000..23622ad6 --- /dev/null +++ b/contracts/astroport/factory/.cargo/config @@ -0,0 +1,6 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib" +integration-test = "test --test integration" +schema = "run --example factory_schema" diff --git a/contracts/astroport/factory/.editorconfig b/contracts/astroport/factory/.editorconfig new file mode 100644 index 00000000..3d36f20b --- /dev/null +++ b/contracts/astroport/factory/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.rs] +indent_size = 4 diff --git a/contracts/astroport/factory/Cargo.toml b/contracts/astroport/factory/Cargo.toml new file mode 100644 index 00000000..e454cc43 --- /dev/null +++ b/contracts/astroport/factory/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "astroport-factory" +version = "1.3.0" +authors = ["Astroport"] +edition = "2021" +description = "Astroport factory contract - pair contract generator and directory" +license = "MIT" + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] + +[dependencies] +cosmwasm-std = "1.1" +astroport = { path = "../../../packages/astroport", default-features = false } +cw-storage-plus = "0.15" +cw2 = "0.15" +thiserror = { version = "1.0" } +protobuf = { version = "2", features = ["with-bytes"] } +itertools = "0.10" +cosmwasm-schema = "1.1" + +[dev-dependencies] +cw-multi-test = "0.15" +astroport-token = {path = "../token" } +astroport-pair = {path = "../pair" } +cw20 = { version = "0.15" } +anyhow = "1.0" diff --git a/contracts/astroport/factory/README.md b/contracts/astroport/factory/README.md new file mode 100644 index 00000000..a2415eaa --- /dev/null +++ b/contracts/astroport/factory/README.md @@ -0,0 +1,268 @@ +# Astroport Factory + +The factory contract can create new Astroport pair contracts (and associated LP token contracts) and it is used as a directory for all pairs. The default pair types are constant product and stableswap but governance may decide to add custom pools that can have any implementation. + +--- + +## InstantiateMsg + +The instantiation message takes in the token code ID for the token type supported on Astroport. It also takes in the `fee_address` that collects fees for governance, the contract `owner`, the Generator contract address and the initial pair types available to create. + +```json +{ + "token_code_id": 123, + "fee_address": "terra...", + "owner": "terra...", + "generator_address": "terra...", + "pair_configs": [{ + "code_id": 123, + "pair_type": { + "xyk": {} + }, + "total_fee_bps": 100, + "maker_fee_bps": 10, + "is_disabled": false + } + ] +} +``` + +## ExecuteMsg + +### `update_config` + +Updates contract variables, namely the code ID of the token implementation used in Astroport, the address that receives governance fees and the Generator contract address. + +```json +{ + "update_config": { + "token_code_id": 123, + "fee_address": "terra...", + "generator_address": "terra..." + } +} +``` + +### `update_pair_config` + +This function can be used to: + +- Update the code ID used to instantiate new pairs of a specific type +- Change the fee structure for a pair +- Disable the pair type so no other pairs can be instantiated + +Note that all fields are optional. + +The fee structure for a pair is set up as follows: + +- `total_fee_bps` is the total amount of fees (in bps) that are charged on each swap +- `maker_fee_bps` is the percentage of fees out of `total_fee_bps` that is sent to governance. 100% is 10,000 + +As an example, let's say a pool charged 30bps (`total_fee_bps` is 30) and we want 1/3r of the fees to go to governance. In this case, `maker_fee_bps` should be 3333 because 3333 / 10,000 * 30 / 100 = 0.1% + +```json +{ + "update_pair_config": { + "config": { + "code_id": 123, + "pair_type": { + "xyk": {} + }, + "total_fee_bps": 100, + "maker_fee_bps": 10, + "is_disabled": false + } + } +} +``` + +### `create_pair` + +Anyone can execute this function to create an Astroport pair. `CreatePair` creates both a `Pair` contract and a `LP(liquidity provider)` token contract. The account that instantiates the pair must specify the pair type they want as well as the assets for which the pool is created. + +Custom pool types may also need extra parameters which can be packed in `init_params`. + +```json +{ + "create_pair": { + "pair_type": { + "xyk": {} + }, + "asset_infos": [ + { + "token": { + "contract_addr": "terra..." + } + }, + { + "native_token": { + "denom": "uusd" + } + } + ], + "init_params": "" + } +} +``` + +### `deregister` + +Deregisters an already registered pair. This allows someone else to create a new pair (of any type) for the tokens that don't have a registered pair anymore. This is how pairs can be "upgraded". + +```json +{ + "deregister": { + "asset_infos": [ + { + "token": { + "contract_address": "terra..." + } + }, + { + "native_token": { + "denom": "uusd" + } + } + ] + } +} +``` + +### `propose_new_owner` + +Creates an offer to change the contract ownership. The validity period of the offer is set in the `expires_in` variable. After `expires_in` seconds pass, the proposal expires and cannot be accepted anymore. + +```json +{ + "propose_new_owner": { + "owner": "terra...", + "expires_in": 1234567 + } +} +``` + +### `drop_ownership_proposal` + +Removes an existing offer to change the contract owner. + +```json +{ + "drop_ownership_proposal": {} +} +``` + +### `claim_ownership` + +Used to claim contract ownership. + +```json +{ + "claim_ownership": {} +} +``` + + +### `mark_as_migrated` + +Mark pairs as migrated. + +```json + { + "mark_as_migrated": { + "pairs": ["terra...", "terra..."] + } + } +``` + +## QueryMsg + +All query messages are described below. A custom struct is defined for each query response. + +### `config` + +Returns general factory parameters (owner, token code ID, pair type configurations). + +```json +{ + "config": {} +} +``` + +### `pair` + +Returns information about a specific pair. + +```json +{ + "pair": { + "asset_infos": [ + { + "token": { + "contract_address": "terra..." + } + }, + { + "native_token": { + "denom": "uusd" + } + } + ] + } +} +``` + +### `pairs` + +Returns information about multiple pairs (the result is paginated). The function starts returning pair information starting after the pair `start_after`. The function returns maximum `limit` pairs. + +```json +{ + "pairs": { + "start_after": [ + { + "token": { + "contract_address": "terra..." + } + }, + { + "native_token": { + "denom": "uusd" + } + } + ], + "limit": 10 + } +} +``` + +### `fee_info` + +Returns the fee information for a specific pair type (`total_fee_bps` and `maker_fee_bps`). + +```json +{ + "pair_type": { + "xyk": {} + } +} +``` + +### `blacklisted_pair_types` + +Returns a vector that contains blacklisted pair types. + +```json +{ + "blacklisted_pair_types": {} +} +``` + +### `pairs_to_migrate` + +Returns a vector that contains pair addresses that are not migrated. + +```json +{ + "pairs_to_migrate": {} +} +``` diff --git a/contracts/astroport/factory/examples/factory_schema.rs b/contracts/astroport/factory/examples/factory_schema.rs new file mode 100644 index 00000000..5da90aaf --- /dev/null +++ b/contracts/astroport/factory/examples/factory_schema.rs @@ -0,0 +1,24 @@ +use std::env::current_dir; +use std::fs::create_dir_all; + +use cosmwasm_schema::{export_schema_with_title, remove_schemas, schema_for}; + +use astroport::asset::PairInfo; +use astroport::factory::{ + ConfigResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, PairsResponse, QueryMsg, +}; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + export_schema_with_title(&schema_for!(InstantiateMsg), &out_dir, "InstantiateMsg"); + export_schema_with_title(&schema_for!(ExecuteMsg), &out_dir, "ExecuteMsg"); + export_schema_with_title(&schema_for!(QueryMsg), &out_dir, "QueryMsg"); + export_schema_with_title(&schema_for!(PairInfo), &out_dir, "PairInfo"); + export_schema_with_title(&schema_for!(PairsResponse), &out_dir, "PairsResponse"); + export_schema_with_title(&schema_for!(ConfigResponse), &out_dir, "ConfigResponse"); + export_schema_with_title(&schema_for!(MigrateMsg), &out_dir, "MigrateMsg"); +} diff --git a/contracts/astroport/factory/src/contract.rs b/contracts/astroport/factory/src/contract.rs new file mode 100644 index 00000000..e9e95e04 --- /dev/null +++ b/contracts/astroport/factory/src/contract.rs @@ -0,0 +1,619 @@ +use cosmwasm_std::{ + attr, entry_point, from_binary, to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Env, + MessageInfo, Order, Reply, ReplyOn, Response, StdError, StdResult, SubMsg, WasmMsg, +}; + +use crate::error::ContractError; +use crate::migration; +use crate::querier::query_pair_info; + +use crate::state::{ + check_asset_infos, pair_key, read_pairs, Config, TmpPairInfo, CONFIG, OWNERSHIP_PROPOSAL, + PAIRS, PAIRS_TO_MIGRATE, PAIR_CONFIGS, TMP_PAIR_INFO, +}; + +use crate::response::MsgInstantiateContractResponse; + +use astroport::asset::{addr_opt_validate, addr_validate_to_lower, AssetInfo, PairInfo}; +use astroport::factory::{ + ConfigResponse, ExecuteMsg, FeeInfoResponse, InstantiateMsg, MigrateMsg, PairConfig, PairType, + PairsResponse, QueryMsg, ROUTE, +}; + +use crate::migration::{migrate_pair_configs_to_v120, save_routes}; +use astroport::common::{ + claim_ownership, drop_ownership_proposal, propose_new_owner, validate_addresses, +}; +use astroport::generator::ExecuteMsg::DeactivatePool; +use astroport::pair::InstantiateMsg as PairInstantiateMsg; +use cw2::{get_contract_version, set_contract_version}; +use itertools::Itertools; +use protobuf::Message; +use std::collections::HashSet; + +/// Contract name that is used for migration. +const CONTRACT_NAME: &str = "astroport-factory"; +/// Contract version that is used for migration. +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +/// A `reply` call code ID used in a sub-message. +const INSTANTIATE_PAIR_REPLY_ID: u64 = 1; + +/// Creates a new contract with the specified parameters packed in the `msg` variable. +/// +/// * **msg** is message which contains the parameters used for creating the contract. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + let mut config = Config { + owner: addr_validate_to_lower(deps.api, &msg.owner)?, + token_code_id: msg.token_code_id, + fee_address: None, + generator_address: None, + whitelist_code_id: msg.whitelist_code_id, + }; + + config.generator_address = addr_opt_validate(deps.api, &msg.generator_address)?; + + config.fee_address = addr_opt_validate(deps.api, &msg.fee_address)?; + + let config_set: HashSet = msg + .pair_configs + .iter() + .map(|pc| pc.pair_type.to_string()) + .collect(); + + if config_set.len() != msg.pair_configs.len() { + return Err(ContractError::PairConfigDuplicate {}); + } + + for pc in msg.pair_configs.iter() { + // Validate total and maker fee bps + if !pc.valid_fee_bps() { + return Err(ContractError::PairConfigInvalidFeeBps {}); + } + PAIR_CONFIGS.save(deps.storage, pc.pair_type.to_string(), pc)?; + } + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new()) +} + +/// Data structure used to update general contract parameters. +pub struct UpdateConfig { + /// This is the CW20 token contract code identifier + token_code_id: Option, + /// Contract address to send governance fees to (the Maker) + fee_address: Option, + /// Generator contract address + generator_address: Option, + /// CW1 whitelist contract code id used to store 3rd party staking rewards + whitelist_code_id: Option, +} + +/// Exposes all the execute functions available in the contract. +/// * **msg** is an object of type [`ExecuteMsg`]. +/// +/// ## Variants +/// * **ExecuteMsg::UpdateConfig { +/// token_code_id, +/// fee_address, +/// generator_address, +/// }** Updates general contract parameters. +/// +/// * **ExecuteMsg::UpdatePairConfig { config }** Updates a pair type +/// * configuration or creates a new pair type if a [`Custom`] name is used (which hasn't been used before). +/// +/// * **ExecuteMsg::CreatePair { +/// pair_type, +/// asset_infos, +/// init_params, +/// }** Creates a new pair with the specified input parameters. +/// +/// * **ExecuteMsg::Deregister { asset_infos }** Removes an existing pair from the factory. +/// * The asset information is for the assets that are traded in the pair. +/// +/// * **ExecuteMsg::ProposeNewOwner { owner, expires_in }** Creates a request to change contract ownership. +/// +/// * **ExecuteMsg::DropOwnershipProposal {}** Removes a request to change contract ownership. +/// +/// * **ExecuteMsg::ClaimOwnership {}** Claims contract ownership. +/// +/// * **ExecuteMsg::MarkAsMigrated {}** Mark pairs as migrated. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::UpdateConfig { + token_code_id, + fee_address, + generator_address, + whitelist_code_id, + } => execute_update_config( + deps, + info, + UpdateConfig { + token_code_id, + fee_address, + generator_address, + whitelist_code_id, + }, + ), + ExecuteMsg::UpdatePairConfig { config } => execute_update_pair_config(deps, info, config), + ExecuteMsg::CreatePair { + pair_type, + asset_infos, + init_params, + } => execute_create_pair(deps, env, pair_type, asset_infos, init_params), + ExecuteMsg::Deregister { asset_infos } => deregister(deps, info, asset_infos), + ExecuteMsg::ProposeNewOwner { owner, expires_in } => { + let config = CONFIG.load(deps.storage)?; + + propose_new_owner( + deps, + info, + env, + owner, + expires_in, + config.owner, + OWNERSHIP_PROPOSAL, + ) + .map_err(Into::into) + } + ExecuteMsg::DropOwnershipProposal {} => { + let config = CONFIG.load(deps.storage)?; + + drop_ownership_proposal(deps, info, config.owner, OWNERSHIP_PROPOSAL) + .map_err(Into::into) + } + ExecuteMsg::ClaimOwnership {} => { + let pairs = PAIRS + .range(deps.storage, None, None, Order::Ascending) + .map(|pair| -> StdResult { Ok(pair?.1) }) + .collect::>>()?; + + PAIRS_TO_MIGRATE.save(deps.storage, &pairs)?; + + claim_ownership(deps, info, env, OWNERSHIP_PROPOSAL, |deps, new_owner| { + CONFIG + .update::<_, StdError>(deps.storage, |mut v| { + v.owner = new_owner; + Ok(v) + }) + .map(|_| ()) + }) + .map_err(Into::into) + } + ExecuteMsg::MarkAsMigrated { pairs } => execute_mark_pairs_as_migrated(deps, info, pairs), + } +} + +/// Updates general contract settings. +/// +/// * **param** is an object of type [`UpdateConfig`] that contains the parameters to update. +/// +/// ## Executor +/// Only the owner can execute this. +pub fn execute_update_config( + deps: DepsMut, + info: MessageInfo, + param: UpdateConfig, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + + // Permission check + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + if let Some(fee_address) = param.fee_address { + // Validate address format + config.fee_address = Some(addr_validate_to_lower(deps.api, &fee_address)?); + } + + if let Some(generator_address) = param.generator_address { + // Validate the address format + config.generator_address = Some(addr_validate_to_lower(deps.api, &generator_address)?); + } + + if let Some(token_code_id) = param.token_code_id { + config.token_code_id = token_code_id; + } + + if let Some(code_id) = param.whitelist_code_id { + config.whitelist_code_id = code_id; + } + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new().add_attribute("action", "update_config")) +} + +/// Updates a pair type's configuration. +/// +/// * **pair_config** is an object of type [`PairConfig`] that contains the pair type information to update. +/// +/// ## Executor +/// Only the owner can execute this. +pub fn execute_update_pair_config( + deps: DepsMut, + info: MessageInfo, + pair_config: PairConfig, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + // Permission check + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + // Validate total and maker fee bps + if !pair_config.valid_fee_bps() { + return Err(ContractError::PairConfigInvalidFeeBps {}); + } + + PAIR_CONFIGS.save( + deps.storage, + pair_config.pair_type.to_string(), + &pair_config, + )?; + + Ok(Response::new().add_attribute("action", "update_pair_config")) +} + +/// Creates a new pair of `pair_type` with the assets specified in `asset_infos`. +/// +/// * **pair_type** is the pair type of the newly created pair. +/// +/// * **asset_infos** is a vector with assets for which we create a pair. +/// +/// * **init_params** These are packed params used for custom pair types that need extra data to be instantiated. +pub fn execute_create_pair( + deps: DepsMut, + env: Env, + pair_type: PairType, + asset_infos: Vec, + init_params: Option, +) -> Result { + check_asset_infos(deps.api, &asset_infos)?; + + let config = CONFIG.load(deps.storage)?; + + if PAIRS.has(deps.storage, &pair_key(&asset_infos)) { + return Err(ContractError::PairWasCreated {}); + } + + // Get pair type from config + let pair_config = PAIR_CONFIGS + .load(deps.storage, pair_type.to_string()) + .map_err(|_| ContractError::PairConfigNotFound {})?; + + // Check if pair config is disabled + if pair_config.is_disabled { + return Err(ContractError::PairConfigDisabled {}); + } + + let pair_key = pair_key(&asset_infos); + TMP_PAIR_INFO.save( + deps.storage, + &TmpPairInfo { + pair_key, + asset_infos: asset_infos.clone(), + }, + )?; + + let sub_msg: Vec = vec![SubMsg { + id: INSTANTIATE_PAIR_REPLY_ID, + msg: WasmMsg::Instantiate { + admin: Some(config.owner.to_string()), + code_id: pair_config.code_id, + msg: to_binary(&PairInstantiateMsg { + asset_infos: asset_infos.clone(), + token_code_id: config.token_code_id, + factory_addr: env.contract.address.to_string(), + init_params, + })?, + funds: vec![], + label: "Astroport pair".to_string(), + } + .into(), + gas_limit: None, + reply_on: ReplyOn::Success, + }]; + + Ok(Response::new() + .add_submessages(sub_msg) + .add_attributes(vec![ + attr("action", "create_pair"), + attr("pair", asset_infos.iter().join("-")), + ])) +} + +/// Marks specified pairs as migrated to the new admin. +/// +/// * **pairs** is a vector of pairs which should be marked as transferred. +fn execute_mark_pairs_as_migrated( + deps: DepsMut, + info: MessageInfo, + pairs: Vec, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + let pairs = validate_addresses(deps.api, &pairs)?; + + let not_migrated: Vec = PAIRS_TO_MIGRATE + .load(deps.storage)? + .into_iter() + .filter(|addr| !pairs.contains(addr)) + .collect(); + + PAIRS_TO_MIGRATE.save(deps.storage, ¬_migrated)?; + + Ok(Response::new().add_attribute("action", "execute_mark_pairs_as_migrated")) +} + +/// The entry point to the contract for processing replies from submessages. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { + let tmp = TMP_PAIR_INFO.load(deps.storage)?; + if PAIRS.has(deps.storage, &tmp.pair_key) { + return Err(ContractError::PairWasRegistered {}); + } + + let data = msg.result.unwrap().data.unwrap(); + let res: MsgInstantiateContractResponse = + Message::parse_from_bytes(data.as_slice()).map_err(|_| { + StdError::parse_err("MsgInstantiateContractResponse", "failed to parse data") + })?; + + let pair_contract = addr_validate_to_lower(deps.api, res.get_contract_address())?; + + PAIRS.save(deps.storage, &tmp.pair_key, &pair_contract)?; + + for asset_info in &tmp.asset_infos { + for asset_info_2 in &tmp.asset_infos { + if asset_info != asset_info_2 { + ROUTE.update::<_, StdError>( + deps.storage, + (asset_info.to_string(), asset_info_2.to_string()), + |maybe_contracts| { + if let Some(mut contracts) = maybe_contracts { + contracts.push(pair_contract.clone()); + Ok(contracts) + } else { + Ok(vec![pair_contract.clone()]) + } + }, + )?; + } + } + } + + Ok(Response::new().add_attributes(vec![ + attr("action", "register"), + attr("pair_contract_addr", pair_contract), + ])) +} + +/// Removes an existing pair from the factory. +/// +/// * **asset_infos** is a vector with assets for which we deregister the pair. +/// +/// ## Executor +/// Only the owner can execute this. +pub fn deregister( + deps: DepsMut, + info: MessageInfo, + asset_infos: Vec, +) -> Result { + asset_infos[0].check(deps.api)?; + asset_infos[1].check(deps.api)?; + + let config = CONFIG.load(deps.storage)?; + + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + let pair_addr = PAIRS.load(deps.storage, &pair_key(&asset_infos))?; + PAIRS.remove(deps.storage, &pair_key(&asset_infos)); + + for asset_info1 in &asset_infos { + for asset_info2 in &asset_infos { + if asset_info1 != asset_info2 { + ROUTE.update::<_, StdError>( + deps.storage, + (asset_info1.to_string(), asset_info2.to_string()), + |pairs| { + Ok(pairs + .unwrap_or_default() + .iter() + .cloned() + .filter(|pair| pair != &pair_addr) + .collect::>()) + }, + )?; + } + } + } + + let mut response = Response::new(); + if let Some(generator) = config.generator_address { + let pair_info = query_pair_info(&deps.querier, &pair_addr)?; + + // sets the allocation point to zero for the lp_token + response = response.add_message(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: generator.to_string(), + msg: to_binary(&DeactivatePool { + lp_token: pair_info.liquidity_token.to_string(), + })?, + funds: vec![], + })); + } + + Ok(response.add_attributes(vec![ + attr("action", "deregister"), + attr("pair_contract_addr", pair_addr), + ])) +} + +/// Exposes all the queries available in the contract. +/// +/// ## Queries +/// * **QueryMsg::Config {}** Returns general contract parameters using a custom [`ConfigResponse`] structure. +/// +/// * **QueryMsg::Pair { asset_infos }** Returns a [`PairInfo`] object with information about a specific Astroport pair. +/// +/// * **QueryMsg::Pairs { start_after, limit }** Returns an array that contains items of type [`PairInfo`]. +/// This returns information about multiple Astroport pairs +/// +/// * **QueryMsg::FeeInfo { pair_type }** Returns the fee structure (total and maker fees) for a specific pair type. +/// +/// * **QueryMsg::BlacklistedPairTypes {}** Returns a vector that contains blacklisted pair types (pair types that cannot get ASTRO emissions). +/// +/// * **QueryMsg::PairsToMigrate {}** Returns a vector that contains pair addresses that are not migrated. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Config {} => to_binary(&query_config(deps)?), + QueryMsg::Pair { asset_infos } => to_binary(&query_pair(deps, asset_infos)?), + QueryMsg::Pairs { start_after, limit } => { + to_binary(&query_pairs(deps, start_after, limit)?) + } + QueryMsg::FeeInfo { pair_type } => to_binary(&query_fee_info(deps, pair_type)?), + QueryMsg::BlacklistedPairTypes {} => to_binary(&query_blacklisted_pair_types(deps)?), + QueryMsg::PairsToMigrate {} => { + to_binary(&PAIRS_TO_MIGRATE.may_load(deps.storage)?.unwrap_or_default()) + } + } +} + +/// Returns a vector that contains blacklisted pair types +pub fn query_blacklisted_pair_types(deps: Deps) -> StdResult> { + PAIR_CONFIGS + .range(deps.storage, None, None, Order::Ascending) + .filter_map(|result| match result { + Ok(v) => { + if v.1.is_disabled || v.1.is_generator_disabled { + Some(Ok(v.1.pair_type)) + } else { + None + } + } + Err(e) => Some(Err(e)), + }) + .collect() +} + +/// Returns general contract parameters using a custom [`ConfigResponse`] structure. +pub fn query_config(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let resp = ConfigResponse { + owner: config.owner, + token_code_id: config.token_code_id, + pair_configs: PAIR_CONFIGS + .range(deps.storage, None, None, Order::Ascending) + .map(|item| Ok(item?.1)) + .collect::>>()?, + fee_address: config.fee_address, + generator_address: config.generator_address, + whitelist_code_id: config.whitelist_code_id, + }; + + Ok(resp) +} + +/// Returns a pair's data using the assets in `asset_infos` as input (those being the assets that are traded in the pair). +/// * **asset_infos** is a vector with assets traded in the pair. +pub fn query_pair(deps: Deps, asset_infos: Vec) -> StdResult { + let pair_addr = PAIRS.load(deps.storage, &pair_key(&asset_infos))?; + query_pair_info(&deps.querier, &pair_addr) +} + +/// Returns a vector with pair data that contains items of type [`PairInfo`]. Querying starts at `start_after` and returns `limit` pairs. +/// * **start_after** is a field which accepts a vector with items of type [`AssetInfo`]. +/// This is the pair from which we start a query. +/// +/// * **limit** sets the number of pairs to be retrieved. +pub fn query_pairs( + deps: Deps, + start_after: Option>, + limit: Option, +) -> StdResult { + let pairs = read_pairs(deps, start_after, limit)? + .iter() + .map(|pair_addr| query_pair_info(&deps.querier, pair_addr)) + .collect::>>()?; + + Ok(PairsResponse { pairs }) +} + +/// Returns the fee setup for a specific pair type using a [`FeeInfoResponse`] struct. +/// * **pair_type** is a struct that represents the fee information (total and maker fees) for a specific pair type. +pub fn query_fee_info(deps: Deps, pair_type: PairType) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let pair_config = PAIR_CONFIGS.load(deps.storage, pair_type.to_string())?; + + Ok(FeeInfoResponse { + fee_address: config.fee_address, + total_fee_bps: pair_config.total_fee_bps, + maker_fee_bps: pair_config.maker_fee_bps, + }) +} + +/// Manages the contract migration. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(mut deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { + let contract_version = get_contract_version(deps.storage)?; + + match contract_version.contract.as_ref() { + "astroport-factory" => match contract_version.version.as_ref() { + "1.0.0" | "1.0.0-fix1" => { + let msg: migration::MigrationMsgV100 = from_binary(&msg.params)?; + + let config_v100 = migration::CONFIGV100.load(deps.storage)?; + + let new_config = Config { + whitelist_code_id: msg.whitelist_code_id, + fee_address: config_v100.fee_address, + generator_address: config_v100.generator_address, + owner: config_v100.owner, + token_code_id: config_v100.token_code_id, + }; + + CONFIG.save(deps.storage, &new_config)?; + + migrate_pair_configs_to_v120(deps.storage)?; + save_routes(deps.branch())?; + } + "1.1.0" => { + migrate_pair_configs_to_v120(deps.storage)?; + save_routes(deps.branch())?; + } + "1.2.0" => save_routes(deps.branch())?, + _ => return Err(ContractError::MigrationError {}), + }, + _ => return Err(ContractError::MigrationError {}), + } + + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Response::new() + .add_attribute("previous_contract_name", &contract_version.contract) + .add_attribute("previous_contract_version", &contract_version.version) + .add_attribute("new_contract_name", CONTRACT_NAME) + .add_attribute("new_contract_version", CONTRACT_VERSION)) +} diff --git a/contracts/astroport/factory/src/error.rs b/contracts/astroport/factory/src/error.rs new file mode 100644 index 00000000..4cda4849 --- /dev/null +++ b/contracts/astroport/factory/src/error.rs @@ -0,0 +1,36 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +/// This enum describes factory contract errors +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Pair was already created")] + PairWasCreated {}, + + #[error("Pair was already registered")] + PairWasRegistered {}, + + #[error("Duplicate of pair configs")] + PairConfigDuplicate {}, + + #[error("Fee bps in pair config must be smaller than or equal to 10,000")] + PairConfigInvalidFeeBps {}, + + #[error("Pair config not found")] + PairConfigNotFound {}, + + #[error("Pair config disabled")] + PairConfigDisabled {}, + + #[error("Doubling assets in asset infos")] + DoublingAssets {}, + + #[error("Contract can't be migrated!")] + MigrationError {}, +} diff --git a/contracts/astroport/factory/src/lib.rs b/contracts/astroport/factory/src/lib.rs new file mode 100644 index 00000000..374400fc --- /dev/null +++ b/contracts/astroport/factory/src/lib.rs @@ -0,0 +1,15 @@ +pub mod contract; +mod migration; +pub mod state; + +pub mod error; + +mod querier; + +mod response; + +#[cfg(test)] +mod testing; + +#[cfg(test)] +mod mock_querier; diff --git a/contracts/astroport/factory/src/migration.rs b/contracts/astroport/factory/src/migration.rs new file mode 100644 index 00000000..fd422bff --- /dev/null +++ b/contracts/astroport/factory/src/migration.rs @@ -0,0 +1,97 @@ +use crate::querier::query_pair_info; +use crate::state::{PAIRS, PAIR_CONFIGS}; +use astroport::factory::{PairConfig, PairType, ROUTE}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, DepsMut, Order, StdError, StdResult, Storage}; +use cw_storage_plus::{Item, Map}; + +/// This structure describes a contract migration message. +#[cw_serde] +pub struct MigrationMsgV100 { + /// CW1 whitelist contract code ID used to store 3rd party staking rewards + pub whitelist_code_id: u64, +} + +/// This structure holds the main parameters for the factory contract. +#[cw_serde] +pub struct ConfigV100 { + /// Address allowed to change contract parameters + pub owner: Addr, + /// CW20 token contract code identifier + pub token_code_id: u64, + /// Generator contract address + pub generator_address: Option, + /// Contract address to send governance fees to (the Maker contract) + pub fee_address: Option, +} + +pub const CONFIGV100: Item = Item::new("config"); + +/// This structure describes a pair's configuration. +#[cw_serde] +pub struct PairConfigV100 { + /// Pair contract code ID that's used to create new pairs of this type + pub code_id: u64, + /// The pair type (e.g XYK, stable) + pub pair_type: PairType, + /// The total amount of fees charged for the swap + pub total_fee_bps: u16, + /// The amount of fees that go to the Maker contract + pub maker_fee_bps: u16, + /// We disable pair configs instead of removing them. If a pair type is disabled, + /// new pairs cannot be created, but existing ones can still function properly + pub is_disabled: Option, +} + +pub const PAIR_CONFIGS_V100: Map = Map::new("pair_configs"); + +pub fn migrate_pair_configs_to_v120(storage: &mut dyn Storage) -> Result<(), StdError> { + let keys = PAIR_CONFIGS_V100 + .keys(storage, None, None, cosmwasm_std::Order::Ascending {}) + .collect::, StdError>>()?; + + for key in keys { + let pair_configs_v100 = PAIR_CONFIGS_V100.load(storage, key.clone())?; + let pair_config = PairConfig { + code_id: pair_configs_v100.code_id, + pair_type: pair_configs_v100.pair_type, + total_fee_bps: pair_configs_v100.total_fee_bps, + maker_fee_bps: pair_configs_v100.maker_fee_bps, + is_disabled: pair_configs_v100.is_disabled.unwrap_or(false), + is_generator_disabled: false, + }; + PAIR_CONFIGS.save(storage, key, &pair_config)?; + } + + Ok(()) +} + +/// Save pairs into routes +pub fn save_routes(deps: DepsMut) -> Result<(), StdError> { + let pairs = PAIRS + .range(deps.storage, None, None, Order::Ascending) + .map(|pair| -> StdResult { Ok(pair?.1) }) + .collect::>>()?; + + for pair in pairs { + let pair_info = query_pair_info(&deps.querier, &pair)?; + ROUTE.save( + deps.storage, + ( + pair_info.asset_infos[0].to_string(), + pair_info.asset_infos[1].to_string(), + ), + &vec![pair.clone()], + )?; + ROUTE.save( + deps.storage, + ( + pair_info.asset_infos[1].to_string(), + pair_info.asset_infos[0].to_string(), + ), + &vec![pair.clone()], + )?; + } + + Ok(()) +} diff --git a/contracts/astroport/factory/src/mock_querier.rs b/contracts/astroport/factory/src/mock_querier.rs new file mode 100644 index 00000000..5709f8a5 --- /dev/null +++ b/contracts/astroport/factory/src/mock_querier.rs @@ -0,0 +1,105 @@ +use astroport::asset::PairInfo; +use astroport::pair::QueryMsg; +use cosmwasm_std::testing::{MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR}; +use cosmwasm_std::{ + from_binary, from_slice, to_binary, Coin, Empty, OwnedDeps, Querier, QuerierResult, + QueryRequest, SystemError, SystemResult, WasmQuery, +}; +use std::collections::HashMap; + +/// mock_dependencies is a drop-in replacement for cosmwasm_std::testing::mock_dependencies. +/// This uses the Astroport CustomQuerier. +pub fn mock_dependencies( + contract_balance: &[Coin], +) -> OwnedDeps { + let custom_querier: WasmMockQuerier = + WasmMockQuerier::new(MockQuerier::new(&[(MOCK_CONTRACT_ADDR, contract_balance)])); + + OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: custom_querier, + custom_query_type: Default::default(), + } +} + +pub struct WasmMockQuerier { + base: MockQuerier, + astroport_pair_querier: AstroportPairQuerier, +} + +#[derive(Clone, Default)] +pub struct AstroportPairQuerier { + pairs: HashMap, +} + +impl AstroportPairQuerier { + pub fn new(pairs: &[(&String, &PairInfo)]) -> Self { + AstroportPairQuerier { + pairs: pairs_to_map(pairs), + } + } +} + +pub(crate) fn pairs_to_map(pairs: &[(&String, &PairInfo)]) -> HashMap { + let mut pairs_map: HashMap = HashMap::new(); + for (key, pair) in pairs.iter() { + pairs_map.insert(key.to_string(), (*pair).clone()); + } + pairs_map +} + +impl Querier for WasmMockQuerier { + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { + // MockQuerier doesn't support Custom, so we ignore it completely + let request: QueryRequest = match from_slice(bin_request) { + Ok(v) => v, + Err(e) => { + return SystemResult::Err(SystemError::InvalidRequest { + error: format!("Parsing query request: {}", e), + request: bin_request.into(), + }) + } + }; + self.handle_query(&request) + } +} + +impl WasmMockQuerier { + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { + match &request { + QueryRequest::Wasm(WasmQuery::Smart {contract_addr, msg})// => { + => match from_binary(msg).unwrap() { + QueryMsg::Pair {} => { + let pair_info: PairInfo = + match self.astroport_pair_querier.pairs.get(contract_addr) { + Some(v) => v.clone(), + None => { + return SystemResult::Err(SystemError::NoSuchContract { + addr: contract_addr.clone(), + }) + } + }; + + SystemResult::Ok(to_binary(&pair_info).into()) + } + _ => panic!("DO NOT ENTER HERE") + } + _ => self.base.handle_query(request), + } + } +} + +impl WasmMockQuerier { + pub fn new(base: MockQuerier) -> Self { + WasmMockQuerier { + base, + astroport_pair_querier: AstroportPairQuerier::default(), + } + } + + // Configure the Astroport pair + pub fn with_astroport_pairs(&mut self, pairs: &[(&String, &PairInfo)]) { + self.astroport_pair_querier = AstroportPairQuerier::new(pairs); + } +} diff --git a/contracts/astroport/factory/src/querier.rs b/contracts/astroport/factory/src/querier.rs new file mode 100644 index 00000000..6be30993 --- /dev/null +++ b/contracts/astroport/factory/src/querier.rs @@ -0,0 +1,13 @@ +use astroport::asset::PairInfo; +use astroport::pair::QueryMsg; +use cosmwasm_std::{QuerierWrapper, StdResult}; + +/// Returns information about a pair (using the [`PairInfo`] struct). +/// +/// `pair_contract` is the pair for which to retrieve information. +pub fn query_pair_info( + querier: &QuerierWrapper, + pair_contract: impl Into, +) -> StdResult { + querier.query_wasm_smart(pair_contract, &QueryMsg::Pair {}) +} diff --git a/contracts/astroport/factory/src/response.proto b/contracts/astroport/factory/src/response.proto new file mode 100644 index 00000000..9e0e76b5 --- /dev/null +++ b/contracts/astroport/factory/src/response.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +// MsgInstantiateContractResponse defines the Msg/InstantiateContract response type. +message MsgInstantiateContractResponse { + // ContractAddress is the bech32 address of the new contract instance. + string contract_address = 1; + // Data contains base64-encoded bytes to returned from the contract + bytes data = 2; +} diff --git a/contracts/astroport/factory/src/response.rs b/contracts/astroport/factory/src/response.rs new file mode 100644 index 00000000..97880957 --- /dev/null +++ b/contracts/astroport/factory/src/response.rs @@ -0,0 +1,255 @@ +// This file is generated by rust-protobuf 2.25.2. Do not edit +// @generated + +// https://github.com/rust-lang/rust-clippy/issues/702 +#![allow(unknown_lints)] +#![allow(clippy::all)] + +#![allow(unused_attributes)] +#![cfg_attr(rustfmt, rustfmt::skip)] + +#![allow(box_pointers)] +#![allow(dead_code)] +#![allow(missing_docs)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(trivial_casts)] +#![allow(unused_imports)] +#![allow(unused_results)] +//! Generated file from `src/response.proto` + +/// Generated files are compatible only with the same version +/// of protobuf runtime. +// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_25_2; + +#[derive(PartialEq,Clone,Default)] +pub struct MsgInstantiateContractResponse { + // message fields + pub contract_address: ::std::string::String, + pub data: ::std::vec::Vec, + // special fields + pub unknown_fields: ::protobuf::UnknownFields, + pub cached_size: ::protobuf::CachedSize, +} + +impl<'a> ::std::default::Default for &'a MsgInstantiateContractResponse { + fn default() -> &'a MsgInstantiateContractResponse { + ::default_instance() + } +} + +impl MsgInstantiateContractResponse { + pub fn new() -> MsgInstantiateContractResponse { + ::std::default::Default::default() + } + + // string contract_address = 1; + + + pub fn get_contract_address(&self) -> &str { + &self.contract_address + } + pub fn clear_contract_address(&mut self) { + self.contract_address.clear(); + } + + // Param is passed by value, moved + pub fn set_contract_address(&mut self, v: ::std::string::String) { + self.contract_address = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_contract_address(&mut self) -> &mut ::std::string::String { + &mut self.contract_address + } + + // Take field + pub fn take_contract_address(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.contract_address, ::std::string::String::new()) + } + + // bytes data = 2; + + + pub fn get_data(&self) -> &[u8] { + &self.data + } + pub fn clear_data(&mut self) { + self.data.clear(); + } + + // Param is passed by value, moved + pub fn set_data(&mut self, v: ::std::vec::Vec) { + self.data = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_data(&mut self) -> &mut ::std::vec::Vec { + &mut self.data + } + + // Take field + pub fn take_data(&mut self) -> ::std::vec::Vec { + ::std::mem::replace(&mut self.data, ::std::vec::Vec::new()) + } +} + +impl ::protobuf::Message for MsgInstantiateContractResponse { + fn is_initialized(&self) -> bool { + true + } + + fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> { + while !is.eof()? { + let (field_number, wire_type) = is.read_tag_unpack()?; + match field_number { + 1 => { + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.contract_address)?; + }, + 2 => { + ::protobuf::rt::read_singular_proto3_bytes_into(wire_type, is, &mut self.data)?; + }, + _ => { + ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?; + }, + }; + } + ::std::result::Result::Ok(()) + } + + // Compute sizes of nested messages + #[allow(unused_variables)] + fn compute_size(&self) -> u32 { + let mut my_size = 0; + if !self.contract_address.is_empty() { + my_size += ::protobuf::rt::string_size(1, &self.contract_address); + } + if !self.data.is_empty() { + my_size += ::protobuf::rt::bytes_size(2, &self.data); + } + my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields()); + self.cached_size.set(my_size); + my_size + } + + fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> { + if !self.contract_address.is_empty() { + os.write_string(1, &self.contract_address)?; + } + if !self.data.is_empty() { + os.write_bytes(2, &self.data)?; + } + os.write_unknown_fields(self.get_unknown_fields())?; + ::std::result::Result::Ok(()) + } + + fn get_cached_size(&self) -> u32 { + self.cached_size.get() + } + + fn get_unknown_fields(&self) -> &::protobuf::UnknownFields { + &self.unknown_fields + } + + fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields { + &mut self.unknown_fields + } + + fn as_any(&self) -> &dyn (::std::any::Any) { + self as &dyn (::std::any::Any) + } + fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) { + self as &mut dyn (::std::any::Any) + } + fn into_any(self: ::std::boxed::Box) -> ::std::boxed::Box { + self + } + + fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor { + Self::descriptor_static() + } + + fn new() -> MsgInstantiateContractResponse { + MsgInstantiateContractResponse::new() + } + + fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor { + static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT; + descriptor.get(|| { + let mut fields = ::std::vec::Vec::new(); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( + "contract_address", + |m: &MsgInstantiateContractResponse| { &m.contract_address }, + |m: &mut MsgInstantiateContractResponse| { &mut m.contract_address }, + )); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeBytes>( + "data", + |m: &MsgInstantiateContractResponse| { &m.data }, + |m: &mut MsgInstantiateContractResponse| { &mut m.data }, + )); + ::protobuf::reflect::MessageDescriptor::new_pb_name::( + "MsgInstantiateContractResponse", + fields, + file_descriptor_proto() + ) + }) + } + + fn default_instance() -> &'static MsgInstantiateContractResponse { + static instance: ::protobuf::rt::LazyV2 = ::protobuf::rt::LazyV2::INIT; + instance.get(MsgInstantiateContractResponse::new) + } +} + +impl ::protobuf::Clear for MsgInstantiateContractResponse { + fn clear(&mut self) { + self.contract_address.clear(); + self.data.clear(); + self.unknown_fields.clear(); + } +} + +impl ::std::fmt::Debug for MsgInstantiateContractResponse { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + ::protobuf::text_format::fmt(self, f) + } +} + +impl ::protobuf::reflect::ProtobufValue for MsgInstantiateContractResponse { + fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef { + ::protobuf::reflect::ReflectValueRef::Message(self) + } +} + +static file_descriptor_proto_data: &'static [u8] = b"\ + \n\x12src/response.proto\"_\n\x1eMsgInstantiateContractResponse\x12)\n\ + \x10contract_address\x18\x01\x20\x01(\tR\x0fcontractAddress\x12\x12\n\ + \x04data\x18\x02\x20\x01(\x0cR\x04dataJ\xf8\x02\n\x06\x12\x04\0\0\x08\ + \x01\n\x08\n\x01\x0c\x12\x03\0\0\x12\n_\n\x02\x04\0\x12\x04\x03\0\x08\ + \x01\x1aS\x20MsgInstantiateContractResponse\x20defines\x20the\x20Msg/Ins\ + tantiateContract\x20response\x20type.\n\n\n\n\x03\x04\0\x01\x12\x03\x03\ + \x08&\nR\n\x04\x04\0\x02\0\x12\x03\x05\x02\x1e\x1aE\x20ContractAddress\ + \x20is\x20the\x20bech32\x20address\x20of\x20the\x20new\x20contract\x20in\ + stance.\n\n\x0c\n\x05\x04\0\x02\0\x05\x12\x03\x05\x02\x08\n\x0c\n\x05\ + \x04\0\x02\0\x01\x12\x03\x05\t\x19\n\x0c\n\x05\x04\0\x02\0\x03\x12\x03\ + \x05\x1c\x1d\nO\n\x04\x04\0\x02\x01\x12\x03\x07\x02\x11\x1aB\x20Data\x20\ + contains\x20base64-encoded\x20bytes\x20to\x20returned\x20from\x20the\x20\ + contract\n\n\x0c\n\x05\x04\0\x02\x01\x05\x12\x03\x07\x02\x07\n\x0c\n\x05\ + \x04\0\x02\x01\x01\x12\x03\x07\x08\x0c\n\x0c\n\x05\x04\0\x02\x01\x03\x12\ + \x03\x07\x0f\x10b\x06proto3\ +"; + +static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT; + +fn parse_descriptor_proto() -> ::protobuf::descriptor::FileDescriptorProto { + ::protobuf::Message::parse_from_bytes(file_descriptor_proto_data).unwrap() +} + +pub fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto { + file_descriptor_proto_lazy.get(|| { + parse_descriptor_proto() + }) +} diff --git a/contracts/astroport/factory/src/state.rs b/contracts/astroport/factory/src/state.rs new file mode 100644 index 00000000..cd21fbe4 --- /dev/null +++ b/contracts/astroport/factory/src/state.rs @@ -0,0 +1,195 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Api, Deps, Order, StdResult}; +use cw_storage_plus::{Bound, Item, Map}; +use itertools::Itertools; + +use crate::error::ContractError; +use astroport::asset::AssetInfo; +use astroport::common::OwnershipProposal; +use astroport::factory::PairConfig; + +/// This structure holds the main contract parameters. +#[cw_serde] +pub struct Config { + /// Address allowed to change contract parameters + pub owner: Addr, + /// CW20 token contract code identifier + pub token_code_id: u64, + /// Generator contract address + pub generator_address: Option, + /// Contract address to send governance fees to (the Maker contract) + pub fee_address: Option, + /// CW1 whitelist contract code id used to store 3rd party generator staking rewards + pub whitelist_code_id: u64, +} + +/// This is an intermediate structure for storing a pair's key. It is used in a submessage response. +#[cw_serde] +pub struct TmpPairInfo { + pub pair_key: Vec, + pub asset_infos: Vec, +} + +/// Saves a pair's key +pub const TMP_PAIR_INFO: Item = Item::new("tmp_pair_info"); + +/// Saves factory settings +pub const CONFIG: Item = Item::new("config"); + +/// Saves created pairs (from olders to latest) +pub const PAIRS: Map<&[u8], Addr> = Map::new("pair_info"); + +/// Calculates a pair key from the specified parameters in the `asset_infos` variable. +/// +/// `asset_infos` is an array with multiple items of type [`AssetInfo`]. +pub fn pair_key(asset_infos: &[AssetInfo]) -> Vec { + asset_infos + .iter() + .map(AssetInfo::as_bytes) + .sorted() + .flatten() + .copied() + .collect() +} + +/// Saves pair type configurations +pub const PAIR_CONFIGS: Map = Map::new("pair_configs"); + +/// ## Pagination settings +/// The maximum limit for reading pairs from [`PAIRS`] +const MAX_LIMIT: u32 = 30; +/// The default limit for reading pairs from [`PAIRS`] +const DEFAULT_LIMIT: u32 = 10; + +/// Reads pairs from the [`PAIRS`] vector according to the `start_after` and `limit` variables. +/// Otherwise, it returns the default number of pairs, starting from the oldest one. +/// +/// `start_after` is the pair from which the function starts to fetch results. +/// +/// `limit` is the number of items to retrieve. +pub fn read_pairs( + deps: Deps, + start_after: Option>, + limit: Option, +) -> StdResult> { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + + if let Some(start) = calc_range_start(start_after) { + PAIRS + .range( + deps.storage, + Some(Bound::exclusive(start.as_slice())), + None, + Order::Ascending, + ) + .take(limit) + .map(|item| { + let (_, pair_addr) = item?; + Ok(pair_addr) + }) + .collect() + } else { + PAIRS + .range(deps.storage, None, None, Order::Ascending) + .take(limit) + .map(|item| { + let (_, pair_addr) = item?; + Ok(pair_addr) + }) + .collect() + } +} + +/// Calculates the key of a pair from which to start reading data. +/// +/// `start_after` is an [`Option`] type that accepts [`AssetInfo`] elements. +/// It is the token pair which we use to determine the start index for a range when returning data for multiple pairs +fn calc_range_start(start_after: Option>) -> Option> { + start_after.map(|ref asset| { + let mut key = pair_key(asset); + key.push(1); + key + }) +} + +pub(crate) fn check_asset_infos( + api: &dyn Api, + asset_infos: &[AssetInfo], +) -> Result<(), ContractError> { + if !asset_infos.iter().all_unique() { + return Err(ContractError::DoublingAssets {}); + } + + asset_infos + .iter() + .try_for_each(|asset_info| asset_info.check(api)) + .map_err(Into::into) +} + +/// Stores the latest contract ownership transfer proposal +pub const OWNERSHIP_PROPOSAL: Item = Item::new("ownership_proposal"); + +/// Stores pairs to migrate +pub const PAIRS_TO_MIGRATE: Item> = Item::new("pairs_to_migrate"); + +#[cfg(test)] +mod tests { + use astroport::asset::{native_asset_info, token_asset_info}; + + use super::*; + + fn get_test_case() -> Vec<[AssetInfo; 2]> { + vec![ + [ + native_asset_info("uluna".to_string()), + native_asset_info("uusd".to_string()), + ], + [ + native_asset_info("uluna".to_string()), + token_asset_info(Addr::unchecked("astro_token_addr")), + ], + [ + token_asset_info(Addr::unchecked("random_token_addr")), + token_asset_info(Addr::unchecked("astro_token_addr")), + ], + ] + } + + #[test] + fn test_legacy_pair_key() { + fn legacy_pair_key(asset_infos: &[AssetInfo; 2]) -> Vec { + let mut asset_infos = asset_infos.to_vec(); + asset_infos.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes())); + + [asset_infos[0].as_bytes(), asset_infos[1].as_bytes()].concat() + } + + for asset_infos in get_test_case() { + assert_eq!(legacy_pair_key(&asset_infos), pair_key(&asset_infos)); + } + } + + #[test] + fn test_legacy_start_after() { + fn legacy_calc_range_start(start_after: Option<[AssetInfo; 2]>) -> Option> { + start_after.map(|asset_infos| { + let mut asset_infos = asset_infos.to_vec(); + asset_infos.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes())); + + let mut v = [asset_infos[0].as_bytes(), asset_infos[1].as_bytes()] + .concat() + .as_slice() + .to_vec(); + v.push(1); + v + }) + } + + for asset_infos in get_test_case() { + assert_eq!( + legacy_calc_range_start(Some(asset_infos.clone())), + calc_range_start(Some(asset_infos.to_vec())) + ); + } + } +} diff --git a/contracts/astroport/factory/src/testing.rs b/contracts/astroport/factory/src/testing.rs new file mode 100644 index 00000000..d7d77f70 --- /dev/null +++ b/contracts/astroport/factory/src/testing.rs @@ -0,0 +1,742 @@ +use cosmwasm_std::{ + attr, from_binary, to_binary, Addr, Reply, ReplyOn, SubMsg, SubMsgResponse, SubMsgResult, + WasmMsg, +}; + +use crate::mock_querier::mock_dependencies; +use crate::state::CONFIG; +use crate::{ + contract::{execute, instantiate, query}, + error::ContractError, +}; + +use astroport::asset::{AssetInfo, PairInfo}; +use astroport::factory::{ + ConfigResponse, ExecuteMsg, InstantiateMsg, PairConfig, PairType, PairsResponse, QueryMsg, +}; + +use crate::contract::reply; +use crate::response::MsgInstantiateContractResponse; +use astroport::pair::InstantiateMsg as PairInstantiateMsg; +use cosmwasm_std::testing::{mock_env, mock_info, MOCK_CONTRACT_ADDR}; +use protobuf::Message; + +#[test] +fn pair_type_to_string() { + assert_eq!(PairType::Xyk {}.to_string(), "xyk"); + assert_eq!(PairType::Stable {}.to_string(), "stable"); +} + +#[test] +fn proper_initialization() { + // Validate total and maker fee bps + let mut deps = mock_dependencies(&[]); + let owner = "owner0000".to_string(); + + let msg = InstantiateMsg { + pair_configs: vec![ + PairConfig { + code_id: 123u64, + pair_type: PairType::Xyk {}, + total_fee_bps: 100, + maker_fee_bps: 10, + is_disabled: false, + is_generator_disabled: false, + }, + PairConfig { + code_id: 325u64, + pair_type: PairType::Xyk {}, + total_fee_bps: 100, + maker_fee_bps: 10, + is_disabled: false, + is_generator_disabled: false, + }, + ], + token_code_id: 123u64, + fee_address: None, + generator_address: Some(String::from("generator")), + owner: owner.clone(), + whitelist_code_id: 234u64, + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + + let res = instantiate(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(res, ContractError::PairConfigDuplicate {}); + + let msg = InstantiateMsg { + pair_configs: vec![PairConfig { + code_id: 123u64, + pair_type: PairType::Xyk {}, + total_fee_bps: 10_001, + maker_fee_bps: 10, + is_disabled: false, + is_generator_disabled: false, + }], + token_code_id: 123u64, + fee_address: None, + generator_address: Some(String::from("generator")), + owner: owner.clone(), + whitelist_code_id: 234u64, + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + + let res = instantiate(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(res, ContractError::PairConfigInvalidFeeBps {}); + + let mut deps = mock_dependencies(&[]); + + let msg = InstantiateMsg { + pair_configs: vec![ + PairConfig { + code_id: 325u64, + pair_type: PairType::Stable {}, + total_fee_bps: 100, + maker_fee_bps: 10, + is_disabled: false, + is_generator_disabled: false, + }, + PairConfig { + code_id: 123u64, + pair_type: PairType::Xyk {}, + total_fee_bps: 100, + maker_fee_bps: 10, + is_disabled: false, + is_generator_disabled: false, + }, + ], + token_code_id: 123u64, + fee_address: None, + generator_address: Some(String::from("generator")), + owner: owner.clone(), + whitelist_code_id: 234u64, + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + + instantiate(deps.as_mut(), env.clone(), info, msg.clone()).unwrap(); + + let query_res = query(deps.as_ref(), env, QueryMsg::Config {}).unwrap(); + let config_res: ConfigResponse = from_binary(&query_res).unwrap(); + assert_eq!(123u64, config_res.token_code_id); + assert_eq!(msg.pair_configs, config_res.pair_configs); + assert_eq!(Addr::unchecked(owner), config_res.owner); +} + +#[test] +fn update_config() { + let mut deps = mock_dependencies(&[]); + let owner = "owner0000"; + + let pair_configs = vec![PairConfig { + code_id: 123u64, + pair_type: PairType::Xyk {}, + total_fee_bps: 3, + maker_fee_bps: 166, + is_disabled: false, + is_generator_disabled: false, + }]; + + let msg = InstantiateMsg { + pair_configs, + token_code_id: 123u64, + fee_address: None, + owner: owner.to_string(), + generator_address: Some(String::from("generator")), + whitelist_code_id: 234u64, + }; + + let env = mock_env(); + let info = mock_info(owner, &[]); + + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + // Update config + let env = mock_env(); + let info = mock_info(owner, &[]); + let msg = ExecuteMsg::UpdateConfig { + token_code_id: Some(200u64), + fee_address: Some(String::from("new_fee_addr")), + generator_address: Some(String::from("new_generator_addr")), + whitelist_code_id: None, + }; + + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + assert_eq!(0, res.messages.len()); + + // It worked, let's query the state + let query_res = query(deps.as_ref(), env, QueryMsg::Config {}).unwrap(); + let config_res: ConfigResponse = from_binary(&query_res).unwrap(); + assert_eq!(200u64, config_res.token_code_id); + assert_eq!(owner, config_res.owner); + assert_eq!( + String::from("new_fee_addr"), + config_res.fee_address.unwrap() + ); + assert_eq!( + String::from("new_generator_addr"), + config_res.generator_address.unwrap() + ); + + // Unauthorized err + let env = mock_env(); + let info = mock_info("addr0000", &[]); + let msg = ExecuteMsg::UpdateConfig { + token_code_id: None, + fee_address: None, + generator_address: None, + whitelist_code_id: None, + }; + + let res = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(res, ContractError::Unauthorized {}); +} + +#[test] +fn update_owner() { + let mut deps = mock_dependencies(&[]); + let owner = "owner0000"; + + let msg = InstantiateMsg { + pair_configs: vec![], + token_code_id: 123u64, + fee_address: None, + owner: owner.to_string(), + generator_address: Some(String::from("generator")), + whitelist_code_id: 234u64, + }; + + let env = mock_env(); + let info = mock_info(owner, &[]); + + // We can just call .unwrap() to assert this was a success + instantiate(deps.as_mut(), env, info, msg).unwrap(); + + let new_owner = String::from("new_owner"); + + // New owner + let env = mock_env(); + let msg = ExecuteMsg::ProposeNewOwner { + owner: new_owner.clone(), + expires_in: 100, // seconds + }; + + let info = mock_info(new_owner.as_str(), &[]); + + // Unauthorized check + let err = execute(deps.as_mut(), env.clone(), info, msg.clone()).unwrap_err(); + assert_eq!(err.to_string(), "Generic error: Unauthorized"); + + // Claim before proposal + let info = mock_info(new_owner.as_str(), &[]); + execute( + deps.as_mut(), + env.clone(), + info, + ExecuteMsg::ClaimOwnership {}, + ) + .unwrap_err(); + + // Propose new owner + let info = mock_info(owner, &[]); + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + assert_eq!(0, res.messages.len()); + + // Unauthorized ownership claim + let info = mock_info("invalid_addr", &[]); + let err = execute( + deps.as_mut(), + env.clone(), + info, + ExecuteMsg::ClaimOwnership {}, + ) + .unwrap_err(); + assert_eq!(err.to_string(), "Generic error: Unauthorized"); + + // Claim ownership + let info = mock_info(new_owner.as_str(), &[]); + let res = execute( + deps.as_mut(), + env.clone(), + info, + ExecuteMsg::ClaimOwnership {}, + ) + .unwrap(); + assert_eq!(0, res.messages.len()); + + // Let's query the state + let config: ConfigResponse = + from_binary(&query(deps.as_ref(), env, QueryMsg::Config {}).unwrap()).unwrap(); + assert_eq!(new_owner, config.owner); +} + +#[test] +fn update_pair_config() { + let mut deps = mock_dependencies(&[]); + let owner = "owner0000"; + let pair_configs = vec![PairConfig { + code_id: 123u64, + pair_type: PairType::Xyk {}, + total_fee_bps: 100, + maker_fee_bps: 10, + is_disabled: false, + is_generator_disabled: false, + }]; + + let msg = InstantiateMsg { + pair_configs: pair_configs.clone(), + token_code_id: 123u64, + fee_address: None, + owner: owner.to_string(), + generator_address: Some(String::from("generator")), + whitelist_code_id: 234u64, + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + + // We can just call .unwrap() to assert this was a success + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + + // It worked, let's query the state + let query_res = query(deps.as_ref(), env, QueryMsg::Config {}).unwrap(); + let config_res: ConfigResponse = from_binary(&query_res).unwrap(); + assert_eq!(pair_configs, config_res.pair_configs); + + // Update config + let pair_config = PairConfig { + code_id: 800, + pair_type: PairType::Xyk {}, + total_fee_bps: 1, + maker_fee_bps: 2, + is_disabled: false, + is_generator_disabled: false, + }; + + // Unauthorized err + let env = mock_env(); + let info = mock_info("wrong-addr0000", &[]); + let msg = ExecuteMsg::UpdatePairConfig { + config: pair_config.clone(), + }; + + let res = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(res, ContractError::Unauthorized {}); + + // Check validation of total and maker fee bps + let env = mock_env(); + let info = mock_info(<&str>::clone(&owner), &[]); + let msg = ExecuteMsg::UpdatePairConfig { + config: PairConfig { + code_id: 123u64, + pair_type: PairType::Xyk {}, + total_fee_bps: 3, + maker_fee_bps: 10_001, + is_disabled: false, + is_generator_disabled: false, + }, + }; + + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + assert_eq!(res, ContractError::PairConfigInvalidFeeBps {}); + + let info = mock_info(<&str>::clone(&owner), &[]); + let msg = ExecuteMsg::UpdatePairConfig { + config: pair_config.clone(), + }; + + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + assert_eq!(0, res.messages.len()); + + // It worked, let's query the state + let query_res = query(deps.as_ref(), env.clone(), QueryMsg::Config {}).unwrap(); + let config_res: ConfigResponse = from_binary(&query_res).unwrap(); + assert_eq!(vec![pair_config.clone()], config_res.pair_configs); + + // Add second config + let pair_config_custom = PairConfig { + code_id: 100, + pair_type: PairType::Custom("test".to_string()), + total_fee_bps: 10, + maker_fee_bps: 20, + is_disabled: false, + is_generator_disabled: false, + }; + + let info = mock_info(<&str>::clone(&owner), &[]); + let msg = ExecuteMsg::UpdatePairConfig { + config: pair_config_custom.clone(), + }; + + execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + + // It worked, let's query the state + let query_res = query(deps.as_ref(), env, QueryMsg::Config {}).unwrap(); + let config_res: ConfigResponse = from_binary(&query_res).unwrap(); + assert_eq!( + vec![pair_config_custom, pair_config], + config_res.pair_configs + ); +} + +#[test] +fn create_pair() { + let mut deps = mock_dependencies(&[]); + + let pair_config = PairConfig { + code_id: 321u64, + pair_type: PairType::Xyk {}, + total_fee_bps: 100, + maker_fee_bps: 10, + is_disabled: false, + is_generator_disabled: false, + }; + + let msg = InstantiateMsg { + pair_configs: vec![pair_config.clone()], + token_code_id: 123u64, + fee_address: None, + owner: "owner0000".to_string(), + generator_address: Some(String::from("generator")), + whitelist_code_id: 234u64, + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg.clone()).unwrap(); + + let asset_infos = vec![ + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0001"), + }, + ]; + + let config = CONFIG.load(&deps.storage); + let env = mock_env(); + let info = mock_info("addr0000", &[]); + + // Check pair creation using a non-whitelisted pair ID + let res = execute( + deps.as_mut(), + env.clone(), + info.clone(), + ExecuteMsg::CreatePair { + pair_type: PairType::Stable {}, + asset_infos: asset_infos.clone(), + init_params: None, + }, + ) + .unwrap_err(); + assert_eq!(res, ContractError::PairConfigNotFound {}); + + let res = execute( + deps.as_mut(), + env, + info, + ExecuteMsg::CreatePair { + pair_type: PairType::Xyk {}, + asset_infos: asset_infos.clone(), + init_params: None, + }, + ) + .unwrap(); + + assert_eq!( + res.attributes, + vec![ + attr("action", "create_pair"), + attr("pair", "asset0000-asset0001") + ] + ); + assert_eq!( + res.messages, + vec![SubMsg { + msg: WasmMsg::Instantiate { + msg: to_binary(&PairInstantiateMsg { + factory_addr: String::from(MOCK_CONTRACT_ADDR), + asset_infos, + token_code_id: msg.token_code_id, + init_params: None + }) + .unwrap(), + code_id: pair_config.code_id, + funds: vec![], + admin: Some(config.unwrap().owner.to_string()), + label: String::from("Astroport pair"), + } + .into(), + id: 1, + gas_limit: None, + reply_on: ReplyOn::Success + }] + ); +} + +#[test] +fn register() { + let mut deps = mock_dependencies(&[]); + let owner = "owner0000"; + + let msg = InstantiateMsg { + pair_configs: vec![PairConfig { + code_id: 123u64, + pair_type: PairType::Xyk {}, + total_fee_bps: 100, + maker_fee_bps: 10, + is_disabled: false, + is_generator_disabled: false, + }], + token_code_id: 123u64, + fee_address: None, + generator_address: Some(String::from("generator")), + owner: owner.to_string(), + whitelist_code_id: 234u64, + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + let asset_infos = vec![ + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0001"), + }, + ]; + + let msg = ExecuteMsg::CreatePair { + pair_type: PairType::Xyk {}, + asset_infos: asset_infos.clone(), + init_params: None, + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + + let pair0_addr = "pair0000".to_string(); + let pair0_info = PairInfo { + asset_infos: asset_infos.clone(), + contract_addr: Addr::unchecked("pair0000"), + liquidity_token: Addr::unchecked("liquidity0000"), + pair_type: PairType::Xyk {}, + }; + + let mut deployed_pairs = vec![(&pair0_addr, &pair0_info)]; + + // Register an Astroport pair querier + deps.querier.with_astroport_pairs(&deployed_pairs); + + let data = MsgInstantiateContractResponse { + contract_address: String::from("pair0000"), + data: vec![], + unknown_fields: Default::default(), + cached_size: Default::default(), + } + .write_to_bytes() + .unwrap(); + + let reply_msg = Reply { + id: 1, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(data.into()), + }), + }; + + let _res = reply(deps.as_mut(), mock_env(), reply_msg.clone()).unwrap(); + + let query_res = query( + deps.as_ref(), + env, + QueryMsg::Pair { + asset_infos: asset_infos.clone(), + }, + ) + .unwrap(); + + let pair_res: PairInfo = from_binary(&query_res).unwrap(); + assert_eq!( + pair_res, + PairInfo { + liquidity_token: Addr::unchecked("liquidity0000"), + contract_addr: Addr::unchecked("pair0000"), + asset_infos: asset_infos.clone(), + pair_type: PairType::Xyk {}, + } + ); + + // Check pair was registered + let res = reply(deps.as_mut(), mock_env(), reply_msg).unwrap_err(); + assert_eq!(res, ContractError::PairWasRegistered {}); + + // Store one more item to test query pairs + let asset_infos_2 = vec![ + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0002"), + }, + ]; + + let msg = ExecuteMsg::CreatePair { + pair_type: PairType::Xyk {}, + asset_infos: asset_infos_2.clone(), + init_params: None, + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + + let pair1_addr = "pair0001".to_string(); + let pair1_info = PairInfo { + asset_infos: asset_infos_2.clone(), + contract_addr: Addr::unchecked("pair0001"), + liquidity_token: Addr::unchecked("liquidity0001"), + pair_type: PairType::Xyk {}, + }; + + deployed_pairs.push((&pair1_addr, &pair1_info)); + + // Register astroport pair querier + deps.querier.with_astroport_pairs(&deployed_pairs); + + let data = MsgInstantiateContractResponse { + contract_address: String::from("pair0001"), + data: vec![], + unknown_fields: Default::default(), + cached_size: Default::default(), + } + .write_to_bytes() + .unwrap(); + + let reply_msg_2 = Reply { + id: 1, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(data.into()), + }), + }; + + let _res = reply(deps.as_mut(), mock_env(), reply_msg_2).unwrap(); + + let query_msg = QueryMsg::Pairs { + start_after: None, + limit: None, + }; + + let res = query(deps.as_ref(), env.clone(), query_msg).unwrap(); + let pairs_res: PairsResponse = from_binary(&res).unwrap(); + assert_eq!( + pairs_res.pairs, + vec![ + PairInfo { + liquidity_token: Addr::unchecked("liquidity0000"), + contract_addr: Addr::unchecked("pair0000"), + asset_infos: asset_infos.clone(), + pair_type: PairType::Xyk {}, + }, + PairInfo { + liquidity_token: Addr::unchecked("liquidity0001"), + contract_addr: Addr::unchecked("pair0001"), + asset_infos: asset_infos_2.clone(), + pair_type: PairType::Xyk {}, + } + ] + ); + + let query_msg = QueryMsg::Pairs { + start_after: None, + limit: Some(1), + }; + + let res = query(deps.as_ref(), env.clone(), query_msg).unwrap(); + let pairs_res: PairsResponse = from_binary(&res).unwrap(); + assert_eq!( + pairs_res.pairs, + vec![PairInfo { + liquidity_token: Addr::unchecked("liquidity0000"), + contract_addr: Addr::unchecked("pair0000"), + asset_infos: asset_infos.clone(), + pair_type: PairType::Xyk {}, + }] + ); + + let query_msg = QueryMsg::Pairs { + start_after: Some(asset_infos.clone()), + limit: None, + }; + + let res = query(deps.as_ref(), env, query_msg).unwrap(); + let pairs_res: PairsResponse = from_binary(&res).unwrap(); + assert_eq!( + pairs_res.pairs, + vec![PairInfo { + liquidity_token: Addr::unchecked("liquidity0001"), + contract_addr: Addr::unchecked("pair0001"), + asset_infos: asset_infos_2.clone(), + pair_type: PairType::Xyk {}, + }] + ); + + // Deregister from wrong acc + let env = mock_env(); + let info = mock_info("wrong_addr0000", &[]); + let res = execute( + deps.as_mut(), + env, + info, + ExecuteMsg::Deregister { + asset_infos: asset_infos_2.clone(), + }, + ) + .unwrap_err(); + + assert_eq!(res, ContractError::Unauthorized {}); + + // Proper deregister + let env = mock_env(); + let info = mock_info(<&str>::clone(&owner), &[]); + let res = execute( + deps.as_mut(), + env.clone(), + info, + ExecuteMsg::Deregister { + asset_infos: asset_infos_2, + }, + ) + .unwrap(); + + assert_eq!(res.attributes[0], attr("action", "deregister")); + + let query_msg = QueryMsg::Pairs { + start_after: None, + limit: None, + }; + + let res = query(deps.as_ref(), env, query_msg).unwrap(); + let pairs_res: PairsResponse = from_binary(&res).unwrap(); + assert_eq!( + pairs_res.pairs, + vec![PairInfo { + liquidity_token: Addr::unchecked("liquidity0000"), + contract_addr: Addr::unchecked("pair0000"), + asset_infos, + pair_type: PairType::Xyk {}, + },] + ); +} diff --git a/contracts/astroport/factory/tests/factory_helper.rs b/contracts/astroport/factory/tests/factory_helper.rs new file mode 100644 index 00000000..73b45de4 --- /dev/null +++ b/contracts/astroport/factory/tests/factory_helper.rs @@ -0,0 +1,205 @@ +use anyhow::Result as AnyResult; +use astroport::asset::{AssetInfo, PairInfo}; +use astroport::factory::{PairConfig, PairType, QueryMsg}; +use cosmwasm_std::{Addr, Binary}; +use cw20::MinterResponse; +use cw_multi_test::{App, AppResponse, ContractWrapper, Executor}; + +pub struct FactoryHelper { + pub owner: Addr, + pub astro_token: Addr, + pub factory: Addr, + pub cw20_token_code_id: u64, +} + +impl FactoryHelper { + pub fn init(router: &mut App, owner: &Addr) -> Self { + let astro_token_contract = Box::new(ContractWrapper::new_with_empty( + astroport_token::contract::execute, + astroport_token::contract::instantiate, + astroport_token::contract::query, + )); + + let cw20_token_code_id = router.store_code(astro_token_contract); + + let msg = astroport::token::InstantiateMsg { + name: String::from("Astro token"), + symbol: String::from("ASTRO"), + decimals: 6, + initial_balances: vec![], + mint: Some(MinterResponse { + minter: owner.to_string(), + cap: None, + }), + marketing: None, + }; + + let astro_token = router + .instantiate_contract( + cw20_token_code_id, + owner.clone(), + &msg, + &[], + String::from("ASTRO"), + None, + ) + .unwrap(); + + let pair_contract = Box::new( + ContractWrapper::new_with_empty( + astroport_pair::contract::execute, + astroport_pair::contract::instantiate, + astroport_pair::contract::query, + ) + .with_reply_empty(astroport_pair::contract::reply), + ); + + let pair_code_id = router.store_code(pair_contract); + + let factory_contract = Box::new( + ContractWrapper::new_with_empty( + astroport_factory::contract::execute, + astroport_factory::contract::instantiate, + astroport_factory::contract::query, + ) + .with_reply_empty(astroport_factory::contract::reply), + ); + + let factory_code_id = router.store_code(factory_contract); + + let msg = astroport::factory::InstantiateMsg { + pair_configs: vec![PairConfig { + code_id: pair_code_id, + pair_type: PairType::Xyk {}, + total_fee_bps: 100, + maker_fee_bps: 10, + is_disabled: false, + is_generator_disabled: false, + }], + token_code_id: cw20_token_code_id, + fee_address: None, + generator_address: None, + owner: owner.to_string(), + whitelist_code_id: 0, + }; + + let factory = router + .instantiate_contract( + factory_code_id, + owner.clone(), + &msg, + &[], + String::from("ASTRO"), + None, + ) + .unwrap(); + + Self { + owner: owner.clone(), + astro_token, + factory, + cw20_token_code_id, + } + } + + pub fn update_config( + &mut self, + router: &mut App, + sender: &Addr, + token_code_id: Option, + fee_address: Option, + generator_address: Option, + whitelist_code_id: Option, + ) -> AnyResult { + let msg = astroport::factory::ExecuteMsg::UpdateConfig { + token_code_id, + fee_address, + generator_address, + whitelist_code_id, + }; + + router.execute_contract(sender.clone(), self.factory.clone(), &msg, &[]) + } + + pub fn create_pair( + &mut self, + router: &mut App, + sender: &Addr, + pair_type: PairType, + tokens: [&Addr; 2], + init_params: Option, + ) -> AnyResult { + let asset_infos = vec![ + AssetInfo::Token { + contract_addr: tokens[0].clone(), + }, + AssetInfo::Token { + contract_addr: tokens[1].clone(), + }, + ]; + + let msg = astroport::factory::ExecuteMsg::CreatePair { + pair_type, + asset_infos, + init_params, + }; + + router.execute_contract(sender.clone(), self.factory.clone(), &msg, &[]) + } + + pub fn create_pair_with_addr( + &mut self, + router: &mut App, + sender: &Addr, + pair_type: PairType, + tokens: [&Addr; 2], + init_params: Option, + ) -> AnyResult { + self.create_pair(router, sender, pair_type, tokens, init_params)?; + + let asset_infos = vec![ + AssetInfo::Token { + contract_addr: tokens[0].clone(), + }, + AssetInfo::Token { + contract_addr: tokens[1].clone(), + }, + ]; + + let res: PairInfo = router + .wrap() + .query_wasm_smart(self.factory.clone(), &QueryMsg::Pair { asset_infos })?; + + Ok(res.contract_addr) + } +} + +pub fn instantiate_token( + app: &mut App, + token_code_id: u64, + owner: &Addr, + token_name: &str, + decimals: Option, +) -> Addr { + let init_msg = astroport::token::InstantiateMsg { + name: token_name.to_string(), + symbol: token_name.to_string(), + decimals: decimals.unwrap_or(6), + initial_balances: vec![], + mint: Some(MinterResponse { + minter: owner.to_string(), + cap: None, + }), + marketing: None, + }; + + app.instantiate_contract( + token_code_id, + owner.clone(), + &init_msg, + &[], + token_name, + None, + ) + .unwrap() +} diff --git a/contracts/astroport/factory/tests/integration.rs b/contracts/astroport/factory/tests/integration.rs new file mode 100644 index 00000000..92eb482e --- /dev/null +++ b/contracts/astroport/factory/tests/integration.rs @@ -0,0 +1,497 @@ +mod factory_helper; + +use cosmwasm_std::{attr, Addr}; + +use astroport::asset::{AssetInfo, PairInfo}; +use astroport::factory::{ + ConfigResponse, ExecuteMsg, FeeInfoResponse, InstantiateMsg, PairConfig, PairType, QueryMsg, +}; + +use crate::factory_helper::{instantiate_token, FactoryHelper}; +use astroport::pair::ExecuteMsg as PairExecuteMsg; +use cw_multi_test::{App, ContractWrapper, Executor}; + +fn mock_app() -> App { + App::default() +} + +fn store_factory_code(app: &mut App) -> u64 { + let factory_contract = Box::new( + ContractWrapper::new_with_empty( + astroport_factory::contract::execute, + astroport_factory::contract::instantiate, + astroport_factory::contract::query, + ) + .with_reply_empty(astroport_factory::contract::reply), + ); + + app.store_code(factory_contract) +} + +#[test] +fn proper_initialization() { + let mut app = mock_app(); + + let owner = Addr::unchecked("owner"); + + let factory_code_id = store_factory_code(&mut app); + + let pair_configs = vec![PairConfig { + code_id: 321, + pair_type: PairType::Xyk {}, + total_fee_bps: 100, + maker_fee_bps: 10, + is_disabled: false, + is_generator_disabled: false, + }]; + + let msg = InstantiateMsg { + pair_configs: pair_configs.clone(), + token_code_id: 123, + fee_address: None, + owner: owner.to_string(), + generator_address: Some(String::from("generator")), + whitelist_code_id: 234u64, + }; + + let factory_instance = app + .instantiate_contract( + factory_code_id, + Addr::unchecked(owner.clone()), + &msg, + &[], + "factory", + None, + ) + .unwrap(); + + let msg = QueryMsg::Config {}; + let config_res: ConfigResponse = app + .wrap() + .query_wasm_smart(&factory_instance, &msg) + .unwrap(); + + assert_eq!(123, config_res.token_code_id); + assert_eq!(pair_configs, config_res.pair_configs); + assert_eq!(owner, config_res.owner); +} + +#[test] +fn update_config() { + let mut app = mock_app(); + let owner = Addr::unchecked("owner"); + let mut helper = FactoryHelper::init(&mut app, &owner); + + // Update config + helper + .update_config( + &mut app, + &owner, + Some(200u64), + Some("fee".to_string()), + Some("generator".to_string()), + None, + ) + .unwrap(); + + let config_res: ConfigResponse = app + .wrap() + .query_wasm_smart(&helper.factory, &QueryMsg::Config {}) + .unwrap(); + + assert_eq!(200u64, config_res.token_code_id); + assert_eq!("fee", config_res.fee_address.unwrap().to_string()); + assert_eq!( + "generator", + config_res.generator_address.unwrap().to_string() + ); + + // Unauthorized err + let res = helper + .update_config( + &mut app, + &Addr::unchecked("not_owner"), + None, + None, + None, + None, + ) + .unwrap_err(); + assert_eq!(res.root_cause().to_string(), "Unauthorized"); +} + +#[test] +fn test_create_pair() { + let mut app = mock_app(); + let owner = Addr::unchecked("owner"); + let mut helper = FactoryHelper::init(&mut app, &owner); + + let token1 = instantiate_token( + &mut app, + helper.cw20_token_code_id, + &owner, + "tokenX", + Some(18), + ); + let token2 = instantiate_token( + &mut app, + helper.cw20_token_code_id, + &owner, + "tokenY", + Some(18), + ); + + let err = helper + .create_pair(&mut app, &owner, PairType::Xyk {}, [&token1, &token1], None) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Doubling assets in asset infos" + ); + + let res = helper + .create_pair(&mut app, &owner, PairType::Xyk {}, [&token1, &token2], None) + .unwrap(); + + let err = helper + .create_pair(&mut app, &owner, PairType::Xyk {}, [&token1, &token2], None) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Pair was already created"); + + assert_eq!(res.events[1].attributes[1], attr("action", "create_pair")); + assert_eq!( + res.events[1].attributes[2], + attr("pair", format!("{}-{}", token1.as_str(), token2.as_str())) + ); + + let res: PairInfo = app + .wrap() + .query_wasm_smart( + helper.factory.clone(), + &QueryMsg::Pair { + asset_infos: vec![ + AssetInfo::Token { + contract_addr: token1.clone(), + }, + AssetInfo::Token { + contract_addr: token2.clone(), + }, + ], + }, + ) + .unwrap(); + + // In multitest, contract names are counted in the order in which contracts are created + assert_eq!("contract1", helper.factory.to_string()); + assert_eq!("contract4", res.contract_addr.to_string()); + assert_eq!("contract5", res.liquidity_token.to_string()); + + // Create disabled pair type + app.execute_contract( + owner.clone(), + helper.factory.clone(), + &ExecuteMsg::UpdatePairConfig { + config: PairConfig { + code_id: 0, + pair_type: PairType::Custom("Custom".to_string()), + total_fee_bps: 100, + maker_fee_bps: 40, + is_disabled: true, + is_generator_disabled: false, + }, + }, + &[], + ) + .unwrap(); + + let token3 = instantiate_token( + &mut app, + helper.cw20_token_code_id, + &owner, + "tokenY", + Some(18), + ); + + let err = helper + .create_pair( + &mut app, + &Addr::unchecked("someone"), + PairType::Custom("Custom".to_string()), + [&token1, &token3], + None, + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Pair config disabled"); + + // Query fee info + let fee_info: FeeInfoResponse = app + .wrap() + .query_wasm_smart( + &helper.factory, + &QueryMsg::FeeInfo { + pair_type: PairType::Custom("Custom".to_string()), + }, + ) + .unwrap(); + assert_eq!(100, fee_info.total_fee_bps); + assert_eq!(40, fee_info.maker_fee_bps); + + // query blacklisted pairs + let pair_types: Vec = app + .wrap() + .query_wasm_smart(&helper.factory, &QueryMsg::BlacklistedPairTypes {}) + .unwrap(); + assert_eq!(pair_types, vec![PairType::Custom("Custom".to_string())]); +} + +#[test] +fn test_pair_migration() { + let mut app = mock_app(); + + let owner = Addr::unchecked("owner"); + let mut helper = FactoryHelper::init(&mut app, &owner); + + let token_instance0 = + instantiate_token(&mut app, helper.cw20_token_code_id, &owner, "tokenX", None); + let token_instance1 = + instantiate_token(&mut app, helper.cw20_token_code_id, &owner, "tokenY", None); + let token_instance2 = + instantiate_token(&mut app, helper.cw20_token_code_id, &owner, "tokenZ", None); + + // Create pairs in factory + let pairs = [ + helper + .create_pair_with_addr( + &mut app, + &owner, + PairType::Xyk {}, + [&token_instance0, &token_instance1], + None, + ) + .unwrap(), + helper + .create_pair_with_addr( + &mut app, + &owner, + PairType::Xyk {}, + [&token_instance0, &token_instance2], + None, + ) + .unwrap(), + ]; + + // Change contract ownership + let new_owner = Addr::unchecked("new_owner"); + + app.execute_contract( + owner.clone(), + helper.factory.clone(), + &ExecuteMsg::ProposeNewOwner { + owner: new_owner.to_string(), + expires_in: 100, + }, + &[], + ) + .unwrap(); + app.execute_contract( + new_owner.clone(), + helper.factory.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap(); + + let pair3 = helper + .create_pair_with_addr( + &mut app, + &owner, + PairType::Xyk {}, + [&token_instance1, &token_instance2], + None, + ) + .unwrap(); + + // Should panic due to pairs are not migrated. + for pair in pairs.clone() { + let res = app + .execute_contract( + new_owner.clone(), + pair, + &PairExecuteMsg::UpdateConfig { + params: Default::default(), + }, + &[], + ) + .unwrap_err(); + + assert_eq!( + res.root_cause().to_string(), + "Pair is not migrated to the new admin!" + ); + } + + // Pair is created after admin migration + let res = app + .execute_contract( + Addr::unchecked("user1"), + pair3, + &PairExecuteMsg::UpdateConfig { + params: Default::default(), + }, + &[], + ) + .unwrap_err(); + + assert_ne!(res.to_string(), "Pair is not migrated to the new admin"); + + let pairs_res: Vec = app + .wrap() + .query_wasm_smart(&helper.factory, &QueryMsg::PairsToMigrate {}) + .unwrap(); + assert_eq!(&pairs_res, &pairs); + + // Factory owner was changed to new owner + let err = app + .execute_contract( + owner, + helper.factory.clone(), + &ExecuteMsg::MarkAsMigrated { + pairs: Vec::from(pairs.clone().map(String::from)), + }, + &[], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Unauthorized"); + + app.execute_contract( + new_owner, + helper.factory.clone(), + &ExecuteMsg::MarkAsMigrated { + pairs: Vec::from(pairs.clone().map(String::from)), + }, + &[], + ) + .unwrap(); + + for pair in pairs { + let res = app + .execute_contract( + Addr::unchecked("user1"), + pair, + &PairExecuteMsg::UpdateConfig { + params: Default::default(), + }, + &[], + ) + .unwrap_err(); + + assert_ne!(res.to_string(), "Pair is not migrated to the new admin!"); + } +} + +#[test] +fn check_update_owner() { + let mut app = mock_app(); + let owner = Addr::unchecked("owner"); + let helper = FactoryHelper::init(&mut app, &owner); + + let new_owner = String::from("new_owner"); + + // New owner + let msg = ExecuteMsg::ProposeNewOwner { + owner: new_owner.clone(), + expires_in: 100, // seconds + }; + + // Unauthed check + let err = app + .execute_contract( + Addr::unchecked("not_owner"), + helper.factory.clone(), + &msg, + &[], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); + + // Claim before proposal + let err = app + .execute_contract( + Addr::unchecked(new_owner.clone()), + helper.factory.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Ownership proposal not found" + ); + + // Propose new owner + app.execute_contract(Addr::unchecked("owner"), helper.factory.clone(), &msg, &[]) + .unwrap(); + + // Claim from invalid addr + let err = app + .execute_contract( + Addr::unchecked("invalid_addr"), + helper.factory.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); + + // Drop ownership proposal + let err = app + .execute_contract( + Addr::unchecked(new_owner.clone()), + helper.factory.clone(), + &ExecuteMsg::DropOwnershipProposal {}, + &[], + ) + .unwrap_err(); + // new_owner is not an owner yet + assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); + + app.execute_contract( + owner.clone(), + helper.factory.clone(), + &ExecuteMsg::DropOwnershipProposal {}, + &[], + ) + .unwrap(); + + // Try to claim ownership + let err = app + .execute_contract( + Addr::unchecked(new_owner.clone()), + helper.factory.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Ownership proposal not found" + ); + + // Propose new owner again + app.execute_contract(Addr::unchecked("owner"), helper.factory.clone(), &msg, &[]) + .unwrap(); + // Claim ownership + app.execute_contract( + Addr::unchecked(new_owner.clone()), + helper.factory.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap(); + + // Let's query the contract state + let msg = QueryMsg::Config {}; + let res: ConfigResponse = app.wrap().query_wasm_smart(&helper.factory, &msg).unwrap(); + + assert_eq!(res.owner, new_owner) +} diff --git a/contracts/astroport/oracle/.cargo/config b/contracts/astroport/oracle/.cargo/config new file mode 100644 index 00000000..ab65a083 --- /dev/null +++ b/contracts/astroport/oracle/.cargo/config @@ -0,0 +1,5 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +integration-test = "test --test integration" +schema = "run --example oracle_schema" diff --git a/contracts/astroport/oracle/.editorconfig b/contracts/astroport/oracle/.editorconfig new file mode 100644 index 00000000..f9bf75df --- /dev/null +++ b/contracts/astroport/oracle/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.rs] +indent_size = 4 \ No newline at end of file diff --git a/contracts/astroport/oracle/Cargo.toml b/contracts/astroport/oracle/Cargo.toml new file mode 100644 index 00000000..db529081 --- /dev/null +++ b/contracts/astroport/oracle/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "astroport-oracle" +version = "2.0.0" +authors = ["Astroport"] +edition = "2021" + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] + +[dependencies] +cosmwasm-std = { version = "1.1" } +cw-storage-plus = "0.15" +thiserror = { version = "1.0" } +cw2 = "0.15" +cw20 = "0.15" +astroport = { path = "../../../packages/astroport", default-features = false } +cosmwasm-schema = { version = "1.1" } + +[dev-dependencies] +astroport-token = {path = "../token" } +astroport-factory = {path = "../factory" } +astroport-pair = {path = "../pair" } +astroport-pair-stable = {path = "../pair_stable" } +cw-multi-test = "0.15" +itertools = "0.10" +anyhow = "1.0" diff --git a/contracts/astroport/oracle/README.md b/contracts/astroport/oracle/README.md new file mode 100644 index 00000000..0b5a68d9 --- /dev/null +++ b/contracts/astroport/oracle/README.md @@ -0,0 +1,60 @@ +# Astroport Oracle + +This demo oracle contract calculates a 1 day TWAP for a xy=k Astroport pool. + +--- + +## InstantiateMsg + +Initializes the oracle and checks that the target asset pair type is x*y=k. + +```json +{ + "factory_contract": "terra...", + "asset_infos": [ + { + "token": { + "contract_addr": "terra..." + } + }, + { + "native_token": { + "denom": "uusd" + } + } + ] +} +``` + +## ExecuteMsg + +### `update` + +Updates the local TWAP value and the target pair's cumulative prices. + +```json +{ + "update": {} +} +``` + +## QueryMsg + +All query messages are described below. A custom struct is defined for each query response. + +### `consult` + +Multiplies a token amount (token that's present in the target pool for the TWAP) by the latest TWAP value for that token. + +```json +{ + "consult": { + "token": { + "native_token": { + "denom": "uluna" + } + }, + "amount": "1000000" + } +} +``` diff --git a/contracts/astroport/oracle/examples/oracle_schema.rs b/contracts/astroport/oracle/examples/oracle_schema.rs new file mode 100644 index 00000000..9a597396 --- /dev/null +++ b/contracts/astroport/oracle/examples/oracle_schema.rs @@ -0,0 +1,11 @@ +use astroport::oracle::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use cosmwasm_schema::write_api; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg + } +} diff --git a/contracts/astroport/oracle/src/contract.rs b/contracts/astroport/oracle/src/contract.rs new file mode 100644 index 00000000..7e25dd52 --- /dev/null +++ b/contracts/astroport/oracle/src/contract.rs @@ -0,0 +1,270 @@ +use crate::error::ContractError; +use crate::querier::{query_cumulative_prices, query_prices}; +use crate::state::{Config, PriceCumulativeLast, CONFIG, LAST_UPDATE_HEIGHT, PRICE_LAST}; +use astroport::asset::{addr_validate_to_lower, Asset, AssetInfo, Decimal256Ext}; +use astroport::cosmwasm_ext::IntegerToDecimal; +use astroport::oracle::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use astroport::pair::TWAP_PRECISION; +use astroport::querier::{query_pair_info, query_token_precision}; +use cosmwasm_std::{ + entry_point, to_binary, Binary, Decimal256, Deps, DepsMut, Env, MessageInfo, Response, + StdError, StdResult, Uint128, Uint256, Uint64, +}; +use cw2::set_contract_version; +use std::ops::Div; + +/// Contract name that is used for migration. +const CONTRACT_NAME: &str = "astroport-oracle"; +/// Contract version that is used for migration. +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// Creates a new contract with the specified parameters in the [`InstantiateMsg`]. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + msg.asset_infos[0].check(deps.api)?; + msg.asset_infos[1].check(deps.api)?; + + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + let factory_contract = addr_validate_to_lower(deps.api, &msg.factory_contract)?; + let pair_info = query_pair_info(&deps.querier, &factory_contract, &msg.asset_infos)?; + + let config = Config { + owner: info.sender, + factory: factory_contract, + asset_infos: msg.asset_infos, + pair: pair_info.clone(), + period: msg.period, + }; + CONFIG.save(deps.storage, &config)?; + let prices = query_cumulative_prices(deps.querier, pair_info.contract_addr)?; + let average_prices = prices + .cumulative_prices + .iter() + .cloned() + .map(|(from, to, _)| (from, to, Decimal256::zero())) + .collect(); + + let price = PriceCumulativeLast { + cumulative_prices: prices.cumulative_prices, + average_prices, + block_timestamp_last: env.block.time.seconds(), + }; + PRICE_LAST.save(deps.storage, &price, env.block.height)?; + LAST_UPDATE_HEIGHT.save(deps.storage, &Uint64::zero())?; + Ok(Response::default()) +} + +/// Exposes all the execute functions available in the contract. +/// +/// ## Variants +/// * **ExecuteMsg::Update {}** Updates the local TWAP values for the assets in the Astroport pool. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Update {} => update(deps, env), + ExecuteMsg::UpdatePeriod { new_period } => update_period(deps, env, info, new_period), + } +} + +pub fn update_period( + deps: DepsMut, + _env: Env, + info: MessageInfo, + new_period: u64, +) -> Result { + let mut config: Config = CONFIG.load(deps.storage)?; + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + config.period = new_period; + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new() + .add_attribute("action", "update_config") + .add_attribute("new_period", config.period.to_string())) +} + +/// Updates the local TWAP values for the tokens in the target Astroport pool. +pub fn update(deps: DepsMut, env: Env) -> Result { + let config = CONFIG.load(deps.storage)?; + let price_last = PRICE_LAST.load(deps.storage)?; + + let prices = query_cumulative_prices(deps.querier, config.pair.contract_addr)?; + let time_elapsed = env.block.time.seconds() - price_last.block_timestamp_last; + + // Ensure that at least one full period has passed since the last update + if time_elapsed < config.period { + return Err(ContractError::WrongPeriod {}); + } + + let mut average_prices = vec![]; + for (asset1_last, asset2_last, price_last) in price_last.cumulative_prices.iter() { + for (asset1, asset2, price) in prices.cumulative_prices.iter() { + if asset1.equal(asset1_last) && asset2.equal(asset2_last) { + average_prices.push(( + asset1.clone(), + asset2.clone(), + Decimal256::from_ratio( + Uint256::from(price.wrapping_sub(*price_last)), + time_elapsed, + ), + )); + } + } + } + + let prices = PriceCumulativeLast { + cumulative_prices: prices.cumulative_prices, + average_prices, + block_timestamp_last: env.block.time.seconds(), + }; + LAST_UPDATE_HEIGHT.save(deps.storage, &Uint64::from(env.block.height))?; + PRICE_LAST.save(deps.storage, &prices, env.block.height)?; + Ok(Response::default()) +} + +/// Exposes all the queries available in the contract. +/// +/// ## Queries +/// * **QueryMsg::Consult { token, amount }** Validates assets and calculates a new average +/// amount with updated precision +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Consult { token, amount } => to_binary(&consult(deps, token, amount)?), + QueryMsg::TWAPAtHeight { token, height } => { + to_binary(&twap_at_height(deps, token, height)?) + } + } +} + +/// Multiplies a token amount by its latest TWAP value. +/// * **token** token for which we multiply its TWAP value by an amount. +/// +/// * **amount** amount of tokens we multiply the TWAP by. +fn consult( + deps: Deps, + token: AssetInfo, + amount: Uint128, +) -> Result, StdError> { + let config = CONFIG.load(deps.storage)?; + let price_last = PRICE_LAST.load(deps.storage)?; + + let mut average_prices = vec![]; + for (from, to, value) in price_last.average_prices { + if from.equal(&token) { + average_prices.push((to, value)); + } + } + + if average_prices.is_empty() { + return Err(StdError::generic_err("Invalid Token")); + } + + // Get the token's precision + let p = query_token_precision(&deps.querier, &token)?; + let one = Uint128::new(10_u128.pow(p.into())); + + average_prices + .iter() + .map(|(asset, price_average)| { + if price_average.is_zero() { + let price = query_prices( + deps.querier, + config.pair.contract_addr.clone(), + Asset { + info: token.clone(), + amount: one, + }, + Some(asset.clone()), + )? + .return_amount; + Ok(( + asset.clone(), + Uint256::from(price).multiply_ratio(Uint256::from(amount), Uint256::from(one)), + )) + } else { + let price_precision = Uint256::from(10_u128.pow(TWAP_PRECISION.into())); + Ok(( + asset.clone(), + Uint256::from(amount) * *price_average / price_precision, + )) + } + }) + .collect::, StdError>>() +} + +/// Returns token TWAP value for given height. +/// * **token** token for which we getting its historicalTWAP value. +/// +/// * **height** height, on which we receive TWAP +fn twap_at_height( + deps: Deps, + token: AssetInfo, + height: Uint64, +) -> Result, StdError> { + let config = CONFIG.load(deps.storage)?; + let last_height = LAST_UPDATE_HEIGHT.load(deps.storage)?; + let mut query_height = height; + // if requested height > last snapshoted time, SnapshotItem.may_load_at_height() will return primary (default) value + // which is very first stored data. To avoid that, in such cases we just query TWAP for last known height. + if height > last_height { + query_height = last_height + } + let price_last = PRICE_LAST + .may_load_at_height(deps.storage, u64::from(query_height)) + .unwrap() + .unwrap(); + let mut average_prices = vec![]; + for (from, to, value) in price_last.average_prices { + if from.equal(&token) { + average_prices.push((to, value)); + } + } + + if average_prices.is_empty() { + return Err(StdError::generic_err("Invalid Token")); + } + + // Get the token's precision + let p = query_token_precision(&deps.querier, &token)?; + let one = Uint128::new(10_u128.pow(p.into())); + + average_prices + .iter() + .map(|(asset, price_average)| { + if price_average.is_zero() { + let price = query_prices( + deps.querier, + config.pair.contract_addr.clone(), + Asset { + info: token.clone(), + amount: one, + }, + Some(asset.clone()), + )? + .return_amount; + Ok(( + asset.clone(), + Decimal256::from_integer(Uint256::from(price)) + .div(Decimal256::from(one.to_decimal())), + )) + } else { + let price_precision = Uint256::from(10_u128.pow(TWAP_PRECISION.into())); + Ok((asset.clone(), *price_average / price_precision)) + } + }) + .collect::, StdError>>() +} diff --git a/contracts/astroport/oracle/src/error.rs b/contracts/astroport/oracle/src/error.rs new file mode 100644 index 00000000..cd75f9df --- /dev/null +++ b/contracts/astroport/oracle/src/error.rs @@ -0,0 +1,18 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +/// This enum describes oracle contract errors +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Period not elapsed")] + WrongPeriod {}, + + #[error("Contract can't be migrated!")] + MigrationError {}, +} diff --git a/contracts/astroport/oracle/src/lib.rs b/contracts/astroport/oracle/src/lib.rs new file mode 100644 index 00000000..ccb1a2a0 --- /dev/null +++ b/contracts/astroport/oracle/src/lib.rs @@ -0,0 +1,10 @@ +pub mod contract; +pub mod error; +mod querier; +pub mod state; + +#[cfg(test)] +mod testing; + +#[cfg(test)] +mod mock_querier; diff --git a/contracts/astroport/oracle/src/mock_querier.rs b/contracts/astroport/oracle/src/mock_querier.rs new file mode 100644 index 00000000..e7282235 --- /dev/null +++ b/contracts/astroport/oracle/src/mock_querier.rs @@ -0,0 +1,128 @@ +use astroport::asset::{Asset, AssetInfo, PairInfo}; +use astroport::factory::PairType; +use astroport::factory::QueryMsg::Pair; +use astroport::pair::CumulativePricesResponse; +use astroport::pair::QueryMsg::CumulativePrices; +use cosmwasm_std::testing::{MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR}; +use cosmwasm_std::{ + from_binary, from_slice, to_binary, Addr, Coin, Empty, OwnedDeps, Querier, QuerierResult, + QueryRequest, SystemError, SystemResult, Uint128, WasmQuery, +}; +use std::collections::HashMap; + +pub fn mock_dependencies( + contract_balance: &[Coin], +) -> OwnedDeps { + let custom_querier: WasmMockQuerier = + WasmMockQuerier::new(MockQuerier::new(&[(MOCK_CONTRACT_ADDR, contract_balance)])); + + OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: custom_querier, + custom_query_type: Default::default(), + } +} + +pub struct WasmMockQuerier { + base: MockQuerier, + token_querier: TokenQuerier, +} + +#[derive(Clone, Default)] +pub struct TokenQuerier { + // this lets us iterate over all pairs that match the first string + pairs: HashMap, +} + +impl TokenQuerier { + pub fn set( + &mut self, + pair: Addr, + assets: Vec, + total: Uint128, + cumulative_prices: Vec<(AssetInfo, AssetInfo, Uint128)>, + ) { + self.pairs = HashMap::new(); + self.pairs.insert( + pair.to_string(), + CumulativePricesResponse { + assets, + total_share: total, + cumulative_prices, + }, + ); + } +} + +impl Querier for WasmMockQuerier { + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { + // MockQuerier doesn't support Custom, so we ignore it completely here + let request: QueryRequest = match from_slice(bin_request) { + Ok(v) => v, + Err(e) => { + return SystemResult::Err(SystemError::InvalidRequest { + error: format!("Parsing query request: {}", e), + request: bin_request.into(), + }) + } + }; + self.handle_query(&request) + } +} + +impl WasmMockQuerier { + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { + match &request { + QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => { + if contract_addr == "factory" { + match from_binary(msg).unwrap() { + Pair { asset_infos } => SystemResult::Ok( + to_binary(&PairInfo { + asset_infos, + contract_addr: Addr::unchecked("pair"), + liquidity_token: Addr::unchecked("lp_token"), + pair_type: PairType::Xyk {}, + }) + .into(), + ), + _ => panic!("DO NOT ENTER HERE"), + } + } else { + match from_binary(msg).unwrap() { + CumulativePrices { .. } => { + let balance = match self.token_querier.pairs.get(contract_addr) { + Some(v) => v, + None => { + return SystemResult::Err(SystemError::Unknown {}); + } + }; + SystemResult::Ok(to_binary(&balance).into()) + } + _ => panic!("DO NOT ENTER HERE"), + } + } + } + _ => self.base.handle_query(request), + } + } +} +impl WasmMockQuerier { + pub fn new(base: MockQuerier) -> Self { + WasmMockQuerier { + base, + token_querier: TokenQuerier::default(), + } + } + + pub fn set_cumulative_price( + &mut self, + pair: Addr, + assert: Vec, + total: Uint128, + cumulative_prices: Vec<(AssetInfo, AssetInfo, Uint128)>, + ) { + self.token_querier + .set(pair, assert, total, cumulative_prices) + } +} diff --git a/contracts/astroport/oracle/src/querier.rs b/contracts/astroport/oracle/src/querier.rs new file mode 100644 index 00000000..01056f67 --- /dev/null +++ b/contracts/astroport/oracle/src/querier.rs @@ -0,0 +1,33 @@ +use astroport::asset::{Asset, AssetInfo}; +use astroport::pair::{CumulativePricesResponse, QueryMsg as PairQueryMsg, SimulationResponse}; +use cosmwasm_std::{QuerierWrapper, StdResult}; + +/// Returns information about a pair's asset cumulative prices using a [`CumulativePricesResponse`] object. +/// +/// * **pair_contract** address of the pair for which we return data. +pub fn query_cumulative_prices( + querier: QuerierWrapper, + pair_contract: impl Into, +) -> StdResult { + querier.query_wasm_smart(pair_contract, &PairQueryMsg::CumulativePrices {}) +} + +/// Returns information about an asset's price from a specific pair. +/// +/// * **pair_contract** pair that holds the target asset. +/// +/// * **asset** asset for which we return the simulated price. +pub fn query_prices( + querier: QuerierWrapper, + pair_contract: impl Into, + offer_asset: Asset, + ask_asset_info: Option, +) -> StdResult { + querier.query_wasm_smart( + pair_contract, + &PairQueryMsg::Simulation { + offer_asset, + ask_asset_info, + }, + ) +} diff --git a/contracts/astroport/oracle/src/state.rs b/contracts/astroport/oracle/src/state.rs new file mode 100644 index 00000000..e0d8939f --- /dev/null +++ b/contracts/astroport/oracle/src/state.rs @@ -0,0 +1,44 @@ +use cosmwasm_schema::cw_serde; + +use astroport::asset::{AssetInfo, PairInfo}; +use cosmwasm_std::{Addr, Decimal256, Uint128, Uint64}; +use cw_storage_plus::{Item, SnapshotItem, Strategy}; + +/// Stores the contract config at the given key +pub const CONFIG: Item = Item::new("config"); +/// Stores the latest cumulative and average prices at the given key and height +pub const PRICE_LAST: SnapshotItem = SnapshotItem::new( + "price_last", + "price_last_checkpoints", + "price_last_changelog", + Strategy::EveryBlock, +); + +/// Stores the height of last prices update +pub const LAST_UPDATE_HEIGHT: Item = Item::new("last_update_height"); + +/// This structure stores the latest cumulative and average token prices for the target pool +#[cw_serde] +pub struct PriceCumulativeLast { + /// The vector contains last cumulative prices for each pair of assets in the pool + pub cumulative_prices: Vec<(AssetInfo, AssetInfo, Uint128)>, + /// The vector contains average prices for each pair of assets in the pool + pub average_prices: Vec<(AssetInfo, AssetInfo, Decimal256)>, + /// The last timestamp block in pool + pub block_timestamp_last: u64, +} + +/// Global configuration for the contract +#[cw_serde] +pub struct Config { + /// The address that's allowed to change contract parameters + pub owner: Addr, + /// The factory contract address + pub factory: Addr, + /// The assets in the pool. Each asset is described using a [`AssetInfo`] + pub asset_infos: Vec, + /// Information about the pair (LP token address, pair type etc) + pub pair: PairInfo, + /// Time between two consecutive TWAP updates. + pub period: u64, +} diff --git a/contracts/astroport/oracle/src/testing.rs b/contracts/astroport/oracle/src/testing.rs new file mode 100644 index 00000000..ccc490c3 --- /dev/null +++ b/contracts/astroport/oracle/src/testing.rs @@ -0,0 +1,99 @@ +use crate::contract::{execute, instantiate}; +use crate::mock_querier::mock_dependencies; +use astroport::asset::{Asset, AssetInfo}; +use astroport::oracle::{ExecuteMsg, InstantiateMsg}; +use cosmwasm_std::testing::{mock_env, mock_info}; +use cosmwasm_std::{Addr, Decimal256, Uint128, Uint256}; +use std::ops::Mul; + +#[test] +fn decimal_overflow() { + let price_cumulative_current = Uint128::from(100u128); + let price_cumulative_last = Uint128::from(192738282u128); + let time_elapsed: u64 = 86400; + let amount = Uint128::from(1000u128); + let price_average = Decimal256::from_ratio( + Uint256::from(price_cumulative_current.wrapping_sub(price_cumulative_last)), + time_elapsed, + ); + + println!("{}", price_average); + + let res: Uint128 = price_average.mul(Uint256::from(amount)).try_into().unwrap(); + println!("{}", res); +} + +#[test] +fn oracle_overflow() { + let mut deps = mock_dependencies(&[]); + let info = mock_info("addr0000", &[]); + + let mut env = mock_env(); + let factory = Addr::unchecked("factory"); + let astro_token_contract = Addr::unchecked("astro-token"); + let usdc_token_contract = Addr::unchecked("usdc-token"); + + let astro_asset_info = AssetInfo::Token { + contract_addr: astro_token_contract, + }; + let usdc_asset_info = AssetInfo::Token { + contract_addr: usdc_token_contract, + }; + let astro_asset = Asset { + info: astro_asset_info.clone(), + amount: Uint128::zero(), + }; + let usdc_asset = Asset { + info: usdc_asset_info.clone(), + amount: Uint128::zero(), + }; + + let asset = vec![astro_asset, usdc_asset]; + + let instantiate_msg = InstantiateMsg { + factory_contract: factory.to_string(), + asset_infos: vec![astro_asset_info, usdc_asset_info], + period: 1, + }; + + // Set cumulative price to 192738282u128 + deps.querier.set_cumulative_price( + Addr::unchecked("pair"), + asset.clone(), + Uint128::from(192738282u128), + vec![ + ( + asset[0].info.clone(), + asset[1].info.clone(), + Uint128::from(192738282u128), + ), + ( + asset[1].info.clone(), + asset[0].info.clone(), + Uint128::from(192738282u128), + ), + ], + ); + let res = instantiate(deps.as_mut(), env.clone(), info.clone(), instantiate_msg).unwrap(); + assert_eq!(0, res.messages.len()); + // Set cumulative price to 100 (overflow) + deps.querier.set_cumulative_price( + Addr::unchecked("pair"), + asset.clone(), + Uint128::from(100u128), + vec![ + ( + asset[0].info.clone(), + asset[1].info.clone(), + Uint128::from(100u128), + ), + ( + asset[1].info.clone(), + asset[0].info.clone(), + Uint128::from(100u128), + ), + ], + ); + env.block.time = env.block.time.plus_seconds(86400); + execute(deps.as_mut(), env, info, ExecuteMsg::Update {}).unwrap(); +} diff --git a/contracts/astroport/oracle/tests/integration.rs b/contracts/astroport/oracle/tests/integration.rs new file mode 100644 index 00000000..d4f96d81 --- /dev/null +++ b/contracts/astroport/oracle/tests/integration.rs @@ -0,0 +1,2679 @@ +use anyhow::Result; +use cosmwasm_std::{ + attr, to_binary, Addr, BlockInfo, Coin, Decimal, Decimal256, QueryRequest, StdResult, Uint128, + Uint64, WasmQuery, +}; +use cw20::{BalanceResponse, Cw20QueryMsg, MinterResponse}; +use cw_multi_test::{App, AppResponse, ContractWrapper, Executor}; +use itertools::Itertools; +use std::str::FromStr; + +use astroport::asset::{Asset, AssetInfo, PairInfo}; +use astroport::token::InstantiateMsg as TokenInstantiateMsg; + +use astroport::factory::{PairConfig, PairType}; + +use astroport::oracle::QueryMsg::{Consult, TWAPAtHeight}; +use astroport::oracle::{ExecuteMsg, InstantiateMsg}; +use astroport::pair::StablePoolParams; + +fn mock_app(owner: Option, coins: Option>) -> App { + if let (Some(own), Some(coinz)) = ((owner), (coins)) { + App::new(|router, _, storage| { + // initialization moved to App construction + router.bank.init_balance(storage, &own, coinz).unwrap() + }) + } else { + App::default() + } +} + +fn instantiate_contracts(router: &mut App, owner: Addr) -> (Addr, Addr, u64) { + let astro_token_contract = Box::new(ContractWrapper::new_with_empty( + astroport_token::contract::execute, + astroport_token::contract::instantiate, + astroport_token::contract::query, + )); + + let astro_token_code_id = router.store_code(astro_token_contract); + + let msg = TokenInstantiateMsg { + name: String::from("Astro token"), + symbol: String::from("ASTRO"), + decimals: 6, + initial_balances: vec![], + mint: Some(MinterResponse { + minter: owner.to_string(), + cap: None, + }), + marketing: None, + }; + + let astro_token_instance = router + .instantiate_contract( + astro_token_code_id, + owner.clone(), + &msg, + &[], + String::from("ASTRO"), + None, + ) + .unwrap(); + + let pair_contract = Box::new( + ContractWrapper::new_with_empty( + astroport_pair::contract::execute, + astroport_pair::contract::instantiate, + astroport_pair::contract::query, + ) + .with_reply_empty(astroport_pair::contract::reply), + ); + + let pair_code_id = router.store_code(pair_contract); + + let pair_stable_contract = Box::new( + ContractWrapper::new_with_empty( + astroport_pair_stable::contract::execute, + astroport_pair_stable::contract::instantiate, + astroport_pair_stable::contract::query, + ) + .with_reply_empty(astroport_pair_stable::contract::reply), + ); + + let pair_stable_code_id = router.store_code(pair_stable_contract); + + let factory_contract = Box::new( + ContractWrapper::new_with_empty( + astroport_factory::contract::execute, + astroport_factory::contract::instantiate, + astroport_factory::contract::query, + ) + .with_reply_empty(astroport_factory::contract::reply), + ); + + let factory_code_id = router.store_code(factory_contract); + let msg = astroport::factory::InstantiateMsg { + pair_configs: vec![ + PairConfig { + code_id: pair_code_id, + pair_type: PairType::Xyk {}, + total_fee_bps: 0, + maker_fee_bps: 0, + is_disabled: false, + is_generator_disabled: false, + }, + PairConfig { + code_id: pair_stable_code_id, + pair_type: PairType::Stable {}, + total_fee_bps: 0, + maker_fee_bps: 0, + is_disabled: false, + is_generator_disabled: false, + }, + ], + token_code_id: 1u64, + fee_address: None, + generator_address: Some(String::from("generator")), + owner: owner.to_string(), + whitelist_code_id: 234u64, + }; + + let factory_instance = router + .instantiate_contract( + factory_code_id, + owner, + &msg, + &[], + String::from("FACTORY"), + None, + ) + .unwrap(); + + let oracle_contract = Box::new(ContractWrapper::new_with_empty( + astroport_oracle::contract::execute, + astroport_oracle::contract::instantiate, + astroport_oracle::contract::query, + )); + let oracle_code_id = router.store_code(oracle_contract); + (astro_token_instance, factory_instance, oracle_code_id) +} + +fn instantiate_token(router: &mut App, owner: Addr, name: String, symbol: String) -> Addr { + let token_contract = Box::new(ContractWrapper::new_with_empty( + astroport_token::contract::execute, + astroport_token::contract::instantiate, + astroport_token::contract::query, + )); + + let token_code_id = router.store_code(token_contract); + + let msg = TokenInstantiateMsg { + name, + symbol: symbol.clone(), + decimals: 6, + initial_balances: vec![], + mint: Some(MinterResponse { + minter: owner.to_string(), + cap: None, + }), + marketing: None, + }; + + router + .instantiate_contract(token_code_id, owner, &msg, &[], symbol, None) + .unwrap() +} + +fn mint_some_token(router: &mut App, owner: Addr, token_instance: Addr, to: Addr, amount: Uint128) { + let msg = cw20::Cw20ExecuteMsg::Mint { + recipient: to.to_string(), + amount, + }; + let res = router + .execute_contract(owner, token_instance, &msg, &[]) + .unwrap(); + assert_eq!(res.events[1].attributes[1], attr("action", "mint")); + assert_eq!(res.events[1].attributes[2], attr("to", to.to_string())); + assert_eq!(res.events[1].attributes[3], attr("amount", amount)); +} + +fn allowance_token(router: &mut App, owner: Addr, spender: Addr, token: Addr, amount: Uint128) { + let msg = cw20::Cw20ExecuteMsg::IncreaseAllowance { + spender: spender.to_string(), + amount, + expires: None, + }; + let res = router + .execute_contract(owner.clone(), token, &msg, &[]) + .unwrap(); + assert_eq!( + res.events[1].attributes[1], + attr("action", "increase_allowance") + ); + assert_eq!( + res.events[1].attributes[2], + attr("owner", owner.to_string()) + ); + assert_eq!( + res.events[1].attributes[3], + attr("spender", spender.to_string()) + ); + assert_eq!(res.events[1].attributes[4], attr("amount", amount)); +} + +fn check_balance(router: &mut App, user: Addr, token: Addr, expected_amount: Uint128) { + let msg = Cw20QueryMsg::Balance { + address: user.to_string(), + }; + + let res: Result = + router.wrap().query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: token.to_string(), + msg: to_binary(&msg).unwrap(), + })); + + let balance = res.unwrap(); + + assert_eq!(balance.balance, expected_amount); +} + +fn provide_liquidity( + router: &mut App, + owner: Addr, + user: Addr, + pair_info: &PairInfo, + assets: Vec, +) -> Result { + let mut funds = vec![]; + + for a in assets.clone() { + match a.info { + AssetInfo::Token { contract_addr } => { + allowance_token( + router, + user.clone(), + pair_info.contract_addr.clone(), + contract_addr.clone(), + a.amount, + ); + } + AssetInfo::NativeToken { denom } => { + funds.push(Coin { + denom, + amount: a.amount, + }); + } + } + } + + // When dealing with native tokens transfer should happen before contract call, which cw-multitest doesn't support + for fund in funds.clone() { + // we cannot transfer empty coins amount + if !fund.amount.is_zero() { + router + .send_tokens(owner.clone(), user.clone(), &[fund]) + .unwrap(); + } + } + + router.execute_contract( + user, + pair_info.contract_addr.clone(), + &astroport::pair::ExecuteMsg::ProvideLiquidity { + assets, + slippage_tolerance: None, + auto_stake: None, + receiver: None, + }, + &funds, + ) +} + +fn create_pair( + router: &mut App, + owner: Addr, + user: Addr, + factory_instance: &Addr, + assets: Vec, +) -> PairInfo { + for a in assets.clone() { + if let AssetInfo::Token { contract_addr } = a.info { + mint_some_token( + router, + owner.clone(), + contract_addr.clone(), + user.clone(), + a.amount, + ); + } + } + + let asset_infos = vec![assets[0].info.clone(), assets[1].info.clone()]; + + // Create pair in factory + let res = router + .execute_contract( + owner, + factory_instance.clone(), + &astroport::factory::ExecuteMsg::CreatePair { + pair_type: PairType::Xyk {}, + asset_infos: asset_infos.clone(), + init_params: None, + }, + &[], + ) + .unwrap(); + + assert_eq!(res.events[1].attributes[1], attr("action", "create_pair")); + assert_eq!( + res.events[1].attributes[2], + attr("pair", format!("{}-{}", asset_infos[0], asset_infos[1]),) + ); + + // Get pair + let pair_info: PairInfo = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: factory_instance.clone().to_string(), + msg: to_binary(&astroport::factory::QueryMsg::Pair { asset_infos }).unwrap(), + })) + .unwrap(); + + pair_info +} + +fn create_pair_stable( + router: &mut App, + owner: Addr, + user: Addr, + factory_instance: &Addr, + assets: Vec, +) -> PairInfo { + for a in assets.clone() { + if let AssetInfo::Token { contract_addr } = a.info { + mint_some_token( + router, + owner.clone(), + contract_addr.clone(), + user.clone(), + a.amount, + ); + } + } + + let asset_infos: Vec = assets.iter().cloned().map(|a| a.info).collect(); + + // Create pair in factory + let res = router + .execute_contract( + owner.clone(), + factory_instance.clone(), + &astroport::factory::ExecuteMsg::CreatePair { + pair_type: PairType::Stable {}, + asset_infos: asset_infos.clone(), + init_params: Some( + to_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }, + &[], + ) + .unwrap(); + + assert_eq!(res.events[1].attributes[1], attr("action", "create_pair")); + assert_eq!( + res.events[1].attributes[2], + attr("pair", asset_infos.iter().join("-"),) + ); + + // Get pair + let pair_info: PairInfo = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: factory_instance.clone().to_string(), + msg: to_binary(&astroport::factory::QueryMsg::Pair { asset_infos }).unwrap(), + })) + .unwrap(); + + let mut funds = vec![]; + + for a in assets.clone() { + match a.info { + AssetInfo::Token { contract_addr } => { + allowance_token( + router, + user.clone(), + pair_info.contract_addr.clone(), + contract_addr.clone(), + a.amount, + ); + } + AssetInfo::NativeToken { denom } => { + funds.push(Coin { + denom, + amount: a.amount, + }); + } + } + } + + // When dealing with native tokens transfer should happen before contract call, which cw-multitest doesn't support + for fund in funds.clone() { + // we cannot transfer empty coins amount + if !fund.amount.is_zero() { + router + .send_tokens(owner.clone(), user.clone(), &[fund]) + .unwrap(); + } + } + + router + .execute_contract( + user, + pair_info.contract_addr.clone(), + &astroport::pair::ExecuteMsg::ProvideLiquidity { + assets, + slippage_tolerance: None, + auto_stake: None, + receiver: None, + }, + &funds, + ) + .unwrap(); + + pair_info +} + +fn change_provide_liquidity( + router: &mut App, + owner: Addr, + user: Addr, + pair_contract: Addr, + assets: Vec<(Addr, Uint128)>, +) { + for (token, amount) in assets.clone() { + mint_some_token(router, owner.clone(), token.clone(), user.clone(), amount); + check_balance(router, user.clone(), token.clone(), amount); + allowance_token( + router, + user.clone(), + pair_contract.clone(), + token.clone(), + amount, + ); + } + + let assets: Vec = assets + .iter() + .cloned() + .map(|(token, amount)| Asset { + info: AssetInfo::Token { + contract_addr: token, + }, + amount, + }) + .collect(); + + router + .execute_contract( + user, + pair_contract, + &astroport::pair::ExecuteMsg::ProvideLiquidity { + assets, + slippage_tolerance: Some(Decimal::percent(50)), + auto_stake: None, + receiver: None, + }, + &[], + ) + .unwrap(); +} + +pub fn next_day(block: &mut BlockInfo) { + block.time = block.time.plus_seconds(86400); + block.height += 17280; +} + +#[test] +fn consult() { + let mut router = mock_app(None, None); + let owner = Addr::unchecked("owner"); + let user = Addr::unchecked("user0000"); + let (astro_token_instance, factory_instance, oracle_code_id) = + instantiate_contracts(&mut router, owner.clone()); + + let usdc_token_instance = instantiate_token( + &mut router, + owner.clone(), + "Usdc token".to_string(), + "USDC".to_string(), + ); + + let asset_infos = vec![ + AssetInfo::Token { + contract_addr: usdc_token_instance.clone(), + }, + AssetInfo::Token { + contract_addr: astro_token_instance.clone(), + }, + ]; + + let assets = vec![ + Asset { + info: asset_infos[0].clone(), + amount: Uint128::from(100_000_u128), + }, + Asset { + info: asset_infos[1].clone(), + amount: Uint128::from(100_000_u128), + }, + ]; + + let pair_info = create_pair( + &mut router, + owner.clone(), + user.clone(), + &factory_instance, + assets.clone(), + ); + provide_liquidity(&mut router, owner.clone(), user.clone(), &pair_info, assets).unwrap(); + + router.update_block(next_day); + let pair_info: PairInfo = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: factory_instance.clone().to_string(), + msg: to_binary(&astroport::factory::QueryMsg::Pair { + asset_infos: asset_infos.clone(), + }) + .unwrap(), + })) + .unwrap(); + + change_provide_liquidity( + &mut router, + owner.clone(), + user.clone(), + pair_info.contract_addr.clone(), + vec![ + (astro_token_instance.clone(), Uint128::from(50_000_u128)), + (usdc_token_instance.clone(), Uint128::from(50_000_u128)), + ], + ); + router.update_block(next_day); + + let msg = InstantiateMsg { + factory_contract: factory_instance.to_string(), + asset_infos, + period: 86400, + }; + let oracle_instance = router + .instantiate_contract( + oracle_code_id, + owner.clone(), + &msg, + &[], + String::from("ORACLE"), + None, + ) + .unwrap(); + + let e = router + .execute_contract( + owner.clone(), + oracle_instance.clone(), + &ExecuteMsg::Update {}, + &[], + ) + .unwrap_err(); + assert_eq!(e.root_cause().to_string(), "Period not elapsed",); + router.update_block(next_day); + + // Change pair liquidity + change_provide_liquidity( + &mut router, + owner.clone(), + user, + pair_info.contract_addr, + vec![ + (astro_token_instance.clone(), Uint128::from(10_000_u128)), + (usdc_token_instance.clone(), Uint128::from(10_000_u128)), + ], + ); + router.update_block(next_day); + router + .execute_contract(owner, oracle_instance.clone(), &ExecuteMsg::Update {}, &[]) + .unwrap(); + + for (addr, amount) in [ + (astro_token_instance, Uint128::from(1000u128)), + (usdc_token_instance, Uint128::from(100u128)), + ] { + let msg = Consult { + token: AssetInfo::Token { + contract_addr: addr, + }, + amount, + }; + let res: Vec<(AssetInfo, Uint128)> = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: oracle_instance.to_string(), + msg: to_binary(&msg).unwrap(), + })) + .unwrap(); + assert_eq!(res[0].1, amount); + } +} + +#[test] +fn twap_at_height() { + let mut router = mock_app(None, None); + let owner = Addr::unchecked("owner"); + let user = Addr::unchecked("user0000"); + let (astro_token_instance, factory_instance, oracle_code_id) = + instantiate_contracts(&mut router, owner.clone()); + + let usdc_token_instance = instantiate_token( + &mut router, + owner.clone(), + "Usdc token".to_string(), + "USDC".to_string(), + ); + + let asset_infos = vec![ + AssetInfo::Token { + contract_addr: usdc_token_instance.clone(), + }, + AssetInfo::Token { + contract_addr: astro_token_instance.clone(), + }, + ]; + + let assets = vec![ + Asset { + info: asset_infos[0].clone(), + amount: Uint128::from(100_000_u128), + }, + Asset { + info: asset_infos[1].clone(), + amount: Uint128::from(100_000_u128), + }, + ]; + + let pair_info = create_pair( + &mut router, + owner.clone(), + user.clone(), + &factory_instance, + assets.clone(), + ); + provide_liquidity(&mut router, owner.clone(), user.clone(), &pair_info, assets).unwrap(); + + router.update_block(next_day); + let pair_info: PairInfo = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: factory_instance.clone().to_string(), + msg: to_binary(&astroport::factory::QueryMsg::Pair { + asset_infos: asset_infos.clone(), + }) + .unwrap(), + })) + .unwrap(); + + change_provide_liquidity( + &mut router, + owner.clone(), + user.clone(), + pair_info.contract_addr.clone(), + vec![ + (astro_token_instance.clone(), Uint128::from(50_000_u128)), + (usdc_token_instance.clone(), Uint128::from(50_000_u128)), + ], + ); + router.update_block(next_day); + + let msg = InstantiateMsg { + factory_contract: factory_instance.to_string(), + asset_infos, + period: 86400, + }; + let oracle_instance = router + .instantiate_contract( + oracle_code_id, + owner.clone(), + &msg, + &[], + String::from("ORACLE"), + None, + ) + .unwrap(); + + let e = router + .execute_contract( + owner.clone(), + oracle_instance.clone(), + &ExecuteMsg::Update {}, + &[], + ) + .unwrap_err(); + assert_eq!(e.root_cause().to_string(), "Period not elapsed",); + router.update_block(next_day); + + // Change pair liquidity + change_provide_liquidity( + &mut router, + owner.clone(), + user, + pair_info.contract_addr, + vec![ + (astro_token_instance.clone(), Uint128::from(10_000_u128)), + (usdc_token_instance.clone(), Uint128::from(10_000_u128)), + ], + ); + router.update_block(next_day); + router + .execute_contract(owner, oracle_instance.clone(), &ExecuteMsg::Update {}, &[]) + .unwrap(); + + for (addr, price, block) in [ + ( + astro_token_instance, + Decimal256::from_str("0.137931").unwrap(), + router.block_info().height, + ), + ( + usdc_token_instance, + Decimal256::from_str("0.137931").unwrap(), + router.block_info().height, + ), + ] { + let msg = TWAPAtHeight { + token: AssetInfo::Token { + contract_addr: addr, + }, + height: Uint64::from(block), + }; + let res: Vec<(AssetInfo, Decimal256)> = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: oracle_instance.to_string(), + msg: to_binary(&msg).unwrap(), + })) + .unwrap(); + assert_eq!(res[0].1, price); + } +} + +#[test] +fn consult_pair_stable() { + let mut router = mock_app(None, None); + let owner = Addr::unchecked("owner"); + let user = Addr::unchecked("user0000"); + let (astro_token_instance, factory_instance, oracle_code_id) = + instantiate_contracts(&mut router, owner.clone()); + + let usdc_token_instance = instantiate_token( + &mut router, + owner.clone(), + "Usdc token".to_string(), + "USDC".to_string(), + ); + + let asset_infos = vec![ + AssetInfo::Token { + contract_addr: usdc_token_instance.clone(), + }, + AssetInfo::Token { + contract_addr: astro_token_instance.clone(), + }, + ]; + create_pair_stable( + &mut router, + owner.clone(), + user.clone(), + &factory_instance, + vec![ + Asset { + info: asset_infos[0].clone(), + amount: Uint128::from(100_000_000_000u128), + }, + Asset { + info: asset_infos[1].clone(), + amount: Uint128::from(100_000_000_000u128), + }, + ], + ); + router.update_block(next_day); + let pair_info: PairInfo = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: factory_instance.clone().to_string(), + msg: to_binary(&astroport::factory::QueryMsg::Pair { + asset_infos: asset_infos.clone(), + }) + .unwrap(), + })) + .unwrap(); + + change_provide_liquidity( + &mut router, + owner.clone(), + user.clone(), + pair_info.contract_addr.clone(), + vec![ + ( + astro_token_instance.clone(), + Uint128::from(500_000_000_000u128), + ), + ( + usdc_token_instance.clone(), + Uint128::from(500_000_000_000u128), + ), + ], + ); + router.update_block(next_day); + + let msg = InstantiateMsg { + factory_contract: factory_instance.to_string(), + asset_infos, + period: 86400, + }; + let oracle_instance = router + .instantiate_contract( + oracle_code_id, + owner.clone(), + &msg, + &[], + String::from("ORACLE"), + None, + ) + .unwrap(); + + let e = router + .execute_contract( + owner.clone(), + oracle_instance.clone(), + &ExecuteMsg::Update {}, + &[], + ) + .unwrap_err(); + assert_eq!(e.root_cause().to_string(), "Period not elapsed",); + router.update_block(next_day); + + // Change pair liquidity + change_provide_liquidity( + &mut router, + owner.clone(), + user, + pair_info.contract_addr, + vec![ + ( + astro_token_instance.clone(), + Uint128::from(100_000_000_000u128), + ), + ( + usdc_token_instance.clone(), + Uint128::from(100_000_000_000u128), + ), + ], + ); + router.update_block(next_day); + router + .execute_contract(owner, oracle_instance.clone(), &ExecuteMsg::Update {}, &[]) + .unwrap(); + + for (addr, amount) in [ + (astro_token_instance, Uint128::from(1000u128)), + (usdc_token_instance, Uint128::from(100u128)), + ] { + let msg = Consult { + token: AssetInfo::Token { + contract_addr: addr, + }, + amount, + }; + let res: Vec<(AssetInfo, Uint128)> = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: oracle_instance.to_string(), + msg: to_binary(&msg).unwrap(), + })) + .unwrap(); + assert_eq!(res[0].1, amount); + } +} + +#[test] +fn twap_at_height_pair_stable() { + let mut router = mock_app(None, None); + let owner = Addr::unchecked("owner"); + let user = Addr::unchecked("user0000"); + let (astro_token_instance, factory_instance, oracle_code_id) = + instantiate_contracts(&mut router, owner.clone()); + + let usdc_token_instance = instantiate_token( + &mut router, + owner.clone(), + "Usdc token".to_string(), + "USDC".to_string(), + ); + + let asset_infos = vec![ + AssetInfo::Token { + contract_addr: usdc_token_instance.clone(), + }, + AssetInfo::Token { + contract_addr: astro_token_instance.clone(), + }, + ]; + create_pair_stable( + &mut router, + owner.clone(), + user.clone(), + &factory_instance, + vec![ + Asset { + info: asset_infos[0].clone(), + amount: Uint128::from(100_000_000_000u128), + }, + Asset { + info: asset_infos[1].clone(), + amount: Uint128::from(100_000_000_000u128), + }, + ], + ); + router.update_block(next_day); + let pair_info: PairInfo = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: factory_instance.clone().to_string(), + msg: to_binary(&astroport::factory::QueryMsg::Pair { + asset_infos: asset_infos.clone(), + }) + .unwrap(), + })) + .unwrap(); + + change_provide_liquidity( + &mut router, + owner.clone(), + user.clone(), + pair_info.contract_addr.clone(), + vec![ + ( + astro_token_instance.clone(), + Uint128::from(500_000_000_000u128), + ), + ( + usdc_token_instance.clone(), + Uint128::from(500_000_000_000u128), + ), + ], + ); + router.update_block(next_day); + + let msg = InstantiateMsg { + factory_contract: factory_instance.to_string(), + asset_infos, + period: 86400, + }; + let oracle_instance = router + .instantiate_contract( + oracle_code_id, + owner.clone(), + &msg, + &[], + String::from("ORACLE"), + None, + ) + .unwrap(); + + let e = router + .execute_contract( + owner.clone(), + oracle_instance.clone(), + &ExecuteMsg::Update {}, + &[], + ) + .unwrap_err(); + assert_eq!(e.root_cause().to_string(), "Period not elapsed",); + router.update_block(next_day); + + // Change pair liquidity + change_provide_liquidity( + &mut router, + owner.clone(), + user, + pair_info.contract_addr, + vec![ + ( + astro_token_instance.clone(), + Uint128::from(100_000_000_000u128), + ), + ( + usdc_token_instance.clone(), + Uint128::from(100_000_000_000u128), + ), + ], + ); + router.update_block(next_day); + router + .execute_contract(owner, oracle_instance.clone(), &ExecuteMsg::Update {}, &[]) + .unwrap(); + + for (addr, amount) in [ + (astro_token_instance, Decimal256::one()), + (usdc_token_instance, Decimal256::one()), + ] { + let msg = TWAPAtHeight { + token: AssetInfo::Token { + contract_addr: addr, + }, + height: Uint64::from(router.block_info().height), + }; + let res: Vec<(AssetInfo, Decimal256)> = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: oracle_instance.to_string(), + msg: to_binary(&msg).unwrap(), + })) + .unwrap(); + assert_eq!(res[0].1, amount); + } +} + +#[test] +fn consult2() { + let mut router = mock_app(None, None); + let owner = Addr::unchecked("owner"); + let user = Addr::unchecked("user0000"); + let (astro_token_instance, factory_instance, oracle_code_id) = + instantiate_contracts(&mut router, owner.clone()); + + let usdc_token_instance = instantiate_token( + &mut router, + owner.clone(), + "Usdc token".to_string(), + "USDC".to_string(), + ); + + let asset_infos = vec![ + AssetInfo::Token { + contract_addr: usdc_token_instance.clone(), + }, + AssetInfo::Token { + contract_addr: astro_token_instance.clone(), + }, + ]; + + let pair_info = create_pair( + &mut router, + owner.clone(), + user.clone(), + &factory_instance, + vec![ + Asset { + info: asset_infos[0].clone(), + amount: Uint128::from(2000_u128), + }, + Asset { + info: asset_infos[1].clone(), + amount: Uint128::from(2000_u128), + }, + ], + ); + + // try to provide less then 1000 + let err = provide_liquidity( + &mut router, + owner.clone(), + user.clone(), + &pair_info, + vec![ + Asset { + info: asset_infos[0].clone(), + amount: Uint128::from(100_u128), + }, + Asset { + info: asset_infos[1].clone(), + amount: Uint128::from(100_u128), + }, + ], + ) + .unwrap_err(); + assert_eq!( + "Initial liquidity must be more than 1000", + err.root_cause().to_string() + ); + + // try to provide MINIMUM_LIQUIDITY_AMOUNT + let err = provide_liquidity( + &mut router, + owner.clone(), + user.clone(), + &pair_info, + vec![ + Asset { + info: asset_infos[0].clone(), + amount: Uint128::from(1000_u128), + }, + Asset { + info: asset_infos[1].clone(), + amount: Uint128::from(1000_u128), + }, + ], + ) + .unwrap_err(); + assert_eq!( + "Initial liquidity must be more than 1000", + err.root_cause().to_string() + ); + + // try to provide more then MINIMUM_LIQUIDITY_AMOUNT + provide_liquidity( + &mut router, + owner.clone(), + user.clone(), + &pair_info, + vec![ + Asset { + info: asset_infos[0].clone(), + amount: Uint128::from(2000_u128), + }, + Asset { + info: asset_infos[1].clone(), + amount: Uint128::from(2000_u128), + }, + ], + ) + .unwrap(); + + router.update_block(next_day); + + change_provide_liquidity( + &mut router, + owner.clone(), + user.clone(), + pair_info.contract_addr.clone(), + vec![ + (astro_token_instance.clone(), Uint128::from(1000_u128)), + (usdc_token_instance.clone(), Uint128::from(1000_u128)), + ], + ); + router.update_block(next_day); + + let msg = InstantiateMsg { + factory_contract: factory_instance.to_string(), + asset_infos, + period: 86400, + }; + let oracle_instance = router + .instantiate_contract( + oracle_code_id, + owner.clone(), + &msg, + &[], + String::from("ORACLE"), + None, + ) + .unwrap(); + + let e = router + .execute_contract( + owner.clone(), + oracle_instance.clone(), + &ExecuteMsg::Update {}, + &[], + ) + .unwrap_err(); + assert_eq!(e.root_cause().to_string(), "Period not elapsed"); + router.update_block(next_day); + router + .execute_contract( + owner.clone(), + oracle_instance.clone(), + &ExecuteMsg::Update {}, + &[], + ) + .unwrap(); + + // Change pair liquidity + for (amount1, amount2) in [ + (Uint128::from(1000_u128), Uint128::from(500_u128)), + (Uint128::from(1000_u128), Uint128::from(500_u128)), + ] { + change_provide_liquidity( + &mut router, + owner.clone(), + user.clone(), + pair_info.contract_addr.clone(), + vec![ + (astro_token_instance.clone(), amount1), + (usdc_token_instance.clone(), amount2), + ], + ); + router.update_block(next_day); + router + .execute_contract( + owner.clone(), + oracle_instance.clone(), + &ExecuteMsg::Update {}, + &[], + ) + .unwrap(); + } + for (addr, amount, amount_exp) in [ + ( + astro_token_instance.clone(), + Uint128::from(1000u128), + Uint128::from(800u128), + ), + ( + usdc_token_instance.clone(), + Uint128::from(1000u128), + Uint128::from(1250u128), + ), + ] { + let msg = Consult { + token: AssetInfo::Token { + contract_addr: addr, + }, + amount, + }; + let res: Vec<(AssetInfo, Uint128)> = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: oracle_instance.to_string(), + msg: to_binary(&msg).unwrap(), + })) + .unwrap(); + assert_eq!(res[0].1, amount_exp); + } + + // Change pair liquidity + for (amount1, amount2) in [ + (Uint128::from(250_u128), Uint128::from(350_u128)), + (Uint128::from(250_u128), Uint128::from(350_u128)), + ] { + change_provide_liquidity( + &mut router, + owner.clone(), + user.clone(), + pair_info.contract_addr.clone(), + vec![ + (astro_token_instance.clone(), amount1), + (usdc_token_instance.clone(), amount2), + ], + ); + router.update_block(next_day); + router + .execute_contract( + owner.clone(), + oracle_instance.clone(), + &ExecuteMsg::Update {}, + &[], + ) + .unwrap(); + } + for (addr, amount, amount_exp) in [ + ( + astro_token_instance, + Uint128::from(1000u128), + Uint128::from(854u128), + ), + ( + usdc_token_instance, + Uint128::from(1000u128), + Uint128::from(1170u128), + ), + ] { + let msg = Consult { + token: AssetInfo::Token { + contract_addr: addr, + }, + amount, + }; + let res: Vec<(AssetInfo, Uint128)> = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: oracle_instance.to_string(), + msg: to_binary(&msg).unwrap(), + })) + .unwrap(); + assert_eq!(res[0].1, amount_exp); + } +} + +#[test] +fn consult_zero_price() { + let owner = Addr::unchecked("owner"); + let mut router = mock_app( + Option::from(owner.clone()), + Some(vec![ + Coin { + denom: "cny".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ]), + ); + let user = Addr::unchecked("user0000"); + + let (astro_token_instance, factory_instance, oracle_code_id) = + instantiate_contracts(&mut router, owner.clone()); + + let usdc_token_instance = instantiate_token( + &mut router, + owner.clone(), + "Usdc token".to_string(), + "USDC".to_string(), + ); + + let asset_infos = vec![ + AssetInfo::Token { + contract_addr: usdc_token_instance.clone(), + }, + AssetInfo::Token { + contract_addr: astro_token_instance.clone(), + }, + ]; + + let pair_info = create_pair( + &mut router, + owner.clone(), + user.clone(), + &factory_instance, + vec![ + Asset { + info: asset_infos[0].clone(), + amount: Uint128::from(100_000_000_000u128), + }, + Asset { + info: asset_infos[1].clone(), + amount: Uint128::from(100_000_000_000u128), + }, + ], + ); + + provide_liquidity( + &mut router, + owner.clone(), + user.clone(), + &pair_info, + vec![ + Asset { + info: asset_infos[0].clone(), + amount: Uint128::from(100_000_000_000u128), + }, + Asset { + info: asset_infos[1].clone(), + amount: Uint128::from(100_000_000_000u128), + }, + ], + ) + .unwrap(); + + router.update_block(next_day); + let msg = InstantiateMsg { + factory_contract: factory_instance.to_string(), + asset_infos, + period: 86400, + }; + let oracle_instance = router + .instantiate_contract( + oracle_code_id, + owner.clone(), + &msg, + &[], + String::from("ORACLE"), + None, + ) + .unwrap(); + + let e = router + .execute_contract( + owner.clone(), + oracle_instance.clone(), + &ExecuteMsg::Update {}, + &[], + ) + .unwrap_err(); + assert_eq!(e.root_cause().to_string(), "Period not elapsed",); + router.update_block(next_day); + router + .execute_contract( + owner.clone(), + oracle_instance.clone(), + &ExecuteMsg::Update {}, + &[], + ) + .unwrap(); + + for (addr, amount_in, amount_out) in [ + ( + astro_token_instance, + Uint128::from(100u128), + Uint128::from(100u128), + ), + ( + usdc_token_instance, + Uint128::from(100u128), + Uint128::from(100u128), + ), + ] { + let msg = Consult { + token: AssetInfo::Token { + contract_addr: addr, + }, + amount: amount_in, + }; + let res: Vec<(AssetInfo, Uint128)> = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: oracle_instance.to_string(), + msg: to_binary(&msg).unwrap(), + })) + .unwrap(); + assert_eq!(res[0].1, amount_out); + } + + let res: StdResult = router.wrap().query_wasm_smart( + &oracle_instance, + &Consult { + token: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Default::default(), + }, + ); + assert_eq!( + res.unwrap_err().to_string(), + "Generic error: Querier contract error: Generic error: Invalid Token" + ); + + // Consult zero price + + let asset_infos = vec![ + AssetInfo::NativeToken { + denom: "cny".to_string(), + }, + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ]; + + let pair_info = create_pair( + &mut router, + owner.clone(), + user.clone(), + &factory_instance, + vec![ + Asset { + info: asset_infos[0].clone(), + amount: Uint128::from(100u8), + }, + Asset { + info: asset_infos[1].clone(), + amount: Uint128::from(100_000_000_000u128), + }, + ], + ); + + provide_liquidity( + &mut router, + owner.clone(), + user, + &pair_info, + vec![ + Asset { + info: asset_infos[0].clone(), + amount: Uint128::from(100u8), + }, + Asset { + info: asset_infos[1].clone(), + amount: Uint128::from(100_000_000_000u128), + }, + ], + ) + .unwrap(); + + let oracle_instance = router + .instantiate_contract( + oracle_code_id, + owner, + &InstantiateMsg { + factory_contract: factory_instance.to_string(), + asset_infos: asset_infos.clone(), + period: 86400, + }, + &[], + String::from("ORACLE 2"), + None, + ) + .unwrap(); + + let res: Vec<(AssetInfo, Uint128)> = router + .wrap() + .query_wasm_smart( + &oracle_instance, + &Consult { + token: asset_infos[1].clone(), + amount: Uint128::from(1u8), + }, + ) + .unwrap(); + // Price is too small thus we get zero + assert_eq!(res[0].1.u128(), 0u128); +} + +#[test] +fn consult_multiple_assets() { + let mut router = mock_app(None, None); + let owner = Addr::unchecked("owner"); + let user = Addr::unchecked("user0000"); + let (astro_token_instance, factory_instance, oracle_code_id) = + instantiate_contracts(&mut router, owner.clone()); + + let usdc_token_instance = instantiate_token( + &mut router, + owner.clone(), + "Usdc token".to_string(), + "USDC".to_string(), + ); + + let usdt_token_instance = instantiate_token( + &mut router, + owner.clone(), + "Usdt token".to_string(), + "USDT".to_string(), + ); + + let asset_infos = vec![ + AssetInfo::Token { + contract_addr: usdc_token_instance.clone(), + }, + AssetInfo::Token { + contract_addr: astro_token_instance.clone(), + }, + AssetInfo::Token { + contract_addr: usdt_token_instance.clone(), + }, + ]; + create_pair_stable( + &mut router, + owner.clone(), + user.clone(), + &factory_instance, + vec![ + Asset { + info: asset_infos[0].clone(), + amount: Uint128::from(500_000_000_000u128), + }, + Asset { + info: asset_infos[1].clone(), + amount: Uint128::from(400_000_000_000u128), + }, + Asset { + info: asset_infos[2].clone(), + amount: Uint128::from(300_000_000_000u128), + }, + ], + ); + router.update_block(next_day); + let pair_info: PairInfo = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: factory_instance.clone().to_string(), + msg: to_binary(&astroport::factory::QueryMsg::Pair { + asset_infos: asset_infos.clone(), + }) + .unwrap(), + })) + .unwrap(); + + change_provide_liquidity( + &mut router, + owner.clone(), + user.clone(), + pair_info.contract_addr.clone(), + vec![ + ( + usdc_token_instance.clone(), + Uint128::from(500_000_000_000u128), + ), + ( + astro_token_instance.clone(), + Uint128::from(400_000_000_000u128), + ), + ( + usdt_token_instance.clone(), + Uint128::from(300_000_000_000u128), + ), + ], + ); + router.update_block(next_day); + + let msg = InstantiateMsg { + factory_contract: factory_instance.to_string(), + asset_infos, + period: 86400, + }; + let oracle_instance = router + .instantiate_contract( + oracle_code_id, + owner.clone(), + &msg, + &[], + String::from("ORACLE"), + None, + ) + .unwrap(); + + let e = router + .execute_contract( + owner.clone(), + oracle_instance.clone(), + &ExecuteMsg::Update {}, + &[], + ) + .unwrap_err(); + assert_eq!(e.root_cause().to_string(), "Period not elapsed"); + router.update_block(next_day); + router + .execute_contract( + owner.clone(), + oracle_instance.clone(), + &ExecuteMsg::Update {}, + &[], + ) + .unwrap(); + + // Change pair liquidity + for (amount1, amount2, amount3) in [ + ( + Uint128::from(500_000_000_000u128), + Uint128::from(400_000_000_000u128), + Uint128::from(300_000_000_000u128), + ), + ( + Uint128::from(500_000_000_000u128), + Uint128::from(400_000_000_000u128), + Uint128::from(300_000_000_000u128), + ), + ] { + change_provide_liquidity( + &mut router, + owner.clone(), + user.clone(), + pair_info.contract_addr.clone(), + vec![ + (usdc_token_instance.clone(), amount1), + (astro_token_instance.clone(), amount2), + (usdt_token_instance.clone(), amount3), + ], + ); + router.update_block(next_day); + router + .execute_contract( + owner.clone(), + oracle_instance.clone(), + &ExecuteMsg::Update {}, + &[], + ) + .unwrap(); + } + for (addr, amount, amounts_exp) in [ + ( + usdc_token_instance.clone(), + Uint128::from(1000u128), + vec![ + ( + AssetInfo::Token { + contract_addr: astro_token_instance.clone(), + }, + Uint128::from(997u128), + ), + ( + AssetInfo::Token { + contract_addr: usdt_token_instance.clone(), + }, + Uint128::from(994u128), + ), + ], + ), + ( + astro_token_instance.clone(), + Uint128::from(1000u128), + vec![ + ( + AssetInfo::Token { + contract_addr: usdc_token_instance.clone(), + }, + Uint128::from(1002u128), + ), + ( + AssetInfo::Token { + contract_addr: usdt_token_instance.clone(), + }, + Uint128::from(996u128), + ), + ], + ), + ( + usdt_token_instance.clone(), + Uint128::from(1000u128), + vec![ + ( + AssetInfo::Token { + contract_addr: usdc_token_instance.clone(), + }, + Uint128::from(1005u128), + ), + ( + AssetInfo::Token { + contract_addr: astro_token_instance.clone(), + }, + Uint128::from(1003u128), + ), + ], + ), + ] { + let msg = Consult { + token: AssetInfo::Token { + contract_addr: addr, + }, + amount, + }; + let res: Vec<(AssetInfo, Uint128)> = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: oracle_instance.to_string(), + msg: to_binary(&msg).unwrap(), + })) + .unwrap(); + assert_eq!(res, amounts_exp); + } + + // Change pair liquidity + for (amount1, amount2, amount3) in [ + ( + Uint128::from(100_000_000_000u128), + Uint128::from(95_000_000_000u128), + Uint128::from(100_000_000_000u128), + ), + ( + Uint128::from(100_000_000_000u128), + Uint128::from(95_000_000_000u128), + Uint128::from(100_000_000_000u128), + ), + ( + Uint128::from(100_000_000_000u128), + Uint128::from(95_000_000_000u128), + Uint128::from(100_000_000_000u128), + ), + ] { + change_provide_liquidity( + &mut router, + owner.clone(), + user.clone(), + pair_info.contract_addr.clone(), + vec![ + (usdc_token_instance.clone(), amount1), + (astro_token_instance.clone(), amount2), + (usdt_token_instance.clone(), amount3), + ], + ); + router.update_block(next_day); + router + .execute_contract( + owner.clone(), + oracle_instance.clone(), + &ExecuteMsg::Update {}, + &[], + ) + .unwrap(); + } + for (addr, amount, amount_exp) in [ + ( + usdc_token_instance.clone(), + Uint128::from(1000u128), + vec![ + ( + AssetInfo::Token { + contract_addr: astro_token_instance.clone(), + }, + Uint128::from(998u128), + ), + ( + AssetInfo::Token { + contract_addr: usdt_token_instance.clone(), + }, + Uint128::from(995u128), + ), + ], + ), + ( + astro_token_instance.clone(), + Uint128::from(1000u128), + vec![ + ( + AssetInfo::Token { + contract_addr: usdc_token_instance.clone(), + }, + Uint128::from(1001u128), + ), + ( + AssetInfo::Token { + contract_addr: usdt_token_instance.clone(), + }, + Uint128::from(997u128), + ), + ], + ), + ( + usdt_token_instance, + Uint128::from(1000u128), + vec![ + ( + AssetInfo::Token { + contract_addr: usdc_token_instance, + }, + Uint128::from(1004u128), + ), + ( + AssetInfo::Token { + contract_addr: astro_token_instance, + }, + Uint128::from(1002u128), + ), + ], + ), + ] { + let msg = Consult { + token: AssetInfo::Token { + contract_addr: addr, + }, + amount, + }; + let res: Vec<(AssetInfo, Uint128)> = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: oracle_instance.to_string(), + msg: to_binary(&msg).unwrap(), + })) + .unwrap(); + assert_eq!(res, amount_exp); + } +} + +#[test] +fn twap_at_height_multiple_assets() { + let mut router = mock_app(None, None); + let owner = Addr::unchecked("owner"); + let user = Addr::unchecked("user0000"); + let mut second_tracked_block: u64 = 0; + let mut third_tracked_block: u64 = 0; + let (astro_token_instance, factory_instance, oracle_code_id) = + instantiate_contracts(&mut router, owner.clone()); + + let usdc_token_instance = instantiate_token( + &mut router, + owner.clone(), + "Usdc token".to_string(), + "USDC".to_string(), + ); + + let usdt_token_instance = instantiate_token( + &mut router, + owner.clone(), + "Usdt token".to_string(), + "USDT".to_string(), + ); + + let asset_infos = vec![ + AssetInfo::Token { + contract_addr: usdc_token_instance.clone(), + }, + AssetInfo::Token { + contract_addr: astro_token_instance.clone(), + }, + AssetInfo::Token { + contract_addr: usdt_token_instance.clone(), + }, + ]; + create_pair_stable( + &mut router, + owner.clone(), + user.clone(), + &factory_instance, + vec![ + Asset { + info: asset_infos[0].clone(), + amount: Uint128::from(500_000_000_000u128), + }, + Asset { + info: asset_infos[1].clone(), + amount: Uint128::from(400_000_000_000u128), + }, + Asset { + info: asset_infos[2].clone(), + amount: Uint128::from(300_000_000_000u128), + }, + ], + ); + router.update_block(next_day); + let pair_info: PairInfo = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: factory_instance.clone().to_string(), + msg: to_binary(&astroport::factory::QueryMsg::Pair { + asset_infos: asset_infos.clone(), + }) + .unwrap(), + })) + .unwrap(); + + change_provide_liquidity( + &mut router, + owner.clone(), + user.clone(), + pair_info.contract_addr.clone(), + vec![ + ( + usdc_token_instance.clone(), + Uint128::from(500_000_000_000u128), + ), + ( + astro_token_instance.clone(), + Uint128::from(400_000_000_000u128), + ), + ( + usdt_token_instance.clone(), + Uint128::from(300_000_000_000u128), + ), + ], + ); + router.update_block(next_day); + + let msg = InstantiateMsg { + factory_contract: factory_instance.to_string(), + asset_infos, + period: 86400, + }; + let oracle_instance = router + .instantiate_contract( + oracle_code_id, + owner.clone(), + &msg, + &[], + String::from("ORACLE"), + None, + ) + .unwrap(); + + let e = router + .execute_contract( + owner.clone(), + oracle_instance.clone(), + &ExecuteMsg::Update {}, + &[], + ) + .unwrap_err(); + assert_eq!(e.root_cause().to_string(), "Period not elapsed"); + router.update_block(next_day); + + let first_tracked_block = router.block_info().height; + router + .execute_contract( + owner.clone(), + oracle_instance.clone(), + &ExecuteMsg::Update {}, + &[], + ) + .unwrap(); + assert_eq!(router.block_info().height, first_tracked_block); + + // Change pair liquidity + for (amount1, amount2, amount3) in [ + ( + Uint128::from(500_000_000_000u128), + Uint128::from(400_000_000_000u128), + Uint128::from(300_000_000_000u128), + ), + ( + Uint128::from(500_000_000_000u128), + Uint128::from(400_000_000_000u128), + Uint128::from(300_000_000_000u128), + ), + ] { + change_provide_liquidity( + &mut router, + owner.clone(), + user.clone(), + pair_info.contract_addr.clone(), + vec![ + (usdc_token_instance.clone(), amount1), + (astro_token_instance.clone(), amount2), + (usdt_token_instance.clone(), amount3), + ], + ); + router.update_block(next_day); + second_tracked_block = router.block_info().height; + router + .execute_contract( + owner.clone(), + oracle_instance.clone(), + &ExecuteMsg::Update {}, + &[], + ) + .unwrap(); + } + + // Change pair liquidity + for (amount1, amount2, amount3) in [ + ( + Uint128::from(100_000_000_000_u128), + Uint128::from(95_000_000_000_u128), + Uint128::from(100_000_000_000_u128), + ), + ( + Uint128::from(100_000_000_000_u128), + Uint128::from(95_000_000_000_u128), + Uint128::from(100_000_000_000_u128), + ), + ( + Uint128::from(100_000_000_000_u128), + Uint128::from(95_000_000_000_u128), + Uint128::from(100_000_000_000_u128), + ), + ] { + change_provide_liquidity( + &mut router, + owner.clone(), + user.clone(), + pair_info.contract_addr.clone(), + vec![ + (usdc_token_instance.clone(), amount1), + (astro_token_instance.clone(), amount2), + (usdt_token_instance.clone(), amount3), + ], + ); + router.update_block(next_day); + router + .execute_contract( + owner.clone(), + oracle_instance.clone(), + &ExecuteMsg::Update {}, + &[], + ) + .unwrap(); + third_tracked_block = router.block_info().height; + } + for (addr, amount_exp) in [ + ( + usdc_token_instance.clone(), + vec![ + ( + AssetInfo::Token { + contract_addr: astro_token_instance.clone(), + }, + Decimal256::from_str("0.998123").unwrap(), + ), + ( + AssetInfo::Token { + contract_addr: usdt_token_instance.clone(), + }, + Decimal256::from_str("0.995465").unwrap(), + ), + ], + ), + ( + astro_token_instance.clone(), + vec![ + ( + AssetInfo::Token { + contract_addr: usdc_token_instance.clone(), + }, + Decimal256::from_str("1.001881").unwrap(), + ), + ( + AssetInfo::Token { + contract_addr: usdt_token_instance.clone(), + }, + Decimal256::from_str("0.997337").unwrap(), + ), + ], + ), + ( + usdt_token_instance.clone(), + vec![ + ( + AssetInfo::Token { + contract_addr: usdc_token_instance.clone(), + }, + Decimal256::from_str("1.004556").unwrap(), + ), + ( + AssetInfo::Token { + contract_addr: astro_token_instance.clone(), + }, + Decimal256::from_str("1.002671").unwrap(), + ), + ], + ), + ] { + let msg = TWAPAtHeight { + token: AssetInfo::Token { + contract_addr: addr.clone(), + }, + height: Uint64::from(first_tracked_block), + }; + let res: Vec<(AssetInfo, Decimal256)> = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: oracle_instance.to_string(), + msg: to_binary(&msg).unwrap(), + })) + .unwrap(); + assert_eq!(res, amount_exp); + } + + for (addr, amount_exp) in [ + ( + usdc_token_instance.clone(), + vec![ + ( + AssetInfo::Token { + contract_addr: astro_token_instance.clone(), + }, + Decimal256::from_str("0.997892").unwrap(), + ), + ( + AssetInfo::Token { + contract_addr: usdt_token_instance.clone(), + }, + Decimal256::from_str("0.994397").unwrap(), + ), + ], + ), + ( + astro_token_instance.clone(), + vec![ + ( + AssetInfo::Token { + contract_addr: usdc_token_instance.clone(), + }, + Decimal256::from_str("1.002114").unwrap(), + ), + ( + AssetInfo::Token { + contract_addr: usdt_token_instance.clone(), + }, + Decimal256::from_str("0.996498").unwrap(), + ), + ], + ), + ( + usdt_token_instance.clone(), + vec![ + ( + AssetInfo::Token { + contract_addr: usdc_token_instance.clone(), + }, + Decimal256::from_str("1.005637").unwrap(), + ), + ( + AssetInfo::Token { + contract_addr: astro_token_instance.clone(), + }, + Decimal256::from_str("1.003516").unwrap(), + ), + ], + ), + ] { + let msg = TWAPAtHeight { + token: AssetInfo::Token { + contract_addr: addr.clone(), + }, + height: Uint64::from(second_tracked_block), + }; + let res: Vec<(AssetInfo, Decimal256)> = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: oracle_instance.to_string(), + msg: to_binary(&msg).unwrap(), + })) + .unwrap(); + assert_eq!(res, amount_exp); + } + + for (addr, amount_exp) in [ + ( + usdc_token_instance.clone(), + vec![ + ( + AssetInfo::Token { + contract_addr: astro_token_instance.clone(), + }, + Decimal256::from_str("0.998055").unwrap(), + ), + ( + AssetInfo::Token { + contract_addr: usdt_token_instance.clone(), + }, + Decimal256::from_str("0.995160").unwrap(), + ), + ], + ), + ( + astro_token_instance.clone(), + vec![ + ( + AssetInfo::Token { + contract_addr: usdc_token_instance.clone(), + }, + Decimal256::from_str("1.001950").unwrap(), + ), + ( + AssetInfo::Token { + contract_addr: usdt_token_instance.clone(), + }, + Decimal256::from_str("0.997100").unwrap(), + ), + ], + ), + ( + usdt_token_instance, + vec![ + ( + AssetInfo::Token { + contract_addr: usdc_token_instance, + }, + Decimal256::from_str("1.004864").unwrap(), + ), + ( + AssetInfo::Token { + contract_addr: astro_token_instance, + }, + Decimal256::from_str("1.002909").unwrap(), + ), + ], + ), + ] { + let msg = TWAPAtHeight { + token: AssetInfo::Token { + contract_addr: addr.clone(), + }, + height: Uint64::from(third_tracked_block), + }; + let res: Vec<(AssetInfo, Decimal256)> = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: oracle_instance.to_string(), + msg: to_binary(&msg).unwrap(), + })) + .unwrap(); + assert_eq!(res, amount_exp); + } +} + +#[test] +fn twap_at_height_multiple_assets_non_accurate_heights() { + let mut router = mock_app(None, None); + let owner = Addr::unchecked("owner"); + let user = Addr::unchecked("user0000"); + let mut second_tracked_block: u64 = 0; + let mut third_tracked_block: u64 = 0; + let (astro_token_instance, factory_instance, oracle_code_id) = + instantiate_contracts(&mut router, owner.clone()); + + let usdc_token_instance = instantiate_token( + &mut router, + owner.clone(), + "Usdc token".to_string(), + "USDC".to_string(), + ); + + let usdt_token_instance = instantiate_token( + &mut router, + owner.clone(), + "Usdt token".to_string(), + "USDT".to_string(), + ); + + let asset_infos = vec![ + AssetInfo::Token { + contract_addr: usdc_token_instance.clone(), + }, + AssetInfo::Token { + contract_addr: astro_token_instance.clone(), + }, + AssetInfo::Token { + contract_addr: usdt_token_instance.clone(), + }, + ]; + create_pair_stable( + &mut router, + owner.clone(), + user.clone(), + &factory_instance, + vec![ + Asset { + info: asset_infos[0].clone(), + amount: Uint128::from(500_000_000_000u128), + }, + Asset { + info: asset_infos[1].clone(), + amount: Uint128::from(400_000_000_000u128), + }, + Asset { + info: asset_infos[2].clone(), + amount: Uint128::from(300_000_000_000u128), + }, + ], + ); + router.update_block(next_day); + let pair_info: PairInfo = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: factory_instance.clone().to_string(), + msg: to_binary(&astroport::factory::QueryMsg::Pair { + asset_infos: asset_infos.clone(), + }) + .unwrap(), + })) + .unwrap(); + + change_provide_liquidity( + &mut router, + owner.clone(), + user.clone(), + pair_info.contract_addr.clone(), + vec![ + ( + usdc_token_instance.clone(), + Uint128::from(500_000_000_000u128), + ), + ( + astro_token_instance.clone(), + Uint128::from(400_000_000_000u128), + ), + ( + usdt_token_instance.clone(), + Uint128::from(300_000_000_000u128), + ), + ], + ); + router.update_block(next_day); + + let msg = InstantiateMsg { + factory_contract: factory_instance.to_string(), + asset_infos, + period: 86400, + }; + let oracle_instance = router + .instantiate_contract( + oracle_code_id, + owner.clone(), + &msg, + &[], + String::from("ORACLE"), + None, + ) + .unwrap(); + + let e = router + .execute_contract( + owner.clone(), + oracle_instance.clone(), + &ExecuteMsg::Update {}, + &[], + ) + .unwrap_err(); + assert_eq!(e.root_cause().to_string(), "Period not elapsed"); + router.update_block(next_day); + + let first_tracked_block = router.block_info().height; + router + .execute_contract( + owner.clone(), + oracle_instance.clone(), + &ExecuteMsg::Update {}, + &[], + ) + .unwrap(); + assert_eq!(router.block_info().height, first_tracked_block); + + // Change pair liquidity + for (amount1, amount2, amount3) in [ + ( + Uint128::from(500_000_000_000u128), + Uint128::from(400_000_000_000u128), + Uint128::from(300_000_000_000u128), + ), + ( + Uint128::from(500_000_000_000u128), + Uint128::from(400_000_000_000u128), + Uint128::from(300_000_000_000u128), + ), + ] { + change_provide_liquidity( + &mut router, + owner.clone(), + user.clone(), + pair_info.contract_addr.clone(), + vec![ + (usdc_token_instance.clone(), amount1), + (astro_token_instance.clone(), amount2), + (usdt_token_instance.clone(), amount3), + ], + ); + router.update_block(next_day); + second_tracked_block = router.block_info().height; + router + .execute_contract( + owner.clone(), + oracle_instance.clone(), + &ExecuteMsg::Update {}, + &[], + ) + .unwrap(); + } + + // Change pair liquidity + for (amount1, amount2, amount3) in [ + ( + Uint128::from(100_000_000_000_u128), + Uint128::from(95_000_000_000_u128), + Uint128::from(100_000_000_000_u128), + ), + ( + Uint128::from(100_000_000_000_u128), + Uint128::from(95_000_000_000_u128), + Uint128::from(100_000_000_000_u128), + ), + ( + Uint128::from(100_000_000_000_u128), + Uint128::from(95_000_000_000_u128), + Uint128::from(100_000_000_000_u128), + ), + ] { + change_provide_liquidity( + &mut router, + owner.clone(), + user.clone(), + pair_info.contract_addr.clone(), + vec![ + (usdc_token_instance.clone(), amount1), + (astro_token_instance.clone(), amount2), + (usdt_token_instance.clone(), amount3), + ], + ); + router.update_block(next_day); + router + .execute_contract( + owner.clone(), + oracle_instance.clone(), + &ExecuteMsg::Update {}, + &[], + ) + .unwrap(); + third_tracked_block = router.block_info().height; + } + for (addr, amount_exp) in [ + ( + usdc_token_instance.clone(), + vec![ + ( + AssetInfo::Token { + contract_addr: astro_token_instance.clone(), + }, + Decimal256::from_str("0.998123").unwrap(), + ), + ( + AssetInfo::Token { + contract_addr: usdt_token_instance.clone(), + }, + Decimal256::from_str("0.995465").unwrap(), + ), + ], + ), + ( + astro_token_instance.clone(), + vec![ + ( + AssetInfo::Token { + contract_addr: usdc_token_instance.clone(), + }, + Decimal256::from_str("1.001881").unwrap(), + ), + ( + AssetInfo::Token { + contract_addr: usdt_token_instance.clone(), + }, + Decimal256::from_str("0.997337").unwrap(), + ), + ], + ), + ( + usdt_token_instance.clone(), + vec![ + ( + AssetInfo::Token { + contract_addr: usdc_token_instance.clone(), + }, + Decimal256::from_str("1.004556").unwrap(), + ), + ( + AssetInfo::Token { + contract_addr: astro_token_instance.clone(), + }, + Decimal256::from_str("1.002671").unwrap(), + ), + ], + ), + ] { + let msg = TWAPAtHeight { + token: AssetInfo::Token { + contract_addr: addr.clone(), + }, + // first_tracked_block is a staring point for snapshot + height: Uint64::from(first_tracked_block), + }; + let res: Vec<(AssetInfo, Decimal256)> = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: oracle_instance.to_string(), + msg: to_binary(&msg).unwrap(), + })) + .unwrap(); + assert_eq!(res, amount_exp); + } + + for (addr, amount_exp) in [ + ( + usdc_token_instance.clone(), + vec![ + ( + AssetInfo::Token { + contract_addr: astro_token_instance.clone(), + }, + Decimal256::from_str("0.997892").unwrap(), + ), + ( + AssetInfo::Token { + contract_addr: usdt_token_instance.clone(), + }, + Decimal256::from_str("0.994397").unwrap(), + ), + ], + ), + ( + astro_token_instance.clone(), + vec![ + ( + AssetInfo::Token { + contract_addr: usdc_token_instance.clone(), + }, + Decimal256::from_str("1.002114").unwrap(), + ), + ( + AssetInfo::Token { + contract_addr: usdt_token_instance.clone(), + }, + Decimal256::from_str("0.996498").unwrap(), + ), + ], + ), + ( + usdt_token_instance.clone(), + vec![ + ( + AssetInfo::Token { + contract_addr: usdc_token_instance.clone(), + }, + Decimal256::from_str("1.005637").unwrap(), + ), + ( + AssetInfo::Token { + contract_addr: astro_token_instance.clone(), + }, + Decimal256::from_str("1.003516").unwrap(), + ), + ], + ), + ] { + let msg = TWAPAtHeight { + token: AssetInfo::Token { + contract_addr: addr.clone(), + }, + height: Uint64::from(second_tracked_block - 100), + }; + let res: Vec<(AssetInfo, Decimal256)> = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: oracle_instance.to_string(), + msg: to_binary(&msg).unwrap(), + })) + .unwrap(); + assert_eq!(res, amount_exp); + } + + for (addr, amount_exp) in [ + ( + usdc_token_instance.clone(), + vec![ + ( + AssetInfo::Token { + contract_addr: astro_token_instance.clone(), + }, + Decimal256::from_str("0.998055").unwrap(), + ), + ( + AssetInfo::Token { + contract_addr: usdt_token_instance.clone(), + }, + Decimal256::from_str("0.995160").unwrap(), + ), + ], + ), + ( + astro_token_instance.clone(), + vec![ + ( + AssetInfo::Token { + contract_addr: usdc_token_instance.clone(), + }, + Decimal256::from_str("1.001950").unwrap(), + ), + ( + AssetInfo::Token { + contract_addr: usdt_token_instance.clone(), + }, + Decimal256::from_str("0.997100").unwrap(), + ), + ], + ), + ( + usdt_token_instance, + vec![ + ( + AssetInfo::Token { + contract_addr: usdc_token_instance, + }, + Decimal256::from_str("1.004864").unwrap(), + ), + ( + AssetInfo::Token { + contract_addr: astro_token_instance, + }, + Decimal256::from_str("1.002909").unwrap(), + ), + ], + ), + ] { + let msg = TWAPAtHeight { + token: AssetInfo::Token { + contract_addr: addr.clone(), + }, + // should return TWAP for last block we calculated it. in this case for third_tracked_block + height: Uint64::from(third_tracked_block + 5), + }; + let res: Vec<(AssetInfo, Decimal256)> = router + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: oracle_instance.to_string(), + msg: to_binary(&msg).unwrap(), + })) + .unwrap(); + assert_eq!(res, amount_exp); + } +} diff --git a/contracts/astroport/pair/.cargo/config b/contracts/astroport/pair/.cargo/config new file mode 100644 index 00000000..6a35afd0 --- /dev/null +++ b/contracts/astroport/pair/.cargo/config @@ -0,0 +1,6 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib" +integration-test = "test --test integration" +schema = "run --example pair_schema" diff --git a/contracts/astroport/pair/.editorconfig b/contracts/astroport/pair/.editorconfig new file mode 100644 index 00000000..3d36f20b --- /dev/null +++ b/contracts/astroport/pair/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.rs] +indent_size = 4 diff --git a/contracts/astroport/pair/Cargo.toml b/contracts/astroport/pair/Cargo.toml new file mode 100644 index 00000000..d7484a09 --- /dev/null +++ b/contracts/astroport/pair/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "astroport-pair" +version = "1.0.0" +authors = ["Astroport"] +edition = "2021" +description = "The Astroport constant product pool contract implementation" +license = "MIT" + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] + +[dependencies] +integer-sqrt = "0.1.5" +astroport = { path = "../../../packages/astroport", default-features = false} +cw2 = { version = "0.15" } +cw20 = { version = "0.15" } +cosmwasm-std = { version = "1.1" } +cw-storage-plus = "0.15" +thiserror = { version = "1.0" } +protobuf = { version = "2", features = ["with-bytes"] } +cosmwasm-schema = "1.1" + +[dev-dependencies] +astroport-token = {path = "../token" } +cw-multi-test = "0.15" +astroport-factory = {path = "../factory" } +proptest = "1.0" diff --git a/contracts/astroport/pair/README.md b/contracts/astroport/pair/README.md new file mode 100644 index 00000000..a41a7771 --- /dev/null +++ b/contracts/astroport/pair/README.md @@ -0,0 +1,284 @@ +# Astroport Constant Product Pair + +The constant product pool uses the widely known xy=k formula. More details around how the pool functions can be found [here](https://docs.astroport.fi/astroport/astroport/astro-pools/constant-product-pools). + +--- + +## Liquidity Providers + +A user can provide liquidity to a constant product pool by calling `provide_liquidity`. Users can also withdraw liquidity by calling `withdraw_liquidity`. + +Whenever liquidity is deposited into a pool, special tokens known as "liquidity tokens" are minted to the provider’s address, in proportion to how much liquidity they contributed to the pool. These tokens are a representation of a liquidity provider’s contribution to a pool. Whenever a trade occurs, the `lp_commission` is distributed pro-rata to all LPs in the pool at the moment of the trade. To receive the underlying liquidity back plus accrued LP fees, LPs must burn their liquidity tokens. + +When providing liquidity from a smart contract, the most important thing to keep in mind is that the amount of tokens deposited into a pool and the amount of tokens withdrawn later from the pool will most likely not be the same. This is because of the way constant product pools work where, as the token prices in the pool change, so do the respective token amounts that a LP can withdraw. + +As an example, let's say the global ratio between two tokens x:y is 10:2 (i.e. 1 x = 0.2 y), but the current ratio between the tokens in an Astroport pair is 5:2 (1 x = 0.4 y). Let's also say that someone may decide to LP in the x:y Astroport pool at the current 5:2 ratio. As the Astroport pool gets arbitraged to the global ratio, the amount of x & y tokens that the LP can withdraw changes because the total amounts of x & y tokens in the pool also change. + +> Note that before executing the `provide_liqudity` operation, a user must allow the pool contract to take tokens from their wallet + +### Slippage Tolerance for Providing Liquidity + +If a user specifies a slippage tolerance when they provide liquidity in a constant product pool, the pool contract makes sure that the transaction goes through only if the pool price does not change more than tolerance. + +As an example, let's say someone LPs in a pool and specifies a 1% slippage tolerance. The user LPs 200 UST and 1 `ASSET`. With a 1% slippage tolerance, `amountUSTMin` (the minimum amount of UST to LP) should be set to 198 UST, and `amountASSETMin` (the minimum amount of `ASSET` to LP) should be set to .99 `ASSET`. This means that, in a worst case scenario, liquidity will be added at a pool rate of 198 `ASSET`/1 UST or 202.02 UST/1 `ASSET` (200 UST + .99 `ASSET`). If the contract cannot add liquidity within these bounds (because the pool ratio changed more than the tolerance), the transaction will revert. + +## Traders + +### Slippage Tolerance for Swaps + +Astroport has two options to protect traders against slippage during swaps: + +1. Providing `max_spread` +The spread is calculated as the difference between the ask amount (using the constant pool price) before and after the swap operation. Once `max_spread` is set, it will be compared against the actual swap spread. In case the swap spread exceeds the provided max limit, the swap will fail. + +Note that the spread is calculated before commission deduction in order to properly represent the pool's ratio change. + +2. Providing `max_spread` + `belief_price` +If `belief_price` is provided in combination with `max_spread`, the pool will check the difference between the return amount (using `belief_price`) and the real pool price. + +Please note that Astroport has the default value for the spread set to 0.5% and the max allowed spread set to 50%. + +## InstantiateMsg + +Initializes a new x*y=k pair. + +```json +{ + "token_code_id": 123, + "factory_addr": "terra...", + "asset_infos": [ + { + "token": { + "contract_addr": "terra..." + } + }, + { + "native_token": { + "denom": "uusd" + } + } + ], + "init_params": "" +} +``` + +## ExecuteMsg + +### `receive` + +Withdraws liquidity or assets that were swapped to (ask assets in a swap operation). + +```json +{ + "receive": { + "sender": "terra...", + "amount": "123", + "msg": "" + } +} +``` + +### `provide_liquidity` + +Provides liquidity by sending a user's native or token assets to the pool. + +__NOTE__: you should increase your token allowance for the pool before providing liquidity! + +1. Providing Liquidity Without Specifying Slippage Tolerance + +```json + { + "provide_liquidity": { + "assets": [ + { + "info": { + "token": { + "contract_addr": "terra..." + } + }, + "amount": "1000000" + }, + { + "info": { + "native_token": { + "denom": "uusd" + } + }, + "amount": "1000000" + } + ], + "auto_stake": false, + "receiver": "terra..." + } + } +``` + +2. Providing Liquidity With Slippage Tolerance + + ```json + { + "provide_liquidity": { + "assets": [ + { + "info": { + "token": { + "contract_addr": "terra..." + } + }, + "amount": "1000000" + }, + { + "info": { + "native_token": { + "denom": "uusd" + } + }, + "amount": "1000000" + } + ], + "slippage_tolerance": "0.01", + "auto_stake": false, + "receiver": "terra..." + } + } +``` + +### `withdraw_liquidity` + +Burn LP tokens and withdraw liquidity from a pool. This call must be sent to a LP token contract associated with the pool from which you want to withdraw liquidity from. + +```json + { + "withdraw_liquidity": {} + } +``` + +### `swap` + +Perform a swap. `offer_asset` is your source asset and `to` is the address that will receive the ask assets. All fields are optional except `offer_asset`. + +NOTE: You should increase token allowance before swap. + +```json + { + "swap": { + "offer_asset": { + "info": { + "native_token": { + "denom": "uluna" + } + }, + "amount": "123" + }, + "belief_price": "123", + "max_spread": "123", + "to": "terra..." + } + } +``` + +### `update_config` + +The contract configuration cannot be updated. + +```json + { + "update_config": { + "params": "" + } + } +``` + +## QueryMsg + +All query messages are described below. A custom struct is defined for each query response. + +### `pair` + +Retrieve a pair's configuration (type, assets traded in it etc) + +```json +{ + "pair": {} +} +``` + +### `pool` + +Returns the amount of tokens in the pool for all assets as well as the amount of LP tokens issued. + +```json +{ + "pool": {} +} +``` + +### `config` + +Get the pair contract configuration. + +```json +{ + "config": {} +} +``` + +### `share` + +Return the amount of assets someone would get from the pool if they were to burn a specific amount of LP tokens. + +```json +{ + "share": { + "amount": "123" + } +} +``` + +### `simulation` + +Simulates a swap and returns the spread and commission amounts. + +```json +{ + "simulation": { + "offer_asset": { + "info": { + "native_token": { + "denom": "uusd" + } + }, + "amount": "1000000" + } + } +} +``` + +### `reverse_simulation` + +Reverse simulates a swap (specifies the ask instead of the offer) and returns the offer amount, spread and commission. + +```json +{ + "reverse_simulation": { + "ask_asset": { + "info": { + "token": { + "contract_addr": "terra..." + } + }, + "amount": "1000000" + } + } +} +``` + +### `cumulative_prices` + +Returns the cumulative prices for the assets in the pair. + +```json +{ + "cumulative_prices": {} +} +``` diff --git a/contracts/astroport/pair/rustfmt.toml b/contracts/astroport/pair/rustfmt.toml new file mode 100644 index 00000000..11a85e6a --- /dev/null +++ b/contracts/astroport/pair/rustfmt.toml @@ -0,0 +1,15 @@ +# stable +newline_style = "unix" +hard_tabs = false +tab_spaces = 4 + +# unstable... should we require `rustup run nightly cargo fmt` ? +# or just update the style guide when they are stable? +#fn_single_line = true +#format_code_in_doc_comments = true +#overflow_delimited_expr = true +#reorder_impl_items = true +#struct_field_align_threshold = 20 +#struct_lit_single_line = true +#report_todo = "Always" + diff --git a/contracts/astroport/pair/src/contract.rs b/contracts/astroport/pair/src/contract.rs new file mode 100644 index 00000000..e2a0d46b --- /dev/null +++ b/contracts/astroport/pair/src/contract.rs @@ -0,0 +1,1126 @@ +use crate::error::ContractError; +use crate::state::{Config, CONFIG}; + +use cosmwasm_std::{ + attr, entry_point, from_binary, to_binary, Addr, Binary, CosmosMsg, Decimal, Decimal256, Deps, + DepsMut, Env, MessageInfo, QuerierWrapper, Reply, ReplyOn, Response, StdError, StdResult, + SubMsg, Uint128, Uint256, WasmMsg, +}; + +use crate::response::MsgInstantiateContractResponse; +use astroport::asset::{ + addr_opt_validate, addr_validate_to_lower, check_swap_parameters, format_lp_token_name, Asset, + AssetInfo, PairInfo, MINIMUM_LIQUIDITY_AMOUNT, +}; +use astroport::decimal2decimal256; +use astroport::factory::PairType; +use astroport::generator::Cw20HookMsg as GeneratorHookMsg; +use astroport::pair::{migration_check, ConfigResponse, DEFAULT_SLIPPAGE, MAX_ALLOWED_SLIPPAGE}; +use astroport::pair::{ + CumulativePricesResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, MigrateMsg, PoolResponse, + QueryMsg, ReverseSimulationResponse, SimulationResponse, TWAP_PRECISION, +}; +use astroport::querier::{query_factory_config, query_fee_info, query_supply}; +use astroport::{token::InstantiateMsg as TokenInstantiateMsg, U256}; +use cw2::set_contract_version; +use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg, MinterResponse}; +use protobuf::Message; +use std::str::FromStr; +use std::vec; + +/// Contract name that is used for migration. +const CONTRACT_NAME: &str = "astroport-pair"; +/// Contract version that is used for migration. +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +/// A `reply` call code ID used for sub-messages. +const INSTANTIATE_TOKEN_REPLY_ID: u64 = 1; + +/// Creates a new contract with the specified parameters in the [`InstantiateMsg`]. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + if msg.asset_infos.len() != 2 { + return Err(StdError::generic_err("asset_infos must contain exactly two elements").into()); + } + + msg.asset_infos[0].check(deps.api)?; + msg.asset_infos[1].check(deps.api)?; + + if msg.asset_infos[0] == msg.asset_infos[1] { + return Err(ContractError::DoublingAssets {}); + } + + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + let config = Config { + pair_info: PairInfo { + contract_addr: env.contract.address.clone(), + liquidity_token: Addr::unchecked(""), + asset_infos: msg.asset_infos.clone(), + pair_type: PairType::Xyk {}, + }, + factory_addr: addr_validate_to_lower(deps.api, msg.factory_addr.as_str())?, + block_time_last: 0, + price0_cumulative_last: Uint128::zero(), + price1_cumulative_last: Uint128::zero(), + }; + + CONFIG.save(deps.storage, &config)?; + + let token_name = format_lp_token_name(&msg.asset_infos, &deps.querier)?; + + // Create the LP token contract + let sub_msg: Vec = vec![SubMsg { + msg: WasmMsg::Instantiate { + code_id: msg.token_code_id, + msg: to_binary(&TokenInstantiateMsg { + name: token_name, + symbol: "uLP".to_string(), + decimals: 6, + initial_balances: vec![], + mint: Some(MinterResponse { + minter: env.contract.address.to_string(), + cap: None, + }), + marketing: None, + })?, + funds: vec![], + admin: None, + label: String::from("Astroport LP token"), + } + .into(), + id: INSTANTIATE_TOKEN_REPLY_ID, + gas_limit: None, + reply_on: ReplyOn::Success, + }]; + + Ok(Response::new().add_submessages(sub_msg)) +} + +/// The entry point to the contract for processing replies from submessages. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { + let mut config: Config = CONFIG.load(deps.storage)?; + + if config.pair_info.liquidity_token != Addr::unchecked("") { + return Err(ContractError::Unauthorized {}); + } + + let data = msg.result.unwrap().data.unwrap(); + let res: MsgInstantiateContractResponse = + Message::parse_from_bytes(data.as_slice()).map_err(|_| { + StdError::parse_err("MsgInstantiateContractResponse", "failed to parse data") + })?; + + config.pair_info.liquidity_token = + addr_validate_to_lower(deps.api, res.get_contract_address())?; + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new().add_attribute("liquidity_token_addr", config.pair_info.liquidity_token)) +} + +/// Exposes all the execute functions available in the contract. +/// +/// ## Variants +/// * **ExecuteMsg::UpdateConfig { params: Binary }** Not supported. +/// +/// * **ExecuteMsg::Receive(msg)** Receives a message of type [`Cw20ReceiveMsg`] and processes +/// it depending on the received template. +/// +/// * **ExecuteMsg::ProvideLiquidity { +/// assets, +/// slippage_tolerance, +/// auto_stake, +/// receiver, +/// }** Provides liquidity in the pair with the specified input parameters. +/// +/// * **ExecuteMsg::Swap { +/// offer_asset, +/// belief_price, +/// max_spread, +/// to, +/// }** Performs a swap operation with the specified parameters. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + let cfg = CONFIG.load(deps.storage)?; + + if migration_check(deps.querier, &cfg.factory_addr, &env.contract.address)? { + return Err(ContractError::PairIsNotMigrated {}); + } + + match msg { + ExecuteMsg::Receive(msg) => receive_cw20(deps, env, info, msg), + ExecuteMsg::ProvideLiquidity { + assets, + slippage_tolerance, + auto_stake, + receiver, + } => provide_liquidity( + deps, + env, + info, + assets, + slippage_tolerance, + auto_stake, + receiver, + ), + ExecuteMsg::Swap { + offer_asset, + belief_price, + max_spread, + to, + .. + } => { + offer_asset.info.check(deps.api)?; + if !offer_asset.is_native_token() { + return Err(ContractError::Unauthorized {}); + } + + let to_addr = addr_opt_validate(deps.api, &to)?; + + swap( + deps, + env, + info.clone(), + info.sender, + offer_asset, + belief_price, + max_spread, + to_addr, + ) + } + _ => Err(ContractError::NonSupported {}), + } +} + +/// Receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template. +/// +/// * **cw20_msg** is the CW20 message that has to be processed. +pub fn receive_cw20( + deps: DepsMut, + env: Env, + info: MessageInfo, + cw20_msg: Cw20ReceiveMsg, +) -> Result { + match from_binary(&cw20_msg.msg) { + Ok(Cw20HookMsg::Swap { + belief_price, + max_spread, + to, + .. + }) => { + // Only asset contract can execute this message + let mut authorized = false; + let config = CONFIG.load(deps.storage)?; + + for pool in config.pair_info.asset_infos { + if let AssetInfo::Token { contract_addr, .. } = &pool { + if contract_addr == &info.sender { + authorized = true; + } + } + } + + if !authorized { + return Err(ContractError::Unauthorized {}); + } + + let to_addr = addr_opt_validate(deps.api, &to)?; + let contract_addr = info.sender.clone(); + let sender = addr_validate_to_lower(deps.api, cw20_msg.sender)?; + swap( + deps, + env, + info, + sender, + Asset { + info: AssetInfo::Token { contract_addr }, + amount: cw20_msg.amount, + }, + belief_price, + max_spread, + to_addr, + ) + } + Ok(Cw20HookMsg::WithdrawLiquidity { .. }) => { + let sender = addr_validate_to_lower(deps.api, cw20_msg.sender)?; + withdraw_liquidity(deps, env, info, sender, cw20_msg.amount) + } + Err(err) => Err(err.into()), + } +} + +/// Provides liquidity in the pair with the specified input parameters. +/// +/// * **assets** is an array with assets available in the pool. +/// +/// * **slippage_tolerance** is an optional parameter which is used to specify how much +/// the pool price can move until the provide liquidity transaction goes through. +/// +/// * **auto_stake** is an optional parameter which determines whether the LP tokens minted after +/// liquidity provision are automatically staked in the Generator contract on behalf of the LP token receiver. +/// +/// * **receiver** is an optional parameter which defines the receiver of the LP tokens. +/// If no custom receiver is specified, the pair will mint LP tokens for the function caller. +/// +/// NOTE - the address that wants to provide liquidity should approve the pair contract to pull its relevant tokens. +pub fn provide_liquidity( + deps: DepsMut, + env: Env, + info: MessageInfo, + assets: Vec, + slippage_tolerance: Option, + auto_stake: Option, + receiver: Option, +) -> Result { + if assets.len() != 2 { + return Err(StdError::generic_err("asset_infos must contain exactly two elements").into()); + } + assets[0].info.check(deps.api)?; + assets[1].info.check(deps.api)?; + + let auto_stake = auto_stake.unwrap_or(false); + for asset in assets.iter() { + asset.assert_sent_native_token_balance(&info)?; + } + + let mut config = CONFIG.load(deps.storage)?; + let mut pools = config + .pair_info + .query_pools(&deps.querier, &env.contract.address)?; + let deposits = [ + assets + .iter() + .find(|a| a.info.equal(&pools[0].info)) + .map(|a| a.amount) + .expect("Wrong asset info is given"), + assets + .iter() + .find(|a| a.info.equal(&pools[1].info)) + .map(|a| a.amount) + .expect("Wrong asset info is given"), + ]; + + if deposits[0].is_zero() || deposits[1].is_zero() { + return Err(ContractError::InvalidZeroAmount {}); + } + + let mut messages = vec![]; + for (i, pool) in pools.iter_mut().enumerate() { + // If the asset is a token contract, then we need to execute a TransferFrom msg to receive assets + if let AssetInfo::Token { contract_addr, .. } = &pool.info { + messages.push(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract_addr.to_string(), + msg: to_binary(&Cw20ExecuteMsg::TransferFrom { + owner: info.sender.to_string(), + recipient: env.contract.address.to_string(), + amount: deposits[i], + })?, + funds: vec![], + })); + } else { + // If the asset is native token, the pool balance is already increased + // To calculate the total amount of deposits properly, we should subtract the user deposit from the pool + pool.amount = pool.amount.checked_sub(deposits[i])?; + } + } + + let total_share = query_supply(&deps.querier, &config.pair_info.liquidity_token)?; + let share = if total_share.is_zero() { + // Initial share = collateral amount + let share = Uint128::new( + (U256::from(deposits[0].u128()) * U256::from(deposits[1].u128())) + .integer_sqrt() + .as_u128(), + ) + .checked_sub(MINIMUM_LIQUIDITY_AMOUNT) + .map_err(|_| ContractError::MinimumLiquidityAmountError {})?; + + messages.extend(mint_liquidity_token_message( + deps.querier, + &config, + &env.contract.address, + &env.contract.address, + MINIMUM_LIQUIDITY_AMOUNT, + false, + )?); + + // share cannot become zero after minimum liquidity subtraction + if share.is_zero() { + return Err(ContractError::MinimumLiquidityAmountError {}); + } + + share + } else { + // Assert slippage tolerance + assert_slippage_tolerance(slippage_tolerance, &deposits, &pools)?; + + // min(1, 2) + // 1. sqrt(deposit_0 * exchange_rate_0_to_1 * deposit_0) * (total_share / sqrt(pool_0 * pool_0)) + // == deposit_0 * total_share / pool_0 + // 2. sqrt(deposit_1 * exchange_rate_1_to_0 * deposit_1) * (total_share / sqrt(pool_1 * pool_1)) + // == deposit_1 * total_share / pool_1 + std::cmp::min( + deposits[0].multiply_ratio(total_share, pools[0].amount), + deposits[1].multiply_ratio(total_share, pools[1].amount), + ) + }; + + // Mint LP tokens for the sender or for the receiver (if set) + let receiver = addr_opt_validate(deps.api, &receiver)?.unwrap_or_else(|| info.sender.clone()); + messages.extend(mint_liquidity_token_message( + deps.querier, + &config, + &env.contract.address, + &receiver, + share, + auto_stake, + )?); + + // Accumulate prices for the assets in the pool + if let Some((price0_cumulative_new, price1_cumulative_new, block_time)) = + accumulate_prices(env, &config, pools[0].amount, pools[1].amount)? + { + config.price0_cumulative_last = price0_cumulative_new; + config.price1_cumulative_last = price1_cumulative_new; + config.block_time_last = block_time; + CONFIG.save(deps.storage, &config)?; + } + + Ok(Response::new().add_messages(messages).add_attributes(vec![ + attr("action", "provide_liquidity"), + attr("sender", info.sender), + attr("receiver", receiver), + attr("assets", format!("{}, {}", assets[0], assets[1])), + attr("share", share), + ])) +} + +/// Mint LP tokens for a beneficiary and auto stake the tokens in the Generator contract (if auto staking is specified). +/// +/// * **recipient** is the LP token recipient. +/// +/// * **amount** is the amount of LP tokens that will be minted for the recipient. +/// +/// * **auto_stake** determines whether the newly minted LP tokens will +/// be automatically staked in the Generator on behalf of the recipient. +fn mint_liquidity_token_message( + querier: QuerierWrapper, + config: &Config, + contract_address: &Addr, + recipient: &Addr, + amount: Uint128, + auto_stake: bool, +) -> Result, ContractError> { + let lp_token = &config.pair_info.liquidity_token; + + // If no auto-stake - just mint to recipient + if !auto_stake { + return Ok(vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: lp_token.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Mint { + recipient: recipient.to_string(), + amount, + })?, + funds: vec![], + })]); + } + + // Mint for the pair contract and stake into the Generator contract + let generator = query_factory_config(&querier, &config.factory_addr)?.generator_address; + + if let Some(generator) = generator { + Ok(vec![ + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: lp_token.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Mint { + recipient: contract_address.to_string(), + amount, + })?, + funds: vec![], + }), + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: lp_token.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Send { + contract: generator.to_string(), + amount, + msg: to_binary(&GeneratorHookMsg::DepositFor(recipient.clone()))?, + })?, + funds: vec![], + }), + ]) + } else { + Err(ContractError::AutoStakeError {}) + } +} + +/// Withdraw liquidity from the pool. +/// * **sender** is the address that will receive assets back from the pair contract. +/// +/// * **amount** is the amount of LP tokens to burn. +pub fn withdraw_liquidity( + deps: DepsMut, + env: Env, + info: MessageInfo, + sender: Addr, + amount: Uint128, +) -> Result { + let mut config = CONFIG.load(deps.storage).unwrap(); + + if info.sender != config.pair_info.liquidity_token { + return Err(ContractError::Unauthorized {}); + } + + let (pools, total_share) = pool_info(deps.querier, &config)?; + let refund_assets = get_share_in_assets(&pools, amount, total_share); + + // Accumulate prices for the pair assets + if let Some((price0_cumulative_new, price1_cumulative_new, block_time)) = + accumulate_prices(env, &config, pools[0].amount, pools[1].amount)? + { + config.price0_cumulative_last = price0_cumulative_new; + config.price1_cumulative_last = price1_cumulative_new; + config.block_time_last = block_time; + CONFIG.save(deps.storage, &config)?; + } + + // Update the pool info + let messages: Vec = vec![ + refund_assets[0] + .clone() + .into_msg(&deps.querier, sender.clone())?, + refund_assets[1] + .clone() + .into_msg(&deps.querier, sender.clone())?, + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: config.pair_info.liquidity_token.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Burn { amount })?, + funds: vec![], + }), + ]; + + Ok(Response::new().add_messages(messages).add_attributes(vec![ + attr("action", "withdraw_liquidity"), + attr("sender", sender), + attr("withdrawn_share", amount), + attr( + "refund_assets", + format!("{}, {}", refund_assets[0], refund_assets[1]), + ), + ])) +} + +/// Returns the amount of pool assets that correspond to an amount of LP tokens. +/// +/// * **pools** is the array with assets in the pool. +/// +/// * **amount** is amount of LP tokens to compute a corresponding amount of assets for. +/// +/// * **total_share** is the total amount of LP tokens currently minted. +pub fn get_share_in_assets(pools: &[Asset], amount: Uint128, total_share: Uint128) -> Vec { + let mut share_ratio = Decimal::zero(); + if !total_share.is_zero() { + share_ratio = Decimal::from_ratio(amount, total_share); + } + + pools + .iter() + .map(|a| Asset { + info: a.info.clone(), + amount: a.amount * share_ratio, + }) + .collect() +} + +/// Performs an swap operation with the specified parameters. The trader must approve the +/// pool contract to transfer offer assets from their wallet. +/// +/// * **sender** is the sender of the swap operation. +/// +/// * **offer_asset** proposed asset for swapping. +/// +/// * **belief_price** is used to calculate the maximum swap spread. +/// +/// * **max_spread** sets the maximum spread of the swap operation. +/// +/// * **to** sets the recipient of the swap operation. +/// +/// NOTE - the address that wants to swap should approve the pair contract to pull the offer token. +#[allow(clippy::too_many_arguments)] +pub fn swap( + deps: DepsMut, + env: Env, + info: MessageInfo, + sender: Addr, + offer_asset: Asset, + belief_price: Option, + max_spread: Option, + to: Option, +) -> Result { + offer_asset.assert_sent_native_token_balance(&info)?; + + let mut config = CONFIG.load(deps.storage)?; + + // If the asset balance is already increased, we should subtract the user deposit from the pool amount + let pools = config + .pair_info + .query_pools(&deps.querier, &env.contract.address)? + .into_iter() + .map(|mut p| { + if p.info.equal(&offer_asset.info) { + p.amount = p.amount.checked_sub(offer_asset.amount)?; + } + Ok(p) + }) + .collect::>>()?; + + let offer_pool: Asset; + let ask_pool: Asset; + + if offer_asset.info.equal(&pools[0].info) { + offer_pool = pools[0].clone(); + ask_pool = pools[1].clone(); + } else if offer_asset.info.equal(&pools[1].info) { + offer_pool = pools[1].clone(); + ask_pool = pools[0].clone(); + } else { + return Err(ContractError::AssetMismatch {}); + } + + // Get fee info from the factory + let fee_info = query_fee_info( + &deps.querier, + &config.factory_addr, + config.pair_info.pair_type.clone(), + )?; + + let offer_amount = offer_asset.amount; + + let (return_amount, spread_amount, commission_amount) = compute_swap( + offer_pool.amount, + ask_pool.amount, + offer_amount, + fee_info.total_fee_rate, + )?; + + // Check the max spread limit (if it was specified) + assert_max_spread( + belief_price, + max_spread, + offer_amount, + return_amount + commission_amount, + spread_amount, + )?; + + // Compute the tax for the receiving asset (if it is a native one) + let return_asset = Asset { + info: ask_pool.info.clone(), + amount: return_amount, + }; + + let tax_amount = return_asset.compute_tax(&deps.querier)?; + let receiver = to.unwrap_or_else(|| sender.clone()); + let mut messages = vec![return_asset.into_msg(&deps.querier, &receiver)?]; + + // Compute the Maker fee + let mut maker_fee_amount = Uint128::zero(); + if let Some(fee_address) = fee_info.fee_address { + if let Some(f) = + calculate_maker_fee(&ask_pool.info, commission_amount, fee_info.maker_fee_rate) + { + maker_fee_amount = f.amount; + messages.push(f.into_msg(&deps.querier, fee_address)?); + } + } + + // Accumulate prices for the assets in the pool + if let Some((price0_cumulative_new, price1_cumulative_new, block_time)) = + accumulate_prices(env, &config, pools[0].amount, pools[1].amount)? + { + config.price0_cumulative_last = price0_cumulative_new; + config.price1_cumulative_last = price1_cumulative_new; + config.block_time_last = block_time; + CONFIG.save(deps.storage, &config)?; + } + + Ok(Response::new() + .add_messages( + // 1. send collateral tokens from the contract to a user + // 2. send inactive commission fees to the Maker contract + messages, + ) + .add_attributes(vec![ + attr("action", "swap"), + attr("sender", sender), + attr("receiver", receiver), + attr("offer_asset", offer_asset.info.to_string()), + attr("ask_asset", ask_pool.info.to_string()), + attr("offer_amount", offer_amount), + attr("return_amount", return_amount), + attr("tax_amount", tax_amount), + attr("spread_amount", spread_amount), + attr("commission_amount", commission_amount), + attr("maker_fee_amount", maker_fee_amount), + ])) +} + +/// Accumulate token prices for the assets in the pool. +/// Note that this function shifts **block_time** when any of the token prices is zero in order to not +/// fill an accumulator with a null price for that period. +/// +/// * **x** is the balance of asset\[\0] in the pool. +/// +/// * **y** is the balance of asset\[\1] in the pool. +pub fn accumulate_prices( + env: Env, + config: &Config, + x: Uint128, + y: Uint128, +) -> StdResult> { + let block_time = env.block.time.seconds(); + if block_time <= config.block_time_last { + return Ok(None); + } + + // We have to shift block_time when any price is zero in order to not fill an accumulator with a null price for that period + let time_elapsed = Uint128::from(block_time - config.block_time_last); + + let mut pcl0 = config.price0_cumulative_last; + let mut pcl1 = config.price1_cumulative_last; + + if !x.is_zero() && !y.is_zero() { + let price_precision = Uint128::from(10u128.pow(TWAP_PRECISION.into())); + pcl0 = config.price0_cumulative_last.wrapping_add( + time_elapsed + .checked_mul(price_precision)? + .multiply_ratio(y, x), + ); + pcl1 = config.price1_cumulative_last.wrapping_add( + time_elapsed + .checked_mul(price_precision)? + .multiply_ratio(x, y), + ); + }; + + Ok(Some((pcl0, pcl1, block_time))) +} + +/// Calculates the amount of fees the Maker contract gets according to specified pair parameters. +/// Returns a [`None`] if the Maker fee is zero, otherwise returns a [`Asset`] struct with the specified attributes. +/// +/// * **pool_info** contains information about the pool asset for which the commission will be calculated. +/// +/// * **commission_amount** is the total amount of fees charged for a swap. +/// +/// * **maker_commission_rate** is the percentage of fees that go to the Maker contract. +pub fn calculate_maker_fee( + pool_info: &AssetInfo, + commission_amount: Uint128, + maker_commission_rate: Decimal, +) -> Option { + let maker_fee: Uint128 = commission_amount * maker_commission_rate; + if maker_fee.is_zero() { + return None; + } + + Some(Asset { + info: pool_info.clone(), + amount: maker_fee, + }) +} + +/// Exposes all the queries available in the contract. +/// +/// ## Queries +/// * **QueryMsg::Pair {}** Returns information about the pair in an object of type [`PairInfo`]. +/// +/// * **QueryMsg::Pool {}** Returns information about the amount of assets in the pair contract as +/// well as the amount of LP tokens issued using an object of type [`PoolResponse`]. +/// +/// * **QueryMsg::Share { amount }** Returns the amount of assets that could be withdrawn from the pool +/// using a specific amount of LP tokens. The result is returned in a vector that contains objects of type [`Asset`]. +/// +/// * **QueryMsg::Simulation { offer_asset }** Returns the result of a swap simulation using a [`SimulationResponse`] object. +/// +/// * **QueryMsg::ReverseSimulation { ask_asset }** Returns the result of a reverse swap simulation using +/// a [`ReverseSimulationResponse`] object. +/// +/// * **QueryMsg::CumulativePrices {}** Returns information about cumulative prices for the assets in the +/// pool using a [`CumulativePricesResponse`] object. +/// +/// * **QueryMsg::Config {}** Returns the configuration for the pair contract using a [`ConfigResponse`] object. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Pair {} => to_binary(&CONFIG.load(deps.storage)?.pair_info), + QueryMsg::Pool {} => to_binary(&query_pool(deps)?), + QueryMsg::Share { amount } => to_binary(&query_share(deps, amount)?), + QueryMsg::Simulation { offer_asset, .. } => { + to_binary(&query_simulation(deps, offer_asset)?) + } + QueryMsg::ReverseSimulation { ask_asset, .. } => { + to_binary(&query_reverse_simulation(deps, ask_asset)?) + } + QueryMsg::CumulativePrices {} => to_binary(&query_cumulative_prices(deps, env)?), + QueryMsg::Config {} => to_binary(&query_config(deps)?), + _ => Err(StdError::generic_err("Query is not supported")), + } +} + +/// Returns the amounts of assets in the pair contract as well as the amount of LP +/// tokens currently minted in an object of type [`PoolResponse`]. +pub fn query_pool(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let (assets, total_share) = pool_info(deps.querier, &config)?; + + let resp = PoolResponse { + assets, + total_share, + }; + + Ok(resp) +} + +/// Returns the amount of assets that could be withdrawn from the pool using a specific amount of LP tokens. +/// The result is returned in a vector that contains objects of type [`Asset`]. +/// +/// * **amount** is the amount of LP tokens for which we calculate associated amounts of assets. +pub fn query_share(deps: Deps, amount: Uint128) -> StdResult> { + let config = CONFIG.load(deps.storage)?; + let (pools, total_share) = pool_info(deps.querier, &config)?; + let refund_assets = get_share_in_assets(&pools, amount, total_share); + + Ok(refund_assets) +} + +/// Returns information about a swap simulation in a [`SimulationResponse`] object. +/// +/// * **offer_asset** is the asset to swap as well as an amount of the said asset. +pub fn query_simulation(deps: Deps, offer_asset: Asset) -> StdResult { + let config = CONFIG.load(deps.storage)?; + + let pools = config + .pair_info + .query_pools(&deps.querier, &config.pair_info.contract_addr)?; + + let offer_pool: Asset; + let ask_pool: Asset; + if offer_asset.info.equal(&pools[0].info) { + offer_pool = pools[0].clone(); + ask_pool = pools[1].clone(); + } else if offer_asset.info.equal(&pools[1].info) { + offer_pool = pools[1].clone(); + ask_pool = pools[0].clone(); + } else { + return Err(StdError::generic_err( + "Given offer asset does not belong in the pair", + )); + } + + // Get fee info from the factory contract + let fee_info = query_fee_info( + &deps.querier, + config.factory_addr, + config.pair_info.pair_type, + )?; + + let (return_amount, spread_amount, commission_amount) = compute_swap( + offer_pool.amount, + ask_pool.amount, + offer_asset.amount, + fee_info.total_fee_rate, + )?; + + Ok(SimulationResponse { + return_amount, + spread_amount, + commission_amount, + }) +} + +/// Returns information about a reverse swap simulation in a [`ReverseSimulationResponse`] object. +/// +/// * **ask_asset** is the asset to swap to as well as the desired amount of ask +/// assets to receive from the swap. +pub fn query_reverse_simulation( + deps: Deps, + ask_asset: Asset, +) -> StdResult { + let config = CONFIG.load(deps.storage)?; + + let pools = config + .pair_info + .query_pools(&deps.querier, &config.pair_info.contract_addr)?; + + let offer_pool: Asset; + let ask_pool: Asset; + if ask_asset.info.equal(&pools[0].info) { + ask_pool = pools[0].clone(); + offer_pool = pools[1].clone(); + } else if ask_asset.info.equal(&pools[1].info) { + ask_pool = pools[1].clone(); + offer_pool = pools[0].clone(); + } else { + return Err(StdError::generic_err( + "Given ask asset doesn't belong to pairs", + )); + } + + // Get fee info from factory + let fee_info = query_fee_info( + &deps.querier, + config.factory_addr, + config.pair_info.pair_type, + )?; + + let (offer_amount, spread_amount, commission_amount) = compute_offer_amount( + offer_pool.amount, + ask_pool.amount, + ask_asset.amount, + fee_info.total_fee_rate, + )?; + + Ok(ReverseSimulationResponse { + offer_amount, + spread_amount, + commission_amount, + }) +} + +/// Returns information about cumulative prices for the assets in the pool using a [`CumulativePricesResponse`] object. +pub fn query_cumulative_prices(deps: Deps, env: Env) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let (assets, total_share) = pool_info(deps.querier, &config)?; + + let mut price0_cumulative_last = config.price0_cumulative_last; + let mut price1_cumulative_last = config.price1_cumulative_last; + + if let Some((price0_cumulative_new, price1_cumulative_new, _)) = + accumulate_prices(env, &config, assets[0].amount, assets[1].amount)? + { + price0_cumulative_last = price0_cumulative_new; + price1_cumulative_last = price1_cumulative_new; + } + + let cumulative_prices = vec![ + ( + assets[0].info.clone(), + assets[1].info.clone(), + price0_cumulative_last, + ), + ( + assets[1].info.clone(), + assets[0].info.clone(), + price1_cumulative_last, + ), + ]; + + let resp = CumulativePricesResponse { + assets, + total_share, + cumulative_prices, + }; + + Ok(resp) +} + +/// Returns the pair contract configuration in a [`ConfigResponse`] object. +pub fn query_config(deps: Deps) -> StdResult { + let config: Config = CONFIG.load(deps.storage)?; + Ok(ConfigResponse { + block_time_last: config.block_time_last, + params: None, + owner: None, + }) +} + +/// Returns the result of a swap. +/// +/// * **offer_pool** total amount of offer assets in the pool. +/// +/// * **ask_pool** total amount of ask assets in the pool. +/// +/// * **offer_amount** amount of offer assets to swap. +/// +/// * **commission_rate** total amount of fees charged for the swap. +pub fn compute_swap( + offer_pool: Uint128, + ask_pool: Uint128, + offer_amount: Uint128, + commission_rate: Decimal, +) -> StdResult<(Uint128, Uint128, Uint128)> { + // offer => ask + check_swap_parameters(vec![offer_pool, ask_pool], offer_amount)?; + + let offer_pool: Uint256 = offer_pool.into(); + let ask_pool: Uint256 = ask_pool.into(); + let offer_amount: Uint256 = offer_amount.into(); + let commission_rate = decimal2decimal256(commission_rate)?; + + // ask_amount = (ask_pool - cp / (offer_pool + offer_amount)) + let cp: Uint256 = offer_pool * ask_pool; + let return_amount: Uint256 = (Decimal256::from_ratio(ask_pool, 1u8) + - Decimal256::from_ratio(cp, offer_pool + offer_amount)) + * Uint256::from(1u8); + + // Calculate spread & commission + let spread_amount: Uint256 = + (offer_amount * Decimal256::from_ratio(ask_pool, offer_pool)) - return_amount; + let commission_amount: Uint256 = return_amount * commission_rate; + + // The commision (minus the part that goes to the Maker contract) will be absorbed by the pool + let return_amount: Uint256 = return_amount - commission_amount; + Ok(( + return_amount.try_into()?, + spread_amount.try_into()?, + commission_amount.try_into()?, + )) +} + +/// Returns an amount of offer assets for a specified amount of ask assets. +/// +/// * **offer_pool** total amount of offer assets in the pool. +/// +/// * **ask_pool** total amount of ask assets in the pool. +/// +/// * **ask_amount** amount of ask assets to swap to. +/// +/// * **commission_rate** total amount of fees charged for the swap. +pub fn compute_offer_amount( + offer_pool: Uint128, + ask_pool: Uint128, + ask_amount: Uint128, + commission_rate: Decimal, +) -> StdResult<(Uint128, Uint128, Uint128)> { + // ask => offer + check_swap_parameters(vec![offer_pool, ask_pool], ask_amount)?; + + // offer_amount = cp / (ask_pool - ask_amount / (1 - commission_rate)) - offer_pool + let cp = Uint256::from(offer_pool) * Uint256::from(ask_pool); + let one_minus_commission = Decimal256::one() - decimal2decimal256(commission_rate)?; + let inv_one_minus_commission = Decimal256::one() / one_minus_commission; + + let offer_amount: Uint128 = cp + .multiply_ratio( + Uint256::from(1u8), + Uint256::from( + ask_pool.checked_sub( + (Uint256::from(ask_amount) * inv_one_minus_commission).try_into()?, + )?, + ), + ) + .checked_sub(offer_pool.into())? + .try_into()?; + + let before_commission_deduction = Uint256::from(ask_amount) * inv_one_minus_commission; + let spread_amount = (offer_amount * Decimal::from_ratio(ask_pool, offer_pool)) + .saturating_sub(before_commission_deduction.try_into()?); + let commission_amount = before_commission_deduction * decimal2decimal256(commission_rate)?; + Ok((offer_amount, spread_amount, commission_amount.try_into()?)) +} + +/// If `belief_price` and `max_spread` are both specified, we compute a new spread, +/// otherwise we just use the swap spread to check `max_spread`. +/// +/// * **belief_price** belief price used in the swap. +/// +/// * **max_spread** max spread allowed so that the swap can be executed successfully. +/// +/// * **offer_amount** amount of assets to swap. +/// +/// * **return_amount** amount of assets to receive from the swap. +/// +/// * **spread_amount** spread used in the swap. +pub fn assert_max_spread( + belief_price: Option, + max_spread: Option, + offer_amount: Uint128, + return_amount: Uint128, + spread_amount: Uint128, +) -> Result<(), ContractError> { + let default_spread = Decimal::from_str(DEFAULT_SLIPPAGE)?; + let max_allowed_spread = Decimal::from_str(MAX_ALLOWED_SLIPPAGE)?; + + let max_spread = max_spread.unwrap_or(default_spread); + if max_spread.gt(&max_allowed_spread) { + return Err(ContractError::AllowedSpreadAssertion {}); + } + + if let Some(belief_price) = belief_price { + let expected_return = offer_amount * (Decimal::one() / belief_price); + let spread_amount = expected_return.saturating_sub(return_amount); + + if return_amount < expected_return + && Decimal::from_ratio(spread_amount, expected_return) > max_spread + { + return Err(ContractError::MaxSpreadAssertion {}); + } + } else if Decimal::from_ratio(spread_amount, return_amount + spread_amount) > max_spread { + return Err(ContractError::MaxSpreadAssertion {}); + } + + Ok(()) +} + +/// This is an internal function that enforces slippage tolerance for swaps. +/// +/// * **slippage_tolerance** slippage tolerance to enforce. +/// +/// * **deposits** array with offer and ask amounts for a swap. +/// +/// * **pools** array with total amount of assets in the pool. +fn assert_slippage_tolerance( + slippage_tolerance: Option, + deposits: &[Uint128; 2], + pools: &[Asset], +) -> Result<(), ContractError> { + let default_slippage = Decimal::from_str(DEFAULT_SLIPPAGE)?; + let max_allowed_slippage = Decimal::from_str(MAX_ALLOWED_SLIPPAGE)?; + + let slippage_tolerance = slippage_tolerance.unwrap_or(default_slippage); + if slippage_tolerance.gt(&max_allowed_slippage) { + return Err(ContractError::AllowedSpreadAssertion {}); + } + + let slippage_tolerance: Decimal256 = decimal2decimal256(slippage_tolerance)?; + let one_minus_slippage_tolerance = Decimal256::one() - slippage_tolerance; + let deposits: [Uint256; 2] = [deposits[0].into(), deposits[1].into()]; + let pools: [Uint256; 2] = [pools[0].amount.into(), pools[1].amount.into()]; + + // Ensure each price does not change more than what the slippage tolerance allows + if Decimal256::from_ratio(deposits[0], deposits[1]) * one_minus_slippage_tolerance + > Decimal256::from_ratio(pools[0], pools[1]) + || Decimal256::from_ratio(deposits[1], deposits[0]) * one_minus_slippage_tolerance + > Decimal256::from_ratio(pools[1], pools[0]) + { + return Err(ContractError::MaxSlippageAssertion {}); + } + + Ok(()) +} + +/// Manages the contract migration. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult { + Ok(Response::default()) +} + +/// Returns the total amount of assets in the pool as well as the total amount of LP tokens currently minted. +pub fn pool_info(querier: QuerierWrapper, config: &Config) -> StdResult<(Vec, Uint128)> { + let pools = config + .pair_info + .query_pools(&querier, &config.pair_info.contract_addr)?; + let total_share = query_supply(&querier, &config.pair_info.liquidity_token)?; + + Ok((pools, total_share)) +} diff --git a/contracts/astroport/pair/src/error.rs b/contracts/astroport/pair/src/error.rs new file mode 100644 index 00000000..587e79b5 --- /dev/null +++ b/contracts/astroport/pair/src/error.rs @@ -0,0 +1,49 @@ +use astroport::asset::MINIMUM_LIQUIDITY_AMOUNT; +use cosmwasm_std::{OverflowError, StdError}; +use thiserror::Error; + +/// This enum describes pair contract errors +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Operation non supported")] + NonSupported {}, + + #[error("Event of zero transfer")] + InvalidZeroAmount {}, + + #[error("Operation exceeds max spread limit")] + MaxSpreadAssertion {}, + + #[error("Provided spread amount exceeds allowed limit")] + AllowedSpreadAssertion {}, + + #[error("Operation exceeds max splippage tolerance")] + MaxSlippageAssertion {}, + + #[error("Doubling assets in asset infos")] + DoublingAssets {}, + + #[error("Asset mismatch between the requested and the stored asset in contract")] + AssetMismatch {}, + + #[error("Generator address is not set in factory. Cannot auto-stake")] + AutoStakeError {}, + + #[error("Pair is not migrated to the new admin!")] + PairIsNotMigrated {}, + + #[error("Initial liquidity must be more than {}", MINIMUM_LIQUIDITY_AMOUNT)] + MinimumLiquidityAmountError {}, +} + +impl From for ContractError { + fn from(o: OverflowError) -> Self { + StdError::from(o).into() + } +} diff --git a/contracts/astroport/pair/src/lib.rs b/contracts/astroport/pair/src/lib.rs new file mode 100644 index 00000000..8f0c36c2 --- /dev/null +++ b/contracts/astroport/pair/src/lib.rs @@ -0,0 +1,12 @@ +pub mod contract; +pub mod state; + +pub mod error; + +mod response; + +#[cfg(test)] +mod testing; + +#[cfg(test)] +mod mock_querier; diff --git a/contracts/astroport/pair/src/mock_querier.rs b/contracts/astroport/pair/src/mock_querier.rs new file mode 100644 index 00000000..e4b40ff6 --- /dev/null +++ b/contracts/astroport/pair/src/mock_querier.rs @@ -0,0 +1,175 @@ +use cosmwasm_std::testing::{MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR}; +use cosmwasm_std::{ + from_binary, from_slice, to_binary, Addr, Coin, Empty, OwnedDeps, Querier, QuerierResult, + QueryRequest, SystemError, SystemResult, Uint128, WasmQuery, +}; +use std::collections::HashMap; + +use astroport::factory::FeeInfoResponse; +use astroport::factory::QueryMsg::FeeInfo; +use cw20::{BalanceResponse, Cw20QueryMsg, TokenInfoResponse}; + +/// mock_dependencies is a drop-in replacement for cosmwasm_std::testing::mock_dependencies. +/// This uses the Astroport CustomQuerier. +pub fn mock_dependencies( + contract_balance: &[Coin], +) -> OwnedDeps { + let custom_querier: WasmMockQuerier = + WasmMockQuerier::new(MockQuerier::new(&[(MOCK_CONTRACT_ADDR, contract_balance)])); + + OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: custom_querier, + custom_query_type: Default::default(), + } +} + +pub struct WasmMockQuerier { + base: MockQuerier, + token_querier: TokenQuerier, +} + +#[derive(Clone, Default)] +pub struct TokenQuerier { + // This lets us iterate over all pairs that match the first string + balances: HashMap>, +} + +impl TokenQuerier { + pub fn new(balances: &[(&String, &[(&String, &Uint128)])]) -> Self { + TokenQuerier { + balances: balances_to_map(balances), + } + } +} + +pub(crate) fn balances_to_map( + balances: &[(&String, &[(&String, &Uint128)])], +) -> HashMap> { + let mut balances_map: HashMap> = HashMap::new(); + for (contract_addr, balances) in balances.iter() { + let mut contract_balances_map: HashMap = HashMap::new(); + for (addr, balance) in balances.iter() { + contract_balances_map.insert(addr.to_string(), **balance); + } + + balances_map.insert(contract_addr.to_string(), contract_balances_map); + } + balances_map +} + +impl Querier for WasmMockQuerier { + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { + // MockQuerier doesn't support Custom, so we ignore it completely + let request: QueryRequest = match from_slice(bin_request) { + Ok(v) => v, + Err(e) => { + return SystemResult::Err(SystemError::InvalidRequest { + error: format!("Parsing query request: {}", e), + request: bin_request.into(), + }) + } + }; + self.handle_query(&request) + } +} + +impl WasmMockQuerier { + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { + match &request { + QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => { + if contract_addr == "factory" { + match from_binary(msg).unwrap() { + FeeInfo { .. } => SystemResult::Ok( + to_binary(&FeeInfoResponse { + fee_address: Some(Addr::unchecked("fee_address")), + total_fee_bps: 30, + maker_fee_bps: 1660, + }) + .into(), + ), + _ => panic!("DO NOT ENTER HERE"), + } + } else { + match from_binary(msg).unwrap() { + Cw20QueryMsg::TokenInfo {} => { + let balances: &HashMap = + match self.token_querier.balances.get(contract_addr) { + Some(balances) => balances, + None => { + return SystemResult::Err(SystemError::Unknown {}); + } + }; + + let mut total_supply = Uint128::zero(); + + for balance in balances { + total_supply += *balance.1; + } + + SystemResult::Ok( + to_binary(&TokenInfoResponse { + name: "mAPPL".to_string(), + symbol: "mAPPL".to_string(), + decimals: 6, + total_supply, + }) + .into(), + ) + } + Cw20QueryMsg::Balance { address } => { + let balances: &HashMap = + match self.token_querier.balances.get(contract_addr) { + Some(balances) => balances, + None => { + return SystemResult::Err(SystemError::Unknown {}); + } + }; + + let balance = match balances.get(&address) { + Some(v) => v, + None => { + return SystemResult::Err(SystemError::Unknown {}); + } + }; + + SystemResult::Ok( + to_binary(&BalanceResponse { balance: *balance }).into(), + ) + } + _ => panic!("DO NOT ENTER HERE"), + } + } + } + QueryRequest::Wasm(WasmQuery::Raw { contract_addr, .. }) => { + if contract_addr == "factory" { + SystemResult::Ok(to_binary(&Vec::::new()).into()) + } else { + panic!("DO NOT ENTER HERE"); + } + } + _ => self.base.handle_query(request), + } + } +} + +impl WasmMockQuerier { + pub fn new(base: MockQuerier) -> Self { + WasmMockQuerier { + base, + token_querier: TokenQuerier::default(), + } + } + + // Configure the mint whitelist mock querier + pub fn with_token_balances(&mut self, balances: &[(&String, &[(&String, &Uint128)])]) { + self.token_querier = TokenQuerier::new(balances); + } + + pub fn with_balance(&mut self, balances: &[(&String, &[Coin])]) { + for (addr, balance) in balances { + self.base.update_balance(addr.to_string(), balance.to_vec()); + } + } +} diff --git a/contracts/astroport/pair/src/response.proto b/contracts/astroport/pair/src/response.proto new file mode 100644 index 00000000..9e0e76b5 --- /dev/null +++ b/contracts/astroport/pair/src/response.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +// MsgInstantiateContractResponse defines the Msg/InstantiateContract response type. +message MsgInstantiateContractResponse { + // ContractAddress is the bech32 address of the new contract instance. + string contract_address = 1; + // Data contains base64-encoded bytes to returned from the contract + bytes data = 2; +} diff --git a/contracts/astroport/pair/src/response.rs b/contracts/astroport/pair/src/response.rs new file mode 100644 index 00000000..97880957 --- /dev/null +++ b/contracts/astroport/pair/src/response.rs @@ -0,0 +1,255 @@ +// This file is generated by rust-protobuf 2.25.2. Do not edit +// @generated + +// https://github.com/rust-lang/rust-clippy/issues/702 +#![allow(unknown_lints)] +#![allow(clippy::all)] + +#![allow(unused_attributes)] +#![cfg_attr(rustfmt, rustfmt::skip)] + +#![allow(box_pointers)] +#![allow(dead_code)] +#![allow(missing_docs)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(trivial_casts)] +#![allow(unused_imports)] +#![allow(unused_results)] +//! Generated file from `src/response.proto` + +/// Generated files are compatible only with the same version +/// of protobuf runtime. +// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_25_2; + +#[derive(PartialEq,Clone,Default)] +pub struct MsgInstantiateContractResponse { + // message fields + pub contract_address: ::std::string::String, + pub data: ::std::vec::Vec, + // special fields + pub unknown_fields: ::protobuf::UnknownFields, + pub cached_size: ::protobuf::CachedSize, +} + +impl<'a> ::std::default::Default for &'a MsgInstantiateContractResponse { + fn default() -> &'a MsgInstantiateContractResponse { + ::default_instance() + } +} + +impl MsgInstantiateContractResponse { + pub fn new() -> MsgInstantiateContractResponse { + ::std::default::Default::default() + } + + // string contract_address = 1; + + + pub fn get_contract_address(&self) -> &str { + &self.contract_address + } + pub fn clear_contract_address(&mut self) { + self.contract_address.clear(); + } + + // Param is passed by value, moved + pub fn set_contract_address(&mut self, v: ::std::string::String) { + self.contract_address = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_contract_address(&mut self) -> &mut ::std::string::String { + &mut self.contract_address + } + + // Take field + pub fn take_contract_address(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.contract_address, ::std::string::String::new()) + } + + // bytes data = 2; + + + pub fn get_data(&self) -> &[u8] { + &self.data + } + pub fn clear_data(&mut self) { + self.data.clear(); + } + + // Param is passed by value, moved + pub fn set_data(&mut self, v: ::std::vec::Vec) { + self.data = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_data(&mut self) -> &mut ::std::vec::Vec { + &mut self.data + } + + // Take field + pub fn take_data(&mut self) -> ::std::vec::Vec { + ::std::mem::replace(&mut self.data, ::std::vec::Vec::new()) + } +} + +impl ::protobuf::Message for MsgInstantiateContractResponse { + fn is_initialized(&self) -> bool { + true + } + + fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> { + while !is.eof()? { + let (field_number, wire_type) = is.read_tag_unpack()?; + match field_number { + 1 => { + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.contract_address)?; + }, + 2 => { + ::protobuf::rt::read_singular_proto3_bytes_into(wire_type, is, &mut self.data)?; + }, + _ => { + ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?; + }, + }; + } + ::std::result::Result::Ok(()) + } + + // Compute sizes of nested messages + #[allow(unused_variables)] + fn compute_size(&self) -> u32 { + let mut my_size = 0; + if !self.contract_address.is_empty() { + my_size += ::protobuf::rt::string_size(1, &self.contract_address); + } + if !self.data.is_empty() { + my_size += ::protobuf::rt::bytes_size(2, &self.data); + } + my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields()); + self.cached_size.set(my_size); + my_size + } + + fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> { + if !self.contract_address.is_empty() { + os.write_string(1, &self.contract_address)?; + } + if !self.data.is_empty() { + os.write_bytes(2, &self.data)?; + } + os.write_unknown_fields(self.get_unknown_fields())?; + ::std::result::Result::Ok(()) + } + + fn get_cached_size(&self) -> u32 { + self.cached_size.get() + } + + fn get_unknown_fields(&self) -> &::protobuf::UnknownFields { + &self.unknown_fields + } + + fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields { + &mut self.unknown_fields + } + + fn as_any(&self) -> &dyn (::std::any::Any) { + self as &dyn (::std::any::Any) + } + fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) { + self as &mut dyn (::std::any::Any) + } + fn into_any(self: ::std::boxed::Box) -> ::std::boxed::Box { + self + } + + fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor { + Self::descriptor_static() + } + + fn new() -> MsgInstantiateContractResponse { + MsgInstantiateContractResponse::new() + } + + fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor { + static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT; + descriptor.get(|| { + let mut fields = ::std::vec::Vec::new(); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( + "contract_address", + |m: &MsgInstantiateContractResponse| { &m.contract_address }, + |m: &mut MsgInstantiateContractResponse| { &mut m.contract_address }, + )); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeBytes>( + "data", + |m: &MsgInstantiateContractResponse| { &m.data }, + |m: &mut MsgInstantiateContractResponse| { &mut m.data }, + )); + ::protobuf::reflect::MessageDescriptor::new_pb_name::( + "MsgInstantiateContractResponse", + fields, + file_descriptor_proto() + ) + }) + } + + fn default_instance() -> &'static MsgInstantiateContractResponse { + static instance: ::protobuf::rt::LazyV2 = ::protobuf::rt::LazyV2::INIT; + instance.get(MsgInstantiateContractResponse::new) + } +} + +impl ::protobuf::Clear for MsgInstantiateContractResponse { + fn clear(&mut self) { + self.contract_address.clear(); + self.data.clear(); + self.unknown_fields.clear(); + } +} + +impl ::std::fmt::Debug for MsgInstantiateContractResponse { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + ::protobuf::text_format::fmt(self, f) + } +} + +impl ::protobuf::reflect::ProtobufValue for MsgInstantiateContractResponse { + fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef { + ::protobuf::reflect::ReflectValueRef::Message(self) + } +} + +static file_descriptor_proto_data: &'static [u8] = b"\ + \n\x12src/response.proto\"_\n\x1eMsgInstantiateContractResponse\x12)\n\ + \x10contract_address\x18\x01\x20\x01(\tR\x0fcontractAddress\x12\x12\n\ + \x04data\x18\x02\x20\x01(\x0cR\x04dataJ\xf8\x02\n\x06\x12\x04\0\0\x08\ + \x01\n\x08\n\x01\x0c\x12\x03\0\0\x12\n_\n\x02\x04\0\x12\x04\x03\0\x08\ + \x01\x1aS\x20MsgInstantiateContractResponse\x20defines\x20the\x20Msg/Ins\ + tantiateContract\x20response\x20type.\n\n\n\n\x03\x04\0\x01\x12\x03\x03\ + \x08&\nR\n\x04\x04\0\x02\0\x12\x03\x05\x02\x1e\x1aE\x20ContractAddress\ + \x20is\x20the\x20bech32\x20address\x20of\x20the\x20new\x20contract\x20in\ + stance.\n\n\x0c\n\x05\x04\0\x02\0\x05\x12\x03\x05\x02\x08\n\x0c\n\x05\ + \x04\0\x02\0\x01\x12\x03\x05\t\x19\n\x0c\n\x05\x04\0\x02\0\x03\x12\x03\ + \x05\x1c\x1d\nO\n\x04\x04\0\x02\x01\x12\x03\x07\x02\x11\x1aB\x20Data\x20\ + contains\x20base64-encoded\x20bytes\x20to\x20returned\x20from\x20the\x20\ + contract\n\n\x0c\n\x05\x04\0\x02\x01\x05\x12\x03\x07\x02\x07\n\x0c\n\x05\ + \x04\0\x02\x01\x01\x12\x03\x07\x08\x0c\n\x0c\n\x05\x04\0\x02\x01\x03\x12\ + \x03\x07\x0f\x10b\x06proto3\ +"; + +static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT; + +fn parse_descriptor_proto() -> ::protobuf::descriptor::FileDescriptorProto { + ::protobuf::Message::parse_from_bytes(file_descriptor_proto_data).unwrap() +} + +pub fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto { + file_descriptor_proto_lazy.get(|| { + parse_descriptor_proto() + }) +} diff --git a/contracts/astroport/pair/src/state.rs b/contracts/astroport/pair/src/state.rs new file mode 100644 index 00000000..e836c0ad --- /dev/null +++ b/contracts/astroport/pair/src/state.rs @@ -0,0 +1,22 @@ +use astroport::asset::PairInfo; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Uint128}; +use cw_storage_plus::Item; + +/// This structure stores the main config parameters for a constant product pair contract. +#[cw_serde] +pub struct Config { + /// General pair information (e.g pair type) + pub pair_info: PairInfo, + /// The factory contract address + pub factory_addr: Addr, + /// The last timestamp when the pair contract update the asset cumulative prices + pub block_time_last: u64, + /// The last cumulative price for asset 0 + pub price0_cumulative_last: Uint128, + /// The last cumulative price for asset 1 + pub price1_cumulative_last: Uint128, +} + +/// Stores the config struct at the given key +pub const CONFIG: Item = Item::new("config"); diff --git a/contracts/astroport/pair/src/testing.rs b/contracts/astroport/pair/src/testing.rs new file mode 100644 index 00000000..688ba904 --- /dev/null +++ b/contracts/astroport/pair/src/testing.rs @@ -0,0 +1,1548 @@ +use cosmwasm_std::testing::{mock_env, mock_info, MOCK_CONTRACT_ADDR}; +use cosmwasm_std::{ + attr, to_binary, Addr, BankMsg, BlockInfo, Coin, CosmosMsg, Decimal, DepsMut, Env, Reply, + ReplyOn, Response, StdError, SubMsg, SubMsgResponse, SubMsgResult, Timestamp, Uint128, WasmMsg, +}; +use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg, MinterResponse}; +use proptest::prelude::*; +use protobuf::Message; + +use astroport::asset::{Asset, AssetInfo, PairInfo}; +use astroport::factory::PairType; +use astroport::pair::{ + Cw20HookMsg, ExecuteMsg, InstantiateMsg, PoolResponse, ReverseSimulationResponse, + SimulationResponse, TWAP_PRECISION, +}; +use astroport::token::InstantiateMsg as TokenInstantiateMsg; + +use crate::contract::compute_offer_amount; +use crate::contract::reply; +use crate::contract::{ + accumulate_prices, assert_max_spread, compute_swap, execute, instantiate, query_pool, + query_reverse_simulation, query_share, query_simulation, +}; +use crate::error::ContractError; +use crate::mock_querier::mock_dependencies; +use crate::response::MsgInstantiateContractResponse; +use crate::state::{Config, CONFIG}; + +fn store_liquidity_token(deps: DepsMut, msg_id: u64, contract_addr: String) { + let data = MsgInstantiateContractResponse { + contract_address: contract_addr, + data: vec![], + unknown_fields: Default::default(), + cached_size: Default::default(), + } + .write_to_bytes() + .unwrap(); + + let reply_msg = Reply { + id: msg_id, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(data.into()), + }), + }; + + let _res = reply(deps, mock_env(), reply_msg).unwrap(); +} + +#[test] +fn proper_initialization() { + let mut deps = mock_dependencies(&[]); + + deps.querier.with_token_balances(&[( + &String::from("asset0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &Uint128::new(123u128))], + )]); + + let msg = InstantiateMsg { + factory_addr: String::from("factory"), + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + ], + token_code_id: 10u64, + init_params: None, + }; + + let sender = "addr0000"; + // We can just call .unwrap() to assert this was a success + let env = mock_env(); + let info = mock_info(sender, &[]); + let res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!( + res.messages, + vec![SubMsg { + msg: WasmMsg::Instantiate { + code_id: 10u64, + msg: to_binary(&TokenInstantiateMsg { + name: "UUSD-MAPP-LP".to_string(), + symbol: "uLP".to_string(), + decimals: 6, + initial_balances: vec![], + mint: Some(MinterResponse { + minter: String::from(MOCK_CONTRACT_ADDR), + cap: None, + }), + marketing: None + }) + .unwrap(), + funds: vec![], + admin: None, + label: String::from("Astroport LP token"), + } + .into(), + id: 1, + gas_limit: None, + reply_on: ReplyOn::Success + },] + ); + + // Store liquidity token + store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); + + // It worked, let's query the state + let pair_info = CONFIG.load(deps.as_ref().storage).unwrap().pair_info; + assert_eq!(Addr::unchecked("liquidity0000"), pair_info.liquidity_token); + assert_eq!( + pair_info.asset_infos, + [ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000") + } + ] + ); +} + +#[test] +fn provide_liquidity() { + let mut deps = mock_dependencies(&[Coin { + denom: "uusd".to_string(), + amount: Uint128::new(200_000000000000000000u128), + }]); + + deps.querier.with_token_balances(&[ + ( + &String::from("asset0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &Uint128::new(0))], + ), + ( + &String::from("liquidity0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &Uint128::new(0))], + ), + ]); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + ], + token_code_id: 10u64, + factory_addr: String::from("factory"), + init_params: None, + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + // Store liquidity token + store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); + + // Successfully provide liquidity for the existing pool + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: Uint128::from(100_000000000000000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Uint128::from(100_000000000000000000u128), + }, + ], + slippage_tolerance: None, + auto_stake: None, + receiver: None, + }; + + let env = mock_env(); + let info = mock_info( + "addr0000", + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::from(100_000000000000000000u128), + }], + ); + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + let transfer_from_msg = res.messages.get(0).expect("no message"); + let mint_min_liquidity_msg = res.messages.get(1).expect("no message"); + let mint_receiver_msg = res.messages.get(2).expect("no message"); + assert_eq!( + transfer_from_msg, + &SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("asset0000"), + msg: to_binary(&Cw20ExecuteMsg::TransferFrom { + owner: String::from("addr0000"), + recipient: String::from(MOCK_CONTRACT_ADDR), + amount: Uint128::from(100_000000000000000000u128), + }) + .unwrap(), + funds: vec![], + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never + } + ); + assert_eq!( + mint_min_liquidity_msg, + &SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("liquidity0000"), + msg: to_binary(&Cw20ExecuteMsg::Mint { + recipient: String::from(MOCK_CONTRACT_ADDR), + amount: Uint128::from(1000_u128), + }) + .unwrap(), + funds: vec![], + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + } + ); + assert_eq!( + mint_receiver_msg, + &SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("liquidity0000"), + msg: to_binary(&Cw20ExecuteMsg::Mint { + recipient: String::from("addr0000"), + amount: Uint128::from(99_999999999999999000u128), + }) + .unwrap(), + funds: vec![], + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + } + ); + + // Provide more liquidity 1:2, which is not propotional to 1:1, + // It must accept 1:1 and treat the leftover amount as a donation + deps.querier.with_balance(&[( + &String::from(MOCK_CONTRACT_ADDR), + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::new(200_000000000000000000 + 200_000000000000000000 /* user deposit must be pre-applied */), + }], + )]); + + deps.querier.with_token_balances(&[ + ( + &String::from("liquidity0000"), + &[( + &String::from(MOCK_CONTRACT_ADDR), + &Uint128::new(100_000000000000000000), + )], + ), + ( + &String::from("asset0000"), + &[( + &String::from(MOCK_CONTRACT_ADDR), + &Uint128::new(200_000000000000000000), + )], + ), + ]); + + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: Uint128::from(100_000000000000000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Uint128::from(200_000000000000000000u128), + }, + ], + slippage_tolerance: Some(Decimal::percent(50)), + auto_stake: None, + receiver: None, + }; + + let env = mock_env_with_block_time(env.block.time.seconds() + 1000); + let info = mock_info( + "addr0000", + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::from(200_000000000000000000u128), + }], + ); + + // Only accept 100, then 50 share will be generated with 100 * (100 / 200) + let res: Response = execute(deps.as_mut(), env, info, msg).unwrap(); + let transfer_from_msg = res.messages.get(0).expect("no message"); + let mint_msg = res.messages.get(1).expect("no message"); + assert_eq!( + transfer_from_msg, + &SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("asset0000"), + msg: to_binary(&Cw20ExecuteMsg::TransferFrom { + owner: String::from("addr0000"), + recipient: String::from(MOCK_CONTRACT_ADDR), + amount: Uint128::from(100_000000000000000000u128), + }) + .unwrap(), + funds: vec![], + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + } + ); + assert_eq!( + mint_msg, + &SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("liquidity0000"), + msg: to_binary(&Cw20ExecuteMsg::Mint { + recipient: String::from("addr0000"), + amount: Uint128::from(50_000000000000000000u128), + }) + .unwrap(), + funds: vec![], + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + } + ); + + // Check wrong argument + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: Uint128::from(100_000000000000000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Uint128::from(50_000000000000000000u128), + }, + ], + slippage_tolerance: None, + auto_stake: None, + receiver: None, + }; + + let env = mock_env(); + let info = mock_info( + "addr0000", + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::from(100_000000000000000000u128), + }], + ); + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + match res { + ContractError::Std(StdError::GenericErr { msg, .. }) => assert_eq!( + msg, + "Native token balance mismatch between the argument and the transferred".to_string() + ), + _ => panic!("Must return generic error"), + } + + // Initialize token amount to the 1:1 ratio + deps.querier.with_balance(&[( + &String::from(MOCK_CONTRACT_ADDR), + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000000000000000000 + 100_000000000000000000 /* user deposit must be pre-applied */), + }], + )]); + + deps.querier.with_token_balances(&[ + ( + &String::from("liquidity0000"), + &[( + &String::from(MOCK_CONTRACT_ADDR), + &Uint128::new(100_000000000000000000), + )], + ), + ( + &String::from("asset0000"), + &[( + &String::from(MOCK_CONTRACT_ADDR), + &Uint128::new(100_000000000000000000), + )], + ), + ]); + + // Failed because the price is under slippage_tolerance + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: Uint128::from(98_000000000000000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Uint128::from(100_000000000000000000u128), + }, + ], + slippage_tolerance: Some(Decimal::percent(1)), + auto_stake: None, + receiver: None, + }; + + let env = mock_env_with_block_time(env.block.time.seconds() + 1000); + let info = mock_info( + "addr0001", + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::from(100_000000000000000000u128), + }], + ); + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + assert_eq!(res, ContractError::MaxSlippageAssertion {}); + + // Initialize token balance to 1:1 + deps.querier.with_balance(&[( + &String::from(MOCK_CONTRACT_ADDR), + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000000000000000000 + 98_000000000000000000 /* user deposit must be pre-applied */), + }], + )]); + + // Failed because the price is under slippage_tolerance + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: Uint128::from(100_000000000000000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Uint128::from(98_000000000000000000u128), + }, + ], + slippage_tolerance: Some(Decimal::percent(1)), + auto_stake: None, + receiver: None, + }; + + let env = mock_env_with_block_time(env.block.time.seconds() + 1000); + let info = mock_info( + "addr0001", + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::from(98_000000000000000000u128), + }], + ); + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + assert_eq!(res, ContractError::MaxSlippageAssertion {}); + + // Initialize token amount with a 1:1 ratio + deps.querier.with_balance(&[( + &String::from(MOCK_CONTRACT_ADDR), + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000000000000000000 + 100_000000000000000000 /* user deposit must be pre-applied */), + }], + )]); + + // Successfully provides liquidity + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: Uint128::from(99_000000000000000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Uint128::from(100_000000000000000000u128), + }, + ], + slippage_tolerance: Some(Decimal::percent(1)), + auto_stake: None, + receiver: None, + }; + + let env = mock_env_with_block_time(env.block.time.seconds() + 1000); + let info = mock_info( + "addr0001", + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::from(100_000000000000000000u128), + }], + ); + let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + + // Initialize token balance to 1:1 + deps.querier.with_balance(&[( + &String::from(MOCK_CONTRACT_ADDR), + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000000000000000000 + 99_000000000000000000 /* user deposit must be pre-applied */), + }], + )]); + + // Successfully provides liquidity + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: Uint128::from(100_000000000000000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Uint128::from(99_000000000000000000u128), + }, + ], + slippage_tolerance: Some(Decimal::percent(1)), + auto_stake: None, + receiver: None, + }; + + let env = mock_env_with_block_time(env.block.time.seconds() + 1000); + let info = mock_info( + "addr0001", + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::from(99_000000000000000000u128), + }], + ); + execute(deps.as_mut(), env, info, msg).unwrap(); + + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: Uint128::zero(), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Uint128::from(99_000000000000000000u128), + }, + ], + slippage_tolerance: Some(Decimal::percent(1)), + auto_stake: None, + receiver: None, + }; + let info = mock_info( + "addr0001", + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::from(99_000000000000000000u128), + }], + ); + let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + assert_eq!(err, ContractError::InvalidZeroAmount {}); + + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: Uint128::from(100_000000000000000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Uint128::from(100_000000000000000000u128), + }, + ], + slippage_tolerance: Some(Decimal::percent(51)), + auto_stake: None, + receiver: None, + }; + let info = mock_info( + "addr0001", + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::from(100_000000000000000000u128), + }], + ); + let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + assert_eq!(err, ContractError::AllowedSpreadAssertion {}); +} + +#[test] +fn withdraw_liquidity() { + let mut deps = mock_dependencies(&[Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100u128), + }]); + + deps.querier.with_token_balances(&[ + ( + &String::from("liquidity0000"), + &[(&String::from("addr0000"), &Uint128::new(100u128))], + ), + ( + &String::from("asset0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &Uint128::new(100u128))], + ), + ]); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + ], + token_code_id: 10u64, + + factory_addr: String::from("factory"), + init_params: None, + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + // Store liquidity token + store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); + + // Withdraw liquidity + let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: String::from("addr0000"), + msg: to_binary(&Cw20HookMsg::WithdrawLiquidity { assets: vec![] }).unwrap(), + amount: Uint128::new(100u128), + }); + + let env = mock_env(); + let info = mock_info("liquidity0000", &[]); + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + let log_withdrawn_share = res.attributes.get(2).expect("no log"); + let log_refund_assets = res.attributes.get(3).expect("no log"); + let msg_refund_0 = res.messages.get(0).expect("no message"); + let msg_refund_1 = res.messages.get(1).expect("no message"); + let msg_burn_liquidity = res.messages.get(2).expect("no message"); + assert_eq!( + msg_refund_0, + &SubMsg { + msg: CosmosMsg::Bank(BankMsg::Send { + to_address: String::from("addr0000"), + amount: vec![Coin { + denom: "uusd".to_string(), + amount: Uint128::from(100u128), + }], + }), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + } + ); + assert_eq!( + msg_refund_1, + &SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("asset0000"), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: String::from("addr0000"), + amount: Uint128::from(100u128), + }) + .unwrap(), + funds: vec![], + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + } + ); + assert_eq!( + msg_burn_liquidity, + &SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("liquidity0000"), + msg: to_binary(&Cw20ExecuteMsg::Burn { + amount: Uint128::from(100u128), + }) + .unwrap(), + funds: vec![], + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + } + ); + + assert_eq!( + log_withdrawn_share, + &attr("withdrawn_share", 100u128.to_string()) + ); + assert_eq!( + log_refund_assets, + &attr("refund_assets", "100uusd, 100asset0000") + ); +} + +#[test] +fn try_native_to_token() { + let total_share = Uint128::new(30000000000u128); + let asset_pool_amount = Uint128::new(20000000000u128); + let collateral_pool_amount = Uint128::new(30000000000u128); + let offer_amount = Uint128::new(1500000000u128); + + let mut deps = mock_dependencies(&[Coin { + denom: "uusd".to_string(), + amount: collateral_pool_amount + offer_amount, /* user deposit must be pre-applied */ + }]); + + deps.querier.with_token_balances(&[ + ( + &String::from("liquidity0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &total_share)], + ), + ( + &String::from("asset0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &asset_pool_amount)], + ), + ]); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + ], + token_code_id: 10u64, + factory_addr: String::from("factory"), + init_params: None, + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + // we can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + // Store liquidity token + store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); + + // Normal swap + let msg = ExecuteMsg::Swap { + offer_asset: Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: offer_amount, + }, + ask_asset_info: None, + belief_price: None, + max_spread: Some(Decimal::percent(50)), + to: None, + }; + let env = mock_env_with_block_time(1000); + let info = mock_info( + "addr0000", + &[Coin { + denom: "uusd".to_string(), + amount: offer_amount, + }], + ); + + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + let msg_transfer = res.messages.get(0).expect("no message"); + + // Current price is 1.5, so expected return without spread is 1000 + // 952380952 = 20000000000 - (30000000000 * 20000000000) / (30000000000 + 1500000000) + let expected_ret_amount = Uint128::new(952_380_952u128); + + // 47619047 = 1500000000 * (20000000000 / 30000000000) - 952380952 + let expected_spread_amount = Uint128::new(47619047u128); + + let expected_commission_amount = expected_ret_amount.multiply_ratio(3u128, 1000u128); // 0.3% + let expected_maker_fee_amount = expected_commission_amount.multiply_ratio(166u128, 1000u128); // 0.166 + + let expected_return_amount = expected_ret_amount + .checked_sub(expected_commission_amount) + .unwrap(); + let expected_tax_amount = Uint128::zero(); // no tax for token + + // Check simulation result + deps.querier.with_balance(&[( + &String::from(MOCK_CONTRACT_ADDR), + &[Coin { + denom: "uusd".to_string(), + amount: collateral_pool_amount, /* user deposit must be pre-applied */ + }], + )]); + + let err = query_simulation( + deps.as_ref(), + Asset { + info: AssetInfo::NativeToken { + denom: "cny".to_string(), + }, + amount: offer_amount, + }, + ) + .unwrap_err(); + assert_eq!( + err.to_string(), + "Generic error: Given offer asset does not belong in the pair" + ); + + let simulation_res: SimulationResponse = query_simulation( + deps.as_ref(), + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: offer_amount, + }, + ) + .unwrap(); + assert_eq!(expected_return_amount, simulation_res.return_amount); + assert_eq!(expected_commission_amount, simulation_res.commission_amount); + assert_eq!(expected_spread_amount, simulation_res.spread_amount); + + // Check reverse simulation result + let err = query_reverse_simulation( + deps.as_ref(), + Asset { + info: AssetInfo::NativeToken { + denom: "cny".to_string(), + }, + amount: expected_return_amount, + }, + ) + .unwrap_err(); + assert_eq!( + err.to_string(), + "Generic error: Given ask asset doesn't belong to pairs" + ); + + let reverse_simulation_res: ReverseSimulationResponse = query_reverse_simulation( + deps.as_ref(), + Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: expected_return_amount, + }, + ) + .unwrap(); + assert!( + (offer_amount.u128() as i128 - reverse_simulation_res.offer_amount.u128() as i128).abs() + < 5i128 + ); + assert!( + (expected_commission_amount.u128() as i128 + - reverse_simulation_res.commission_amount.u128() as i128) + .abs() + < 5i128 + ); + assert!( + (expected_spread_amount.u128() as i128 + - reverse_simulation_res.spread_amount.u128() as i128) + .abs() + < 5i128 + ); + + assert_eq!( + res.attributes, + vec![ + attr("action", "swap"), + attr("sender", "addr0000"), + attr("receiver", "addr0000"), + attr("offer_asset", "uusd"), + attr("ask_asset", "asset0000"), + attr("offer_amount", offer_amount.to_string()), + attr("return_amount", expected_return_amount.to_string()), + attr("tax_amount", expected_tax_amount.to_string()), + attr("spread_amount", expected_spread_amount.to_string()), + attr("commission_amount", expected_commission_amount.to_string()), + attr("maker_fee_amount", expected_maker_fee_amount.to_string()), + ] + ); + + assert_eq!( + &SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("asset0000"), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: String::from("addr0000"), + amount: expected_return_amount, + }) + .unwrap(), + funds: vec![], + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + }, + msg_transfer, + ); +} + +#[test] +fn try_token_to_native() { + let total_share = Uint128::new(20000000000u128); + let asset_pool_amount = Uint128::new(30000000000u128); + let collateral_pool_amount = Uint128::new(20000000000u128); + let offer_amount = Uint128::new(1500000000u128); + + let mut deps = mock_dependencies(&[Coin { + denom: "uusd".to_string(), + amount: collateral_pool_amount, + }]); + + deps.querier.with_token_balances(&[ + ( + &String::from("liquidity0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &total_share)], + ), + ( + &String::from("asset0000"), + &[( + &String::from(MOCK_CONTRACT_ADDR), + &(asset_pool_amount + offer_amount), + )], + ), + ]); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + ], + token_code_id: 10u64, + factory_addr: String::from("factory"), + init_params: None, + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + // Store liquidity token + store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); + + // Unauthorized access; can not execute swap directy for token swap + let msg = ExecuteMsg::Swap { + offer_asset: Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: offer_amount, + }, + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: None, + }; + let env = mock_env_with_block_time(1000); + let info = mock_info("addr0000", &[]); + let res = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(res, ContractError::Unauthorized {}); + + // Normal sell + let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: String::from("addr0000"), + amount: offer_amount, + msg: to_binary(&Cw20HookMsg::Swap { + ask_asset_info: None, + belief_price: None, + max_spread: Some(Decimal::percent(50)), + to: None, + }) + .unwrap(), + }); + let env = mock_env_with_block_time(1000); + let info = mock_info("asset0000", &[]); + + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + let msg_transfer = res.messages.get(0).expect("no message"); + + // Current price is 1.5, so expected return without spread is 1000 + // 952380952,3809524 = 20000000000 - (30000000000 * 20000000000) / (30000000000 + 1500000000) + let expected_ret_amount = Uint128::new(952_380_952u128); + + // 47619047 = 1500000000 * (20000000000 / 30000000000) - 952380952,3809524 + let expected_spread_amount = Uint128::new(47619047u128); + + let expected_commission_amount = expected_ret_amount.multiply_ratio(3u128, 1000u128); // 0.3% + let expected_maker_fee_amount = expected_commission_amount.multiply_ratio(166u128, 1000u128); + let expected_return_amount = expected_ret_amount + .checked_sub(expected_commission_amount) + .unwrap(); + + // Check simulation res + // Return asset token balance as normal + deps.querier.with_token_balances(&[ + ( + &String::from("liquidity0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &total_share)], + ), + ( + &String::from("asset0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &(asset_pool_amount))], + ), + ]); + + let simulation_res: SimulationResponse = query_simulation( + deps.as_ref(), + Asset { + amount: offer_amount, + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + }, + ) + .unwrap(); + assert_eq!(expected_return_amount, simulation_res.return_amount); + assert_eq!(expected_commission_amount, simulation_res.commission_amount); + assert_eq!(expected_spread_amount, simulation_res.spread_amount); + + // Check reverse simulation result + let reverse_simulation_res: ReverseSimulationResponse = query_reverse_simulation( + deps.as_ref(), + Asset { + amount: expected_return_amount, + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + }, + ) + .unwrap(); + assert!( + (offer_amount.u128() as i128 - reverse_simulation_res.offer_amount.u128() as i128).abs() + < 5i128, + ); + assert!( + (expected_commission_amount.u128() as i128 + - reverse_simulation_res.commission_amount.u128() as i128) + .abs() + < 5i128 + ); + assert!( + (expected_spread_amount.u128() as i128 + - reverse_simulation_res.spread_amount.u128() as i128) + .abs() + < 5i128, + ); + + assert_eq!( + res.attributes, + vec![ + attr("action", "swap"), + attr("sender", "addr0000"), + attr("receiver", "addr0000"), + attr("offer_asset", "asset0000"), + attr("ask_asset", "uusd"), + attr("offer_amount", offer_amount.to_string()), + attr("return_amount", expected_return_amount.to_string()), + attr("tax_amount", Uint128::zero().to_string()), + attr("spread_amount", expected_spread_amount.to_string()), + attr("commission_amount", expected_commission_amount.to_string()), + attr("maker_fee_amount", expected_maker_fee_amount.to_string()), + ] + ); + + assert_eq!( + &SubMsg { + msg: CosmosMsg::Bank(BankMsg::Send { + to_address: String::from("addr0000"), + amount: vec![Coin { + denom: "uusd".to_string(), + amount: expected_return_amount + }], + }), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + }, + msg_transfer, + ); + + // Failed due to trying to swap a non token (specifying an address of a non token contract) + let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: String::from("addr0000"), + amount: offer_amount, + msg: to_binary(&Cw20HookMsg::Swap { + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: None, + }) + .unwrap(), + }); + let env = mock_env_with_block_time(1000); + let info = mock_info("liquidtity0000", &[]); + let res = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(res, ContractError::Unauthorized {}); +} + +#[test] +fn test_max_spread() { + assert_max_spread( + Some(Decimal::from_ratio(1200u128, 1u128)), + Some(Decimal::percent(1)), + Uint128::from(1200000000u128), + Uint128::from(989999u128), + Uint128::zero(), + ) + .unwrap_err(); + + assert_max_spread( + Some(Decimal::from_ratio(1200u128, 1u128)), + Some(Decimal::percent(1)), + Uint128::from(1200000000u128), + Uint128::from(990000u128), + Uint128::zero(), + ) + .unwrap(); + + assert_max_spread( + None, + Some(Decimal::percent(1)), + Uint128::zero(), + Uint128::from(989999u128), + Uint128::from(10001u128), + ) + .unwrap_err(); + + assert_max_spread( + None, + Some(Decimal::percent(1)), + Uint128::zero(), + Uint128::from(990000u128), + Uint128::from(10000u128), + ) + .unwrap(); + + assert_max_spread( + Some(Decimal::from_ratio(1200u128, 1u128)), + Some(Decimal::percent(51)), + Uint128::from(1200000000u128), + Uint128::from(989999u128), + Uint128::zero(), + ) + .unwrap_err(); +} + +#[test] +fn test_query_pool() { + let total_share_amount = Uint128::from(111u128); + let asset_0_amount = Uint128::from(222u128); + let asset_1_amount = Uint128::from(333u128); + let mut deps = mock_dependencies(&[Coin { + denom: "uusd".to_string(), + amount: asset_0_amount, + }]); + + deps.querier.with_token_balances(&[ + ( + &String::from("asset0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &asset_1_amount)], + ), + ( + &String::from("liquidity0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &total_share_amount)], + ), + ]); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + ], + token_code_id: 10u64, + factory_addr: String::from("factory"), + init_params: None, + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + // Store liquidity token + store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); + + let res: PoolResponse = query_pool(deps.as_ref()).unwrap(); + + assert_eq!( + res.assets, + [ + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: asset_0_amount + }, + Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: asset_1_amount + } + ] + ); + assert_eq!(res.total_share, total_share_amount); +} + +#[test] +fn test_query_share() { + let total_share_amount = Uint128::from(500u128); + let asset_0_amount = Uint128::from(250u128); + let asset_1_amount = Uint128::from(1000u128); + let mut deps = mock_dependencies(&[Coin { + denom: "uusd".to_string(), + amount: asset_0_amount, + }]); + + deps.querier.with_token_balances(&[ + ( + &String::from("asset0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &asset_1_amount)], + ), + ( + &String::from("liquidity0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &total_share_amount)], + ), + ]); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + ], + token_code_id: 10u64, + factory_addr: String::from("factory"), + init_params: None, + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + // Store liquidity token + store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); + + let res = query_share(deps.as_ref(), Uint128::new(250)).unwrap(); + + assert_eq!(res[0].amount, Uint128::new(125)); + assert_eq!(res[1].amount, Uint128::new(500)); +} + +#[test] +fn test_accumulate_prices() { + struct Case { + block_time: u64, + block_time_last: u64, + last0: u128, + last1: u128, + x_amount: u128, + y_amount: u128, + } + + struct Result { + block_time_last: u64, + price_x: u128, + price_y: u128, + is_some: bool, + } + + let price_precision = 10u128.pow(TWAP_PRECISION.into()); + + let test_cases: Vec<(Case, Result)> = vec![ + ( + Case { + block_time: 1000, + block_time_last: 0, + last0: 0, + last1: 0, + x_amount: 250, + y_amount: 500, + }, + Result { + block_time_last: 1000, + price_x: 2000, // 500/250*1000 + price_y: 500, // 250/500*1000 + is_some: true, + }, + ), + // Same block height, no changes + ( + Case { + block_time: 1000, + block_time_last: 1000, + last0: price_precision, + last1: 2 * price_precision, + x_amount: 250, + y_amount: 500, + }, + Result { + block_time_last: 1000, + price_x: 1, + price_y: 2, + is_some: false, + }, + ), + ( + Case { + block_time: 1500, + block_time_last: 1000, + last0: 500 * price_precision, + last1: 2000 * price_precision, + x_amount: 250, + y_amount: 500, + }, + Result { + block_time_last: 1500, + price_x: 1500, // 500 + (500/250*500) + price_y: 2250, // 2000 + (250/500*500) + is_some: true, + }, + ), + ]; + + for test_case in test_cases { + let (case, result) = test_case; + + let env = mock_env_with_block_time(case.block_time); + let config = accumulate_prices( + env, + &Config { + pair_info: PairInfo { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + ], + contract_addr: Addr::unchecked("pair"), + liquidity_token: Addr::unchecked("lp_token"), + pair_type: PairType::Xyk {}, // Implemented in mock querier + }, + factory_addr: Addr::unchecked("factory"), + block_time_last: case.block_time_last, + price0_cumulative_last: Uint128::new(case.last0), + price1_cumulative_last: Uint128::new(case.last1), + }, + Uint128::new(case.x_amount), + Uint128::new(case.y_amount), + ) + .unwrap(); + + assert_eq!(result.is_some, config.is_some()); + + if let Some(config) = config { + assert_eq!(config.2, result.block_time_last); + assert_eq!( + config.0 / Uint128::from(price_precision), + Uint128::new(result.price_x) + ); + assert_eq!( + config.1 / Uint128::from(price_precision), + Uint128::new(result.price_y) + ); + } + } +} + +fn mock_env_with_block_time(time: u64) -> Env { + let mut env = mock_env(); + env.block = BlockInfo { + height: 1, + time: Timestamp::from_seconds(time), + chain_id: "columbus".to_string(), + }; + env +} + +#[test] +fn compute_swap_rounding() { + let offer_pool = Uint128::from(5_000_000_000_000_u128); + let ask_pool = Uint128::from(1_000_000_000_u128); + let return_amount = Uint128::from(0_u128); + let spread_amount = Uint128::from(0_u128); + let commission_amount = Uint128::from(0_u128); + let offer_amount = Uint128::from(1_u128); + + assert_eq!( + compute_swap(offer_pool, ask_pool, offer_amount, Decimal::zero()), + Ok((return_amount, spread_amount, commission_amount)) + ); +} + +proptest! { + #[test] + fn compute_swap_overflow_test( + offer_pool in 1_000_000..9_000_000_000_000_000_000u128, + ask_pool in 1_000_000..9_000_000_000_000_000_000u128, + offer_amount in 1..100_000_000_000_u128, + ) { + + let offer_pool = Uint128::from(offer_pool); + let ask_pool = Uint128::from(ask_pool); + let offer_amount = Uint128::from(offer_amount); + let commission_amount = Decimal::zero(); + + // Make sure there are no overflows + compute_swap( + offer_pool, + ask_pool, + offer_amount, + commission_amount, + ).unwrap(); + } +} + +#[test] +fn ensure_useful_error_messages_are_given_on_swaps() { + const OFFER: Uint128 = Uint128::new(1_000_000_000_000); + const ASK: Uint128 = Uint128::new(1_000_000_000_000); + const AMOUNT: Uint128 = Uint128::new(1_000000); + const ZERO: Uint128 = Uint128::zero(); + const DZERO: Decimal = Decimal::zero(); + + // Computing ask + assert_eq!( + compute_swap(ZERO, ZERO, ZERO, DZERO).unwrap_err(), + StdError::generic_err("One of the pools is empty") + ); + assert_eq!( + compute_swap(ZERO, ZERO, AMOUNT, DZERO).unwrap_err(), + StdError::generic_err("One of the pools is empty") + ); + assert_eq!( + compute_swap(ZERO, ASK, ZERO, DZERO).unwrap_err(), + StdError::generic_err("One of the pools is empty") + ); + assert_eq!( + compute_swap(ZERO, ASK, AMOUNT, DZERO).unwrap_err(), + StdError::generic_err("One of the pools is empty") + ); + assert_eq!( + compute_swap(OFFER, ZERO, ZERO, DZERO).unwrap_err(), + StdError::generic_err("One of the pools is empty") + ); + assert_eq!( + compute_swap(OFFER, ZERO, AMOUNT, DZERO).unwrap_err(), + StdError::generic_err("One of the pools is empty") + ); + assert_eq!( + compute_swap(OFFER, ASK, ZERO, DZERO).unwrap_err(), + StdError::generic_err("Swap amount must not be zero") + ); + compute_swap(OFFER, ASK, AMOUNT, DZERO).unwrap(); + + // Computing offer + assert_eq!( + compute_offer_amount(ZERO, ZERO, ZERO, DZERO).unwrap_err(), + StdError::generic_err("One of the pools is empty") + ); + assert_eq!( + compute_offer_amount(ZERO, ZERO, AMOUNT, DZERO).unwrap_err(), + StdError::generic_err("One of the pools is empty") + ); + assert_eq!( + compute_offer_amount(ZERO, ASK, ZERO, DZERO).unwrap_err(), + StdError::generic_err("One of the pools is empty") + ); + assert_eq!( + compute_offer_amount(ZERO, ASK, AMOUNT, DZERO).unwrap_err(), + StdError::generic_err("One of the pools is empty") + ); + assert_eq!( + compute_offer_amount(OFFER, ZERO, ZERO, DZERO).unwrap_err(), + StdError::generic_err("One of the pools is empty") + ); + assert_eq!( + compute_offer_amount(OFFER, ZERO, AMOUNT, DZERO).unwrap_err(), + StdError::generic_err("One of the pools is empty") + ); + assert_eq!( + compute_offer_amount(OFFER, ASK, ZERO, DZERO).unwrap_err(), + StdError::generic_err("Swap amount must not be zero") + ); + compute_offer_amount(OFFER, ASK, AMOUNT, DZERO).unwrap(); +} diff --git a/contracts/astroport/pair/tests/integration.rs b/contracts/astroport/pair/tests/integration.rs new file mode 100644 index 00000000..d7e82e5f --- /dev/null +++ b/contracts/astroport/pair/tests/integration.rs @@ -0,0 +1,798 @@ +use astroport::asset::{native_asset_info, Asset, AssetInfo, PairInfo}; +use astroport::factory::{ + ExecuteMsg as FactoryExecuteMsg, InstantiateMsg as FactoryInstantiateMsg, PairConfig, PairType, + QueryMsg as FactoryQueryMsg, +}; +use astroport::pair::{ + ConfigResponse, CumulativePricesResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, QueryMsg, + TWAP_PRECISION, +}; +use astroport::token::InstantiateMsg as TokenInstantiateMsg; +use cosmwasm_std::{attr, to_binary, Addr, Coin, Decimal, Uint128}; +use cw20::{BalanceResponse, Cw20Coin, Cw20ExecuteMsg, Cw20QueryMsg, MinterResponse}; +use cw_multi_test::{App, ContractWrapper, Executor}; + +const OWNER: &str = "owner"; + +fn mock_app(owner: Addr, coins: Vec) -> App { + App::new(|router, _, storage| { + // initialization moved to App construction + router.bank.init_balance(storage, &owner, coins).unwrap() + }) +} + +fn store_token_code(app: &mut App) -> u64 { + let astro_token_contract = Box::new(ContractWrapper::new_with_empty( + astroport_token::contract::execute, + astroport_token::contract::instantiate, + astroport_token::contract::query, + )); + + app.store_code(astro_token_contract) +} + +fn store_pair_code(app: &mut App) -> u64 { + let pair_contract = Box::new( + ContractWrapper::new_with_empty( + astroport_pair::contract::execute, + astroport_pair::contract::instantiate, + astroport_pair::contract::query, + ) + .with_reply_empty(astroport_pair::contract::reply), + ); + + app.store_code(pair_contract) +} + +fn store_factory_code(app: &mut App) -> u64 { + let factory_contract = Box::new( + ContractWrapper::new_with_empty( + astroport_factory::contract::execute, + astroport_factory::contract::instantiate, + astroport_factory::contract::query, + ) + .with_reply_empty(astroport_factory::contract::reply), + ); + + app.store_code(factory_contract) +} + +fn instantiate_pair(router: &mut App, owner: &Addr) -> Addr { + let token_contract_code_id = store_token_code(router); + + let pair_contract_code_id = store_pair_code(router); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ], + token_code_id: token_contract_code_id, + factory_addr: String::from("factory"), + init_params: None, + }; + + let pair = router + .instantiate_contract( + pair_contract_code_id, + owner.clone(), + &msg, + &[], + String::from("PAIR"), + None, + ) + .unwrap(); + + let res: PairInfo = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Pair {}) + .unwrap(); + assert_eq!("contract0", res.contract_addr); + assert_eq!("contract1", res.liquidity_token); + + pair +} + +#[test] +fn test_provide_and_withdraw_liquidity() { + let owner = Addr::unchecked("owner"); + let alice_address = Addr::unchecked("alice"); + let mut router = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "cny".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ], + ); + + // Set Alice's balances + router + .send_tokens( + owner.clone(), + alice_address.clone(), + &[ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(233_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(200_000_000u128), + }, + Coin { + denom: "cny".to_string(), + amount: Uint128::from(100_000_000u128), + }, + ], + ) + .unwrap(); + + // Init pair + let pair_instance = instantiate_pair(&mut router, &owner); + + let res: PairInfo = router + .wrap() + .query_wasm_smart(pair_instance.to_string(), &QueryMsg::Pair {}) + .unwrap(); + let lp_token = res.liquidity_token; + + assert_eq!( + res.asset_infos, + [ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ], + ); + + // When dealing with native tokens the transfer should happen before the contract call, which cw-multitest doesn't support + // Set Alice's balances + router + .send_tokens( + owner.clone(), + pair_instance.clone(), + &[ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000u128), + }, + ], + ) + .unwrap(); + + // Provide liquidity + let (msg, coins) = provide_liquidity_msg( + Uint128::new(100_000_000), + Uint128::new(100_000_000), + None, + None, + ); + let res = router + .execute_contract(alice_address.clone(), pair_instance.clone(), &msg, &coins) + .unwrap(); + + assert_eq!( + res.events[1].attributes[1], + attr("action", "provide_liquidity") + ); + assert_eq!(res.events[1].attributes[3], attr("receiver", "alice"),); + assert_eq!( + res.events[1].attributes[4], + attr("assets", "100000000uusd, 100000000uluna") + ); + assert_eq!( + res.events[1].attributes[5], + attr("share", 99999000u128.to_string()) + ); + assert_eq!(res.events[3].attributes[1], attr("action", "mint")); + assert_eq!(res.events[3].attributes[2], attr("to", "contract0")); + assert_eq!( + res.events[3].attributes[3], + attr("amount", 1000.to_string()) + ); + assert_eq!(res.events[5].attributes[1], attr("action", "mint")); + assert_eq!(res.events[5].attributes[2], attr("to", "alice")); + assert_eq!( + res.events[5].attributes[3], + attr("amount", 99999000.to_string()) + ); + + // Provide liquidity for receiver + let (msg, coins) = provide_liquidity_msg( + Uint128::new(100), + Uint128::new(100), + Some("bob".to_string()), + None, + ); + let res = router + .execute_contract(alice_address.clone(), pair_instance.clone(), &msg, &coins) + .unwrap(); + + assert_eq!( + res.events[1].attributes[1], + attr("action", "provide_liquidity") + ); + assert_eq!(res.events[1].attributes[3], attr("receiver", "bob"),); + assert_eq!( + res.events[1].attributes[4], + attr("assets", "100uusd, 100uluna") + ); + assert_eq!( + res.events[1].attributes[5], + attr("share", 50u128.to_string()) + ); + assert_eq!(res.events[3].attributes[1], attr("action", "mint")); + assert_eq!(res.events[3].attributes[2], attr("to", "bob")); + assert_eq!(res.events[3].attributes[3], attr("amount", 50.to_string())); + + // Checking withdraw liquidity + let token_contract_code_id = store_token_code(&mut router); + let foo_token = router + .instantiate_contract( + token_contract_code_id, + owner.clone(), + &astroport::token::InstantiateMsg { + name: "Foo token".to_string(), + symbol: "FOO".to_string(), + decimals: 6, + initial_balances: vec![Cw20Coin { + address: alice_address.to_string(), + amount: Uint128::from(1000000000u128), + }], + mint: None, + marketing: None, + }, + &[], + String::from("FOO"), + None, + ) + .unwrap(); + + let msg = Cw20ExecuteMsg::Send { + contract: pair_instance.to_string(), + amount: Uint128::from(50u8), + msg: to_binary(&Cw20HookMsg::WithdrawLiquidity { assets: vec![] }).unwrap(), + }; + // Try to send withdraw liquidity with FOO token + let err = router + .execute_contract(alice_address.clone(), foo_token, &msg, &[]) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Unauthorized"); + // Withdraw with LP token is successful + router + .execute_contract(alice_address.clone(), lp_token, &msg, &[]) + .unwrap(); + + let err = router + .execute_contract( + alice_address, + pair_instance.clone(), + &ExecuteMsg::Swap { + offer_asset: Asset { + info: AssetInfo::NativeToken { + denom: "cny".to_string(), + }, + amount: Uint128::from(10u8), + }, + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: None, + }, + &[Coin { + denom: "cny".to_string(), + amount: Uint128::from(10u8), + }], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Asset mismatch between the requested and the stored asset in contract" + ); + + // Check pair config + let config: ConfigResponse = router + .wrap() + .query_wasm_smart(pair_instance.to_string(), &QueryMsg::Config {}) + .unwrap(); + assert_eq!( + config, + ConfigResponse { + block_time_last: router.block_info().time.seconds(), + params: None, + owner: None + } + ) +} + +fn provide_liquidity_msg( + uusd_amount: Uint128, + uluna_amount: Uint128, + receiver: Option, + slippage_tolerance: Option, +) -> (ExecuteMsg, [Coin; 2]) { + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: uusd_amount, + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + amount: uluna_amount, + }, + ], + slippage_tolerance, + auto_stake: None, + receiver, + }; + + let coins = [ + Coin { + denom: "uluna".to_string(), + amount: uluna_amount, + }, + Coin { + denom: "uusd".to_string(), + amount: uusd_amount, + }, + ]; + + (msg, coins) +} + +#[test] +fn test_compatibility_of_tokens_with_different_precision() { + let owner = Addr::unchecked(OWNER); + + let mut app = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000_000u128), + }, + ], + ); + + let token_code_id = store_token_code(&mut app); + + let x_amount = Uint128::new(100_000_000_000); + let y_amount = Uint128::new(1000000_0000000); + let x_offer = Uint128::new(1_00000); + let y_expected_return = Uint128::new(1_0000000); + + let token_name = "Xtoken"; + + let init_msg = TokenInstantiateMsg { + name: token_name.to_string(), + symbol: token_name.to_string(), + decimals: 5, + initial_balances: vec![Cw20Coin { + address: OWNER.to_string(), + amount: x_amount + x_offer, + }], + mint: Some(MinterResponse { + minter: String::from(OWNER), + cap: None, + }), + marketing: None, + }; + + let token_x_instance = app + .instantiate_contract( + token_code_id, + owner.clone(), + &init_msg, + &[], + token_name, + None, + ) + .unwrap(); + + let token_name = "Ytoken"; + + let init_msg = TokenInstantiateMsg { + name: token_name.to_string(), + symbol: token_name.to_string(), + decimals: 7, + initial_balances: vec![Cw20Coin { + address: OWNER.to_string(), + amount: y_amount, + }], + mint: Some(MinterResponse { + minter: String::from(OWNER), + cap: None, + }), + marketing: None, + }; + + let token_y_instance = app + .instantiate_contract( + token_code_id, + owner.clone(), + &init_msg, + &[], + token_name, + None, + ) + .unwrap(); + + let pair_code_id = store_pair_code(&mut app); + let factory_code_id = store_factory_code(&mut app); + + let init_msg = FactoryInstantiateMsg { + fee_address: None, + pair_configs: vec![PairConfig { + code_id: pair_code_id, + maker_fee_bps: 0, + pair_type: PairType::Xyk {}, + total_fee_bps: 0, + is_disabled: false, + is_generator_disabled: false, + }], + token_code_id, + generator_address: Some(String::from("generator")), + owner: owner.to_string(), + whitelist_code_id: 234u64, + }; + + let factory_instance = app + .instantiate_contract( + factory_code_id, + owner.clone(), + &init_msg, + &[], + "FACTORY", + None, + ) + .unwrap(); + + let msg = FactoryExecuteMsg::CreatePair { + asset_infos: vec![ + AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + ], + pair_type: PairType::Xyk {}, + init_params: None, + }; + + app.execute_contract(owner.clone(), factory_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = FactoryQueryMsg::Pair { + asset_infos: vec![ + AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + ], + }; + + let res: PairInfo = app + .wrap() + .query_wasm_smart(&factory_instance, &msg) + .unwrap(); + + let pair_instance = res.contract_addr; + + let msg = Cw20ExecuteMsg::IncreaseAllowance { + spender: pair_instance.to_string(), + expires: None, + amount: x_amount + x_offer, + }; + + app.execute_contract(owner.clone(), token_x_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = Cw20ExecuteMsg::IncreaseAllowance { + spender: pair_instance.to_string(), + expires: None, + amount: y_amount, + }; + + app.execute_contract(owner.clone(), token_y_instance.clone(), &msg, &[]) + .unwrap(); + + let user = Addr::unchecked("user"); + + let swap_msg = Cw20ExecuteMsg::Send { + contract: pair_instance.to_string(), + msg: to_binary(&Cw20HookMsg::Swap { + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: Some(user.to_string()), + }) + .unwrap(), + amount: x_offer, + }; + + let err = app + .execute_contract(owner.clone(), token_x_instance.clone(), &swap_msg, &[]) + .unwrap_err(); + assert_eq!( + "Generic error: One of the pools is empty", + err.root_cause().to_string() + ); + + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + amount: x_amount, + }, + Asset { + info: AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + amount: y_amount, + }, + ], + slippage_tolerance: None, + auto_stake: None, + receiver: None, + }; + + app.execute_contract(owner.clone(), pair_instance, &msg, &[]) + .unwrap(); + + // try to swap after provide liquidity + app.execute_contract(owner, token_x_instance, &swap_msg, &[]) + .unwrap(); + + let msg = Cw20QueryMsg::Balance { + address: user.to_string(), + }; + + let res: BalanceResponse = app + .wrap() + .query_wasm_smart(&token_y_instance, &msg) + .unwrap(); + + let acceptable_spread_amount = Uint128::new(10); + + assert_eq!(res.balance, y_expected_return - acceptable_spread_amount); +} + +#[test] +fn test_if_twap_is_calculated_correctly_when_pool_idles() { + let owner = Addr::unchecked("owner"); + let user1 = Addr::unchecked("user1"); + + let mut app = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000_000u128), + }, + ], + ); + + // Set Alice's balances + app.send_tokens( + owner, + user1.clone(), + &[ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(4_000_000_000_000), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(2_000_000_000_000), + }, + ], + ) + .unwrap(); + + // Instantiate pair + let pair_instance = instantiate_pair(&mut app, &user1); + + // Provide liquidity, accumulators are empty + let (msg, coins) = provide_liquidity_msg( + Uint128::new(1_000_000_000_000), + Uint128::new(1_000_000_000_000), + None, + Option::from(Decimal::one()), + ); + app.execute_contract(user1.clone(), pair_instance.clone(), &msg, &coins) + .unwrap(); + + const BLOCKS_PER_DAY: u64 = 17280; + const ELAPSED_SECONDS: u64 = BLOCKS_PER_DAY * 5; + + // A day later + app.update_block(|b| { + b.height += BLOCKS_PER_DAY; + b.time = b.time.plus_seconds(ELAPSED_SECONDS); + }); + + // Provide liquidity, accumulators firstly filled with the same prices + let (msg, coins) = provide_liquidity_msg( + Uint128::new(2_000_000_000_000), + Uint128::new(1_000_000_000_000), + None, + Some(Decimal::percent(50)), + ); + app.execute_contract(user1.clone(), pair_instance.clone(), &msg, &coins) + .unwrap(); + + // Get current twap accumulator values + let msg = QueryMsg::CumulativePrices {}; + let cpr_old: CumulativePricesResponse = + app.wrap().query_wasm_smart(&pair_instance, &msg).unwrap(); + + // A day later + app.update_block(|b| { + b.height += BLOCKS_PER_DAY; + b.time = b.time.plus_seconds(ELAPSED_SECONDS); + }); + + // Get current cumulative price values; they should have been updated by the query method with new 2/1 ratio + let msg = QueryMsg::CumulativePrices {}; + let cpr_new: CumulativePricesResponse = + app.wrap().query_wasm_smart(&pair_instance, &msg).unwrap(); + + let twap0 = cpr_new.cumulative_prices[0].2 - cpr_old.cumulative_prices[0].2; + let twap1 = cpr_new.cumulative_prices[1].2 - cpr_old.cumulative_prices[1].2; + + // Prices weren't changed for the last day, uusd amount in pool = 3000000_000000, uluna = 2000000_000000 + // In accumulators we don't have any precision so we rely on elapsed time so we don't need to consider it + let price_precision = Uint128::from(10u128.pow(TWAP_PRECISION.into())); + assert_eq!(twap0 / price_precision, Uint128::new(57600)); // 0.666666 * ELAPSED_SECONDS (86400) + assert_eq!(twap1 / price_precision, Uint128::new(129600)); // 1.5 * ELAPSED_SECONDS +} + +#[test] +fn create_pair_with_same_assets() { + let owner = Addr::unchecked("owner"); + let mut router = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ], + ); + + let token_contract_code_id = store_token_code(&mut router); + let pair_contract_code_id = store_pair_code(&mut router); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + ], + token_code_id: token_contract_code_id, + factory_addr: String::from("factory"), + init_params: None, + }; + + let resp = router + .instantiate_contract( + pair_contract_code_id, + owner, + &msg, + &[], + String::from("PAIR"), + None, + ) + .unwrap_err(); + + assert_eq!( + resp.root_cause().to_string(), + "Doubling assets in asset infos" + ) +} + +#[test] +fn wrong_number_of_assets() { + let owner = Addr::unchecked("owner"); + let mut router = mock_app(owner.clone(), vec![]); + + let pair_contract_code_id = store_pair_code(&mut router); + + let msg = InstantiateMsg { + asset_infos: vec![AssetInfo::NativeToken { + denom: "uusd".to_string(), + }], + token_code_id: 123, + factory_addr: String::from("factory"), + init_params: None, + }; + + let err = router + .instantiate_contract( + pair_contract_code_id, + owner.clone(), + &msg, + &[], + String::from("PAIR"), + None, + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Generic error: asset_infos must contain exactly two elements" + ); + + let msg = InstantiateMsg { + asset_infos: vec![ + native_asset_info("uusd".to_string()), + native_asset_info("dust".to_string()), + native_asset_info("stone".to_string()), + ], + token_code_id: 123, + factory_addr: String::from("factory"), + init_params: None, + }; + + let err = router + .instantiate_contract( + pair_contract_code_id, + owner, + &msg, + &[], + String::from("PAIR"), + None, + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Generic error: asset_infos must contain exactly two elements" + ); +} diff --git a/contracts/astroport/pair_stable/.cargo/config b/contracts/astroport/pair_stable/.cargo/config new file mode 100644 index 00000000..e6dbd730 --- /dev/null +++ b/contracts/astroport/pair_stable/.cargo/config @@ -0,0 +1,6 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib" +integration-test = "test --test integration" +schema = "run --example pair_stable_schema" diff --git a/contracts/astroport/pair_stable/.editorconfig b/contracts/astroport/pair_stable/.editorconfig new file mode 100644 index 00000000..3d36f20b --- /dev/null +++ b/contracts/astroport/pair_stable/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.rs] +indent_size = 4 diff --git a/contracts/astroport/pair_stable/Cargo.toml b/contracts/astroport/pair_stable/Cargo.toml new file mode 100644 index 00000000..a60885a2 --- /dev/null +++ b/contracts/astroport/pair_stable/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "astroport-pair-stable" +version = "2.0.0" +authors = ["Astroport"] +edition = "2021" +description = "The Astroport stableswap pair contract implementation" +license = "MIT" + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +library = [] + +[dependencies] +astroport = { path = "../../../packages/astroport", default-features = false } +cw2 = { version = "0.15" } +cw20 = { version = "0.15" } +cosmwasm-std = { version = "1.1" } +cw-storage-plus = "0.15" +thiserror = { version = "1.0" } +protobuf = { version = "2", features = ["with-bytes"] } +itertools = "0.10" +cosmwasm-schema = "1.1" + +[dev-dependencies] +anyhow = "1.0" +proptest = "1.0.0" +sim = { git = "https://github.com/astroport-fi/astroport-sims.git", rev = "6869900d4db9061ff8f0c1bfd7c7560056f7e74f", package = "sim" } +astroport-token = {path = "../token"} +cw-multi-test = "0.15" +astroport-factory = {path = "../factory"} +derivative = "2.2.0" diff --git a/contracts/astroport/pair_stable/README.md b/contracts/astroport/pair_stable/README.md new file mode 100644 index 00000000..be0bb7ed --- /dev/null +++ b/contracts/astroport/pair_stable/README.md @@ -0,0 +1,326 @@ +# Astroport Base Stableswap Pair + +The stableswap pool uses the 4A(Rx+Ry) + D formula, resulting in a constant price ∆x / ∆y = 1. More details around how the pool functions can be found [here](https://docs.astroport.fi/astroport/astroport/astro-pools/stableswap-invariant-pools). + +--- + +### Liquidity Providers + +A user can provide liquidity to a constant product pool by calling `provide_liquidity`. Users can also withdraw liquidity by calling `withdraw_liquidity`. + +Whenever liquidity is deposited into a pool, special tokens known as "liquidity tokens" are minted to the provider’s address, in proportion to how much liquidity they contributed to the pool. These tokens are a representation of a liquidity provider’s contribution to a pool. Whenever a trade occurs, the `lp_commission` is distributed pro-rata to all LPs in the pool at the moment of the trade. To receive the underlying liquidity back plus accrued LP fees, LPs must burn their liquidity tokens. + +When providing liquidity from a smart contract, the most important thing to keep in mind is that the amount of tokens deposited into a pool and the amount of tokens withdrawn later from the pool will most likely not be the same (even if stableswap encourages a constant 1:1 ratio between all assets in the pool). + +As an example, let's say the global ratio between two tokens x:y is 1.01:1 (1 x = 0.99 y), but the current ratio between the tokens in an Astroport pair is 1:1.01 (1 x = 1.01 y). Let's also say that someone may decide to LP in the x:y Astroport pool at the current 1:1.01 ratio. As the Astroport pool gets arbitraged to the global ratio, the amount of x & y tokens that the LP can withdraw changes because the total amounts of x & y tokens in the pool also change. + +> Note that before executing the `provide_liqudity` operation, a user must allow the pool contract to take tokens from their wallet + +### Slippage Tolerance for Providing Liquidity + +If a user specifies a slippage tolerance when they provide liquidity in a constant product pool, the pool contract makes sure that the transaction goes through only if the pool price does not change more than tolerance. + +As an example, let's say someone LPs in a pool and specifies a 1% slippage tolerance. The user LPs 200 UST and 200 `ASSET`. With a 1% slippage tolerance, `amountUSTMin` (the minimum amount of UST to LP) should be set to 198 UST, and `amountASSETMin` (the minimum amount of `ASSET` to LP) should be set to .99 `ASSET`. This means that, in a worst case scenario, liquidity will be added at a pool rate of 198 `ASSET`/1 UST or 202.02 UST/1 `ASSET` (200 UST + .99 `ASSET`). If the contract cannot add liquidity within these bounds (because the pool ratio changed more than the tolerance), the transaction will revert. + +## Traders + +### Slippage Tolerance for Swaps + +Astroport has two options to protect traders against slippage during swaps: + +1. Providing `max_spread` +The spread is calculated as the difference between the ask amount (using the constant pool price) before and after the swap operation. Once `max_spread` is set, it will be compared against the actual swap spread. In case the swap spread exceeds the provided max limit, the swap will fail. + +Note that the spread is calculated before commission deduction in order to properly represent the pool's ratio change. + +2. Providing `max_spread` + `belief_price` +If `belief_price` is provided in combination with `max_spread`, the pool will check the difference between the return amount (using `belief_price`) and the real pool price. + +Please note that Astroport has the default value for the spread set to 0.5% and the max allowed spread set to 50%. + +## InstantiateMsg + +Initializes a new stableswap pair. + +```json +{ + "token_code_id": 123, + "factory_addr": "terra...", + "asset_infos": [ + { + "token": { + "contract_addr": "terra..." + } + }, + { + "native_token": { + "denom": "uusd" + } + } + ], + "init_params": "" +} +``` + +## ExecuteMsg + +## ExecuteMsg + +### `receive` + +Withdraws liquidity or assets that were swapped to (ask assets from a swap operation). + +```json +{ + "receive": { + "sender": "terra...", + "amount": "123", + "msg": "" + } +} +``` + +### `provide_liquidity` + +Provides liquidity by sending a user's native or token assets to the pool. + +__NOTE__: you should increase your token allowance for the pool before providing liquidity! + +1. Providing Liquidity Without Specifying Slippage Tolerance + +```json + { + "provide_liquidity": { + "assets": [ + { + "info": { + "token": { + "contract_addr": "terra..." + } + }, + "amount": "1000000" + }, + { + "info": { + "native_token": { + "denom": "uusd" + } + }, + "amount": "1000000" + } + ], + "auto_stake": false, + "receiver": "terra..." + } + } +``` + +2. Providing Liquidity With Slippage Tolerance + + ```json + { + "provide_liquidity": { + "assets": [ + { + "info": { + "token": { + "contract_addr": "terra..." + } + }, + "amount": "1000000" + }, + { + "info": { + "native_token": { + "denom": "uusd" + } + }, + "amount": "1000000" + } + ], + "slippage_tolerance": "0.01", + "auto_stake": false, + "receiver": "terra..." + } + } +``` + +3. Provides the liquidity with a single token. We can do this only for the non-empty pool. + + ```json + { + "provide_liquidity": { + "assets": [ + { + "info": { + "token": { + "contract_addr": "terra..." + } + }, + "amount": "1000000" + }, + { + "info": { + "token": { + "contract_addr": "terra..." + } + }, + "amount": "0" + } + ], + "slippage_tolerance": "0", + "auto_stake": false, + "receiver": "terra..." + } + } +``` + +### `withdraw_liquidity` + +Burn LP tokens and withdraw liquidity from a pool. This call must be sent to a LP token contract associated with the pool from which you want to withdraw liquidity from. + +```json + { + "withdraw_liquidity": {} + } +``` + +### `swap` + +Perform a swap. `offer_asset` is your source asset and `to` is the address that will receive the ask assets. All fields are optional except `offer_asset`. + +NOTE: You should increase your token allowance for the pool before the swap. + +```json + { + "swap": { + "offer_asset": { + "info": { + "native_token": { + "denom": "uluna" + } + }, + "amount": "123" + }, + "belief_price": "123", + "max_spread": "123", + "to": "terra..." + } + } +``` + +### `update_config` + +Update the pair's configuration. + +```json + { + "update_config": { + "params": ": binary serialised parameters for stable pool types; example: {'amp': 100} " + } + } +``` + +## QueryMsg + +All query messages are described below. A custom struct is defined for each query response. + +### `pair` + +Retrieve a pair's configuration (type, assets traded in it etc). + +```json +{ + "pair": {} +} +``` + +### `pool` + +Returns the amount of tokens in the pool for all assets as well as the amount of LP tokens issued. + +```json +{ + "pool": {} +} +``` + +### `config` + +Get the pair contract configuration. + +```json +{ + "config": {} +} +``` + +### `share` + +Return the amount of assets someone would get from the pool if they were to burn a specific amount of LP tokens. + +```json +{ + "share": { + "amount": "123" + } +} +``` + +### `simulation` + +Simulates a swap and returns the spread and commission amounts. + +```json +{ + "simulation": { + "offer_asset": { + "info": { + "native_token": { + "denom": "uusd" + } + }, + "amount": "1000000" + } + } +} +``` + +### `reverse_simulation` + +Reverse simulates a swap (specifies the ask instead of the offer) and returns the offer amount, spread and commission. + +```json +{ + "reverse_simulation": { + "ask_asset": { + "info": { + "token": { + "contract_addr": "terra..." + } + }, + "amount": "1000000" + } + } +} +``` + +### `cumulative_prices` + +Returns the cumulative prices for the assets in the pair. + +```json +{ + "cumulative_prices": {} +} +``` + +### `query_compute_d` + +Returns current D value for the pool. + +```json +{ + "query_compute_d": {} +} +``` diff --git a/contracts/astroport/pair_stable/examples/pair_stable_schema.rs b/contracts/astroport/pair_stable/examples/pair_stable_schema.rs new file mode 100644 index 00000000..00b9e7cb --- /dev/null +++ b/contracts/astroport/pair_stable/examples/pair_stable_schema.rs @@ -0,0 +1,11 @@ +use astroport::pair::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use cosmwasm_schema::write_api; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg + } +} diff --git a/contracts/astroport/pair_stable/rustfmt.toml b/contracts/astroport/pair_stable/rustfmt.toml new file mode 100644 index 00000000..11a85e6a --- /dev/null +++ b/contracts/astroport/pair_stable/rustfmt.toml @@ -0,0 +1,15 @@ +# stable +newline_style = "unix" +hard_tabs = false +tab_spaces = 4 + +# unstable... should we require `rustup run nightly cargo fmt` ? +# or just update the style guide when they are stable? +#fn_single_line = true +#format_code_in_doc_comments = true +#overflow_delimited_expr = true +#reorder_impl_items = true +#struct_field_align_threshold = 20 +#struct_lit_single_line = true +#report_todo = "Always" + diff --git a/contracts/astroport/pair_stable/src/contract.rs b/contracts/astroport/pair_stable/src/contract.rs new file mode 100644 index 00000000..5faf0061 --- /dev/null +++ b/contracts/astroport/pair_stable/src/contract.rs @@ -0,0 +1,1405 @@ +use std::collections::HashMap; +use std::str::FromStr; +use std::vec; + +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + attr, from_binary, to_binary, wasm_execute, wasm_instantiate, Addr, Binary, CosmosMsg, Decimal, + Decimal256, Deps, DepsMut, Env, Fraction, MessageInfo, QuerierWrapper, Reply, Response, + StdError, StdResult, SubMsg, Uint128, Uint256, WasmMsg, +}; +use cw2::{get_contract_version, set_contract_version}; +use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg, MinterResponse}; +use itertools::Itertools; +use protobuf::Message; + +use astroport::asset::{ + addr_opt_validate, addr_validate_to_lower, check_swap_parameters, format_lp_token_name, Asset, + AssetInfo, Decimal256Ext, DecimalAsset, PairInfo, MINIMUM_LIQUIDITY_AMOUNT, +}; +use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; +use astroport::factory::PairType; +use astroport::pair::{ + migration_check, ConfigResponse, InstantiateMsg, StablePoolParams, StablePoolUpdateParams, + DEFAULT_SLIPPAGE, MAX_ALLOWED_SLIPPAGE, +}; +use astroport::pair::{ + CumulativePricesResponse, Cw20HookMsg, ExecuteMsg, MigrateMsg, PoolResponse, QueryMsg, + ReverseSimulationResponse, SimulationResponse, StablePoolConfig, +}; +use astroport::querier::{query_factory_config, query_fee_info, query_supply}; +use astroport::token::InstantiateMsg as TokenInstantiateMsg; +use astroport::DecimalCheckedOps; + +use crate::error::ContractError; +use crate::math::{ + calc_y, compute_d, AMP_PRECISION, MAX_AMP, MAX_AMP_CHANGE, MIN_AMP_CHANGING_TIME, +}; +use crate::migration::CONFIG_V100; +use crate::response::MsgInstantiateContractResponse; +use crate::state::{get_precision, store_precisions, Config, CONFIG, OWNERSHIP_PROPOSAL}; +use crate::utils::{ + accumulate_prices, adjust_precision, check_asset_infos, check_assets, check_cw20_in_pool, + compute_current_amp, compute_swap, get_share_in_assets, mint_liquidity_token_message, + select_pools, SwapResult, +}; + +/// Contract name that is used for migration. +const CONTRACT_NAME: &str = "astroport-pair-stable"; +/// Contract version that is used for migration. +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +/// A `reply` call code ID of sub-message. +const INSTANTIATE_TOKEN_REPLY_ID: u64 = 1; +/// An LP token precision. +const LP_TOKEN_PRECISION: u8 = 6; + +/// Creates a new contract with the specified parameters in [`InstantiateMsg`]. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + mut deps: DepsMut, + env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + check_asset_infos(deps.api, &msg.asset_infos)?; + + if msg.asset_infos.len() > 5 || msg.asset_infos.len() < 2 { + return Err(ContractError::InvalidNumberOfAssets {}); + } + + if msg.init_params.is_none() { + return Err(ContractError::InitParamsNotFound {}); + } + + let params: StablePoolParams = from_binary(&msg.init_params.unwrap())?; + + if params.amp == 0 || params.amp > MAX_AMP { + return Err(ContractError::IncorrectAmp {}); + } + + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + let greatest_precision = store_precisions(deps.branch(), &msg.asset_infos)?; + + // Initializing cumulative prices + let mut cumulative_prices = vec![]; + for from_pool in &msg.asset_infos { + for to_pool in &msg.asset_infos { + if !from_pool.eq(to_pool) { + cumulative_prices.push((from_pool.clone(), to_pool.clone(), Uint128::zero())) + } + } + } + + let config = Config { + owner: addr_opt_validate(deps.api, ¶ms.owner)?, + pair_info: PairInfo { + contract_addr: env.contract.address.clone(), + liquidity_token: Addr::unchecked(""), + asset_infos: msg.asset_infos.clone(), + pair_type: PairType::Stable {}, + }, + factory_addr: addr_validate_to_lower(deps.api, msg.factory_addr)?, + block_time_last: 0, + init_amp: params.amp * AMP_PRECISION, + init_amp_time: env.block.time.seconds(), + next_amp: params.amp * AMP_PRECISION, + next_amp_time: env.block.time.seconds(), + greatest_precision, + cumulative_prices, + }; + + CONFIG.save(deps.storage, &config)?; + + let token_name = format_lp_token_name(&msg.asset_infos, &deps.querier)?; + + // Create LP token + let sub_msg = SubMsg::reply_on_success( + wasm_instantiate( + msg.token_code_id, + &TokenInstantiateMsg { + name: token_name, + symbol: "uLP".to_string(), + decimals: LP_TOKEN_PRECISION, + initial_balances: vec![], + mint: Some(MinterResponse { + minter: env.contract.address.to_string(), + cap: None, + }), + marketing: None, + }, + vec![], + String::from("Astroport LP token"), + )?, + INSTANTIATE_TOKEN_REPLY_ID, + ); + + Ok(Response::new().add_submessage(sub_msg)) +} + +/// The entry point to the contract for processing replies from submessages. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { + let mut config: Config = CONFIG.load(deps.storage)?; + + if config.pair_info.liquidity_token != Addr::unchecked("") { + return Err(ContractError::Unauthorized {}); + } + + let data = msg.result.unwrap().data.unwrap(); + let res: MsgInstantiateContractResponse = + Message::parse_from_bytes(data.as_slice()).map_err(|_| { + StdError::parse_err("MsgInstantiateContractResponse", "failed to parse data") + })?; + + config.pair_info.liquidity_token = + addr_validate_to_lower(deps.api, res.get_contract_address())?; + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new().add_attribute("liquidity_token_addr", config.pair_info.liquidity_token)) +} + +/// Exposes all the execute functions available in the contract. +/// +/// ## Variants +/// * **ExecuteMsg::UpdateConfig { params: Binary }** Updates the contract configuration with the specified +/// input parameters. +/// +/// * **ExecuteMsg::Receive(msg)** Receives a message of type [`Cw20ReceiveMsg`] and processes +/// it depending on the received template. +/// +/// * **ExecuteMsg::ProvideLiquidity { +/// assets, +/// slippage_tolerance, +/// auto_stake, +/// receiver, +/// }** Provides liquidity in the pair using the specified input parameters. +/// +/// * **ExecuteMsg::Swap { +/// offer_asset, +/// belief_price, +/// max_spread, +/// to, +/// }** Performs an swap using the specified parameters. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + let cfg = CONFIG.load(deps.storage)?; + + if migration_check(deps.querier, &cfg.factory_addr, &env.contract.address)? { + return Err(ContractError::PairIsNotMigrated {}); + } + + match msg { + ExecuteMsg::UpdateConfig { params } => update_config(deps, env, info, params), + ExecuteMsg::Receive(msg) => receive_cw20(deps, env, info, msg), + ExecuteMsg::ProvideLiquidity { + assets, + auto_stake, + receiver, + .. + } => provide_liquidity(deps, env, info, assets, auto_stake, receiver), + ExecuteMsg::Swap { + offer_asset, + ask_asset_info, + belief_price, + max_spread, + to, + } => { + offer_asset.info.check(deps.api)?; + if !offer_asset.is_native_token() { + return Err(ContractError::Unauthorized {}); + } + offer_asset.assert_sent_native_token_balance(&info)?; + + let to_addr = addr_opt_validate(deps.api, &to)?; + + swap( + deps, + env, + info.sender, + offer_asset, + ask_asset_info, + belief_price, + max_spread, + to_addr, + ) + } + ExecuteMsg::ProposeNewOwner { owner, expires_in } => { + let config = CONFIG.load(deps.storage)?; + let factory_config = query_factory_config(&deps.querier, config.factory_addr)?; + + propose_new_owner( + deps, + info, + env, + owner, + expires_in, + config.owner.unwrap_or(factory_config.owner), + OWNERSHIP_PROPOSAL, + ) + .map_err(|e| e.into()) + } + ExecuteMsg::DropOwnershipProposal {} => { + let config = CONFIG.load(deps.storage)?; + let factory_config = query_factory_config(&deps.querier, config.factory_addr)?; + + drop_ownership_proposal( + deps, + info, + config.owner.unwrap_or(factory_config.owner), + OWNERSHIP_PROPOSAL, + ) + .map_err(|e| e.into()) + } + ExecuteMsg::ClaimOwnership {} => { + claim_ownership(deps, info, env, OWNERSHIP_PROPOSAL, |deps, new_owner| { + CONFIG.update::<_, StdError>(deps.storage, |mut config| { + config.owner = Some(new_owner); + Ok(config) + })?; + + Ok(()) + }) + .map_err(|e| e.into()) + } + } +} + +/// Receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template. +/// +/// * **cw20_msg** is the CW20 receive message to process. +pub fn receive_cw20( + deps: DepsMut, + env: Env, + info: MessageInfo, + cw20_msg: Cw20ReceiveMsg, +) -> Result { + match from_binary(&cw20_msg.msg)? { + Cw20HookMsg::Swap { + ask_asset_info, + belief_price, + max_spread, + to, + } => { + let config = CONFIG.load(deps.storage)?; + + // Only asset contract can execute this message + check_cw20_in_pool(&config, &info.sender)?; + + let to_addr = addr_opt_validate(deps.api, &to)?; + let sender = addr_validate_to_lower(deps.api, cw20_msg.sender)?; + swap( + deps, + env, + sender, + Asset { + info: AssetInfo::Token { + contract_addr: info.sender, + }, + amount: cw20_msg.amount, + }, + ask_asset_info, + belief_price, + max_spread, + to_addr, + ) + } + Cw20HookMsg::WithdrawLiquidity { assets } => { + let sender = addr_validate_to_lower(deps.api, cw20_msg.sender)?; + withdraw_liquidity(deps, env, info, sender, cw20_msg.amount, assets) + } + } +} + +/// Provides liquidity with the specified input parameters. +/// +/// * **assets** vector with assets available in the pool. +/// +/// * **auto_stake** determines whether the resulting LP tokens are automatically staked in +/// the Generator contract to receive token incentives. +/// +/// * **receiver** address that receives LP tokens. If this address isn't specified, the function will default to the caller. +/// +/// NOTE - the address that wants to provide liquidity should approve the pair contract to pull its relevant tokens. +pub fn provide_liquidity( + deps: DepsMut, + env: Env, + info: MessageInfo, + assets: Vec, + auto_stake: Option, + receiver: Option, +) -> Result { + check_assets(deps.api, &assets)?; + + let auto_stake = auto_stake.unwrap_or(false); + let mut config = CONFIG.load(deps.storage)?; + + if assets.len() > config.pair_info.asset_infos.len() { + return Err(ContractError::InvalidNumberOfAssets {}); + } + + let pools: HashMap<_, _> = config + .pair_info + .query_pools(&deps.querier, &env.contract.address)? + .into_iter() + .map(|pool| (pool.info, pool.amount)) + .collect(); + + let mut non_zero_flag = false; + + let mut assets_collection = assets + .clone() + .into_iter() + .map(|asset| { + asset.assert_sent_native_token_balance(&info)?; + + // Check that at least one asset is non-zero + if !asset.amount.is_zero() { + non_zero_flag = true; + } + + // Get appropriate pool + let pool = pools + .get(&asset.info) + .copied() + .ok_or_else(|| ContractError::InvalidAsset(asset.info.to_string()))?; + + Ok((asset, pool)) + }) + .collect::, ContractError>>()?; + + // If some assets are omitted then add them explicitly with 0 deposit + pools.iter().for_each(|(pool_info, pool_amount)| { + if !assets.iter().any(|asset| asset.info.eq(pool_info)) { + assets_collection.push(( + Asset { + amount: Uint128::zero(), + info: pool_info.clone(), + }, + *pool_amount, + )); + } + }); + + if !non_zero_flag { + return Err(ContractError::InvalidZeroAmount {}); + } + + let mut messages = vec![]; + for (deposit, pool) in assets_collection.iter_mut() { + // We cannot put a zero amount into an empty pool. + if deposit.amount.is_zero() && pool.is_zero() { + return Err(ContractError::InvalidProvideLPsWithSingleToken {}); + } + + // Transfer only non-zero amount + if !deposit.amount.is_zero() { + // If the pool is a token contract, then we need to execute a TransferFrom msg to receive funds + if let AssetInfo::Token { contract_addr } = &deposit.info { + messages.push(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract_addr.to_string(), + msg: to_binary(&Cw20ExecuteMsg::TransferFrom { + owner: info.sender.to_string(), + recipient: env.contract.address.to_string(), + amount: deposit.amount, + })?, + funds: vec![], + })) + } else { + // If the asset is a native token, the pool balance already increased + // To calculate the pool balance properly, we should subtract the user deposit from the recorded pool token amount + *pool = pool.checked_sub(deposit.amount)?; + } + } + } + + let assets_collection = assets_collection + .iter() + .cloned() + .map(|(asset, pool)| { + let coin_precision = get_precision(deps.storage, &asset.info)?; + Ok(( + asset.to_decimal_asset(coin_precision)?, + Decimal256::with_precision(pool, coin_precision)?, + )) + }) + .collect::>>()?; + + let n_coins = config.pair_info.asset_infos.len() as u8; + + let amp = compute_current_amp(&config, &env)?; + + // Initial invariant (D) + let old_balances = assets_collection + .iter() + .map(|(_, pool)| *pool) + .collect_vec(); + let init_d = compute_d(amp, &old_balances, config.greatest_precision)?; + + // Invariant (D) after deposit added + let mut new_balances = assets_collection + .iter() + .map(|(deposit, pool)| Ok(pool + deposit.amount)) + .collect::>>()?; + let deposit_d = compute_d(amp, &new_balances, config.greatest_precision)?; + + let total_share = query_supply(&deps.querier, &config.pair_info.liquidity_token)?; + let share = if total_share.is_zero() { + let share = deposit_d + .to_uint128_with_precision(config.greatest_precision)? + .checked_sub(MINIMUM_LIQUIDITY_AMOUNT) + .map_err(|_| ContractError::MinimumLiquidityAmountError {})?; + + messages.extend(mint_liquidity_token_message( + deps.querier, + &config, + &env.contract.address, + &env.contract.address, + MINIMUM_LIQUIDITY_AMOUNT, + false, + )?); + + // share cannot become zero after minimum liquidity subtraction + if share.is_zero() { + return Err(ContractError::MinimumLiquidityAmountError {}); + } + + share + } else { + // Get fee info from the factory + let fee_info = query_fee_info( + &deps.querier, + &config.factory_addr, + config.pair_info.pair_type.clone(), + )?; + + // total_fee_rate * N_COINS / (4 * (N_COINS - 1)) + let fee = fee_info + .total_fee_rate + .checked_mul(Decimal::from_ratio(n_coins, 4 * (n_coins - 1)))?; + + let fee = Decimal256::new(fee.atomics().into()); + + for i in 0..n_coins as usize { + let ideal_balance = deposit_d.checked_multiply_ratio(old_balances[i], init_d)?; + let difference = if ideal_balance > new_balances[i] { + ideal_balance - new_balances[i] + } else { + new_balances[i] - ideal_balance + }; + // Fee will be charged only during imbalanced provide i.e. if invariant D was changed + new_balances[i] -= fee.checked_mul(difference)?; + } + + let after_fee_d = compute_d(amp, &new_balances, config.greatest_precision)?; + + let share = Decimal256::with_precision(total_share, config.greatest_precision)? + .checked_multiply_ratio(after_fee_d.saturating_sub(init_d), init_d)? + .to_uint128_with_precision(config.greatest_precision)?; + + if share.is_zero() { + return Err(ContractError::LiquidityAmountTooSmall {}); + } + + share + }; + + // Mint LP token for the caller (or for the receiver if it was set) + let receiver = addr_opt_validate(deps.api, &receiver)?.unwrap_or_else(|| info.sender.clone()); + messages.extend(mint_liquidity_token_message( + deps.querier, + &config, + &env.contract.address, + &receiver, + share, + auto_stake, + )?); + + let pools = pools + .into_iter() + .map(|(info, amount)| { + let precision = get_precision(deps.storage, &info)?; + Ok(DecimalAsset { + info, + amount: Decimal256::with_precision(amount, precision)?, + }) + }) + .collect::>>()?; + + if accumulate_prices(deps.as_ref(), env, &mut config, &pools)? { + CONFIG.save(deps.storage, &config)?; + } + + Ok(Response::new().add_messages(messages).add_attributes(vec![ + attr("action", "provide_liquidity"), + attr("sender", info.sender), + attr("receiver", receiver), + attr("assets", assets.iter().join(", ")), + attr("share", share), + ])) +} + +/// Withdraw liquidity from the pool. +/// * **sender** is the address that will receive assets back from the pair contract. +/// +/// * **amount** is the amount of LP tokens to burn. +/// +/// * **assets** optional array which specifies the assets amount to withdraw. +pub fn withdraw_liquidity( + deps: DepsMut, + env: Env, + info: MessageInfo, + sender: Addr, + amount: Uint128, + assets: Vec, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + + if info.sender != config.pair_info.liquidity_token { + return Err(ContractError::Unauthorized {}); + } + + let burn_amount; + let refund_assets; + let mut messages = vec![]; + + let (pools, total_share) = pool_info(deps.querier, &config)?; + if assets.is_empty() { + burn_amount = amount; + refund_assets = get_share_in_assets(&pools, amount, total_share); + } else { + // Imbalanced withdraw + burn_amount = imbalanced_withdraw(deps.as_ref(), &env, &config, amount, &assets)?; + if burn_amount < amount { + // Returning unused LP tokens back to the user + messages.push( + wasm_execute( + &config.pair_info.liquidity_token, + &Cw20ExecuteMsg::Transfer { + recipient: sender.to_string(), + amount: amount - burn_amount, + }, + vec![], + )? + .into(), + ) + } + refund_assets = assets; + } + + messages.extend( + refund_assets + .clone() + .into_iter() + .map(|asset| asset.into_msg(&deps.querier, &sender)) + .collect::>>()?, + ); + messages.push( + wasm_execute( + &config.pair_info.liquidity_token, + &Cw20ExecuteMsg::Burn { + amount: burn_amount, + }, + vec![], + )? + .into(), + ); + + let pools = pools + .iter() + .map(|pool| { + let precision = get_precision(deps.storage, &pool.info)?; + pool.to_decimal_asset(precision) + }) + .collect::>>()?; + + if accumulate_prices(deps.as_ref(), env, &mut config, &pools)? { + CONFIG.save(deps.storage, &config)?; + } + + Ok(Response::new().add_messages(messages).add_attributes(vec![ + attr("action", "withdraw_liquidity"), + attr("sender", sender), + attr("withdrawn_share", amount), + attr("refund_assets", refund_assets.iter().join(", ")), + ])) +} + +/// Imbalanced withdraw liquidity from the pool. Returns a [`ContractError`] on failure, +/// otherwise returns the number of LP tokens to burn. +/// +/// * **provided_amount** amount of provided LP tokens to withdraw liquidity with. +/// +/// * **assets** specifies the assets amount to withdraw. +fn imbalanced_withdraw( + deps: Deps, + env: &Env, + config: &Config, + provided_amount: Uint128, + assets: &[Asset], +) -> Result { + check_assets(deps.api, assets)?; + + if assets.len() > config.pair_info.asset_infos.len() { + return Err(ContractError::InvalidNumberOfAssets {}); + } + + let pools: HashMap<_, _> = config + .pair_info + .query_pools(&deps.querier, &env.contract.address)? + .into_iter() + .map(|pool| (pool.info, pool.amount)) + .collect(); + + let mut assets_collection = assets + .iter() + .cloned() + .map(|asset| { + let precision = get_precision(deps.storage, &asset.info)?; + // Get appropriate pool + let pool = pools + .get(&asset.info) + .copied() + .ok_or_else(|| ContractError::InvalidAsset(asset.info.to_string()))?; + + Ok(( + asset.to_decimal_asset(precision)?, + Decimal256::with_precision(pool, precision)?, + )) + }) + .collect::, ContractError>>()?; + + // If some assets are omitted then add them explicitly with 0 withdraw amount + pools + .into_iter() + .try_for_each(|(pool_info, pool_amount)| -> StdResult<()> { + if !assets.iter().any(|asset| asset.info == pool_info) { + let precision = get_precision(deps.storage, &pool_info)?; + + assets_collection.push(( + DecimalAsset { + amount: Decimal256::zero(), + info: pool_info, + }, + Decimal256::with_precision(pool_amount, precision)?, + )); + } + Ok(()) + })?; + + let n_coins = config.pair_info.asset_infos.len() as u8; + + let amp = compute_current_amp(config, env)?; + + // Initial invariant (D) + let old_balances = assets_collection + .iter() + .map(|(_, pool)| *pool) + .collect_vec(); + let init_d = compute_d(amp, &old_balances, config.greatest_precision)?; + + // Invariant (D) after assets withdrawn + let mut new_balances = assets_collection + .iter() + .cloned() + .map(|(withdraw, pool)| Ok(pool - withdraw.amount)) + .collect::>>()?; + let withdraw_d = compute_d(amp, &new_balances, config.greatest_precision)?; + + // Get fee info from the factory + let fee_info = query_fee_info( + &deps.querier, + &config.factory_addr, + config.pair_info.pair_type.clone(), + )?; + + // total_fee_rate * N_COINS / (4 * (N_COINS - 1)) + let fee = fee_info + .total_fee_rate + .checked_mul(Decimal::from_ratio(n_coins, 4 * (n_coins - 1)))?; + + let fee = Decimal256::new(fee.atomics().into()); + + for i in 0..n_coins as usize { + let ideal_balance = withdraw_d.checked_multiply_ratio(old_balances[i], init_d)?; + let difference = if ideal_balance > new_balances[i] { + ideal_balance - new_balances[i] + } else { + new_balances[i] - ideal_balance + }; + new_balances[i] -= fee.checked_mul(difference)?; + } + + let after_fee_d = compute_d(amp, &new_balances, config.greatest_precision)?; + + let total_share = Uint256::from(query_supply( + &deps.querier, + &config.pair_info.liquidity_token, + )?); + // How many tokens do we need to burn to withdraw asked assets? + let burn_amount = total_share + .checked_multiply_ratio( + init_d.atomics().checked_sub(after_fee_d.atomics())?, + init_d.atomics(), + )? + .checked_add(Uint256::from(1u8))?; // In case of rounding errors - make it unfavorable for the "attacker" + + let burn_amount = burn_amount.try_into()?; + + if burn_amount > provided_amount { + return Err(StdError::generic_err(format!( + "Not enough LP tokens. You need {} LP tokens.", + burn_amount + )) + .into()); + } + + Ok(burn_amount) +} + +/// Performs an swap operation with the specified parameters. +/// +/// * **sender** is the sender of the swap operation. +/// +/// * **offer_asset** proposed asset for swapping. +/// +/// * **belief_price** is used to calculate the maximum swap spread. +/// +/// * **max_spread** sets the maximum spread of the swap operation. +/// +/// * **to** sets the recipient of the swap operation. +/// +/// NOTE - the address that wants to swap should approve the pair contract to pull the offer token. +#[allow(clippy::too_many_arguments)] +pub fn swap( + deps: DepsMut, + env: Env, + sender: Addr, + offer_asset: Asset, + ask_asset_info: Option, + belief_price: Option, + max_spread: Option, + to: Option, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + + // If the asset balance already increased + // We should subtract the user deposit from the pool offer asset amount + let pools = config + .pair_info + .query_pools(&deps.querier, &env.contract.address)? + .into_iter() + .map(|mut pool| { + if pool.info.equal(&offer_asset.info) { + pool.amount = pool.amount.checked_sub(offer_asset.amount)?; + } + let token_precision = get_precision(deps.storage, &pool.info)?; + Ok(DecimalAsset { + info: pool.info, + amount: Decimal256::with_precision(pool.amount, token_precision)?, + }) + }) + .collect::>>()?; + + let (offer_pool, ask_pool) = + select_pools(Some(&offer_asset.info), ask_asset_info.as_ref(), &pools)?; + + let offer_precision = get_precision(deps.storage, &offer_pool.info)?; + + // Check if the liquidity is non-zero + check_swap_parameters( + pools + .iter() + .map(|pool| { + pool.amount + .to_uint128_with_precision(get_precision(deps.storage, &pool.info)?) + }) + .collect::>>()?, + offer_asset.amount, + )?; + + let SwapResult { + return_amount, + spread_amount, + } = compute_swap( + deps.storage, + &env, + &config, + &offer_asset.to_decimal_asset(offer_precision)?, + &offer_pool, + &ask_pool, + &pools, + )?; + + // Get fee info from the factory + let fee_info = query_fee_info( + &deps.querier, + &config.factory_addr, + config.pair_info.pair_type.clone(), + )?; + let commission_amount = fee_info.total_fee_rate.checked_mul_uint128(return_amount)?; + let return_amount = return_amount.saturating_sub(commission_amount); + + // Check the max spread limit (if it was specified) + assert_max_spread( + belief_price, + max_spread, + offer_asset.amount, + return_amount, + spread_amount + commission_amount, + )?; + + let receiver = to.unwrap_or_else(|| sender.clone()); + + let mut messages = vec![Asset { + info: ask_pool.info.clone(), + amount: return_amount, + } + .into_msg(&deps.querier, &receiver)?]; + + // Compute the Maker fee + let mut maker_fee_amount = Uint128::zero(); + if let Some(fee_address) = fee_info.fee_address { + if let Some(f) = + calculate_maker_fee(&ask_pool.info, commission_amount, fee_info.maker_fee_rate) + { + maker_fee_amount = f.amount; + messages.push(f.into_msg(&deps.querier, fee_address)?); + } + } + + if accumulate_prices(deps.as_ref(), env, &mut config, &pools)? { + CONFIG.save(deps.storage, &config)?; + } + + Ok(Response::new() + .add_messages( + // 1. send collateral tokens from the contract to a user + // 2. send inactive commission fees to the Maker contract + messages, + ) + .add_attributes(vec![ + attr("action", "swap"), + attr("sender", sender), + attr("receiver", receiver), + attr("offer_asset", offer_asset.info.to_string()), + attr("ask_asset", ask_pool.info.to_string()), + attr("offer_amount", offer_asset.amount), + attr("return_amount", return_amount), + attr("spread_amount", spread_amount), + attr("commission_amount", commission_amount), + attr("maker_fee_amount", maker_fee_amount), + ])) +} + +/// Calculates the amount of fees the Maker contract gets according to specified pair parameters. +/// Returns a [`None`] if the Maker fee is zero, otherwise returns a [`Asset`] struct with the specified attributes. +/// +/// * **pool_info** contains information about the pool asset for which the commission will be calculated. +/// +/// * **commission_amount** is the total amount of fees charged for a swap. +/// +/// * **maker_commission_rate** is the percentage of fees that go to the Maker contract. +pub fn calculate_maker_fee( + pool_info: &AssetInfo, + commission_amount: Uint128, + maker_commission_rate: Decimal, +) -> Option { + let maker_fee: Uint128 = commission_amount * maker_commission_rate; + if maker_fee.is_zero() { + return None; + } + + Some(Asset { + info: pool_info.clone(), + amount: maker_fee, + }) +} + +/// Exposes all the queries available in the contract. +/// ## Queries +/// * **QueryMsg::Pair {}** Returns information about the pair in an object of type [`PairInfo`]. +/// +/// * **QueryMsg::Pool {}** Returns information about the amount of assets in the pair contract as +/// well as the amount of LP tokens issued using an object of type [`PoolResponse`]. +/// +/// * **QueryMsg::Share { amount }** Returns the amount of assets that could be withdrawn from the pool +/// using a specific amount of LP tokens. The result is returned in a vector that contains objects of type [`Asset`]. +/// +/// * **QueryMsg::Simulation { offer_asset }** Returns the result of a swap simulation using a [`SimulationResponse`] object. +/// +/// * **QueryMsg::ReverseSimulation { ask_asset }** Returns the result of a reverse swap simulation using +/// a [`ReverseSimulationResponse`] object. +/// +/// * **QueryMsg::CumulativePrices {}** Returns information about cumulative prices for the assets in the +/// pool using a [`CumulativePricesResponse`] object. +/// +/// * **QueryMsg::Config {}** Returns the configuration for the pair contract using a [`ConfigResponse`] object. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Pair {} => to_binary(&CONFIG.load(deps.storage)?.pair_info), + QueryMsg::Pool {} => to_binary(&query_pool(deps)?), + QueryMsg::Share { amount } => to_binary(&query_share(deps, amount)?), + QueryMsg::Simulation { + offer_asset, + ask_asset_info, + } => to_binary(&query_simulation(deps, env, offer_asset, ask_asset_info)?), + QueryMsg::ReverseSimulation { + offer_asset_info, + ask_asset, + } => to_binary(&query_reverse_simulation( + deps, + env, + ask_asset, + offer_asset_info, + )?), + QueryMsg::CumulativePrices {} => to_binary(&query_cumulative_prices(deps, env)?), + QueryMsg::Config {} => to_binary(&query_config(deps, env)?), + QueryMsg::QueryComputeD {} => to_binary(&query_compute_d(deps, env)?), + } +} + +/// Returns the amounts of assets in the pair contract as well as the amount of LP +/// tokens currently minted in an object of type [`PoolResponse`]. +pub fn query_pool(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let (assets, total_share) = pool_info(deps.querier, &config)?; + + let resp = PoolResponse { + assets, + total_share, + }; + + Ok(resp) +} + +/// Returns the amount of assets that could be withdrawn from the pool using a specific amount of LP tokens. +/// The result is returned in a vector that contains objects of type [`Asset`]. +/// +/// * **amount** is the amount of LP tokens for which we calculate associated amounts of assets. +pub fn query_share(deps: Deps, amount: Uint128) -> StdResult> { + let config = CONFIG.load(deps.storage)?; + let (pools, total_share) = pool_info(deps.querier, &config)?; + let refund_assets = get_share_in_assets(&pools, amount, total_share); + + Ok(refund_assets) +} + +/// Returns information about a swap simulation in a [`SimulationResponse`] object. +/// +/// * **offer_asset** is the asset to swap as well as an amount of the said asset. +pub fn query_simulation( + deps: Deps, + env: Env, + offer_asset: Asset, + ask_asset_info: Option, +) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let pools = config + .pair_info + .query_pools_decimal(&deps.querier, &config.pair_info.contract_addr)?; + + let (offer_pool, ask_pool) = + select_pools(Some(&offer_asset.info), ask_asset_info.as_ref(), &pools) + .map_err(|err| StdError::generic_err(format!("{err}")))?; + + let offer_precision = get_precision(deps.storage, &offer_pool.info)?; + + if check_swap_parameters( + pools + .iter() + .map(|pool| { + pool.amount + .to_uint128_with_precision(get_precision(deps.storage, &pool.info)?) + }) + .collect::>>()?, + offer_asset.amount, + ) + .is_err() + { + return Ok(SimulationResponse { + return_amount: Uint128::zero(), + spread_amount: Uint128::zero(), + commission_amount: Uint128::zero(), + }); + } + + let SwapResult { + return_amount, + spread_amount, + } = compute_swap( + deps.storage, + &env, + &config, + &offer_asset.to_decimal_asset(offer_precision)?, + &offer_pool, + &ask_pool, + &pools, + ) + .map_err(|err| StdError::generic_err(format!("{err}")))?; + + // Get fee info from factory + let fee_info = query_fee_info( + &deps.querier, + &config.factory_addr, + config.pair_info.pair_type.clone(), + )?; + + let commission_amount = fee_info.total_fee_rate.checked_mul_uint128(return_amount)?; + let return_amount = return_amount.saturating_sub(commission_amount); + + Ok(SimulationResponse { + return_amount, + spread_amount, + commission_amount, + }) +} + +/// Returns information about a reverse swap simulation in a [`ReverseSimulationResponse`] object. +/// +/// * **ask_asset** is the asset to swap to as well as the desired amount of ask +/// assets to receive from the swap. +/// +/// * **offer_asset_info** is optional field which specifies the asset to swap from. +/// May be omitted only in case the pool length is 2. +pub fn query_reverse_simulation( + deps: Deps, + env: Env, + ask_asset: Asset, + offer_asset_info: Option, +) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let pools = config + .pair_info + .query_pools_decimal(&deps.querier, &config.pair_info.contract_addr)?; + let (offer_pool, ask_pool) = + select_pools(offer_asset_info.as_ref(), Some(&ask_asset.info), &pools) + .map_err(|err| StdError::generic_err(format!("{err}")))?; + + let offer_precision = get_precision(deps.storage, &offer_pool.info)?; + let ask_precision = get_precision(deps.storage, &ask_asset.info)?; + + // Check the swap parameters are valid + if check_swap_parameters( + pools + .iter() + .map(|pool| { + pool.amount + .to_uint128_with_precision(get_precision(deps.storage, &pool.info)?) + }) + .collect::>>()?, + ask_asset.amount, + ) + .is_err() + { + return Ok(ReverseSimulationResponse { + offer_amount: Uint128::zero(), + spread_amount: Uint128::zero(), + commission_amount: Uint128::zero(), + }); + } + + // Get fee info from factory + let fee_info = query_fee_info( + &deps.querier, + &config.factory_addr, + config.pair_info.pair_type.clone(), + )?; + let before_commission = (Decimal256::one() + - Decimal256::new(fee_info.total_fee_rate.atomics().into())) + .inv() + .unwrap_or_else(Decimal256::one) + .checked_mul(Decimal256::with_precision(ask_asset.amount, ask_precision)?)?; + + let new_offer_pool_amount = calc_y( + &ask_pool, + &offer_pool.info, + ask_pool.amount - before_commission, + &pools, + compute_current_amp(&config, &env)?, + config.greatest_precision, + )?; + + let offer_amount = new_offer_pool_amount.checked_sub( + offer_pool + .amount + .to_uint128_with_precision(config.greatest_precision)?, + )?; + let offer_amount = adjust_precision(offer_amount, config.greatest_precision, offer_precision)?; + + Ok(ReverseSimulationResponse { + offer_amount, + spread_amount: offer_amount + .saturating_sub(before_commission.to_uint128_with_precision(offer_precision)?), + commission_amount: fee_info + .total_fee_rate + .checked_mul_uint128(before_commission.to_uint128_with_precision(ask_precision)?)?, + }) +} + +/// Returns information about cumulative prices for the assets in the pool using a [`CumulativePricesResponse`] object. +pub fn query_cumulative_prices(deps: Deps, env: Env) -> StdResult { + let mut config = CONFIG.load(deps.storage)?; + let (assets, total_share) = pool_info(deps.querier, &config)?; + let decimal_assets = assets + .iter() + .cloned() + .map(|asset| { + let precision = get_precision(deps.storage, &asset.info)?; + asset.to_decimal_asset(precision) + }) + .collect::>>()?; + + accumulate_prices(deps, env, &mut config, &decimal_assets) + .map_err(|err| StdError::generic_err(format!("{err}")))?; + + Ok(CumulativePricesResponse { + assets, + total_share, + cumulative_prices: config.cumulative_prices, + }) +} + +/// Returns the pair contract configuration in a [`ConfigResponse`] object. +pub fn query_config(deps: Deps, env: Env) -> StdResult { + let config = CONFIG.load(deps.storage)?; + Ok(ConfigResponse { + block_time_last: config.block_time_last, + params: Some(to_binary(&StablePoolConfig { + amp: Decimal::from_ratio(compute_current_amp(&config, &env)?, AMP_PRECISION), + })?), + owner: config.owner, + }) +} + +/// If `belief_price` and `max_spread` are both specified, we compute a new spread, +/// otherwise we just use the swap spread to check `max_spread`. +/// +/// * **belief_price** belief price used in the swap. +/// +/// * **max_spread** max spread allowed so that the swap can be executed successfully. +/// +/// * **offer_amount** amount of assets to swap. +/// +/// * **return_amount** amount of assets to receive from the swap. +/// +/// * **spread_amount** spread used in the swap. +pub fn assert_max_spread( + belief_price: Option, + max_spread: Option, + offer_amount: Uint128, + return_amount: Uint128, + spread_amount: Uint128, +) -> Result<(), ContractError> { + let default_spread = Decimal::from_str(DEFAULT_SLIPPAGE)?; + let max_allowed_spread = Decimal::from_str(MAX_ALLOWED_SLIPPAGE)?; + + let max_spread = max_spread.unwrap_or(default_spread); + if max_spread.gt(&max_allowed_spread) { + return Err(ContractError::AllowedSpreadAssertion {}); + } + + if let Some(belief_price) = belief_price { + let expected_return = offer_amount + * belief_price.inv().ok_or_else(|| { + ContractError::Std(StdError::generic_err( + "Invalid belief_price. Check the input values.", + )) + })?; + + let spread_amount = expected_return.saturating_sub(return_amount); + + if return_amount < expected_return + && Decimal::from_ratio(spread_amount, expected_return) > max_spread + { + return Err(ContractError::MaxSpreadAssertion {}); + } + } else if Decimal::from_ratio(spread_amount, return_amount + spread_amount) > max_spread { + return Err(ContractError::MaxSpreadAssertion {}); + } + + Ok(()) +} + +/// Manages the contract migration. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(mut deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + let contract_version = get_contract_version(deps.storage)?; + + match contract_version.contract.as_ref() { + "astroport-pair-stable" => match contract_version.version.as_ref() { + "1.0.0" => { + let cfg_v100 = CONFIG_V100.load(deps.storage)?; + + let cumulative_prices = vec![ + ( + cfg_v100.pair_info.asset_infos[0].clone(), + cfg_v100.pair_info.asset_infos[1].clone(), + cfg_v100.price0_cumulative_last, + ), + ( + cfg_v100.pair_info.asset_infos[1].clone(), + cfg_v100.pair_info.asset_infos[0].clone(), + cfg_v100.price1_cumulative_last, + ), + ]; + let greatest_precision = + store_precisions(deps.branch(), &cfg_v100.pair_info.asset_infos)?; + + CONFIG.save( + deps.storage, + &Config { + owner: None, + pair_info: cfg_v100.pair_info, + factory_addr: cfg_v100.factory_addr, + block_time_last: cfg_v100.block_time_last, + init_amp: cfg_v100.next_amp, + init_amp_time: cfg_v100.init_amp_time, + next_amp: cfg_v100.next_amp, + next_amp_time: cfg_v100.next_amp_time, + greatest_precision, + cumulative_prices, + }, + )?; + } + _ => return Err(ContractError::MigrationError {}), + }, + _ => return Err(ContractError::MigrationError {}), + } + + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Response::new() + .add_attribute("previous_contract_name", &contract_version.contract) + .add_attribute("previous_contract_version", &contract_version.version) + .add_attribute("new_contract_name", CONTRACT_NAME) + .add_attribute("new_contract_version", CONTRACT_VERSION)) +} + +/// Returns the total amount of assets in the pool as well as the total amount of LP tokens currently minted. +pub fn pool_info(querier: QuerierWrapper, config: &Config) -> StdResult<(Vec, Uint128)> { + let pools = config + .pair_info + .query_pools(&querier, &config.pair_info.contract_addr)?; + let total_share = query_supply(&querier, &config.pair_info.liquidity_token)?; + + Ok((pools, total_share)) +} + +/// Updates the pool configuration with the specified parameters in the `params` variable. +/// +/// * **params** new parameter values. +pub fn update_config( + deps: DepsMut, + env: Env, + info: MessageInfo, + params: Binary, +) -> Result { + let config = CONFIG.load(deps.storage)?; + let factory_config = query_factory_config(&deps.querier, &config.factory_addr)?; + + if info.sender + != if let Some(ref owner) = config.owner { + owner.to_owned() + } else { + factory_config.owner + } + { + return Err(ContractError::Unauthorized {}); + } + + match from_binary::(¶ms)? { + StablePoolUpdateParams::StartChangingAmp { + next_amp, + next_amp_time, + } => start_changing_amp(config, deps, env, next_amp, next_amp_time)?, + StablePoolUpdateParams::StopChangingAmp {} => stop_changing_amp(config, deps, env)?, + } + + Ok(Response::default()) +} + +/// Start changing the AMP value. +/// +/// * **next_amp** new value for AMP. +/// +/// * **next_amp_time** end time when the pool amplification will be equal to `next_amp`. +fn start_changing_amp( + mut config: Config, + deps: DepsMut, + env: Env, + next_amp: u64, + next_amp_time: u64, +) -> Result<(), ContractError> { + if next_amp == 0 || next_amp > MAX_AMP { + return Err(ContractError::IncorrectAmp {}); + } + + let current_amp = compute_current_amp(&config, &env)?.u64(); + + let next_amp_with_precision = next_amp * AMP_PRECISION; + + if next_amp_with_precision * MAX_AMP_CHANGE < current_amp + || next_amp_with_precision > current_amp * MAX_AMP_CHANGE + { + return Err(ContractError::MaxAmpChangeAssertion {}); + } + + let block_time = env.block.time.seconds(); + + if block_time < config.init_amp_time + MIN_AMP_CHANGING_TIME + || next_amp_time < block_time + MIN_AMP_CHANGING_TIME + { + return Err(ContractError::MinAmpChangingTimeAssertion {}); + } + + config.init_amp = current_amp; + config.next_amp = next_amp_with_precision; + config.init_amp_time = block_time; + config.next_amp_time = next_amp_time; + + CONFIG.save(deps.storage, &config)?; + + Ok(()) +} + +/// Stop changing the AMP value. +fn stop_changing_amp(mut config: Config, deps: DepsMut, env: Env) -> StdResult<()> { + let current_amp = compute_current_amp(&config, &env)?; + let block_time = env.block.time.seconds(); + + config.init_amp = current_amp.u64(); + config.next_amp = current_amp.u64(); + config.init_amp_time = block_time; + config.next_amp_time = block_time; + + // now (block_time < next_amp_time) is always False, so we return the saved AMP + CONFIG.save(deps.storage, &config)?; + + Ok(()) +} + +/// Compute the current pool D value. +fn query_compute_d(deps: Deps, env: Env) -> StdResult { + let config = CONFIG.load(deps.storage)?; + + let amp = compute_current_amp(&config, &env)?; + let pools = config + .pair_info + .query_pools_decimal(&deps.querier, env.contract.address)? + .into_iter() + .map(|pool| pool.amount) + .collect::>(); + + compute_d(amp, &pools, config.greatest_precision) + .map_err(|_| StdError::generic_err("Failed to calculate the D"))? + .to_uint128_with_precision(config.greatest_precision) +} diff --git a/contracts/astroport/pair_stable/src/error.rs b/contracts/astroport/pair_stable/src/error.rs new file mode 100644 index 00000000..ead5c98d --- /dev/null +++ b/contracts/astroport/pair_stable/src/error.rs @@ -0,0 +1,97 @@ +use crate::math::{MAX_AMP, MAX_AMP_CHANGE, MIN_AMP_CHANGING_TIME}; +use astroport::asset::MINIMUM_LIQUIDITY_AMOUNT; +use cosmwasm_std::{CheckedMultiplyRatioError, ConversionOverflowError, OverflowError, StdError}; +use thiserror::Error; + +/// This enum describes stableswap pair contract errors +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + CheckedMultiplyRatioError(#[from] CheckedMultiplyRatioError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Doubling assets in asset infos")] + DoublingAssets {}, + + #[error("Event of zero transfer")] + InvalidZeroAmount {}, + + #[error("Insufficient amount of liquidity")] + LiquidityAmountTooSmall {}, + + #[error("Provided spread amount exceeds allowed limit")] + AllowedSpreadAssertion {}, + + #[error("Operation exceeds max spread limit")] + MaxSpreadAssertion {}, + + #[error("Native token balance mismatch between the argument and the transferred")] + AssetMismatch {}, + + #[error( + "Amp coefficient must be greater than 0 and less than or equal to {}", + MAX_AMP + )] + IncorrectAmp {}, + + #[error( + "The difference between the old and new amp value must not exceed {} times", + MAX_AMP_CHANGE + )] + MaxAmpChangeAssertion {}, + + #[error( + "Amp coefficient cannot be changed more often than once per {} seconds", + MIN_AMP_CHANGING_TIME + )] + MinAmpChangingTimeAssertion {}, + + #[error("You need to provide init params")] + InitParamsNotFound {}, + + #[error("Generator address is not set in factory. Cannot autostake")] + AutoStakeError {}, + + #[error("It is not possible to provide liquidity with one token for an empty pool")] + InvalidProvideLPsWithSingleToken {}, + + #[error("Pair is not migrated to the new admin!")] + PairIsNotMigrated {}, + + #[error("The asset {0} does not belong to the pair")] + InvalidAsset(String), + + #[error("Ask or offer asset is missed")] + VariableAssetMissed {}, + + #[error("Source and target assets are the same")] + SameAssets {}, + + #[error( + "Invalid number of assets. The Astroport supports at least 2 and at most 5 assets within a stable pool" + )] + InvalidNumberOfAssets {}, + + #[error("Contract can't be migrated!")] + MigrationError {}, + + #[error("Initial liquidity must be more than {}", MINIMUM_LIQUIDITY_AMOUNT)] + MinimumLiquidityAmountError {}, +} + +impl From for ContractError { + fn from(o: OverflowError) -> Self { + StdError::from(o).into() + } +} + +impl From for ContractError { + fn from(o: ConversionOverflowError) -> Self { + StdError::from(o).into() + } +} diff --git a/contracts/astroport/pair_stable/src/lib.rs b/contracts/astroport/pair_stable/src/lib.rs new file mode 100644 index 00000000..17d96b9b --- /dev/null +++ b/contracts/astroport/pair_stable/src/lib.rs @@ -0,0 +1,15 @@ +pub mod contract; +pub mod math; +pub mod state; + +pub mod error; + +mod migration; +mod response; +mod utils; + +#[cfg(test)] +mod testing; + +#[cfg(test)] +mod mock_querier; diff --git a/contracts/astroport/pair_stable/src/math.rs b/contracts/astroport/pair_stable/src/math.rs new file mode 100644 index 00000000..f27bcb30 --- /dev/null +++ b/contracts/astroport/pair_stable/src/math.rs @@ -0,0 +1,203 @@ +use astroport::asset::{AssetInfo, Decimal256Ext, DecimalAsset}; +use cosmwasm_std::{Decimal256, StdError, StdResult, Uint128, Uint256, Uint64}; +use itertools::Itertools; + +/// The maximum number of calculation steps for Newton's method. +const ITERATIONS: u8 = 32; + +pub const MAX_AMP: u64 = 1_000_000; +pub const MAX_AMP_CHANGE: u64 = 10; +pub const MIN_AMP_CHANGING_TIME: u64 = 86400; +pub const AMP_PRECISION: u64 = 100; + +/// Computes the stableswap invariant (D). +/// +/// * **Equation** +/// +/// A * sum(x_i) * n**n + D = A * D * n**n + D**(n+1) / (n**n * prod(x_i)) +/// +pub(crate) fn compute_d( + amp: Uint64, + pools: &[Decimal256], + greatest_precision: u8, +) -> StdResult { + if pools.iter().any(|pool| pool.is_zero()) { + return Ok(Decimal256::zero()); + } + let sum_x = pools.iter().fold(Decimal256::zero(), |acc, x| acc + (*x)); + + if sum_x.is_zero() { + Ok(Decimal256::zero()) + } else { + let n_coins = pools.len() as u8; + let ann = Decimal256::from_ratio(amp.checked_mul(n_coins.into())?.u64(), AMP_PRECISION); + let n_coins = Decimal256::from_integer(n_coins); + let mut d = sum_x; + let ann_sum_x = ann * sum_x; + for _ in 0..ITERATIONS { + // loop: D_P = D_P * D / (_x * N_COINS) + let d_p = pools + .iter() + .try_fold::<_, _, StdResult<_>>(d, |acc, pool| { + let denominator = pool.checked_mul(n_coins)?; + acc.checked_multiply_ratio(d, denominator) + })?; + let d_prev = d; + d = (ann_sum_x + d_p * n_coins) * d + / ((ann - Decimal256::one()) * d + (n_coins + Decimal256::one()) * d_p); + if d >= d_prev { + if d - d_prev <= Decimal256::with_precision(1u8, greatest_precision)? { + return Ok(d); + } + } else if d < d_prev + && d_prev - d <= Decimal256::with_precision(1u8, greatest_precision)? + { + return Ok(d); + } + } + + Ok(d) + } +} + +/// Computes the new balance of a `to` pool if one makes `from` pool = `new_amount`. +/// +/// Done by solving quadratic equation iteratively. +/// +/// `x_1**2 + x_1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A)` +/// +/// `x_1**2 + b*x_1 = c` +/// +/// `x_1 = (x_1**2 + c) / (2*x_1 + b)` +pub(crate) fn calc_y( + from_asset: &DecimalAsset, + to: &AssetInfo, + new_amount: Decimal256, + pools: &[DecimalAsset], + amp: Uint64, + target_precision: u8, +) -> StdResult { + if to.equal(&from_asset.info) { + return Err(StdError::generic_err( + "The offer asset and ask asset cannot be the same.", + )); + } + if from_asset.amount.eq(&new_amount) { + return Err(StdError::generic_err("The swap amount cannot be zero.")); + } + let n_coins = Uint64::from(pools.len() as u8); + let ann = Uint256::from(amp.checked_mul(n_coins)?.u64() / AMP_PRECISION); + let mut sum = Decimal256::zero(); + let pool_values = pools.iter().map(|asset| asset.amount).collect_vec(); + let d = compute_d(amp, &pool_values, target_precision)? + .to_uint256_with_precision(target_precision)?; + let mut c = d; + for pool in pools { + let pool_amount: Decimal256 = if pool.info.eq(&from_asset.info) { + new_amount + } else if !pool.info.eq(to) { + pool.amount + } else { + continue; + }; + sum += pool_amount; + c = c + .checked_multiply_ratio( + d, + pool_amount.to_uint256_with_precision(target_precision)? * Uint256::from(n_coins), + ) + .map_err(|_| StdError::generic_err("CheckedMultiplyRatioError"))?; + } + let c = c * d / (ann * Uint256::from(n_coins)); + let sum = sum.to_uint256_with_precision(target_precision)?; + let b = sum + d / ann; + let mut y = d; + for _ in 0..ITERATIONS { + let y_prev = y; + y = (y * y + c) / (y + y + b - d); + if y >= y_prev { + if y - y_prev <= Uint256::from(1u8) { + return Ok(y.try_into()?); + } + } else if y < y_prev && y_prev - y <= Uint256::from(1u8) { + return Ok(y.try_into()?); + } + } + + // Should definitely converge in 32 iterations. + Err(StdError::generic_err("y is not converging")) +} + +#[cfg(test)] +mod tests { + use super::*; + use astroport::asset::native_asset; + use astroport::querier::NATIVE_TOKEN_PRECISION; + use cosmwasm_std::{Uint128, Uint256}; + use sim::StableSwapModel; + + #[test] + fn test_compute_d() { + let amp = Uint64::from(100u64); + let pool1 = Uint128::from(100_000_000_000_u128); + let pool2 = Uint128::from(100_000_000_000_u128); + let pool3 = Uint128::from(100_000_000_000_u128); + let model = StableSwapModel::new( + amp.u64().into(), + vec![pool1.u128(), pool2.u128(), pool3.u128()], + 3, + ); + + let sim_d = model.sim_d(); + let d = compute_d( + amp, + &[ + Decimal256::from_integer(pool1.u128()), + Decimal256::from_integer(pool2.u128()), + Decimal256::from_integer(pool3.u128()), + ], + 6, + ) + .unwrap(); + + assert_eq!(Uint256::from(sim_d), d.to_uint256()); + } + + #[test] + fn test_compute_y() { + let amp = Uint64::from(100u64); + let pool1 = Uint128::from(100_000_000_000_u128); + let pool2 = Uint128::from(100_000_000_000_u128); + let pool3 = Uint128::from(100_000_000_000_u128); + let model = StableSwapModel::new( + amp.u64().into(), + vec![pool1.u128(), pool2.u128(), pool3.u128()], + 3, + ); + + let pools = vec![ + native_asset("test1".to_string(), pool1), + native_asset("test2".to_string(), pool2), + native_asset("test3".to_string(), pool3), + ]; + + let offer_amount = Uint128::from(100_000000u128); + let sim_y = model.sim_y(0, 1, pool1.u128() + offer_amount.u128()); + let y = calc_y( + &pools[0].to_decimal_asset(NATIVE_TOKEN_PRECISION).unwrap(), + &pools[1].info, + Decimal256::with_precision(pools[0].amount + offer_amount, NATIVE_TOKEN_PRECISION) + .unwrap(), + &pools + .iter() + .map(|pool| pool.to_decimal_asset(NATIVE_TOKEN_PRECISION).unwrap()) + .collect::>(), + amp * Uint64::from(AMP_PRECISION), + NATIVE_TOKEN_PRECISION, + ) + .unwrap() + .u128(); + + assert_eq!(sim_y, y); + } +} diff --git a/contracts/astroport/pair_stable/src/migration.rs b/contracts/astroport/pair_stable/src/migration.rs new file mode 100644 index 00000000..e029ca33 --- /dev/null +++ b/contracts/astroport/pair_stable/src/migration.rs @@ -0,0 +1,29 @@ +use astroport::asset::PairInfo; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Uint128}; +use cw_storage_plus::Item; + +/// This structure stores the main stableswap pair parameters. +#[cw_serde] +pub struct ConfigV100 { + /// The pair information stored in a [`PairInfo`] struct + pub pair_info: PairInfo, + /// The factory contract address + pub factory_addr: Addr, + /// The last timestamp when the pair contract update the asset cumulative prices + pub block_time_last: u64, + /// The last cumulative price for asset 0 + pub price0_cumulative_last: Uint128, + /// The last cumulative price for asset 1 + pub price1_cumulative_last: Uint128, + /// This is the current amplification used in the pool + pub init_amp: u64, + /// This is the start time when amplification starts to scale up or down + pub init_amp_time: u64, + /// This is the target amplification to reach at `next_amp_time` + pub next_amp: u64, + /// This is the timestamp when the current pool amplification should be `next_amp` + pub next_amp_time: u64, +} + +pub const CONFIG_V100: Item = Item::new("config"); diff --git a/contracts/astroport/pair_stable/src/mock_querier.rs b/contracts/astroport/pair_stable/src/mock_querier.rs new file mode 100644 index 00000000..13ba1a98 --- /dev/null +++ b/contracts/astroport/pair_stable/src/mock_querier.rs @@ -0,0 +1,186 @@ +use cosmwasm_std::testing::{MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR}; +use cosmwasm_std::{ + from_binary, from_slice, to_binary, Addr, Coin, Empty, OwnedDeps, Querier, QuerierResult, + QueryRequest, SystemError, SystemResult, Uint128, WasmQuery, +}; +use std::collections::HashMap; + +use astroport::factory::QueryMsg::{Config, FeeInfo}; +use astroport::factory::{ConfigResponse, FeeInfoResponse}; +use cw20::{BalanceResponse, Cw20QueryMsg, TokenInfoResponse}; + +/// mock_dependencies is a drop-in replacement for cosmwasm_std::testing::mock_dependencies. +/// This uses the Astroport CustomQuerier. +pub fn mock_dependencies( + contract_balance: &[Coin], +) -> OwnedDeps { + let custom_querier: WasmMockQuerier = + WasmMockQuerier::new(MockQuerier::new(&[(MOCK_CONTRACT_ADDR, contract_balance)])); + + OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: custom_querier, + custom_query_type: Default::default(), + } +} + +pub struct WasmMockQuerier { + base: MockQuerier, + token_querier: TokenQuerier, +} + +#[derive(Clone, Default)] +pub struct TokenQuerier { + // This lets us iterate over all pairs that match the first string + balances: HashMap>, +} + +impl TokenQuerier { + pub fn new(balances: &[(&String, &[(&String, &Uint128)])]) -> Self { + TokenQuerier { + balances: balances_to_map(balances), + } + } +} + +pub(crate) fn balances_to_map( + balances: &[(&String, &[(&String, &Uint128)])], +) -> HashMap> { + let mut balances_map: HashMap> = HashMap::new(); + for (contract_addr, balances) in balances.iter() { + let mut contract_balances_map: HashMap = HashMap::new(); + for (addr, balance) in balances.iter() { + contract_balances_map.insert(addr.to_string(), **balance); + } + + balances_map.insert(contract_addr.to_string(), contract_balances_map); + } + balances_map +} + +impl Querier for WasmMockQuerier { + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { + // MockQuerier doesn't support Custom, so we ignore it completely + let request: QueryRequest = match from_slice(bin_request) { + Ok(v) => v, + Err(e) => { + return SystemResult::Err(SystemError::InvalidRequest { + error: format!("Parsing query request: {}", e), + request: bin_request.into(), + }) + } + }; + self.handle_query(&request) + } +} + +impl WasmMockQuerier { + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { + match &request { + QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => { + if contract_addr == "factory" { + match from_binary(msg).unwrap() { + FeeInfo { .. } => SystemResult::Ok( + to_binary(&FeeInfoResponse { + fee_address: Some(Addr::unchecked("fee_address")), + total_fee_bps: 30, + maker_fee_bps: 1660, + }) + .into(), + ), + Config {} => SystemResult::Ok( + to_binary(&ConfigResponse { + owner: Addr::unchecked("owner"), + pair_configs: vec![], + token_code_id: 0, + fee_address: Some(Addr::unchecked("fee_address")), + generator_address: None, + whitelist_code_id: 0, + }) + .into(), + ), + _ => panic!("DO NOT ENTER HERE"), + } + } else { + match from_binary(msg).unwrap() { + Cw20QueryMsg::TokenInfo {} => { + let balances: &HashMap = + match self.token_querier.balances.get(contract_addr) { + Some(balances) => balances, + None => { + return SystemResult::Err(SystemError::Unknown {}); + } + }; + + let mut total_supply = Uint128::zero(); + + for balance in balances { + total_supply += *balance.1; + } + + SystemResult::Ok( + to_binary(&TokenInfoResponse { + name: "mAPPL".to_string(), + symbol: "mAPPL".to_string(), + decimals: 6, + total_supply, + }) + .into(), + ) + } + Cw20QueryMsg::Balance { address } => { + let balances: &HashMap = + match self.token_querier.balances.get(contract_addr) { + Some(balances) => balances, + None => { + return SystemResult::Err(SystemError::Unknown {}); + } + }; + + let balance = match balances.get(&address) { + Some(v) => v, + None => { + return SystemResult::Err(SystemError::Unknown {}); + } + }; + + SystemResult::Ok( + to_binary(&BalanceResponse { balance: *balance }).into(), + ) + } + _ => panic!("DO NOT ENTER HERE"), + } + } + } + QueryRequest::Wasm(WasmQuery::Raw { contract_addr, .. }) => { + if contract_addr == "factory" { + SystemResult::Ok(to_binary(&Vec::::new()).into()) + } else { + panic!("DO NOT ENTER HERE"); + } + } + _ => self.base.handle_query(request), + } + } +} + +impl WasmMockQuerier { + pub fn new(base: MockQuerier) -> Self { + WasmMockQuerier { + base, + token_querier: TokenQuerier::default(), + } + } + + // Configure the mint whitelist mock querier + pub fn with_token_balances(&mut self, balances: &[(&String, &[(&String, &Uint128)])]) { + self.token_querier = TokenQuerier::new(balances); + } + + pub fn with_balance(&mut self, balances: &[(&String, &[Coin])]) { + for (addr, balance) in balances { + self.base.update_balance(addr.to_string(), balance.to_vec()); + } + } +} diff --git a/contracts/astroport/pair_stable/src/response.proto b/contracts/astroport/pair_stable/src/response.proto new file mode 100644 index 00000000..9e0e76b5 --- /dev/null +++ b/contracts/astroport/pair_stable/src/response.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +// MsgInstantiateContractResponse defines the Msg/InstantiateContract response type. +message MsgInstantiateContractResponse { + // ContractAddress is the bech32 address of the new contract instance. + string contract_address = 1; + // Data contains base64-encoded bytes to returned from the contract + bytes data = 2; +} diff --git a/contracts/astroport/pair_stable/src/response.rs b/contracts/astroport/pair_stable/src/response.rs new file mode 100644 index 00000000..1ad316ea --- /dev/null +++ b/contracts/astroport/pair_stable/src/response.rs @@ -0,0 +1,255 @@ +// This file is generated by rust-protobuf 2.25.2. Do not edit +// @generated + +// https://github.com/rust-lang/rust-clippy/issues/702 +#![allow(unknown_lints)] +#![allow(clippy::all)] + +#![allow(unused_attributes)] +#![cfg_attr(rustfmt, rustfmt::skip)] + +#![allow(box_pointers)] +#![allow(dead_code)] +#![allow(missing_docs)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(trivial_casts)] +#![allow(unused_imports)] +#![allow(unused_results)] +//! Generated file from `contracts/pair_stable/src/response.proto` + +/// Generated files are compatible only with the same version +/// of protobuf runtime. +// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_25_2; + +#[derive(PartialEq,Clone,Default)] +pub struct MsgInstantiateContractResponse { + // message fields + pub contract_address: ::std::string::String, + pub data: ::std::vec::Vec, + // special fields + pub unknown_fields: ::protobuf::UnknownFields, + pub cached_size: ::protobuf::CachedSize, +} + +impl<'a> ::std::default::Default for &'a MsgInstantiateContractResponse { + fn default() -> &'a MsgInstantiateContractResponse { + ::default_instance() + } +} + +impl MsgInstantiateContractResponse { + pub fn new() -> MsgInstantiateContractResponse { + ::std::default::Default::default() + } + + // string contract_address = 1; + + + pub fn get_contract_address(&self) -> &str { + &self.contract_address + } + pub fn clear_contract_address(&mut self) { + self.contract_address.clear(); + } + + // Param is passed by value, moved + pub fn set_contract_address(&mut self, v: ::std::string::String) { + self.contract_address = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_contract_address(&mut self) -> &mut ::std::string::String { + &mut self.contract_address + } + + // Take field + pub fn take_contract_address(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.contract_address, ::std::string::String::new()) + } + + // bytes data = 2; + + + pub fn get_data(&self) -> &[u8] { + &self.data + } + pub fn clear_data(&mut self) { + self.data.clear(); + } + + // Param is passed by value, moved + pub fn set_data(&mut self, v: ::std::vec::Vec) { + self.data = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_data(&mut self) -> &mut ::std::vec::Vec { + &mut self.data + } + + // Take field + pub fn take_data(&mut self) -> ::std::vec::Vec { + ::std::mem::replace(&mut self.data, ::std::vec::Vec::new()) + } +} + +impl ::protobuf::Message for MsgInstantiateContractResponse { + fn is_initialized(&self) -> bool { + true + } + + fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> { + while !is.eof()? { + let (field_number, wire_type) = is.read_tag_unpack()?; + match field_number { + 1 => { + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.contract_address)?; + }, + 2 => { + ::protobuf::rt::read_singular_proto3_bytes_into(wire_type, is, &mut self.data)?; + }, + _ => { + ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?; + }, + }; + } + ::std::result::Result::Ok(()) + } + + // Compute sizes of nested messages + #[allow(unused_variables)] + fn compute_size(&self) -> u32 { + let mut my_size = 0; + if !self.contract_address.is_empty() { + my_size += ::protobuf::rt::string_size(1, &self.contract_address); + } + if !self.data.is_empty() { + my_size += ::protobuf::rt::bytes_size(2, &self.data); + } + my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields()); + self.cached_size.set(my_size); + my_size + } + + fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> { + if !self.contract_address.is_empty() { + os.write_string(1, &self.contract_address)?; + } + if !self.data.is_empty() { + os.write_bytes(2, &self.data)?; + } + os.write_unknown_fields(self.get_unknown_fields())?; + ::std::result::Result::Ok(()) + } + + fn get_cached_size(&self) -> u32 { + self.cached_size.get() + } + + fn get_unknown_fields(&self) -> &::protobuf::UnknownFields { + &self.unknown_fields + } + + fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields { + &mut self.unknown_fields + } + + fn as_any(&self) -> &dyn (::std::any::Any) { + self as &dyn (::std::any::Any) + } + fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) { + self as &mut dyn (::std::any::Any) + } + fn into_any(self: ::std::boxed::Box) -> ::std::boxed::Box { + self + } + + fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor { + Self::descriptor_static() + } + + fn new() -> MsgInstantiateContractResponse { + MsgInstantiateContractResponse::new() + } + + fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor { + static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT; + descriptor.get(|| { + let mut fields = ::std::vec::Vec::new(); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( + "contract_address", + |m: &MsgInstantiateContractResponse| { &m.contract_address }, + |m: &mut MsgInstantiateContractResponse| { &mut m.contract_address }, + )); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeBytes>( + "data", + |m: &MsgInstantiateContractResponse| { &m.data }, + |m: &mut MsgInstantiateContractResponse| { &mut m.data }, + )); + ::protobuf::reflect::MessageDescriptor::new_pb_name::( + "MsgInstantiateContractResponse", + fields, + file_descriptor_proto() + ) + }) + } + + fn default_instance() -> &'static MsgInstantiateContractResponse { + static instance: ::protobuf::rt::LazyV2 = ::protobuf::rt::LazyV2::INIT; + instance.get(MsgInstantiateContractResponse::new) + } +} + +impl ::protobuf::Clear for MsgInstantiateContractResponse { + fn clear(&mut self) { + self.contract_address.clear(); + self.data.clear(); + self.unknown_fields.clear(); + } +} + +impl ::std::fmt::Debug for MsgInstantiateContractResponse { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + ::protobuf::text_format::fmt(self, f) + } +} + +impl ::protobuf::reflect::ProtobufValue for MsgInstantiateContractResponse { + fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef { + ::protobuf::reflect::ReflectValueRef::Message(self) + } +} + +static file_descriptor_proto_data: &'static [u8] = b"\ + \n(contracts/pair_stable/src/response.proto\"_\n\x1eMsgInstantiateContra\ + ctResponse\x12)\n\x10contract_address\x18\x01\x20\x01(\tR\x0fcontractAdd\ + ress\x12\x12\n\x04data\x18\x02\x20\x01(\x0cR\x04dataJ\xf8\x02\n\x06\x12\ + \x04\0\0\x08\x01\n\x08\n\x01\x0c\x12\x03\0\0\x12\n_\n\x02\x04\0\x12\x04\ + \x03\0\x08\x01\x1aS\x20MsgInstantiateContractResponse\x20defines\x20the\ + \x20Msg/InstantiateContract\x20response\x20type.\n\n\n\n\x03\x04\0\x01\ + \x12\x03\x03\x08&\nR\n\x04\x04\0\x02\0\x12\x03\x05\x02\x1e\x1aE\x20Contr\ + actAddress\x20is\x20the\x20bech32\x20address\x20of\x20the\x20new\x20cont\ + ract\x20instance.\n\n\x0c\n\x05\x04\0\x02\0\x05\x12\x03\x05\x02\x08\n\ + \x0c\n\x05\x04\0\x02\0\x01\x12\x03\x05\t\x19\n\x0c\n\x05\x04\0\x02\0\x03\ + \x12\x03\x05\x1c\x1d\nO\n\x04\x04\0\x02\x01\x12\x03\x07\x02\x11\x1aB\x20\ + Data\x20contains\x20base64-encoded\x20bytes\x20to\x20returned\x20from\ + \x20the\x20contract\n\n\x0c\n\x05\x04\0\x02\x01\x05\x12\x03\x07\x02\x07\ + \n\x0c\n\x05\x04\0\x02\x01\x01\x12\x03\x07\x08\x0c\n\x0c\n\x05\x04\0\x02\ + \x01\x03\x12\x03\x07\x0f\x10b\x06proto3\ +"; + +static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT; + +fn parse_descriptor_proto() -> ::protobuf::descriptor::FileDescriptorProto { + ::protobuf::Message::parse_from_bytes(file_descriptor_proto_data).unwrap() +} + +pub fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto { + file_descriptor_proto_lazy.get(|| { + parse_descriptor_proto() + }) +} diff --git a/contracts/astroport/pair_stable/src/state.rs b/contracts/astroport/pair_stable/src/state.rs new file mode 100644 index 00000000..d3655fa1 --- /dev/null +++ b/contracts/astroport/pair_stable/src/state.rs @@ -0,0 +1,56 @@ +use astroport::asset::{AssetInfo, PairInfo}; +use astroport::common::OwnershipProposal; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, DepsMut, StdResult, Storage, Uint128}; +use cw_storage_plus::{Item, Map}; + +/// This structure stores the main stableswap pair parameters. +#[cw_serde] +pub struct Config { + /// The contract owner + pub owner: Option, + /// The pair information stored in a [`PairInfo`] struct + pub pair_info: PairInfo, + /// The factory contract address + pub factory_addr: Addr, + /// The last timestamp when the pair contract update the asset cumulative prices + pub block_time_last: u64, + /// This is the current amplification used in the pool + pub init_amp: u64, + /// This is the start time when amplification starts to scale up or down + pub init_amp_time: u64, + /// This is the target amplification to reach at `next_amp_time` + pub next_amp: u64, + /// This is the timestamp when the current pool amplification should be `next_amp` + pub next_amp_time: u64, + /// The greatest precision of assets in the pool + pub greatest_precision: u8, + /// The vector contains cumulative prices for each pair of assets in the pool + pub cumulative_prices: Vec<(AssetInfo, AssetInfo, Uint128)>, +} + +pub const CONFIG: Item = Item::new("config"); + +/// Stores map of AssetInfo (as String) -> precision +const PRECISIONS: Map = Map::new("precisions"); + +/// Stores the latest contract ownership transfer proposal +pub const OWNERSHIP_PROPOSAL: Item = Item::new("ownership_proposal"); + +/// Store all token precisions and return the greatest one. +pub(crate) fn store_precisions(deps: DepsMut, asset_infos: &[AssetInfo]) -> StdResult { + let mut max = 0u8; + + for asset_info in asset_infos { + let precision = asset_info.decimals(&deps.querier)?; + max = max.max(precision); + PRECISIONS.save(deps.storage, asset_info.to_string(), &precision)?; + } + + Ok(max) +} + +/// Loads precision of the given asset info. +pub(crate) fn get_precision(storage: &dyn Storage, asset_info: &AssetInfo) -> StdResult { + PRECISIONS.load(storage, asset_info.to_string()) +} diff --git a/contracts/astroport/pair_stable/src/testing.rs b/contracts/astroport/pair_stable/src/testing.rs new file mode 100644 index 00000000..63ffb66f --- /dev/null +++ b/contracts/astroport/pair_stable/src/testing.rs @@ -0,0 +1,1368 @@ +use crate::contract::{ + assert_max_spread, execute, instantiate, query_pool, query_reverse_simulation, query_share, + query_simulation, reply, +}; +use crate::error::ContractError; +use crate::math::AMP_PRECISION; +use crate::mock_querier::mock_dependencies; + +use crate::response::MsgInstantiateContractResponse; +use crate::state::{store_precisions, Config, CONFIG}; +use astroport::asset::{native_asset, native_asset_info, Asset, AssetInfo, PairInfo}; + +use astroport::pair::{ + Cw20HookMsg, ExecuteMsg, InstantiateMsg, PoolResponse, SimulationResponse, StablePoolParams, + TWAP_PRECISION, +}; +use astroport::token::InstantiateMsg as TokenInstantiateMsg; +use cosmwasm_std::testing::{mock_env, mock_info, MOCK_CONTRACT_ADDR}; +use cosmwasm_std::{ + attr, coin, to_binary, Addr, BankMsg, BlockInfo, Coin, CosmosMsg, Decimal, DepsMut, Env, Reply, + ReplyOn, Response, StdError, SubMsg, SubMsgResponse, SubMsgResult, Timestamp, Uint128, WasmMsg, +}; +use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg, MinterResponse}; +use itertools::Itertools; +use protobuf::Message; + +fn store_liquidity_token(deps: DepsMut, msg_id: u64, contract_addr: String) { + let data = MsgInstantiateContractResponse { + contract_address: contract_addr, + data: vec![], + unknown_fields: Default::default(), + cached_size: Default::default(), + } + .write_to_bytes() + .unwrap(); + + let reply_msg = Reply { + id: msg_id, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: Some(data.into()), + }), + }; + + let _res = reply(deps, mock_env(), reply_msg).unwrap(); +} + +#[test] +fn proper_initialization() { + let mut deps = mock_dependencies(&[]); + + deps.querier.with_token_balances(&[( + &String::from("asset0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &Uint128::new(123u128))], + )]); + + let msg = InstantiateMsg { + factory_addr: String::from("factory"), + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + ], + token_code_id: 10u64, + init_params: Some( + to_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + let sender = "addr0000"; + // We can just call .unwrap() to assert this was a success + let env = mock_env(); + let info = mock_info(sender, &[]); + let res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!( + res.messages, + vec![SubMsg { + msg: WasmMsg::Instantiate { + code_id: 10u64, + msg: to_binary(&TokenInstantiateMsg { + name: "UUSD-MAPP-LP".to_string(), + symbol: "uLP".to_string(), + decimals: 6, + initial_balances: vec![], + mint: Some(MinterResponse { + minter: String::from(MOCK_CONTRACT_ADDR), + cap: None, + }), + marketing: None + }) + .unwrap(), + funds: vec![], + admin: None, + label: String::from("Astroport LP token"), + } + .into(), + id: 1, + gas_limit: None, + reply_on: ReplyOn::Success + },] + ); + + // Store liquidity token + store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); + + // It worked, let's query the state + let pair_info = CONFIG.load(deps.as_ref().storage).unwrap().pair_info; + assert_eq!(Addr::unchecked("liquidity0000"), pair_info.liquidity_token); + assert_eq!( + pair_info.asset_infos, + vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000") + } + ] + ); +} + +#[test] +fn provide_liquidity() { + let mut deps = mock_dependencies(&[Coin { + denom: "uusd".to_string(), + amount: Uint128::new(200_000000000000000000u128), + }]); + + deps.querier.with_token_balances(&[ + ( + &String::from("asset0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &Uint128::new(0))], + ), + ( + &String::from("liquidity0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &Uint128::new(0))], + ), + ]); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + ], + token_code_id: 10u64, + factory_addr: String::from("factory"), + init_params: Some( + to_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + // Store the liquidity token + store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); + + // Successfully provide liquidity for the existing pool + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: Uint128::from(100_000000000000000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Uint128::from(100_000000000000000000u128), + }, + ], + slippage_tolerance: None, + auto_stake: None, + receiver: None, + }; + + let env = mock_env(); + let info = mock_info( + "addr0000", + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::from(100_000000000000000000u128), + }], + ); + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + let transfer_from_msg = res.messages.get(0).expect("no message"); + let mint_min_liquidity_msg = res.messages.get(1).expect("no message"); + let mint_receiver_msg = res.messages.get(2).expect("no message"); + + assert_eq!( + transfer_from_msg, + &SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("asset0000"), + msg: to_binary(&Cw20ExecuteMsg::TransferFrom { + owner: String::from("addr0000"), + recipient: String::from(MOCK_CONTRACT_ADDR), + amount: Uint128::from(100_000000000000000000u128), + }) + .unwrap(), + funds: vec![], + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never + } + ); + + assert_eq!( + mint_min_liquidity_msg, + &SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("liquidity0000"), + msg: to_binary(&Cw20ExecuteMsg::Mint { + recipient: String::from(MOCK_CONTRACT_ADDR), + amount: Uint128::from(1000_u128), + }) + .unwrap(), + funds: vec![], + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + } + ); + + assert_eq!( + mint_receiver_msg, + &SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("liquidity0000"), + msg: to_binary(&Cw20ExecuteMsg::Mint { + recipient: String::from("addr0000"), + amount: Uint128::from(299_814_698_523_989_456_628u128), + }) + .unwrap(), + funds: vec![], + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + } + ); + + // Provide more liquidity using a 1:2 ratio + deps.querier.with_balance(&[( + &String::from(MOCK_CONTRACT_ADDR), + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::new(200_000000000000000000 + 200_000000000000000000 /* user deposit must be pre-applied */), + }], + )]); + + deps.querier.with_token_balances(&[ + ( + &String::from("liquidity0000"), + &[( + &String::from(MOCK_CONTRACT_ADDR), + &Uint128::new(100_000000000000000000), + )], + ), + ( + &String::from("asset0000"), + &[( + &String::from(MOCK_CONTRACT_ADDR), + &Uint128::new(200_000000000000000000), + )], + ), + ]); + + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: Uint128::from(100_000000000000000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Uint128::from(200_000000000000000000u128), + }, + ], + slippage_tolerance: None, + auto_stake: None, + receiver: None, + }; + + let env = mock_env_with_block_time(env.block.time.seconds() + 1000); + let info = mock_info( + "addr0000", + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::from(200_000000000000000000u128), + }], + ); + + let res: Response = execute(deps.as_mut(), env, info, msg).unwrap(); + let transfer_from_msg = res.messages.get(0).expect("no message"); + let mint_msg = res.messages.get(1).expect("no message"); + assert_eq!( + transfer_from_msg, + &SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("asset0000"), + msg: to_binary(&Cw20ExecuteMsg::TransferFrom { + owner: String::from("addr0000"), + recipient: String::from(MOCK_CONTRACT_ADDR), + amount: Uint128::from(100_000000000000000000u128), + }) + .unwrap(), + funds: vec![], + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + } + ); + assert_eq!( + mint_msg, + &SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("liquidity0000"), + msg: to_binary(&Cw20ExecuteMsg::Mint { + recipient: String::from("addr0000"), + amount: Uint128::from(74_944_452_888_487_171_363u128), + }) + .unwrap(), + funds: vec![], + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + } + ); + + // Check wrong argument + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: Uint128::from(100_000000000000000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Uint128::from(50_000000000000000000u128), + }, + ], + slippage_tolerance: None, + auto_stake: None, + receiver: None, + }; + + let env = mock_env(); + let info = mock_info( + "addr0000", + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::from(100_000000000000000000u128), + }], + ); + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + match res { + ContractError::Std(StdError::GenericErr { msg, .. }) => assert_eq!( + msg, + "Native token balance mismatch between the argument and the transferred".to_string() + ), + _ => panic!("Must return generic error"), + } + + // Initialize token balances with a ratio of 1:1 + deps.querier.with_balance(&[( + &String::from(MOCK_CONTRACT_ADDR), + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000000000000000000 + 100_000000000000000000 /* user deposit must be pre-applied */), + }], + )]); + + deps.querier.with_token_balances(&[ + ( + &String::from("liquidity0000"), + &[( + &String::from(MOCK_CONTRACT_ADDR), + &Uint128::new(100_000000000000000000), + )], + ), + ( + &String::from("asset0000"), + &[( + &String::from(MOCK_CONTRACT_ADDR), + &Uint128::new(100_000000000000000000), + )], + ), + ]); + + // Initialize token balances with a ratio of 1:1 + deps.querier.with_balance(&[( + &String::from(MOCK_CONTRACT_ADDR), + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000000000000000000 + 98_000000000000000000 /* user deposit must be pre-applied */), + }], + )]); + + // Initialize token balances with a ratio of 1:1 + deps.querier.with_balance(&[( + &String::from(MOCK_CONTRACT_ADDR), + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000000000000000000 + 100_000000000000000000 /* user deposit must be pre-applied */), + }], + )]); + + // Successfully provide liquidity + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: Uint128::from(99_000000000000000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Uint128::from(100_000000000000000000u128), + }, + ], + slippage_tolerance: Some(Decimal::percent(1)), + auto_stake: None, + receiver: None, + }; + + let env = mock_env_with_block_time(env.block.time.seconds() + 1000); + let info = mock_info( + "addr0001", + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::from(100_000000000000000000u128), + }], + ); + let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + + // Initialize token balances with a ratio of 1:1 + deps.querier.with_balance(&[( + &String::from(MOCK_CONTRACT_ADDR), + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000000000000000000 + 99_000000000000000000 /* user deposit must be pre-applied */), + }], + )]); + + // Successfully provide liquidity + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: Uint128::from(100_000000000000000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Uint128::from(99_000000000000000000u128), + }, + ], + slippage_tolerance: Some(Decimal::percent(1)), + auto_stake: None, + receiver: None, + }; + + let env = mock_env_with_block_time(env.block.time.seconds() + 1000); + let info = mock_info( + "addr0001", + &[Coin { + denom: "uusd".to_string(), + amount: Uint128::from(99_000000000000000000u128), + }], + ); + let _res = execute(deps.as_mut(), env, info, msg).unwrap(); +} + +#[test] +fn withdraw_liquidity() { + let mut deps = mock_dependencies(&[Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100u128), + }]); + + deps.querier.with_token_balances(&[ + ( + &String::from("liquidity0000"), + &[(&String::from("addr0000"), &Uint128::new(100u128))], + ), + ( + &String::from("asset0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &Uint128::new(100u128))], + ), + ]); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + ], + token_code_id: 10u64, + factory_addr: String::from("factory"), + init_params: Some( + to_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + // Store the liquidity token + store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); + + // Withdraw liquidity + let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: String::from("addr0000"), + msg: to_binary(&Cw20HookMsg::WithdrawLiquidity { assets: vec![] }).unwrap(), + amount: Uint128::new(100u128), + }); + + let env = mock_env(); + let info = mock_info("liquidity0000", &[]); + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + let log_withdrawn_share = res.attributes.get(2).expect("no log"); + let log_refund_assets = res.attributes.get(3).expect("no log"); + let msg_refund_0 = res.messages.get(0).expect("no message"); + let msg_refund_1 = res.messages.get(1).expect("no message"); + let msg_burn_liquidity = res.messages.get(2).expect("no message"); + assert_eq!( + msg_refund_0, + &SubMsg { + msg: CosmosMsg::Bank(BankMsg::Send { + to_address: String::from("addr0000"), + amount: vec![Coin { + denom: "uusd".to_string(), + amount: Uint128::from(100u128), + }], + }), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + } + ); + assert_eq!( + msg_refund_1, + &SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("asset0000"), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: String::from("addr0000"), + amount: Uint128::from(100u128), + }) + .unwrap(), + funds: vec![], + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + } + ); + assert_eq!( + msg_burn_liquidity, + &SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("liquidity0000"), + msg: to_binary(&Cw20ExecuteMsg::Burn { + amount: Uint128::from(100u128), + }) + .unwrap(), + funds: vec![], + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + } + ); + + assert_eq!( + log_withdrawn_share, + &attr("withdrawn_share", 100u128.to_string()) + ); + assert_eq!( + log_refund_assets, + &attr("refund_assets", "100uusd, 100asset0000") + ); +} + +#[test] +fn try_native_to_token() { + let total_share = Uint128::new(30000000000u128); + let asset_pool_amount = Uint128::new(20000000000u128); + let collateral_pool_amount = Uint128::new(30000000000u128); + let offer_amount = Uint128::new(1500000000u128); + + let mut deps = mock_dependencies(&[Coin { + denom: "uusd".to_string(), + amount: collateral_pool_amount + offer_amount, /* user deposit must be pre-applied */ + }]); + + deps.querier.with_token_balances(&[ + ( + &String::from("liquidity0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &total_share)], + ), + ( + &String::from("asset0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &asset_pool_amount)], + ), + ]); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + ], + token_code_id: 10u64, + factory_addr: String::from("factory"), + init_params: Some( + to_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + let env = mock_env_with_block_time(100); + let info = mock_info("addr0000", &[]); + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + // Store the liquidity token + store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); + + // Normal swap + let msg = ExecuteMsg::Swap { + offer_asset: Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: offer_amount, + }, + ask_asset_info: None, + belief_price: None, + max_spread: Some(Decimal::percent(50)), + to: None, + }; + let env = mock_env_with_block_time(1000); + let info = mock_info( + "addr0000", + &[Coin { + denom: "uusd".to_string(), + amount: offer_amount, + }], + ); + + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + let msg_transfer = res.messages.get(0).expect("no message"); + + let model: StableSwapModel = StableSwapModel::new( + 100, + vec![collateral_pool_amount.into(), asset_pool_amount.into()], + 2, + ); + + let sim_result = model.sim_exchange(0, 1, offer_amount.into()); + + let expected_ret_amount = Uint128::new(sim_result); + let expected_spread_amount = offer_amount.saturating_sub(expected_ret_amount); + let expected_commission_amount = expected_ret_amount.multiply_ratio(3u128, 1000u128); // 0.3% + let expected_maker_fee_amount = expected_commission_amount.multiply_ratio(166u128, 1000u128); + + let expected_return_amount = expected_ret_amount + .checked_sub(expected_commission_amount) + .unwrap(); + + // Check simulation result + deps.querier.with_balance(&[( + &String::from(MOCK_CONTRACT_ADDR), + &[Coin { + denom: "uusd".to_string(), + amount: collateral_pool_amount, /* user deposit must be pre-applied */ + }], + )]); + + let simulation_res: SimulationResponse = query_simulation( + deps.as_ref(), + env, + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: offer_amount, + }, + None, + ) + .unwrap(); + assert_eq!(expected_return_amount, simulation_res.return_amount); + assert_eq!(expected_commission_amount, simulation_res.commission_amount); + assert_eq!(expected_spread_amount, simulation_res.spread_amount); + + assert_eq!( + res.attributes, + vec![ + attr("action", "swap"), + attr("sender", "addr0000"), + attr("receiver", "addr0000"), + attr("offer_asset", "uusd"), + attr("ask_asset", "asset0000"), + attr("offer_amount", offer_amount.to_string()), + attr("return_amount", expected_return_amount.to_string()), + attr("spread_amount", expected_spread_amount.to_string()), + attr("commission_amount", expected_commission_amount.to_string()), + attr("maker_fee_amount", expected_maker_fee_amount.to_string()), + ] + ); + + assert_eq!( + &SubMsg { + msg: WasmMsg::Execute { + contract_addr: String::from("asset0000"), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: String::from("addr0000"), + amount: expected_return_amount, + }) + .unwrap(), + funds: vec![], + } + .into(), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + }, + msg_transfer, + ); +} + +#[test] +fn try_token_to_native() { + let total_share = Uint128::new(30000000000u128); + let asset_pool_amount = Uint128::new(20000000000u128); + let collateral_pool_amount = Uint128::new(30000000000u128); + let offer_amount = Uint128::new(1500000000u128); + + let mut deps = mock_dependencies(&[Coin { + denom: "uusd".to_string(), + amount: collateral_pool_amount, + }]); + + deps.querier.with_token_balances(&[ + ( + &String::from("liquidity0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &total_share)], + ), + ( + &String::from("asset0000"), + &[( + &String::from(MOCK_CONTRACT_ADDR), + &(asset_pool_amount + offer_amount), + )], + ), + ]); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + ], + token_code_id: 10u64, + factory_addr: String::from("factory"), + init_params: Some( + to_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + let env = mock_env_with_block_time(100); + let info = mock_info("addr0000", &[]); + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + // Store the liquidity token + store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); + + // Unauthorized access; can not execute swap directy for token swap + let msg = ExecuteMsg::Swap { + offer_asset: Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: offer_amount, + }, + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: None, + }; + let env = mock_env_with_block_time(1000); + let info = mock_info("addr0000", &[]); + let res = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(res, ContractError::Unauthorized {}); + + // Normal sell + let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: String::from("addr0000"), + amount: offer_amount, + msg: to_binary(&Cw20HookMsg::Swap { + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: None, + }) + .unwrap(), + }); + let env = mock_env_with_block_time(1000); + let info = mock_info("asset0000", &[]); + + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + let msg_transfer = res.messages.get(0).expect("no message"); + + let model: StableSwapModel = StableSwapModel::new( + 100, + vec![collateral_pool_amount.into(), asset_pool_amount.into()], + 2, + ); + + let sim_result = model.sim_exchange(1, 0, offer_amount.into()); + + let expected_ret_amount = Uint128::new(sim_result); + let expected_spread_amount = offer_amount.saturating_sub(expected_ret_amount); + let expected_commission_amount = expected_ret_amount.multiply_ratio(3u128, 1000u128); // 0.3% + let expected_maker_fee_amount = expected_commission_amount.multiply_ratio(166u128, 1000u128); + + let expected_return_amount = expected_ret_amount + .checked_sub(expected_commission_amount) + .unwrap(); + + // Check simulation result + // Return asset token balance as normal + deps.querier.with_token_balances(&[ + ( + &String::from("liquidity0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &total_share)], + ), + ( + &String::from("asset0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &(asset_pool_amount))], + ), + ]); + + let simulation_res: SimulationResponse = query_simulation( + deps.as_ref(), + env, + Asset { + amount: offer_amount, + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + }, + None, + ) + .unwrap(); + assert_eq!(expected_return_amount, simulation_res.return_amount); + assert_eq!(expected_commission_amount, simulation_res.commission_amount); + assert_eq!(expected_spread_amount, simulation_res.spread_amount); + + assert_eq!( + res.attributes, + vec![ + attr("action", "swap"), + attr("sender", "addr0000"), + attr("receiver", "addr0000"), + attr("offer_asset", "asset0000"), + attr("ask_asset", "uusd"), + attr("offer_amount", offer_amount.to_string()), + attr("return_amount", expected_return_amount.to_string()), + attr("spread_amount", expected_spread_amount.to_string()), + attr("commission_amount", expected_commission_amount.to_string()), + attr("maker_fee_amount", expected_maker_fee_amount.to_string()), + ] + ); + + assert_eq!( + &SubMsg { + msg: CosmosMsg::Bank(BankMsg::Send { + to_address: String::from("addr0000"), + amount: vec![Coin { + denom: "uusd".to_string(), + amount: expected_return_amount, + }], + }), + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + }, + msg_transfer, + ); + + // Failed due to non asset token contract being used in a swap + let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: String::from("addr0000"), + amount: offer_amount, + msg: to_binary(&Cw20HookMsg::Swap { + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: None, + }) + .unwrap(), + }); + let env = mock_env_with_block_time(1000); + let info = mock_info("liquidtity0000", &[]); + let res = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(res, ContractError::Unauthorized {}); +} + +#[test] +fn test_max_spread() { + assert_max_spread( + Some(Decimal::from_ratio(1200u128, 1u128)), + Some(Decimal::percent(1)), + Uint128::from(1200000000u128), + Uint128::from(989999u128), + Uint128::zero(), + ) + .unwrap_err(); + + assert_max_spread( + Some(Decimal::from_ratio(1200u128, 1u128)), + Some(Decimal::percent(1)), + Uint128::from(1200000000u128), + Uint128::from(990000u128), + Uint128::zero(), + ) + .unwrap(); + + assert_max_spread( + None, + Some(Decimal::percent(1)), + Uint128::zero(), + Uint128::from(989999u128), + Uint128::from(10001u128), + ) + .unwrap_err(); + + assert_max_spread( + None, + Some(Decimal::percent(1)), + Uint128::zero(), + Uint128::from(990000u128), + Uint128::from(10000u128), + ) + .unwrap(); +} + +#[test] +fn test_query_pool() { + let total_share_amount = Uint128::from(111u128); + let asset_0_amount = Uint128::from(222u128); + let asset_1_amount = Uint128::from(333u128); + let mut deps = mock_dependencies(&[Coin { + denom: "uusd".to_string(), + amount: asset_0_amount, + }]); + + deps.querier.with_token_balances(&[ + ( + &String::from("asset0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &asset_1_amount)], + ), + ( + &String::from("liquidity0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &total_share_amount)], + ), + ]); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + ], + token_code_id: 10u64, + factory_addr: String::from("factory"), + init_params: Some( + to_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + // Store the liquidity token + store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); + + let res: PoolResponse = query_pool(deps.as_ref()).unwrap(); + + assert_eq!( + res.assets, + [ + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: asset_0_amount + }, + Asset { + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + amount: asset_1_amount + } + ] + ); + assert_eq!(res.total_share, total_share_amount); +} + +#[test] +fn test_query_share() { + let total_share_amount = Uint128::from(500u128); + let asset_0_amount = Uint128::from(250u128); + let asset_1_amount = Uint128::from(1000u128); + let mut deps = mock_dependencies(&[Coin { + denom: "uusd".to_string(), + amount: asset_0_amount, + }]); + + deps.querier.with_token_balances(&[ + ( + &String::from("asset0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &asset_1_amount)], + ), + ( + &String::from("liquidity0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &total_share_amount)], + ), + ]); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + ], + token_code_id: 10u64, + factory_addr: String::from("factory"), + init_params: Some( + to_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + // We can just call .unwrap() to assert this was a success + let _res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + // Store the liquidity token + store_liquidity_token(deps.as_mut(), 1, "liquidity0000".to_string()); + + let res = query_share(deps.as_ref(), Uint128::new(250)).unwrap(); + + assert_eq!(res[0].amount, Uint128::new(125)); + assert_eq!(res[1].amount, Uint128::new(500)); +} + +#[test] +fn test_accumulate_prices() { + struct Case { + block_time: u64, + block_time_last: u64, + last0: u128, + last1: u128, + x_amount: u128, + y_amount: u128, + } + + struct Result { + block_time_last: u64, + cumulative_price_x: u128, + cumulative_price_y: u128, + } + + let price_precision = 10u128.pow(TWAP_PRECISION.into()); + + let test_cases: Vec<(Case, Result)> = vec![ + ( + Case { + block_time: 1000, + block_time_last: 0, + last0: 0, + last1: 0, + x_amount: 250_000000, + y_amount: 500_000000, + }, + Result { + block_time_last: 1000, + cumulative_price_x: 1008, + cumulative_price_y: 991, + }, + ), + // Same block height, no changes + ( + Case { + block_time: 1000, + block_time_last: 1000, + last0: price_precision, + last1: 2 * price_precision, + x_amount: 250_000000, + y_amount: 500_000000, + }, + Result { + block_time_last: 1000, + cumulative_price_x: 1, + cumulative_price_y: 2, + }, + ), + ( + Case { + block_time: 1500, + block_time_last: 1000, + last0: 500 * price_precision, + last1: 2000 * price_precision, + x_amount: 250_000000, + y_amount: 500_000000, + }, + Result { + block_time_last: 1500, + cumulative_price_x: 1004, + cumulative_price_y: 2495, + }, + ), + ]; + + for test_case in test_cases { + let (case, result) = test_case; + let asset_x = native_asset_info("uusd".to_string()); + let asset_y = native_asset_info("uluna".to_string()); + let mut deps = mock_dependencies(&[]); + store_precisions(deps.as_mut(), &[asset_x.clone(), asset_y.clone()]).unwrap(); + + let cumulative_prices = vec![ + (asset_x.clone(), asset_y.clone(), case.last0.into()), + (asset_y.clone(), asset_x.clone(), case.last1.into()), + ]; + let pools = vec![ + native_asset(asset_x.to_string(), case.x_amount.into()), + native_asset(asset_y.to_string(), case.y_amount.into()), + ]; + + let env = mock_env_with_block_time(case.block_time); + let mut config = Config { + owner: None, + pair_info: PairInfo { + asset_infos: vec![asset_x, asset_y], + contract_addr: Addr::unchecked(MOCK_CONTRACT_ADDR), + liquidity_token: Addr::unchecked("lp_token"), + pair_type: PairType::Stable {}, + }, + factory_addr: Addr::unchecked("factory"), + block_time_last: case.block_time_last, + init_amp: 100 * AMP_PRECISION, + init_amp_time: env.block.time.seconds(), + next_amp: 100 * AMP_PRECISION, + next_amp_time: env.block.time.seconds(), + greatest_precision: 6, + cumulative_prices, + }; + + let pools = pools + .iter() + .cloned() + .map(|pool| pool.to_decimal_asset(NATIVE_TOKEN_PRECISION).unwrap()) + .collect_vec(); + + accumulate_prices(deps.as_ref(), env.clone(), &mut config, &pools).unwrap(); + + assert_eq!(config.block_time_last, result.block_time_last); + assert_eq!( + config.cumulative_prices[0].2 / Uint128::from(price_precision), + Uint128::new(result.cumulative_price_x) + ); + assert_eq!( + config.cumulative_prices[1].2 / Uint128::from(price_precision), + Uint128::new(result.cumulative_price_y) + ); + } +} + +fn mock_env_with_block_time(time: u64) -> Env { + let mut env = mock_env(); + env.block = BlockInfo { + height: 1, + time: Timestamp::from_seconds(time), + chain_id: "columbus".to_string(), + }; + env +} + +use crate::utils::{accumulate_prices, compute_swap, select_pools}; +use astroport::factory::PairType; +use astroport::querier::NATIVE_TOKEN_PRECISION; +use proptest::prelude::*; +use sim::StableSwapModel; + +proptest! { + #[test] + fn constant_product_swap_no_fee( + balance_in in 100..1_000_000_000_000_000_000u128, + balance_out in 100..1_000_000_000_000_000_000u128, + amount_in in 100..100_000_000_000u128, + amp in 1..150u64 + ) { + prop_assume!(amount_in < balance_in && balance_out > balance_in); + + let offer_asset = native_asset("uusd".to_string(), Uint128::from(amount_in)); + let ask_asset = native_asset_info("uluna".to_string()); + + let msg = InstantiateMsg { + factory_addr: String::from("factory"), + asset_infos: vec![offer_asset.info.clone(), ask_asset], + token_code_id: 10u64, + init_params: Some(to_binary(&StablePoolParams { amp, owner: None }).unwrap()), + }; + + let env = mock_env(); + let info = mock_info("owner", &[]); + let mut deps = mock_dependencies(&[coin(balance_in, "uusd"), coin(balance_out, "uluna")]); + + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + let config = CONFIG.load(deps.as_ref().storage).unwrap(); + let pools = config + .pair_info + .query_pools_decimal(&deps.as_ref().querier, &env.contract.address) + .unwrap(); + let (offer_pool, ask_pool) = + select_pools(Some(&offer_asset.info), None, &pools).unwrap(); + + let result = compute_swap( + deps.as_ref().storage, + &env, + &config, + &offer_asset.to_decimal_asset(offer_asset.info.decimals(&deps.as_ref().querier).unwrap()).unwrap(), + &offer_pool, + &ask_pool, + &pools, + ) + .unwrap(); + + let model: StableSwapModel = StableSwapModel::new(amp.into(), vec![balance_in, balance_out], 2); + let sim_result = model.sim_exchange(0, 1, amount_in); + + let diff = (sim_result as i128 - result.return_amount.u128() as i128).abs(); + + assert!( + diff <= 4, + "result={}, sim_result={}, amp={}, amount_in={}, balance_in={}, balance_out={}, diff={}", + result.return_amount, + sim_result, + amp, + amount_in, + balance_in, + balance_out, + diff + ); + + let reverse_result = query_reverse_simulation( + deps.as_ref(), + env, + native_asset("uluna".to_string(), result.return_amount), + None, + ) + .unwrap(); + + let amount_in_f = amount_in as f64; + let reverse_diff = + (reverse_result.offer_amount.u128() as f64 - amount_in_f) / amount_in_f * 100.; + + assert!( + reverse_diff <= 0.5, + "result={}, sim_result={}, amp={}, amount_out={}, balance_in={}, balance_out={}, diff(%)={}", + reverse_result.offer_amount.u128(), + amount_in, + amp, + result.return_amount.u128(), + balance_in, + balance_out, + reverse_diff + ); + } +} diff --git a/contracts/astroport/pair_stable/src/utils.rs b/contracts/astroport/pair_stable/src/utils.rs new file mode 100644 index 00000000..a70aab55 --- /dev/null +++ b/contracts/astroport/pair_stable/src/utils.rs @@ -0,0 +1,338 @@ +use cosmwasm_std::{ + to_binary, wasm_execute, Addr, Api, CosmosMsg, Decimal, Decimal256, Deps, Env, QuerierWrapper, + StdResult, Storage, Uint128, Uint64, +}; +use cw20::Cw20ExecuteMsg; +use itertools::Itertools; +use std::cmp::Ordering; + +use astroport::asset::{Asset, AssetInfo, Decimal256Ext, DecimalAsset}; +use astroport::pair::TWAP_PRECISION; +use astroport::querier::query_factory_config; + +use crate::error::ContractError; +use crate::math::calc_y; +use crate::state::{get_precision, Config}; + +/// Helper function to check if the given asset infos are valid. +pub(crate) fn check_asset_infos( + api: &dyn Api, + asset_infos: &[AssetInfo], +) -> Result<(), ContractError> { + if !asset_infos.iter().all_unique() { + return Err(ContractError::DoublingAssets {}); + } + + asset_infos + .iter() + .try_for_each(|asset_info| asset_info.check(api)) + .map_err(Into::into) +} + +/// Helper function to check that the assets in a given array are valid. +pub(crate) fn check_assets(api: &dyn Api, assets: &[Asset]) -> Result<(), ContractError> { + let asset_infos = assets.iter().map(|asset| asset.info.clone()).collect_vec(); + check_asset_infos(api, &asset_infos) +} + +/// Checks that cw20 token is part of the pool. +/// +/// * **cw20_sender** is cw20 token address which is being checked. +pub(crate) fn check_cw20_in_pool(config: &Config, cw20_sender: &Addr) -> Result<(), ContractError> { + for asset_info in &config.pair_info.asset_infos { + match asset_info { + AssetInfo::Token { contract_addr } if contract_addr == cw20_sender => return Ok(()), + _ => {} + } + } + + Err(ContractError::Unauthorized {}) +} + +/// Select offer and ask pools based on given offer and ask infos. +/// This function works with pools with up to 5 assets. Returns (offer_pool, ask_pool) in case of success. +/// If it is impossible to define offer and ask pools, returns [`ContractError`]. +/// +/// * **offer_asset_info** - asset info of the offer asset. +/// +/// * **ask_asset_info** - asset info of the ask asset. +/// +/// * **pools** - list of pools. +pub(crate) fn select_pools( + offer_asset_info: Option<&AssetInfo>, + ask_asset_info: Option<&AssetInfo>, + pools: &[DecimalAsset], +) -> Result<(DecimalAsset, DecimalAsset), ContractError> { + if pools.len() == 2 { + match (offer_asset_info, ask_asset_info) { + (Some(offer_asset_info), _) => { + let (offer_ind, offer_pool) = pools + .iter() + .find_position(|pool| pool.info.eq(offer_asset_info)) + .ok_or(ContractError::AssetMismatch {})?; + Ok((offer_pool.clone(), pools[(offer_ind + 1) % 2].clone())) + } + (_, Some(ask_asset_info)) => { + let (ask_ind, ask_pool) = pools + .iter() + .find_position(|pool| pool.info.eq(ask_asset_info)) + .ok_or(ContractError::AssetMismatch {})?; + Ok((pools[(ask_ind + 1) % 2].clone(), ask_pool.clone())) + } + _ => Err(ContractError::VariableAssetMissed {}), // Should always be unreachable + } + } else if let (Some(offer_asset_info), Some(ask_asset_info)) = + (offer_asset_info, ask_asset_info) + { + if ask_asset_info.eq(offer_asset_info) { + return Err(ContractError::SameAssets {}); + } + + let offer_pool = pools + .iter() + .find(|pool| pool.info.eq(offer_asset_info)) + .ok_or(ContractError::AssetMismatch {})?; + let ask_pool = pools + .iter() + .find(|pool| pool.info.eq(ask_asset_info)) + .ok_or(ContractError::AssetMismatch {})?; + + Ok((offer_pool.clone(), ask_pool.clone())) + } else { + Err(ContractError::VariableAssetMissed {}) // Should always be unreachable + } +} + +/// Compute the current pool amplification coefficient (AMP). +pub(crate) fn compute_current_amp(config: &Config, env: &Env) -> StdResult { + let block_time = env.block.time.seconds(); + if block_time < config.next_amp_time { + let elapsed_time: Uint128 = block_time.saturating_sub(config.init_amp_time).into(); + let time_range = config + .next_amp_time + .saturating_sub(config.init_amp_time) + .into(); + let init_amp = Uint128::from(config.init_amp); + let next_amp = Uint128::from(config.next_amp); + + if next_amp > init_amp { + let amp_range = next_amp - init_amp; + let res = init_amp + (amp_range * elapsed_time).checked_div(time_range)?; + Ok(res.try_into()?) + } else { + let amp_range = init_amp - next_amp; + let res = init_amp - (amp_range * elapsed_time).checked_div(time_range)?; + Ok(res.try_into()?) + } + } else { + Ok(Uint64::from(config.next_amp)) + } +} + +/// Returns a value using a newly specified precision. +/// +/// * **value** value that will have its precision adjusted. +/// +/// * **current_precision** `value`'s current precision +/// +/// * **new_precision** new precision to use when returning the `value`. +pub(crate) fn adjust_precision( + value: Uint128, + current_precision: u8, + new_precision: u8, +) -> StdResult { + Ok(match current_precision.cmp(&new_precision) { + Ordering::Equal => value, + Ordering::Less => value.checked_mul(Uint128::new( + 10_u128.pow((new_precision - current_precision) as u32), + ))?, + Ordering::Greater => value.checked_div(Uint128::new( + 10_u128.pow((current_precision - new_precision) as u32), + ))?, + }) +} + +/// Mint LP tokens for a beneficiary and auto stake the tokens in the Generator contract (if auto staking is specified). +/// +/// * **recipient** LP token recipient. +/// +/// * **amount** amount of LP tokens that will be minted for the recipient. +/// +/// * **auto_stake** whether the newly minted LP tokens will be automatically staked in the Generator on behalf of the recipient. +pub(crate) fn mint_liquidity_token_message( + querier: QuerierWrapper, + config: &Config, + contract_address: &Addr, + recipient: &Addr, + amount: Uint128, + auto_stake: bool, +) -> Result, ContractError> { + let lp_token = &config.pair_info.liquidity_token; + + // If no auto-stake - just mint to recipient + if !auto_stake { + return Ok(vec![wasm_execute( + lp_token, + &Cw20ExecuteMsg::Mint { + recipient: recipient.to_string(), + amount, + }, + vec![], + )? + .into()]); + } + + // Mint for the pair contract and stake into the Generator contract + let generator = query_factory_config(&querier, &config.factory_addr)?.generator_address; + + if let Some(generator) = generator { + Ok(vec![ + wasm_execute( + lp_token, + &Cw20ExecuteMsg::Mint { + recipient: contract_address.to_string(), + amount, + }, + vec![], + )? + .into(), + wasm_execute( + lp_token, + &Cw20ExecuteMsg::Send { + contract: generator.to_string(), + amount, + msg: to_binary(&astroport::generator::Cw20HookMsg::DepositFor( + recipient.clone(), + ))?, + }, + vec![], + )? + .into(), + ]) + } else { + Err(ContractError::AutoStakeError {}) + } +} + +/// Return the amount of tokens that a specific amount of LP tokens would withdraw. +/// +/// * **pools** array with assets available in the pool. +/// +/// * **amount** amount of LP tokens to calculate underlying amounts for. +/// +/// * **total_share** total amount of LP tokens currently issued by the pool. +pub(crate) fn get_share_in_assets( + pools: &[Asset], + amount: Uint128, + total_share: Uint128, +) -> Vec { + let mut share_ratio = Decimal::zero(); + if !total_share.is_zero() { + share_ratio = Decimal::from_ratio(amount, total_share); + } + + pools + .iter() + .map(|pool| Asset { + info: pool.info.clone(), + amount: pool.amount * share_ratio, + }) + .collect() +} + +/// Structure for internal use which represents swap result. +pub(crate) struct SwapResult { + pub return_amount: Uint128, + pub spread_amount: Uint128, +} + +/// Returns the result of a swap in form of a [`SwapResult`] object. +/// +/// * **offer_asset** asset that is being offered. +/// +/// * **offer_pool** pool of offered asset. +/// +/// * **ask_pool** asked asset. +/// +/// * **pools** array with assets available in the pool. +pub(crate) fn compute_swap( + storage: &dyn Storage, + env: &Env, + config: &Config, + offer_asset: &DecimalAsset, + offer_pool: &DecimalAsset, + ask_pool: &DecimalAsset, + pools: &[DecimalAsset], +) -> Result { + let token_precision = get_precision(storage, &ask_pool.info)?; + + let new_ask_pool = calc_y( + offer_asset, + &ask_pool.info, + offer_pool.amount + offer_asset.amount, + pools, + compute_current_amp(config, env)?, + token_precision, + )?; + + let return_amount = ask_pool.amount.to_uint128_with_precision(token_precision)? - new_ask_pool; + let offer_asset_amount = offer_asset + .amount + .to_uint128_with_precision(token_precision)?; + + // We consider swap rate 1:1 in stable swap thus any difference is considered as spread. + let spread_amount = offer_asset_amount.saturating_sub(return_amount); + + Ok(SwapResult { + return_amount, + spread_amount, + }) +} + +/// Accumulate token prices for the assets in the pool. +/// +/// * **pools** array with assets available in the pool. +pub fn accumulate_prices( + deps: Deps, + env: Env, + config: &mut Config, + pools: &[DecimalAsset], +) -> Result { + let block_time = env.block.time.seconds(); + if block_time <= config.block_time_last { + return Ok(false); + } + + let time_elapsed = Uint128::from(block_time - config.block_time_last); + + if pools.iter().all(|pool| !pool.amount.is_zero()) { + let immut_config = config.clone(); + for (from, to, value) in config.cumulative_prices.iter_mut() { + let offer_asset = DecimalAsset { + info: from.clone(), + amount: Decimal256::one(), + }; + + let (offer_pool, ask_pool) = select_pools(Some(from), Some(to), pools)?; + let SwapResult { return_amount, .. } = compute_swap( + deps.storage, + &env, + &immut_config, + &offer_asset, + &offer_pool, + &ask_pool, + pools, + )?; + + *value = value.wrapping_add(time_elapsed.checked_mul(adjust_precision( + return_amount, + get_precision(deps.storage, &ask_pool.info)?, + TWAP_PRECISION, + )?)?); + } + } + + config.block_time_last = block_time; + + Ok(true) +} diff --git a/contracts/astroport/pair_stable/tests/3pool_tests.rs b/contracts/astroport/pair_stable/tests/3pool_tests.rs new file mode 100644 index 00000000..cf61ae6d --- /dev/null +++ b/contracts/astroport/pair_stable/tests/3pool_tests.rs @@ -0,0 +1,501 @@ +use cosmwasm_std::Addr; +use itertools::Itertools; + +use astroport::asset::AssetInfoExt; +use astroport_pair_stable::error::ContractError; +use helper::AppExtension; + +use crate::helper::{Helper, TestCoin}; + +mod helper; + +#[test] +fn provide_and_withdraw_no_fee() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![ + TestCoin::native("uluna"), + TestCoin::cw20("USDC"), + TestCoin::cw20("USDD"), + ]; + + let mut helper = Helper::new(&owner, test_coins.clone(), 100u64, Some(0u16)).unwrap(); + + let user1 = Addr::unchecked("user1"); + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(100_000000u128), + helper.assets[&test_coins[1]].with_balance(100_000000u128), + helper.assets[&test_coins[2]].with_balance(100_000000u128), + ]; + helper.give_me_money(&assets, &user1); + + helper.provide_liquidity(&user1, &assets).unwrap(); + + assert_eq!(299999000, helper.token_balance(&helper.lp_token, &user1)); + assert_eq!(0, helper.coin_balance(&test_coins[0], &user1)); + assert_eq!(0, helper.coin_balance(&test_coins[1], &user1)); + assert_eq!(0, helper.coin_balance(&test_coins[2], &user1)); + + // The user2 with the same assets should receive the same share + let user2 = Addr::unchecked("user2"); + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(100_000000u128), + helper.assets[&test_coins[1]].with_balance(100_000000u128), + helper.assets[&test_coins[2]].with_balance(100_000000u128), + ]; + helper.give_me_money(&assets, &user2); + helper.provide_liquidity(&user2, &assets).unwrap(); + assert_eq!(300_000000, helper.token_balance(&helper.lp_token, &user2)); + + // The user3 makes imbalanced provide thus he is charged with fees + let user3 = Addr::unchecked("user3"); + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(200_000000u128), + helper.assets[&test_coins[1]].with_balance(100_000000u128), + ]; + helper.give_me_money(&assets, &user3); + helper.provide_liquidity(&user3, &assets).unwrap(); + assert_eq!(299_629321, helper.token_balance(&helper.lp_token, &user3)); + + // Providing last asset with explicit zero amount should give nearly the same result + let user4 = Addr::unchecked("user4"); + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(200_000000u128), + helper.assets[&test_coins[1]].with_balance(100_000000u128), + helper.assets[&test_coins[2]].with_balance(0u128), + ]; + helper.give_me_money(&assets, &user4); + helper.provide_liquidity(&user4, &assets).unwrap(); + assert_eq!(299_056292, helper.token_balance(&helper.lp_token, &user4)); + + helper + .withdraw_liquidity(&user1, 299999000, vec![]) + .unwrap(); + + assert_eq!(0, helper.token_balance(&helper.lp_token, &user1)); + // Previous imbalanced provides resulted in different share in assets + assert_eq!(150163977, helper.coin_balance(&test_coins[0], &user1)); + assert_eq!(100109318, helper.coin_balance(&test_coins[1], &user1)); + assert_eq!(50054659, helper.coin_balance(&test_coins[2], &user1)); + + // Checking imbalanced withdraw. Withdrawing only the first asset x 300 with the whole amount of LP tokens + helper + .withdraw_liquidity( + &user2, + 300_000000, + vec![helper.assets[&test_coins[0]].with_balance(300_000000u128)], + ) + .unwrap(); + + // Previous imbalanced provides resulted in small LP balance residual + assert_eq!(619390, helper.token_balance(&helper.lp_token, &user2)); + assert_eq!(300_000000, helper.coin_balance(&test_coins[0], &user2)); + assert_eq!(0, helper.coin_balance(&test_coins[1], &user2)); + assert_eq!(0, helper.coin_balance(&test_coins[2], &user2)); + + // Trying to receive more than possible + let err = helper + .withdraw_liquidity( + &user3, + 100_000000, + vec![helper.assets[&test_coins[1]].with_balance(101_000000u128)], + ) + .unwrap_err(); + assert_eq!( + "Generic error: Not enough LP tokens. You need 100679731 LP tokens.", + err.root_cause().to_string() + ); + + // Providing more LP tokens than needed. The rest will be kept on the user's balance + helper + .withdraw_liquidity( + &user3, + 200_892384, + vec![helper.assets[&test_coins[1]].with_balance(101_000000u128)], + ) + .unwrap(); + + // initial balance - spent amount; 100 goes back to the user3 + assert_eq!( + 299_629321 - 100679731, + helper.token_balance(&helper.lp_token, &user3) + ); + assert_eq!(0, helper.coin_balance(&test_coins[0], &user3)); + assert_eq!(101_000000, helper.coin_balance(&test_coins[1], &user3)); + assert_eq!(0, helper.coin_balance(&test_coins[2], &user3)); +} + +#[test] +fn provide_with_different_precision() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![ + TestCoin::cw20precise("FOO", 4), + TestCoin::cw20precise("BAR", 5), + TestCoin::cw20precise("ADN", 6), + ]; + + let mut helper = Helper::new(&owner, test_coins.clone(), 100u64, None).unwrap(); + + for user_name in ["user1", "user2"] { + let user = Addr::unchecked(user_name); + + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(100_0000u128), + helper.assets[&test_coins[1]].with_balance(100_00000u128), + helper.assets[&test_coins[2]].with_balance(100_000000u128), + ]; + helper.give_me_money(&assets, &user); + + helper.provide_liquidity(&user, &assets).unwrap(); + } + + let user1 = Addr::unchecked("user1"); + + assert_eq!(299999000, helper.token_balance(&helper.lp_token, &user1)); + assert_eq!(0, helper.coin_balance(&test_coins[0], &user1)); + assert_eq!(0, helper.coin_balance(&test_coins[1], &user1)); + assert_eq!(0, helper.coin_balance(&test_coins[2], &user1)); + + helper + .withdraw_liquidity(&user1, 299999000, vec![]) + .unwrap(); + + assert_eq!(0, helper.token_balance(&helper.lp_token, &user1)); + assert_eq!(999996, helper.coin_balance(&test_coins[0], &user1)); + assert_eq!(9999966, helper.coin_balance(&test_coins[1], &user1)); + assert_eq!(99999666, helper.coin_balance(&test_coins[2], &user1)); + + let user2 = Addr::unchecked("user2"); + assert_eq!(300000000, helper.token_balance(&helper.lp_token, &user2)); + assert_eq!(0, helper.coin_balance(&test_coins[0], &user2)); + assert_eq!(0, helper.coin_balance(&test_coins[1], &user2)); + assert_eq!(0, helper.coin_balance(&test_coins[2], &user2)); + + helper + .withdraw_liquidity(&user2, 300000000, vec![]) + .unwrap(); + + assert_eq!(0, helper.token_balance(&helper.lp_token, &user2)); + assert_eq!(1000000, helper.coin_balance(&test_coins[0], &user2)); + assert_eq!(10000000, helper.coin_balance(&test_coins[1], &user2)); + assert_eq!(100000000, helper.coin_balance(&test_coins[2], &user2)); +} + +#[test] +fn swap_different_precisions() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![ + TestCoin::cw20precise("FOO", 4), + TestCoin::cw20precise("BAR", 5), + TestCoin::cw20precise("ADN", 6), + ]; + + let mut helper = Helper::new(&owner, test_coins.clone(), 100u64, None).unwrap(); + + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(1_000_000_000_u128), + helper.assets[&test_coins[1]].with_balance(10_000_000_000_u128), + helper.assets[&test_coins[2]].with_balance(100_000_000_000u128), + ]; + helper.provide_liquidity(&owner, &assets).unwrap(); + + let user = Addr::unchecked("user"); + // 100 x FOO tokens + let offer_asset = helper.assets[&test_coins[0]].with_balance(100_0000u128); + // Checking direct swap simulation + let sim_resp = helper + .simulate_swap(&offer_asset, Some(helper.assets[&test_coins[2]].clone())) + .unwrap(); + // And reverse swap as well + let reverse_sim_resp = helper + .simulate_reverse_swap( + &helper.assets[&test_coins[2]].with_balance(sim_resp.return_amount.u128()), + Some(helper.assets[&test_coins[0]].clone()), + ) + .unwrap(); + assert_eq!(offer_asset.amount, reverse_sim_resp.offer_amount); + + helper.give_me_money(&[offer_asset.clone()], &user); + helper + .swap( + &user, + &offer_asset, + Some(helper.assets[&test_coins[2]].clone()), + ) + .unwrap(); + assert_eq!(0, helper.coin_balance(&test_coins[0], &user)); + // 99.999010 x ADN tokens + assert_eq!(99_949011, sim_resp.return_amount.u128()); + assert_eq!(99_949011, helper.coin_balance(&test_coins[2], &user)); +} + +#[test] +fn check_swaps() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![ + TestCoin::native("uluna"), + TestCoin::cw20("USDC"), + TestCoin::cw20("USDD"), + ]; + + let mut helper = Helper::new(&owner, test_coins.clone(), 100u64, None).unwrap(); + + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(100_000_000_000u128), + helper.assets[&test_coins[1]].with_balance(100_000_000_000u128), + helper.assets[&test_coins[2]].with_balance(100_000_000_000u128), + ]; + helper.provide_liquidity(&owner, &assets).unwrap(); + + let user = Addr::unchecked("user"); + let offer_asset = helper.assets[&test_coins[0]].with_balance(100_000_000u128); + helper.give_me_money(&[offer_asset.clone()], &user); + + let err = helper.swap(&user, &offer_asset, None).unwrap_err(); + assert_eq!( + ContractError::VariableAssetMissed {}, + err.downcast().unwrap() + ); + + let err = helper + .swap( + &user, + &offer_asset, + Some(helper.assets[&test_coins[0]].clone()), + ) + .unwrap_err(); + assert_eq!(ContractError::SameAssets {}, err.downcast().unwrap()); + + helper + .swap( + &user, + &offer_asset, + Some(helper.assets[&test_coins[1]].clone()), + ) + .unwrap(); + assert_eq!(0, helper.coin_balance(&test_coins[0], &user)); + assert_eq!(99_949011, helper.coin_balance(&test_coins[1], &user)); +} + +#[test] +fn check_wrong_initializations() { + let owner = Addr::unchecked("owner"); + + let err = Helper::new(&owner, vec![TestCoin::native("uluna")], 100u64, None).unwrap_err(); + + assert_eq!( + ContractError::InvalidNumberOfAssets {}, + err.downcast().unwrap() + ); + + let err = Helper::new( + &owner, + vec![ + TestCoin::native("one"), + TestCoin::cw20("two"), + TestCoin::native("three"), + TestCoin::cw20("four"), + TestCoin::native("five"), + TestCoin::cw20("six"), + ], + 100u64, + None, + ) + .unwrap_err(); + + assert_eq!( + ContractError::InvalidNumberOfAssets {}, + err.downcast().unwrap() + ); + + let err = Helper::new( + &owner, + vec![ + TestCoin::native("uluna"), + TestCoin::native("uluna"), + TestCoin::cw20("USDC"), + ], + 100u64, + None, + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Doubling assets in asset infos" + ); + + // 5 assets in the pool is okay + Helper::new( + &owner, + vec![ + TestCoin::native("one"), + TestCoin::cw20("two"), + TestCoin::native("three"), + TestCoin::cw20("four"), + TestCoin::native("five"), + ], + 100u64, + None, + ) + .unwrap(); +} + +#[test] +fn check_withdraw_charges_fees() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![ + TestCoin::native("uluna"), + TestCoin::cw20("USDC"), + TestCoin::cw20("USDD"), + ]; + + let mut helper = Helper::new(&owner, test_coins.clone(), 100u64, None).unwrap(); + + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(100_000_000_000_000u128), + helper.assets[&test_coins[1]].with_balance(100_000_000_000_000u128), + helper.assets[&test_coins[2]].with_balance(100_000_000_000_000u128), + ]; + helper.provide_liquidity(&owner, &assets).unwrap(); + + let user1 = Addr::unchecked("user1"); + let offer_asset = helper.assets[&test_coins[0]].with_balance(100_000000u128); + + // Usual swap for reference + helper.give_me_money(&[offer_asset.clone()], &user1); + helper + .swap( + &user1, + &offer_asset, + Some(helper.assets[&test_coins[1]].clone()), + ) + .unwrap(); + let usual_swap_amount = helper.coin_balance(&test_coins[1], &user1); + assert_eq!(99_950000, usual_swap_amount); + + // Trying to swap LUNA -> USDC via provide/withdraw + let user2 = Addr::unchecked("user2"); + helper.give_me_money(&[offer_asset.clone()], &user2); + + // Provide 100 x LUNA + helper.provide_liquidity(&user2, &[offer_asset]).unwrap(); + + // Withdraw 100 x USDC + let lp_tokens_amount = helper.token_balance(&helper.lp_token, &user2); + let err = helper + .withdraw_liquidity( + &user2, + lp_tokens_amount, + vec![helper.assets[&test_coins[1]].with_balance(100_000000u128)], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Not enough LP tokens. You need 100025002 LP tokens." + ); + + helper + .withdraw_liquidity( + &user2, + lp_tokens_amount, + vec![helper.assets[&test_coins[1]].with_balance(usual_swap_amount)], + ) + .unwrap(); + + // A small residual of LP tokens is left + assert_eq!(8, helper.token_balance(&helper.lp_token, &user2)); + assert_eq!( + usual_swap_amount, + helper.coin_balance(&test_coins[1], &user2) + ); +} + +#[test] +fn check_5pool_prices() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![ + TestCoin::native("uusd"), + TestCoin::cw20("USDX"), + TestCoin::cw20("USDY"), + TestCoin::cw20("USDZ"), + TestCoin::native("ibc/usd"), + ]; + + let mut helper = Helper::new(&owner, test_coins.clone(), 100u64, None).unwrap(); + + let check_prices = |helper: &Helper| { + let prices = helper.query_prices().unwrap(); + + test_coins + .iter() + .cartesian_product(test_coins.iter()) + .filter(|(a, b)| a != b) + .for_each(|(from_coin, to_coin)| { + let price = prices + .cumulative_prices + .iter() + .filter(|(from, to, _)| { + from.eq(&helper.assets[from_coin]) && to.eq(&helper.assets[to_coin]) + }) + .collect::>(); + assert_eq!(price.len(), 1); + assert!(!price[0].2.is_zero()); + }); + }; + + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(100_000_000_000_000u128), + helper.assets[&test_coins[1]].with_balance(100_000_000_000_000u128), + helper.assets[&test_coins[2]].with_balance(100_000_000_000_000u128), + helper.assets[&test_coins[3]].with_balance(100_000_000_000_000u128), + helper.assets[&test_coins[4]].with_balance(100_000_000_000_000u128), + ]; + helper.provide_liquidity(&owner, &assets).unwrap(); + helper.app.next_block(1000); + check_prices(&helper); + + let user1 = Addr::unchecked("user1"); + let offer_asset = helper.assets[&test_coins[0]].with_balance(1_000_000_000u128); + helper.give_me_money(&[offer_asset.clone()], &user1); + + helper + .swap( + &user1, + &offer_asset, + Some(helper.assets[&test_coins[1]].clone()), + ) + .unwrap(); + + helper.app.next_block(86400); + check_prices(&helper); + + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(100_000000u128), + helper.assets[&test_coins[1]].with_balance(100_000000u128), + helper.assets[&test_coins[2]].with_balance(100_000000u128), + ]; + helper.give_me_money(&assets, &user1); + + // Imbalanced provide + helper.provide_liquidity(&user1, &assets).unwrap(); + helper.app.next_block(14 * 86400); + check_prices(&helper); + + let offer_asset = helper.assets[&test_coins[3]].with_balance(10_000_000_000u128); + helper.give_me_money(&[offer_asset.clone()], &user1); + helper + .swap( + &user1, + &offer_asset, + Some(helper.assets[&test_coins[4]].clone()), + ) + .unwrap(); + helper.app.next_block(86400); + check_prices(&helper); +} diff --git a/contracts/astroport/pair_stable/tests/helper.rs b/contracts/astroport/pair_stable/tests/helper.rs new file mode 100644 index 00000000..e1ee7ebf --- /dev/null +++ b/contracts/astroport/pair_stable/tests/helper.rs @@ -0,0 +1,461 @@ +use std::collections::HashMap; + +use anyhow::Result as AnyResult; +use cosmwasm_std::{coin, to_binary, Addr, Coin, Empty, StdResult, Uint128}; +use cw20::{BalanceResponse, Cw20Coin, Cw20ExecuteMsg, Cw20QueryMsg}; +use cw_multi_test::{App, AppResponse, Contract, ContractWrapper, Executor}; +use derivative::Derivative; +use itertools::Itertools; + +use astroport::asset::{native_asset_info, token_asset_info, Asset, AssetInfo, PairInfo}; +use astroport::factory::{PairConfig, PairType}; +use astroport::pair::{ + CumulativePricesResponse, Cw20HookMsg, ExecuteMsg, QueryMsg, ReverseSimulationResponse, + SimulationResponse, StablePoolParams, +}; +use astroport::querier::NATIVE_TOKEN_PRECISION; +use astroport_pair_stable::contract::{execute, instantiate, query, reply}; + +const INIT_BALANCE: u128 = 1_000_000_000_000; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum TestCoin { + Cw20(String), + Cw20Precise(String, u8), + Native(String), +} + +impl TestCoin { + pub fn denom(&self) -> Option { + match self { + TestCoin::Native(denom) => Some(denom.clone()), + _ => None, + } + } + + pub fn cw20_init_data(&self) -> Option<(String, u8)> { + match self { + TestCoin::Cw20(name) => Some((name.clone(), 6u8)), + TestCoin::Cw20Precise(name, precision) => Some((name.clone(), *precision)), + _ => None, + } + } + + pub fn native(denom: &str) -> Self { + Self::Native(denom.to_string()) + } + + pub fn cw20(name: &str) -> Self { + Self::Cw20(name.to_string()) + } + + pub fn cw20precise(name: &str, precision: u8) -> Self { + Self::Cw20Precise(name.to_string(), precision) + } +} + +pub fn init_native_coins(test_coins: &[TestCoin]) -> Vec { + test_coins + .iter() + .filter_map(|test_coin| match test_coin { + TestCoin::Native(name) => { + let init_balance = INIT_BALANCE * 10u128.pow(NATIVE_TOKEN_PRECISION as u32); + Some(coin(init_balance, name)) + } + _ => None, + }) + .collect() +} + +fn token_contract() -> Box> { + Box::new(ContractWrapper::new_with_empty( + astroport_token::contract::execute, + astroport_token::contract::instantiate, + astroport_token::contract::query, + )) +} + +fn pair_contract() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query).with_reply_empty(reply)) +} + +fn factory_contract() -> Box> { + Box::new( + ContractWrapper::new_with_empty( + astroport_factory::contract::execute, + astroport_factory::contract::instantiate, + astroport_factory::contract::query, + ) + .with_reply_empty(astroport_factory::contract::reply), + ) +} + +#[derive(Derivative)] +#[derivative(Debug)] +pub struct Helper { + #[derivative(Debug = "ignore")] + pub app: App, + pub owner: Addr, + pub assets: HashMap, + pub factory: Addr, + pub pair_addr: Addr, + pub lp_token: Addr, + pub amp: u64, +} + +impl Helper { + pub fn new( + owner: &Addr, + test_coins: Vec, + amp: u64, + swap_fee: Option, + ) -> AnyResult { + let mut app = App::new(|router, _, storage| { + router + .bank + .init_balance(storage, owner, init_native_coins(&test_coins)) + .unwrap() + }); + + let mut asset_infos_vec: Vec<_> = test_coins + .clone() + .into_iter() + .filter_map(|coin| Some((coin.clone(), native_asset_info(coin.denom()?)))) + .collect(); + + let token_code_id = app.store_code(token_contract()); + + test_coins.into_iter().for_each(|coin| { + if let Some((name, decimals)) = coin.cw20_init_data() { + let token_addr = Self::init_token(&mut app, token_code_id, name, decimals, owner); + asset_infos_vec.push((coin, token_asset_info(token_addr))) + } + }); + + let pair_code_id = app.store_code(pair_contract()); + let factory_code_id = app.store_code(factory_contract()); + + let init_msg = astroport::factory::InstantiateMsg { + fee_address: None, + pair_configs: vec![PairConfig { + code_id: pair_code_id, + maker_fee_bps: 5000, + total_fee_bps: swap_fee.unwrap_or(5u16), + pair_type: PairType::Stable {}, + is_disabled: false, + is_generator_disabled: false, + }], + token_code_id, + generator_address: None, + owner: owner.to_string(), + whitelist_code_id: 234u64, + }; + + let factory = app.instantiate_contract( + factory_code_id, + owner.clone(), + &init_msg, + &[], + "FACTORY", + None, + )?; + + let asset_infos = asset_infos_vec + .clone() + .into_iter() + .map(|(_, asset_info)| asset_info) + .collect_vec(); + let init_pair_msg = astroport::factory::ExecuteMsg::CreatePair { + pair_type: PairType::Stable {}, + asset_infos: asset_infos.clone(), + init_params: Some(to_binary(&StablePoolParams { amp, owner: None }).unwrap()), + }; + + app.execute_contract(owner.clone(), factory.clone(), &init_pair_msg, &[])?; + + let resp: PairInfo = app.wrap().query_wasm_smart( + &factory, + &astroport::factory::QueryMsg::Pair { asset_infos }, + )?; + + Ok(Self { + app, + owner: owner.clone(), + assets: asset_infos_vec.into_iter().collect(), + factory, + pair_addr: resp.contract_addr, + lp_token: resp.liquidity_token, + amp, + }) + } + + pub fn provide_liquidity(&mut self, sender: &Addr, assets: &[Asset]) -> AnyResult { + let funds = + assets.mock_coins_sent(&mut self.app, sender, &self.pair_addr, SendType::Allowance); + + let msg = ExecuteMsg::ProvideLiquidity { + assets: assets.to_vec(), + slippage_tolerance: None, + auto_stake: None, + receiver: None, + }; + + self.app + .execute_contract(sender.clone(), self.pair_addr.clone(), &msg, &funds) + } + + pub fn withdraw_liquidity( + &mut self, + sender: &Addr, + amount: u128, + assets: Vec, + ) -> AnyResult { + let msg = Cw20ExecuteMsg::Send { + contract: self.pair_addr.to_string(), + amount: Uint128::from(amount), + msg: to_binary(&Cw20HookMsg::WithdrawLiquidity { assets }).unwrap(), + }; + + self.app + .execute_contract(sender.clone(), self.lp_token.clone(), &msg, &[]) + } + + pub fn swap( + &mut self, + sender: &Addr, + offer_asset: &Asset, + ask_asset_info: Option, + ) -> AnyResult { + match &offer_asset.info { + AssetInfo::Token { contract_addr } => { + let msg = Cw20ExecuteMsg::Send { + contract: self.pair_addr.to_string(), + amount: offer_asset.amount, + msg: to_binary(&Cw20HookMsg::Swap { + ask_asset_info, + belief_price: None, + max_spread: None, + to: None, + }) + .unwrap(), + }; + + self.app + .execute_contract(sender.clone(), contract_addr.clone(), &msg, &[]) + } + AssetInfo::NativeToken { .. } => { + let funds = offer_asset.mock_coin_sent( + &mut self.app, + sender, + &self.pair_addr, + SendType::None, + ); + + let msg = ExecuteMsg::Swap { + offer_asset: offer_asset.clone(), + ask_asset_info, + belief_price: None, + max_spread: None, + to: None, + }; + + self.app + .execute_contract(sender.clone(), self.pair_addr.clone(), &msg, &funds) + } + } + } + + pub fn simulate_swap( + &self, + offer_asset: &Asset, + ask_asset_info: Option, + ) -> StdResult { + self.app.wrap().query_wasm_smart( + &self.pair_addr, + &QueryMsg::Simulation { + offer_asset: offer_asset.clone(), + ask_asset_info, + }, + ) + } + + pub fn simulate_reverse_swap( + &self, + ask_asset: &Asset, + offer_asset_info: Option, + ) -> StdResult { + self.app.wrap().query_wasm_smart( + &self.pair_addr, + &QueryMsg::ReverseSimulation { + ask_asset: ask_asset.clone(), + offer_asset_info, + }, + ) + } + + pub fn query_prices(&self) -> StdResult { + self.app + .wrap() + .query_wasm_smart(&self.pair_addr, &QueryMsg::CumulativePrices {}) + } + + fn init_token( + app: &mut App, + token_code: u64, + name: String, + decimals: u8, + owner: &Addr, + ) -> Addr { + let init_balance = INIT_BALANCE * 10u128.pow(decimals as u32); + app.instantiate_contract( + token_code, + owner.clone(), + &astroport::token::InstantiateMsg { + symbol: name.to_string(), + name, + decimals, + initial_balances: vec![Cw20Coin { + address: owner.to_string(), + amount: Uint128::from(init_balance), + }], + mint: None, + marketing: None, + }, + &[], + "{name}_token", + None, + ) + .unwrap() + } + + pub fn token_balance(&self, token_addr: &Addr, user: &Addr) -> u128 { + let resp: BalanceResponse = self + .app + .wrap() + .query_wasm_smart( + token_addr, + &Cw20QueryMsg::Balance { + address: user.to_string(), + }, + ) + .unwrap(); + + resp.balance.u128() + } + + pub fn coin_balance(&self, coin: &TestCoin, user: &Addr) -> u128 { + match &self.assets[coin] { + AssetInfo::Token { contract_addr } => self.token_balance(contract_addr, user), + AssetInfo::NativeToken { denom } => self + .app + .wrap() + .query_balance(user, denom) + .unwrap() + .amount + .u128(), + } + } + + pub fn give_me_money(&mut self, assets: &[Asset], recipient: &Addr) { + let funds = + assets.mock_coins_sent(&mut self.app, &self.owner, recipient, SendType::Transfer); + + if !funds.is_empty() { + self.app + .send_tokens(self.owner.clone(), recipient.clone(), &funds) + .unwrap(); + } + } +} + +#[derive(Clone, Copy)] +pub enum SendType { + Allowance, + Transfer, + None, +} + +pub trait AssetExt { + fn mock_coin_sent( + &self, + app: &mut App, + user: &Addr, + spender: &Addr, + typ: SendType, + ) -> Vec; +} + +impl AssetExt for Asset { + fn mock_coin_sent( + &self, + app: &mut App, + user: &Addr, + spender: &Addr, + typ: SendType, + ) -> Vec { + let mut funds = vec![]; + match &self.info { + AssetInfo::Token { contract_addr } if !self.amount.is_zero() => { + let msg = match typ { + SendType::Allowance => Cw20ExecuteMsg::IncreaseAllowance { + spender: spender.to_string(), + amount: self.amount, + expires: None, + }, + SendType::Transfer => Cw20ExecuteMsg::Transfer { + recipient: spender.to_string(), + amount: self.amount, + }, + _ => unimplemented!(), + }; + app.execute_contract(user.clone(), contract_addr.clone(), &msg, &[]) + .unwrap(); + } + AssetInfo::NativeToken { denom } if !self.amount.is_zero() => { + funds = vec![coin(self.amount.u128(), denom)]; + } + _ => {} + } + + funds + } +} + +pub trait AssetsExt { + fn mock_coins_sent( + &self, + app: &mut App, + user: &Addr, + spender: &Addr, + typ: SendType, + ) -> Vec; +} + +impl AssetsExt for &[Asset] { + fn mock_coins_sent( + &self, + app: &mut App, + user: &Addr, + spender: &Addr, + typ: SendType, + ) -> Vec { + let mut funds = vec![]; + for asset in self.iter() { + funds.extend(asset.mock_coin_sent(app, user, spender, typ)); + } + funds + } +} + +pub trait AppExtension { + fn next_block(&mut self, time: u64); +} + +impl AppExtension for App { + fn next_block(&mut self, time: u64) { + self.update_block(|block| { + block.time = block.time.plus_seconds(time); + block.height += 1 + }); + } +} diff --git a/contracts/astroport/pair_stable/tests/integration.rs b/contracts/astroport/pair_stable/tests/integration.rs new file mode 100644 index 00000000..2627c412 --- /dev/null +++ b/contracts/astroport/pair_stable/tests/integration.rs @@ -0,0 +1,1289 @@ +use astroport::asset::{Asset, AssetInfo, PairInfo}; +use astroport::factory::{ + ExecuteMsg as FactoryExecuteMsg, InstantiateMsg as FactoryInstantiateMsg, PairConfig, PairType, + QueryMsg as FactoryQueryMsg, +}; +use astroport::pair::{ + ConfigResponse, CumulativePricesResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, QueryMsg, + StablePoolConfig, StablePoolParams, StablePoolUpdateParams, TWAP_PRECISION, +}; + +use astroport::token::InstantiateMsg as TokenInstantiateMsg; +use astroport_pair_stable::math::{MAX_AMP, MAX_AMP_CHANGE, MIN_AMP_CHANGING_TIME}; +use cosmwasm_std::{ + attr, from_binary, to_binary, Addr, Coin, Decimal, QueryRequest, Uint128, WasmQuery, +}; +use cw20::{BalanceResponse, Cw20Coin, Cw20ExecuteMsg, Cw20QueryMsg, MinterResponse}; +use cw_multi_test::{App, ContractWrapper, Executor}; + +const OWNER: &str = "owner"; + +fn mock_app(owner: Addr, coins: Vec) -> App { + App::new(|router, _, storage| { + // initialization moved to App construction + router.bank.init_balance(storage, &owner, coins).unwrap() + }) +} + +fn store_token_code(app: &mut App) -> u64 { + let astro_token_contract = Box::new(ContractWrapper::new_with_empty( + astroport_token::contract::execute, + astroport_token::contract::instantiate, + astroport_token::contract::query, + )); + + app.store_code(astro_token_contract) +} + +fn store_pair_code(app: &mut App) -> u64 { + let pair_contract = Box::new( + ContractWrapper::new_with_empty( + astroport_pair_stable::contract::execute, + astroport_pair_stable::contract::instantiate, + astroport_pair_stable::contract::query, + ) + .with_reply_empty(astroport_pair_stable::contract::reply), + ); + + app.store_code(pair_contract) +} + +fn store_factory_code(app: &mut App) -> u64 { + let factory_contract = Box::new( + ContractWrapper::new_with_empty( + astroport_factory::contract::execute, + astroport_factory::contract::instantiate, + astroport_factory::contract::query, + ) + .with_reply_empty(astroport_factory::contract::reply), + ); + + app.store_code(factory_contract) +} + +fn instantiate_pair(router: &mut App, owner: &Addr) -> Addr { + let token_contract_code_id = store_token_code(router); + let pair_contract_code_id = store_pair_code(router); + let factory_code_id = store_factory_code(router); + + let factory_init_msg = FactoryInstantiateMsg { + fee_address: None, + pair_configs: vec![PairConfig { + code_id: pair_contract_code_id, + maker_fee_bps: 5000, + total_fee_bps: 5u16, + pair_type: PairType::Stable {}, + is_disabled: false, + is_generator_disabled: false, + }], + token_code_id: token_contract_code_id, + generator_address: None, + owner: owner.to_string(), + whitelist_code_id: 234u64, + }; + + let factory_addr = router + .instantiate_contract( + factory_code_id, + owner.clone(), + &factory_init_msg, + &[], + "FACTORY", + None, + ) + .unwrap(); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ], + token_code_id: token_contract_code_id, + factory_addr: factory_addr.to_string(), + init_params: None, + }; + + let resp = router + .instantiate_contract( + pair_contract_code_id, + owner.clone(), + &msg, + &[], + String::from("PAIR"), + None, + ) + .unwrap_err(); + assert_eq!( + "You need to provide init params", + resp.root_cause().to_string() + ); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ], + token_code_id: token_contract_code_id, + factory_addr: factory_addr.to_string(), + init_params: Some( + to_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + let pair = router + .instantiate_contract( + pair_contract_code_id, + owner.clone(), + &msg, + &[], + String::from("PAIR"), + None, + ) + .unwrap(); + + let res: PairInfo = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Pair {}) + .unwrap(); + assert_eq!("contract1", res.contract_addr); + assert_eq!("contract2", res.liquidity_token); + + pair +} + +#[test] +fn test_provide_and_withdraw_liquidity() { + let owner = Addr::unchecked("owner"); + let alice_address = Addr::unchecked("alice"); + + let mut router = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ], + ); + + // Set Alice's balances + router + .send_tokens( + owner.clone(), + alice_address.clone(), + &[ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(233_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(200_000u128), + }, + ], + ) + .unwrap(); + + // Init pair + let pair_instance = instantiate_pair(&mut router, &owner); + + let res: Result = router.wrap().query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: pair_instance.to_string(), + msg: to_binary(&QueryMsg::Pair {}).unwrap(), + })); + let res = res.unwrap(); + + assert_eq!( + res.asset_infos, + [ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ], + ); + + // When dealing with native tokens, the transfer should happen before the contract call, which cw-multitest doesn't support + router + .send_tokens( + owner.clone(), + pair_instance.clone(), + &[ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000u128), + }, + ], + ) + .unwrap(); + + // Provide liquidity + let (msg, coins) = provide_liquidity_msg(Uint128::new(100), Uint128::new(100), None); + let res = router + .execute_contract(alice_address.clone(), pair_instance.clone(), &msg, &coins) + .unwrap(); + + assert_eq!( + res.events[1].attributes[1], + attr("action", "provide_liquidity") + ); + assert_eq!(res.events[1].attributes[3], attr("receiver", "alice"),); + assert_eq!( + res.events[1].attributes[4], + attr("assets", "100uusd, 100uluna") + ); + assert_eq!( + res.events[1].attributes[5], + attr("share", 199200u128.to_string()) + ); + + assert_eq!(res.events[3].attributes[1], attr("action", "mint")); + assert_eq!(res.events[3].attributes[2], attr("to", "contract1")); + assert_eq!( + res.events[3].attributes[3], + attr("amount", 1000.to_string()) + ); + + assert_eq!(res.events[5].attributes[1], attr("action", "mint")); + assert_eq!(res.events[5].attributes[2], attr("to", "alice")); + assert_eq!( + res.events[5].attributes[3], + attr("amount", 199200u128.to_string()) + ); + + // Provide liquidity for a custom receiver + let (msg, coins) = provide_liquidity_msg( + Uint128::new(100), + Uint128::new(100), + Some("bob".to_string()), + ); + let res = router + .execute_contract(alice_address, pair_instance, &msg, &coins) + .unwrap(); + + assert_eq!( + res.events[1].attributes[1], + attr("action", "provide_liquidity") + ); + assert_eq!(res.events[1].attributes[3], attr("receiver", "bob"),); + assert_eq!( + res.events[1].attributes[4], + attr("assets", "100uusd, 100uluna") + ); + assert_eq!( + res.events[1].attributes[5], + attr("share", 200u128.to_string()) + ); + assert_eq!(res.events[3].attributes[1], attr("action", "mint")); + assert_eq!(res.events[3].attributes[2], attr("to", "bob")); + assert_eq!(res.events[3].attributes[3], attr("amount", 200.to_string())); +} + +fn provide_liquidity_msg( + uusd_amount: Uint128, + uluna_amount: Uint128, + receiver: Option, +) -> (ExecuteMsg, [Coin; 2]) { + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: uusd_amount, + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + amount: uluna_amount, + }, + ], + slippage_tolerance: None, + auto_stake: None, + receiver, + }; + + let coins = [ + Coin { + denom: "uluna".to_string(), + amount: uluna_amount, + }, + Coin { + denom: "uusd".to_string(), + amount: uusd_amount, + }, + ]; + + (msg, coins) +} + +#[test] +fn provide_lp_for_single_token() { + let owner = Addr::unchecked(OWNER); + let mut app = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ], + ); + + let token_code_id = store_token_code(&mut app); + + let x_amount = Uint128::new(9_000_000_000_000_000); + let y_amount = Uint128::new(9_000_000_000_000_000); + let x_offer = Uint128::new(1_000_000_000_000_000); + let swap_amount = Uint128::new(120_000_000); + + let token_name = "Xtoken"; + + let init_msg = TokenInstantiateMsg { + name: token_name.to_string(), + symbol: token_name.to_string(), + decimals: 6, + initial_balances: vec![Cw20Coin { + address: OWNER.to_string(), + amount: x_amount, + }], + mint: Some(MinterResponse { + minter: String::from(OWNER), + cap: None, + }), + marketing: None, + }; + + let token_x_instance = app + .instantiate_contract( + token_code_id, + owner.clone(), + &init_msg, + &[], + token_name, + None, + ) + .unwrap(); + + let token_name = "Ytoken"; + + let init_msg = TokenInstantiateMsg { + name: token_name.to_string(), + symbol: token_name.to_string(), + decimals: 6, + initial_balances: vec![Cw20Coin { + address: OWNER.to_string(), + amount: y_amount, + }], + mint: Some(MinterResponse { + minter: String::from(OWNER), + cap: None, + }), + marketing: None, + }; + + let token_y_instance = app + .instantiate_contract( + token_code_id, + owner.clone(), + &init_msg, + &[], + token_name, + None, + ) + .unwrap(); + + let pair_code_id = store_pair_code(&mut app); + let factory_code_id = store_factory_code(&mut app); + + let init_msg = FactoryInstantiateMsg { + fee_address: None, + pair_configs: vec![PairConfig { + code_id: pair_code_id, + maker_fee_bps: 0, + total_fee_bps: 0, + pair_type: PairType::Stable {}, + is_disabled: false, + is_generator_disabled: false, + }], + token_code_id, + generator_address: Some(String::from("generator")), + owner: String::from("owner0000"), + whitelist_code_id: 234u64, + }; + + let factory_instance = app + .instantiate_contract( + factory_code_id, + owner.clone(), + &init_msg, + &[], + "FACTORY", + None, + ) + .unwrap(); + + let msg = FactoryExecuteMsg::CreatePair { + pair_type: PairType::Stable {}, + asset_infos: vec![ + AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + ], + init_params: Some( + to_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + app.execute_contract(owner.clone(), factory_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = FactoryQueryMsg::Pair { + asset_infos: vec![ + AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + ], + }; + + let res: PairInfo = app + .wrap() + .query_wasm_smart(&factory_instance, &msg) + .unwrap(); + + let pair_instance = res.contract_addr; + + let msg = Cw20ExecuteMsg::IncreaseAllowance { + spender: pair_instance.to_string(), + expires: None, + amount: x_amount, + }; + + app.execute_contract(owner.clone(), token_x_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = Cw20ExecuteMsg::IncreaseAllowance { + spender: pair_instance.to_string(), + expires: None, + amount: y_amount, + }; + + app.execute_contract(owner.clone(), token_y_instance.clone(), &msg, &[]) + .unwrap(); + + let swap_msg = Cw20ExecuteMsg::Send { + contract: pair_instance.to_string(), + msg: to_binary(&Cw20HookMsg::Swap { + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: None, + }) + .unwrap(), + amount: swap_amount, + }; + + let err = app + .execute_contract(owner.clone(), token_x_instance.clone(), &swap_msg, &[]) + .unwrap_err(); + assert_eq!( + "Generic error: One of the pools is empty", + err.root_cause().to_string() + ); + + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + amount: x_offer, + }, + Asset { + info: AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + amount: Uint128::zero(), + }, + ], + slippage_tolerance: None, + auto_stake: None, + receiver: None, + }; + + let err = app + .execute_contract(owner.clone(), pair_instance.clone(), &msg, &[]) + .unwrap_err(); + assert_eq!( + "It is not possible to provide liquidity with one token for an empty pool", + err.root_cause().to_string() + ); + + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + amount: Uint128::new(1_000_000_000_000_000), + }, + Asset { + info: AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + amount: Uint128::new(1_000_000_000_000_000), + }, + ], + slippage_tolerance: None, + auto_stake: None, + receiver: None, + }; + + app.execute_contract(owner.clone(), pair_instance.clone(), &msg, &[]) + .unwrap(); + + // try to provide for single token and increase the ratio in the pool from 1 to 1.5 + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + amount: Uint128::new(500_000_000_000_000), + }, + Asset { + info: AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + amount: Uint128::zero(), + }, + ], + slippage_tolerance: None, + auto_stake: None, + receiver: None, + }; + + app.execute_contract(owner.clone(), pair_instance.clone(), &msg, &[]) + .unwrap(); + + // try swap 120_000_000 from token_y to token_x (from lower token amount to higher) + app.execute_contract(owner.clone(), token_y_instance.clone(), &swap_msg, &[]) + .unwrap(); + + // try swap 120_000_000 from token_x to token_y (from higher token amount to lower ) + app.execute_contract(owner.clone(), token_x_instance.clone(), &swap_msg, &[]) + .unwrap(); + + // try to provide for single token and increase the ratio in the pool from 1 to 2.5 + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + amount: Uint128::new(1_000_000_000_000_000), + }, + Asset { + info: AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + amount: Uint128::zero(), + }, + ], + slippage_tolerance: None, + auto_stake: None, + receiver: None, + }; + + app.execute_contract(owner.clone(), pair_instance.clone(), &msg, &[]) + .unwrap(); + + // try swap 120_000_000 from token_y to token_x (from lower token amount to higher) + let msg = Cw20ExecuteMsg::Send { + contract: pair_instance.to_string(), + msg: to_binary(&Cw20HookMsg::Swap { + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: None, + }) + .unwrap(), + amount: swap_amount, + }; + + app.execute_contract(owner.clone(), token_y_instance, &msg, &[]) + .unwrap(); + + // try swap 120_000_000 from token_x to token_y (from higher token amount to lower ) + let msg = Cw20ExecuteMsg::Send { + contract: pair_instance.to_string(), + msg: to_binary(&Cw20HookMsg::Swap { + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: None, + }) + .unwrap(), + amount: swap_amount, + }; + + let err = app + .execute_contract(owner, token_x_instance, &msg, &[]) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Operation exceeds max spread limit" + ); +} + +#[test] +fn test_compatibility_of_tokens_with_different_precision() { + let owner = Addr::unchecked(OWNER); + let mut app = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ], + ); + + let token_code_id = store_token_code(&mut app); + + let x_amount = Uint128::new(100_000_000_000); + let y_amount = Uint128::new(1000000_0000000); + let x_offer = Uint128::new(1_00000); + let y_expected_return = Uint128::new(1_0000000); + + let token_name = "Xtoken"; + + let init_msg = TokenInstantiateMsg { + name: token_name.to_string(), + symbol: token_name.to_string(), + decimals: 5, + initial_balances: vec![Cw20Coin { + address: OWNER.to_string(), + amount: x_amount + x_offer, + }], + mint: Some(MinterResponse { + minter: String::from(OWNER), + cap: None, + }), + marketing: None, + }; + + let token_x_instance = app + .instantiate_contract( + token_code_id, + owner.clone(), + &init_msg, + &[], + token_name, + None, + ) + .unwrap(); + + let token_name = "Ytoken"; + + let init_msg = TokenInstantiateMsg { + name: token_name.to_string(), + symbol: token_name.to_string(), + decimals: 7, + initial_balances: vec![Cw20Coin { + address: OWNER.to_string(), + amount: y_amount, + }], + mint: Some(MinterResponse { + minter: String::from(OWNER), + cap: None, + }), + marketing: None, + }; + + let token_y_instance = app + .instantiate_contract( + token_code_id, + owner.clone(), + &init_msg, + &[], + token_name, + None, + ) + .unwrap(); + + let pair_code_id = store_pair_code(&mut app); + let factory_code_id = store_factory_code(&mut app); + + let init_msg = FactoryInstantiateMsg { + fee_address: None, + pair_configs: vec![PairConfig { + code_id: pair_code_id, + maker_fee_bps: 0, + total_fee_bps: 0, + pair_type: PairType::Stable {}, + is_disabled: false, + is_generator_disabled: false, + }], + token_code_id, + generator_address: Some(String::from("generator")), + owner: String::from("owner0000"), + whitelist_code_id: 234u64, + }; + + let factory_instance = app + .instantiate_contract( + factory_code_id, + owner.clone(), + &init_msg, + &[], + "FACTORY", + None, + ) + .unwrap(); + + let msg = FactoryExecuteMsg::CreatePair { + pair_type: PairType::Stable {}, + asset_infos: vec![ + AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + ], + init_params: Some( + to_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + app.execute_contract(owner.clone(), factory_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = FactoryQueryMsg::Pair { + asset_infos: vec![ + AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + ], + }; + + let res: PairInfo = app + .wrap() + .query_wasm_smart(&factory_instance, &msg) + .unwrap(); + + let pair_instance = res.contract_addr; + + let msg = Cw20ExecuteMsg::IncreaseAllowance { + spender: pair_instance.to_string(), + expires: None, + amount: x_amount + x_offer, + }; + + app.execute_contract(owner.clone(), token_x_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = Cw20ExecuteMsg::IncreaseAllowance { + spender: pair_instance.to_string(), + expires: None, + amount: y_amount, + }; + + app.execute_contract(owner.clone(), token_y_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::Token { + contract_addr: token_x_instance.clone(), + }, + amount: x_amount, + }, + Asset { + info: AssetInfo::Token { + contract_addr: token_y_instance.clone(), + }, + amount: y_amount, + }, + ], + slippage_tolerance: None, + auto_stake: None, + receiver: None, + }; + + app.execute_contract(owner.clone(), pair_instance.clone(), &msg, &[]) + .unwrap(); + + let d: u128 = app + .wrap() + .query_wasm_smart(&pair_instance, &QueryMsg::QueryComputeD {}) + .unwrap(); + assert_eq!(d, 20000000000000); + + let user = Addr::unchecked("user"); + + let msg = Cw20ExecuteMsg::Send { + contract: pair_instance.to_string(), + msg: to_binary(&Cw20HookMsg::Swap { + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: Some(user.to_string()), + }) + .unwrap(), + amount: x_offer, + }; + + app.execute_contract(owner, token_x_instance, &msg, &[]) + .unwrap(); + + let msg = Cw20QueryMsg::Balance { + address: user.to_string(), + }; + + let res: BalanceResponse = app + .wrap() + .query_wasm_smart(&token_y_instance, &msg) + .unwrap(); + + assert_eq!(res.balance, y_expected_return); + + let d: u128 = app + .wrap() + .query_wasm_smart(&pair_instance, &QueryMsg::QueryComputeD {}) + .unwrap(); + assert_eq!(d, 19999999999999); +} + +#[test] +fn test_if_twap_is_calculated_correctly_when_pool_idles() { + let owner = Addr::unchecked(OWNER); + let mut app = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000_000_u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000_000_u128), + }, + ], + ); + + let user1 = Addr::unchecked("user1"); + + // Set User1's balances + app.send_tokens( + owner, + user1.clone(), + &[ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(4_666_666_000_000), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(2_000_000_000_000), + }, + ], + ) + .unwrap(); + + // Instantiate pair + let pair_instance = instantiate_pair(&mut app, &user1); + + // Provide liquidity, accumulators are empty + let (msg, coins) = provide_liquidity_msg( + Uint128::new(1_000_000_000_000), + Uint128::new(1_000_000_000_000), + None, + ); + app.execute_contract(user1.clone(), pair_instance.clone(), &msg, &coins) + .unwrap(); + + const BLOCKS_PER_DAY: u64 = 17280; + const ELAPSED_SECONDS: u64 = BLOCKS_PER_DAY * 5; + + // A day later + app.update_block(|b| { + b.height += BLOCKS_PER_DAY; + b.time = b.time.plus_seconds(ELAPSED_SECONDS); + }); + + // Provide liquidity, accumulators firstly filled with the same prices + let (msg, coins) = provide_liquidity_msg( + Uint128::new(3_000_000_000_000), + Uint128::new(1_000_000_000_000), + None, + ); + app.execute_contract(user1.clone(), pair_instance.clone(), &msg, &coins) + .unwrap(); + + // Get current TWAP accumulator values + let msg = QueryMsg::CumulativePrices {}; + let cpr_old: CumulativePricesResponse = + app.wrap().query_wasm_smart(&pair_instance, &msg).unwrap(); + + // A day later + app.update_block(|b| { + b.height += BLOCKS_PER_DAY; + b.time = b.time.plus_seconds(ELAPSED_SECONDS); + }); + + // Get current twap accumulator values + let msg = QueryMsg::CumulativePrices {}; + let cpr_new: CumulativePricesResponse = + app.wrap().query_wasm_smart(&pair_instance, &msg).unwrap(); + + let twap0 = cpr_new.cumulative_prices[0].2 - cpr_old.cumulative_prices[0].2; + let twap1 = cpr_new.cumulative_prices[1].2 - cpr_old.cumulative_prices[1].2; + + // Prices weren't changed for the last day, uusd amount in pool = 4000000_000000, uluna = 2000000_000000 + let price_precision = Uint128::from(10u128.pow(TWAP_PRECISION.into())); + assert_eq!(twap0 / price_precision, Uint128::new(85684)); // 1.008356286 * ELAPSED_SECONDS (86400) + assert_eq!(twap1 / price_precision, Uint128::new(87121)); // 0.991712963 * ELAPSED_SECONDS +} + +#[test] +fn create_pair_with_same_assets() { + let owner = Addr::unchecked(OWNER); + let mut router = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ], + ); + + let token_contract_code_id = store_token_code(&mut router); + let pair_contract_code_id = store_pair_code(&mut router); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + ], + token_code_id: token_contract_code_id, + factory_addr: String::from("factory"), + init_params: None, + }; + + let resp = router + .instantiate_contract( + pair_contract_code_id, + owner, + &msg, + &[], + String::from("PAIR"), + None, + ) + .unwrap_err(); + + assert_eq!( + resp.root_cause().to_string(), + "Doubling assets in asset infos" + ) +} + +#[test] +fn update_pair_config() { + let owner = Addr::unchecked(OWNER); + let mut router = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ], + ); + + let token_contract_code_id = store_token_code(&mut router); + let pair_contract_code_id = store_pair_code(&mut router); + + let factory_code_id = store_factory_code(&mut router); + + let init_msg = FactoryInstantiateMsg { + fee_address: None, + pair_configs: vec![], + token_code_id: token_contract_code_id, + generator_address: Some(String::from("generator")), + owner: owner.to_string(), + whitelist_code_id: 234u64, + }; + + let factory_instance = router + .instantiate_contract( + factory_code_id, + owner.clone(), + &init_msg, + &[], + "FACTORY", + None, + ) + .unwrap(); + + let msg = InstantiateMsg { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ], + token_code_id: token_contract_code_id, + factory_addr: factory_instance.to_string(), + init_params: Some( + to_binary(&StablePoolParams { + amp: 100, + owner: None, + }) + .unwrap(), + ), + }; + + let pair = router + .instantiate_contract( + pair_contract_code_id, + owner.clone(), + &msg, + &[], + String::from("PAIR"), + None, + ) + .unwrap(); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Config {}) + .unwrap(); + + let params: StablePoolConfig = from_binary(&res.params.unwrap()).unwrap(); + + assert_eq!(params.amp, Decimal::from_ratio(100u32, 1u32)); + + // Start changing amp with incorrect next amp + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&StablePoolUpdateParams::StartChangingAmp { + next_amp: MAX_AMP + 1, + next_amp_time: router.block_info().time.seconds(), + }) + .unwrap(), + }; + + let resp = router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap_err(); + + assert_eq!( + resp.root_cause().to_string(), + format!( + "Amp coefficient must be greater than 0 and less than or equal to {}", + MAX_AMP + ) + ); + + // Start changing amp with big difference between the old and new amp value + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&StablePoolUpdateParams::StartChangingAmp { + next_amp: 100 * MAX_AMP_CHANGE + 1, + next_amp_time: router.block_info().time.seconds(), + }) + .unwrap(), + }; + + let resp = router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap_err(); + + assert_eq!( + resp.root_cause().to_string(), + format!( + "The difference between the old and new amp value must not exceed {} times", + MAX_AMP_CHANGE + ) + ); + + // Start changing amp before the MIN_AMP_CHANGING_TIME has elapsed + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&StablePoolUpdateParams::StartChangingAmp { + next_amp: 250, + next_amp_time: router.block_info().time.seconds(), + }) + .unwrap(), + }; + + let resp = router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap_err(); + + assert_eq!( + resp.root_cause().to_string(), + format!( + "Amp coefficient cannot be changed more often than once per {} seconds", + MIN_AMP_CHANGING_TIME + ) + ); + + // Start increasing amp + router.update_block(|b| { + b.time = b.time.plus_seconds(MIN_AMP_CHANGING_TIME); + }); + + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&StablePoolUpdateParams::StartChangingAmp { + next_amp: 250, + next_amp_time: router.block_info().time.seconds() + MIN_AMP_CHANGING_TIME, + }) + .unwrap(), + }; + + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap(); + + router.update_block(|b| { + b.time = b.time.plus_seconds(MIN_AMP_CHANGING_TIME / 2); + }); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Config {}) + .unwrap(); + + let params: StablePoolConfig = from_binary(&res.params.unwrap()).unwrap(); + + assert_eq!(params.amp, Decimal::from_ratio(175u32, 1u32)); + + router.update_block(|b| { + b.time = b.time.plus_seconds(MIN_AMP_CHANGING_TIME / 2); + }); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Config {}) + .unwrap(); + + let params: StablePoolConfig = from_binary(&res.params.unwrap()).unwrap(); + + assert_eq!(params.amp, Decimal::from_ratio(250u32, 1u32)); + + // Start decreasing amp + router.update_block(|b| { + b.time = b.time.plus_seconds(MIN_AMP_CHANGING_TIME); + }); + + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&StablePoolUpdateParams::StartChangingAmp { + next_amp: 50, + next_amp_time: router.block_info().time.seconds() + MIN_AMP_CHANGING_TIME, + }) + .unwrap(), + }; + + router + .execute_contract(owner.clone(), pair.clone(), &msg, &[]) + .unwrap(); + + router.update_block(|b| { + b.time = b.time.plus_seconds(MIN_AMP_CHANGING_TIME / 2); + }); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair.clone(), &QueryMsg::Config {}) + .unwrap(); + + let params: StablePoolConfig = from_binary(&res.params.unwrap()).unwrap(); + + assert_eq!(params.amp, Decimal::from_ratio(150u32, 1u32)); + + // Stop changing amp + let msg = ExecuteMsg::UpdateConfig { + params: to_binary(&StablePoolUpdateParams::StopChangingAmp {}).unwrap(), + }; + + router + .execute_contract(owner, pair.clone(), &msg, &[]) + .unwrap(); + + router.update_block(|b| { + b.time = b.time.plus_seconds(MIN_AMP_CHANGING_TIME / 2); + }); + + let res: ConfigResponse = router + .wrap() + .query_wasm_smart(pair, &QueryMsg::Config {}) + .unwrap(); + + let params: StablePoolConfig = from_binary(&res.params.unwrap()).unwrap(); + + assert_eq!(params.amp, Decimal::from_ratio(150u32, 1u32)); +} diff --git a/contracts/astroport/token/.cargo/config b/contracts/astroport/token/.cargo/config new file mode 100644 index 00000000..caa2968c --- /dev/null +++ b/contracts/astroport/token/.cargo/config @@ -0,0 +1,6 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib" +integration-test = "test --test integration" +schema = "run --example token_schema" diff --git a/contracts/astroport/token/Cargo.toml b/contracts/astroport/token/Cargo.toml new file mode 100644 index 00000000..18ea7f5e --- /dev/null +++ b/contracts/astroport/token/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "astroport-token" +version = "1.0.0" +authors = ["Astroport"] +edition = "2021" +description = "Expanded implementation of a CosmWasm-20 compliant token for the Astroport ASTRO token" +license = "MIT" +repository = "https://github.com/CosmWasm/cosmwasm-plus" +homepage = "https://cosmwasm.com" +documentation = "https://docs.cosmwasm.com" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all init/handle/query exports +library = [] + +[dependencies] +astroport = { path = "../../../packages/astroport", default-features = false } +cw2 = "0.15" +cw20 = "0.15" +cw20-base = { version = "0.15", features = ["library"] } +cosmwasm-std = { version = "1.1" } +snafu = { version = "0.6.3" } +cosmwasm-schema = { version = "1.1" } + diff --git a/contracts/astroport/token/README.md b/contracts/astroport/token/README.md new file mode 100644 index 00000000..1465b3f1 --- /dev/null +++ b/contracts/astroport/token/README.md @@ -0,0 +1,9 @@ +# Astroport Token + +This is the contract implementation for the ASTRO token. + +--- + +# CW20 Based Token Contract + +This is a basic implementation of a cw20-base contract [CW20-base](https://github.com/CosmWasm/cw-plus/tree/main/contracts/cw20-base). It implements the [CW20 spec](https://github.com/CosmWasm/cosmwasm-plus/tree/master/packages/cw20) and is designed to be imported into other contracts in order to easily build cw20-compatible tokens with custom logic. diff --git a/contracts/astroport/token/examples/token_schema.rs b/contracts/astroport/token/examples/token_schema.rs new file mode 100644 index 00000000..127eccd0 --- /dev/null +++ b/contracts/astroport/token/examples/token_schema.rs @@ -0,0 +1,12 @@ +use cosmwasm_schema::write_api; + +use astroport::token::InstantiateMsg; +use cw20_base::msg::{ExecuteMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg + } +} diff --git a/contracts/astroport/token/src/contract.rs b/contracts/astroport/token/src/contract.rs new file mode 100644 index 00000000..c1ad4c2b --- /dev/null +++ b/contracts/astroport/token/src/contract.rs @@ -0,0 +1,170 @@ +use cosmwasm_std::{ + entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, +}; +use cw20::{EmbeddedLogo, Logo, LogoInfo, MarketingInfoResponse}; + +use cw2::set_contract_version; +use cw20_base::contract::{create_accounts, execute as cw20_execute, query as cw20_query}; +use cw20_base::msg::{ExecuteMsg, QueryMsg}; +use cw20_base::state::{MinterData, TokenInfo, LOGO, MARKETING_INFO, TOKEN_INFO}; +use cw20_base::ContractError; + +use astroport::asset::{addr_opt_validate, addr_validate_to_lower}; +use astroport::token::{InstantiateMsg, MigrateMsg}; + +/// Contract name that is used for migration. +const CONTRACT_NAME: &str = "astroport-token"; +/// Contract version that is used for migration. +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +const LOGO_SIZE_CAP: usize = 5 * 1024; + +/// Checks if data starts with XML preamble +fn verify_xml_preamble(data: &[u8]) -> Result<(), ContractError> { + // The easiest way to perform this check would be just match on regex, however regex + // compilation is heavy and probably not worth it. + + let preamble = data + .split_inclusive(|c| *c == b'>') + .next() + .ok_or(ContractError::InvalidXmlPreamble {})?; + + const PREFIX: &[u8] = b""; + + if !(preamble.starts_with(PREFIX) && preamble.ends_with(POSTFIX)) { + Err(ContractError::InvalidXmlPreamble {}) + } else { + Ok(()) + } + + // Additionally attributes format could be validated as they are well defined, as well as + // comments presence inside of preable, but it is probably not worth it. +} + +/// Validates XML logo +fn verify_xml_logo(logo: &[u8]) -> Result<(), ContractError> { + verify_xml_preamble(logo)?; + + if logo.len() > LOGO_SIZE_CAP { + Err(ContractError::LogoTooBig {}) + } else { + Ok(()) + } +} + +/// Validates png logo +fn verify_png_logo(logo: &[u8]) -> Result<(), ContractError> { + // PNG header format: + // 0x89 - magic byte, out of ASCII table to fail on 7-bit systems + // "PNG" ascii representation + // [0x0d, 0x0a] - dos style line ending + // 0x1a - dos control character, stop displaying rest of the file + // 0x0a - unix style line ending + const HEADER: [u8; 8] = [0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]; + if logo.len() > LOGO_SIZE_CAP { + Err(ContractError::LogoTooBig {}) + } else if !logo.starts_with(&HEADER) { + Err(ContractError::InvalidPngHeader {}) + } else { + Ok(()) + } +} + +/// Checks if passed logo is correct, and if not, returns an error +fn verify_logo(logo: &Logo) -> Result<(), ContractError> { + match logo { + Logo::Embedded(EmbeddedLogo::Svg(logo)) => verify_xml_logo(logo), + Logo::Embedded(EmbeddedLogo::Png(logo)) => verify_png_logo(logo), + Logo::Url(_) => Ok(()), // Any reasonable url validation would be regex based, probably not worth it + } +} + +/// Creates a new contract with the specified parameters in the [`InstantiateMsg`]. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + mut deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + // Check valid token info + msg.validate()?; + + // Create initial accounts + let total_supply = create_accounts(&mut deps, &msg.initial_balances)?; + + // Check supply cap + if let Some(limit) = msg.get_cap() { + if total_supply > limit { + return Err(StdError::generic_err("Initial supply greater than cap").into()); + } + } + + let mint = match msg.mint { + Some(m) => Some(MinterData { + minter: addr_validate_to_lower(deps.api, &m.minter)?, + cap: m.cap, + }), + None => None, + }; + + // Store token info + let data = TokenInfo { + name: msg.name, + symbol: msg.symbol, + decimals: msg.decimals, + total_supply, + mint, + }; + + TOKEN_INFO.save(deps.storage, &data)?; + + if let Some(marketing) = msg.marketing { + let logo = if let Some(logo) = marketing.logo { + verify_logo(&logo)?; + LOGO.save(deps.storage, &logo)?; + + match logo { + Logo::Url(url) => Some(LogoInfo::Url(url)), + Logo::Embedded(_) => Some(LogoInfo::Embedded), + } + } else { + None + }; + + let data = MarketingInfoResponse { + project: marketing.project, + description: marketing.description, + marketing: addr_opt_validate(deps.api, &marketing.marketing)?, + logo, + }; + MARKETING_INFO.save(deps.storage, &data)?; + } + + Ok(Response::default()) +} + +/// Exposes execute functions available in the contract. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + cw20_execute(deps, env, info, msg) +} + +/// Exposes queries available in the contract. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + cw20_query(deps, env, msg) +} + +/// Manages contract migration. +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + Ok(Response::default()) +} diff --git a/contracts/astroport/token/src/lib.rs b/contracts/astroport/token/src/lib.rs new file mode 100644 index 00000000..2943dbb5 --- /dev/null +++ b/contracts/astroport/token/src/lib.rs @@ -0,0 +1 @@ +pub mod contract; diff --git a/packages/astroport/.cargo/config b/packages/astroport/.cargo/config new file mode 100644 index 00000000..71deaf22 --- /dev/null +++ b/packages/astroport/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib" diff --git a/packages/astroport/Cargo.toml b/packages/astroport/Cargo.toml new file mode 100644 index 00000000..67c81f02 --- /dev/null +++ b/packages/astroport/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "astroport" +version = "2.0.0" +authors = ["Astroport"] +edition = "2021" +description = "Common Astroport types, queriers and other utils" +license = "Apache-2.0" +repository = "https://github.com/astroport-fi/astroport" +homepage = "https://astroport.fi" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] + +[dependencies] +cw20 = { version = "0.15" } +cosmwasm-std = { version = "1.1" } +uint = "0.9" +cw-storage-plus = "0.15" +itertools = "0.10" +cosmwasm-schema = "1.1" diff --git a/packages/astroport/README.md b/packages/astroport/README.md new file mode 100644 index 00000000..461bc7a8 --- /dev/null +++ b/packages/astroport/README.md @@ -0,0 +1,115 @@ +# Astroport: Common Types + +This is a collection of common types and queriers which are commonly used in Astroport contracts. + +## Data Types + +### AssetInfo + +AssetInfo is a convenience wrapper to represent whether a token is the native one (from a specific chain, like LUNA for Terra) or not. It also returns the contract address of that token. + +```rust +pub enum AssetInfo { + Token { contract_addr: Addr }, + NativeToken { denom: String }, +} +``` + +### Asset + +It contains asset info and a token amount. + +```rust +pub struct Asset { + pub info: AssetInfo, + pub amount: Uint128, +} +``` + +### PairInfo + +It is used to represent response data coming from a [Pair-Info-Querier](#Pair-Info-Querier). + +```rust +pub struct PairInfo { + pub asset_infos: [AssetInfo; 2], + pub contract_addr: Addr, + pub liquidity_token: Addr, + pub pair_type: PairType, +} +``` + +## Queriers + +### Native Token Balance Querier + +It uses the CosmWasm standard interface to query an account's balance. + +```rust +pub fn query_balance( + querier: &QuerierWrapper, + account_addr: impl Into, + denom: impl Into, +) -> StdResult +``` + +### Token Balance Querier + +It provides a similar query interface to [Native-Token-Balance-Querier](Native-Token-Balance-Querier) for fetching CW20 token balances. + +```rust +pub fn query_token_balance( + querier: &QuerierWrapper, + contract_addr: impl Into, + account_addr: impl Into, +) -> StdResult +``` + +### Token Supply Querier + +It fetches a CW20 token's total supply. + +```rust +pub fn query_supply( + querier: &QuerierWrapper, + contract_addr: impl Into, +) -> StdResult +``` + +### Pair Info Querier + +Accepts two tokens as input and returns a pair's information. + +```rust +pub fn query_pair_info( + querier: &QuerierWrapper, + factory_contract: impl Into, + asset_infos: &[AssetInfo; 2], +) -> StdResult +``` + +## Swap Pairs Simulating + +### Simulate + +Simulates a swap and returns the output amount, the spread and commission amounts. + +```rust +pub fn simulate( + querier: &QuerierWrapper, + pair_contract: impl Into, + offer_asset: &Asset, +) -> StdResult +``` + +### Reverse Simulate + +Simulates a reverse swap and returns an input amount, the spread and commission amounts. + +```rust +pub fn reverse_simulate( + querier: &QuerierWrapper, + pair_contract: impl Into, + offer_asset: &Asset, +) -> StdResult +``` diff --git a/packages/astroport/src/asset.rs b/packages/astroport/src/asset.rs new file mode 100644 index 00000000..fab26d17 --- /dev/null +++ b/packages/astroport/src/asset.rs @@ -0,0 +1,488 @@ +use cosmwasm_schema::cw_serde; +use std::fmt; + +use crate::factory::PairType; +use crate::pair::QueryMsg as PairQueryMsg; +use crate::querier::{ + query_balance, query_token_balance, query_token_symbol, NATIVE_TOKEN_PRECISION, +}; +use cosmwasm_std::{ + to_binary, Addr, Api, BankMsg, Coin, ConversionOverflowError, CosmosMsg, Decimal256, Fraction, + MessageInfo, QuerierWrapper, StdError, StdResult, Uint128, Uint256, WasmMsg, +}; +use cw20::{Cw20ExecuteMsg, Cw20QueryMsg, MinterResponse, TokenInfoResponse}; +use itertools::Itertools; + +/// UST token denomination +pub const UUSD_DENOM: &str = "uusd"; +/// LUNA token denomination +pub const ULUNA_DENOM: &str = "uluna"; +/// Minimum initial LP share +pub const MINIMUM_LIQUIDITY_AMOUNT: Uint128 = Uint128::new(1_000); + +/// This enum describes a Terra asset (native or CW20). +#[cw_serde] +pub struct Asset { + /// Information about an asset stored in a [`AssetInfo`] struct + pub info: AssetInfo, + /// A token amount + pub amount: Uint128, +} + +/// This struct describes a Terra asset as decimal. +#[cw_serde] +pub struct DecimalAsset { + pub info: AssetInfo, + pub amount: Decimal256, +} + +impl fmt::Display for Asset { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}{}", self.amount, self.info) + } +} + +impl Asset { + /// Returns true if the token is native. Otherwise returns false. + pub fn is_native_token(&self) -> bool { + self.info.is_native_token() + } + + /// Calculates and returns a tax for a chain's native token. For other tokens it returns zero. + pub fn compute_tax(&self, _querier: &QuerierWrapper) -> StdResult { + // tax rate in Terra is set to zero https://terrawiki.org/en/developers/tx-fees + Ok(Uint128::zero()) + } + + /// Calculates and returns a deducted tax for transferring the native token from the chain. For other tokens it returns an [`Err`]. + pub fn deduct_tax(&self, querier: &QuerierWrapper) -> StdResult { + if let AssetInfo::NativeToken { denom } = &self.info { + Ok(Coin { + denom: denom.to_string(), + amount: self.amount.checked_sub(self.compute_tax(querier)?)?, + }) + } else { + Err(StdError::generic_err("cannot deduct tax from token asset")) + } + } + + /// For native tokens of type [`AssetInfo`] uses the default method [`BankMsg::Send`] to send a token amount to a recipient. + /// Before the token is sent, we need to deduct a tax. + /// + /// For a token of type [`AssetInfo`] we use the default method [`Cw20ExecuteMsg::Transfer`] and so there's no need to deduct any other tax. + pub fn into_msg( + self, + querier: &QuerierWrapper, + recipient: impl Into, + ) -> StdResult { + let recipient = recipient.into(); + match &self.info { + AssetInfo::Token { contract_addr } => Ok(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract_addr.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient, + amount: self.amount, + })?, + funds: vec![], + })), + AssetInfo::NativeToken { .. } => Ok(CosmosMsg::Bank(BankMsg::Send { + to_address: recipient, + amount: vec![self.deduct_tax(querier)?], + })), + } + } + + /// Validates an amount of native tokens being sent. + pub fn assert_sent_native_token_balance(&self, message_info: &MessageInfo) -> StdResult<()> { + if let AssetInfo::NativeToken { denom } = &self.info { + match message_info.funds.iter().find(|x| x.denom == *denom) { + Some(coin) => { + if self.amount == coin.amount { + Ok(()) + } else { + Err(StdError::generic_err("Native token balance mismatch between the argument and the transferred")) + } + } + None => { + if self.amount.is_zero() { + Ok(()) + } else { + Err(StdError::generic_err("Native token balance mismatch between the argument and the transferred")) + } + } + } + } else { + Ok(()) + } + } + + pub fn to_decimal_asset(&self, precision: impl Into) -> StdResult { + Ok(DecimalAsset { + info: self.info.clone(), + amount: Decimal256::with_precision(self.amount, precision.into())?, + }) + } +} + +/// This enum describes available Token types. +/// ## Examples +/// ``` +/// # use cosmwasm_std::Addr; +/// # use astroport::asset::AssetInfo::{NativeToken, Token}; +/// Token { contract_addr: Addr::unchecked("stake...") }; +/// NativeToken { denom: String::from("uluna") }; +/// ``` +#[cw_serde] +#[derive(Hash, Eq)] +pub enum AssetInfo { + /// Non-native Token + Token { contract_addr: Addr }, + /// Native token + NativeToken { denom: String }, +} + +impl fmt::Display for AssetInfo { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AssetInfo::NativeToken { denom } => write!(f, "{}", denom), + AssetInfo::Token { contract_addr } => write!(f, "{}", contract_addr), + } + } +} + +impl AssetInfo { + /// Returns true if the caller is a native token. Otherwise returns false. + pub fn is_native_token(&self) -> bool { + match self { + AssetInfo::NativeToken { .. } => true, + AssetInfo::Token { .. } => false, + } + } + + /// Returns the balance of token in a pool. + /// + /// * **pool_addr** is the address of the contract whose token balance we check. + pub fn query_pool( + &self, + querier: &QuerierWrapper, + pool_addr: impl Into, + ) -> StdResult { + match self { + AssetInfo::Token { contract_addr, .. } => { + query_token_balance(querier, contract_addr, pool_addr) + } + AssetInfo::NativeToken { denom } => query_balance(querier, pool_addr, denom), + } + } + + /// Returns the number of decimals that a token has. + pub fn decimals(&self, querier: &QuerierWrapper) -> StdResult { + let decimals = match &self { + AssetInfo::NativeToken { .. } => NATIVE_TOKEN_PRECISION, + AssetInfo::Token { contract_addr } => { + let res: TokenInfoResponse = + querier.query_wasm_smart(contract_addr, &Cw20QueryMsg::TokenInfo {})?; + + res.decimals + } + }; + + Ok(decimals) + } + + /// Returns **true** if the calling token is the same as the token specified in the input parameters. + /// Otherwise returns **false**. + pub fn equal(&self, asset: &AssetInfo) -> bool { + match (self, asset) { + (AssetInfo::NativeToken { denom }, AssetInfo::NativeToken { denom: other_denom }) => { + denom == other_denom + } + ( + AssetInfo::Token { contract_addr }, + AssetInfo::Token { + contract_addr: other_contract_addr, + }, + ) => contract_addr == other_contract_addr, + _ => false, + } + } + + /// If the caller object is a native token of type [`AssetInfo`] then his `denom` field converts to a byte string. + /// + /// If the caller object is a token of type [`AssetInfo`] then its `contract_addr` field converts to a byte string. + pub fn as_bytes(&self) -> &[u8] { + match self { + AssetInfo::NativeToken { denom } => denom.as_bytes(), + AssetInfo::Token { contract_addr } => contract_addr.as_bytes(), + } + } + + /// Checks that the tokens' denom or contract addr is lowercased and valid. + pub fn check(&self, api: &dyn Api) -> StdResult<()> { + match self { + AssetInfo::Token { contract_addr } => { + addr_validate_to_lower(api, contract_addr)?; + } + AssetInfo::NativeToken { denom } => { + if !denom.starts_with("ibc/") && denom != &denom.to_lowercase() { + return Err(StdError::generic_err(format!( + "Non-IBC token denom {} should be lowercase", + denom + ))); + } + } + } + Ok(()) + } +} + +/// This structure stores the main parameters for an Astroport pair +#[cw_serde] +pub struct PairInfo { + /// Asset information for the assets in the pool + pub asset_infos: Vec, + /// Pair contract address + pub contract_addr: Addr, + /// Pair LP token address + pub liquidity_token: Addr, + /// The pool type (xyk, stableswap etc) available in [`PairType`] + pub pair_type: PairType, +} + +impl PairInfo { + /// Returns the balance for each asset in the pool. + /// + /// * **contract_addr** is pair's pool address. + pub fn query_pools( + &self, + querier: &QuerierWrapper, + contract_addr: impl Into, + ) -> StdResult> { + let contract_addr = contract_addr.into(); + self.asset_infos + .iter() + .map(|asset_info| { + Ok(Asset { + info: asset_info.clone(), + amount: asset_info.query_pool(querier, &contract_addr)?, + }) + }) + .collect() + } + + /// Returns the balance for each asset in the pool in decimal. + /// + /// * **contract_addr** is pair's pool address. + pub fn query_pools_decimal( + &self, + querier: &QuerierWrapper, + contract_addr: impl Into, + ) -> StdResult> { + let contract_addr = contract_addr.into(); + self.asset_infos + .iter() + .map(|asset_info| { + Ok(DecimalAsset { + info: asset_info.clone(), + amount: Decimal256::from_atomics( + asset_info.query_pool(querier, &contract_addr)?, + asset_info.decimals(querier)?.into(), + ) + .map_err(|_| StdError::generic_err("Decimal256RangeExceeded"))?, + }) + }) + .collect() + } +} + +/// Returns a lowercased, validated address upon success. +pub fn addr_validate_to_lower(api: &dyn Api, addr: impl Into) -> StdResult { + let addr = addr.into(); + if addr.to_lowercase() != addr { + return Err(StdError::generic_err(format!( + "Address {} should be lowercase", + addr + ))); + } + api.addr_validate(&addr) +} + +/// Returns a lowercased, validated address upon success if present. +pub fn addr_opt_validate(api: &dyn Api, addr: &Option) -> StdResult> { + addr.as_ref() + .map(|addr| addr_validate_to_lower(api, addr)) + .transpose() +} + +const TOKEN_SYMBOL_MAX_LENGTH: usize = 4; + +/// Returns a formatted LP token name +pub fn format_lp_token_name( + asset_infos: &[AssetInfo], + querier: &QuerierWrapper, +) -> StdResult { + let mut short_symbols: Vec = vec![]; + for asset_info in asset_infos { + let short_symbol = match &asset_info { + AssetInfo::NativeToken { denom } => { + denom.chars().take(TOKEN_SYMBOL_MAX_LENGTH).collect() + } + AssetInfo::Token { contract_addr } => { + let token_symbol = query_token_symbol(querier, contract_addr)?; + token_symbol.chars().take(TOKEN_SYMBOL_MAX_LENGTH).collect() + } + }; + short_symbols.push(short_symbol); + } + Ok(format!("{}-LP", short_symbols.iter().join("-")).to_uppercase()) +} + +/// Returns an [`Asset`] object representing a native token and an amount of tokens. +/// +/// * **denom** native asset denomination. +/// +/// * **amount** amount of native assets. +pub fn native_asset(denom: String, amount: Uint128) -> Asset { + Asset { + info: AssetInfo::NativeToken { denom }, + amount, + } +} + +/// Returns an [`Asset`] object representing a non-native token and an amount of tokens. +/// ## Params +/// * **contract_addr** iaddress of the token contract. +/// +/// * **amount** amount of tokens. +pub fn token_asset(contract_addr: Addr, amount: Uint128) -> Asset { + Asset { + info: AssetInfo::Token { contract_addr }, + amount, + } +} + +/// Returns an [`AssetInfo`] object representing the denomination for native asset. +pub fn native_asset_info(denom: String) -> AssetInfo { + AssetInfo::NativeToken { denom } +} + +/// Returns an [`AssetInfo`] object representing the address of a token contract. +pub fn token_asset_info(contract_addr: Addr) -> AssetInfo { + AssetInfo::Token { contract_addr } +} + +/// Returns [`PairInfo`] by specified pool address. +/// +/// * **pool_addr** address of the pool. +pub fn pair_info_by_pool(querier: &QuerierWrapper, pool: impl Into) -> StdResult { + let minter_info: MinterResponse = querier.query_wasm_smart(pool, &Cw20QueryMsg::Minter {})?; + + let pair_info: PairInfo = + querier.query_wasm_smart(minter_info.minter, &PairQueryMsg::Pair {})?; + + Ok(pair_info) +} + +/// Checks swap parameters. +/// +/// * **pools** amount of tokens in pools. +/// +/// * **swap_amount** amount to swap. +pub fn check_swap_parameters(pools: Vec, swap_amount: Uint128) -> StdResult<()> { + if pools.iter().any(|pool| pool.is_zero()) { + return Err(StdError::generic_err("One of the pools is empty")); + } + + if swap_amount.is_zero() { + return Err(StdError::generic_err("Swap amount must not be zero")); + } + + Ok(()) +} + +/// Trait extension for AssetInfo to produce [`Asset`] objects from [`AssetInfo`]. +pub trait AssetInfoExt { + fn with_balance(&self, balance: impl Into) -> Asset; +} + +impl AssetInfoExt for AssetInfo { + fn with_balance(&self, balance: impl Into) -> Asset { + Asset { + info: self.clone(), + amount: balance.into(), + } + } +} + +/// Trait extension for Decimal256 to work with token precisions more accurately. +pub trait Decimal256Ext { + fn to_uint256(&self) -> Uint256; + + fn to_uint128_with_precision(&self, precision: impl Into) -> StdResult; + + fn to_uint256_with_precision(&self, precision: impl Into) -> StdResult; + + fn from_integer(i: impl Into) -> Self; + + fn checked_multiply_ratio( + &self, + numerator: Decimal256, + denominator: Decimal256, + ) -> StdResult; + + fn with_precision( + value: impl Into, + precision: impl Into, + ) -> StdResult; +} + +impl Decimal256Ext for Decimal256 { + fn to_uint256(&self) -> Uint256 { + self.numerator() / self.denominator() + } + + fn to_uint128_with_precision(&self, precision: impl Into) -> StdResult { + let value = self.atomics(); + let precision = precision.into(); + + value + .checked_div(10u128.pow(self.decimal_places() - precision).into())? + .try_into() + .map_err(|o: ConversionOverflowError| { + StdError::generic_err(format!("Error converting {}", o.value)) + }) + } + + fn to_uint256_with_precision(&self, precision: impl Into) -> StdResult { + let value = self.atomics(); + let precision = precision.into(); + + value + .checked_div(10u128.pow(self.decimal_places() - precision).into()) + .map_err(|_| StdError::generic_err("DivideByZeroError")) + } + + fn from_integer(i: impl Into) -> Self { + Decimal256::from_ratio(i.into(), 1u8) + } + + fn checked_multiply_ratio( + &self, + numerator: Decimal256, + denominator: Decimal256, + ) -> StdResult { + Ok(Decimal256::new( + self.atomics() + .checked_multiply_ratio(numerator.atomics(), denominator.atomics()) + .map_err(|_| StdError::generic_err("CheckedMultiplyRatioError"))?, + )) + } + + fn with_precision( + value: impl Into, + precision: impl Into, + ) -> StdResult { + Decimal256::from_atomics(value, precision.into()) + .map_err(|_| StdError::generic_err("Decimal256 range exceeded")) + } +} diff --git a/packages/astroport/src/common.rs b/packages/astroport/src/common.rs new file mode 100644 index 00000000..bcd56215 --- /dev/null +++ b/packages/astroport/src/common.rs @@ -0,0 +1,134 @@ +use crate::asset::addr_validate_to_lower; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{attr, Addr, Api, DepsMut, Env, MessageInfo, Response, StdError, StdResult}; +use cw_storage_plus::Item; + +const MAX_PROPOSAL_TTL: u64 = 1209600; + +/// This structure describes the parameters used for creating a request for a change of contract ownership. +#[cw_serde] +pub struct OwnershipProposal { + /// The newly proposed contract owner + pub owner: Addr, + /// Time until the proposal to change ownership expires + pub ttl: u64, +} + +/// Creates a new request to change contract ownership. +/// +/// `new_owner` is the newly proposed owner. +/// +/// `expires_in` is the time during which the ownership change proposal is still valid. +/// +/// `owner` is the current owner. +/// +/// ## Executor +/// Only the current contract owner can execute this. +pub fn propose_new_owner( + deps: DepsMut, + info: MessageInfo, + env: Env, + new_owner: String, + expires_in: u64, + owner: Addr, + proposal: Item, +) -> StdResult { + // Permission check + if info.sender != owner { + return Err(StdError::generic_err("Unauthorized")); + } + + let new_owner = addr_validate_to_lower(deps.api, new_owner.as_str())?; + + // Check that the new owner is not the same as the current one + if new_owner == owner { + return Err(StdError::generic_err("New owner cannot be same")); + } + + if MAX_PROPOSAL_TTL < expires_in { + return Err(StdError::generic_err(format!( + "Parameter expires_in cannot be higher than {}", + MAX_PROPOSAL_TTL + ))); + } + + proposal.save( + deps.storage, + &OwnershipProposal { + owner: new_owner.clone(), + ttl: env.block.time.seconds() + expires_in, + }, + )?; + + Ok(Response::new().add_attributes(vec![ + attr("action", "propose_new_owner"), + attr("new_owner", new_owner), + ])) +} + +/// Removes a request to change contract ownership. +/// `owner` is the current contract owner. +/// +/// ## Executor +/// Only the current owner can execute this. +pub fn drop_ownership_proposal( + deps: DepsMut, + info: MessageInfo, + owner: Addr, + proposal: Item, +) -> StdResult { + // Permission check + if info.sender != owner { + return Err(StdError::generic_err("Unauthorized")); + } + + proposal.remove(deps.storage); + + Ok(Response::new().add_attributes(vec![attr("action", "drop_ownership_proposal")])) +} + +/// Claims ownership over the contract. +/// +/// `cb` is a callback function to process ownership transition. +/// +/// ## Executor +/// Only the newly proposed owner can execute this. +pub fn claim_ownership( + deps: DepsMut, + info: MessageInfo, + env: Env, + proposal: Item, + cb: fn(DepsMut, Addr) -> StdResult<()>, +) -> StdResult { + let p = proposal + .load(deps.storage) + .map_err(|_| StdError::generic_err("Ownership proposal not found"))?; + + // Check the sender + if info.sender != p.owner { + return Err(StdError::generic_err("Unauthorized")); + } + + if env.block.time.seconds() > p.ttl { + return Err(StdError::generic_err("Ownership proposal expired")); + } + + proposal.remove(deps.storage); + + // Run callback + cb(deps, p.owner.clone())?; + + Ok(Response::new().add_attributes(vec![ + attr("action", "claim_ownership"), + attr("new_owner", p.owner), + ])) +} + +/// Bulk validation and conversion between [`String`] -> [`Addr`] for an array of addresses. +/// If any address is invalid, the function returns [`StdError`]. +pub fn validate_addresses(api: &dyn Api, admins: &[String]) -> StdResult> { + admins + .iter() + .map(|addr| addr_validate_to_lower(api, addr)) + .collect() +} diff --git a/packages/astroport/src/cosmwasm_ext.rs b/packages/astroport/src/cosmwasm_ext.rs new file mode 100644 index 00000000..34132358 --- /dev/null +++ b/packages/astroport/src/cosmwasm_ext.rs @@ -0,0 +1,52 @@ +use std::ops; + +use crate::asset::Decimal256Ext; +use cosmwasm_std::{ + ConversionOverflowError, Decimal, Decimal256, Fraction, StdResult, Uint128, Uint256, Uint64, +}; + +pub trait AbsDiff +where + Self: Copy + PartialOrd + ops::Sub, +{ + fn diff(self, rhs: Self) -> Self { + if self > rhs { + self - rhs + } else { + rhs - self + } + } +} + +impl AbsDiff for Uint256 {} +impl AbsDiff for Uint128 {} +impl AbsDiff for Uint64 {} +impl AbsDiff for Decimal {} +impl AbsDiff for Decimal256 {} + +pub trait IntegerToDecimal +where + Self: Copy + Into + Into, +{ + fn to_decimal(self) -> Decimal { + Decimal::from_ratio(self, 1u8) + } + + fn to_decimal256(self, precision: impl Into) -> StdResult { + Decimal256::with_precision(self, precision) + } +} + +impl IntegerToDecimal for u64 {} +impl IntegerToDecimal for Uint128 {} + +pub trait DecimalToInteger { + fn to_uint(self, precision: impl Into) -> Result; +} + +impl DecimalToInteger for Decimal256 { + fn to_uint(self, precision: impl Into) -> Result { + let multiplier = Uint256::from(10u8).pow(precision.into()); + (multiplier * self.numerator() / self.denominator()).try_into() + } +} diff --git a/packages/astroport/src/factory.rs b/packages/astroport/src/factory.rs new file mode 100644 index 00000000..b9771a50 --- /dev/null +++ b/packages/astroport/src/factory.rs @@ -0,0 +1,229 @@ +use crate::asset::{AssetInfo, PairInfo}; + +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Binary}; +use cw_storage_plus::Map; +use std::fmt::{Display, Formatter, Result}; + +const MAX_TOTAL_FEE_BPS: u16 = 10_000; +const MAX_MAKER_FEE_BPS: u16 = 10_000; + +/// This enum describes available pair types. +/// ## Available pool types +/// ``` +/// # use astroport::factory::PairType::{Custom, Stable, Xyk}; +/// Xyk {}; +/// Stable {}; +/// Custom(String::from("Custom")); +/// ``` +#[cw_serde] +pub enum PairType { + /// XYK pair type + Xyk {}, + /// Stable pair type + Stable {}, + /// Concentrated liquidity pair type + Concentrated {}, + /// Custom pair type + Custom(String), +} + +/// Returns a raw encoded string representing the name of each pool type +impl Display for PairType { + fn fmt(&self, fmt: &mut Formatter) -> Result { + match self { + PairType::Xyk {} => fmt.write_str("xyk"), + PairType::Stable {} => fmt.write_str("stable"), + PairType::Concentrated {} => fmt.write_str("concentrated"), + PairType::Custom(pair_type) => fmt.write_str(format!("custom-{}", pair_type).as_str()), + } + } +} + +/// This structure stores a pair type's configuration. +#[cw_serde] +pub struct PairConfig { + /// ID of contract which is allowed to create pairs of this type + pub code_id: u64, + /// The pair type (provided in a [`PairType`]) + pub pair_type: PairType, + /// The total fees (in bps) charged by a pair of this type + pub total_fee_bps: u16, + /// The amount of fees (in bps) collected by the Maker contract from this pair type + pub maker_fee_bps: u16, + /// Whether a pair type is disabled or not. If it is disabled, new pairs cannot be + /// created, but existing ones can still read the pair configuration + pub is_disabled: bool, + /// Setting this to true means that pairs of this type will not be able + /// to get an ASTRO generator + pub is_generator_disabled: bool, +} + +impl PairConfig { + /// This method is used to check fee bps. + pub fn valid_fee_bps(&self) -> bool { + self.total_fee_bps <= MAX_TOTAL_FEE_BPS && self.maker_fee_bps <= MAX_MAKER_FEE_BPS + } +} + +/// This structure stores the basic settings for creating a new factory contract. +#[cw_serde] +pub struct InstantiateMsg { + /// IDs of contracts that are allowed to instantiate pairs + pub pair_configs: Vec, + /// CW20 token contract code identifier + pub token_code_id: u64, + /// Contract address to send governance fees to (the Maker) + pub fee_address: Option, + /// Address of contract that is used to auto_stake LP tokens once someone provides liquidity in a pool + pub generator_address: Option, + /// Address of owner that is allowed to change factory contract parameters + pub owner: String, + /// CW1 whitelist contract code id used to store 3rd party rewards for staking Astroport LP tokens + pub whitelist_code_id: u64, +} + +/// This structure describes the execute messages of the contract. +#[cw_serde] +pub enum ExecuteMsg { + /// UpdateConfig updates relevant code IDs + UpdateConfig { + /// CW20 token contract code identifier + token_code_id: Option, + /// Contract address to send governance fees to (the Maker) + fee_address: Option, + /// Contract address where Lp tokens can be auto_staked after someone provides liquidity in an incentivized Astroport pool + generator_address: Option, + /// CW1 whitelist contract code id used to store 3rd party rewards for staking Astroport LP tokens + whitelist_code_id: Option, + }, + /// UpdatePairConfig updates the config for a pair type. + UpdatePairConfig { + /// New [`PairConfig`] settings for a pair type + config: PairConfig, + }, + /// CreatePair instantiates a new pair contract. + CreatePair { + /// The pair type (exposed in [`PairType`]) + pair_type: PairType, + /// The assets to create the pool for + asset_infos: Vec, + /// Optional binary serialised parameters for custom pool types + init_params: Option, + }, + /// Deregister removes a previously created pair. + Deregister { + /// The assets for which we deregister a pool + asset_infos: Vec, + }, + /// ProposeNewOwner creates a proposal to change contract ownership. + /// The validity period for the proposal is set in the `expires_in` variable. + ProposeNewOwner { + /// Newly proposed contract owner + owner: String, + /// The date after which this proposal expires + expires_in: u64, + }, + /// DropOwnershipProposal removes the existing offer to change contract ownership. + DropOwnershipProposal {}, + /// Used to claim contract ownership. + ClaimOwnership {}, + /// MarkAsMigrated marks pairs as migrated + MarkAsMigrated { pairs: Vec }, +} + +/// This structure describes the available query messages for the factory contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Config returns contract settings specified in the custom [`ConfigResponse`] structure. + #[returns(ConfigResponse)] + Config {}, + /// Pair returns information about a specific pair according to the specified assets. + #[returns(PairInfo)] + Pair { + /// The assets for which we return a pair + asset_infos: Vec, + }, + /// Pairs returns an array of pairs and their information according to the specified parameters in `start_after` and `limit` variables. + #[returns(PairsResponse)] + Pairs { + /// The pair item to start reading from. It is an [`Option`] type that accepts [`AssetInfo`] elements. + start_after: Option>, + /// The number of pairs to read and return. It is an [`Option`] type. + limit: Option, + }, + /// FeeInfo returns fee parameters for a specific pair. The response is returned using a [`FeeInfoResponse`] structure + #[returns(FeeInfoResponse)] + FeeInfo { + /// The pair type for which we return fee information. Pair type is a [`PairType`] struct + pair_type: PairType, + }, + /// Returns a vector that contains blacklisted pair types + #[returns(Vec)] + BlacklistedPairTypes {}, + /// Returns a vector that contains pair addresses that are not migrated + #[returns(Vec)] + PairsToMigrate {}, +} + +/// A custom struct for each query response that returns general contract settings/configs. +#[cw_serde] +pub struct ConfigResponse { + /// Addres of owner that is allowed to change contract parameters + pub owner: Addr, + /// IDs of contracts which are allowed to create pairs + pub pair_configs: Vec, + /// CW20 token contract code identifier + pub token_code_id: u64, + /// Address of contract to send governance fees to (the Maker) + pub fee_address: Option, + /// Address of contract used to auto_stake LP tokens for Astroport pairs that are incentivized + pub generator_address: Option, + /// CW1 whitelist contract code id used to store 3rd party rewards for staking Astroport LP tokens + pub whitelist_code_id: u64, +} + +/// This structure stores the parameters used in a migration message. +#[cw_serde] +pub struct MigrateMsg { + pub params: Binary, +} + +/// A custom struct for each query response that returns an array of objects of type [`PairInfo`]. +#[cw_serde] +pub struct PairsResponse { + /// Arrays of structs containing information about multiple pairs + pub pairs: Vec, +} + +/// A custom struct for each query response that returns an object of type [`FeeInfoResponse`]. +#[cw_serde] +pub struct FeeInfoResponse { + /// Contract address to send governance fees to + pub fee_address: Option, + /// Total amount of fees (in bps) charged on a swap + pub total_fee_bps: u16, + /// Amount of fees (in bps) sent to the Maker contract + pub maker_fee_bps: u16, +} + +/// This is an enum used for setting and removing a contract address. +#[cw_serde] +pub enum UpdateAddr { + /// Sets a new contract address. + Set(String), + /// Removes a contract address. + Remove {}, +} + +/// Map which contains a list of all pairs which are able to convert X <> Y assets. +/// Example: given 3 pools (X, Y), (X,Y,Z) and (X,Y,Z,W), the map will contain the following entries +/// (pair addresses): +/// `ROUTE[X][Y] = [(X,Y), (X,Y,Z), (X,Y,Z,W)]` +/// `ROUTE[X][Z] = [(X,Y,Z), (X,Y,Z,W)]` +/// `ROUTE[X][W] = [(X,Y,Z,W)]` +/// ... +/// +/// Notice that `ROUTE[X][Y] = ROUTE[Y][X]` +pub const ROUTE: Map<(String, String), Vec> = Map::new("routes"); diff --git a/packages/astroport/src/generator.rs b/packages/astroport/src/generator.rs new file mode 100644 index 00000000..998adf2d --- /dev/null +++ b/packages/astroport/src/generator.rs @@ -0,0 +1,432 @@ +use crate::asset::{Asset, AssetInfo}; +use crate::factory::PairType; +use crate::restricted_vector::RestrictedVector; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{to_binary, Addr, Decimal, Env, StdResult, SubMsg, Uint128, Uint64, WasmMsg}; +use cw20::Cw20ReceiveMsg; + +/// This structure describes the parameters used for creating a contract. +#[cw_serde] +pub struct InstantiateMsg { + /// Address that can change contract settings + pub owner: String, + /// Address of factory contract + pub factory: String, + /// Address that can set active generators and their alloc points + pub generator_controller: Option, + /// The voting escrow delegation contract address + pub voting_escrow_delegation: Option, + /// The voting escrow contract address + pub voting_escrow: Option, + /// Address of guardian + pub guardian: Option, + /// ASTRO token contract address + pub astro_token: String, + /// Amount of ASTRO distributed per block among all pairs + pub tokens_per_block: Uint128, + /// Start block for distributing ASTRO + pub start_block: Uint64, + /// The ASTRO vesting contract that drips ASTRO rewards + pub vesting_contract: String, + /// Whitelist code id + pub whitelist_code_id: u64, +} + +#[cw_serde] +pub enum ExecuteMsg { + /// Update the address of the ASTRO vesting contract + /// ## Executor + /// Only the owner can execute it. + UpdateConfig { + /// The new vesting contract address + vesting_contract: Option, + /// The new generator controller contract address + generator_controller: Option, + /// The new generator guardian + guardian: Option, + /// The new voting escrow delegation contract address + voting_escrow_delegation: Option, + /// The new voting escrow contract address + voting_escrow: Option, + /// The amount of generators + checkpoint_generator_limit: Option, + }, + /// Setup generators with their respective allocation points. + /// ## Executor + /// Only the owner or generator controller can execute this. + SetupPools { + /// The list of pools with allocation point. + pools: Vec<(String, Uint128)>, + }, + /// Update the given pool's ASTRO allocation slice + /// ## Executor + /// Only the owner or generator controller can execute this. + UpdatePool { + /// The address of the LP token contract address whose allocation we change + lp_token: String, + /// This flag determines whether the pool gets 3rd party token rewards + has_asset_rewards: bool, + }, + /// Update rewards and return it to user. + ClaimRewards { + /// the LP token contract address + lp_tokens: Vec, + }, + /// Withdraw LP tokens from the Generator + Withdraw { + /// The address of the LP token to withdraw + lp_token: String, + /// The amount to withdraw + amount: Uint128, + }, + /// Withdraw LP tokens from the Generator without withdrawing outstanding rewards + EmergencyWithdraw { + /// The address of the LP token to withdraw + lp_token: String, + }, + /// Sends orphan proxy rewards (which were left behind after emergency withdrawals) to another address + SendOrphanProxyReward { + /// The transfer recipient + recipient: String, + /// The address of the LP token contract for which we send orphaned rewards + lp_token: String, + }, + /// Receives a message of type [`Cw20ReceiveMsg`] + Receive(Cw20ReceiveMsg), + /// Set a new amount of ASTRO to distribute per block + /// ## Executor + /// Only the owner can execute this. + SetTokensPerBlock { + /// The new amount of ASTRO to distro per block + amount: Uint128, + }, + /// Creates a request to change contract ownership + /// ## Executor + /// Only the current owner can execute this. + ProposeNewOwner { + /// The newly proposed owner + owner: String, + /// The validity period of the proposal to change the contract owner + expires_in: u64, + }, + /// Removes a request to change contract ownership + /// ## Executor + /// Only the current owner can execute this + DropOwnershipProposal {}, + /// Claims contract ownership + /// ## Executor + /// Only the newly proposed owner can execute this + ClaimOwnership {}, + /// Sets a new proxy contract for a specific generator + /// Sets a proxy for the pool + /// ## Executor + /// Only the current owner or generator controller can execute this + MoveToProxy { + lp_token: String, + proxy: String, + }, + MigrateProxy { + lp_token: String, + new_proxy: String, + }, + /// Add or remove token to the block list + UpdateBlockedTokenslist { + /// Tokens to add + add: Option>, + /// Tokens to remove + remove: Option>, + }, + /// Sets the allocation point to zero for the specified pool + DeactivatePool { + lp_token: String, + }, + /// Sets the allocation point to zero for each pool by the pair type + DeactivatePools { + pair_types: Vec, + }, + /// Updates the boost emissions for specified user and generators + CheckpointUserBoost { + generators: Vec, + user: Option, + }, + /// Process action after the callback + Callback { + action: ExecuteOnReply, + }, +} + +#[cw_serde] +pub enum ExecuteOnReply { + /// Updates reward and returns it to user. + ClaimRewards { + /// The list of LP tokens contract + lp_tokens: Vec, + /// The rewards recipient + account: Addr, + }, + /// Stake LP tokens in the Generator to receive token emissions + Deposit { + /// The LP token to stake + lp_token: Addr, + /// The account that receives ownership of the staked tokens + account: Addr, + /// The amount of tokens to deposit + amount: Uint128, + }, + /// Withdraw LP tokens from the Generator + Withdraw { + /// The LP tokens to withdraw + lp_token: Addr, + /// The account that receives the withdrawn LP tokens + account: Addr, + /// The amount of tokens to withdraw + amount: Uint128, + }, + /// Sets a new amount of ASTRO to distribute per block between all active generators + SetTokensPerBlock { + /// The new amount of ASTRO to distribute per block + amount: Uint128, + }, + /// Migrate LP tokens and collected rewards to new proxy + MigrateProxy { lp_addr: Addr, new_proxy_addr: Addr }, + /// Stake LP tokens into new reward proxy + MigrateProxyDepositLP { + lp_addr: Addr, + prev_proxy_addr: Addr, + amount: Uint128, + }, +} + +impl ExecuteOnReply { + pub fn into_submsg(self, env: &Env) -> StdResult { + let msg = SubMsg::new(WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_binary(&ExecuteMsg::Callback { action: self })?, + funds: vec![], + }); + + Ok(msg) + } +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Returns the length of the array that contains all the active pool generators + #[returns(usize)] + ActivePoolLength {}, + /// PoolLength returns the length of the array that contains all the instantiated pool generators + #[returns(usize)] + PoolLength {}, + /// Deposit returns the LP token amount deposited in a specific generator + #[returns(Uint128)] + Deposit { lp_token: String, user: String }, + /// Returns the current virtual amount in a specific generator + #[returns(Uint128)] + UserVirtualAmount { lp_token: String, user: String }, + /// Returns the total virtual supply of generator + #[returns(Uint128)] + TotalVirtualSupply { generator: String }, + /// PendingToken returns the amount of rewards that can be claimed by an account that deposited a specific LP token in a generator + #[returns(PendingTokenResponse)] + PendingToken { lp_token: String, user: String }, + /// Config returns the main contract parameters + #[returns(Config)] + Config {}, + /// RewardInfo returns reward information for a specified LP token + #[returns(RewardInfoResponse)] + RewardInfo { lp_token: String }, + /// OrphanProxyRewards returns orphaned reward information for the specified LP token + #[returns(Vec<(AssetInfo, Uint128)>)] + OrphanProxyRewards { lp_token: String }, + /// PoolInfo returns information about a pool associated with the specified LP token alongside + /// the total pending amount of ASTRO and proxy rewards claimable by generator stakers (for that LP token) + #[returns(PoolInfoResponse)] + PoolInfo { lp_token: String }, + /// SimulateFutureReward returns the amount of ASTRO that will be distributed until a future block and for a specific generator + #[returns(Uint128)] + SimulateFutureReward { lp_token: String, future_block: u64 }, + /// Returns a list of stakers for a specific generator + #[returns(Vec)] + PoolStakers { + lp_token: String, + start_after: Option, + limit: Option, + }, + /// Returns the blocked list of tokens + #[returns(Vec)] + BlockedTokensList {}, + /// Returns a list of reward proxy contracts which have been ever used + #[returns(Vec)] + RewardProxiesList {}, +} + +/// This structure holds the response returned when querying the amount of pending rewards that can be withdrawn from a 3rd party +/// rewards contract +#[cw_serde] +pub struct PendingTokenResponse { + /// The amount of pending ASTRO + pub pending: Uint128, + /// The amount of pending 3rd party reward tokens + pub pending_on_proxy: Option>, +} + +/// This structure describes the main information of pool +#[cw_serde] +pub struct PoolInfo { + /// Accumulated amount of reward per share unit. Used for reward calculations + pub last_reward_block: Uint64, + pub reward_global_index: Decimal, + /// the reward proxy contract + pub reward_proxy: Option, + /// Accumulated reward indexes per reward proxy. Vector of pairs (reward_proxy, index). + pub accumulated_proxy_rewards_per_share: RestrictedVector, + /// for calculation of new proxy rewards + pub proxy_reward_balance_before_update: Uint128, + /// the orphan proxy rewards which are left by emergency withdrawals. Vector of pairs (reward_proxy, index). + pub orphan_proxy_rewards: RestrictedVector, + /// The pool has assets giving additional rewards + pub has_asset_rewards: bool, + /// Total virtual amount + pub total_virtual_supply: Uint128, +} + +/// This structure stores the outstanding amount of token rewards that a user accrued. +/// Currently the contract works with UserInfoV2 structure, but this structure is kept for +/// compatibility with the old version. +#[cw_serde] +#[derive(Default)] +pub struct UserInfo { + /// The amount of LP tokens staked + pub amount: Uint128, + /// The amount of ASTRO rewards a user already received or is not eligible for; used for proper reward calculation + pub reward_debt: Uint128, + /// Proxy reward amount a user already received or is not eligible for; used for proper reward calculation + pub reward_debt_proxy: Uint128, +} + +/// This structure stores the outstanding amount of token rewards that a user accrued. +#[cw_serde] +#[derive(Default)] +pub struct UserInfoV2 { + /// The amount of LP tokens staked + pub amount: Uint128, + /// The amount of ASTRO rewards a user already received or is not eligible for; used for proper reward calculation + pub reward_user_index: Decimal, + /// Proxy reward amount a user already received per reward proxy; used for proper reward calculation + /// Vector of pairs (reward_proxy, reward debited). + pub reward_debt_proxy: RestrictedVector, + /// The amount of user boosted emissions + pub virtual_amount: Uint128, +} + +/// This structure holds the response returned when querying for the token addresses used to reward a specific generator +#[cw_serde] +pub struct RewardInfoResponse { + /// The address of the base reward token + pub base_reward_token: Addr, + /// The address of the 3rd party reward token + pub proxy_reward_token: Option, +} + +/// This structure holds the response returned when querying for a pool's information +#[cw_serde] +pub struct PoolInfoResponse { + /// The slice of ASTRO that this pool's generator gets per block + pub alloc_point: Uint128, + /// Amount of ASTRO tokens being distributed per block to this LP pool + pub astro_tokens_per_block: Uint128, + /// The last block when token emissions were snapshotted (distributed) + pub last_reward_block: u64, + /// Current block number. Useful for computing APRs off-chain + pub current_block: u64, + /// Total amount of ASTRO rewards already accumulated per LP token staked + pub global_reward_index: Decimal, + /// Pending amount of total ASTRO rewards which are claimable by stakers right now + pub pending_astro_rewards: Uint128, + /// The address of the 3rd party reward proxy contract + pub reward_proxy: Option, + /// Pending amount of total proxy rewards which are claimable by stakers right now + pub pending_proxy_rewards: Option, + /// Total amount of 3rd party token rewards already accumulated per LP token staked per proxy + pub accumulated_proxy_rewards_per_share: Vec<(Addr, Decimal)>, + /// Reward balance for the dual rewards proxy before updating accrued rewards + pub proxy_reward_balance_before_update: Uint128, + /// The amount of orphan proxy rewards which are left behind by emergency withdrawals and not yet transferred out + pub orphan_proxy_rewards: Vec<(Addr, Uint128)>, + /// Total amount of lp tokens staked in the pool's generator + pub lp_supply: Uint128, +} + +/// This structure stores the core parameters for the Generator contract. +#[cw_serde] +pub struct Config { + /// Address allowed to change contract parameters + pub owner: Addr, + /// The Factory address + pub factory: Addr, + /// Contract address which can only set active generators and their alloc points + pub generator_controller: Option, + /// The voting escrow contract address + pub voting_escrow: Option, + /// The voting escrow delegation contract address + pub voting_escrow_delegation: Option, + /// The ASTRO token address + pub astro_token: Addr, + /// Total amount of ASTRO rewards per block + pub tokens_per_block: Uint128, + /// Total allocation points. Must be the sum of all allocation points in all active generators + pub total_alloc_point: Uint128, + /// The block number when the ASTRO distribution starts + pub start_block: Uint64, + /// The vesting contract from which rewards are distributed + pub vesting_contract: Addr, + /// The list of active pools with allocation points + pub active_pools: Vec<(Addr, Uint128)>, + /// The list of blocked tokens + pub blocked_tokens_list: Vec, + /// The guardian address which can add or remove tokens from blacklist + pub guardian: Option, + /// The amount of generators + pub checkpoint_generator_limit: Option, +} + +/// This structure describes a migration message. +#[cw_serde] +pub struct MigrateMsg { + /// The Factory address + pub factory: Option, + /// Contract address which can only set active generators and their alloc points + pub generator_controller: Option, + /// The blocked list of tokens + pub blocked_list_tokens: Option>, + /// The guardian address + pub guardian: Option, + /// Whitelist code id + pub whitelist_code_id: Option, + /// The voting escrow contract + pub voting_escrow: Option, + /// The voting escrow delegation contract + pub voting_escrow_delegation: Option, + /// The limit of generators + pub generator_limit: Option, +} + +/// This structure describes custom hooks for the CW20. +#[cw_serde] +pub enum Cw20HookMsg { + /// Deposit performs a token deposit on behalf of the message sender. + Deposit {}, + /// DepositFor performs a token deposit on behalf of another address that's not the message sender. + DepositFor(Addr), +} + +/// This structure holds the parameters used to return information about a staked in +/// a specific generator. +#[cw_serde] +pub struct StakerResponse { + // The staker's address + pub account: String, + // The amount that the staker currently has in the generator + pub amount: Uint128, +} diff --git a/packages/astroport/src/generator_proxy.rs b/packages/astroport/src/generator_proxy.rs new file mode 100644 index 00000000..9e2cd7cc --- /dev/null +++ b/packages/astroport/src/generator_proxy.rs @@ -0,0 +1,90 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Uint128}; +use cw20::Cw20ReceiveMsg; + +/// This structure describes the basic parameters for creating a contract. +#[cw_serde] +pub struct InstantiateMsg { + /// The generator contract address + pub generator_contract_addr: String, + /// The pair contract address used in this generator proxy + pub pair_addr: String, + /// The LP contract address which can be staked in the reward_contract + pub lp_token_addr: String, + /// The 3rd party reward contract address + pub reward_contract_addr: String, + /// The 3rd party reward token contract address + pub reward_token_addr: String, +} + +#[cw_serde] +pub enum Cw20HookMsg { + Deposit {}, +} + +/// This structure describes the execute messages available in the contract. +#[cw_serde] +pub enum ExecuteMsg { + /// Receives a message of type [`Cw20ReceiveMsg`] + Receive(Cw20ReceiveMsg), + /// Withdraw pending token rewards from the 3rd party staking contract + UpdateRewards {}, + /// Sends rewards to a recipient + SendRewards { account: String, amount: Uint128 }, + /// Withdraw LP tokens and outstanding token rewards + Withdraw { + /// The address that will receive the withdrawn tokens and rewards + account: String, + /// The amount of LP tokens to withdraw + amount: Uint128, + }, + /// Withdraw LP tokens without claiming rewards + EmergencyWithdraw { + /// The address that will receive the withdrawn tokens + account: String, + /// The amount of LP tokens to withdraw + amount: Uint128, + }, + /// Callback of type [`CallbackMsg`] + Callback(CallbackMsg), +} + +/// This structure describes the callback messages available in the contract. +#[cw_serde] +pub enum CallbackMsg { + TransferLpTokensAfterWithdraw { + /// The LP token recipient + account: Addr, + /// The previous LP balance for the contract. This is used to calculate + /// the amount of received LP tokens after withdrawing from a third party contract + prev_lp_balance: Uint128, + }, +} + +/// This structure describes query messages available in the contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Returns the contract's core configuration + #[returns(ConfigResponse)] + Config {}, + /// Returns the amount of deposited LP tokens + #[returns(Uint128)] + Deposit {}, + /// Returns the amount of rewards to be distributed + #[returns(Uint128)] + Reward {}, + /// Returns the amount of pending rewards which can be claimed right now + #[returns(Uint128)] + PendingToken {}, + /// Returns the 3rd party reward token contract address + #[returns(Addr)] + RewardInfo {}, +} + +pub type ConfigResponse = InstantiateMsg; + +/// This structure describes a migration message. +/// We currently take no arguments for migrations +#[cw_serde] +pub struct MigrateMsg {} diff --git a/packages/astroport/src/lib.rs b/packages/astroport/src/lib.rs new file mode 100644 index 00000000..e2a4bce0 --- /dev/null +++ b/packages/astroport/src/lib.rs @@ -0,0 +1,90 @@ +pub mod asset; +pub mod common; +pub mod cosmwasm_ext; +pub mod factory; +pub mod generator; +pub mod generator_proxy; +pub mod maker; +pub mod oracle; +pub mod pair; +pub mod pair_bonded; +pub mod pair_concentrated; +pub mod pair_stable_bluna; +pub mod querier; +pub mod restricted_vector; +pub mod router; +pub mod staking; +pub mod token; +pub mod vesting; +pub mod xastro_token; + +#[cfg(test)] +mod mock_querier; + +#[cfg(test)] +mod testing; + +#[allow(clippy::all)] +mod uints { + use uint::construct_uint; + construct_uint! { + pub struct U256(4); + } +} + +mod decimal_checked_ops { + use cosmwasm_std::{Decimal, Fraction, OverflowError, Uint128, Uint256}; + use std::convert::TryInto; + pub trait DecimalCheckedOps { + fn checked_add(self, other: Decimal) -> Result; + fn checked_mul_uint128(self, other: Uint128) -> Result; + } + + impl DecimalCheckedOps for Decimal { + fn checked_add(self, other: Decimal) -> Result { + self.numerator() + .checked_add(other.numerator()) + .map(|_| self + other) + } + fn checked_mul_uint128(self, other: Uint128) -> Result { + if self.is_zero() || other.is_zero() { + return Ok(Uint128::zero()); + } + let multiply_ratio = + other.full_mul(self.numerator()) / Uint256::from(self.denominator()); + if multiply_ratio > Uint256::from(Uint128::MAX) { + Err(OverflowError::new( + cosmwasm_std::OverflowOperation::Mul, + self, + other, + )) + } else { + Ok(multiply_ratio.try_into().unwrap()) + } + } + } +} + +use cosmwasm_std::{Decimal, Decimal256, StdError, StdResult, Uint128}; + +/// Converts [`Decimal`] to [`Decimal256`]. +/// TODO: can be safely removed as there is Decimal256::from(v: Decimal) +pub fn decimal2decimal256(dec_value: Decimal) -> StdResult { + Decimal256::from_atomics(dec_value.atomics(), dec_value.decimal_places()).map_err(|_| { + StdError::generic_err(format!( + "Failed to convert Decimal {} to Decimal256", + dec_value + )) + }) +} + +/// Converts [`Decimal256`] to [`Decimal`]. +pub fn to_decimal(value: Decimal256) -> StdResult { + let atomics = Uint128::try_from(value.atomics())?; + Decimal::from_atomics(atomics, value.decimal_places()).map_err(|_| { + StdError::generic_err(format!("Failed to convert Decimal256 {} to Decimal", value)) + }) +} + +pub use decimal_checked_ops::DecimalCheckedOps; +pub use uints::U256; diff --git a/packages/astroport/src/maker.rs b/packages/astroport/src/maker.rs new file mode 100644 index 00000000..f8b17754 --- /dev/null +++ b/packages/astroport/src/maker.rs @@ -0,0 +1,125 @@ +use crate::asset::{Asset, AssetInfo}; +use crate::factory::UpdateAddr; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Decimal, Uint128, Uint64}; + +/// This structure stores general parameters for the contract. +#[cw_serde] +pub struct InstantiateMsg { + /// Address that's allowed to change contract parameters + pub owner: String, + /// The ASTRO token contract address + pub astro_token_contract: String, + /// The factory contract address + pub factory_contract: String, + /// The xASTRO staking contract address + pub staking_contract: String, + /// The governance contract address (fee distributor for vxASTRO) + pub governance_contract: Option, + /// The percentage of fees that go to governance_contract + pub governance_percent: Option, + /// The maximum spread used when swapping fee tokens to ASTRO + pub max_spread: Option, +} + +/// This structure describes the functions that can be executed in this contract. +#[cw_serde] +pub enum ExecuteMsg { + /// Collects and swaps fee tokens to ASTRO + Collect { + /// The assets to swap to ASTRO + assets: Vec, + }, + /// Updates general settings + UpdateConfig { + /// The factory contract address + factory_contract: Option, + /// The xASTRO staking contract address + staking_contract: Option, + /// The governance contract address (fee distributor for vxASTRO) + governance_contract: Option, + /// The percentage of fees that go to governance_contract + governance_percent: Option, + /// The maximum spread used when swapping fee tokens to ASTRO + max_spread: Option, + }, + /// Add bridge tokens used to swap specific fee tokens to ASTRO (effectively declaring a swap route) + UpdateBridges { + add: Option>, + remove: Option>, + }, + /// Swap fee tokens via bridge assets + SwapBridgeAssets { assets: Vec, depth: u64 }, + /// Distribute ASTRO to stakers and to governance + DistributeAstro {}, + /// Creates a request to change the contract's ownership + ProposeNewOwner { + /// The newly proposed owner + owner: String, + /// The validity period of the proposal to change the owner + expires_in: u64, + }, + /// Removes a request to change contract ownership + DropOwnershipProposal {}, + /// Claims contract ownership + ClaimOwnership {}, + /// Enables the distribution of current fees accrued in the contract over "blocks" number of blocks + EnableRewards { blocks: u64 }, +} + +/// This structure describes the query functions available in the contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Returns information about the maker configs that contains in the [`ConfigResponse`] + #[returns(ConfigResponse)] + Config {}, + /// Returns the balance for each asset in the specified input parameters + #[returns(BalancesResponse)] + Balances { assets: Vec }, + #[returns(Vec<(String, String)>)] + Bridges {}, +} + +/// A custom struct that holds contract parameters and is used to retrieve them. +#[cw_serde] +pub struct ConfigResponse { + /// Address that is allowed to update contract parameters + pub owner: Addr, + /// The ASTRO token contract address + pub astro_token_contract: Addr, + /// The factory contract address + pub factory_contract: Addr, + /// The xASTRO staking contract address + pub staking_contract: Addr, + /// The governance contract address (fee distributor for vxASTRO stakers) + pub governance_contract: Option, + /// The percentage of fees that go to governance_contract + pub governance_percent: Uint64, + /// The maximum spread used when swapping fee tokens to ASTRO + pub max_spread: Decimal, + /// The remainder ASTRO tokens (accrued before the Maker is upgraded) to be distributed to xASTRO stakers + pub remainder_reward: Uint128, + /// The amount of ASTRO tokens accrued before upgrading the Maker implementation and enabling reward distribution + pub pre_upgrade_astro_amount: Uint128, +} + +/// A custom struct used to return multiple asset balances. +#[cw_serde] +pub struct BalancesResponse { + pub balances: Vec, +} + +/// This structure describes a migration message. +/// We currently take no arguments for migrations. +#[cw_serde] +pub struct MigrateMsg {} + +/// This struct holds parameters to help with swapping a specific amount of a fee token to ASTRO. +#[cw_serde] +pub struct AssetWithLimit { + /// Information about the fee token to swap + pub info: AssetInfo, + /// The amount of tokens to swap + pub limit: Option, +} diff --git a/packages/astroport/src/mock_querier.rs b/packages/astroport/src/mock_querier.rs new file mode 100644 index 00000000..2db60349 --- /dev/null +++ b/packages/astroport/src/mock_querier.rs @@ -0,0 +1,234 @@ +use cosmwasm_std::testing::{MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR}; +use cosmwasm_std::{ + from_binary, from_slice, to_binary, Coin, Empty, OwnedDeps, Querier, QuerierResult, + QueryRequest, SystemError, SystemResult, Uint128, WasmQuery, +}; + +use std::collections::HashMap; + +use crate::asset::PairInfo; +use crate::factory::QueryMsg as FactoryQueryMsg; +use cw20::{BalanceResponse, Cw20QueryMsg, TokenInfoResponse}; + +/// mock_dependencies is a drop-in replacement for cosmwasm_std::testing::mock_dependencies +/// This uses the Astroport CustomQuerier. +pub fn mock_dependencies( + contract_balance: &[Coin], +) -> OwnedDeps { + let custom_querier: WasmMockQuerier = + WasmMockQuerier::new(MockQuerier::new(&[(MOCK_CONTRACT_ADDR, contract_balance)])); + + OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: custom_querier, + custom_query_type: Default::default(), + } +} +enum QueryHandler { + Default, + Cw20, +} + +pub struct WasmMockQuerier { + query_handler: DefaultQueryHandler, + cw20_query_handler: CW20QueryHandler, + handler: QueryHandler, +} + +#[derive(Clone, Default)] +pub struct TokenQuerier { + /// This lets us iterate over all pairs that match the first string + balances: HashMap>, +} + +impl TokenQuerier { + pub fn new(balances: &[(&String, &[(&String, &Uint128)])]) -> Self { + TokenQuerier { + balances: balances_to_map(balances), + } + } +} + +pub(crate) fn balances_to_map( + balances: &[(&String, &[(&String, &Uint128)])], +) -> HashMap> { + let mut balances_map: HashMap> = HashMap::new(); + for (contract_addr, balances) in balances.iter() { + let mut contract_balances_map: HashMap = HashMap::new(); + for (addr, balance) in balances.iter() { + contract_balances_map.insert(addr.to_string(), **balance); + } + + balances_map.insert(String::from(*contract_addr), contract_balances_map); + } + balances_map +} + +#[derive(Clone, Default)] +pub struct AstroportFactoryQuerier { + pairs: HashMap, +} + +impl AstroportFactoryQuerier { + pub fn new(pairs: &[(&String, &PairInfo)]) -> Self { + AstroportFactoryQuerier { + pairs: pairs_to_map(pairs), + } + } +} + +pub(crate) fn pairs_to_map(pairs: &[(&String, &PairInfo)]) -> HashMap { + let mut pairs_map: HashMap = HashMap::new(); + for (key, pair) in pairs.iter() { + pairs_map.insert(key.to_string(), (*pair).clone()); + } + pairs_map +} + +impl Querier for WasmMockQuerier { + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { + // MockQuerier doesn't support Custom, so we ignore it completely here + let request: QueryRequest = match from_slice(bin_request) { + Ok(v) => v, + Err(e) => { + return SystemResult::Err(SystemError::InvalidRequest { + error: format!("Parsing query request: {}", e), + request: bin_request.into(), + }); + } + }; + self.handle_query(&request) + } +} + +impl WasmMockQuerier { + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { + match self.handler { + QueryHandler::Default => self.query_handler.execute(request), + QueryHandler::Cw20 => self.cw20_query_handler.execute(request), + } + } +} + +struct CW20QueryHandler { + token_querier: TokenQuerier, +} + +impl CW20QueryHandler { + pub fn execute(&self, request: &QueryRequest) -> QuerierResult { + match &request { + QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => { + match from_binary(msg).unwrap() { + Cw20QueryMsg::TokenInfo {} => { + let balances: &HashMap = + match self.token_querier.balances.get(contract_addr) { + Some(balances) => balances, + None => { + return SystemResult::Err(SystemError::Unknown {}); + } + }; + + let mut total_supply = Uint128::zero(); + + for balance in balances { + total_supply += *balance.1; + } + + SystemResult::Ok( + to_binary(&TokenInfoResponse { + name: "mAPPL".to_string(), + symbol: "mAPPL".to_string(), + decimals: 6, + total_supply, + }) + .into(), + ) + } + Cw20QueryMsg::Balance { address } => { + let balances: &HashMap = + match self.token_querier.balances.get(contract_addr) { + Some(balances) => balances, + None => { + return SystemResult::Err(SystemError::Unknown {}); + } + }; + + let balance = match balances.get(&address) { + Some(v) => v, + None => { + return SystemResult::Err(SystemError::Unknown {}); + } + }; + + SystemResult::Ok(to_binary(&BalanceResponse { balance: *balance }).into()) + } + _ => panic!("DO NOT ENTER HERE"), + } + } + _ => panic!("DO NOT ENTER HERE"), + } + } +} + +struct DefaultQueryHandler { + base: MockQuerier, + astroport_factory_querier: AstroportFactoryQuerier, +} + +impl DefaultQueryHandler { + pub fn execute(&self, request: &QueryRequest) -> QuerierResult { + match &request { + QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: _, + msg, + }) => match from_binary(msg).unwrap() { + FactoryQueryMsg::Pair { asset_infos } => { + let key = asset_infos[0].to_string() + asset_infos[1].to_string().as_str(); + match self.astroport_factory_querier.pairs.get(&key) { + Some(v) => SystemResult::Ok(to_binary(&v).into()), + None => SystemResult::Err(SystemError::InvalidRequest { + error: "No pair info exists".to_string(), + request: msg.as_slice().into(), + }), + } + } + _ => panic!("DO NOT ENTER HERE"), + }, + _ => self.base.handle_query(request), + } + } +} + +impl WasmMockQuerier { + pub fn new(base: MockQuerier) -> Self { + WasmMockQuerier { + query_handler: DefaultQueryHandler { + base, + astroport_factory_querier: AstroportFactoryQuerier::default(), + }, + cw20_query_handler: CW20QueryHandler { + token_querier: TokenQuerier::default(), + }, + handler: QueryHandler::Default, + } + } + + // Configure the mint whitelist mock querier + pub fn with_token_balances(&mut self, balances: &[(&String, &[(&String, &Uint128)])]) { + self.cw20_query_handler.token_querier = TokenQuerier::new(balances); + } + + // Configure the Astroport pair + pub fn with_astroport_pairs(&mut self, pairs: &[(&String, &PairInfo)]) { + self.query_handler.astroport_factory_querier = AstroportFactoryQuerier::new(pairs); + } + + pub fn with_default_query_handler(&mut self) { + self.handler = QueryHandler::Default; + } + + pub fn with_cw20_query_handler(&mut self) { + self.handler = QueryHandler::Cw20; + } +} diff --git a/packages/astroport/src/oracle.rs b/packages/astroport/src/oracle.rs new file mode 100644 index 00000000..5f85e4c4 --- /dev/null +++ b/packages/astroport/src/oracle.rs @@ -0,0 +1,50 @@ +use crate::asset::AssetInfo; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Decimal256, Uint128, Uint256, Uint64}; + +/// This structure stores general parameters for the contract. +/// Modified by us +#[cw_serde] +pub struct InstantiateMsg { + /// The factory contract address + pub factory_contract: String, + /// The assets that have a pool for which this contract provides price feeds + pub asset_infos: Vec, + /// Minimal interval between Update{}'s + pub period: u64, +} + +/// This structure describes the execute functions available in the contract. +#[cw_serde] +pub enum ExecuteMsg { + /// Update/accumulate prices + Update {}, + /// Update period + UpdatePeriod { new_period: u64 }, +} + +/// This structure describes the query messages available in the contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Calculates a new TWAP with updated precision + #[returns(Vec<(AssetInfo, Uint256)>)] + Consult { + /// The asset for which to compute a new TWAP value + token: AssetInfo, + /// The amount of tokens for which to compute the token price + amount: Uint128, + }, + #[returns(Vec<(AssetInfo, Decimal256)>)] + TWAPAtHeight { + /// The asset for which to compute a new TWAP value + token: AssetInfo, + /// The amount of tokens for which to compute the token price + height: Uint64, + }, +} + +/// This structure describes a migration message. +/// We currently take no arguments for migrations. +#[cw_serde] +pub struct MigrateMsg {} diff --git a/packages/astroport/src/pair.rs b/packages/astroport/src/pair.rs new file mode 100644 index 00000000..3900ecce --- /dev/null +++ b/packages/astroport/src/pair.rs @@ -0,0 +1,276 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; + +use crate::asset::{Asset, AssetInfo, PairInfo}; + +use cosmwasm_std::{from_slice, Addr, Binary, Decimal, QuerierWrapper, StdResult, Uint128}; +use cw20::Cw20ReceiveMsg; + +/// The default swap slippage +pub const DEFAULT_SLIPPAGE: &str = "0.005"; +/// The maximum allowed swap slippage +pub const MAX_ALLOWED_SLIPPAGE: &str = "0.5"; + +/// Decimal precision for TWAP results +pub const TWAP_PRECISION: u8 = 6; + +/// This structure describes the parameters used for creating a contract. +#[cw_serde] +pub struct InstantiateMsg { + /// Information about assets in the pool + pub asset_infos: Vec, + /// The token contract code ID used for the tokens in the pool + pub token_code_id: u64, + /// The factory contract address + pub factory_addr: String, + /// Optional binary serialised parameters for custom pool types + pub init_params: Option, +} + +/// This structure describes the execute messages available in the contract. +#[cw_serde] +pub enum ExecuteMsg { + /// Receives a message of type [`Cw20ReceiveMsg`] + Receive(Cw20ReceiveMsg), + /// ProvideLiquidity allows someone to provide liquidity in the pool + ProvideLiquidity { + /// The assets available in the pool + assets: Vec, + /// The slippage tolerance that allows liquidity provision only if the price in the pool doesn't move too much + slippage_tolerance: Option, + /// Determines whether the LP tokens minted for the user is auto_staked in the Generator contract + auto_stake: Option, + /// The receiver of LP tokens + receiver: Option, + }, + /// Swap performs a swap in the pool + Swap { + offer_asset: Asset, + ask_asset_info: Option, + belief_price: Option, + max_spread: Option, + to: Option, + }, + /// Update the pair configuration + UpdateConfig { params: Binary }, + /// ProposeNewOwner creates a proposal to change contract ownership. + /// The validity period for the proposal is set in the `expires_in` variable. + ProposeNewOwner { + /// Newly proposed contract owner + owner: String, + /// The date after which this proposal expires + expires_in: u64, + }, + /// DropOwnershipProposal removes the existing offer to change contract ownership. + DropOwnershipProposal {}, + /// Used to claim contract ownership. + ClaimOwnership {}, +} + +/// This structure describes a CW20 hook message. +#[cw_serde] +pub enum Cw20HookMsg { + /// Swap a given amount of asset + Swap { + ask_asset_info: Option, + belief_price: Option, + max_spread: Option, + to: Option, + }, + /// Withdraw liquidity from the pool + WithdrawLiquidity { + #[serde(default)] + assets: Vec, + }, +} + +/// This structure describes the query messages available in the contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Returns information about a pair in an object of type [`super::asset::PairInfo`]. + #[returns(PairInfo)] + Pair {}, + /// Returns information about a pool in an object of type [`PoolResponse`]. + #[returns(PoolResponse)] + Pool {}, + /// Returns contract configuration settings in a custom [`ConfigResponse`] structure. + #[returns(ConfigResponse)] + Config {}, + /// Returns information about the share of the pool in a vector that contains objects of type [`Asset`]. + #[returns(Vec)] + Share { amount: Uint128 }, + /// Returns information about a swap simulation in a [`SimulationResponse`] object. + #[returns(SimulationResponse)] + Simulation { + offer_asset: Asset, + ask_asset_info: Option, + }, + /// Returns information about cumulative prices in a [`ReverseSimulationResponse`] object. + #[returns(ReverseSimulationResponse)] + ReverseSimulation { + offer_asset_info: Option, + ask_asset: Asset, + }, + /// Returns information about the cumulative prices in a [`CumulativePricesResponse`] object + #[returns(CumulativePricesResponse)] + CumulativePrices {}, + /// Returns current D invariant in as a [`u128`] value + #[returns(Uint128)] + QueryComputeD {}, +} + +/// This struct is used to return a query result with the total amount of LP tokens and assets in a specific pool. +#[cw_serde] +pub struct PoolResponse { + /// The assets in the pool together with asset amounts + pub assets: Vec, + /// The total amount of LP tokens currently issued + pub total_share: Uint128, +} + +/// This struct is used to return a query result with the general contract configuration. +#[cw_serde] +pub struct ConfigResponse { + /// Last timestamp when the cumulative prices in the pool were updated + pub block_time_last: u64, + /// The pool's parameters + pub params: Option, + /// The contract owner + pub owner: Option, +} + +/// This structure holds the parameters that are returned from a swap simulation response +#[cw_serde] +pub struct SimulationResponse { + /// The amount of ask assets returned by the swap + pub return_amount: Uint128, + /// The spread used in the swap operation + pub spread_amount: Uint128, + /// The amount of fees charged by the transaction + pub commission_amount: Uint128, +} + +/// This structure holds the parameters that are returned from a reverse swap simulation response. +#[cw_serde] +pub struct ReverseSimulationResponse { + /// The amount of offer assets returned by the reverse swap + pub offer_amount: Uint128, + /// The spread used in the swap operation + pub spread_amount: Uint128, + /// The amount of fees charged by the transaction + pub commission_amount: Uint128, +} + +/// This structure is used to return a cumulative prices query response. +#[cw_serde] +pub struct CumulativePricesResponse { + /// The assets in the pool to query + pub assets: Vec, + /// The total amount of LP tokens currently issued + pub total_share: Uint128, + /// The vector contains cumulative prices for each pair of assets in the pool + pub cumulative_prices: Vec<(AssetInfo, AssetInfo, Uint128)>, +} + +/// This structure describes a migration message. +/// We currently take no arguments for migrations. +#[cw_serde] +pub struct MigrateMsg {} + +/// This structure holds stableswap pool parameters. +#[cw_serde] +pub struct StablePoolParams { + /// The current stableswap pool amplification + pub amp: u64, + /// The contract owner + pub owner: Option, +} + +/// This structure stores a stableswap pool's configuration. +#[cw_serde] +pub struct StablePoolConfig { + /// The stableswap pool amplification + pub amp: Decimal, +} + +/// This enum stores the options available to start and stop changing a stableswap pool's amplification. +#[cw_serde] +pub enum StablePoolUpdateParams { + StartChangingAmp { next_amp: u64, next_amp_time: u64 }, + StopChangingAmp {}, +} + +/// This function makes raw query to the factory contract and +/// checks whether the pair needs to update an owner or not. +pub fn migration_check( + querier: QuerierWrapper, + factory: &Addr, + pair_addr: &Addr, +) -> StdResult { + if let Some(res) = querier.query_wasm_raw(factory, b"pairs_to_migrate".as_slice())? { + let res: Vec = from_slice(&res)?; + Ok(res.contains(pair_addr)) + } else { + Ok(false) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::asset::native_asset_info; + use cosmwasm_std::{from_binary, to_binary}; + + #[cw_serde] + pub struct LegacyInstantiateMsg { + pub asset_infos: [AssetInfo; 2], + pub token_code_id: u64, + pub factory_addr: String, + pub init_params: Option, + } + + #[cw_serde] + pub struct LegacyConfigResponse { + pub block_time_last: u64, + pub params: Option, + } + + #[test] + fn test_init_msg_compatability() { + let inst_msg = LegacyInstantiateMsg { + asset_infos: [ + native_asset_info("uusd".to_string()), + native_asset_info("uluna".to_string()), + ], + token_code_id: 0, + factory_addr: "factory".to_string(), + init_params: None, + }; + + let ser_msg = to_binary(&inst_msg).unwrap(); + // This .unwrap() is enough to make sure that [AssetInfo; 2] and Vec are compatible. + let _: InstantiateMsg = from_binary(&ser_msg).unwrap(); + } + + #[test] + fn test_config_response_compatability() { + let ser_msg = to_binary(&LegacyConfigResponse { + block_time_last: 12, + params: Some( + to_binary(&StablePoolConfig { + amp: Decimal::one(), + }) + .unwrap(), + ), + }) + .unwrap(); + + let _: ConfigResponse = from_binary(&ser_msg).unwrap(); + } + + #[test] + fn check_empty_vec_deserialization() { + let variant: Cw20HookMsg = from_slice(br#"{"withdraw_liquidity": {} }"#).unwrap(); + assert_eq!(variant, Cw20HookMsg::WithdrawLiquidity { assets: vec![] }); + } +} diff --git a/packages/astroport/src/pair_bonded.rs b/packages/astroport/src/pair_bonded.rs new file mode 100644 index 00000000..c2fc7c05 --- /dev/null +++ b/packages/astroport/src/pair_bonded.rs @@ -0,0 +1,88 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; + +use crate::asset::{Asset, AssetInfo, PairInfo}; +use crate::pair::{ + ConfigResponse, CumulativePricesResponse, PoolResponse, ReverseSimulationResponse, + SimulationResponse, +}; + +use cosmwasm_std::{Addr, Binary, Decimal, Uint128}; +use cw20::Cw20ReceiveMsg; + +/// The default swap slippage +pub const DEFAULT_SLIPPAGE: &str = "0.005"; +/// The maximum allowed swap slippage +pub const MAX_ALLOWED_SLIPPAGE: &str = "0.5"; + +/// This structure stores the main config parameters for a constant product pair contract. +#[cw_serde] +pub struct Config { + /// General pair information (e.g pair type) + pub pair_info: PairInfo, + /// The factory contract address + pub factory_addr: Addr, +} + +/// This structure describes the execute messages available in the contract. +#[cw_serde] +pub enum ExecuteMsg { + /// Receives a message of type [`Cw20ReceiveMsg`] + Receive(Cw20ReceiveMsg), + /// ProvideLiquidity allows someone to provide liquidity in the pool + ProvideLiquidity { + /// The assets available in the pool + assets: [Asset; 2], + /// The slippage tolerance that allows liquidity provision only if the price in the pool doesn't move too much + slippage_tolerance: Option, + /// Determines whether the LP tokens minted for the user is auto_staked in the Generator contract + auto_stake: Option, + /// The receiver of LP tokens + receiver: Option, + }, + /// Swap performs a swap in the pool + Swap { + offer_asset: Asset, + belief_price: Option, + max_spread: Option, + to: Option, + }, + /// Update the pair configuration + UpdateConfig { params: Binary }, + /// Callback to process post-swap operation + AssertAndSend { + offer_asset: Asset, + /// Information about an asset stored in a [`AssetInfo`] struct + ask_asset_info: AssetInfo, + /// Receiver who should receive the funds + receiver: Addr, + /// Sender who initiated the transaction + sender: Addr, + }, +} + +/// This structure describes the query messages available in the contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Returns information about a pair in an object of type [`super::asset::PairInfo`]. + #[returns(PairInfo)] + Pair {}, + /// Returns information about a pool in an object of type [`PoolResponse`]. + #[returns(PoolResponse)] + Pool {}, + /// Returns contract configuration settings in a custom [`ConfigResponse`] structure. + #[returns(ConfigResponse)] + Config {}, + /// Returns information about the share of the pool in a vector that contains objects of type [`Asset`]. + #[returns(Vec)] + Share { amount: Uint128 }, + /// Returns information about a swap simulation in a [`SimulationResponse`] object. + #[returns(SimulationResponse)] + Simulation { offer_asset: Asset }, + /// Returns information about cumulative prices in a [`ReverseSimulationResponse`] object. + #[returns(ReverseSimulationResponse)] + ReverseSimulation { ask_asset: Asset }, + /// Returns information about the cumulative prices in a [`CumulativePricesResponse`] object + #[returns(CumulativePricesResponse)] + CumulativePrices {}, +} diff --git a/packages/astroport/src/pair_concentrated.rs b/packages/astroport/src/pair_concentrated.rs new file mode 100644 index 00000000..702f0fe9 --- /dev/null +++ b/packages/astroport/src/pair_concentrated.rs @@ -0,0 +1,103 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Decimal, Decimal256, Uint128}; + +use crate::asset::PairInfo; +use crate::asset::{Asset, AssetInfo}; +use crate::pair::{ + ConfigResponse, CumulativePricesResponse, PoolResponse, ReverseSimulationResponse, + SimulationResponse, +}; + +/// This structure holds concentrated pool parameters. +#[cw_serde] +pub struct ConcentratedPoolParams { + /// Amplification coefficient affects trades close to price_scale + pub amp: Decimal, + /// Affects how gradual the curve changes from constant sum to constant product + /// as price moves away from price scale. Low values mean more gradual. + pub gamma: Decimal, + /// The minimum fee, charged when pool is fully balanced + pub mid_fee: Decimal, + /// The maximum fee, charged when pool is imbalanced + pub out_fee: Decimal, + /// Parameter that defines how gradual the fee changes from fee_mid to fee_out + /// based on distance from price_scale. + pub fee_gamma: Decimal, + /// Minimum profit before initiating a new repeg + pub repeg_profit_threshold: Decimal, + /// Minimum amount to change price_scale when repegging. + pub min_price_scale_delta: Decimal, + /// 1 x\[0] = price_scale * x\[1]. + pub price_scale: Decimal, + /// Half-time used for calculating the price oracle. + pub ma_half_time: u64, +} + +/// This structure holds concentrated pool parameters which can be changed immediately. +#[cw_serde] +pub struct UpdatePoolParams { + pub mid_fee: Option, + pub out_fee: Option, + pub fee_gamma: Option, + pub repeg_profit_threshold: Option, + pub min_price_scale_delta: Option, + pub ma_half_time: Option, +} + +/// Amp and gamma should be changed gradually. This structure holds all necessary parameters. +#[cw_serde] +pub struct PromoteParams { + pub next_amp: Decimal, + pub next_gamma: Decimal, + pub future_time: u64, +} + +/// This enum intended for parameters update. +#[cw_serde] +pub enum ConcentratedPoolUpdateParams { + /// Allows to update fee parameters as well as repeg_profit_threshold, min_price_scale_delta and EMA interval. + Update(UpdatePoolParams), + /// Starts gradual (de/in)crease of Amp or Gamma parameters. Can handle an update of both of them. + Promote(PromoteParams), + /// Stops Amp and Gamma update and stores current values. + StopChangingAmpGamma {}, +} + +/// This structure describes the query messages available in the contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Returns information about a pair + #[returns(PairInfo)] + Pair {}, + /// Returns information about a pool + #[returns(PoolResponse)] + Pool {}, + /// Returns contract configuration + #[returns(ConfigResponse)] + Config {}, + /// Returns information about the share of the pool in a vector that contains objects of type [`Asset`]. + #[returns(Vec)] + Share { amount: Uint128 }, + /// Returns information about a swap simulation + #[returns(SimulationResponse)] + Simulation { + offer_asset: Asset, + ask_asset_info: Option, + }, + /// Returns information about a reverse swap simulation + #[returns(ReverseSimulationResponse)] + ReverseSimulation { + offer_asset_info: Option, + ask_asset: Asset, + }, + /// Returns information about the cumulative prices + #[returns(CumulativePricesResponse)] + CumulativePrices {}, + /// Returns current D invariant + #[returns(Decimal256)] + ComputeD {}, + /// Query LP token virtual price + #[returns(Decimal256)] + LpPrice {}, +} diff --git a/packages/astroport/src/pair_stable_bluna.rs b/packages/astroport/src/pair_stable_bluna.rs new file mode 100644 index 00000000..f954338d --- /dev/null +++ b/packages/astroport/src/pair_stable_bluna.rs @@ -0,0 +1,127 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; + +use crate::asset::{Asset, PairInfo}; +use crate::pair::{ + ConfigResponse, CumulativePricesResponse, PoolResponse, ReverseSimulationResponse, + SimulationResponse, +}; +use cosmwasm_std::{Addr, Binary, Decimal, Uint128}; +use cw20::Cw20ReceiveMsg; + +/// This structure describes the execute messages available in the contract. +#[cw_serde] +pub enum ExecuteMsg { + /// Receives a message of type [`Cw20ReceiveMsg`] + Receive(Cw20ReceiveMsg), + /// ProvideLiquidity allows an account to provide liquidity in a pool with bLUNA + ProvideLiquidity { + /// The two assets available in the pool + assets: [Asset; 2], + /// The slippage tolerance that allows liquidity provision only if the price in the pool doesn't move too much + slippage_tolerance: Option, + /// Determines whether the LP tokens minted for the user is auto_staked in the Generator contract + auto_stake: Option, + /// The receiver of LP tokens + receiver: Option, + }, + /// Swap performs a swap in the pool + Swap { + offer_asset: Asset, + belief_price: Option, + max_spread: Option, + to: Option, + }, + /// Update the pair configuration + UpdateConfig { params: Binary }, + /// Claims bLUNA rewards and sends them to the specified receiver + ClaimReward { + /// An address which will receive the bLUNA reward + receiver: Option, + }, + /// Claims the bLUNA reward for a user that deposited their LP tokens in the Generator contract + ClaimRewardByGenerator { + /// The user whose LP tokens are/were staked in the Generator + user: String, + /// The user's LP token amount before the LP token transfer between their wallet and the Generator + user_share: Uint128, + /// The total LP token amount already deposited by all users in the Generator + total_share: Uint128, + }, + /// Callback for distributing bLUNA rewards + HandleReward { + previous_reward_balance: Uint128, + user: Addr, + user_share: Uint128, + total_share: Uint128, + receiver: Option, + }, +} + +/// This structure describes the query messages available in the contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Returns information about a pair in an object of type [`super::asset::PairInfo`]. + #[returns(PairInfo)] + Pair {}, + /// Returns information about a pool in an object of type [`super::pair::PoolResponse`]. + #[returns(PoolResponse)] + Pool {}, + /// Returns contract configuration settings in a custom [`super::pair::ConfigResponse`] structure. + #[returns(ConfigResponse)] + Config {}, + /// Returns information about the share of the pool in a vector that contains objects of type [`Asset`]. + #[returns(Vec)] + Share { amount: Uint128 }, + /// Returns information about a swap simulation in a [`super::pair::SimulationResponse`] object. + #[returns(SimulationResponse)] + Simulation { offer_asset: Asset }, + /// Returns information about a reverse simulation in a [`super::pair::ReverseSimulationResponse`] object. + #[returns(ReverseSimulationResponse)] + ReverseSimulation { ask_asset: Asset }, + /// Returns information about cumulative prices (used for TWAPs) in a [`super::pair::CumulativePricesResponse`] object. + #[returns(CumulativePricesResponse)] + CumulativePrices {}, + /// Returns pending token rewards that can be claimed by a specific user using a [`Asset`] object. + #[returns(Asset)] + PendingReward { user: String }, +} + +/// This struct is used to store bLUNA stableswap specific parameters. +#[cw_serde] +pub struct StablePoolParams { + /// The current pool amplification + pub amp: u64, + /// The bLUNA rewarder contract + pub bluna_rewarder: String, + /// The Astroport Generator contract + pub generator: String, +} + +/// This struct is used to store the stableswap pool configuration. +#[cw_serde] +pub struct StablePoolConfig { + /// The current pool amplification + pub amp: Decimal, + /// The bLUNA rewarder contract + pub bluna_rewarder: Addr, + /// The Astroport Generator contract + pub generator: Addr, +} + +/// This enum stores the options available to update bLUNA stableswap pool parameters. +#[cw_serde] +pub enum StablePoolUpdateParams { + StartChangingAmp { next_amp: u64, next_amp_time: u64 }, + StopChangingAmp {}, + BlunaRewarder { address: String }, +} + +/// This struct contains the parameters used to migrate the bLUNA-LUNA stableswap pool implementation. +#[cw_serde] +pub struct MigrateMsg { + /// The bLUNA rewarder contract + pub bluna_rewarder: Option, + /// The Astroport Generator contract + pub generator: Option, +} diff --git a/packages/astroport/src/querier.rs b/packages/astroport/src/querier.rs new file mode 100644 index 00000000..4f28f6d6 --- /dev/null +++ b/packages/astroport/src/querier.rs @@ -0,0 +1,210 @@ +use crate::asset::{Asset, AssetInfo, PairInfo}; +use crate::factory::{ + ConfigResponse as FactoryConfigResponse, FeeInfoResponse, PairType, PairsResponse, + QueryMsg as FactoryQueryMsg, +}; +use crate::pair::{QueryMsg as PairQueryMsg, ReverseSimulationResponse, SimulationResponse}; + +use cosmwasm_std::{ + Addr, AllBalanceResponse, BankQuery, Coin, Decimal, QuerierWrapper, QueryRequest, StdResult, + Uint128, +}; + +use cw20::{BalanceResponse as Cw20BalanceResponse, Cw20QueryMsg, TokenInfoResponse}; + +// It's defined at https://github.com/terra-money/core/blob/d8e277626e74f9d6417dcd598574686882f0274c/types/assets/assets.go#L15 +pub const NATIVE_TOKEN_PRECISION: u8 = 6; + +/// Returns a native token's balance for a specific account. +/// +/// * **denom** specifies the denomination used to return the balance (e.g uluna). +pub fn query_balance( + querier: &QuerierWrapper, + account_addr: impl Into, + denom: impl Into, +) -> StdResult { + querier + .query_balance(account_addr, denom) + .map(|coin| coin.amount) +} + +/// Returns the total balances for all coins at a specified account address. +/// +/// * **account_addr** address for which we query balances. +pub fn query_all_balances(querier: &QuerierWrapper, account_addr: Addr) -> StdResult> { + let all_balances: AllBalanceResponse = + querier.query(&QueryRequest::Bank(BankQuery::AllBalances { + address: String::from(account_addr), + }))?; + Ok(all_balances.amount) +} + +/// Returns a token balance for an account. +/// +/// * **contract_addr** token contract for which we return a balance. +/// +/// * **account_addr** account address for which we return a balance. +pub fn query_token_balance( + querier: &QuerierWrapper, + contract_addr: impl Into, + account_addr: impl Into, +) -> StdResult { + // load balance from the token contract + let resp: Cw20BalanceResponse = querier + .query_wasm_smart( + contract_addr, + &Cw20QueryMsg::Balance { + address: account_addr.into(), + }, + ) + .unwrap_or_else(|_| Cw20BalanceResponse { + balance: Uint128::zero(), + }); + + Ok(resp.balance) +} + +/// Returns a token's symbol. +/// +/// * **contract_addr** token contract address. +pub fn query_token_symbol( + querier: &QuerierWrapper, + contract_addr: impl Into, +) -> StdResult { + let res: TokenInfoResponse = + querier.query_wasm_smart(contract_addr, &Cw20QueryMsg::TokenInfo {})?; + + Ok(res.symbol) +} + +/// Returns the total supply of a specific token. +/// +/// * **contract_addr** token contract address. +pub fn query_supply( + querier: &QuerierWrapper, + contract_addr: impl Into, +) -> StdResult { + let res: TokenInfoResponse = + querier.query_wasm_smart(contract_addr, &Cw20QueryMsg::TokenInfo {})?; + + Ok(res.total_supply) +} + +/// Returns the number of decimals that a token has. +/// +/// * **asset_info** asset details for a specific token. +pub fn query_token_precision(querier: &QuerierWrapper, asset_info: &AssetInfo) -> StdResult { + let decimals = match asset_info { + AssetInfo::NativeToken { .. } => NATIVE_TOKEN_PRECISION, + AssetInfo::Token { contract_addr } => { + let res: TokenInfoResponse = + querier.query_wasm_smart(contract_addr, &Cw20QueryMsg::TokenInfo {})?; + + res.decimals + } + }; + + Ok(decimals) +} + +/// Returns the configuration for the factory contract. +pub fn query_factory_config( + querier: &QuerierWrapper, + factory_contract: impl Into, +) -> StdResult { + querier.query_wasm_smart(factory_contract, &FactoryQueryMsg::Config {}) +} + +/// This structure holds parameters that describe the fee structure for a pool. +pub struct FeeInfo { + /// The fee address + pub fee_address: Option, + /// The total amount of fees charged per swap + pub total_fee_rate: Decimal, + /// The amount of fees sent to the Maker contract + pub maker_fee_rate: Decimal, +} + +/// Returns the fee information for a specific pair type. +/// +/// * **pair_type** pair type we query information for. +pub fn query_fee_info( + querier: &QuerierWrapper, + factory_contract: impl Into, + pair_type: PairType, +) -> StdResult { + let res: FeeInfoResponse = + querier.query_wasm_smart(factory_contract, &FactoryQueryMsg::FeeInfo { pair_type })?; + + Ok(FeeInfo { + fee_address: res.fee_address, + total_fee_rate: Decimal::from_ratio(res.total_fee_bps, 10000u16), + maker_fee_rate: Decimal::from_ratio(res.maker_fee_bps, 10000u16), + }) +} + +/// Accepts two tokens as input and returns a pair's information. +pub fn query_pair_info( + querier: &QuerierWrapper, + factory_contract: impl Into, + asset_infos: &[AssetInfo], +) -> StdResult { + querier.query_wasm_smart( + factory_contract, + &FactoryQueryMsg::Pair { + asset_infos: asset_infos.to_vec(), + }, + ) +} + +/// Returns a vector that contains items of type [`PairInfo`] which +/// symbolize pairs instantiated in the Astroport factory +pub fn query_pairs_info( + querier: &QuerierWrapper, + factory_contract: impl Into, + start_after: Option>, + limit: Option, +) -> StdResult { + querier.query_wasm_smart( + factory_contract, + &FactoryQueryMsg::Pairs { start_after, limit }, + ) +} + +/// Returns information about a swap simulation using a [`SimulationResponse`] object. +/// +/// * **pair_contract** address of the pair for which we return swap simulation info. +/// +/// * **offer_asset** asset that is being swapped. +pub fn simulate( + querier: &QuerierWrapper, + pair_contract: impl Into, + offer_asset: &Asset, +) -> StdResult { + querier.query_wasm_smart( + pair_contract, + &PairQueryMsg::Simulation { + offer_asset: offer_asset.clone(), + ask_asset_info: None, + }, + ) +} + +/// Returns information about a reverse swap simulation using a [`ReverseSimulationResponse`] object. +/// +/// * **pair_contract** address of the pair for which we return swap simulation info. +/// +/// * **ask_asset** represents the asset that we swap to. +pub fn reverse_simulate( + querier: &QuerierWrapper, + pair_contract: impl Into, + ask_asset: &Asset, +) -> StdResult { + querier.query_wasm_smart( + pair_contract, + &PairQueryMsg::ReverseSimulation { + offer_asset_info: None, + ask_asset: ask_asset.clone(), + }, + ) +} diff --git a/packages/astroport/src/restricted_vector.rs b/packages/astroport/src/restricted_vector.rs new file mode 100644 index 00000000..3bbf2778 --- /dev/null +++ b/packages/astroport/src/restricted_vector.rs @@ -0,0 +1,90 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Decimal, StdError, StdResult, Uint128}; +use std::fmt::Display; + +/// Vec wrapper for internal use. +/// Some business logic relies on an order of this vector, thus it is forbidden to sort it +/// or remove elements. New values can be added using .update() ONLY. +#[cw_serde] +pub struct RestrictedVector(Vec<(K, V)>); + +pub trait Increaseable +where + Self: Sized, +{ + fn increase(self, new: Self) -> StdResult; +} + +impl RestrictedVector +where + K: Clone + PartialEq + Display, + V: Copy + Increaseable, +{ + pub fn new(key: K, value: V) -> Self { + Self(vec![(key, value)]) + } + + pub fn get_last(&self, key: &K) -> StdResult { + self.0 + .last() + .filter(|(k, _)| k == key) + .map(|(_, v)| v) + .cloned() + .ok_or_else(|| StdError::generic_err(format!("Key {} not found", key))) + } + + pub fn update(&mut self, key: &K, value: V) -> StdResult { + let found = self.0.iter_mut().find(|(k, _)| k == key); + let r = match found { + Some((_, v)) => { + *v = v.increase(value)?; + *v + } + None => { + self.0.push((key.clone(), value)); + value + } + }; + + Ok(r) + } + + pub fn load(&self, key: &K) -> Option { + self.0 + .iter() + .find(|(k, _)| k == key) + .map(|(_, value)| *value) + } + + pub fn inner_ref(&self) -> &Vec<(K, V)> { + &self.0 + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl Increaseable for Decimal { + fn increase(self, new: Decimal) -> StdResult { + self.checked_add(new).map_err(Into::into) + } +} + +impl Increaseable for Uint128 { + fn increase(self, new: Uint128) -> StdResult { + self.checked_add(new).map_err(Into::into) + } +} + +impl Default for RestrictedVector { + fn default() -> Self { + Self(vec![]) + } +} + +impl From> for RestrictedVector { + fn from(v: Vec<(K, V)>) -> Self { + Self(v) + } +} diff --git a/packages/astroport/src/router.rs b/packages/astroport/src/router.rs new file mode 100644 index 00000000..b927fd16 --- /dev/null +++ b/packages/astroport/src/router.rs @@ -0,0 +1,126 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; + +use cosmwasm_std::{Decimal, Uint128}; +use cw20::Cw20ReceiveMsg; + +use crate::asset::AssetInfo; + +pub const MAX_SWAP_OPERATIONS: usize = 50; + +/// This structure holds the parameters used for creating a contract. +#[cw_serde] +pub struct InstantiateMsg { + /// The astroport factory contract address + pub astroport_factory: String, +} + +/// This enum describes a swap operation. +#[cw_serde] +pub enum SwapOperation { + /// Native swap + NativeSwap { + /// The name (denomination) of the native asset to swap from + offer_denom: String, + /// The name (denomination) of the native asset to swap to + ask_denom: String, + }, + /// ASTRO swap + AstroSwap { + /// Information about the asset being swapped + offer_asset_info: AssetInfo, + /// Information about the asset we swap to + ask_asset_info: AssetInfo, + }, +} + +impl SwapOperation { + pub fn get_target_asset_info(&self) -> AssetInfo { + match self { + SwapOperation::NativeSwap { ask_denom, .. } => AssetInfo::NativeToken { + denom: ask_denom.clone(), + }, + SwapOperation::AstroSwap { ask_asset_info, .. } => ask_asset_info.clone(), + } + } +} + +/// This structure describes the execute messages available in the contract. +#[cw_serde] +pub enum ExecuteMsg { + /// Receive receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template + Receive(Cw20ReceiveMsg), + /// ExecuteSwapOperations processes multiple swaps while mentioning the minimum amount of tokens to receive for the last swap operation + ExecuteSwapOperations { + operations: Vec, + minimum_receive: Option, + to: Option, + max_spread: Option, + }, + + /// Internal use + /// ExecuteSwapOperation executes a single swap operation + ExecuteSwapOperation { + operation: SwapOperation, + to: Option, + max_spread: Option, + single: bool, + }, + /// Internal use + /// AssertMinimumReceive checks that a receiver will get a minimum amount of tokens from a swap + AssertMinimumReceive { + asset_info: AssetInfo, + prev_balance: Uint128, + minimum_receive: Uint128, + receiver: String, + }, +} + +#[cw_serde] +pub enum Cw20HookMsg { + ExecuteSwapOperations { + /// A vector of swap operations + operations: Vec, + /// The minimum amount of tokens to get from a swap + minimum_receive: Option, + /// + to: Option, + /// Max spread + max_spread: Option, + }, +} + +/// This structure describes the query messages available in the contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Config returns configuration parameters for the contract using a custom [`ConfigResponse`] structure + #[returns(ConfigResponse)] + Config {}, + /// SimulateSwapOperations simulates multi-hop swap operations + #[returns(SimulateSwapOperationsResponse)] + SimulateSwapOperations { + /// The amount of tokens to swap + offer_amount: Uint128, + /// The swap operations to perform, each swap involving a specific pool + operations: Vec, + }, +} + +/// This structure describes a custom struct to return a query response containing the base contract configuration. +#[cw_serde] +pub struct ConfigResponse { + /// The Astroport factory contract address + pub astroport_factory: String, +} + +/// This structure describes a custom struct to return a query response containing the end amount of a swap simulation +#[cw_serde] +pub struct SimulateSwapOperationsResponse { + /// The amount of tokens received in a swap simulation + pub amount: Uint128, +} + +/// This structure describes a migration message. +/// We currently take no arguments for migrations. +#[cw_serde] +pub struct MigrateMsg {} diff --git a/packages/astroport/src/staking.rs b/packages/astroport/src/staking.rs new file mode 100644 index 00000000..7cc9b9a8 --- /dev/null +++ b/packages/astroport/src/staking.rs @@ -0,0 +1,58 @@ +use crate::xastro_token::InstantiateMarketingInfo; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Uint128}; +use cw20::Cw20ReceiveMsg; + +/// This structure describes the parameters used for creating a contract. +#[cw_serde] +pub struct InstantiateMsg { + /// The contract owner address + pub owner: String, + /// CW20 token code identifier + pub token_code_id: u64, + /// The ASTRO token contract address + pub deposit_token_addr: String, + /// the marketing info of type [`InstantiateMarketingInfo`] + pub marketing: Option, +} + +/// This structure describes the execute messages available in the contract. +#[cw_serde] +pub enum ExecuteMsg { + /// Receive receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template. + Receive(Cw20ReceiveMsg), +} + +/// This structure describes the query messages available in the contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Config returns the contract configuration specified in a custom [`ConfigResponse`] structure + #[returns(ConfigResponse)] + Config {}, + #[returns(Uint128)] + TotalShares {}, + #[returns(Uint128)] + TotalDeposit {}, +} + +#[cw_serde] +pub struct ConfigResponse { + /// The ASTRO token address + pub deposit_token_addr: Addr, + /// The xASTRO token address + pub share_token_addr: Addr, +} + +/// This structure describes a migration message. +#[cw_serde] +pub struct MigrateMsg {} + +/// This structure describes a CW20 hook message. +#[cw_serde] +pub enum Cw20HookMsg { + /// Deposits ASTRO in exchange for xASTRO + Enter {}, + /// Burns xASTRO in exchange for ASTRO + Leave {}, +} diff --git a/packages/astroport/src/testing.rs b/packages/astroport/src/testing.rs new file mode 100644 index 00000000..0a197683 --- /dev/null +++ b/packages/astroport/src/testing.rs @@ -0,0 +1,343 @@ +use crate::asset::{format_lp_token_name, Asset, AssetInfo, PairInfo}; +use crate::mock_querier::mock_dependencies; +use crate::querier::{ + query_all_balances, query_balance, query_pair_info, query_supply, query_token_balance, +}; + +use crate::factory::PairType; +use crate::DecimalCheckedOps; +use cosmwasm_std::testing::MOCK_CONTRACT_ADDR; +use cosmwasm_std::{to_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal, Uint128, WasmMsg}; +use cw20::Cw20ExecuteMsg; + +#[test] +fn token_balance_querier() { + let mut deps = mock_dependencies(&[]); + + deps.querier.with_token_balances(&[( + &String::from("liquidity0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &Uint128::new(123u128))], + )]); + + deps.querier.with_cw20_query_handler(); + assert_eq!( + Uint128::new(123u128), + query_token_balance( + &deps.as_ref().querier, + Addr::unchecked("liquidity0000"), + Addr::unchecked(MOCK_CONTRACT_ADDR), + ) + .unwrap() + ); + deps.querier.with_default_query_handler() +} + +#[test] +fn balance_querier() { + let deps = mock_dependencies(&[Coin { + denom: "uusd".to_string(), + amount: Uint128::new(200u128), + }]); + + assert_eq!( + query_balance( + &deps.as_ref().querier, + Addr::unchecked(MOCK_CONTRACT_ADDR), + "uusd".to_string() + ) + .unwrap(), + Uint128::new(200u128) + ); +} + +#[test] +fn all_balances_querier() { + let deps = mock_dependencies(&[ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(200u128), + }, + Coin { + denom: "ukrw".to_string(), + amount: Uint128::new(300u128), + }, + ]); + + assert_eq!( + query_all_balances(&deps.as_ref().querier, Addr::unchecked(MOCK_CONTRACT_ADDR),).unwrap(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(200u128), + }, + Coin { + denom: "ukrw".to_string(), + amount: Uint128::new(300u128), + } + ] + ); +} + +#[test] +fn supply_querier() { + let mut deps = mock_dependencies(&[]); + + deps.querier.with_token_balances(&[( + &String::from("liquidity0000"), + &[ + (&String::from(MOCK_CONTRACT_ADDR), &Uint128::new(123u128)), + (&String::from("addr00000"), &Uint128::new(123u128)), + (&String::from("addr00001"), &Uint128::new(123u128)), + (&String::from("addr00002"), &Uint128::new(123u128)), + ], + )]); + + deps.querier.with_cw20_query_handler(); + + assert_eq!( + query_supply(&deps.as_ref().querier, Addr::unchecked("liquidity0000")).unwrap(), + Uint128::new(492u128) + ) +} + +#[test] +fn test_asset_info() { + let token_info: AssetInfo = AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }; + let native_token_info: AssetInfo = AssetInfo::NativeToken { + denom: "uusd".to_string(), + }; + + assert!(!token_info.equal(&native_token_info)); + + assert!(!token_info.equal(&AssetInfo::Token { + contract_addr: Addr::unchecked("asset0001"), + })); + + assert!(token_info.equal(&AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + })); + + assert!(native_token_info.is_native_token()); + assert!(!token_info.is_native_token()); + + let mut deps = mock_dependencies(&[Coin { + denom: "uusd".to_string(), + amount: Uint128::new(123), + }]); + deps.querier.with_token_balances(&[( + &String::from("asset0000"), + &[ + (&String::from(MOCK_CONTRACT_ADDR), &Uint128::new(123u128)), + (&String::from("addr00000"), &Uint128::new(123u128)), + (&String::from("addr00001"), &Uint128::new(123u128)), + (&String::from("addr00002"), &Uint128::new(123u128)), + ], + )]); + + assert_eq!( + native_token_info + .query_pool(&deps.as_ref().querier, Addr::unchecked(MOCK_CONTRACT_ADDR)) + .unwrap(), + Uint128::new(123u128) + ); + deps.querier.with_cw20_query_handler(); + assert_eq!( + token_info + .query_pool(&deps.as_ref().querier, Addr::unchecked(MOCK_CONTRACT_ADDR)) + .unwrap(), + Uint128::new(123u128) + ); +} + +#[test] +fn test_asset() { + let mut deps = mock_dependencies(&[Coin { + denom: "uusd".to_string(), + amount: Uint128::new(123), + }]); + + deps.querier.with_token_balances(&[( + &String::from("asset0000"), + &[ + (&String::from(MOCK_CONTRACT_ADDR), &Uint128::new(123u128)), + (&String::from("addr00000"), &Uint128::new(123u128)), + (&String::from("addr00001"), &Uint128::new(123u128)), + (&String::from("addr00002"), &Uint128::new(123u128)), + ], + )]); + + let token_asset = Asset { + amount: Uint128::new(123123u128), + info: AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + }; + + let native_token_asset = Asset { + amount: Uint128::new(123123u128), + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + }; + + assert_eq!( + token_asset.compute_tax(&deps.as_ref().querier).unwrap(), + Uint128::zero() + ); + assert_eq!( + native_token_asset + .compute_tax(&deps.as_ref().querier) + .unwrap(), + Uint128::zero() + ); + + assert_eq!( + native_token_asset + .deduct_tax(&deps.as_ref().querier) + .unwrap(), + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(123123u128), + } + ); + + assert_eq!( + token_asset + .into_msg(&deps.as_ref().querier, Addr::unchecked("addr0000")) + .unwrap(), + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: String::from("asset0000"), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: String::from("addr0000"), + amount: Uint128::new(123123u128), + }) + .unwrap(), + funds: vec![], + }) + ); + + assert_eq!( + native_token_asset + .into_msg(&deps.as_ref().querier, Addr::unchecked("addr0000")) + .unwrap(), + CosmosMsg::Bank(BankMsg::Send { + to_address: String::from("addr0000"), + amount: vec![Coin { + denom: "uusd".to_string(), + amount: Uint128::new(123123u128), + }] + }) + ); +} + +#[test] +fn query_astroport_pair_contract() { + let mut deps = mock_dependencies(&[]); + + deps.querier.with_astroport_pairs(&[( + &"asset0000uusd".to_string(), + &PairInfo { + asset_infos: vec![ + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + ], + contract_addr: Addr::unchecked("pair0000"), + liquidity_token: Addr::unchecked("liquidity0000"), + pair_type: PairType::Xyk {}, + }, + )]); + + let pair_info: PairInfo = query_pair_info( + &deps.as_ref().querier, + Addr::unchecked(MOCK_CONTRACT_ADDR), + &[ + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + ], + ) + .unwrap(); + + assert_eq!(pair_info.contract_addr, String::from("pair0000"),); + assert_eq!(pair_info.liquidity_token, String::from("liquidity0000"),); +} + +#[test] +fn test_format_lp_token_name() { + let mut deps = mock_dependencies(&[]); + deps.querier.with_astroport_pairs(&[( + &"asset0000uusd".to_string(), + &PairInfo { + asset_infos: vec![ + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + ], + contract_addr: Addr::unchecked("pair0000"), + liquidity_token: Addr::unchecked("liquidity0000"), + pair_type: PairType::Xyk {}, + }, + )]); + + let pair_info: PairInfo = query_pair_info( + &deps.as_ref().querier, + Addr::unchecked(MOCK_CONTRACT_ADDR), + &[ + AssetInfo::Token { + contract_addr: Addr::unchecked("asset0000"), + }, + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + ], + ) + .unwrap(); + + deps.querier.with_token_balances(&[( + &String::from("asset0000"), + &[(&String::from(MOCK_CONTRACT_ADDR), &Uint128::new(123u128))], + )]); + + deps.querier.with_cw20_query_handler(); + + let lp_name = format_lp_token_name(&pair_info.asset_infos, &deps.as_ref().querier).unwrap(); + assert_eq!(lp_name, "MAPP-UUSD-LP") +} + +#[test] +fn test_decimal_checked_ops() { + for i in 0u32..100u32 { + let dec = Decimal::from_ratio(i, 1u32); + assert_eq!(dec + dec, dec.checked_add(dec).unwrap()); + } + assert!( + Decimal::from_ratio(Uint128::MAX, Uint128::from(10u128.pow(18u32))) + .checked_add(Decimal::one()) + .is_err() + ); + + for i in 0u128..100u128 { + let dec = Decimal::from_ratio(i, 1u128); + assert_eq!( + dec * Uint128::new(i), + dec.checked_mul_uint128(Uint128::from(i)).unwrap() + ); + } + assert!( + Decimal::from_ratio(Uint128::MAX, Uint128::from(10u128.pow(18u32))) + .checked_mul(Decimal::new(Uint128::from(10u128.pow(18u32) + 1u128))) + .is_err() + ); +} diff --git a/packages/astroport/src/token.rs b/packages/astroport/src/token.rs new file mode 100644 index 00000000..9ad56e22 --- /dev/null +++ b/packages/astroport/src/token.rs @@ -0,0 +1,85 @@ +use cosmwasm_schema::cw_serde; + +use cosmwasm_std::{StdError, StdResult, Uint128}; +use cw20::{Cw20Coin, Logo, MinterResponse}; + +/// This structure describes the marketing info settings such as project, description, and token logo. +#[cw_serde] +pub struct InstantiateMarketingInfo { + /// The project name + pub project: Option, + /// The project description + pub description: Option, + /// The address of an admin who is able to update marketing info + pub marketing: Option, + /// The token logo + pub logo: Option, +} + +/// This structure describes the parameters used for creating a token contract. +#[cw_serde] +pub struct InstantiateMsg { + /// Token name + pub name: String, + /// Token symbol + pub symbol: String, + /// The amount of decimals the token has + pub decimals: u8, + /// Initial token balances + pub initial_balances: Vec, + /// Minting controls specified in a [`MinterResponse`] structure + pub mint: Option, + /// the marketing info of type [`InstantiateMarketingInfo`] + pub marketing: Option, +} + +/// This structure describes a migration message. +#[cw_serde] +pub struct MigrateMsg {} + +impl InstantiateMsg { + pub fn get_cap(&self) -> Option { + self.mint.as_ref().and_then(|v| v.cap) + } + + pub fn validate(&self) -> StdResult<()> { + // Check name, symbol, decimals + if !is_valid_name(&self.name) { + return Err(StdError::generic_err( + "Name is not in the expected format (3-50 UTF-8 bytes)", + )); + } + if !is_valid_symbol(&self.symbol) { + return Err(StdError::generic_err( + "Ticker symbol is not in expected format [a-zA-Z\\-]{3,12}", + )); + } + if self.decimals > 18 { + return Err(StdError::generic_err("Decimals must not exceed 18")); + } + Ok(()) + } +} + +/// Checks the validity of the token name +fn is_valid_name(name: &str) -> bool { + let bytes = name.as_bytes(); + if bytes.len() < 3 || bytes.len() > 50 { + return false; + } + true +} + +/// Checks the validity of the token symbol +fn is_valid_symbol(symbol: &str) -> bool { + let bytes = symbol.as_bytes(); + if bytes.len() < 3 || bytes.len() > 12 { + return false; + } + for byte in bytes.iter() { + if (*byte != 45) && (*byte < 65 || *byte > 90) && (*byte < 97 || *byte > 122) { + return false; + } + } + true +} diff --git a/packages/astroport/src/vesting.rs b/packages/astroport/src/vesting.rs new file mode 100644 index 00000000..bea05e5d --- /dev/null +++ b/packages/astroport/src/vesting.rs @@ -0,0 +1,164 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; + +use cosmwasm_std::{Addr, Order, Uint128}; +use cw20::Cw20ReceiveMsg; + +/// This structure describes the parameters used for creating a contract. +#[cw_serde] +pub struct InstantiateMsg { + /// Address allowed to change contract parameters + pub owner: String, + /// The address of the token that's being vested + pub token_addr: String, +} + +/// This structure describes the execute messages available in the contract. +#[cw_serde] +pub enum ExecuteMsg { + /// Claim claims vested tokens and sends them to a recipient + Claim { + /// The address that receives the vested tokens + recipient: Option, + /// The amount of tokens to claim + amount: Option, + }, + /// Receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template + Receive(Cw20ReceiveMsg), + /// Creates a request to change contract ownership + /// ## Executor + /// Only the current owner can execute this + ProposeNewOwner { + /// The newly proposed owner + owner: String, + /// The validity period of the offer to change the owner + expires_in: u64, + }, + /// Removes a request to change contract ownership + /// ## Executor + /// Only the current owner can execute this + DropOwnershipProposal {}, + /// Claims contract ownership + /// ## Executor + /// Only the newly proposed owner can execute this + ClaimOwnership {}, +} + +/// This structure stores vesting information for a specific address that is getting tokens. +#[cw_serde] +pub struct VestingAccount { + /// The address that is getting tokens + pub address: String, + /// The vesting schedules targeted at the `address` + pub schedules: Vec, +} + +/// This structure stores parameters for a batch of vesting schedules. +#[cw_serde] +pub struct VestingInfo { + /// The vesting schedules + pub schedules: Vec, + /// The total amount of ASTRO already claimed + pub released_amount: Uint128, +} + +/// This structure stores parameters for a specific vesting schedule +#[cw_serde] +pub struct VestingSchedule { + /// The start date for the vesting schedule + pub start_point: VestingSchedulePoint, + /// The end point for the vesting schedule + pub end_point: Option, +} + +/// This structure stores the parameters used to create a vesting schedule. +#[cw_serde] +pub struct VestingSchedulePoint { + /// The start time for the vesting schedule + pub time: u64, + /// The amount of tokens being vested + pub amount: Uint128, +} + +/// This structure describes the query messages available in the contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Returns the configuration for the contract using a [`ConfigResponse`] object. + #[returns(ConfigResponse)] + Config {}, + /// Returns information about an address vesting tokens using a [`VestingAccountResponse`] object. + #[returns(VestingAccountResponse)] + VestingAccount { address: String }, + /// Returns a list of addresses that are vesting tokens using a [`VestingAccountsResponse`] object. + #[returns(VestingAccountsResponse)] + VestingAccounts { + start_after: Option, + limit: Option, + order_by: Option, + }, + /// Returns the total unvested amount of tokens for a specific address. + #[returns(Uint128)] + AvailableAmount { address: String }, + /// Timestamp returns the current timestamp + #[returns(u64)] + Timestamp {}, +} + +/// This structure describes a custom struct used to return the contract configuration. +#[cw_serde] +pub struct ConfigResponse { + /// Address allowed to set contract parameters + pub owner: Addr, + /// The address of the token being vested + pub token_addr: Addr, +} + +/// This structure describes a custom struct used to return vesting data about a specific vesting target. +#[cw_serde] +pub struct VestingAccountResponse { + /// The address that's vesting tokens + pub address: Addr, + /// Vesting information + pub info: VestingInfo, +} + +/// This structure describes a custom struct used to return vesting data for multiple vesting targets. +#[cw_serde] +pub struct VestingAccountsResponse { + /// A list of accounts that are vesting tokens + pub vesting_accounts: Vec, +} + +/// This enum describes the types of sorting that can be applied to some piece of data +#[cw_serde] +pub enum OrderBy { + Asc, + Desc, +} + +// We suppress this clippy warning because Order in cosmwasm doesn't implement Debug and +// PartialEq for usage in QueryMsg. We need to use our own OrderBy and convert the result to cosmwasm's Order +#[allow(clippy::from_over_into)] +impl Into for OrderBy { + fn into(self) -> Order { + if self == OrderBy::Asc { + Order::Ascending + } else { + Order::Descending + } + } +} + +/// This structure describes a migration message. +/// We currently take no arguments for migrations. +#[cw_serde] +pub struct MigrateMsg {} + +/// This structure describes a CW20 hook message. +#[cw_serde] +pub enum Cw20HookMsg { + /// RegisterVestingAccounts registers vesting targets/accounts + RegisterVestingAccounts { + vesting_accounts: Vec, + }, +} diff --git a/packages/astroport/src/xastro_token.rs b/packages/astroport/src/xastro_token.rs new file mode 100644 index 00000000..b7251559 --- /dev/null +++ b/packages/astroport/src/xastro_token.rs @@ -0,0 +1,132 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; + +use cosmwasm_std::{StdError, StdResult, Uint128}; +use cw20::{ + AllAccountsResponse, AllAllowancesResponse, AllowanceResponse, BalanceResponse, Cw20Coin, + DownloadLogoResponse, Logo, MarketingInfoResponse, MinterResponse, TokenInfoResponse, +}; + +/// This structure describes the marketing info settings such as project, description, and token logo. +#[cw_serde] +pub struct InstantiateMarketingInfo { + /// The project name + pub project: Option, + /// The project description + pub description: Option, + /// The address of an admin who is able to update marketing info + pub marketing: Option, + /// The token logo + pub logo: Option, +} + +/// This structure describes the parameters used for creating a xASTRO token contract. +#[cw_serde] +pub struct InstantiateMsg { + /// Token name + pub name: String, + /// Token symbol + pub symbol: String, + /// The number of decimals the token has + pub decimals: u8, + /// Initial token balances + pub initial_balances: Vec, + /// Token minting permissions + pub mint: Option, + /// the marketing info of type [`InstantiateMarketingInfo`] + pub marketing: Option, +} + +/// This enum describes the query messages available in the contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Balance returns the current balance of a given address, 0 if unset. + #[returns(BalanceResponse)] + Balance { address: String }, + /// BalanceAt returns balance of the given address at the given block, 0 if unset. + #[returns(BalanceResponse)] + BalanceAt { address: String, block: u64 }, + /// TotalSupplyAt returns the total token supply at the given block. + #[returns(Uint128)] + TotalSupplyAt { block: u64 }, + /// TokenInfo returns the contract's metadata - name, decimals, supply, etc. + #[returns(TokenInfoResponse)] + TokenInfo {}, + /// Returns who can mint xASTRO and the hard cap on maximum tokens after minting. + #[returns(Option)] + Minter {}, + /// Allowance returns an amount of tokens the spender can spend from the owner account, 0 if unset. + #[returns(AllowanceResponse)] + Allowance { owner: String, spender: String }, + /// AllAllowances returns all the allowances this token holder has approved. Supports pagination. + #[returns(AllAllowancesResponse)] + AllAllowances { + owner: String, + start_after: Option, + limit: Option, + }, + /// AllAccounts returns all the accounts that have xASTRO balances. Supports pagination. + #[returns(AllAccountsResponse)] + AllAccounts { + start_after: Option, + limit: Option, + }, + /// Returns marketing related contract metadata: + /// - description, logo, project url, etc. + #[returns(MarketingInfoResponse)] + MarketingInfo {}, + /// Downloads embeded logo data (if stored on chain). Errors if no logo data was stored for this contract. + #[returns(DownloadLogoResponse)] + DownloadLogo {}, +} + +/// This structure describes a migration message. +#[cw_serde] +pub struct MigrateMsg {} + +impl InstantiateMsg { + pub fn get_cap(&self) -> Option { + self.mint.as_ref().and_then(|v| v.cap) + } + + pub fn validate(&self) -> StdResult<()> { + // Check name, symbol, decimals + if !is_valid_name(&self.name) { + return Err(StdError::generic_err( + "Name is not in the expected format (3-50 UTF-8 bytes)", + )); + } + if !is_valid_symbol(&self.symbol) { + return Err(StdError::generic_err( + "Ticker symbol is not in expected format [a-zA-Z\\-]{3,12}", + )); + } + if self.decimals > 18 { + return Err(StdError::generic_err("Decimals must not exceed 18")); + } + Ok(()) + } +} + +/// Checks the validity of a token's name. +fn is_valid_name(name: &str) -> bool { + let bytes = name.as_bytes(); + if bytes.len() < 3 || bytes.len() > 50 { + return false; + } + true +} + +/// Checks the validity of a token's symbol. +fn is_valid_symbol(symbol: &str) -> bool { + let bytes = symbol.as_bytes(); + if bytes.len() < 3 || bytes.len() > 12 { + return false; + } + for byte in bytes.iter() { + if (*byte != 45) && (*byte < 65 || *byte > 90) && (*byte < 97 || *byte > 122) { + return false; + } + } + true +}