diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 42af0ab81..2270048e3 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -21,7 +21,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: nightly-2023-02-02 + toolchain: nightly-2023-12-07 target: wasm32-unknown-unknown override: true diff --git a/Cargo.lock b/Cargo.lock index 6128b4f1c..f72280a3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,9 +39,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "59d2a3357dde987206219e78ecfbbb6e8dad06cbb65292758d3270e6254f7355" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "assert_matches" @@ -68,18 +74,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] name = "async-trait" -version = "0.1.74" +version = "0.1.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +checksum = "fdf6721fb0140e4f897002dd086c06f6c27775df19cfe1fccb21181a48fd2c98" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -203,7 +209,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.39", + "syn 2.0.42", "which", ] @@ -237,6 +243,18 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -284,6 +302,30 @@ dependencies = [ "serde_yaml", ] +[[package]] +name = "borsh" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d4d6dafc1a3bb54687538972158f07b2c948bc57d5890df22c0739098b3028" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4918709cc4dd777ad2b6303ed03cb37f3ca0ccede8c1b0d28ac6db8f4710e0" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.42", + "syn_derive", +] + [[package]] name = "bs58" version = "0.4.0" @@ -299,6 +341,28 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "bytecheck" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "byteorder" version = "1.5.0" @@ -338,6 +402,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "chrono" version = "0.4.31" @@ -379,9 +449,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "core-foundation" @@ -652,6 +722,36 @@ dependencies = [ "zeroize", ] +[[package]] +name = "cw-abc" +version = "2.4.1" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-address-like", + "cw-curves", + "cw-multi-test", + "cw-ownable", + "cw-paginate-storage 2.4.1", + "cw-storage-plus 1.2.0", + "cw-tokenfactory-issuer", + "cw-utils 1.0.3", + "cw2 1.1.2", + "dao-interface", + "dao-proposal-single", + "dao-testing", + "dao-voting 2.4.1", + "dao-voting-token-staked", + "getrandom", + "osmosis-std", + "osmosis-test-tube", + "serde", + "serde_json", + "speculoos", + "thiserror", +] + [[package]] name = "cw-address-like" version = "1.0.4" @@ -787,6 +887,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "cw-curves" +version = "2.4.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "integer-cbrt", + "integer-sqrt", + "rust_decimal", +] + [[package]] name = "cw-denom" version = "2.4.1" @@ -1572,6 +1683,33 @@ dependencies = [ "thiserror", ] +[[package]] +name = "dao-abc-factory" +version = "2.4.1" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-abc", + "cw-multi-test", + "cw-ownable", + "cw-storage-plus 1.2.0", + "cw-tokenfactory-issuer", + "cw-utils 1.0.3", + "cw2 1.1.2", + "dao-dao-macros", + "dao-interface", + "dao-proposal-hook-counter", + "dao-proposal-single", + "dao-testing", + "dao-voting 2.4.1", + "dao-voting-token-staked", + "osmosis-std", + "osmosis-test-tube", + "serde", + "thiserror", +] + [[package]] name = "dao-cw721-extensions" version = "2.4.1" @@ -1957,6 +2095,7 @@ version = "2.4.1" dependencies = [ "cosmwasm-schema", "cosmwasm-std", + "cw-abc", "cw-core", "cw-hooks", "cw-multi-test", @@ -1972,6 +2111,7 @@ dependencies = [ "cw4-group 1.1.2", "cw721-base 0.18.0", "cw721-roles", + "dao-abc-factory", "dao-dao-core", "dao-interface", "dao-pre-propose-multiple", @@ -2369,9 +2509,9 @@ dependencies = [ [[package]] name = "eyre" -version = "0.6.9" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80f656be11ddf91bd709454d15d5bd896fbaf4cc3314e69349e4d1569f5b46cd" +checksum = "b6267a1fa6f59179ea4afc8e50fd8612a3cc60bc858f786ff877a4a8cb042799" dependencies = [ "indenter", "once_cell", @@ -2428,6 +2568,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.29" @@ -2484,7 +2630,7 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -2656,11 +2802,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.5" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -2676,9 +2822,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http", @@ -2705,9 +2851,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.27" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", @@ -2720,7 +2866,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2", "tokio", "tower-service", "tracing", @@ -2825,6 +2971,24 @@ dependencies = [ "hashbrown 0.14.3", ] +[[package]] +name = "integer-cbrt" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "151bce4481ba7da831c7d12c32353cc79c73bf79732e343b92786e4cbbb2948c" +dependencies = [ + "num-traits", +] + +[[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 = "integration-tests" version = "0.1.0" @@ -2890,9 +3054,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" @@ -2964,9 +3128,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.150" +version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "libloading" @@ -3031,9 +3195,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "wasi", @@ -3050,6 +3214,40 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + [[package]] name = "num-derive" version = "0.3.3" @@ -3061,6 +3259,39 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.17" @@ -3100,9 +3331,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" @@ -3264,7 +3495,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -3295,7 +3526,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -3343,14 +3574,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 2.0.39", + "syn 2.0.42", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a" +dependencies = [ + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", ] [[package]] name = "proc-macro2" -version = "1.0.70" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8" dependencies = [ "unicode-ident", ] @@ -3410,7 +3674,7 @@ dependencies = [ "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -3431,6 +3695,26 @@ dependencies = [ "prost 0.12.3", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quote" version = "1.0.33" @@ -3440,6 +3724,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -3505,6 +3795,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "rend" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2571463863a6bd50c32f94402933f03457a3fbaf697a707c5be741e459f08fd" +dependencies = [ + "bytecheck", +] + [[package]] name = "rfc6979" version = "0.3.1" @@ -3561,6 +3860,35 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "rkyv" +version = "0.7.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527a97cdfef66f65998b5f3b637c26f5a5ec09cc52a3f9932313ac645f4190f5" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid 1.6.1", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5c462a1328c8e67e4d6dbad1eb0355dd43e8ab432c6e227a43657f16ade5033" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "ron" version = "0.7.1" @@ -3582,6 +3910,22 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rust_decimal" +version = "1.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06676aec5ccb8fc1da723cc8c0f9a46549f21ebb8753d3915c6c41db1e7f1dc4" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -3596,9 +3940,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.26" +version = "0.38.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9470c4bf8246c8daf25f9598dca807fb6510347b1e1cfa55749113850c79d88a" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" dependencies = [ "bitflags 2.4.1", "errno", @@ -3640,9 +3984,9 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "same-file" @@ -3696,6 +4040,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "sec1" version = "0.3.0" @@ -3797,7 +4147,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -3830,14 +4180,14 @@ checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] name = "serde_yaml" -version = "0.9.27" +version = "0.9.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cc7a1570e38322cfe4154732e5110f887ea57e22b76f4bfd32b5bdd3368666c" +checksum = "a15e0ef66bf939a7c890a0bf6d5a733c70202225f9888a89ed5c62298b019129" dependencies = [ "indexmap 2.1.0", "itoa", @@ -3917,6 +4267,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simdutf8" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" + [[package]] name = "slab" version = "0.4.9" @@ -3928,22 +4284,21 @@ dependencies = [ [[package]] name = "socket2" -version = "0.4.10" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "winapi", + "windows-sys 0.48.0", ] [[package]] -name = "socket2" -version = "0.5.5" +name = "speculoos" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +checksum = "65881c9270d6157f30a09233305da51bed97eef9192d0ea21e57b1c8f05c3620" dependencies = [ - "libc", - "windows-sys 0.48.0", + "num", ] [[package]] @@ -4064,21 +4419,39 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "5b7d0a2c048d661a1a59fcd7355baa232f7ed34e0ee4df2eef3c1c1c0d3852d8" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "syn_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.42", +] + [[package]] name = "sync_wrapper" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tendermint" version = "0.23.9" @@ -4252,7 +4625,7 @@ dependencies = [ "tokio", "tracing", "url", - "uuid", + "uuid 0.8.2", "walkdir", ] @@ -4286,7 +4659,7 @@ dependencies = [ "tokio", "tracing", "url", - "uuid", + "uuid 0.8.2", "walkdir", ] @@ -4338,22 +4711,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -4390,9 +4763,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.34.0" +version = "1.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" dependencies = [ "backtrace", "bytes", @@ -4400,7 +4773,7 @@ dependencies = [ "mio", "num_cpus", "pin-project-lite", - "socket2 0.5.5", + "socket2", "tokio-macros", "windows-sys 0.48.0", ] @@ -4423,7 +4796,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -4471,6 +4844,23 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.1.0", + "toml_datetime", + "winnow", +] + [[package]] name = "tonic" version = "0.8.3" @@ -4582,7 +4972,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -4606,9 +4996,9 @@ dependencies = [ [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" @@ -4624,9 +5014,9 @@ checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" [[package]] name = "unicode-ident" @@ -4645,9 +5035,9 @@ dependencies = [ [[package]] name = "unsafe-libyaml" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" +checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b" [[package]] name = "untrusted" @@ -4672,6 +5062,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +[[package]] +name = "uuid" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" + [[package]] name = "version_check" version = "0.9.4" @@ -4736,7 +5132,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", "wasm-bindgen-shared", ] @@ -4758,7 +5154,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4973,6 +5369,15 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +[[package]] +name = "winnow" +version = "0.5.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b5c3db89721d50d0e2a673f5043fc4722f76dcc352d7b1ab8b8288bed4ed2c5" +dependencies = [ + "memchr", +] + [[package]] name = "wynd-utils" version = "0.4.1" @@ -4985,6 +5390,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "yaml-rust" version = "0.4.5" @@ -5011,5 +5425,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] diff --git a/Cargo.toml b/Cargo.toml index a6701bf57..0ec1dd330 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ cosm-tome = "0.2" cosmos-sdk-proto = "0.19" cosmwasm-schema = { version = "1.2" } cosmwasm-std = { version = "1.5.0", features = ["ibc3"] } +cw-address-like = "1.0.4" cw-controllers = "1.1" cw-multi-test = "0.18" cw-storage-plus = { version = "1.1" } @@ -53,13 +54,17 @@ cw4-group = "1.1" cw721 = "0.18" cw721-base = "0.18" env_logger = "0.10" +getrandom = "0.2" +integer-sqrt = "0.1.5" +integer-cbrt = "0.1.2" once_cell = "1.18" osmosis-std = "0.20.1" osmosis-std-derive = "0.20.1" -osmosis-test-tube = "20.1.1" +osmosis-test-tube = "20.1.2" proc-macro2 = "1.0" prost = { version = "0.12.3", features = ["prost-derive"] } prost-types = { version = "0.12.3", default-features = false } +rust_decimal = "1.14.3" quote = "1.0" rand = "0.8" schemars = "0.8" @@ -71,7 +76,8 @@ sg-multi-test = "3.1.0" sg-std = "3.1.0" sg721 = "3.1.0" sg721-base = "3.1.0" -syn = { version = "1.0", features = ["derive"] } +speculoos = "0.11.0" +syn = {version = "1.0", features = ["derive"]} test-context = "0.1" thiserror = { version = "1.0" } wynd-utils = "0.4" @@ -80,7 +86,9 @@ wynd-utils = "0.4" # optional owner. cw-ownable = "0.5" +cw-abc = { path = "./contracts/external/cw-abc", version = "*" } cw-admin-factory = { path = "./contracts/external/cw-admin-factory", version = "2.4.1" } +cw-curves = { path = "./packages/cw-curves", version = "2.4.1" } cw-denom = { path = "./packages/cw-denom", version = "2.4.1" } cw-hooks = { path = "./packages/cw-hooks", version = "2.4.1" } cw-paginate-storage = { path = "./packages/cw-paginate-storage", version = "2.4.1" } @@ -93,6 +101,7 @@ cw-wormhole = { path = "./packages/cw-wormhole", version = "2.4.1" } cw20-stake = { path = "./contracts/staking/cw20-stake", version = "2.4.1" } cw721-controllers = { path = "./packages/cw721-controllers", version = "2.4.1" } cw721-roles = { path = "./contracts/external/cw721-roles", version = "2.4.1" } +dao-abc-factory = { path = "./contracts/external/dao-abc-factory", version = "*" } dao-cw721-extensions = { path = "./packages/dao-cw721-extensions", version = "2.4.1" } dao-dao-core = { path = "./contracts/dao-dao-core", version = "2.4.1" } dao-dao-macros = { path = "./packages/dao-dao-macros", version = "2.4.1" } diff --git a/ci/bootstrap-env/src/main.rs b/ci/bootstrap-env/src/main.rs index c1c321c91..83d6b82c9 100644 --- a/ci/bootstrap-env/src/main.rs +++ b/ci/bootstrap-env/src/main.rs @@ -183,7 +183,8 @@ fn main() -> Result<()> { ); // Persist contract code_ids in local.yaml so we can use SKIP_CONTRACT_STORE locally to avoid having to re-store them again - cfg.contract_deploy_info = orc.contract_map.deploy_info().clone(); + cfg.contract_deploy_info + .clone_from(orc.contract_map.deploy_info()); fs::write( "ci/configs/cosm-orc/local.yaml", serde_yaml::to_string(&cfg)?, diff --git a/ci/integration-tests/src/helpers/chain.rs b/ci/integration-tests/src/helpers/chain.rs index 20b3d27d4..fd7445cde 100644 --- a/ci/integration-tests/src/helpers/chain.rs +++ b/ci/integration-tests/src/helpers/chain.rs @@ -99,7 +99,8 @@ fn global_setup() -> Cfg { .unwrap(); save_gas_report(&orc, &gas_report_dir); // persist stored code_ids in CONFIG, so we can reuse for all tests - cfg.contract_deploy_info = orc.contract_map.deploy_info().clone(); + cfg.contract_deploy_info + .clone_from(orc.contract_map.deploy_info()); } Cfg { diff --git a/contracts/dao-dao-core/schema/dao-dao-core.json b/contracts/dao-dao-core/schema/dao-dao-core.json index 624208e82..4581a2a16 100644 --- a/contracts/dao-dao-core/schema/dao-dao-core.json +++ b/contracts/dao-dao-core/schema/dao-dao-core.json @@ -1723,7 +1723,7 @@ "additionalProperties": false }, { - "description": "Lists all of the items associted with the contract. For example, given the items `{ \"group\": \"foo\", \"subdao\": \"bar\"}` this query would return `[(\"group\", \"foo\"), (\"subdao\", \"bar\")]`.", + "description": "Lists all of the items associated with the contract. For example, given the items `{ \"group\": \"foo\", \"subdao\": \"bar\"}` this query would return `[(\"group\", \"foo\"), (\"subdao\", \"bar\")]`.", "type": "object", "required": [ "list_items" diff --git a/contracts/external/cw-abc/.cargo/config b/contracts/external/cw-abc/.cargo/config new file mode 100644 index 000000000..8d4bc738b --- /dev/null +++ b/contracts/external/cw-abc/.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 schema" diff --git a/contracts/external/cw-abc/Cargo.toml b/contracts/external/cw-abc/Cargo.toml new file mode 100644 index 000000000..3eb423ba0 --- /dev/null +++ b/contracts/external/cw-abc/Cargo.toml @@ -0,0 +1,56 @@ +[package] +name = "cw-abc" +authors = [ + "Ethan Frey ", + "Jake Hartnell", + "Adair ", + "Gabe Lopez ", +] +description = "Implements an Augmented Bonding Curve" +# Inherits license from previous work +license = "Apache-2.0" +edition = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] +# use test tube feature to enable test-tube integration tests, for example +# cargo test --features "test-tube" +test-tube = [] +# when writing tests you may wish to enable test-tube as a default feature +# default = ["test-tube"] + +[dependencies] +cw-utils = { workspace = true } +cw2 = { workspace = true } +cw-storage-plus = { workspace = true } +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-address-like = { workspace = true } +cw-ownable = { workspace = true } +cw-paginate-storage = { workspace = true } +cw-tokenfactory-issuer = { workspace = true, features = ["library"] } +dao-interface = { workspace = true } +getrandom = { workspace = true, features = ["js"] } +thiserror = { workspace = true } +cw-curves = { workspace = true } + +[dev-dependencies] +speculoos = { workspace = true } +anyhow = { workspace = true } +cw-multi-test = { workspace = true } +dao-testing = { workspace = true, features = ["test-tube"] } +osmosis-std = { workspace = true } +osmosis-test-tube = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +cw-tokenfactory-issuer = { workspace = true } +dao-voting-token-staked = { workspace = true } +dao-proposal-single = { workspace = true } +dao-voting = { workspace = true } \ No newline at end of file diff --git a/contracts/external/cw-abc/NOTICE b/contracts/external/cw-abc/NOTICE new file mode 100644 index 000000000..12ff9ef26 --- /dev/null +++ b/contracts/external/cw-abc/NOTICE @@ -0,0 +1,15 @@ +CW20-Bonding: Bonding Curve to release CW20 token +Copyright 2020-21 Ethan Frey +Copyright 2021-22 Confio GmbH + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/contracts/external/cw-abc/README.md b/contracts/external/cw-abc/README.md new file mode 100644 index 000000000..05d2c175e --- /dev/null +++ b/contracts/external/cw-abc/README.md @@ -0,0 +1,150 @@ +# cw-abc + +Implements an [Augmented Bonding Curve](https://medium.com/commonsstack/deep-dive-augmented-bonding-curves-b5ca4fad4436). + +Forked from and heavily inspired by the work on [cw20-bonding](https://github.com/cosmwasm/cw-tokens/tree/main/contracts/cw20-bonding). This contract uses native and token factory tokens instead. + +NOTE: this contract is NOT AUDITED and experimental. NOT RECOMMENDED FOR PRODUCTION USE. Use at your own risk. + +## What are Augmented Bonding Curves? +Before we get to the *Augmented* part, we must first describe bonding curves themselves. + +### Token Bonding Curves + +"A token bonding curve (TBC) is a mathematical curve that defines a relationship between price and token supply." ~[Aavegotchi Wiki](https://wiki.aavegotchi.com/en/curve) + +Each bonding curve has a pricing function, also known as the price curve (or `curve_fn` in our implementation). The `curve_fn` is used to determine the price of the asset. + +With bonding curves, we will always know what the price of an asset will be based on supply! More on benefits later. + +This contract implements two methods: +- `Buy {}` is called with sending along some reserve currency (such as $USDC, or whatever the bonding curve is backed by). The reserve currency is stored by the bonding curve contract, and new tokens are minted and sent to the user. +- `Sell {}` is called along with sending some supply currency (the token minted by the bonding curve). The supply tokens are burned, and reserve currency is returned. + +It is possible to use this contact as a basic bonding curve, without any of the augmented features. + +#### Math + +Given a price curve `f(x)` = price of the `x`th token, we want to figure out how to buy into and sell from the bonding curve. In fact we can look at the total supply issued. let `F(x)` be the integral of `f(x)`. We have issued `x` tokens for `F(x)` sent to the contract. Or, in reverse, if we send `x` tokens to the contract, it will mint `F^-1(x)` tokens. + +From this we can create some formulas. Assume we currently have issued `S` tokens in exchange for `N = F(S)` input tokens. If someone sends us `x` tokens, how much will we issue? + +`F^-1(N+x) - F^-1(N)` = `F^-1(N+x) - S` + +And if we sell `x` tokens, how much we will get out: + +`F(S) - F(S-x)` = `N - F(S-x)` + +Just one calculation each side. To be safe, make sure to round down and always check against `F(S)` when using `F^-1(S)` to estimate how much should be issued. This will also safely give us how many tokens to return. + +There is built in support for safely [raising i128 to an integer power](https://doc.rust-lang.org/std/primitive.i128.html#method.checked_pow). There is also a crate to [provide nth-root of for all integers](https://docs.rs/num-integer/0.1.43/num_integer/trait.Roots.html). With these two, we can handle most math except for logs/exponents. + +Compare this to [writing it all in solidity](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/7b7ff729b82ea73ea168e495d9c94cb901ae95ce/contracts/math/Power.sol) + +Examples: + +Price Constant: `f(x) = k` and `F(x) = kx` and `F^-1(x) = x/k` + +Price Linear: `f(x) = kx` and `F(x) = kx^2/2` and `F^-1(x) = (2x/k)^(0.5)` + +Price Square Root: `f(x) = x^0.5` and `F(x) = x^1.5/1.5` and `F^-1(x) = (1.5*x)^(2/3)` + +[You can read more about bonding curve math here](https://yos.io/2018/11/10/bonding-curves/). + +#### Benefits + +There are a number of benefits to bonding curves: +- There is enough liquidity to back the entire supply without having to list tokens on DEXs +- Easier to wind down projects (there is no going to zero) +- Transparent pricing: looking at the curve will tell you a lot about what kind of project it is. + +### Augmented Bonding Curves + +Augmented Bonding Curves are nothing new, some articles that inspired this implementation: +- https://medium.com/commonsstack/deep-dive-augmented-bonding-curves-b5ca4fad4436 +- https://tokeneconomy.co/token-bonding-curves-in-practice-3eb904720cb8 + +At a high level, augmented bonding curves extend bonding curves with new functionality: +- Entry and exit fees +- Different phases representing the life cycles of projects + +## Features + +Example Instantiation message: + +``` json +{ + "fees_recipient": "address that receives fees", + "token_issuer_code_id": 0, + "supply": { + "subdenom": "utokenname", + "metadata": { + "name": "tokenname", + "description": "Token description.", + "symbol": "TOKEN", + "display": "Token", + }, + "decimals": 6, + "max_supply": "100000000000000" + }, + "reserve": { + "denom": "ujuno", + "decimals": 6, + }, + "curve_type": { + "linear": { + "slope": "2", + "scale": 1 + } + }, + "phase_config": { + "hatch": { + "contribution_limits": { + "min": "10000000", + "max": "100000000000" + }, + "initial_raise": { + "min": "10000000", + "max": "100000000000" + }, + "entry_fee": "0.25" + }, + "open": { + "exit_fee": "0.01", + "entry_fee": "0.01" + }, + "closed": {} + }, + "hatcher_allowlist": [ + { + "addr": "dao_address", + "config": { + "config_type": { "dao": { "priority": 1 } }, + "contribution_limits_override": { + "min": "100000000", + "max": "99999999999999" + } + } + }, + { + "addr": "address", + "config": { + "config_type": { "address": {} } + } + } + ], +} +``` + +- `fees_recipient`: the address that will receive fees (usually a DAO). +- `token_issuer_code_id`: the CosmWasm code ID for a `cw-tokenfactory_issuer` contract. +- `supply`: info about the token that will be minted by the curve. This is the token that is created by the bonding curve. +- `reserve`: this is the token that is used to mint the supply token. +- `curve_type`: information about the pricing curve. +- `phase_config`: configuration for the different phase of the augmented bonding curve. +- `hatcher_allowlist`: the list of address allowed to participate in a hatch. + +## Future Work +- [ ] Optionally vest tokens during the hatch phase +- [ ] Implement an expanded set of pricing curves to choose from + diff --git a/contracts/external/cw-abc/examples/schema.rs b/contracts/external/cw-abc/examples/schema.rs new file mode 100644 index 000000000..aea9bb4b8 --- /dev/null +++ b/contracts/external/cw-abc/examples/schema.rs @@ -0,0 +1,11 @@ +use cosmwasm_schema::write_api; + +use cw_abc::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + } +} diff --git a/contracts/external/cw-abc/schema/cw-abc.json b/contracts/external/cw-abc/schema/cw-abc.json new file mode 100644 index 000000000..ffc6ec4d8 --- /dev/null +++ b/contracts/external/cw-abc/schema/cw-abc.json @@ -0,0 +1,2288 @@ +{ + "contract_name": "cw-abc", + "contract_version": "2.4.1", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "curve_type", + "phase_config", + "reserve", + "supply", + "token_issuer_code_id" + ], + "properties": { + "curve_type": { + "description": "Curve type for this contract", + "allOf": [ + { + "$ref": "#/definitions/CurveType" + } + ] + }, + "funding_pool_forwarding": { + "description": "An optional address for automatically forwarding funding pool gains", + "type": [ + "string", + "null" + ] + }, + "hatcher_allowlist": { + "description": "TODO different ways of doing this, for example DAO members? Using a whitelist contract? Merkle tree? Hatcher allowlist", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/HatcherAllowlistEntryMsg" + } + }, + "phase_config": { + "description": "Hatch configuration information", + "allOf": [ + { + "$ref": "#/definitions/CommonsPhaseConfig" + } + ] + }, + "reserve": { + "description": "Reserve token information", + "allOf": [ + { + "$ref": "#/definitions/ReserveToken" + } + ] + }, + "supply": { + "description": "Supply token information", + "allOf": [ + { + "$ref": "#/definitions/SupplyToken" + } + ] + }, + "token_issuer_code_id": { + "description": "The code id of the cw-tokenfactory-issuer contract", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "ClosedConfig": { + "type": "object", + "additionalProperties": false + }, + "CommonsPhaseConfig": { + "type": "object", + "required": [ + "closed", + "hatch", + "open" + ], + "properties": { + "closed": { + "description": "The Closed phase where the Commons is closed to new members.", + "allOf": [ + { + "$ref": "#/definitions/ClosedConfig" + } + ] + }, + "hatch": { + "description": "The Hatch phase where initial contributors (Hatchers) participate in a hatch sale.", + "allOf": [ + { + "$ref": "#/definitions/HatchConfig" + } + ] + }, + "open": { + "description": "TODO Vest tokens after hatch phase The Vesting phase where tokens minted during the Hatch phase are locked (burning is disabled) to combat early speculation/arbitrage. pub vesting: VestingConfig, The Open phase where anyone can mint tokens by contributing the reserve token into the curve and becoming members of the Commons.", + "allOf": [ + { + "$ref": "#/definitions/OpenConfig" + } + ] + } + }, + "additionalProperties": false + }, + "CurveType": { + "oneOf": [ + { + "description": "Constant always returns `value * 10^-scale` as spot price", + "type": "object", + "required": [ + "constant" + ], + "properties": { + "constant": { + "type": "object", + "required": [ + "scale", + "value" + ], + "properties": { + "scale": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "value": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Linear returns `slope * 10^-scale * supply` as spot price", + "type": "object", + "required": [ + "linear" + ], + "properties": { + "linear": { + "type": "object", + "required": [ + "scale", + "slope" + ], + "properties": { + "scale": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "slope": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "SquareRoot returns `slope * 10^-scale * supply^0.5` as spot price", + "type": "object", + "required": [ + "square_root" + ], + "properties": { + "square_root": { + "type": "object", + "required": [ + "scale", + "slope" + ], + "properties": { + "scale": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "slope": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "DenomUnit": { + "description": "DenomUnit represents a struct that describes a given denomination unit of the basic token.", + "type": "object", + "required": [ + "aliases", + "denom", + "exponent" + ], + "properties": { + "aliases": { + "description": "aliases is a list of string aliases for the given denom", + "type": "array", + "items": { + "type": "string" + } + }, + "denom": { + "description": "denom represents the string name of the given denom unit (e.g uatom).", + "type": "string" + }, + "exponent": { + "description": "exponent represents power of 10 exponent that one must raise the base_denom to in order to equal the given DenomUnit's denom 1 denom = 1^exponent base_denom (e.g. with a base_denom of uatom, one can create a DenomUnit of 'atom' with exponent = 6, thus: 1 atom = 10^6 uatom).", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + } + }, + "HatchConfig": { + "type": "object", + "required": [ + "contribution_limits", + "entry_fee", + "initial_raise" + ], + "properties": { + "contribution_limits": { + "description": "The minimum and maximum contribution amounts (min, max) in the reserve token", + "allOf": [ + { + "$ref": "#/definitions/MinMax" + } + ] + }, + "entry_fee": { + "description": "The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "initial_raise": { + "description": "The initial raise range (min, max) in the reserve token", + "allOf": [ + { + "$ref": "#/definitions/MinMax" + } + ] + } + }, + "additionalProperties": false + }, + "HatcherAllowlistConfigMsg": { + "type": "object", + "required": [ + "config_type" + ], + "properties": { + "config_type": { + "description": "The type of the configuration", + "allOf": [ + { + "$ref": "#/definitions/HatcherAllowlistConfigType" + } + ] + }, + "contribution_limits_override": { + "description": "An optional override of the hatch_config's contribution limit", + "anyOf": [ + { + "$ref": "#/definitions/MinMax" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "HatcherAllowlistConfigType": { + "oneOf": [ + { + "type": "object", + "required": [ + "d_a_o" + ], + "properties": { + "d_a_o": { + "type": "object", + "properties": { + "priority": { + "description": "The optional priority for checking a DAO config None will append the item to the end of the priority queue (least priority)", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "HatcherAllowlistEntryMsg": { + "type": "object", + "required": [ + "addr", + "config" + ], + "properties": { + "addr": { + "type": "string" + }, + "config": { + "$ref": "#/definitions/HatcherAllowlistConfigMsg" + } + }, + "additionalProperties": false + }, + "MinMax": { + "description": "Struct for minimum and maximum values", + "type": "object", + "required": [ + "max", + "min" + ], + "properties": { + "max": { + "$ref": "#/definitions/Uint128" + }, + "min": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, + "NewDenomMetadata": { + "type": "object", + "required": [ + "description", + "display", + "name", + "symbol" + ], + "properties": { + "additional_denom_units": { + "description": "Used define additional units of the token (e.g. \"tiger\") These must have an exponent larger than 0.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/DenomUnit" + } + }, + "description": { + "description": "The description of the token", + "type": "string" + }, + "display": { + "description": "The unit commonly used in communication (e.g. \"cat\")", + "type": "string" + }, + "name": { + "description": "The name of the token (e.g. \"Cat Coin\")", + "type": "string" + }, + "symbol": { + "description": "The ticker symbol of the token (e.g. \"CAT\")", + "type": "string" + } + }, + "additionalProperties": false + }, + "OpenConfig": { + "type": "object", + "required": [ + "entry_fee", + "exit_fee" + ], + "properties": { + "entry_fee": { + "description": "Percentage of capital put into the Reserve Pool during the Open phase when buying from the curve.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "exit_fee": { + "description": "Exit taxation ratio", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "ReserveToken": { + "type": "object", + "required": [ + "decimals", + "denom" + ], + "properties": { + "decimals": { + "description": "Number of decimal places for the reserve token, needed for proper curve math. Same format as decimals above, eg. if it is uatom, where 1 unit is 10^-6 ATOM, use 6 here", + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "denom": { + "description": "Reserve token denom (only support native for now)", + "type": "string" + } + }, + "additionalProperties": false + }, + "SupplyToken": { + "type": "object", + "required": [ + "decimals", + "subdenom" + ], + "properties": { + "decimals": { + "description": "Number of decimal places for the supply token, needed for proper curve math. Default for token factory is 6", + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "max_supply": { + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "metadata": { + "description": "Metadata for the supply token to create", + "anyOf": [ + { + "$ref": "#/definitions/NewDenomMetadata" + }, + { + "type": "null" + } + ] + }, + "subdenom": { + "description": "The denom to create for the supply token", + "type": "string" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Buy will attempt to purchase as many supply tokens as possible. You must send only reserve tokens.", + "type": "object", + "required": [ + "buy" + ], + "properties": { + "buy": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Sell burns supply tokens in return for the reserve token. You must send only supply tokens.", + "type": "object", + "required": [ + "sell" + ], + "properties": { + "sell": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Donate will donate tokens to the funding pool. You must send only reserve tokens.", + "type": "object", + "required": [ + "donate" + ], + "properties": { + "donate": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Withdraw will withdraw tokens from the funding pool.", + "type": "object", + "required": [ + "withdraw" + ], + "properties": { + "withdraw": { + "type": "object", + "properties": { + "amount": { + "description": "The amount to withdraw (defaults to full amount).", + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Sets (or unsets if set to None) the maximum supply", + "type": "object", + "required": [ + "update_max_supply" + ], + "properties": { + "update_max_supply": { + "type": "object", + "properties": { + "max_supply": { + "description": "The maximum supply able to be minted.", + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Updates the curve type used for pricing tokens. Only callable by owner. TODO think about other potential limitations on this.", + "type": "object", + "required": [ + "update_curve" + ], + "properties": { + "update_curve": { + "type": "object", + "required": [ + "curve_type" + ], + "properties": { + "curve_type": { + "$ref": "#/definitions/CurveType" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Update the hatch phase allowlist. Only callable by owner.", + "type": "object", + "required": [ + "update_hatch_allowlist" + ], + "properties": { + "update_hatch_allowlist": { + "type": "object", + "required": [ + "to_add", + "to_remove" + ], + "properties": { + "to_add": { + "description": "Addresses to be added.", + "type": "array", + "items": { + "$ref": "#/definitions/HatcherAllowlistEntryMsg" + } + }, + "to_remove": { + "description": "Addresses to be removed.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Toggles the paused state (circuit breaker)", + "type": "object", + "required": [ + "toggle_pause" + ], + "properties": { + "toggle_pause": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Update the funding pool forwarding. Only callable by owner.", + "type": "object", + "required": [ + "update_funding_pool_forwarding" + ], + "properties": { + "update_funding_pool_forwarding": { + "type": "object", + "properties": { + "address": { + "description": "The address to receive the funding pool forwarding. Set to None to stop forwarding.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Update the configuration of a certain phase. This can only be called by the owner.", + "type": "object", + "required": [ + "update_phase_config" + ], + "properties": { + "update_phase_config": { + "$ref": "#/definitions/UpdatePhaseConfigMsg" + } + }, + "additionalProperties": false + }, + { + "description": "Closing the bonding curve means no more buys are enabled and exit tax is set to zero. For example, this could be used in the event of a project shutting down.", + "type": "object", + "required": [ + "close" + ], + "properties": { + "close": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Update the contract's ownership. The `action` to be provided can be either to propose transferring ownership to an account, accept a pending ownership transfer, or renounce the ownership permanently.", + "type": "object", + "required": [ + "update_ownership" + ], + "properties": { + "update_ownership": { + "$ref": "#/definitions/Action" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Action": { + "description": "Actions that can be taken to alter the contract's ownership", + "oneOf": [ + { + "description": "Propose to transfer the contract's ownership to another account, optionally with an expiry time.\n\nCan only be called by the contract's current owner.\n\nAny existing pending ownership transfer is overwritten.", + "type": "object", + "required": [ + "transfer_ownership" + ], + "properties": { + "transfer_ownership": { + "type": "object", + "required": [ + "new_owner" + ], + "properties": { + "expiry": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "new_owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Accept the pending ownership transfer.\n\nCan only be called by the pending owner.", + "type": "string", + "enum": [ + "accept_ownership" + ] + }, + { + "description": "Give up the contract's ownership and the possibility of appointing a new owner.\n\nCan only be invoked by the contract's current owner.\n\nAny existing pending ownership transfer is canceled.", + "type": "string", + "enum": [ + "renounce_ownership" + ] + } + ] + }, + "CurveType": { + "oneOf": [ + { + "description": "Constant always returns `value * 10^-scale` as spot price", + "type": "object", + "required": [ + "constant" + ], + "properties": { + "constant": { + "type": "object", + "required": [ + "scale", + "value" + ], + "properties": { + "scale": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "value": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Linear returns `slope * 10^-scale * supply` as spot price", + "type": "object", + "required": [ + "linear" + ], + "properties": { + "linear": { + "type": "object", + "required": [ + "scale", + "slope" + ], + "properties": { + "scale": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "slope": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "SquareRoot returns `slope * 10^-scale * supply^0.5` as spot price", + "type": "object", + "required": [ + "square_root" + ], + "properties": { + "square_root": { + "type": "object", + "required": [ + "scale", + "slope" + ], + "properties": { + "scale": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "slope": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "HatcherAllowlistConfigMsg": { + "type": "object", + "required": [ + "config_type" + ], + "properties": { + "config_type": { + "description": "The type of the configuration", + "allOf": [ + { + "$ref": "#/definitions/HatcherAllowlistConfigType" + } + ] + }, + "contribution_limits_override": { + "description": "An optional override of the hatch_config's contribution limit", + "anyOf": [ + { + "$ref": "#/definitions/MinMax" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "HatcherAllowlistConfigType": { + "oneOf": [ + { + "type": "object", + "required": [ + "d_a_o" + ], + "properties": { + "d_a_o": { + "type": "object", + "properties": { + "priority": { + "description": "The optional priority for checking a DAO config None will append the item to the end of the priority queue (least priority)", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "HatcherAllowlistEntryMsg": { + "type": "object", + "required": [ + "addr", + "config" + ], + "properties": { + "addr": { + "type": "string" + }, + "config": { + "$ref": "#/definitions/HatcherAllowlistConfigMsg" + } + }, + "additionalProperties": false + }, + "MinMax": { + "description": "Struct for minimum and maximum values", + "type": "object", + "required": [ + "max", + "min" + ], + "properties": { + "max": { + "$ref": "#/definitions/Uint128" + }, + "min": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + }, + "UpdatePhaseConfigMsg": { + "description": "Update the phase configurations. These can only be called by the owner.", + "oneOf": [ + { + "description": "Update the hatch phase configuration", + "type": "object", + "required": [ + "hatch" + ], + "properties": { + "hatch": { + "type": "object", + "properties": { + "contribution_limits": { + "anyOf": [ + { + "$ref": "#/definitions/MinMax" + }, + { + "type": "null" + } + ] + }, + "entry_fee": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "initial_raise": { + "anyOf": [ + { + "$ref": "#/definitions/MinMax" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Update the open phase configuration.", + "type": "object", + "required": [ + "open" + ], + "properties": { + "open": { + "type": "object", + "properties": { + "entry_fee": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "exit_fee": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Update the closed phase configuration. TODO Set the curve type to be used on close?", + "type": "object", + "required": [ + "closed" + ], + "properties": { + "closed": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "Returns the reserve and supply quantities, as well as the spot price to buy 1 token Returns [`CurveInfoResponse`]", + "type": "object", + "required": [ + "curve_info" + ], + "properties": { + "curve_info": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about the curve type (i.e. linear, constant, etc.)", + "type": "object", + "required": [ + "curve_type" + ], + "properties": { + "curve_type": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns Token Factory Denom for the supply", + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns a list of the donors and their donations Returns [`DonationsResponse`]", + "type": "object", + "required": [ + "donations" + ], + "properties": { + "donations": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "is_paused" + ], + "properties": { + "is_paused": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the funding pool forwarding config for the contract. This is the address that receives any fees collected from bonding curve operation and donations", + "type": "object", + "required": [ + "funding_pool_forwarding" + ], + "properties": { + "funding_pool_forwarding": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "List the hatchers and their contributions Returns [`HatchersResponse`]", + "type": "object", + "required": [ + "hatchers" + ], + "properties": { + "hatchers": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the contribution of a hatcher", + "type": "object", + "required": [ + "hatcher" + ], + "properties": { + "hatcher": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Lists the hatcher allowlist Returns [`HatcherAllowlistResponse`]", + "type": "object", + "required": [ + "hatcher_allowlist" + ], + "properties": { + "hatcher_allowlist": { + "type": "object", + "properties": { + "config_type": { + "anyOf": [ + { + "$ref": "#/definitions/HatcherAllowlistConfigType" + }, + { + "type": "null" + } + ] + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the Maximum Supply of the supply token", + "type": "object", + "required": [ + "max_supply" + ], + "properties": { + "max_supply": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the amount of tokens to receive from buying", + "type": "object", + "required": [ + "buy_quote" + ], + "properties": { + "buy_quote": { + "type": "object", + "required": [ + "payment" + ], + "properties": { + "payment": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the amount of tokens to receive from selling", + "type": "object", + "required": [ + "sell_quote" + ], + "properties": { + "sell_quote": { + "type": "object", + "required": [ + "payment" + ], + "properties": { + "payment": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the current phase", + "type": "object", + "required": [ + "phase" + ], + "properties": { + "phase": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the current phase configuration Returns [`CommonsPhaseConfigResponse`]", + "type": "object", + "required": [ + "phase_config" + ], + "properties": { + "phase_config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the address of the cw-tokenfactory-issuer contract", + "type": "object", + "required": [ + "token_contract" + ], + "properties": { + "token_contract": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Query the contract's ownership information", + "type": "object", + "required": [ + "ownership" + ], + "properties": { + "ownership": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "HatcherAllowlistConfigType": { + "oneOf": [ + { + "type": "object", + "required": [ + "d_a_o" + ], + "properties": { + "d_a_o": { + "type": "object", + "properties": { + "priority": { + "description": "The optional priority for checking a DAO config None will append the item to the end of the priority queue (least priority)", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "migrate": null, + "sudo": null, + "responses": { + "buy_quote": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QuoteResponse", + "type": "object", + "required": [ + "amount", + "funded", + "new_reserve", + "new_supply" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "funded": { + "$ref": "#/definitions/Uint128" + }, + "new_reserve": { + "$ref": "#/definitions/Uint128" + }, + "new_supply": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false, + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "curve_info": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CurveInfoResponse", + "type": "object", + "required": [ + "funding", + "reserve", + "reserve_denom", + "spot_price", + "supply" + ], + "properties": { + "funding": { + "description": "The amount of tokens in the funding pool", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "reserve": { + "description": "How many reserve tokens have been received", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "reserve_denom": { + "description": "Current reserve denom", + "type": "string" + }, + "spot_price": { + "description": "Current spot price of the token", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "supply": { + "description": "How many supply tokens have been issued", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "curve_type": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CurveType", + "oneOf": [ + { + "description": "Constant always returns `value * 10^-scale` as spot price", + "type": "object", + "required": [ + "constant" + ], + "properties": { + "constant": { + "type": "object", + "required": [ + "scale", + "value" + ], + "properties": { + "scale": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "value": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Linear returns `slope * 10^-scale * supply` as spot price", + "type": "object", + "required": [ + "linear" + ], + "properties": { + "linear": { + "type": "object", + "required": [ + "scale", + "slope" + ], + "properties": { + "scale": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "slope": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "SquareRoot returns `slope * 10^-scale * supply^0.5` as spot price", + "type": "object", + "required": [ + "square_root" + ], + "properties": { + "square_root": { + "type": "object", + "required": [ + "scale", + "slope" + ], + "properties": { + "scale": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "slope": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "denom": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "DenomResponse", + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + }, + "donations": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "DonationsResponse", + "type": "object", + "required": [ + "donations" + ], + "properties": { + "donations": { + "description": "The donators mapped to their donation in the reserve token", + "type": "array", + "items": { + "type": "array", + "items": [ + { + "$ref": "#/definitions/Addr" + }, + { + "$ref": "#/definitions/Uint128" + } + ], + "maxItems": 2, + "minItems": 2 + } + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "funding_pool_forwarding": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Nullable_Addr", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ], + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + } + } + }, + "hatcher": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Uint128", + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "hatcher_allowlist": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "HatcherAllowlistResponse", + "type": "object", + "properties": { + "allowlist": { + "description": "Hatcher allowlist", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/HatcherAllowlistEntry" + } + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "HatcherAllowlistConfig": { + "description": "The configuration for a member of the hatcher allowlist", + "type": "object", + "required": [ + "config_height", + "config_type" + ], + "properties": { + "config_height": { + "description": "The height of the config insertion For use when checking allowlist of DAO configs", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "config_type": { + "description": "The type of the configuration", + "allOf": [ + { + "$ref": "#/definitions/HatcherAllowlistConfigType" + } + ] + }, + "contribution_limits_override": { + "description": "An optional override of the hatch_config's contribution limit", + "anyOf": [ + { + "$ref": "#/definitions/MinMax" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "HatcherAllowlistConfigType": { + "oneOf": [ + { + "type": "object", + "required": [ + "d_a_o" + ], + "properties": { + "d_a_o": { + "type": "object", + "properties": { + "priority": { + "description": "The optional priority for checking a DAO config None will append the item to the end of the priority queue (least priority)", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "HatcherAllowlistEntry": { + "type": "object", + "required": [ + "addr", + "config" + ], + "properties": { + "addr": { + "$ref": "#/definitions/Addr" + }, + "config": { + "$ref": "#/definitions/HatcherAllowlistConfig" + } + }, + "additionalProperties": false + }, + "MinMax": { + "description": "Struct for minimum and maximum values", + "type": "object", + "required": [ + "max", + "min" + ], + "properties": { + "max": { + "$ref": "#/definitions/Uint128" + }, + "min": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "hatchers": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "HatchersResponse", + "type": "object", + "required": [ + "hatchers" + ], + "properties": { + "hatchers": { + "description": "The hatchers mapped to their contribution in the reserve token", + "type": "array", + "items": { + "type": "array", + "items": [ + { + "$ref": "#/definitions/Addr" + }, + { + "$ref": "#/definitions/Uint128" + } + ], + "maxItems": 2, + "minItems": 2 + } + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "is_paused": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Boolean", + "type": "boolean" + }, + "max_supply": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Uint128", + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "ownership": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Ownership_for_String", + "description": "The contract's ownership info", + "type": "object", + "properties": { + "owner": { + "description": "The contract's current owner. `None` if the ownership has been renounced.", + "type": [ + "string", + "null" + ] + }, + "pending_expiry": { + "description": "The deadline for the pending owner to accept the ownership. `None` if there isn't a pending ownership transfer, or if a transfer exists and it doesn't have a deadline.", + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "pending_owner": { + "description": "The account who has been proposed to take over the ownership. `None` if there isn't a pending ownership transfer.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "phase": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CommonsPhase", + "type": "string", + "enum": [ + "hatch", + "open", + "closed" + ] + }, + "phase_config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CommonsPhaseConfigResponse", + "type": "object", + "required": [ + "phase", + "phase_config" + ], + "properties": { + "phase": { + "description": "Current phase", + "allOf": [ + { + "$ref": "#/definitions/CommonsPhase" + } + ] + }, + "phase_config": { + "description": "The phase configuration", + "allOf": [ + { + "$ref": "#/definitions/CommonsPhaseConfig" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "ClosedConfig": { + "type": "object", + "additionalProperties": false + }, + "CommonsPhase": { + "type": "string", + "enum": [ + "hatch", + "open", + "closed" + ] + }, + "CommonsPhaseConfig": { + "type": "object", + "required": [ + "closed", + "hatch", + "open" + ], + "properties": { + "closed": { + "description": "The Closed phase where the Commons is closed to new members.", + "allOf": [ + { + "$ref": "#/definitions/ClosedConfig" + } + ] + }, + "hatch": { + "description": "The Hatch phase where initial contributors (Hatchers) participate in a hatch sale.", + "allOf": [ + { + "$ref": "#/definitions/HatchConfig" + } + ] + }, + "open": { + "description": "TODO Vest tokens after hatch phase The Vesting phase where tokens minted during the Hatch phase are locked (burning is disabled) to combat early speculation/arbitrage. pub vesting: VestingConfig, The Open phase where anyone can mint tokens by contributing the reserve token into the curve and becoming members of the Commons.", + "allOf": [ + { + "$ref": "#/definitions/OpenConfig" + } + ] + } + }, + "additionalProperties": false + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "HatchConfig": { + "type": "object", + "required": [ + "contribution_limits", + "entry_fee", + "initial_raise" + ], + "properties": { + "contribution_limits": { + "description": "The minimum and maximum contribution amounts (min, max) in the reserve token", + "allOf": [ + { + "$ref": "#/definitions/MinMax" + } + ] + }, + "entry_fee": { + "description": "The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "initial_raise": { + "description": "The initial raise range (min, max) in the reserve token", + "allOf": [ + { + "$ref": "#/definitions/MinMax" + } + ] + } + }, + "additionalProperties": false + }, + "MinMax": { + "description": "Struct for minimum and maximum values", + "type": "object", + "required": [ + "max", + "min" + ], + "properties": { + "max": { + "$ref": "#/definitions/Uint128" + }, + "min": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, + "OpenConfig": { + "type": "object", + "required": [ + "entry_fee", + "exit_fee" + ], + "properties": { + "entry_fee": { + "description": "Percentage of capital put into the Reserve Pool during the Open phase when buying from the curve.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "exit_fee": { + "description": "Exit taxation ratio", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "sell_quote": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QuoteResponse", + "type": "object", + "required": [ + "amount", + "funded", + "new_reserve", + "new_supply" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "funded": { + "$ref": "#/definitions/Uint128" + }, + "new_reserve": { + "$ref": "#/definitions/Uint128" + }, + "new_supply": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false, + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "token_contract": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Addr", + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + } + } +} diff --git a/contracts/external/cw-abc/src/abc.rs b/contracts/external/cw-abc/src/abc.rs new file mode 100644 index 000000000..1d86a26cb --- /dev/null +++ b/contracts/external/cw-abc/src/abc.rs @@ -0,0 +1,219 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{ensure, Decimal, Uint128}; +use cw_curves::{ + curves::{Constant, Linear, SquareRoot}, + utils::decimal, + Curve, DecimalPlaces, +}; +use dao_interface::token::NewDenomMetadata; + +use crate::ContractError; + +#[cw_serde] +pub struct SupplyToken { + /// The denom to create for the supply token + pub subdenom: String, + /// Metadata for the supply token to create + pub metadata: Option, + /// Number of decimal places for the supply token, needed for proper curve math. + /// Default for token factory is 6 + pub decimals: u8, + // Optional maximum supply + pub max_supply: Option, +} + +#[cw_serde] +pub struct ReserveToken { + /// Reserve token denom (only support native for now) + pub denom: String, + /// Number of decimal places for the reserve token, needed for proper curve math. + /// Same format as decimals above, eg. if it is uatom, where 1 unit is 10^-6 ATOM, use 6 here + pub decimals: u8, +} + +/// Struct for minimum and maximum values +#[cw_serde] +pub struct MinMax { + pub min: Uint128, + pub max: Uint128, +} + +impl Copy for MinMax {} + +#[cw_serde] +pub struct HatchConfig { + /// The minimum and maximum contribution amounts (min, max) in the reserve token + pub contribution_limits: MinMax, + /// The initial raise range (min, max) in the reserve token + pub initial_raise: MinMax, + /// The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool + pub entry_fee: Decimal, +} + +impl Copy for HatchConfig {} + +impl HatchConfig { + /// Validate the hatch config + pub fn validate(&self) -> Result<(), ContractError> { + ensure!( + self.initial_raise.min < self.initial_raise.max, + ContractError::HatchPhaseConfigError( + "Initial raise minimum value must be less than maximum value.".to_string() + ) + ); + + ensure!( + self.entry_fee <= Decimal::percent(100u64), + ContractError::HatchPhaseConfigError( + "Initial allocation percentage must be between 0 and 100.".to_string() + ) + ); + + Ok(()) + } +} + +#[cw_serde] +pub struct OpenConfig { + /// Percentage of capital put into the Reserve Pool during the Open phase + /// when buying from the curve. + pub entry_fee: Decimal, + /// Exit taxation ratio + pub exit_fee: Decimal, +} + +impl OpenConfig { + /// Validate the open config + pub fn validate(&self) -> Result<(), ContractError> { + ensure!( + self.entry_fee <= Decimal::percent(100u64), + ContractError::OpenPhaseConfigError( + "Reserve percentage must be between 0 and 100.".to_string() + ) + ); + + ensure!( + self.exit_fee <= Decimal::percent(100u64), + ContractError::OpenPhaseConfigError( + "Exit taxation percentage must be between 0 and 100.".to_string() + ) + ); + + Ok(()) + } +} + +#[cw_serde] +pub struct ClosedConfig {} + +impl ClosedConfig { + /// Validate the closed config + pub fn validate(&self) -> Result<(), ContractError> { + Ok(()) + } +} + +#[cw_serde] +pub struct CommonsPhaseConfig { + /// The Hatch phase where initial contributors (Hatchers) participate in a hatch sale. + pub hatch: HatchConfig, + /// TODO Vest tokens after hatch phase + /// The Vesting phase where tokens minted during the Hatch phase are locked (burning is disabled) to combat early speculation/arbitrage. + /// pub vesting: VestingConfig, + /// The Open phase where anyone can mint tokens by contributing the reserve token into the curve and becoming members of the Commons. + pub open: OpenConfig, + /// The Closed phase where the Commons is closed to new members. + pub closed: ClosedConfig, +} + +#[cw_serde] +pub enum CommonsPhase { + Hatch, + Open, + Closed, +} + +impl CommonsPhase { + pub fn expect_hatch(&self) -> Result<(), ContractError> { + ensure!( + matches!(self, CommonsPhase::Hatch), + ContractError::InvalidPhase { + expected: "Hatch".to_string(), + actual: format!("{:?}", self) + } + ); + Ok(()) + } + + pub fn expect_open(&self) -> Result<(), ContractError> { + ensure!( + matches!(self, CommonsPhase::Open), + ContractError::InvalidPhase { + expected: "Open".to_string(), + actual: format!("{:?}", self) + } + ); + Ok(()) + } + + pub fn expect_closed(&self) -> Result<(), ContractError> { + ensure!( + matches!(self, CommonsPhase::Closed), + ContractError::InvalidPhase { + expected: "Closed".to_string(), + actual: format!("{:?}", self) + } + ); + Ok(()) + } +} + +impl CommonsPhaseConfig { + /// Validate that the commons configuration is valid + pub fn validate(&self) -> Result<(), ContractError> { + self.hatch.validate()?; + self.open.validate()?; + self.closed.validate()?; + + Ok(()) + } +} + +pub type CurveFn = Box Box>; + +// TODO Curve type validation? +// TODO add S-curve and taylor series +#[cw_serde] +pub enum CurveType { + /// Constant always returns `value * 10^-scale` as spot price + Constant { value: Uint128, scale: u32 }, + /// Linear returns `slope * 10^-scale * supply` as spot price + Linear { slope: Uint128, scale: u32 }, + /// SquareRoot returns `slope * 10^-scale * supply^0.5` as spot price + SquareRoot { slope: Uint128, scale: u32 }, +} + +impl CurveType { + pub fn to_curve_fn(&self) -> CurveFn { + match self.clone() { + CurveType::Constant { value, scale } => { + let calc = move |places| -> Box { + Box::new(Constant::new(decimal(value, scale), places)) + }; + Box::new(calc) + } + CurveType::Linear { slope, scale } => { + let calc = move |places| -> Box { + Box::new(Linear::new(decimal(slope, scale), places)) + }; + Box::new(calc) + } + CurveType::SquareRoot { slope, scale } => { + let calc = move |places| -> Box { + Box::new(SquareRoot::new(decimal(slope, scale), places)) + }; + Box::new(calc) + } + } + } +} diff --git a/contracts/external/cw-abc/src/commands.rs b/contracts/external/cw-abc/src/commands.rs new file mode 100644 index 000000000..c929866c2 --- /dev/null +++ b/contracts/external/cw-abc/src/commands.rs @@ -0,0 +1,810 @@ +use cosmwasm_std::{ + to_json_binary, Addr, BankMsg, Coin, CosmosMsg, DepsMut, Env, MessageInfo, QuerierWrapper, + Response, StdResult, Storage, Uint128, WasmMsg, +}; +use cw_tokenfactory_issuer::msg::ExecuteMsg as IssuerExecuteMsg; +use cw_utils::must_pay; +use std::ops::Deref; + +use crate::abc::{CommonsPhase, CurveType, HatchConfig, MinMax}; +use crate::helpers::{calculate_buy_quote, calculate_sell_quote}; +use crate::msg::{HatcherAllowlistEntryMsg, UpdatePhaseConfigMsg}; +use crate::state::{ + hatcher_allowlist, HatcherAllowlistConfig, HatcherAllowlistConfigType, CURVE_STATE, CURVE_TYPE, + DONATIONS, FUNDING_POOL_FORWARDING, HATCHERS, HATCHER_DAO_PRIORITY_QUEUE, IS_PAUSED, + MAX_SUPPLY, PHASE, PHASE_CONFIG, SUPPLY_DENOM, TOKEN_ISSUER_CONTRACT, +}; +use crate::ContractError; + +pub fn buy(deps: DepsMut, _env: Env, info: MessageInfo) -> Result { + let curve_type = CURVE_TYPE.load(deps.storage)?; + let mut curve_state = CURVE_STATE.load(deps.storage)?; + + let payment = must_pay(&info, &curve_state.reserve_denom)?; + + // Load the phase config and phase + let phase_config = PHASE_CONFIG.load(deps.storage)?; + let mut phase = PHASE.load(deps.storage)?; + + // Calculate the curve state from the buy + let buy_quote = calculate_buy_quote(payment, &curve_type, &curve_state, &phase, &phase_config)?; + + // Validate phase + match &phase { + CommonsPhase::Hatch => { + // Check that the potential hatcher is allowlisted + let hatch_config = assert_allowlisted( + deps.querier, + deps.storage, + &info.sender, + &phase_config.hatch, + )?; + + // Update hatcher contribution + let contribution = + HATCHERS.update(deps.storage, &info.sender, |amount| -> StdResult<_> { + Ok(amount.unwrap_or_default() + payment) + })?; + + // Check contribution is within limits + if contribution < hatch_config.contribution_limits.min + || contribution > hatch_config.contribution_limits.max + { + return Err(ContractError::ContributionLimit { + min: hatch_config.contribution_limits.min, + max: hatch_config.contribution_limits.max, + }); + } + + // Check if the initial_raise max has been met + if buy_quote.new_reserve >= hatch_config.initial_raise.max { + // Transition to the Open phase + phase = CommonsPhase::Open; + + // Can clean up state here + hatcher_allowlist().clear(deps.storage); + + PHASE.save(deps.storage, &phase)?; + } + } + CommonsPhase::Open => {} + CommonsPhase::Closed => { + return Err(ContractError::CommonsClosed {}); + } + }; + + // Check that the minted amount has not exceeded the max supply (if configured) + if let Some(max_supply) = MAX_SUPPLY.may_load(deps.storage)? { + if buy_quote.new_supply > max_supply { + return Err(ContractError::CannotExceedMaxSupply { max: max_supply }); + } + } + + // Mint tokens for sender by calling mint on the cw-tokenfactory-issuer contract + let issuer_addr = TOKEN_ISSUER_CONTRACT.load(deps.storage)?; + let mut msgs: Vec = vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: issuer_addr.to_string(), + msg: to_json_binary(&IssuerExecuteMsg::Mint { + to_address: info.sender.to_string(), + amount: buy_quote.amount, + })?, + funds: vec![], + })]; + + // Send funding to fee recipient + if buy_quote.funded > Uint128::zero() { + if let Some(funding_pool_forwarding) = FUNDING_POOL_FORWARDING.may_load(deps.storage)? { + msgs.push(CosmosMsg::Bank(BankMsg::Send { + to_address: funding_pool_forwarding.to_string(), + amount: vec![Coin { + amount: buy_quote.funded, + denom: curve_state.reserve_denom.clone(), + }], + })) + } else { + curve_state.funding += buy_quote.funded; + } + }; + + // Save the new curve state + curve_state.supply = buy_quote.new_supply; + curve_state.reserve = buy_quote.new_reserve; + + CURVE_STATE.save(deps.storage, &curve_state)?; + + Ok(Response::new() + .add_messages(msgs) + .add_attribute("action", "buy") + .add_attribute("from", info.sender) + .add_attribute("amount", payment) + .add_attribute("reserved", buy_quote.new_reserve) + .add_attribute("minted", buy_quote.amount) + .add_attribute("funded", buy_quote.funded) + .add_attribute("supply", buy_quote.new_supply)) +} + +/// Sell tokens on the bonding curve +pub fn sell(deps: DepsMut, _env: Env, info: MessageInfo) -> Result { + let curve_type = CURVE_TYPE.load(deps.storage)?; + let supply_denom = SUPPLY_DENOM.load(deps.storage)?; + let burn_amount = must_pay(&info, &supply_denom)?; + + let mut curve_state = CURVE_STATE.load(deps.storage)?; + + // Load the phase configuration and the current phase + let phase_config = PHASE_CONFIG.load(deps.storage)?; + let phase = PHASE.load(deps.storage)?; + + // Calculate the sell quote + let sell_quote = calculate_sell_quote( + burn_amount, + &curve_type, + &curve_state, + &phase, + &phase_config, + )?; + + let mut send_msgs: Vec = vec![CosmosMsg::Bank(BankMsg::Send { + to_address: info.sender.to_string(), + amount: vec![Coin { + amount: sell_quote.amount, + denom: curve_state.reserve_denom.clone(), + }], + })]; + + let issuer_addr = TOKEN_ISSUER_CONTRACT.load(deps.storage)?; + + // Burn the sent supply tokens + let burn_msgs: Vec = vec![ + // Send tokens to the issuer contract to be burned + CosmosMsg::Bank(BankMsg::Send { + to_address: issuer_addr.to_string().clone(), + amount: vec![Coin { + amount: burn_amount, + denom: supply_denom, + }], + }), + // Execute burn on the cw-tokenfactory-issuer contract + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: issuer_addr.to_string(), + msg: to_json_binary(&IssuerExecuteMsg::Burn { + from_address: issuer_addr.to_string(), + amount: burn_amount, + })?, + funds: vec![], + }), + ]; + + // Send exit fee to the funding pool + if sell_quote.funded > Uint128::zero() { + if let Some(funding_pool_forwarding) = FUNDING_POOL_FORWARDING.may_load(deps.storage)? { + send_msgs.push(CosmosMsg::Bank(BankMsg::Send { + to_address: funding_pool_forwarding.to_string(), + amount: vec![Coin { + amount: sell_quote.funded, + denom: curve_state.reserve_denom.clone(), + }], + })) + } else { + curve_state.funding += sell_quote.funded; + } + } + + // Update the curve state + curve_state.reserve = sell_quote.new_reserve; + curve_state.supply = sell_quote.new_supply; + CURVE_STATE.save(deps.storage, &curve_state)?; + + Ok(Response::new() + .add_messages(burn_msgs) + .add_messages(send_msgs) + .add_attribute("action", "sell") + .add_attribute("from", info.sender) + .add_attribute("amount", burn_amount) + .add_attribute("reserved", sell_quote.new_reserve) + .add_attribute("supply", sell_quote.new_supply) + .add_attribute("burned", sell_quote.amount) + .add_attribute("funded", sell_quote.funded)) +} + +/// Transitions the bonding curve to a closed phase where only sells are allowed +pub fn close(deps: DepsMut, info: MessageInfo) -> Result { + cw_ownable::assert_owner(deps.storage, &info.sender)?; + + PHASE.save(deps.storage, &CommonsPhase::Closed)?; + + Ok(Response::new().add_attribute("action", "close")) +} + +/// Send a donation to the funding pool +pub fn donate(deps: DepsMut, _env: Env, info: MessageInfo) -> Result { + let mut curve_state = CURVE_STATE.load(deps.storage)?; + + let payment = must_pay(&info, &curve_state.reserve_denom)?; + + let msgs = + if let Some(funding_pool_forwarding) = FUNDING_POOL_FORWARDING.may_load(deps.storage)? { + vec![CosmosMsg::Bank(BankMsg::Send { + to_address: funding_pool_forwarding.to_string(), + amount: info.funds, + })] + } else { + curve_state.funding += payment; + + CURVE_STATE.save(deps.storage, &curve_state)?; + + vec![] + }; + + // No minting of tokens is necessary, the supply stays the same + let total_donation = + DONATIONS.update(deps.storage, &info.sender, |maybe_amount| -> StdResult<_> { + if let Some(amount) = maybe_amount { + Ok(amount.checked_add(payment)?) + } else { + Ok(payment) + } + })?; + + Ok(Response::new() + .add_attribute("action", "donate") + .add_attribute("donor", info.sender) + .add_attribute("amount", payment) + .add_attribute("total_donation", total_donation) + .add_messages(msgs)) +} + +/// Withdraw funds from the funding pool (only callable by owner) +pub fn withdraw( + deps: DepsMut, + _env: Env, + info: MessageInfo, + amount: Option, +) -> Result { + // Validate ownership + cw_ownable::assert_owner(deps.storage, &info.sender)?; + + let mut curve_state = CURVE_STATE.load(deps.storage)?; + + // Get amount to withdraw + let amount = amount.unwrap_or(curve_state.funding); + + // Construct the withdraw message + let msg = CosmosMsg::Bank(BankMsg::Send { + to_address: info.sender.to_string(), + amount: vec![Coin { + denom: curve_state.reserve_denom.clone(), + amount, + }], + }); + + // Update the curve state + curve_state.funding = curve_state.funding.checked_sub(amount)?; + CURVE_STATE.save(deps.storage, &curve_state)?; + + Ok(Response::new() + .add_attribute("action", "withdraw") + .add_attribute("withdrawer", info.sender) + .add_attribute("amount", amount) + .add_message(msg)) +} + +/// Updates the funding pool forwarding (only callable by owner) +pub fn update_funding_pool_forwarding( + deps: DepsMut, + env: Env, + info: MessageInfo, + address: Option, +) -> Result { + // Validate ownership + cw_ownable::assert_owner(deps.storage, &info.sender)?; + + // Update the funding pool forwarding + match &address { + Some(address) => { + FUNDING_POOL_FORWARDING.save(deps.storage, &deps.api.addr_validate(address)?)?; + } + None => FUNDING_POOL_FORWARDING.remove(deps.storage), + }; + + Ok(Response::new() + .add_attribute("action", "update_funding_pool_forwarding") + .add_attribute( + "address", + address.unwrap_or(env.contract.address.to_string()), + )) +} + +/// Check if the sender is allowlisted for the hatch phase +fn assert_allowlisted( + querier: QuerierWrapper, + storage: &dyn Storage, + hatcher: &Addr, + hatch_config: &HatchConfig, +) -> Result { + if !hatcher_allowlist().is_empty(storage) { + // Specific configs should trump everything + if hatcher_allowlist().has(storage, hatcher) { + let config = hatcher_allowlist().load(storage, hatcher)?; + + // Do not allow DAO's to purchase themselves when allowlisted as a DAO + if matches!( + config.config_type, + HatcherAllowlistConfigType::DAO { priority: _ } + ) { + return Err(ContractError::SenderNotAllowlisted { + sender: hatcher.to_string(), + }); + } + + return Ok(HatchConfig { + contribution_limits: config + .contribution_limits_override + .unwrap_or(hatch_config.contribution_limits), + ..*hatch_config + }); + } + + // If not allowlisted as individual, then check any DAO allowlists + return Ok(HatchConfig { + contribution_limits: assert_allowlisted_through_daos(querier, storage, hatcher)? + .unwrap_or(hatch_config.contribution_limits), + ..*hatch_config + }); + } + + Ok(*hatch_config) +} + +fn assert_allowlisted_through_daos( + querier: QuerierWrapper, + storage: &dyn Storage, + hatcher: &Addr, +) -> Result, ContractError> { + if let Some(hatcher_dao_priority_queue) = HATCHER_DAO_PRIORITY_QUEUE.may_load(storage)? { + for entry in hatcher_dao_priority_queue { + let voting_power_response_result: StdResult< + dao_interface::voting::VotingPowerAtHeightResponse, + > = querier.query_wasm_smart( + entry.addr, + &dao_interface::msg::QueryMsg::VotingPowerAtHeight { + address: hatcher.to_string(), + height: Some(entry.config.config_height), + }, + ); + + if let Ok(voting_power_response) = voting_power_response_result { + if voting_power_response.power > Uint128::zero() { + return Ok(entry.config.contribution_limits_override); + } + } + } + } + + Err(ContractError::SenderNotAllowlisted { + sender: hatcher.to_string(), + }) +} + +/// Set the maximum supply (only callable by owner) +/// If `max_supply` is set to None there will be no limit.` +pub fn update_max_supply( + deps: DepsMut, + info: MessageInfo, + max_supply: Option, +) -> Result { + cw_ownable::assert_owner(deps.storage, &info.sender)?; + + match max_supply { + Some(max) => MAX_SUPPLY.save(deps.storage, &max)?, + None => MAX_SUPPLY.remove(deps.storage), + } + + Ok(Response::new() + .add_attribute("action", "update_max_supply") + .add_attribute("value", max_supply.unwrap_or(Uint128::MAX).to_string())) +} + +/// Toggles the paused state (only callable by owner) +pub fn toggle_pause(deps: DepsMut, info: MessageInfo) -> Result { + cw_ownable::assert_owner(deps.storage, &info.sender)?; + + let is_paused = + IS_PAUSED.update(deps.storage, |is_paused| -> StdResult<_> { Ok(!is_paused) })?; + + Ok(Response::new() + .add_attribute("action", "toggle_pause") + .add_attribute("is_paused", is_paused.to_string())) +} + +/// Add and remove addresses from the hatcher allowlist (only callable by owner and self) +pub fn update_hatch_allowlist( + deps: DepsMut, + env: Env, + info: MessageInfo, + to_add: Vec, + to_remove: Vec, +) -> Result { + if env.contract.address != info.sender { + cw_ownable::assert_owner(deps.storage, &info.sender)?; + } + + let list = hatcher_allowlist(); + + // Add addresses to the allowlist + for allow in to_add { + let entry = allow.into_entry(deps.as_ref(), env.block.height)?; + + let old_data = list.may_load(deps.storage, &entry.addr)?; + + list.replace( + deps.storage, + &entry.addr, + Some(&entry.config), + old_data.as_ref(), + )?; + + // If the old data was previously a DAO config, then it should be removed + if let Some(old_data) = old_data { + try_remove_from_priority_queue(deps.storage, &entry.addr, &old_data)?; + } + + match allow.config.config_type { + HatcherAllowlistConfigType::DAO { priority } => { + if !HATCHER_DAO_PRIORITY_QUEUE.exists(deps.storage) { + HATCHER_DAO_PRIORITY_QUEUE.save(deps.storage, &vec![entry])?; + } else { + HATCHER_DAO_PRIORITY_QUEUE.update( + deps.storage, + |mut queue| -> StdResult<_> { + match priority { + Some(priority_value) => { + // Insert based on priority + let pos = queue + .binary_search_by(|entry| { + match &entry.config.config_type { + HatcherAllowlistConfigType::DAO { + priority: Some(entry_priority), + } => entry_priority + .cmp(&priority_value) + .then(std::cmp::Ordering::Less), + _ => std::cmp::Ordering::Less, // Treat non-DAO or DAO without priority as lower priority + } + }) + .unwrap_or_else(|e| e); + queue.insert(pos, entry); + } + None => { + // Append to the end if no priority + queue.push(entry); + } + } + + Ok(queue) + }, + )?; + } + } + HatcherAllowlistConfigType::Address {} => {} + } + } + + // Remove addresses from the allowlist + for deny in to_remove { + let addr = deps.api.addr_validate(deny.as_str())?; + + let old_data = list.may_load(deps.storage, &addr)?; + + if let Some(old_data) = old_data { + list.replace(deps.storage, &addr, None, Some(&old_data))?; + + try_remove_from_priority_queue(deps.storage, &addr, &old_data)?; + } + } + + Ok(Response::new().add_attributes(vec![("action", "update_hatch_allowlist")])) +} + +fn try_remove_from_priority_queue( + storage: &mut dyn Storage, + addr: &Addr, + config: &HatcherAllowlistConfig, +) -> Result<(), ContractError> { + if matches!( + config.config_type, + HatcherAllowlistConfigType::DAO { priority: _ } + ) && HATCHER_DAO_PRIORITY_QUEUE.exists(storage) + { + HATCHER_DAO_PRIORITY_QUEUE.update(storage, |mut x| -> StdResult<_> { + if let Some(i) = x.iter().position(|y| y.addr == addr) { + x.remove(i); + } + + Ok(x) + })?; + } + + Ok(()) +} + +/// Update the configuration of a particular phase (only callable by owner) +pub fn update_phase_config( + deps: DepsMut, + _env: Env, + info: MessageInfo, + update_phase_config_msg: UpdatePhaseConfigMsg, +) -> Result { + // Assert that the sender is the contract owner + cw_ownable::assert_owner(deps.storage, &info.sender)?; + + // Load phase and phase config + let phase = PHASE.load(deps.storage)?; + + // Load the current phase config + let mut phase_config = PHASE_CONFIG.load(deps.storage)?; + + match update_phase_config_msg { + UpdatePhaseConfigMsg::Hatch { + initial_raise, + entry_fee, + contribution_limits, + } => { + // Check we are in the hatch phase + phase.expect_hatch()?; + + // Update the hatch config if new values are provided + if let Some(contribution_limits) = contribution_limits { + phase_config.hatch.contribution_limits = contribution_limits; + } + if let Some(initial_raise) = initial_raise { + phase_config.hatch.initial_raise = initial_raise; + } + if let Some(entry_fee) = entry_fee { + phase_config.hatch.entry_fee = entry_fee; + } + + // Validate config + phase_config.hatch.validate()?; + PHASE_CONFIG.save(deps.storage, &phase_config)?; + + Ok(Response::new().add_attribute("action", "update_hatch_phase_config")) + } + UpdatePhaseConfigMsg::Open { + exit_fee, + entry_fee, + } => { + // Check we are in the open phase + phase.expect_open()?; + + // Update the hatch config if new values are provided + if let Some(entry_fee) = entry_fee { + phase_config.open.entry_fee = entry_fee; + } + if let Some(exit_fee) = exit_fee { + phase_config.open.exit_fee = exit_fee; + } + + // Validate config + phase_config.open.validate()?; + PHASE_CONFIG.save(deps.storage, &phase_config)?; + + Ok(Response::new().add_attribute("action", "update_open_phase_config")) + } + // TODO what should the closed phase configuration be, is there one? + _ => todo!(), + } +} + +/// Update the bonding curve. (only callable by owner) +/// NOTE: this changes the pricing. Use with caution. +/// TODO: what other limitations do we want to put on this? +pub fn update_curve( + deps: DepsMut, + info: MessageInfo, + curve_type: CurveType, +) -> Result { + cw_ownable::assert_owner(deps.storage, &info.sender)?; + + CURVE_TYPE.save(deps.storage, &curve_type)?; + + Ok(Response::new().add_attribute("action", "close")) +} + +/// Update the ownership of the contract +pub fn update_ownership( + deps: DepsMut, + env: &Env, + info: &MessageInfo, + action: cw_ownable::Action, +) -> Result { + let ownership = cw_ownable::update_ownership( + DepsMut { + storage: deps.storage, + api: deps.api, + querier: QuerierWrapper::new(deps.querier.deref()), + }, + &env.block, + &info.sender, + action, + )?; + + Ok(Response::default().add_attributes(ownership.into_attributes())) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::testing::prelude::*; + use cosmwasm_std::testing::*; + + mod donate { + use super::*; + use crate::abc::CurveType; + use crate::testing::{mock_init, TEST_CREATOR}; + use cosmwasm_std::coin; + use cw_utils::PaymentError; + + const TEST_DONOR: &str = "donor"; + + fn exec_donate(deps: DepsMut, donation_amount: u128) -> Result { + donate( + deps, + mock_env(), + mock_info(TEST_DONOR, &[coin(donation_amount, TEST_RESERVE_DENOM)]), + ) + } + + #[test] + fn should_fail_with_no_funds() -> Result<(), ContractError> { + let mut deps = mock_dependencies(); + let curve_type = CurveType::Linear { + slope: Uint128::new(1), + scale: 1, + }; + let init_msg = default_instantiate_msg(2, 8, curve_type); + mock_init(deps.as_mut(), init_msg)?; + + let res = exec_donate(deps.as_mut(), 0); + assert_that!(res) + .is_err() + .is_equal_to(ContractError::Payment(PaymentError::NoFunds {})); + + Ok(()) + } + + #[test] + fn should_fail_with_incorrect_denom() -> Result<(), ContractError> { + let mut deps = mock_dependencies(); + let curve_type = CurveType::Linear { + slope: Uint128::new(1), + scale: 1, + }; + let init_msg = default_instantiate_msg(2, 8, curve_type); + mock_init(deps.as_mut(), init_msg)?; + + let res = donate( + deps.as_mut(), + mock_env(), + mock_info(TEST_DONOR, &[coin(1, "fake")]), + ); + assert_that!(res) + .is_err() + .is_equal_to(ContractError::Payment(PaymentError::MissingDenom( + TEST_RESERVE_DENOM.to_string(), + ))); + + Ok(()) + } + + #[test] + fn should_donate_to_forwarding() -> Result<(), ContractError> { + let mut deps = mock_dependencies(); + // this matches `linear_curve` test case from curves.rs + let curve_type = CurveType::SquareRoot { + slope: Uint128::new(1), + scale: 1, + }; + let mut init_msg = default_instantiate_msg(2, 8, curve_type); + init_msg.funding_pool_forwarding = Some(TEST_CREATOR.to_string()); + mock_init(deps.as_mut(), init_msg)?; + + let donation_amount = 5; + let _res = exec_donate(deps.as_mut(), donation_amount)?; + + // Check that the funding pool did not increase, because it was sent to the funding pool forwarding + // NOTE: the balance cannot be checked with mock_dependencies + let curve_state = CURVE_STATE.load(&deps.storage)?; + assert_that!(curve_state.funding).is_equal_to(Uint128::zero()); + + // check that the donor is in the donations map + let donation = DONATIONS.load(&deps.storage, &Addr::unchecked(TEST_DONOR))?; + assert_that!(donation).is_equal_to(Uint128::new(donation_amount)); + + Ok(()) + } + + #[test] + fn test_donate_and_withdraw() -> Result<(), ContractError> { + // Init + let mut deps = mock_dependencies(); + + let curve_type = CurveType::SquareRoot { + slope: Uint128::new(1), + scale: 1, + }; + let init_msg = default_instantiate_msg(2, 8, curve_type); + mock_init(deps.as_mut(), init_msg)?; + + // Donate + let donation_amount = 5; + let _res = exec_donate(deps.as_mut(), donation_amount)?; + + // Check funding pool + let curve_state = CURVE_STATE.load(&deps.storage)?; + assert_that!(curve_state.funding).is_equal_to(Uint128::from(donation_amount)); + + // Check random can't withdraw from the funding pool + let result = withdraw(deps.as_mut(), mock_env(), mock_info("random", &[]), None); + assert_that!(result) + .is_err() + .is_equal_to(ContractError::Ownership( + cw_ownable::OwnershipError::NotOwner, + )); + + // Check owner can withdraw + let result = withdraw( + deps.as_mut(), + mock_env(), + mock_info(crate::testing::TEST_CREATOR, &[]), + None, + ); + assert!(result.is_ok()); + + Ok(()) + } + + #[test] + fn test_pause() -> Result<(), ContractError> { + let mut deps = mock_dependencies(); + // this matches `linear_curve` test case from curves.rs + let curve_type = CurveType::SquareRoot { + slope: Uint128::new(1), + scale: 1, + }; + let init_msg = default_instantiate_msg(2, 8, curve_type); + mock_init(deps.as_mut(), init_msg)?; + + // Ensure not paused on instantiate + assert!(!IS_PAUSED.load(&deps.storage)?); + + // Ensure random cannot pause + let res = toggle_pause(deps.as_mut(), mock_info("random", &[])); + assert_that!(res) + .is_err() + .is_equal_to(ContractError::Ownership( + cw_ownable::OwnershipError::NotOwner, + )); + + // Ensure paused after toggling + toggle_pause(deps.as_mut(), mock_info(TEST_CREATOR, &[]))?; + assert!(IS_PAUSED.load(&deps.storage)?); + + // Ensure random cannot do anything + let res = crate::contract::execute( + deps.as_mut(), + mock_env(), + mock_info("random", &[]), + crate::msg::ExecuteMsg::TogglePause {}, + ); + assert_that!(res) + .is_err() + .is_equal_to(ContractError::Paused {}); + + // Ensure unpaused after toggling + toggle_pause(deps.as_mut(), mock_info(TEST_CREATOR, &[]))?; + assert!(!IS_PAUSED.load(&deps.storage)?); + + Ok(()) + } + } +} diff --git a/contracts/external/cw-abc/src/contract.rs b/contracts/external/cw-abc/src/contract.rs new file mode 100644 index 000000000..c14dfb723 --- /dev/null +++ b/contracts/external/cw-abc/src/contract.rs @@ -0,0 +1,308 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_json_binary, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, + SubMsg, Uint128, WasmMsg, +}; +use cw2::set_contract_version; +use cw_curves::DecimalPlaces; +use cw_tokenfactory_issuer::msg::{ + DenomUnit, ExecuteMsg as IssuerExecuteMsg, InstantiateMsg as IssuerInstantiateMsg, Metadata, +}; +use cw_utils::parse_reply_instantiate_data; + +use crate::abc::{CommonsPhase, CurveFn}; +use crate::error::ContractError; +use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use crate::state::{ + CurveState, CURVE_STATE, CURVE_TYPE, FUNDING_POOL_FORWARDING, IS_PAUSED, MAX_SUPPLY, PHASE, + PHASE_CONFIG, SUPPLY_DENOM, TEMP_SUPPLY, TOKEN_ISSUER_CONTRACT, +}; +use crate::{commands, queries}; + +// version info for migration info +pub(crate) const CONTRACT_NAME: &str = "crates.io:cw-abc"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +const INSTANTIATE_TOKEN_FACTORY_ISSUER_REPLY_ID: u64 = 0; + +#[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 InstantiateMsg { + token_issuer_code_id, + funding_pool_forwarding, + supply, + reserve, + curve_type, + phase_config, + hatcher_allowlist, + } = msg; + + phase_config.validate()?; + + // Validate and store the funding pool forwarding + if let Some(funding_pool_forwarding) = funding_pool_forwarding { + FUNDING_POOL_FORWARDING.save( + deps.storage, + &deps.api.addr_validate(&funding_pool_forwarding)?, + )?; + } + + if supply.subdenom.is_empty() { + return Err(ContractError::SupplyTokenError( + "Token subdenom must not be empty.".to_string(), + )); + } + + if let Some(max_supply) = supply.max_supply { + MAX_SUPPLY.save(deps.storage, &max_supply)?; + } + + // Save the curve type + CURVE_TYPE.save(deps.storage, &curve_type)?; + + PHASE_CONFIG.save(deps.storage, &phase_config)?; + + // TODO don't hardcode this? Make it configurable? Hatch config can be optional + PHASE.save(deps.storage, &CommonsPhase::Hatch)?; + + // Initialize owner to sender + cw_ownable::initialize_owner(deps.storage, deps.api, Some(info.sender.as_str()))?; + + // Setup the curve state + let normalization_places = DecimalPlaces::new(supply.decimals, reserve.decimals); + let curve_state = CurveState::new(reserve.denom, normalization_places); + + // Save subdenom for handling in the reply + TEMP_SUPPLY.save(deps.storage, &supply)?; + + // Instantiate cw-token-factory-issuer contract + let msg = SubMsg::reply_always( + WasmMsg::Instantiate { + // Contract is immutable, no admin + admin: None, + code_id: token_issuer_code_id, + msg: to_json_binary(&IssuerInstantiateMsg::NewToken { + subdenom: supply.subdenom, + })?, + funds: info.funds, + label: "cw-tokenfactory-issuer".to_string(), + }, + INSTANTIATE_TOKEN_FACTORY_ISSUER_REPLY_ID, + ); + + // Save the curve state + CURVE_STATE.save(deps.storage, &curve_state)?; + + // Set the paused state + IS_PAUSED.save(deps.storage, &false)?; + + // Set hatcher allowlist through internal method + let msgs = if let Some(hatcher_allowlist) = hatcher_allowlist { + vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_json_binary(&ExecuteMsg::UpdateHatchAllowlist { + to_add: hatcher_allowlist, + to_remove: vec![], + })?, + funds: vec![], + })] + } else { + vec![] + }; + + Ok(Response::default().add_messages(msgs).add_submessage(msg)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + // If paused, then only the owner can perform actions + if IS_PAUSED.load(deps.storage)? { + cw_ownable::assert_owner(deps.storage, &info.sender) + .map_err(|_| ContractError::Paused {})?; + } + + match msg { + ExecuteMsg::Buy {} => commands::buy(deps, env, info), + ExecuteMsg::Sell {} => commands::sell(deps, env, info), + ExecuteMsg::Close {} => commands::close(deps, info), + ExecuteMsg::Donate {} => commands::donate(deps, env, info), + ExecuteMsg::Withdraw { amount } => commands::withdraw(deps, env, info, amount), + ExecuteMsg::UpdateFundingPoolForwarding { address } => { + commands::update_funding_pool_forwarding(deps, env, info, address) + } + ExecuteMsg::UpdateMaxSupply { max_supply } => { + commands::update_max_supply(deps, info, max_supply) + } + ExecuteMsg::UpdateCurve { curve_type } => commands::update_curve(deps, info, curve_type), + ExecuteMsg::UpdateHatchAllowlist { to_add, to_remove } => { + commands::update_hatch_allowlist(deps, env, info, to_add, to_remove) + } + ExecuteMsg::TogglePause {} => commands::toggle_pause(deps, info), + ExecuteMsg::UpdatePhaseConfig(update_msg) => { + commands::update_phase_config(deps, env, info, update_msg) + } + ExecuteMsg::UpdateOwnership(action) => { + commands::update_ownership(deps, &env, &info, action) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + // default implementation stores curve info as enum, you can do something else in a derived + // contract and just pass in your custom curve to do_execute + let curve_type = CURVE_TYPE.load(deps.storage)?; + let curve_fn = curve_type.to_curve_fn(); + do_query(deps, env, msg, curve_fn) +} + +/// We pull out logic here, so we can import this from another contract and set a different Curve. +/// This contacts sets a curve with an enum in [`InstantiateMsg`] and stored in state, but you may want +/// to use custom math not included - make this easily reusable +pub fn do_query(deps: Deps, _env: Env, msg: QueryMsg, curve_fn: CurveFn) -> StdResult { + match msg { + // custom queries + QueryMsg::CurveInfo {} => to_json_binary(&queries::query_curve_info(deps, curve_fn)?), + QueryMsg::CurveType {} => to_json_binary(&CURVE_TYPE.load(deps.storage)?), + QueryMsg::Denom {} => to_json_binary(&queries::get_denom(deps)?), + QueryMsg::Donations { start_after, limit } => { + to_json_binary(&queries::query_donations(deps, start_after, limit)?) + } + QueryMsg::FundingPoolForwarding {} => { + to_json_binary(&FUNDING_POOL_FORWARDING.may_load(deps.storage)?) + } + QueryMsg::Hatchers { start_after, limit } => { + to_json_binary(&queries::query_hatchers(deps, start_after, limit)?) + } + QueryMsg::Hatcher { addr } => to_json_binary(&queries::query_hatcher(deps, addr)?), + QueryMsg::HatcherAllowlist { + start_after, + limit, + config_type, + } => to_json_binary(&queries::query_hatcher_allowlist( + deps, + start_after, + limit, + config_type, + )?), + QueryMsg::IsPaused {} => to_json_binary(&IS_PAUSED.load(deps.storage)?), + QueryMsg::MaxSupply {} => to_json_binary(&queries::query_max_supply(deps)?), + QueryMsg::Ownership {} => to_json_binary(&cw_ownable::get_ownership(deps.storage)?), + QueryMsg::PhaseConfig {} => to_json_binary(&queries::query_phase_config(deps)?), + QueryMsg::Phase {} => to_json_binary(&PHASE.load(deps.storage)?), + QueryMsg::TokenContract {} => to_json_binary(&TOKEN_ISSUER_CONTRACT.load(deps.storage)?), + QueryMsg::BuyQuote { payment } => to_json_binary(&queries::query_buy_quote(deps, payment)?), + QueryMsg::SellQuote { payment } => { + to_json_binary(&queries::query_sell_quote(deps, payment)?) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + // Set contract to version to latest + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + Ok(Response::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result { + match msg.id { + INSTANTIATE_TOKEN_FACTORY_ISSUER_REPLY_ID => { + // Parse and save address of cw-tokenfactory-issuer + let issuer_addr = parse_reply_instantiate_data(msg)?.contract_address; + TOKEN_ISSUER_CONTRACT.save(deps.storage, &deps.api.addr_validate(&issuer_addr)?)?; + + // Load the temporary supply + let supply = TEMP_SUPPLY.load(deps.storage)?; + + // Clear the temporary state + TEMP_SUPPLY.remove(deps.storage); + + // Format the denom and save it + // By default, the prefix for token factory tokens is "factory" + let denom = format!("factory/{}/{}", &issuer_addr, supply.subdenom); + + SUPPLY_DENOM.save(deps.storage, &denom)?; + + // Msgs to be executed to finalize setup + let mut msgs: Vec = vec![ + // Grant an allowance to mint + WasmMsg::Execute { + contract_addr: issuer_addr.clone(), + msg: to_json_binary(&IssuerExecuteMsg::SetMinterAllowance { + address: env.contract.address.to_string(), + // Allowance needs to be max as this the is the amount of tokens + // the minter is allowed to mint, not to be confused with max supply + // which we have to enforce elsewhere. + allowance: Uint128::MAX, + })?, + funds: vec![], + }, + // Grant an allowance to burn + WasmMsg::Execute { + contract_addr: issuer_addr.clone(), + msg: to_json_binary(&IssuerExecuteMsg::SetBurnerAllowance { + address: env.contract.address.to_string(), + allowance: Uint128::MAX, + })?, + funds: vec![], + }, + ]; + + // If metadata, set it by calling the contract + if let Some(metadata) = supply.metadata { + // The first denom_unit must be the same as the tf and base denom. + // It must have an exponent of 0. This the smallest unit of the token. + // For more info: https://docs.cosmos.network/main/architecture/adr-024-coin-metadata + let mut denom_units = vec![DenomUnit { + denom: denom.clone(), + exponent: 0, + aliases: vec![supply.subdenom], + }]; + + // Caller can optionally define additional units + if let Some(mut additional_units) = metadata.additional_denom_units { + denom_units.append(&mut additional_units); + } + + // Sort denom units by exponent, must be in ascending order + denom_units.sort_by(|a, b| a.exponent.cmp(&b.exponent)); + + msgs.push(WasmMsg::Execute { + contract_addr: issuer_addr.clone(), + msg: to_json_binary(&IssuerExecuteMsg::SetDenomMetadata { + metadata: Metadata { + description: metadata.description, + denom_units, + base: denom.clone(), + display: metadata.display, + name: metadata.name, + symbol: metadata.symbol, + }, + })?, + funds: vec![], + }); + } + + Ok(Response::new() + .add_attribute("cw-tokenfactory-issuer-address", issuer_addr) + .add_attribute("denom", denom) + .add_messages(msgs)) + } + _ => Err(ContractError::UnknownReplyId { id: msg.id }), + } +} diff --git a/contracts/external/cw-abc/src/error.rs b/contracts/external/cw-abc/src/error.rs new file mode 100644 index 000000000..ef59faafc --- /dev/null +++ b/contracts/external/cw-abc/src/error.rs @@ -0,0 +1,69 @@ +use cosmwasm_std::{CheckedMultiplyFractionError, OverflowError, StdError, Uint128}; +use cw_utils::{ParseReplyError, PaymentError}; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + Overflow(#[from] OverflowError), + + #[error(transparent)] + Payment(#[from] PaymentError), + + #[error(transparent)] + ParseReplyError(#[from] ParseReplyError), + + #[error("{0}")] + Ownership(#[from] cw_ownable::OwnershipError), + + #[error("{0}")] + CheckedMultiplyFraction(#[from] CheckedMultiplyFractionError), + + #[error("Cannot mint more tokens than the maximum supply of {max}")] + CannotExceedMaxSupply { max: Uint128 }, + + #[error("The commons is closed to new contributions")] + CommonsClosed {}, + + #[error("The commons is locked against liquidations")] + CommonsHatch {}, + + #[error("Contribution must be less than or equal to {max} and greater than or equal to {min}")] + ContributionLimit { min: Uint128, max: Uint128 }, + + #[error("Hatch phase config error {0}")] + HatchPhaseConfigError(String), + + #[error("Invalid exit fee, must be less than 100%.")] + InvalidExitFee {}, + + #[error("Invalid subdenom: {subdenom:?}")] + InvalidSubdenom { subdenom: String }, + + #[error("Invalid phase, expected {expected:?}, actual {actual:?}")] + InvalidPhase { expected: String, actual: String }, + + #[error("Invalid sell amount")] + MismatchedSellAmount {}, + + #[error("Open phase config error {0}")] + OpenPhaseConfigError(String), + + #[error("Sender {sender:?} is not in the hatcher allowlist.")] + SenderNotAllowlisted { sender: String }, + + #[error("Supply token error {0}")] + SupplyTokenError(String), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Got a submessage reply with unknown id: {id}")] + UnknownReplyId { id: u64 }, + + #[error("Contract is paused")] + Paused {}, +} diff --git a/contracts/external/cw-abc/src/helpers.rs b/contracts/external/cw-abc/src/helpers.rs new file mode 100644 index 000000000..96a275ec9 --- /dev/null +++ b/contracts/external/cw-abc/src/helpers.rs @@ -0,0 +1,109 @@ +use cosmwasm_std::{Decimal, Deps, StdResult, Uint128}; + +use crate::{ + abc::{CommonsPhase, CommonsPhaseConfig, CurveType}, + msg::{HatcherAllowlistEntryMsg, QuoteResponse}, + state::{CurveState, HatcherAllowlistConfig, HatcherAllowlistEntry}, + ContractError, +}; + +/// Calculate the buy quote for a payment +pub fn calculate_buy_quote( + payment: Uint128, + curve_type: &CurveType, + curve_state: &CurveState, + phase: &CommonsPhase, + phase_config: &CommonsPhaseConfig, +) -> Result { + // Generate the bonding curve + let curve_fn = curve_type.to_curve_fn(); + let curve = curve_fn(curve_state.decimals); + + // Calculate the reserved and funded amounts based on the Commons phase + let (reserved, funded) = match phase { + CommonsPhase::Hatch => calculate_reserved_and_funded(payment, phase_config.hatch.entry_fee), + CommonsPhase::Open => calculate_reserved_and_funded(payment, phase_config.open.entry_fee), + CommonsPhase::Closed => Err(ContractError::CommonsClosed {}), + }?; + + // Update the reserve and calculate the new supply from the new reserve + let new_reserve = curve_state.reserve.checked_add(reserved)?; + let new_supply = curve.supply(new_reserve); + + // Calculate the difference between the new and old supply to get the minted tokens + let minted = new_supply.checked_sub(curve_state.supply)?; + + Ok(QuoteResponse { + new_reserve, + funded, + amount: minted, + new_supply, + }) +} + +/// Calculate the sell quote for a payment +pub fn calculate_sell_quote( + payment: Uint128, + curve_type: &CurveType, + curve_state: &CurveState, + phase: &CommonsPhase, + phase_config: &CommonsPhaseConfig, +) -> Result { + // Generate the bonding curve + let curve_fn = curve_type.to_curve_fn(); + let curve = curve_fn(curve_state.decimals); + + // Reduce the supply by the amount being burned + let new_supply = curve_state.supply.checked_sub(payment)?; + + // Determine the exit fee based on the current Commons phase + let exit_fee = match &phase { + CommonsPhase::Hatch => Err(ContractError::CommonsHatch {}), + CommonsPhase::Open => Ok(phase_config.open.exit_fee), + CommonsPhase::Closed => Ok(Decimal::zero()), + }?; + + // Calculate the new reserve based on the new supply + let new_reserve = curve.reserve(new_supply); + + // Calculate how many reserve tokens to release based on the amount being burned + let released_reserve = curve_state.reserve.checked_sub(new_reserve)?; + + // Calculate the reserved and funded amounts based on the exit fee + let (reserved, funded) = calculate_reserved_and_funded(released_reserve, exit_fee)?; + + Ok(QuoteResponse { + new_reserve, + funded, + amount: reserved, + new_supply, + }) +} + +/// Return the reserved and funded amounts based on the payment and the allocation ratio +pub(crate) fn calculate_reserved_and_funded( + payment: Uint128, + allocation_ratio: Decimal, +) -> Result<(Uint128, Uint128), ContractError> { + if allocation_ratio.is_zero() { + return Ok((payment, Uint128::zero())); + } + + let funded = payment.checked_mul_floor(allocation_ratio)?; + let reserved = payment - funded; // Since allocation_ratio is < 1, this subtraction is safe + + Ok((reserved, funded)) +} + +impl HatcherAllowlistEntryMsg { + pub fn into_entry(&self, deps: Deps, height: u64) -> StdResult { + Ok(HatcherAllowlistEntry { + addr: deps.api.addr_validate(&self.addr)?, + config: HatcherAllowlistConfig { + config_type: self.config.config_type, + contribution_limits_override: self.config.contribution_limits_override, + config_height: height, + }, + }) + } +} diff --git a/contracts/external/cw-abc/src/lib.rs b/contracts/external/cw-abc/src/lib.rs new file mode 100644 index 000000000..275d47efd --- /dev/null +++ b/contracts/external/cw-abc/src/lib.rs @@ -0,0 +1,20 @@ +pub mod abc; +pub(crate) mod commands; +pub mod contract; +mod error; +pub(crate) mod helpers; +pub mod msg; +mod queries; +pub mod state; + +// Integration tests using an actual chain binary, requires +// the "test-tube" feature to be enabled +// cargo test --features test-tube +#[cfg(test)] +#[cfg(feature = "test-tube")] +mod test_tube; + +#[cfg(test)] +mod testing; + +pub use crate::error::ContractError; diff --git a/contracts/external/cw-abc/src/msg.rs b/contracts/external/cw-abc/src/msg.rs new file mode 100644 index 000000000..f8ee0d289 --- /dev/null +++ b/contracts/external/cw-abc/src/msg.rs @@ -0,0 +1,243 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Decimal, Uint128}; + +use crate::{ + abc::{CommonsPhase, CommonsPhaseConfig, CurveType, MinMax, ReserveToken, SupplyToken}, + state::{HatcherAllowlistConfigType, HatcherAllowlistEntry}, +}; + +#[cw_serde] +pub struct InstantiateMsg { + /// The code id of the cw-tokenfactory-issuer contract + pub token_issuer_code_id: u64, + + /// An optional address for automatically forwarding funding pool gains + pub funding_pool_forwarding: Option, + + /// Supply token information + pub supply: SupplyToken, + + /// Reserve token information + pub reserve: ReserveToken, + + /// Curve type for this contract + pub curve_type: CurveType, + + /// Hatch configuration information + pub phase_config: CommonsPhaseConfig, + + /// TODO different ways of doing this, for example DAO members? + /// Using a whitelist contract? Merkle tree? + /// Hatcher allowlist + pub hatcher_allowlist: Option>, +} + +/// Update the phase configurations. +/// These can only be called by the owner. +#[cw_serde] +pub enum UpdatePhaseConfigMsg { + /// Update the hatch phase configuration + Hatch { + contribution_limits: Option, + // TODO what is the minimum used for? + initial_raise: Option, + entry_fee: Option, + }, + /// Update the open phase configuration. + Open { + exit_fee: Option, + entry_fee: Option, + }, + /// Update the closed phase configuration. + /// TODO Set the curve type to be used on close? + Closed {}, +} + +#[cw_ownable::cw_ownable_execute] +#[cw_serde] +pub enum ExecuteMsg { + /// Buy will attempt to purchase as many supply tokens as possible. + /// You must send only reserve tokens. + Buy {}, + /// Sell burns supply tokens in return for the reserve token. + /// You must send only supply tokens. + Sell {}, + /// Donate will donate tokens to the funding pool. + /// You must send only reserve tokens. + Donate {}, + /// Withdraw will withdraw tokens from the funding pool. + Withdraw { + /// The amount to withdraw (defaults to full amount). + amount: Option, + }, + /// Sets (or unsets if set to None) the maximum supply + UpdateMaxSupply { + /// The maximum supply able to be minted. + max_supply: Option, + }, + /// Updates the curve type used for pricing tokens. + /// Only callable by owner. + /// TODO think about other potential limitations on this. + UpdateCurve { curve_type: CurveType }, + /// Update the hatch phase allowlist. + /// Only callable by owner. + UpdateHatchAllowlist { + /// Addresses to be added. + to_add: Vec, + /// Addresses to be removed. + to_remove: Vec, + }, + /// Toggles the paused state (circuit breaker) + TogglePause {}, + /// Update the funding pool forwarding. + /// Only callable by owner. + UpdateFundingPoolForwarding { + /// The address to receive the funding pool forwarding. + /// Set to None to stop forwarding. + address: Option, + }, + /// Update the configuration of a certain phase. + /// This can only be called by the owner. + UpdatePhaseConfig(UpdatePhaseConfigMsg), + /// Closing the bonding curve means no more buys are enabled and exit tax is set + /// to zero. + /// For example, this could be used in the event of a project shutting down. + Close {}, +} + +#[cw_ownable::cw_ownable_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Returns the reserve and supply quantities, as well as the spot price to buy 1 token + /// Returns [`CurveInfoResponse`] + #[returns(CurveInfoResponse)] + CurveInfo {}, + /// Returns information about the curve type (i.e. linear, constant, etc.) + #[returns(CurveType)] + CurveType {}, + /// Returns Token Factory Denom for the supply + #[returns(DenomResponse)] + Denom {}, + /// Returns a list of the donors and their donations + /// Returns [`DonationsResponse`] + #[returns(DonationsResponse)] + Donations { + start_after: Option, + limit: Option, + }, + #[returns(bool)] + IsPaused {}, + /// Returns the funding pool forwarding config for the contract. This is the address that + /// receives any fees collected from bonding curve operation and donations + #[returns(Option<::cosmwasm_std::Addr>)] + FundingPoolForwarding {}, + /// List the hatchers and their contributions + /// Returns [`HatchersResponse`] + #[returns(HatchersResponse)] + Hatchers { + start_after: Option, + limit: Option, + }, + /// Returns the contribution of a hatcher + #[returns(Uint128)] + Hatcher { addr: String }, + /// Lists the hatcher allowlist + /// Returns [`HatcherAllowlistResponse`] + #[returns(HatcherAllowlistResponse)] + HatcherAllowlist { + start_after: Option, + limit: Option, + config_type: Option, + }, + /// Returns the Maximum Supply of the supply token + #[returns(Uint128)] + MaxSupply {}, + /// Returns the amount of tokens to receive from buying + #[returns(QuoteResponse)] + BuyQuote { payment: Uint128 }, + /// Returns the amount of tokens to receive from selling + #[returns(QuoteResponse)] + SellQuote { payment: Uint128 }, + /// Returns the current phase + #[returns(CommonsPhase)] + Phase {}, + /// Returns the current phase configuration + /// Returns [`CommonsPhaseConfigResponse`] + #[returns(CommonsPhaseConfigResponse)] + PhaseConfig {}, + /// Returns the address of the cw-tokenfactory-issuer contract + #[returns(::cosmwasm_std::Addr)] + TokenContract {}, +} + +#[cw_serde] +pub struct HatcherAllowlistEntryMsg { + pub addr: String, + pub config: HatcherAllowlistConfigMsg, +} + +#[cw_serde] +pub struct HatcherAllowlistConfigMsg { + /// The type of the configuration + pub config_type: HatcherAllowlistConfigType, + /// An optional override of the hatch_config's contribution limit + pub contribution_limits_override: Option, +} + +#[cw_serde] +pub struct CurveInfoResponse { + /// How many reserve tokens have been received + pub reserve: Uint128, + /// How many supply tokens have been issued + pub supply: Uint128, + /// The amount of tokens in the funding pool + pub funding: Uint128, + /// Current spot price of the token + pub spot_price: Decimal, + /// Current reserve denom + pub reserve_denom: String, +} + +#[cw_serde] +pub struct DenomResponse { + pub denom: String, +} + +#[cw_serde] +pub struct HatcherAllowlistResponse { + /// Hatcher allowlist + pub allowlist: Option>, +} + +#[cw_serde] +pub struct CommonsPhaseConfigResponse { + /// The phase configuration + pub phase_config: CommonsPhaseConfig, + + /// Current phase + pub phase: CommonsPhase, +} + +#[cw_serde] +pub struct DonationsResponse { + /// The donators mapped to their donation in the reserve token + pub donations: Vec<(Addr, Uint128)>, +} + +#[cw_serde] +pub struct HatchersResponse { + /// The hatchers mapped to their contribution in the reserve token + pub hatchers: Vec<(Addr, Uint128)>, +} + +#[cw_serde] +pub struct QuoteResponse { + pub new_reserve: Uint128, + pub funded: Uint128, + pub amount: Uint128, + pub new_supply: Uint128, +} + +#[cw_serde] +pub struct MigrateMsg {} diff --git a/contracts/external/cw-abc/src/queries.rs b/contracts/external/cw-abc/src/queries.rs new file mode 100644 index 000000000..0e07a8e28 --- /dev/null +++ b/contracts/external/cw-abc/src/queries.rs @@ -0,0 +1,172 @@ +use crate::abc::CurveFn; +use crate::helpers::{calculate_buy_quote, calculate_sell_quote}; +use crate::msg::{ + CommonsPhaseConfigResponse, CurveInfoResponse, DenomResponse, DonationsResponse, + HatcherAllowlistResponse, HatchersResponse, QuoteResponse, +}; +use crate::state::{ + hatcher_allowlist, CurveState, HatcherAllowlistConfigType, HatcherAllowlistEntry, CURVE_STATE, + CURVE_TYPE, DONATIONS, HATCHERS, MAX_SUPPLY, PHASE, PHASE_CONFIG, SUPPLY_DENOM, +}; +use cosmwasm_std::{Deps, Order, QuerierWrapper, StdError, StdResult, Uint128}; +use cw_storage_plus::Bound; +use std::ops::Deref; + +/// Get the current state of the curve +pub fn query_curve_info(deps: Deps, curve_fn: CurveFn) -> StdResult { + let CurveState { + reserve, + supply, + reserve_denom, + decimals, + funding, + } = CURVE_STATE.load(deps.storage)?; + + // This we can get from the local digits stored in instantiate + let curve = curve_fn(decimals); + let spot_price = curve.spot_price(supply); + + Ok(CurveInfoResponse { + reserve, + supply, + funding, + spot_price, + reserve_denom, + }) +} + +/// Returns information about the supply Denom +pub fn get_denom(deps: Deps) -> StdResult { + let denom = SUPPLY_DENOM.load(deps.storage)?; + Ok(DenomResponse { denom }) +} + +pub fn query_donations( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + let donations = cw_paginate_storage::paginate_map( + Deps { + storage: deps.storage, + api: deps.api, + querier: QuerierWrapper::new(deps.querier.deref()), + }, + &DONATIONS, + start_after + .map(|addr| deps.api.addr_validate(&addr)) + .transpose()? + .as_ref(), + limit, + Order::Descending, + )?; + + Ok(DonationsResponse { donations }) +} + +/// Query hatchers who contributed during the hatch phase +pub fn query_hatchers( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + let hatchers = cw_paginate_storage::paginate_map( + Deps { + storage: deps.storage, + api: deps.api, + querier: QuerierWrapper::new(deps.querier.deref()), + }, + &HATCHERS, + start_after + .map(|addr| deps.api.addr_validate(&addr)) + .transpose()? + .as_ref(), + limit, + Order::Descending, + )?; + + Ok(HatchersResponse { hatchers }) +} + +/// Query the contribution of a hatcher +pub fn query_hatcher(deps: Deps, addr: String) -> StdResult { + let addr = deps.api.addr_validate(&addr)?; + + HATCHERS.load(deps.storage, &addr) +} + +/// Query hatcher allowlist +pub fn query_hatcher_allowlist( + deps: Deps, + start_after: Option, + limit: Option, + config_type: Option, +) -> StdResult { + if hatcher_allowlist().is_empty(deps.storage) { + return Ok(HatcherAllowlistResponse { allowlist: None }); + } + + let binding = start_after + .map(|x| deps.api.addr_validate(&x)) + .transpose()?; + let start_after_bound = binding.as_ref().map(Bound::exclusive); + + let iter = match config_type { + Some(config_type) => hatcher_allowlist() + .idx + .config_type + .prefix(config_type.to_string()) + .range(deps.storage, start_after_bound, None, Order::Ascending), + None => hatcher_allowlist().range(deps.storage, start_after_bound, None, Order::Ascending), + } + .map(|result| result.map(|(addr, config)| HatcherAllowlistEntry { addr, config })); + + let allowlist = match limit { + Some(limit) => iter + .take(limit.try_into().unwrap()) + .collect::>(), + None => iter.collect::>(), + }?; + + Ok(HatcherAllowlistResponse { + allowlist: Some(allowlist), + }) +} + +/// Query the max supply of the supply token +pub fn query_max_supply(deps: Deps) -> StdResult { + let max_supply = MAX_SUPPLY.may_load(deps.storage)?; + Ok(max_supply.unwrap_or(Uint128::MAX)) +} + +/// Load and return the phase config +pub fn query_phase_config(deps: Deps) -> StdResult { + let phase = PHASE.load(deps.storage)?; + let phase_config = PHASE_CONFIG.load(deps.storage)?; + Ok(CommonsPhaseConfigResponse { + phase_config, + phase, + }) +} + +/// Get a buy quote +pub fn query_buy_quote(deps: Deps, payment: Uint128) -> StdResult { + let curve_type = CURVE_TYPE.load(deps.storage)?; + let curve_state = CURVE_STATE.load(deps.storage)?; + let phase_config = PHASE_CONFIG.load(deps.storage)?; + let phase = PHASE.load(deps.storage)?; + + calculate_buy_quote(payment, &curve_type, &curve_state, &phase, &phase_config) + .map_err(|e| StdError::generic_err(e.to_string())) +} + +/// Get a sell quote +pub fn query_sell_quote(deps: Deps, payment: Uint128) -> StdResult { + let curve_type = CURVE_TYPE.load(deps.storage)?; + let curve_state = CURVE_STATE.load(deps.storage)?; + let phase_config = PHASE_CONFIG.load(deps.storage)?; + let phase = PHASE.load(deps.storage)?; + + calculate_sell_quote(payment, &curve_type, &curve_state, &phase, &phase_config) + .map_err(|e| StdError::generic_err(e.to_string())) +} diff --git a/contracts/external/cw-abc/src/state.rs b/contracts/external/cw-abc/src/state.rs new file mode 100644 index 000000000..74393cf0a --- /dev/null +++ b/contracts/external/cw-abc/src/state.rs @@ -0,0 +1,145 @@ +use std::fmt::{self, Display}; + +use crate::abc::{CommonsPhase, CommonsPhaseConfig, CurveType, MinMax, SupplyToken}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Uint128, Uint64}; +use cw_curves::DecimalPlaces; +use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex}; + +/// Supply is dynamic and tracks the current supply of staked and ERC20 tokens. +#[cw_serde] +pub struct CurveState { + /// reserve is how many native tokens exist bonded to the validator + pub reserve: Uint128, + /// funding is how many native tokens exist unbonded and in the contract + pub funding: Uint128, + /// supply is how many tokens this contract has issued + pub supply: Uint128, + + /// the denom of the reserve token + pub reserve_denom: String, + + /// how to normalize reserve and supply + pub decimals: DecimalPlaces, +} + +impl CurveState { + pub fn new(reserve_denom: String, decimals: DecimalPlaces) -> Self { + CurveState { + reserve: Uint128::zero(), + funding: Uint128::zero(), + supply: Uint128::zero(), + reserve_denom, + decimals, + } + } +} + +/// The configuration for a member of the hatcher allowlist +#[cw_serde] +pub struct HatcherAllowlistConfig { + /// The type of the configuration + pub config_type: HatcherAllowlistConfigType, + /// An optional override of the hatch_config's contribution limit + pub contribution_limits_override: Option, + /// The height of the config insertion + /// For use when checking allowlist of DAO configs + pub config_height: u64, +} + +#[cw_serde] +pub struct HatcherAllowlistEntry { + pub addr: Addr, + pub config: HatcherAllowlistConfig, +} + +#[cw_serde] +pub enum HatcherAllowlistConfigType { + DAO { + /// The optional priority for checking a DAO config + /// None will append the item to the end of the priority queue (least priority) + priority: Option, + }, + Address {}, +} + +impl Copy for HatcherAllowlistConfigType {} + +impl Display for HatcherAllowlistConfigType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + HatcherAllowlistConfigType::DAO { priority: _ } => write!(f, "DAO"), + HatcherAllowlistConfigType::Address {} => write!(f, "Address"), + } + } +} + +pub struct HatcherAllowlistIndexes<'a> { + pub config_type: MultiIndex<'a, String, HatcherAllowlistConfig, &'a Addr>, +} + +impl<'a> IndexList for HatcherAllowlistIndexes<'a> { + fn get_indexes( + &'_ self, + ) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.config_type]; + Box::new(v.into_iter()) + } +} + +pub fn hatcher_allowlist<'a>( +) -> IndexedMap<'a, &'a Addr, HatcherAllowlistConfig, HatcherAllowlistIndexes<'a>> { + let indexes = HatcherAllowlistIndexes { + config_type: MultiIndex::new( + |_, x: &HatcherAllowlistConfig| x.config_type.to_string(), + "hatcher_allowlist", + "hatcher_allowlist__config_type", + ), + }; + + IndexedMap::new("hatcher_allowlist", indexes) +} + +/// The hatcher allowlist with configurations +pub const HATCHER_ALLOWLIST: Map<&Addr, HatcherAllowlistConfig> = Map::new("hatcher_allowlist"); + +/// The DAO portion of the hatcher allowlist implemented as a priority queue +/// If someone is a member of multiple allowlisted DAO's, we want to be able to control the checking order +pub const HATCHER_DAO_PRIORITY_QUEUE: Item> = + Item::new("HATCHER_DAO_PRIORITY_QUEUE"); + +/// The paused state for implementing a circuit breaker +pub const IS_PAUSED: Item = Item::new("is_paused"); + +pub const CURVE_STATE: Item = Item::new("curve_state"); + +pub const CURVE_TYPE: Item = Item::new("curve_type"); + +/// The address for automatically forwarding funding pool gains +pub const FUNDING_POOL_FORWARDING: Item = Item::new("funding_pool_forwarding"); + +/// The denom used for the supply token +pub const SUPPLY_DENOM: Item = Item::new("denom"); + +/// The maximum supply of the supply token, new tokens cannot be minted beyond this cap +pub const MAX_SUPPLY: Item = Item::new("max_supply"); + +/// Keep track of who has contributed to the hatch phase +/// TODO: cw-set? This should be a map because in the open-phase we need to be able +/// to ascertain the amount contributed by a user +pub static HATCHERS: Map<&Addr, Uint128> = Map::new("hatchers"); + +/// Keep track of the donated amounts per user +pub static DONATIONS: Map<&Addr, Uint128> = Map::new("donations"); + +/// The phase configuration of the Augmented Bonding Curve +pub static PHASE_CONFIG: Item = Item::new("phase_config"); + +/// The phase state of the Augmented Bonding Curve +pub static PHASE: Item = Item::new("phase"); + +/// Temporarily holds the supply config when creating a new Token Factory denom +pub const TEMP_SUPPLY: Item = Item::new("temp_supply"); + +/// The address of the cw-tokenfactory-issuer contract +pub const TOKEN_ISSUER_CONTRACT: Item = Item::new("token_issuer_contract"); diff --git a/contracts/external/cw-abc/src/test_tube/integration_tests.rs b/contracts/external/cw-abc/src/test_tube/integration_tests.rs new file mode 100644 index 000000000..290f1efcc --- /dev/null +++ b/contracts/external/cw-abc/src/test_tube/integration_tests.rs @@ -0,0 +1,779 @@ +use crate::{ + abc::{ + ClosedConfig, CommonsPhase, CommonsPhaseConfig, CurveType, HatchConfig, MinMax, OpenConfig, + ReserveToken, SupplyToken, + }, + msg::{ + CommonsPhaseConfigResponse, CurveInfoResponse, DenomResponse, ExecuteMsg, + HatcherAllowlistConfigMsg, HatcherAllowlistEntryMsg, InstantiateMsg, QueryMsg, + QuoteResponse, + }, + state::HatcherAllowlistConfigType, + ContractError, +}; + +use super::test_env::{TestEnv, TestEnvBuilder, DENOM, RESERVE}; + +use cosmwasm_std::{coins, Decimal, Uint128, Uint64}; +use cw_tokenfactory_issuer::msg::QueryMsg as IssuerQueryMsg; +use osmosis_std::types::cosmos::bank::v1beta1::QueryBalanceRequest; +use osmosis_test_tube::{osmosis_std::types::cosmos::base::v1beta1::Coin, Account, OsmosisTestApp}; + +#[test] +fn test_happy_path() { + let app = OsmosisTestApp::new(); + let builder = TestEnvBuilder::new(); + let env = builder.default_setup(&app); + let TestEnv { + ref abc, + ref accounts, + ref tf_issuer, + .. + } = env; + + // Query buy quote + let quote = abc + .query::(&QueryMsg::BuyQuote { + payment: Uint128::new(1000u128), + }) + .unwrap(); + assert_eq!( + quote, + QuoteResponse { + new_reserve: Uint128::new(900u128), + funded: Uint128::new(100u128), + amount: Uint128::new(9000u128), + new_supply: Uint128::new(9000u128), + } + ); + + // Buy tokens + abc.execute(&ExecuteMsg::Buy {}, &coins(1000, RESERVE), &accounts[0]) + .unwrap(); + + // Query denom + let denom = tf_issuer + .query::(&IssuerQueryMsg::Denom {}) + .unwrap() + .denom; + + // Query balances + let user_balance = env + .bank() + .query_balance(&QueryBalanceRequest { + address: accounts[0].address(), + denom: denom.clone(), + }) + .unwrap(); + let contract_balance = env + .bank() + .query_balance(&QueryBalanceRequest { + address: abc.contract_addr.to_string(), + denom: RESERVE.to_string(), + }) + .unwrap(); + + // Check balances + assert_eq!( + user_balance.balance, + Some(Coin { + denom: denom.clone(), + amount: "9000".to_string(), + }) + ); + assert_eq!( + contract_balance.balance, + Some(Coin { + denom: RESERVE.to_string(), + amount: "900".to_string(), // Minus 10% to fees_recipient + }) + ); + + // Query curve + let curve_info: CurveInfoResponse = abc.query(&QueryMsg::CurveInfo {}).unwrap(); + assert_eq!( + curve_info, + CurveInfoResponse { + reserve: Uint128::new(900), + supply: Uint128::new(9000), + funding: Uint128::new(0), + spot_price: Decimal::percent(10u64), + reserve_denom: RESERVE.to_string(), + } + ); + + // Query phase + let phase: CommonsPhaseConfigResponse = abc.query(&QueryMsg::PhaseConfig {}).unwrap(); + assert!(matches!(phase.phase, CommonsPhase::Hatch)); + assert_eq!( + phase.phase_config, + CommonsPhaseConfig { + hatch: HatchConfig { + contribution_limits: MinMax { + min: Uint128::from(10u128), + max: Uint128::from(1000000u128), + }, + initial_raise: MinMax { + min: Uint128::from(10u128), + max: Uint128::from(900_000u128), + }, + entry_fee: Decimal::percent(10u64), + }, + open: OpenConfig { + entry_fee: Decimal::percent(10u64), + exit_fee: Decimal::percent(10u64), + }, + closed: ClosedConfig {}, + } + ); + + // Trying to sell is an error + let err = abc + .execute( + &ExecuteMsg::Sell {}, + &coins(1000, denom.clone()), + &accounts[0], + ) + .unwrap_err(); + assert_eq!(err, abc.execute_error(ContractError::CommonsHatch {})); + + // Buy enough tokens to end the hatch phase + abc.execute(&ExecuteMsg::Buy {}, &coins(999999, RESERVE), &accounts[1]) + .unwrap(); + + // Contract is now in open phase + let phase: CommonsPhaseConfigResponse = abc.query(&QueryMsg::PhaseConfig {}).unwrap(); + assert_eq!(phase.phase, CommonsPhase::Open); + + // Query sell quote + let quote = abc + .query::(&QueryMsg::SellQuote { + payment: Uint128::new(1000u128), + }) + .unwrap(); + assert_eq!( + quote, + QuoteResponse { + new_reserve: Uint128::new(900800u128), + funded: Uint128::new(10u128), + amount: Uint128::new(90u128), + new_supply: Uint128::new(9008000u128), + } + ); + + // Sell + abc.execute( + &ExecuteMsg::Sell {}, + &coins(1000, denom.clone()), + &accounts[0], + ) + .unwrap(); + + // Query curve + let curve_info: CurveInfoResponse = abc.query(&QueryMsg::CurveInfo {}).unwrap(); + assert_eq!( + curve_info, + CurveInfoResponse { + reserve: Uint128::new(900800u128), + supply: Uint128::new(9008000u128), + funding: Uint128::new(0), + spot_price: Decimal::percent(10u64), + reserve_denom: RESERVE.to_string(), + } + ); + + // Query balances + let user_balance = env + .bank() + .query_balance(&QueryBalanceRequest { + address: accounts[0].address(), + denom: denom.clone(), + }) + .unwrap(); + let contract_balance = env + .bank() + .query_balance(&QueryBalanceRequest { + address: abc.contract_addr.to_string(), + denom: RESERVE.to_string(), + }) + .unwrap(); + + // Check balances + assert_eq!( + user_balance.balance, + Some(Coin { + denom: denom.clone(), + amount: "8000".to_string(), + }) + ); + assert_eq!( + contract_balance.balance, + Some(Coin { + denom: RESERVE.to_string(), + amount: "900800".to_string(), + }) + ); +} + +#[test] +fn test_contribution_limits_enforced() { + let app = OsmosisTestApp::new(); + let builder = TestEnvBuilder::new(); + let env = builder.default_setup(&app); + let TestEnv { + ref abc, + ref accounts, + .. + } = env; + + // Buy more tokens then the max contribution limit errors + let err = abc + .execute( + &ExecuteMsg::Buy {}, + &coins(1_000_000_000, RESERVE), + &accounts[0], + ) + .unwrap_err(); + assert_eq!( + err, + abc.execute_error(ContractError::ContributionLimit { + min: Uint128::from(10u128), + max: Uint128::from(1000000u128), + }) + ); + + // Buy less tokens then the min contribution limit errors + let err = abc + .execute(&ExecuteMsg::Buy {}, &coins(1, RESERVE), &accounts[0]) + .unwrap_err(); + + assert_eq!( + err, + abc.execute_error(ContractError::ContributionLimit { + min: Uint128::from(10u128), + max: Uint128::from(1000000u128), + }) + ); +} + +#[test] +fn test_max_supply() { + let app = OsmosisTestApp::new(); + let builder = TestEnvBuilder::new(); + let env = builder.default_setup(&app); + let TestEnv { + ref abc, + ref accounts, + .. + } = env; + + // Buy enough tokens to end the hatch phase + abc.execute( + &ExecuteMsg::Buy {}, + &coins(1_000_000, RESERVE), + &accounts[0], + ) + .unwrap(); + + // Buy enough tokens to trigger a max supply error + let err = abc + .execute( + &ExecuteMsg::Buy {}, + &coins(10000000000000, RESERVE), + &accounts[0], + ) + .unwrap_err(); + assert_eq!( + err, + abc.execute_error(ContractError::CannotExceedMaxSupply { + max: Uint128::from(1000000000u128) + }) + ); + + // Only owner can update the max supply + let err = abc + .execute( + &ExecuteMsg::UpdateMaxSupply { max_supply: None }, + &[], + &accounts[1], + ) + .unwrap_err(); + assert_eq!( + err, + abc.execute_error(ContractError::Ownership( + cw_ownable::OwnershipError::NotOwner + )) + ); + + // Update the max supply to no limit + abc.execute( + &ExecuteMsg::UpdateMaxSupply { max_supply: None }, + &[], + &accounts[0], + ) + .unwrap(); + + // Purchase large amount of coins succeeds + abc.execute( + &ExecuteMsg::Buy {}, + &coins(10000000000000, RESERVE), + &accounts[0], + ) + .unwrap(); +} + +#[test] +fn test_allowlist() { + let app = OsmosisTestApp::new(); + let builder = TestEnvBuilder::new(); + let instantiate_msg = InstantiateMsg { + token_issuer_code_id: 0, + funding_pool_forwarding: Some("replaced to accounts[0]".to_string()), + supply: SupplyToken { + subdenom: DENOM.to_string(), + metadata: None, + decimals: 6, + max_supply: Some(Uint128::from(1000000000u128)), + }, + reserve: ReserveToken { + denom: RESERVE.to_string(), + decimals: 6, + }, + phase_config: CommonsPhaseConfig { + hatch: HatchConfig { + contribution_limits: MinMax { + min: Uint128::from(10u128), + max: Uint128::from(1000000u128), + }, + initial_raise: MinMax { + min: Uint128::from(10u128), + max: Uint128::from(1000000u128), + }, + entry_fee: Decimal::percent(10u64), + }, + open: OpenConfig { + entry_fee: Decimal::percent(10u64), + exit_fee: Decimal::percent(10u64), + }, + closed: ClosedConfig {}, + }, + hatcher_allowlist: Some(vec![HatcherAllowlistEntryMsg { + addr: "replaced to accounts[9]".to_string(), + config: HatcherAllowlistConfigMsg { + config_type: HatcherAllowlistConfigType::Address {}, + contribution_limits_override: None, + }, + }]), + curve_type: CurveType::Constant { + value: Uint128::one(), + scale: 1, + }, + }; + let env = builder.setup(&app, instantiate_msg).unwrap(); + let TestEnv { + ref abc, + ref accounts, + .. + } = env; + + // Only owner can update hatch list + let err = abc + .execute( + &ExecuteMsg::UpdateHatchAllowlist { + to_add: vec![ + HatcherAllowlistEntryMsg { + addr: accounts[0].address(), + config: HatcherAllowlistConfigMsg { + config_type: HatcherAllowlistConfigType::Address {}, + contribution_limits_override: None, + }, + }, + HatcherAllowlistEntryMsg { + addr: accounts[1].address(), + config: HatcherAllowlistConfigMsg { + config_type: HatcherAllowlistConfigType::Address {}, + contribution_limits_override: None, + }, + }, + ], + to_remove: vec![], + }, + &[], + &accounts[1], + ) + .unwrap_err(); + assert_eq!( + err, + abc.execute_error(ContractError::Ownership( + cw_ownable::OwnershipError::NotOwner + )) + ); + + // Update the allowlist + abc.execute( + &ExecuteMsg::UpdateHatchAllowlist { + to_add: vec![ + HatcherAllowlistEntryMsg { + addr: accounts[0].address(), + config: HatcherAllowlistConfigMsg { + config_type: HatcherAllowlistConfigType::Address {}, + contribution_limits_override: None, + }, + }, + HatcherAllowlistEntryMsg { + addr: accounts[1].address(), + config: HatcherAllowlistConfigMsg { + config_type: HatcherAllowlistConfigType::Address {}, + contribution_limits_override: None, + }, + }, + ], + to_remove: vec![], + }, + &[], + &accounts[0], + ) + .unwrap(); + + // Account not on the hatch allowlist can't purchase + let err = abc + .execute(&ExecuteMsg::Buy {}, &coins(1000, RESERVE), &accounts[3]) + .unwrap_err(); + assert_eq!( + err, + abc.execute_error(ContractError::SenderNotAllowlisted { + sender: accounts[3].address() + }) + ); + + // Account on allowlist can purchase + abc.execute(&ExecuteMsg::Buy {}, &coins(1000, RESERVE), &accounts[1]) + .unwrap(); +} + +#[test] +fn test_close_curve() { + let app = OsmosisTestApp::new(); + let builder = TestEnvBuilder::new(); + let env = builder.default_setup(&app); + let TestEnv { + ref abc, + ref accounts, + ref tf_issuer, + .. + } = env; + + // Query denom + let denom = tf_issuer + .query::(&IssuerQueryMsg::Denom {}) + .unwrap() + .denom; + + // Buy enough tokens to end the hatch phase + abc.execute(&ExecuteMsg::Buy {}, &coins(1000000, RESERVE), &accounts[0]) + .unwrap(); + + // Only owner can close the curve + let err = abc + .execute(&ExecuteMsg::Close {}, &[], &accounts[1]) + .unwrap_err(); + assert_eq!( + err, + abc.execute_error(ContractError::Ownership( + cw_ownable::OwnershipError::NotOwner + )) + ); + + // Owner closes curve + abc.execute(&ExecuteMsg::Close {}, &[], &accounts[0]) + .unwrap(); + + // Can no longer buy + let err = abc + .execute(&ExecuteMsg::Buy {}, &coins(1000, RESERVE), &accounts[0]) + .unwrap_err(); + assert_eq!(err, abc.execute_error(ContractError::CommonsClosed {})); + + // Can sell + abc.execute(&ExecuteMsg::Sell {}, &coins(100, denom), &accounts[0]) + .unwrap(); +} + +// TODO maybe we don't allow for updating the curve in the MVP as it could lead +// to weird edge cases? +#[test] +fn test_update_curve() { + let app = OsmosisTestApp::new(); + let builder = TestEnvBuilder::new(); + let env = builder.default_setup(&app); + let TestEnv { + ref abc, + ref accounts, + ref tf_issuer, + .. + } = env; + + // Query denom + let denom = tf_issuer + .query::(&IssuerQueryMsg::Denom {}) + .unwrap() + .denom; + + // Buy enough tokens to end the hatch phase + abc.execute( + &ExecuteMsg::Buy {}, + &coins(1_000_000, RESERVE), + &accounts[0], + ) + .unwrap(); + + // Only owner can update the curve + let err = abc + .execute( + &ExecuteMsg::UpdateCurve { + curve_type: CurveType::Linear { + slope: Uint128::new(2), + scale: 5, + }, + }, + &[], + &accounts[1], + ) + .unwrap_err(); + assert_eq!( + err, + abc.execute_error(ContractError::Ownership( + cw_ownable::OwnershipError::NotOwner + )) + ); + + // Owner updates curve + abc.execute( + &ExecuteMsg::UpdateCurve { + curve_type: CurveType::Linear { + slope: Uint128::new(2), + scale: 5, + }, + }, + &[], + &accounts[0], + ) + .unwrap(); + + // All tokens are sold successfully + let user_balance = env + .bank() + .query_balance(&QueryBalanceRequest { + address: accounts[0].address(), + denom: denom.clone(), + }) + .unwrap(); + assert_eq!( + user_balance.balance, + Some(Coin { + denom: denom.clone(), + amount: "9000000".to_string(), + }) + ); + + abc.execute( + &ExecuteMsg::Sell {}, + &coins(9000000, denom.clone()), + &accounts[0], + ) + .unwrap(); + + // No money is left over in the contract + let contract_balance = env + .bank() + .query_balance(&QueryBalanceRequest { + address: abc.contract_addr.to_string(), + denom: RESERVE.to_string(), + }) + .unwrap(); + assert_eq!( + contract_balance.balance, + Some(Coin { + denom: RESERVE.to_string(), + amount: "0".to_string(), + }) + ); +} + +#[test] +fn test_dao_hatcher() { + let app = OsmosisTestApp::new(); + let builder = TestEnvBuilder::new(); + let env = builder.default_setup(&app); + let TestEnv { + ref abc, + ref accounts, + .. + } = env; + + // Setup a dao with the 1st half of accounts + let dao_ids = env.init_dao_ids(); + let daos: Vec<_> = (0..5) + .into_iter() + .map(|_| env.setup_default_dao(dao_ids)) + .collect(); + app.increase_time(1u64); + + // Update hatcher allowlist for DAO membership + // The max contribution of 50 should have the highest priority + for (i, dao) in daos.iter().enumerate() { + let result = abc.execute( + &ExecuteMsg::UpdateHatchAllowlist { + to_add: vec![HatcherAllowlistEntryMsg { + addr: dao.contract_addr.to_string(), + config: HatcherAllowlistConfigMsg { + config_type: HatcherAllowlistConfigType::DAO { + priority: Some(Uint64::MAX - Uint64::new(i as u64)), // Insert in reverse priority to ensure insertion ordering is valid + }, + contribution_limits_override: Some(MinMax { + min: Uint128::one(), + max: Uint128::from(10u128) * Uint128::from(i as u128 + 1u128), + }), + }, + }], + to_remove: vec![], + }, + &[], + &accounts[0], + ); + assert!(result.is_ok()); + } + + // Let's also insert a dao with no priority to make sure it's added to the end + let dao = env.setup_default_dao(dao_ids); + let result = abc.execute( + &ExecuteMsg::UpdateHatchAllowlist { + to_add: vec![HatcherAllowlistEntryMsg { + addr: dao.contract_addr.to_string(), + config: HatcherAllowlistConfigMsg { + config_type: HatcherAllowlistConfigType::DAO { priority: None }, + contribution_limits_override: Some(MinMax { + min: Uint128::one(), + max: Uint128::from(100u128), + }), + }, + }], + to_remove: vec![], + }, + &[], + &accounts[0], + ); + assert!(result.is_ok()); + + // Also add a DAO tied for the highest priority + // This should not update contribution limit, because the 1st DAO was added first and user is a member of it + let dao = env.setup_default_dao(dao_ids); + let result = abc.execute( + &ExecuteMsg::UpdateHatchAllowlist { + to_add: vec![HatcherAllowlistEntryMsg { + addr: dao.contract_addr.to_string(), + config: HatcherAllowlistConfigMsg { + config_type: HatcherAllowlistConfigType::DAO { + priority: Some(Uint64::MAX - Uint64::from(4u64)), + }, + contribution_limits_override: Some(MinMax { + min: Uint128::one(), + max: Uint128::from(1000u128), + }), + }, + }], + to_remove: vec![], + }, + &[], + &accounts[0], + ); + assert!(result.is_ok()); + + // Check contribution limit at this point + let err = abc + .execute(&ExecuteMsg::Buy {}, &coins(1000, RESERVE), &accounts[0]) + .unwrap_err(); + assert_eq!( + err, + abc.execute_error(ContractError::ContributionLimit { + min: Uint128::one(), + max: Uint128::from(50u128) + }) + ); + + // Check removing a dao config updates the contribution limit + let result = abc.execute( + &ExecuteMsg::UpdateHatchAllowlist { + to_add: vec![], + to_remove: vec![daos.last().unwrap().contract_addr.to_string()], + }, + &[], + &accounts[0], + ); + assert!(result.is_ok()); + + // The error should say 1k is the max contribution now + let err = abc + .execute(&ExecuteMsg::Buy {}, &coins(2000, RESERVE), &accounts[0]) + .unwrap_err(); + assert_eq!( + err, + abc.execute_error(ContractError::ContributionLimit { + min: Uint128::one(), + max: Uint128::from(1000u128) + }) + ); + + // Adhering to the limit makes this ok now + let result = abc.execute(&ExecuteMsg::Buy {}, &coins(40, RESERVE), &accounts[0]); + assert!(result.is_ok()); + + // Check not allowlisted + let result = abc.execute( + &ExecuteMsg::Buy {}, + &coins(1000, RESERVE), + &accounts[accounts.len() - 1], + ); + assert_eq!( + result.unwrap_err(), + abc.execute_error(ContractError::SenderNotAllowlisted { + sender: accounts[accounts.len() - 1].address().to_string() + }) + ); + + // Check an address config takes complete priority + let result = abc.execute( + &ExecuteMsg::UpdateHatchAllowlist { + to_add: vec![HatcherAllowlistEntryMsg { + addr: accounts[0].address(), + config: HatcherAllowlistConfigMsg { + config_type: HatcherAllowlistConfigType::Address {}, + contribution_limits_override: Some(MinMax { + min: Uint128::one(), + max: Uint128::from(2000u128), + }), + }, + }], + to_remove: vec![], + }, + &[], + &accounts[0], + ); + assert!(result.is_ok()); + + // The user has already funded 40, so providing their limit should error + let err = abc + .execute(&ExecuteMsg::Buy {}, &coins(2000, RESERVE), &accounts[0]) + .unwrap_err(); + assert_eq!( + err, + abc.execute_error(ContractError::ContributionLimit { + min: Uint128::one(), + max: Uint128::from(2000u128) + }) + ); + + // Funding the remainder is ok + let result = abc.execute(&ExecuteMsg::Buy {}, &coins(1960, RESERVE), &accounts[0]); + assert!(result.is_ok()); +} diff --git a/contracts/external/cw-abc/src/test_tube/mod.rs b/contracts/external/cw-abc/src/test_tube/mod.rs new file mode 100644 index 000000000..eb0b4f91b --- /dev/null +++ b/contracts/external/cw-abc/src/test_tube/mod.rs @@ -0,0 +1,2 @@ +mod integration_tests; +mod test_env; diff --git a/contracts/external/cw-abc/src/test_tube/test_env.rs b/contracts/external/cw-abc/src/test_tube/test_env.rs new file mode 100644 index 000000000..87cae932b --- /dev/null +++ b/contracts/external/cw-abc/src/test_tube/test_env.rs @@ -0,0 +1,515 @@ +// The code is used in tests but reported as dead code +// see https://github.com/rust-lang/rust/issues/46379 +#![allow(dead_code)] + +use crate::{ + abc::{ + ClosedConfig, CommonsPhaseConfig, CurveType, HatchConfig, MinMax, OpenConfig, ReserveToken, + SupplyToken, + }, + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + ContractError, +}; + +use cosmwasm_std::{to_json_binary, Addr, Coin, Decimal, Uint128}; +use cw_utils::Duration; +use dao_interface::{ + state::{Admin, ModuleInstantiateInfo}, + token::{DenomUnit, InitialBalance, NewDenomMetadata, NewTokenInfo}, + voting::DenomResponse, +}; +use dao_testing::test_tube::{ + cw_tokenfactory_issuer::TokenfactoryIssuer, dao_dao_core::DaoCore, + dao_proposal_single::DaoProposalSingle, dao_voting_token_staked::TokenVotingContract, +}; +use dao_voting::{ + pre_propose::PreProposeInfo, + threshold::{ActiveThreshold, PercentageThreshold, Threshold}, +}; +use dao_voting_token_staked::msg::TokenInfo; +use osmosis_test_tube::{ + osmosis_std::types::{ + cosmos::bank::v1beta1::QueryAllBalancesRequest, + cosmwasm::wasm::v1::MsgExecuteContractResponse, + }, + Account, Bank, Module, OsmosisTestApp, RunnerError, RunnerExecuteResult, RunnerResult, + SigningAccount, Wasm, +}; +use serde::de::DeserializeOwned; +use std::fmt::Debug; +use std::path::PathBuf; + +pub const DENOM: &str = "ucat"; + +// Needs to match what's configured for test-tube +pub const RESERVE: &str = "uosmo"; + +pub struct TestEnv<'a> { + pub app: &'a OsmosisTestApp, + pub abc: CwAbc<'a>, + pub tf_issuer: TokenfactoryIssuer<'a>, + pub accounts: Vec, +} + +impl<'a> TestEnv<'a> { + pub fn instantiate( + &self, + msg: &InstantiateMsg, + signer: SigningAccount, + ) -> Result { + CwAbc::<'a>::instantiate(self.app, self.abc.code_id, msg, &signer) + } + + pub fn get_tf_issuer_code_id(&self) -> u64 { + self.tf_issuer.code_id + } + + pub fn bank(&self) -> Bank<'_, OsmosisTestApp> { + Bank::new(self.app) + } + + pub fn init_dao_ids(&self) -> (u64, u64) { + ( + TokenVotingContract::upload(self.app, &self.accounts[0]).unwrap(), + DaoProposalSingle::upload(self.app, &self.accounts[0]).unwrap(), + ) + } + + pub fn setup_default_dao(&self, dao_ids: (u64, u64)) -> DaoCore<'a> { + // Only the 1st half of self.accounts are part of the DAO + let initial_balances: Vec = self + .accounts + .iter() + .take(self.accounts.len() / 2) + .map(|acc| InitialBalance { + address: acc.address(), + amount: Uint128::from(100u128), + }) + .collect(); + + let msg = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that makes DAO tooling".to_string(), + image_url: None, + automatically_add_cw20s: false, + automatically_add_cw721s: false, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: dao_ids.0, + msg: to_json_binary(&dao_voting_token_staked::msg::InstantiateMsg { + token_info: TokenInfo::New(NewTokenInfo { + token_issuer_code_id: self.tf_issuer.code_id, + subdenom: DENOM.to_string(), + metadata: Some(NewDenomMetadata { + description: "Awesome token, get it meow!".to_string(), + additional_denom_units: Some(vec![DenomUnit { + denom: "cat".to_string(), + exponent: 6, + aliases: vec![], + }]), + display: "cat".to_string(), + name: "Cat Token".to_string(), + symbol: "CAT".to_string(), + }), + initial_balances, + initial_dao_balance: Some(Uint128::new(900)), + }), + unstaking_duration: Some(Duration::Time(2)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(75), + }), + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Voting Module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: dao_ids.1, + msg: to_json_binary(&dao_proposal_single::msg::InstantiateMsg { + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Majority {}, + quorum: PercentageThreshold::Percent(Decimal::percent(35)), + }, + max_voting_period: Duration::Time(432000), + allow_revoting: false, + only_members_execute: true, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Proposal Module".to_string(), + }], + initial_items: None, + }; + + let dao = DaoCore::new(self.app, &msg, &self.accounts[0], &[]).unwrap(); + + // Get voting module address, setup vp_contract helper + let vp_addr: Addr = dao + .query(&dao_interface::msg::QueryMsg::VotingModule {}) + .unwrap(); + let vp_contract = + TokenVotingContract::new_with_values(self.app, dao_ids.0, vp_addr.to_string()).unwrap(); + + // Get the denom + let result: RunnerResult = + vp_contract.query(&dao_voting_token_staked::msg::QueryMsg::Denom {}); + let denom = result.unwrap().denom; + + // Stake all members + for acc in self.accounts.iter().take(self.accounts.len() / 2) { + vp_contract + .execute( + &dao_voting_token_staked::msg::ExecuteMsg::Stake {}, + &[Coin::new(100, denom.clone())], + acc, + ) + .unwrap(); + } + + dao + } + + pub fn assert_account_balances( + &self, + account: SigningAccount, + expected_balances: Vec, + ignore_denoms: Vec<&str>, + ) { + let account_balances: Vec = Bank::new(self.app) + .query_all_balances(&QueryAllBalancesRequest { + address: account.address(), + pagination: None, + }) + .unwrap() + .balances + .into_iter() + .map(|coin| Coin::new(coin.amount.parse().unwrap(), coin.denom)) + .filter(|coin| !ignore_denoms.contains(&coin.denom.as_str())) + .collect(); + + assert_eq!(account_balances, expected_balances); + } + + pub fn assert_contract_balances(&self, expected_balances: &[Coin]) { + let contract_balances: Vec = Bank::new(self.app) + .query_all_balances(&QueryAllBalancesRequest { + address: self.abc.contract_addr.clone(), + pagination: None, + }) + .unwrap() + .balances + .into_iter() + .map(|coin| Coin::new(coin.amount.parse().unwrap(), coin.denom)) + .collect(); + + assert_eq!(contract_balances, expected_balances); + } +} + +pub struct TestEnvBuilder {} + +impl TestEnvBuilder { + pub fn new() -> Self { + Self {} + } + + pub fn default_setup(self, app: &'_ OsmosisTestApp) -> TestEnv<'_> { + let accounts = app + .init_accounts(&[Coin::new(1000000000000000u128, RESERVE)], 10) + .unwrap(); + + let issuer_id = TokenfactoryIssuer::upload(app, &accounts[0]).unwrap(); + + let abc = CwAbc::deploy( + app, + &InstantiateMsg { + token_issuer_code_id: issuer_id, + funding_pool_forwarding: Some(accounts[0].address()), + supply: SupplyToken { + subdenom: DENOM.to_string(), + metadata: Some(NewDenomMetadata { + description: "Awesome token, get it meow!".to_string(), + additional_denom_units: Some(vec![DenomUnit { + denom: "cat".to_string(), + exponent: 6, + aliases: vec![], + }]), + display: "cat".to_string(), + name: "Cat Token".to_string(), + symbol: "CAT".to_string(), + }), + decimals: 6, + max_supply: Some(Uint128::from(1_000_000_000u128)), + }, + reserve: ReserveToken { + denom: RESERVE.to_string(), + decimals: 6, + }, + phase_config: CommonsPhaseConfig { + hatch: HatchConfig { + contribution_limits: MinMax { + min: Uint128::from(10u128), + max: Uint128::from(1_000_000u128), + }, + initial_raise: MinMax { + min: Uint128::from(10u128), + max: Uint128::from(900_000u128), // 1m - 10% + }, + entry_fee: Decimal::percent(10u64), + }, + open: OpenConfig { + entry_fee: Decimal::percent(10u64), + exit_fee: Decimal::percent(10u64), + }, + closed: ClosedConfig {}, + }, + hatcher_allowlist: None, + curve_type: CurveType::Constant { + value: Uint128::one(), + scale: 1, + }, + }, + &accounts[0], + ) + .unwrap(); + + let issuer_addr = CwAbc::query(&abc, &QueryMsg::TokenContract {}).unwrap(); + + let tf_issuer = TokenfactoryIssuer::new_with_values(app, issuer_id, issuer_addr).unwrap(); + + TestEnv { + app, + abc, + tf_issuer, + accounts, + } + } + + pub fn setup( + self, + app: &'_ OsmosisTestApp, + mut msg: InstantiateMsg, + ) -> Result, RunnerError> { + let accounts = app + .init_accounts(&[Coin::new(1000000000000000u128, RESERVE)], 10) + .unwrap(); + + let issuer_id = TokenfactoryIssuer::upload(app, &accounts[0])?; + + msg.token_issuer_code_id = issuer_id; + + msg.funding_pool_forwarding = Some(accounts[0].address()); + + if let Some(allowlist) = msg.hatcher_allowlist.as_mut() { + for member in allowlist { + member.addr = accounts[9].address(); + } + } + + let abc = CwAbc::deploy(app, &msg, &accounts[0])?; + + let issuer_addr = CwAbc::query(&abc, &QueryMsg::TokenContract {})?; + + let tf_issuer = TokenfactoryIssuer::new_with_values(app, issuer_id, issuer_addr)?; + + Ok(TestEnv { + app, + abc, + tf_issuer, + accounts, + }) + } + + pub fn upload_issuer(self, app: &'_ OsmosisTestApp, signer: &SigningAccount) -> u64 { + TokenfactoryIssuer::upload(app, signer).unwrap() + } +} + +pub fn assert_contract_err(expected: ContractError, actual: RunnerError) { + match actual { + RunnerError::ExecuteError { msg } => { + if !msg.contains(&expected.to_string()) { + panic!( + "assertion failed:\n\n must contain \t: \"{}\",\n actual \t: \"{}\"\n", + expected, msg + ) + } + } + _ => panic!("unexpected error, expect execute error but got: {}", actual), + }; +} + +#[derive(Debug)] +pub struct CwAbc<'a> { + pub app: &'a OsmosisTestApp, + pub code_id: u64, + pub contract_addr: String, +} + +impl<'a> CwAbc<'a> { + pub fn deploy( + app: &'a OsmosisTestApp, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + ) -> Result { + let wasm = Wasm::new(app); + let token_creation_fee = Coin::new(10000000, RESERVE); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + let contract_addr = wasm + .instantiate( + code_id, + &instantiate_msg, + Some(&signer.address()), + None, + &[token_creation_fee], + signer, + )? + .data + .address; + + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + pub fn new_with_values( + app: &'a OsmosisTestApp, + code_id: u64, + contract_addr: String, + ) -> Result { + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + /// uploads contract and returns a code ID + pub fn upload(app: &OsmosisTestApp, signer: &SigningAccount) -> Result { + let wasm = Wasm::new(app); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + Ok(code_id) + } + + pub fn instantiate( + app: &'a OsmosisTestApp, + code_id: u64, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + ) -> Result { + let wasm = Wasm::new(app); + let contract_addr = wasm + .instantiate( + code_id, + &instantiate_msg, + Some(&signer.address()), + None, + &[], + signer, + )? + .data + .address; + + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + // executes + pub fn execute( + &self, + execute_msg: &ExecuteMsg, + funds: &[Coin], + signer: &SigningAccount, + ) -> RunnerExecuteResult { + let wasm = Wasm::new(self.app); + wasm.execute(&self.contract_addr, execute_msg, funds, signer) + } + + // queries + pub fn query(&self, query_msg: &QueryMsg) -> Result + where + T: DeserializeOwned, + { + let wasm = Wasm::new(self.app); + wasm.query(&self.contract_addr, query_msg) + } + + // pub fn migrate( + // &self, + // testdata: &str, + // signer: &SigningAccount, + // ) -> RunnerExecuteResult { + // let wasm = Wasm::new(self.app); + // let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + // let wasm_byte_code = + // std::fs::read(manifest_path.join("tests").join("testdata").join(testdata)).unwrap(); + + // let code_id = wasm.store_code(&wasm_byte_code, None, signer)?.data.code_id; + // self.app.execute( + // MsgMigrateContract { + // sender: signer.address(), + // contract: self.contract_addr.clone(), + // code_id, + // msg: serde_json::to_vec(&MigrateMsg {}).unwrap(), + // }, + // "/cosmwasm.wasm.v1.MsgMigrateContract", + // signer, + // ) + // } + + fn get_wasm_byte_code() -> Vec { + let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let byte_code = std::fs::read( + manifest_path + .join("..") + .join("..") + .join("..") + .join("artifacts") + .join("cw_abc.wasm"), + ); + match byte_code { + Ok(byte_code) => byte_code, + // On arm processors, the above path is not found, so we try the following path + Err(_) => std::fs::read( + manifest_path + .join("..") + .join("..") + .join("..") + .join("artifacts") + .join("cw_abc-aarch64.wasm"), + ) + .unwrap(), + } + } + + pub fn execute_error(&self, err: ContractError) -> RunnerError { + RunnerError::ExecuteError { + msg: format!( + "failed to execute message; message index: 0: {}: execute wasm contract failed", + err + ), + } + } +} diff --git a/contracts/external/cw-abc/src/testing.rs b/contracts/external/cw-abc/src/testing.rs new file mode 100644 index 000000000..4caa65889 --- /dev/null +++ b/contracts/external/cw-abc/src/testing.rs @@ -0,0 +1,84 @@ +use cosmwasm_std::{ + testing::{mock_env, mock_info}, + Decimal, DepsMut, Response, Uint128, +}; +use dao_interface::token::NewDenomMetadata; + +use crate::contract; +use crate::msg::InstantiateMsg; +use crate::{ + abc::{ + ClosedConfig, CommonsPhaseConfig, CurveType, HatchConfig, MinMax, OpenConfig, ReserveToken, + SupplyToken, + }, + ContractError, +}; + +pub(crate) mod prelude { + pub use super::{default_instantiate_msg, TEST_RESERVE_DENOM}; + pub use speculoos::prelude::*; +} + +pub const TEST_RESERVE_DENOM: &str = "satoshi"; +pub const TEST_CREATOR: &str = "creator"; +pub const _TEST_INVESTOR: &str = "investor"; +pub const _TEST_BUYER: &str = "buyer"; + +pub const TEST_SUPPLY_DENOM: &str = "subdenom"; + +pub fn default_supply_metadata() -> NewDenomMetadata { + NewDenomMetadata { + name: "Bonded".to_string(), + symbol: "EPOXY".to_string(), + description: "Forever".to_string(), + display: "EPOXY".to_string(), + additional_denom_units: None, + } +} + +pub fn default_instantiate_msg( + decimals: u8, + reserve_decimals: u8, + curve_type: CurveType, +) -> InstantiateMsg { + InstantiateMsg { + token_issuer_code_id: 1, + funding_pool_forwarding: None, + supply: SupplyToken { + subdenom: TEST_SUPPLY_DENOM.to_string(), + metadata: Some(default_supply_metadata()), + decimals, + max_supply: None, + }, + reserve: ReserveToken { + denom: TEST_RESERVE_DENOM.to_string(), + decimals: reserve_decimals, + }, + phase_config: CommonsPhaseConfig { + hatch: HatchConfig { + contribution_limits: MinMax { + min: Uint128::one(), + max: Uint128::from(1000000u128), + }, + initial_raise: MinMax { + min: Uint128::one(), + max: Uint128::from(1000000u128), + }, + entry_fee: Decimal::percent(10u64), + }, + open: OpenConfig { + entry_fee: Decimal::percent(10u64), + exit_fee: Decimal::percent(10u64), + }, + closed: ClosedConfig {}, + }, + hatcher_allowlist: None, + curve_type, + } +} + +pub fn mock_init(deps: DepsMut, init_msg: InstantiateMsg) -> Result { + let info = mock_info(TEST_CREATOR, &[]); + let env = mock_env(); + contract::instantiate(deps, env, info, init_msg) +} diff --git a/contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json b/contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json index 5a5ca6b86..7f7f44465 100644 --- a/contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json +++ b/contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json @@ -21,7 +21,7 @@ ], "properties": { "subdenom": { - "description": "component of fulldenom (`factory//`).", + "description": "component of full denom (`factory//`).", "type": "string" } }, @@ -31,7 +31,7 @@ "additionalProperties": false }, { - "description": "`ExistingToken` will use already created token. So to set this up, Token Factory admin for the existing token needs trasfer admin over to this contract, and optionally set the `BeforeSendHook` manually.", + "description": "`ExistingToken` will use already created token. So to set this up, Token Factory admin for the existing token needs transfer admin over to this contract, and optionally set the `BeforeSendHook` manually.", "type": "object", "required": [ "existing_token" @@ -60,7 +60,7 @@ "description": "State changing methods available to this smart contract.", "oneOf": [ { - "description": "Allow adds the target address to the allowlist to be able to send or recieve tokens even if the token is frozen. Token Factory's BeforeSendHook listener must be set to this contract in order for this feature to work.\n\nThis functionality is intedended for DAOs who do not wish to have a their tokens liquid while bootstrapping their DAO. For example, a DAO may wish to white list a Token Staking contract (to allow users to stake their tokens in the DAO) or a Merkle Drop contract (to allow users to claim their tokens).", + "description": "Allow adds the target address to the allowlist to be able to send or receive tokens even if the token is frozen. Token Factory's BeforeSendHook listener must be set to this contract in order for this feature to work.\n\nThis functionality is intended for DAOs who do not wish to have a their tokens liquid while bootstrapping their DAO. For example, a DAO may wish to white list a Token Staking contract (to allow users to stake their tokens in the DAO) or a Merkle Drop contract (to allow users to claim their tokens).", "type": "object", "required": [ "allow" @@ -86,7 +86,7 @@ "additionalProperties": false }, { - "description": "Burn token to address. Burn allowance is required and wiil be deducted after successful burn.", + "description": "Burn token to address. Burn allowance is required and will be deducted after successful burn.", "type": "object", "required": [ "burn" @@ -112,7 +112,7 @@ "additionalProperties": false }, { - "description": "Mint token to address. Mint allowance is required and wiil be deducted after successful mint.", + "description": "Mint token to address. Mint allowance is required and will be deducted after successful mint.", "type": "object", "required": [ "mint" @@ -138,7 +138,7 @@ "additionalProperties": false }, { - "description": "Deny adds the target address to the denylist, whis prevents them from sending/receiving the token attached to this contract tokenfactory's BeforeSendHook listener must be set to this contract in order for this feature to work as intended.", + "description": "Deny adds the target address to the denylist, which prevents them from sending/receiving the token attached to this contract tokenfactory's BeforeSendHook listener must be set to this contract in order for this feature to work as intended.", "type": "object", "required": [ "deny" @@ -601,7 +601,7 @@ "additionalProperties": false }, { - "description": "Enumerates over all burn allownances. Response: AllowancesResponse", + "description": "Enumerates over all burn allowances. Response: AllowancesResponse", "type": "object", "required": [ "burn_allowances" @@ -653,7 +653,7 @@ "additionalProperties": false }, { - "description": "Enumerates over all mint allownances. Response: AllowancesResponse", + "description": "Enumerates over all mint allowances. Response: AllowancesResponse", "type": "object", "required": [ "mint_allowances" diff --git a/contracts/external/cw-tokenfactory-issuer/src/msg.rs b/contracts/external/cw-tokenfactory-issuer/src/msg.rs index 4587a0ea5..be093bb86 100644 --- a/contracts/external/cw-tokenfactory-issuer/src/msg.rs +++ b/contracts/external/cw-tokenfactory-issuer/src/msg.rs @@ -1,3 +1,4 @@ +#[allow(unused_imports)] use crate::state::BeforeSendHookInfo; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Coin, Uint128}; @@ -11,11 +12,11 @@ pub enum InstantiateMsg { /// Newly created token will have full denom as `factory//`. /// It will be attached to the contract setup the beforesend listener automatically. NewToken { - /// component of fulldenom (`factory//`). + /// component of full denom (`factory//`). subdenom: String, }, /// `ExistingToken` will use already created token. So to set this up, - /// Token Factory admin for the existing token needs trasfer admin over + /// Token Factory admin for the existing token needs transfer admin over /// to this contract, and optionally set the `BeforeSendHook` manually. ExistingToken { denom: String }, } @@ -23,25 +24,25 @@ pub enum InstantiateMsg { /// State changing methods available to this smart contract. #[cw_serde] pub enum ExecuteMsg { - /// Allow adds the target address to the allowlist to be able to send or recieve tokens even if the token + /// Allow adds the target address to the allowlist to be able to send or receive tokens even if the token /// is frozen. Token Factory's BeforeSendHook listener must be set to this contract in order for this feature /// to work. /// - /// This functionality is intedended for DAOs who do not wish to have a their tokens liquid while bootstrapping + /// This functionality is intended for DAOs who do not wish to have a their tokens liquid while bootstrapping /// their DAO. For example, a DAO may wish to white list a Token Staking contract (to allow users to stake their /// tokens in the DAO) or a Merkle Drop contract (to allow users to claim their tokens). Allow { address: String, status: bool }, - /// Burn token to address. Burn allowance is required and wiil be deducted after successful burn. + /// Burn token to address. Burn allowance is required and will be deducted after successful burn. Burn { from_address: String, amount: Uint128, }, - /// Mint token to address. Mint allowance is required and wiil be deducted after successful mint. + /// Mint token to address. Mint allowance is required and will be deducted after successful mint. Mint { to_address: String, amount: Uint128 }, - /// Deny adds the target address to the denylist, whis prevents them from sending/receiving the token attached + /// Deny adds the target address to the denylist, which prevents them from sending/receiving the token attached /// to this contract tokenfactory's BeforeSendHook listener must be set to this contract in order for this /// feature to work as intended. Deny { address: String, status: bool }, @@ -123,7 +124,7 @@ pub enum QueryMsg { #[returns(AllowanceResponse)] BurnAllowance { address: String }, - /// Enumerates over all burn allownances. Response: AllowancesResponse + /// Enumerates over all burn allowances. Response: AllowancesResponse #[returns(AllowancesResponse)] BurnAllowances { start_after: Option, @@ -134,7 +135,7 @@ pub enum QueryMsg { #[returns(AllowanceResponse)] MintAllowance { address: String }, - /// Enumerates over all mint allownances. Response: AllowancesResponse + /// Enumerates over all mint allowances. Response: AllowancesResponse #[returns(AllowancesResponse)] MintAllowances { start_after: Option, @@ -202,7 +203,7 @@ pub struct DenomResponse { } /// Returns the current owner of this issuer contract who is allowed to -/// call priviledged methods. +/// call privileged methods. #[cw_serde] pub struct OwnerResponse { pub address: String, diff --git a/contracts/external/cw-tokenfactory-issuer/src/queries.rs b/contracts/external/cw-tokenfactory-issuer/src/queries.rs index a6b7f3b5d..6e8025c55 100644 --- a/contracts/external/cw-tokenfactory-issuer/src/queries.rs +++ b/contracts/external/cw-tokenfactory-issuer/src/queries.rs @@ -79,7 +79,7 @@ pub fn query_allowances( .collect() } -/// Enumerates over all allownances. Response: AllowancesResponse +/// Enumerates over all allowances. Response: AllowancesResponse pub fn query_mint_allowances( deps: Deps, start_after: Option, @@ -90,7 +90,7 @@ pub fn query_mint_allowances( }) } -/// Enumerates over all burn allownances. Response: AllowancesResponse +/// Enumerates over all burn allowances. Response: AllowancesResponse pub fn query_burn_allowances( deps: Deps, start_after: Option, diff --git a/contracts/external/cw-vesting/README.md b/contracts/external/cw-vesting/README.md index acd62106e..5e5b163fd 100644 --- a/contracts/external/cw-vesting/README.md +++ b/contracts/external/cw-vesting/README.md @@ -34,13 +34,13 @@ It supports 2 types of [curves](https://docs.rs/wynd-utils/0.4.1/wynd_utils/enum ##### Piecewise Linear -Piecsewise Curves can be used to create more complicated vesting +Piecewise Curves can be used to create more complicated vesting schedules. For example, let's say we have a schedule that vests 50% over 1 month and the remaining 50% over 1 year. We can implement this complex schedule with a Piecewise Linear curve. Piecewise Linear curves take a `steps` parameter which is a list of -tuples `(timestamp, vested)`. It will then linearally interpolate +tuples `(timestamp, vested)`. It will then linearly interpolate between those points to create the vesting curve. For example, given the points `(0, 0), (2, 2), (4, 8)`, it would create a vesting curve that looks like this: diff --git a/contracts/external/cw721-roles/src/contract.rs b/contracts/external/cw721-roles/src/contract.rs index 833007e46..ea31936a1 100644 --- a/contracts/external/cw721-roles/src/contract.rs +++ b/contracts/external/cw721-roles/src/contract.rs @@ -319,7 +319,7 @@ pub fn execute_update_token_role( let mut token = contract.tokens.load(deps.storage, &token_id)?; // Update role with new value - token.extension.role = role.clone(); + token.extension.role.clone_from(&role); contract.tokens.save(deps.storage, &token_id, &token)?; Ok(Response::default() @@ -341,7 +341,7 @@ pub fn execute_update_token_uri( let mut token = contract.tokens.load(deps.storage, &token_id)?; // Set new token URI - token.token_uri = token_uri.clone(); + token.token_uri.clone_from(&token_uri); contract.tokens.save(deps.storage, &token_id, &token)?; Ok(Response::new() diff --git a/contracts/external/dao-abc-factory/.cargo/config b/contracts/external/dao-abc-factory/.cargo/config new file mode 100644 index 000000000..336b618a1 --- /dev/null +++ b/contracts/external/dao-abc-factory/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" diff --git a/contracts/external/dao-abc-factory/Cargo.toml b/contracts/external/dao-abc-factory/Cargo.toml new file mode 100644 index 000000000..1966d393b --- /dev/null +++ b/contracts/external/dao-abc-factory/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "dao-abc-factory" +authors = ["Jake Hartnell"] +description = "A factory contract for cw-abc, intended for use with DAO DAO." +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] +# use test tube feature to enable test-tube integration tests, for example +# cargo test --features "test-tube" +test-tube = [] +# # when writing tests you may wish to enable test-tube as a default feature +# default = ["test-tube"] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-abc = { workspace = true, features = ["library"] } +cw2 = { workspace = true } +cw-ownable = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +thiserror = { workspace = true } +dao-dao-macros = { workspace = true } +dao-interface = { workspace = true } +dao-voting = { workspace = true } +cw-tokenfactory-issuer = { workspace = true, features = ["library"] } + +[dev-dependencies] +anyhow = { workspace = true } +cw-multi-test = { workspace = true } +cw-tokenfactory-issuer = { workspace = true } +dao-proposal-single = { workspace = true } +dao-proposal-hook-counter = { workspace = true } +dao-testing = { workspace = true, features = ["test-tube"] } +dao-voting-token-staked = { workspace = true } +osmosis-std = { workspace = true } +osmosis-test-tube = { workspace = true } +serde = { workspace = true } diff --git a/contracts/external/dao-abc-factory/README b/contracts/external/dao-abc-factory/README new file mode 100644 index 000000000..0322b0f4d --- /dev/null +++ b/contracts/external/dao-abc-factory/README @@ -0,0 +1,4 @@ +# DAO ABC Factory +Used for creating DAOs with `dao-voting-token-staked` and `cw-abc`. + +NOTE: this contract is NOT AUDITED, use at your own risk. diff --git a/contracts/external/dao-abc-factory/examples/schema.rs b/contracts/external/dao-abc-factory/examples/schema.rs new file mode 100644 index 000000000..1c48cc544 --- /dev/null +++ b/contracts/external/dao-abc-factory/examples/schema.rs @@ -0,0 +1,10 @@ +use cosmwasm_schema::write_api; +use dao_abc_factory::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + } +} diff --git a/contracts/external/dao-abc-factory/schema/dao-abc-factory.json b/contracts/external/dao-abc-factory/schema/dao-abc-factory.json new file mode 100644 index 000000000..d1560feea --- /dev/null +++ b/contracts/external/dao-abc-factory/schema/dao-abc-factory.json @@ -0,0 +1,645 @@ +{ + "contract_name": "dao-abc-factory", + "contract_version": "2.4.1", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "additionalProperties": false + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "abc_factory" + ], + "properties": { + "abc_factory": { + "type": "object", + "required": [ + "code_id", + "instantiate_msg" + ], + "properties": { + "code_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "instantiate_msg": { + "$ref": "#/definitions/InstantiateMsg" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "ClosedConfig": { + "type": "object", + "additionalProperties": false + }, + "CommonsPhaseConfig": { + "type": "object", + "required": [ + "closed", + "hatch", + "open" + ], + "properties": { + "closed": { + "description": "The Closed phase where the Commons is closed to new members.", + "allOf": [ + { + "$ref": "#/definitions/ClosedConfig" + } + ] + }, + "hatch": { + "description": "The Hatch phase where initial contributors (Hatchers) participate in a hatch sale.", + "allOf": [ + { + "$ref": "#/definitions/HatchConfig" + } + ] + }, + "open": { + "description": "TODO Vest tokens after hatch phase The Vesting phase where tokens minted during the Hatch phase are locked (burning is disabled) to combat early speculation/arbitrage. pub vesting: VestingConfig, The Open phase where anyone can mint tokens by contributing the reserve token into the curve and becoming members of the Commons.", + "allOf": [ + { + "$ref": "#/definitions/OpenConfig" + } + ] + } + }, + "additionalProperties": false + }, + "CurveType": { + "oneOf": [ + { + "description": "Constant always returns `value * 10^-scale` as spot price", + "type": "object", + "required": [ + "constant" + ], + "properties": { + "constant": { + "type": "object", + "required": [ + "scale", + "value" + ], + "properties": { + "scale": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "value": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Linear returns `slope * 10^-scale * supply` as spot price", + "type": "object", + "required": [ + "linear" + ], + "properties": { + "linear": { + "type": "object", + "required": [ + "scale", + "slope" + ], + "properties": { + "scale": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "slope": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "SquareRoot returns `slope * 10^-scale * supply^0.5` as spot price", + "type": "object", + "required": [ + "square_root" + ], + "properties": { + "square_root": { + "type": "object", + "required": [ + "scale", + "slope" + ], + "properties": { + "scale": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "slope": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "DenomUnit": { + "description": "DenomUnit represents a struct that describes a given denomination unit of the basic token.", + "type": "object", + "required": [ + "aliases", + "denom", + "exponent" + ], + "properties": { + "aliases": { + "description": "aliases is a list of string aliases for the given denom", + "type": "array", + "items": { + "type": "string" + } + }, + "denom": { + "description": "denom represents the string name of the given denom unit (e.g uatom).", + "type": "string" + }, + "exponent": { + "description": "exponent represents power of 10 exponent that one must raise the base_denom to in order to equal the given DenomUnit's denom 1 denom = 1^exponent base_denom (e.g. with a base_denom of uatom, one can create a DenomUnit of 'atom' with exponent = 6, thus: 1 atom = 10^6 uatom).", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + } + }, + "HatchConfig": { + "type": "object", + "required": [ + "contribution_limits", + "entry_fee", + "initial_raise" + ], + "properties": { + "contribution_limits": { + "description": "The minimum and maximum contribution amounts (min, max) in the reserve token", + "allOf": [ + { + "$ref": "#/definitions/MinMax" + } + ] + }, + "entry_fee": { + "description": "The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "initial_raise": { + "description": "The initial raise range (min, max) in the reserve token", + "allOf": [ + { + "$ref": "#/definitions/MinMax" + } + ] + } + }, + "additionalProperties": false + }, + "HatcherAllowlistConfigMsg": { + "type": "object", + "required": [ + "config_type" + ], + "properties": { + "config_type": { + "description": "The type of the configuration", + "allOf": [ + { + "$ref": "#/definitions/HatcherAllowlistConfigType" + } + ] + }, + "contribution_limits_override": { + "description": "An optional override of the hatch_config's contribution limit", + "anyOf": [ + { + "$ref": "#/definitions/MinMax" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "HatcherAllowlistConfigType": { + "oneOf": [ + { + "type": "object", + "required": [ + "d_a_o" + ], + "properties": { + "d_a_o": { + "type": "object", + "properties": { + "priority": { + "description": "The optional priority for checking a DAO config None will append the item to the end of the priority queue (least priority)", + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "HatcherAllowlistEntryMsg": { + "type": "object", + "required": [ + "addr", + "config" + ], + "properties": { + "addr": { + "type": "string" + }, + "config": { + "$ref": "#/definitions/HatcherAllowlistConfigMsg" + } + }, + "additionalProperties": false + }, + "InstantiateMsg": { + "type": "object", + "required": [ + "curve_type", + "phase_config", + "reserve", + "supply", + "token_issuer_code_id" + ], + "properties": { + "curve_type": { + "description": "Curve type for this contract", + "allOf": [ + { + "$ref": "#/definitions/CurveType" + } + ] + }, + "funding_pool_forwarding": { + "description": "An optional address for automatically forwarding funding pool gains", + "type": [ + "string", + "null" + ] + }, + "hatcher_allowlist": { + "description": "TODO different ways of doing this, for example DAO members? Using a whitelist contract? Merkle tree? Hatcher allowlist", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/HatcherAllowlistEntryMsg" + } + }, + "phase_config": { + "description": "Hatch configuration information", + "allOf": [ + { + "$ref": "#/definitions/CommonsPhaseConfig" + } + ] + }, + "reserve": { + "description": "Reserve token information", + "allOf": [ + { + "$ref": "#/definitions/ReserveToken" + } + ] + }, + "supply": { + "description": "Supply token information", + "allOf": [ + { + "$ref": "#/definitions/SupplyToken" + } + ] + }, + "token_issuer_code_id": { + "description": "The code id of the cw-tokenfactory-issuer contract", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "MinMax": { + "description": "Struct for minimum and maximum values", + "type": "object", + "required": [ + "max", + "min" + ], + "properties": { + "max": { + "$ref": "#/definitions/Uint128" + }, + "min": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, + "NewDenomMetadata": { + "type": "object", + "required": [ + "description", + "display", + "name", + "symbol" + ], + "properties": { + "additional_denom_units": { + "description": "Used define additional units of the token (e.g. \"tiger\") These must have an exponent larger than 0.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/DenomUnit" + } + }, + "description": { + "description": "The description of the token", + "type": "string" + }, + "display": { + "description": "The unit commonly used in communication (e.g. \"cat\")", + "type": "string" + }, + "name": { + "description": "The name of the token (e.g. \"Cat Coin\")", + "type": "string" + }, + "symbol": { + "description": "The ticker symbol of the token (e.g. \"CAT\")", + "type": "string" + } + }, + "additionalProperties": false + }, + "OpenConfig": { + "type": "object", + "required": [ + "entry_fee", + "exit_fee" + ], + "properties": { + "entry_fee": { + "description": "Percentage of capital put into the Reserve Pool during the Open phase when buying from the curve.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "exit_fee": { + "description": "Exit taxation ratio", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "ReserveToken": { + "type": "object", + "required": [ + "decimals", + "denom" + ], + "properties": { + "decimals": { + "description": "Number of decimal places for the reserve token, needed for proper curve math. Same format as decimals above, eg. if it is uatom, where 1 unit is 10^-6 ATOM, use 6 here", + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "denom": { + "description": "Reserve token denom (only support native for now)", + "type": "string" + } + }, + "additionalProperties": false + }, + "SupplyToken": { + "type": "object", + "required": [ + "decimals", + "subdenom" + ], + "properties": { + "decimals": { + "description": "Number of decimal places for the supply token, needed for proper curve math. Default for token factory is 6", + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "max_supply": { + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "metadata": { + "description": "Metadata for the supply token to create", + "anyOf": [ + { + "$ref": "#/definitions/NewDenomMetadata" + }, + { + "type": "null" + } + ] + }, + "subdenom": { + "description": "The denom to create for the supply token", + "type": "string" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "daos" + ], + "properties": { + "daos": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "migrate": null, + "sudo": null, + "responses": { + "daos": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_Addr", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + }, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + } + } + }, + "info": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InfoResponse", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "$ref": "#/definitions/ContractVersion" + } + }, + "additionalProperties": false, + "definitions": { + "ContractVersion": { + "type": "object", + "required": [ + "contract", + "version" + ], + "properties": { + "contract": { + "description": "contract is the crate name of the implementing contract, eg. `crate:cw20-base` we will use other prefixes for other languages, and their standard global namespacing", + "type": "string" + }, + "version": { + "description": "version is any string that this implementation knows. It may be simple counter \"1\", \"2\". or semantic version on release tags \"v0.7.0\", or some custom feature flag list. the only code that needs to understand the version parsing is code that knows how to migrate from the given contract (and is tied to it's implementation somehow)", + "type": "string" + } + }, + "additionalProperties": false + } + } + } + } +} diff --git a/contracts/external/dao-abc-factory/src/contract.rs b/contracts/external/dao-abc-factory/src/contract.rs new file mode 100644 index 000000000..b2836742f --- /dev/null +++ b/contracts/external/dao-abc-factory/src/contract.rs @@ -0,0 +1,187 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_json_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Order, Reply, + Response, StdResult, SubMsg, WasmMsg, +}; +use cw2::set_contract_version; +use cw_abc::msg::{ + DenomResponse, ExecuteMsg as AbcExecuteMsg, InstantiateMsg as AbcInstantiateMsg, + QueryMsg as AbcQueryMsg, +}; +use cw_storage_plus::{Bound, Item, Map}; +use cw_utils::parse_reply_instantiate_data; +use dao_interface::{ + state::ModuleInstantiateCallback, token::TokenFactoryCallback, + voting::Query as VotingModuleQueryMsg, +}; + +use crate::{ + error::ContractError, + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, +}; + +const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +const INSTANTIATE_ABC_REPLY_ID: u64 = 1; + +const DAOS: Map = Map::new("daos"); +const CURRENT_DAO: Item = Item::new("current_dao"); +const VOTING_MODULE: Item = Item::new("voting_module"); + +#[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)?; + + Ok(Response::new().add_attribute("method", "instantiate")) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::AbcFactory { + code_id, + instantiate_msg, + } => execute_token_factory_factory(deps, env, info, code_id, instantiate_msg), + } +} + +pub fn execute_token_factory_factory( + deps: DepsMut, + _env: Env, + info: MessageInfo, + code_id: u64, + msg: AbcInstantiateMsg, +) -> Result { + // Save voting module address + VOTING_MODULE.save(deps.storage, &info.sender)?; + + // Query for DAO + let dao: Addr = deps + .querier + .query_wasm_smart(info.sender, &VotingModuleQueryMsg::Dao {})?; + + DAOS.save(deps.storage, dao.clone(), &Empty {})?; + CURRENT_DAO.save(deps.storage, &dao)?; + + // Instantiate new contract, further setup is handled in the + // SubMsg reply. + let msg = SubMsg::reply_on_success( + WasmMsg::Instantiate { + // No admin as we want the bonding curve contract to be immutable + admin: None, + code_id, + msg: to_json_binary(&msg)?, + funds: vec![], + label: "cw_abc".to_string(), + }, + INSTANTIATE_ABC_REPLY_ID, + ); + + Ok(Response::new().add_submessage(msg)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Info {} => query_info(deps), + QueryMsg::Daos { start_after, limit } => query_daos(deps, start_after, limit), + } +} + +pub fn query_info(deps: Deps) -> StdResult { + let info = cw2::get_contract_version(deps.storage)?; + to_json_binary(&dao_interface::voting::InfoResponse { info }) +} + +pub fn query_daos( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + to_json_binary( + &DAOS + .keys( + deps.storage, + None, + start_after + .map(|s| deps.api.addr_validate(&s)) + .transpose()? + .map(Bound::exclusive), + Order::Descending, + ) + .take(limit.unwrap_or(25) as usize) + .collect::>>()?, + ) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { + match msg.id { + INSTANTIATE_ABC_REPLY_ID => { + // Load DAO + let dao = CURRENT_DAO.load(deps.storage)?; + + // Parse issuer address from instantiate reply + let abc_addr = parse_reply_instantiate_data(msg)?.contract_address; + + // Query for denom + let denom: DenomResponse = deps + .querier + .query_wasm_smart(abc_addr.clone(), &AbcQueryMsg::Denom {})?; + + // Query for token contract + let token_contract: Addr = deps + .querier + .query_wasm_smart(abc_addr.clone(), &AbcQueryMsg::TokenContract {})?; + + // Update the owner to be the DAO + let msg = WasmMsg::Execute { + contract_addr: abc_addr.clone(), + msg: to_json_binary(&AbcExecuteMsg::UpdateOwnership( + cw_ownable::Action::TransferOwnership { + new_owner: dao.to_string(), + expiry: None, + }, + ))?, + funds: vec![], + }; + + // DAO must accept ownership transfer. Here we include a + // ModuleInstantiateCallback message that will be called by the + // dao-dao-core contract when voting module instantiation is + // complete. + let callback = ModuleInstantiateCallback { + msgs: vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: abc_addr.clone(), + msg: to_json_binary(&AbcExecuteMsg::UpdateOwnership( + cw_ownable::Action::AcceptOwnership {}, + ))?, + funds: vec![], + })], + }; + + // Responses for `dao-voting-token-staked` MUST include a + // TokenFactoryCallback. + Ok(Response::new() + .add_message(msg) + .set_data(to_json_binary(&TokenFactoryCallback { + denom: denom.denom, + token_contract: Some(token_contract.to_string()), + module_instantiate_callback: Some(callback), + })?)) + } + _ => Err(ContractError::UnknownReplyId { id: msg.id }), + } +} diff --git a/contracts/external/dao-abc-factory/src/error.rs b/contracts/external/dao-abc-factory/src/error.rs new file mode 100644 index 000000000..4a29782c7 --- /dev/null +++ b/contracts/external/dao-abc-factory/src/error.rs @@ -0,0 +1,28 @@ +use cosmwasm_std::StdError; +use cw_utils::{ParseReplyError, PaymentError}; +use dao_voting::threshold::ActiveThresholdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error(transparent)] + ActiveThresholdError(#[from] ActiveThresholdError), + + #[error(transparent)] + ParseReplyError(#[from] ParseReplyError), + + #[error(transparent)] + PaymentError(#[from] PaymentError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Got a submessage reply with unknown id: {id}")] + UnknownReplyId { id: u64 }, + + #[error("Factory message must serialize to WasmMsg::Execute")] + UnsupportedFactoryMsg {}, +} diff --git a/contracts/external/dao-abc-factory/src/lib.rs b/contracts/external/dao-abc-factory/src/lib.rs new file mode 100644 index 000000000..286ec8c24 --- /dev/null +++ b/contracts/external/dao-abc-factory/src/lib.rs @@ -0,0 +1,8 @@ +pub mod contract; +mod error; +pub mod msg; + +pub use crate::error::ContractError; + +#[cfg(test)] +mod test_tube; diff --git a/contracts/external/dao-abc-factory/src/msg.rs b/contracts/external/dao-abc-factory/src/msg.rs new file mode 100644 index 000000000..143224330 --- /dev/null +++ b/contracts/external/dao-abc-factory/src/msg.rs @@ -0,0 +1,25 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cw_abc::msg::InstantiateMsg as AbcInstantiateMsg; + +#[cw_serde] +pub struct InstantiateMsg {} + +#[cw_serde] +pub enum ExecuteMsg { + AbcFactory { + instantiate_msg: AbcInstantiateMsg, + code_id: u64, + }, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(dao_interface::voting::InfoResponse)] + Info {}, + #[returns(Vec)] + Daos { + start_after: Option, + limit: Option, + }, +} diff --git a/contracts/external/dao-abc-factory/src/test_tube/integration_tests.rs b/contracts/external/dao-abc-factory/src/test_tube/integration_tests.rs new file mode 100644 index 000000000..5b3bd3126 --- /dev/null +++ b/contracts/external/dao-abc-factory/src/test_tube/integration_tests.rs @@ -0,0 +1,147 @@ +use cosmwasm_std::{coins, Addr, Coin, Uint128}; +use cw_ownable::Ownership; +use dao_interface::voting::{DenomResponse, IsActiveResponse, VotingPowerAtHeightResponse}; +use dao_voting_token_staked::msg::{ + ExecuteMsg as VotingTokenExecuteMsg, QueryMsg as VotingTokenQueryMsg, +}; +use osmosis_test_tube::{Account, OsmosisTestApp}; + +use super::test_env::{TestEnv, TestEnvBuilder, RESERVE}; + +#[test] +fn test_full_integration_correct_setup() { + let app = OsmosisTestApp::new(); + let env = TestEnvBuilder::new(); + let TestEnv { + dao, + tf_issuer, + cw_abc, + vp_contract, + .. + } = env.full_dao_setup(&app); + + // Issuer owner should be set to the abc contract + let issuer_admin = tf_issuer + .query::>(&cw_tokenfactory_issuer::msg::QueryMsg::Ownership {}) + .unwrap() + .owner; + assert_eq!( + issuer_admin, + Some(Addr::unchecked(cw_abc.contract_addr.clone())) + ); + + // Abc contract should have DAO as owner + let abc_admin = cw_abc + .query::>(&cw_abc::msg::QueryMsg::Ownership {}) + .unwrap() + .owner; + assert_eq!( + abc_admin, + Some(Addr::unchecked(dao.unwrap().contract_addr.clone())) + ); + + let issuer_denom = tf_issuer + .query::( + &cw_tokenfactory_issuer::msg::QueryMsg::Denom {}, + ) + .unwrap() + .denom; + + let abc_denom = cw_abc + .query::(&cw_abc::msg::QueryMsg::Denom {}) + .unwrap() + .denom; + + let vp_denom = vp_contract + .query::(&VotingTokenQueryMsg::Denom {}) + .unwrap() + .denom; + + // Denoms for contracts should be the same + assert_eq!(issuer_denom, abc_denom); + assert_eq!(issuer_denom, vp_denom); +} + +#[test] +fn test_stake_unstake_new_denom() { + let app = OsmosisTestApp::new(); + let env = TestEnvBuilder::new(); + let TestEnv { + vp_contract, + accounts, + cw_abc, + .. + } = env.full_dao_setup(&app); + + let denom = vp_contract + .query::(&VotingTokenQueryMsg::Denom {}) + .unwrap() + .denom; + + // Buy tokens off of bonding curve + cw_abc + .execute( + &cw_abc::msg::ExecuteMsg::Buy {}, + &coins(100000, RESERVE), + &accounts[0], + ) + .unwrap(); + + // Stake 100 tokens + let stake_msg = VotingTokenExecuteMsg::Stake {}; + vp_contract + .execute(&stake_msg, &[Coin::new(100, denom)], &accounts[0]) + .unwrap(); + + app.increase_time(1); + + // Query voting power + let voting_power: VotingPowerAtHeightResponse = vp_contract + .query(&VotingTokenQueryMsg::VotingPowerAtHeight { + address: accounts[0].address(), + height: None, + }) + .unwrap(); + assert_eq!(voting_power.power, Uint128::new(100)); + + // DAO is active (default threshold is absolute count of 75) + let active = vp_contract + .query::(&VotingTokenQueryMsg::IsActive {}) + .unwrap() + .active; + assert!(active); + + // Unstake 50 tokens + let unstake_msg = VotingTokenExecuteMsg::Unstake { + amount: Uint128::new(50), + }; + vp_contract + .execute(&unstake_msg, &[], &accounts[0]) + .unwrap(); + app.increase_time(1); + let voting_power: VotingPowerAtHeightResponse = vp_contract + .query(&VotingTokenQueryMsg::VotingPowerAtHeight { + address: accounts[0].address(), + height: None, + }) + .unwrap(); + assert_eq!(voting_power.power, Uint128::new(50)); + + // DAO is not active + let active = vp_contract + .query::(&VotingTokenQueryMsg::IsActive {}) + .unwrap() + .active; + assert!(!active); + + // Can't claim before unstaking period (2 seconds) + vp_contract + .execute(&VotingTokenExecuteMsg::Claim {}, &[], &accounts[0]) + .unwrap_err(); + + // Pass time, unstaking duration is set to 2 seconds + app.increase_time(5); + vp_contract + .execute(&VotingTokenExecuteMsg::Claim {}, &[], &accounts[0]) + .unwrap(); +} diff --git a/contracts/external/dao-abc-factory/src/test_tube/mod.rs b/contracts/external/dao-abc-factory/src/test_tube/mod.rs new file mode 100644 index 000000000..796c18a34 --- /dev/null +++ b/contracts/external/dao-abc-factory/src/test_tube/mod.rs @@ -0,0 +1,9 @@ +// Ignore integration tests for code coverage since there will be problems with dynamic linking libosmosistesttube +// and also, tarpaulin will not be able read coverage out of wasm binary anyway +#![cfg(not(tarpaulin))] + +#[cfg(feature = "test-tube")] +mod integration_tests; + +#[cfg(feature = "test-tube")] +mod test_env; diff --git a/contracts/external/dao-abc-factory/src/test_tube/test_env.rs b/contracts/external/dao-abc-factory/src/test_tube/test_env.rs new file mode 100644 index 000000000..d57f6d57b --- /dev/null +++ b/contracts/external/dao-abc-factory/src/test_tube/test_env.rs @@ -0,0 +1,558 @@ +// The code is used in tests but reported as dead code +// see https://github.com/rust-lang/rust/issues/46379 +#![allow(dead_code)] + +use crate::{ + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + ContractError, +}; + +use cosmwasm_std::{to_json_binary, Addr, Coin, Decimal, Uint128, WasmMsg}; +use cw_abc::abc::{ + ClosedConfig, CommonsPhaseConfig, CurveType, HatchConfig, MinMax, OpenConfig, ReserveToken, + SupplyToken, +}; +use cw_utils::Duration; +use dao_interface::{ + msg::QueryMsg as DaoQueryMsg, + state::{Admin, ModuleInstantiateInfo, ProposalModule}, +}; +use dao_voting::{ + pre_propose::PreProposeInfo, threshold::PercentageThreshold, threshold::Threshold, +}; +use dao_voting_token_staked::msg::{QueryMsg as TokenVotingQueryMsg, TokenInfo}; + +use dao_testing::test_tube::{ + cw_abc::CwAbc, cw_tokenfactory_issuer::TokenfactoryIssuer, dao_dao_core::DaoCore, + dao_proposal_single::DaoProposalSingle, dao_voting_token_staked::TokenVotingContract, +}; +use dao_voting::threshold::ActiveThreshold; +use osmosis_test_tube::{ + osmosis_std::types::{ + cosmos::bank::v1beta1::QueryAllBalancesRequest, + cosmwasm::wasm::v1::MsgExecuteContractResponse, + }, + Account, Bank, Module, OsmosisTestApp, RunnerError, RunnerExecuteResult, RunnerResult, + SigningAccount, Wasm, +}; +use serde::de::DeserializeOwned; +use std::path::PathBuf; + +pub const DENOM: &str = "ucat"; +pub const JUNO: &str = "ujuno"; + +// Needs to match what's configured for test-tube +pub const RESERVE: &str = "uosmo"; + +pub struct TestEnv<'a> { + pub app: &'a OsmosisTestApp, + pub dao: Option>, + pub proposal_single: Option>, + pub vp_contract: TokenVotingContract<'a>, + pub tf_issuer: TokenfactoryIssuer<'a>, + pub dao_abc_factory: AbcFactoryContract<'a>, + pub accounts: Vec, + pub cw_abc: CwAbc<'a>, +} + +impl<'a> TestEnv<'a> { + pub fn get_tf_issuer_code_id(&self) -> u64 { + self.tf_issuer.code_id + } + + pub fn bank(&self) -> Bank<'_, OsmosisTestApp> { + Bank::new(self.app) + } + + pub fn assert_account_balances( + &self, + account: SigningAccount, + expected_balances: Vec, + ignore_denoms: Vec<&str>, + ) { + let account_balances: Vec = Bank::new(self.app) + .query_all_balances(&QueryAllBalancesRequest { + address: account.address(), + pagination: None, + }) + .unwrap() + .balances + .into_iter() + .map(|coin| Coin::new(coin.amount.parse().unwrap(), coin.denom)) + .filter(|coin| !ignore_denoms.contains(&coin.denom.as_str())) + .collect(); + + assert_eq!(account_balances, expected_balances); + } + + pub fn assert_contract_balances(&self, expected_balances: &[Coin]) { + let contract_balances: Vec = Bank::new(self.app) + .query_all_balances(&QueryAllBalancesRequest { + address: self.vp_contract.contract_addr.clone(), + pagination: None, + }) + .unwrap() + .balances + .into_iter() + .map(|coin| Coin::new(coin.amount.parse().unwrap(), coin.denom)) + .collect(); + + assert_eq!(contract_balances, expected_balances); + } +} + +pub struct TestEnvBuilder { + pub accounts: Vec, + pub instantiate_msg: Option, +} + +impl TestEnvBuilder { + pub fn new() -> Self { + Self { + accounts: vec![], + instantiate_msg: None, + } + } + + // Minimal default setup with just the key contracts + pub fn default_setup(self, app: &'_ OsmosisTestApp) -> TestEnv<'_> { + let accounts = app + .init_accounts(&[Coin::new(1000000000000000u128, "uosmo")], 10) + .unwrap(); + + let issuer_id = TokenfactoryIssuer::upload(app, &accounts[0]).unwrap(); + let abc_id = CwAbc::upload(app, &accounts[0]).unwrap(); + + // Upload and instantiate abc factory + let dao_abc_factory = + AbcFactoryContract::new(app, &InstantiateMsg {}, &accounts[0]).unwrap(); + + let vp_contract = TokenVotingContract::new( + app, + &dao_voting_token_staked::msg::InstantiateMsg { + token_info: TokenInfo::Factory( + to_json_binary(&WasmMsg::Execute { + contract_addr: dao_abc_factory.contract_addr.clone(), + msg: to_json_binary(&ExecuteMsg::AbcFactory { + instantiate_msg: cw_abc::msg::InstantiateMsg { + token_issuer_code_id: issuer_id, + funding_pool_forwarding: Some(accounts[0].address()), + supply: SupplyToken { + subdenom: DENOM.to_string(), + metadata: None, + decimals: 6, + max_supply: Some(Uint128::from(1000000000u128)), + }, + reserve: ReserveToken { + denom: RESERVE.to_string(), + decimals: 6, + }, + phase_config: CommonsPhaseConfig { + hatch: HatchConfig { + contribution_limits: MinMax { + min: Uint128::from(10u128), + max: Uint128::from(1000000u128), + }, + initial_raise: MinMax { + min: Uint128::from(10u128), + max: Uint128::from(1000000u128), + }, + entry_fee: Decimal::percent(10u64), + }, + open: OpenConfig { + entry_fee: Decimal::percent(10u64), + exit_fee: Decimal::percent(10u64), + }, + closed: ClosedConfig {}, + }, + hatcher_allowlist: None, + curve_type: CurveType::Constant { + value: Uint128::one(), + scale: 1, + }, + }, + code_id: abc_id, + }) + .unwrap(), + funds: vec![], + }) + .unwrap(), + ), + unstaking_duration: Some(Duration::Time(2)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(75), + }), + }, + &accounts[0], + ) + .unwrap(); + + let issuer_addr = + TokenVotingContract::query(&vp_contract, &TokenVotingQueryMsg::TokenContract {}) + .unwrap(); + + let tf_issuer = TokenfactoryIssuer::new_with_values(app, issuer_id, issuer_addr).unwrap(); + + // The abc contract is the owner of the issuer + let abc_addr = tf_issuer + .query::>( + &cw_tokenfactory_issuer::msg::QueryMsg::Ownership {}, + ) + .unwrap() + .owner; + let cw_abc = CwAbc::new_with_values(app, abc_id, abc_addr.unwrap().to_string()).unwrap(); + + TestEnv { + app, + accounts, + cw_abc, + dao: None, + proposal_single: None, + tf_issuer, + vp_contract, + dao_abc_factory, + } + } + + // Full DAO setup + pub fn full_dao_setup(self, app: &'_ OsmosisTestApp) -> TestEnv<'_> { + let accounts = app + .init_accounts(&[Coin::new(1000000000000000u128, "uosmo")], 10) + .unwrap(); + + // Upload all needed code ids + let issuer_id = TokenfactoryIssuer::upload(app, &accounts[0]).unwrap(); + let vp_contract_id = TokenVotingContract::upload(app, &accounts[0]).unwrap(); + let proposal_single_id = DaoProposalSingle::upload(app, &accounts[0]).unwrap(); + let abc_id = CwAbc::upload(app, &accounts[0]).unwrap(); + + // Upload and instantiate abc factory + let dao_abc_factory = + AbcFactoryContract::new(app, &InstantiateMsg {}, &accounts[0]).unwrap(); + + let msg = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that makes DAO tooling".to_string(), + image_url: None, + automatically_add_cw20s: false, + automatically_add_cw721s: false, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: vp_contract_id, + msg: to_json_binary(&dao_voting_token_staked::msg::InstantiateMsg { + token_info: TokenInfo::Factory( + to_json_binary(&WasmMsg::Execute { + contract_addr: dao_abc_factory.contract_addr.clone(), + msg: to_json_binary(&ExecuteMsg::AbcFactory { + instantiate_msg: cw_abc::msg::InstantiateMsg { + token_issuer_code_id: issuer_id, + funding_pool_forwarding: Some(accounts[0].address()), + supply: SupplyToken { + subdenom: DENOM.to_string(), + metadata: None, + decimals: 6, + max_supply: Some(Uint128::from(1000000000u128)), + }, + reserve: ReserveToken { + denom: RESERVE.to_string(), + decimals: 6, + }, + phase_config: CommonsPhaseConfig { + hatch: HatchConfig { + contribution_limits: MinMax { + min: Uint128::from(10u128), + max: Uint128::from(1000000u128), + }, + initial_raise: MinMax { + min: Uint128::from(10u128), + max: Uint128::from(1000000u128), + }, + entry_fee: Decimal::percent(10u64), + }, + open: OpenConfig { + entry_fee: Decimal::percent(10u64), + exit_fee: Decimal::percent(10u64), + }, + closed: ClosedConfig {}, + }, + hatcher_allowlist: None, + curve_type: CurveType::Constant { + value: Uint128::one(), + scale: 1, + }, + }, + code_id: abc_id, + }) + .unwrap(), + funds: vec![], + }) + .unwrap(), + ), + unstaking_duration: Some(Duration::Time(2)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(75), + }), + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Voting Module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_single_id, + msg: to_json_binary(&dao_proposal_single::msg::InstantiateMsg { + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Majority {}, + quorum: PercentageThreshold::Percent(Decimal::percent(35)), + }, + max_voting_period: Duration::Time(432000), + allow_revoting: false, + only_members_execute: true, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Proposal Module".to_string(), + }], + initial_items: None, + }; + + // Instantiate DAO + let dao = DaoCore::new(app, &msg, &accounts[0], &[]).unwrap(); + + // Get voting module address, setup vp_contract helper + let vp_addr: Addr = dao.query(&DaoQueryMsg::VotingModule {}).unwrap(); + let vp_contract = + TokenVotingContract::new_with_values(app, vp_contract_id, vp_addr.to_string()).unwrap(); + + // Get proposal module address, setup proposal_single helper + let proposal_modules: Vec = dao + .query(&DaoQueryMsg::ProposalModules { + limit: None, + start_after: None, + }) + .unwrap(); + let proposal_single = DaoProposalSingle::new_with_values( + app, + proposal_single_id, + proposal_modules[0].address.to_string(), + ) + .unwrap(); + + // Get issuer address, setup tf_issuer helper + let issuer_addr = + TokenVotingContract::query(&vp_contract, &TokenVotingQueryMsg::TokenContract {}) + .unwrap(); + let tf_issuer = TokenfactoryIssuer::new_with_values(app, issuer_id, issuer_addr).unwrap(); + + // Get ABC Contract address + // The abc contract is the owner of the issuer + let abc_addr = tf_issuer + .query::>( + &cw_tokenfactory_issuer::msg::QueryMsg::Ownership {}, + ) + .unwrap() + .owner; + let cw_abc = CwAbc::new_with_values(app, abc_id, abc_addr.unwrap().to_string()).unwrap(); + + TestEnv { + app, + dao: Some(dao), + cw_abc, + vp_contract, + proposal_single: Some(proposal_single), + tf_issuer, + accounts, + dao_abc_factory, + } + } + + pub fn upload_issuer(self, app: &'_ OsmosisTestApp, signer: &SigningAccount) -> u64 { + TokenfactoryIssuer::upload(app, signer).unwrap() + } + + pub fn set_accounts(mut self, accounts: Vec) -> Self { + self.accounts = accounts; + self + } + + pub fn with_account(mut self, account: SigningAccount) -> Self { + self.accounts.push(account); + self + } + + pub fn with_instantiate_msg(mut self, msg: InstantiateMsg) -> Self { + self.instantiate_msg = Some(msg); + self + } +} + +#[derive(Debug)] +pub struct AbcFactoryContract<'a> { + pub app: &'a OsmosisTestApp, + pub contract_addr: String, + pub code_id: u64, +} + +impl<'a> AbcFactoryContract<'a> { + pub fn new( + app: &'a OsmosisTestApp, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + ) -> Result { + let wasm = Wasm::new(app); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + let contract_addr = wasm + .instantiate( + code_id, + &instantiate_msg, + Some(&signer.address()), + None, + &[], + signer, + )? + .data + .address; + + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + pub fn new_with_values( + app: &'a OsmosisTestApp, + code_id: u64, + contract_addr: String, + ) -> Result { + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + /// uploads contract and returns a code ID + pub fn upload(app: &OsmosisTestApp, signer: &SigningAccount) -> Result { + let wasm = Wasm::new(app); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + Ok(code_id) + } + + pub fn instantiate( + app: &'a OsmosisTestApp, + code_id: u64, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + ) -> Result { + let wasm = Wasm::new(app); + let contract_addr = wasm + .instantiate( + code_id, + &instantiate_msg, + Some(&signer.address()), + None, + &[], + signer, + )? + .data + .address; + + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + pub fn execute( + &self, + msg: &ExecuteMsg, + funds: &[Coin], + signer: &SigningAccount, + ) -> RunnerExecuteResult { + let wasm = Wasm::new(self.app); + wasm.execute(&self.contract_addr, msg, funds, signer) + } + + pub fn query(&self, msg: &QueryMsg) -> RunnerResult + where + T: ?Sized + DeserializeOwned, + { + let wasm = Wasm::new(self.app); + wasm.query(&self.contract_addr, msg) + } + + fn get_wasm_byte_code() -> Vec { + let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let byte_code = std::fs::read( + manifest_path + .join("..") + .join("..") + .join("..") + .join("artifacts") + .join("dao_abc_factory.wasm"), + ); + match byte_code { + Ok(byte_code) => byte_code, + // On arm processors, the above path is not found, so we try the following path + Err(_) => std::fs::read( + manifest_path + .join("..") + .join("..") + .join("..") + .join("artifacts") + .join("dao_abc_factory-aarch64.wasm"), + ) + .unwrap(), + } + } + + pub fn execute_error(err: ContractError) -> RunnerError { + RunnerError::ExecuteError { + msg: format!( + "failed to execute message; message index: 0: {}: execute wasm contract failed", + err + ), + } + } + + pub fn execute_submessage_error(err: ContractError) -> RunnerError { + RunnerError::ExecuteError { + msg: format!( + "failed to execute message; message index: 0: dispatch: submessages: reply: {}: execute wasm contract failed", + err + ), + } + } +} + +pub fn assert_contract_err(expected: ContractError, actual: RunnerError) { + match actual { + RunnerError::ExecuteError { msg } => { + if !msg.contains(&expected.to_string()) { + panic!( + "assertion failed:\n\n must contain \t: \"{}\",\n actual \t: \"{}\"\n", + expected, msg + ) + } + } + _ => panic!("unexpected error, expect execute error but got: {}", actual), + }; +} diff --git a/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json b/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json index c5203092d..3b6a7e888 100644 --- a/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json +++ b/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json @@ -32,7 +32,7 @@ ] }, "min_voting_period": { - "description": "The minimum amount of time a proposal must be open before passing. A proposal may fail before this amount of time has elapsed, but it will not pass. This can be useful for preventing governance attacks wherein an attacker aquires a large number of tokens and forces a proposal through.", + "description": "The minimum amount of time a proposal must be open before passing. A proposal may fail before this amount of time has elapsed, but it will not pass. This can be useful for preventing governance attacks wherein an attacker acquires a large number of tokens and forces a proposal through.", "anyOf": [ { "$ref": "#/definitions/Duration" diff --git a/contracts/proposal/dao-proposal-single/src/msg.rs b/contracts/proposal/dao-proposal-single/src/msg.rs index 302303b48..590eb194f 100644 --- a/contracts/proposal/dao-proposal-single/src/msg.rs +++ b/contracts/proposal/dao-proposal-single/src/msg.rs @@ -16,7 +16,7 @@ pub struct InstantiateMsg { /// The minimum amount of time a proposal must be open before /// passing. A proposal may fail before this amount of time has /// elapsed, but it will not pass. This can be useful for - /// preventing governance attacks wherein an attacker aquires a + /// preventing governance attacks wherein an attacker acquires a /// large number of tokens and forces a proposal through. pub min_voting_period: Option, /// If set to true only members may execute passed diff --git a/contracts/proposal/dao-proposal-single/src/proposal.rs b/contracts/proposal/dao-proposal-single/src/proposal.rs index a597f3754..1fd483446 100644 --- a/contracts/proposal/dao-proposal-single/src/proposal.rs +++ b/contracts/proposal/dao-proposal-single/src/proposal.rs @@ -184,7 +184,7 @@ impl SingleChoiceProposal { // and there are possible votes, then this is // rejected if there is a single no vote. // - // We need this check becuase otherwise when + // We need this check because otherwise when // we invert the threshold (`Decimal::one() - // threshold`) we get a 0% requirement for no // votes. Zero no votes do indeed meet a 0% @@ -217,7 +217,7 @@ impl SingleChoiceProposal { // and there are possible votes, then this is // rejected if there is a single no vote. // - // We need this check becuase + // We need this check because // otherwise when we invert the // threshold (`Decimal::one() - // threshold`) we get a 0% requirement @@ -954,7 +954,7 @@ mod test { )); // Total power of 33. 13 total votes. 8 no votes, 3 yes, 2 // abstain. 39.3% turnout. Expired. As it is expired we see if - // the 8 no votes excede the 50% failing threshold, which they + // the 8 no votes exceed the 50% failing threshold, which they // do. assert!(check_is_rejected( quorum.clone(), @@ -980,7 +980,7 @@ mod test { // Over quorum, but under threshold fails if the proposal is // not expired. If the proposal is expired though it passes as // the total vote count used is the number of votes, and not - // the total number of votes avaliable. + // the total number of votes available. assert!(check_is_rejected( quorum.clone(), failing.clone(), diff --git a/contracts/test/dao-proposal-hook-counter/src/msg.rs b/contracts/test/dao-proposal-hook-counter/src/msg.rs index ad6048228..63fed29fe 100644 --- a/contracts/test/dao-proposal-hook-counter/src/msg.rs +++ b/contracts/test/dao-proposal-hook-counter/src/msg.rs @@ -1,4 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; +#[allow(unused_imports)] use cosmwasm_std::Uint128; use dao_hooks::{proposal::ProposalHookMsg, stake::StakeChangedHookMsg, vote::VoteHookMsg}; diff --git a/contracts/voting/dao-voting-cw20-staked/src/msg.rs b/contracts/voting/dao-voting-cw20-staked/src/msg.rs index bdb5e9f2a..2fc7b4da5 100644 --- a/contracts/voting/dao-voting-cw20-staked/src/msg.rs +++ b/contracts/voting/dao-voting-cw20-staked/src/msg.rs @@ -5,6 +5,7 @@ use cw20_base::msg::InstantiateMarketingInfo; use cw_utils::Duration; use dao_dao_macros::{active_query, cw20_token_query, voting_module_query}; +#[allow(unused_imports)] use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; /// Information about the staking contract to be used with this voting diff --git a/contracts/voting/dao-voting-cw721-staked/src/msg.rs b/contracts/voting/dao-voting-cw721-staked/src/msg.rs index 837851ed3..657dc1085 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/msg.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/msg.rs @@ -3,6 +3,7 @@ use cosmwasm_std::Binary; use cw721::Cw721ReceiveMsg; use cw_utils::Duration; use dao_dao_macros::{active_query, voting_module_query}; +#[allow(unused_imports)] use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; #[cw_serde] diff --git a/contracts/voting/dao-voting-token-staked/src/contract.rs b/contracts/voting/dao-voting-token-staked/src/contract.rs index a8d1e2d6d..53aaae888 100644 --- a/contracts/voting/dao-voting-token-staked/src/contract.rs +++ b/contracts/voting/dao-voting-token-staked/src/contract.rs @@ -30,15 +30,15 @@ use dao_voting::{ }, }; -use crate::error::ContractError; use crate::msg::{ ExecuteMsg, GetHooksResponse, InstantiateMsg, ListStakersResponse, MigrateMsg, QueryMsg, - StakerBalanceResponse, TokenInfo, + StakerBalanceResponse, }; use crate::state::{ Config, ACTIVE_THRESHOLD, CLAIMS, CONFIG, DAO, DENOM, HOOKS, MAX_CLAIMS, STAKED_BALANCES, STAKED_TOTAL, TOKEN_INSTANTIATION_INFO, TOKEN_ISSUER_CONTRACT, }; +use crate::{error::ContractError, msg::TokenInfo}; pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-voting-token-staked"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -135,10 +135,10 @@ pub fn instantiate( funds, } => { // Call factory contract. Use only a trusted factory contract, - // as this is a critical security component and valdiation of + // as this is a critical security component and validation of // setup will happen in the factory. Ok(Response::new() - .add_attribute("action", "intantiate") + .add_attribute("action", "instantiate") .add_attribute("token", "custom_factory") .add_submessage(SubMsg::reply_on_success( WasmMsg::Execute { diff --git a/contracts/voting/dao-voting-token-staked/src/msg.rs b/contracts/voting/dao-voting-token-staked/src/msg.rs index 98809ec9e..ac194b885 100644 --- a/contracts/voting/dao-voting-token-staked/src/msg.rs +++ b/contracts/voting/dao-voting-token-staked/src/msg.rs @@ -3,6 +3,7 @@ use cosmwasm_std::{Binary, Uint128}; use cw_utils::Duration; use dao_dao_macros::{active_query, native_token_query, voting_module_query}; use dao_interface::token::NewTokenInfo; +#[allow(unused_imports)] use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; #[cw_serde] diff --git a/contracts/voting/dao-voting-token-staked/src/tests/test_tube/integration_tests.rs b/contracts/voting/dao-voting-token-staked/src/tests/test_tube/integration_tests.rs index 77effcda6..926abe763 100644 --- a/contracts/voting/dao-voting-token-staked/src/tests/test_tube/integration_tests.rs +++ b/contracts/voting/dao-voting-token-staked/src/tests/test_tube/integration_tests.rs @@ -1,8 +1,3 @@ -use crate::{ - msg::{ExecuteMsg, InstantiateMsg, QueryMsg, TokenInfo}, - tests::test_tube::test_env::TokenVotingContract, - ContractError, -}; use cosmwasm_std::{to_json_binary, Addr, Coin, Decimal, Uint128, WasmMsg}; use cw_ownable::Ownership; use cw_tokenfactory_issuer::msg::{DenomUnit, QueryMsg as IssuerQueryMsg}; @@ -22,6 +17,12 @@ use osmosis_test_tube::{ RunnerError, }; +use crate::{ + msg::{ExecuteMsg, InstantiateMsg, QueryMsg, TokenInfo}, + tests::test_tube::test_env::TokenVotingContract, + ContractError, +}; + use super::test_env::{TestEnv, TestEnvBuilder, DENOM}; #[test] diff --git a/packages/cw-curves/Cargo.toml b/packages/cw-curves/Cargo.toml new file mode 100644 index 000000000..87c25b6cd --- /dev/null +++ b/packages/cw-curves/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "cw-curves" +authors = [ + "Ethan Frey ", + "Jake Hartnell", + "Adair ", + "Gabe Lopez ", +] +description = "A package for defining curves to be used in augmented bonding curves." +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +rust_decimal = { workspace = true } +integer-sqrt = { workspace = true } +integer-cbrt = { workspace = true } \ No newline at end of file diff --git a/packages/cw-curves/README.md b/packages/cw-curves/README.md new file mode 100644 index 000000000..9159a1070 --- /dev/null +++ b/packages/cw-curves/README.md @@ -0,0 +1,6 @@ +# CosmWasm Curves + +This package provides the curves to be used for +[cw-abc](../../contracts/external/cw-abc). + +It provides a framework for defining various types of curves, such as constant, linear, and square root curves. The library ensures precision in token operations allowing for the easy implementation of custom curves in CosmWasm-based applications. diff --git a/packages/cw-curves/src/curve.rs b/packages/cw-curves/src/curve.rs new file mode 100644 index 000000000..002c58019 --- /dev/null +++ b/packages/cw-curves/src/curve.rs @@ -0,0 +1,74 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Decimal as StdDecimal, Uint128}; +use rust_decimal::prelude::ToPrimitive; +use rust_decimal::Decimal; + +use crate::utils::decimal; + +/// This defines the curves we are using. +/// +/// I am struggling on what type to use for the math. Tokens are often stored as Uint128, +/// but they may have 6 or 9 digits. When using constant or linear functions, this doesn't matter +/// much, but for non-linear functions a lot more. Also, supply and reserve most likely have different +/// decimals... either we leave it for the callers to normalize and accept a `Decimal` input, +/// or we pass in `Uint128` as well as the decimal places for supply and reserve. +/// +/// After working the first route and realizing that `Decimal` is not all that great to work with +/// when you want to do more complex math than add and multiply `Uint128`, I decided to go the second +/// route. That made the signatures quite complex and my final idea was to pass in `supply_decimal` +/// and `reserve_decimal` in the curve constructors. +pub trait Curve { + /// Returns the spot price given the supply. + /// `f(x)` from the README + fn spot_price(&self, supply: Uint128) -> StdDecimal; + + /// Returns the total price paid up to purchase supply tokens (integral) + /// `F(x)` from the README + fn reserve(&self, supply: Uint128) -> Uint128; + + /// Inverse of reserve. Returns how many tokens would be issued + /// with a total paid amount of reserve. + /// `F^-1(x)` from the README + fn supply(&self, reserve: Uint128) -> Uint128; +} + +/// DecimalPlaces should be passed into curve constructors +#[cw_serde] +#[derive(Copy)] +pub struct DecimalPlaces { + /// Number of decimal places for the supply token (this is what was passed in cw20-base instantiate + pub supply: u32, + /// Number of decimal places for the reserve token (eg. 6 for uatom, 9 for nstep, 18 for wei) + pub reserve: u32, +} + +impl DecimalPlaces { + pub fn new(supply: u8, reserve: u8) -> Self { + DecimalPlaces { + supply: supply as u32, + reserve: reserve as u32, + } + } + + pub fn to_reserve(self, reserve: Decimal) -> Uint128 { + let factor = decimal(10u128.pow(self.reserve), 0); + let out = reserve * factor; + // TODO: execute overflow better? Result? + out.floor().to_u128().unwrap().into() + } + + pub fn to_supply(self, supply: Decimal) -> Uint128 { + let factor = decimal(10u128.pow(self.supply), 0); + let out = supply * factor; + // TODO: execute overflow better? Result? + out.floor().to_u128().unwrap().into() + } + + pub fn from_supply(&self, supply: Uint128) -> Decimal { + decimal(supply, self.supply) + } + + pub fn from_reserve(&self, reserve: Uint128) -> Decimal { + decimal(reserve, self.reserve) + } +} diff --git a/packages/cw-curves/src/curves/constant.rs b/packages/cw-curves/src/curves/constant.rs new file mode 100644 index 000000000..a9fed2310 --- /dev/null +++ b/packages/cw-curves/src/curves/constant.rs @@ -0,0 +1,39 @@ +use cosmwasm_std::{Decimal as StdDecimal, Uint128}; +use rust_decimal::Decimal; + +use crate::{utils::decimal_to_std, Curve, DecimalPlaces}; + +/// spot price is always a constant value +pub struct Constant { + pub value: Decimal, + pub normalize: DecimalPlaces, +} + +impl Constant { + pub fn new(value: Decimal, normalize: DecimalPlaces) -> Self { + Self { value, normalize } + } +} + +impl Curve for Constant { + // we need to normalize value with the reserve decimal places + // (eg 0.1 value would return 100_000 if reserve was uatom) + fn spot_price(&self, _supply: Uint128) -> StdDecimal { + // f(x) = self.value + decimal_to_std(self.value) + } + + /// Returns total number of reserve tokens needed to purchase a given number of supply tokens. + /// Note that both need to be normalized. + fn reserve(&self, supply: Uint128) -> Uint128 { + // f(x) = supply * self.value + let reserve = self.normalize.from_supply(supply) * self.value; + self.normalize.to_reserve(reserve) + } + + fn supply(&self, reserve: Uint128) -> Uint128 { + // f(x) = reserve / self.value + let supply = self.normalize.from_reserve(reserve) / self.value; + self.normalize.to_supply(supply) + } +} diff --git a/packages/cw-curves/src/curves/linear.rs b/packages/cw-curves/src/curves/linear.rs new file mode 100644 index 000000000..c823a25b2 --- /dev/null +++ b/packages/cw-curves/src/curves/linear.rs @@ -0,0 +1,44 @@ +use cosmwasm_std::{Decimal as StdDecimal, Uint128}; +use rust_decimal::Decimal; + +use crate::{ + utils::{decimal_to_std, square_root}, + Curve, DecimalPlaces, +}; + +/// spot_price is slope * supply +pub struct Linear { + pub slope: Decimal, + pub normalize: DecimalPlaces, +} + +impl Linear { + pub fn new(slope: Decimal, normalize: DecimalPlaces) -> Self { + Self { slope, normalize } + } +} + +impl Curve for Linear { + fn spot_price(&self, supply: Uint128) -> StdDecimal { + // f(x) = supply * self.value + let out = self.normalize.from_supply(supply) * self.slope; + decimal_to_std(out) + } + + fn reserve(&self, supply: Uint128) -> Uint128 { + // f(x) = self.slope * supply * supply / 2 + let normalized = self.normalize.from_supply(supply); + let square = normalized * normalized; + // Note: multiplying by 0.5 is much faster than dividing by 2 + let reserve = square * self.slope * Decimal::new(5, 1); + self.normalize.to_reserve(reserve) + } + + fn supply(&self, reserve: Uint128) -> Uint128 { + // f(x) = (2 * reserve / self.slope) ^ 0.5 + // note: use addition here to optimize 2* operation + let square = self.normalize.from_reserve(reserve + reserve) / self.slope; + let supply = square_root(square); + self.normalize.to_supply(supply) + } +} diff --git a/packages/cw-curves/src/curves/mod.rs b/packages/cw-curves/src/curves/mod.rs new file mode 100644 index 000000000..e25a90429 --- /dev/null +++ b/packages/cw-curves/src/curves/mod.rs @@ -0,0 +1,8 @@ +pub mod constant; +pub use constant::Constant; + +pub mod linear; +pub use linear::Linear; + +pub mod square_root; +pub use square_root::SquareRoot; diff --git a/packages/cw-curves/src/curves/square_root.rs b/packages/cw-curves/src/curves/square_root.rs new file mode 100644 index 000000000..539dce07f --- /dev/null +++ b/packages/cw-curves/src/curves/square_root.rs @@ -0,0 +1,44 @@ +use cosmwasm_std::{Decimal as StdDecimal, Uint128}; +use rust_decimal::Decimal; + +use crate::{ + utils::{cube_root, decimal_to_std, square_root}, + Curve, DecimalPlaces, +}; + +/// spot_price is slope * (supply)^0.5 +pub struct SquareRoot { + pub slope: Decimal, + pub normalize: DecimalPlaces, +} + +impl SquareRoot { + pub fn new(slope: Decimal, normalize: DecimalPlaces) -> Self { + Self { slope, normalize } + } +} + +impl Curve for SquareRoot { + fn spot_price(&self, supply: Uint128) -> StdDecimal { + // f(x) = self.slope * supply^0.5 + let square = self.normalize.from_supply(supply); + let root = square_root(square); + decimal_to_std(root * self.slope) + } + + fn reserve(&self, supply: Uint128) -> Uint128 { + // f(x) = self.slope * supply * supply^0.5 / 1.5 + let normalized = self.normalize.from_supply(supply); + let root = square_root(normalized); + let reserve = self.slope * normalized * root / Decimal::new(15, 1); + self.normalize.to_reserve(reserve) + } + + fn supply(&self, reserve: Uint128) -> Uint128 { + // f(x) = (1.5 * reserve / self.slope) ^ (2/3) + let base = self.normalize.from_reserve(reserve) * Decimal::new(15, 1) / self.slope; + let squared = base * base; + let supply = cube_root(squared); + self.normalize.to_supply(supply) + } +} diff --git a/packages/cw-curves/src/lib.rs b/packages/cw-curves/src/lib.rs new file mode 100644 index 000000000..a4b6170a5 --- /dev/null +++ b/packages/cw-curves/src/lib.rs @@ -0,0 +1,8 @@ +pub mod curve; +pub mod curves; +pub mod utils; + +#[cfg(test)] +mod tests; + +pub use curve::{Curve, DecimalPlaces}; diff --git a/packages/cw-curves/src/tests.rs b/packages/cw-curves/src/tests.rs new file mode 100644 index 000000000..97d60c17f --- /dev/null +++ b/packages/cw-curves/src/tests.rs @@ -0,0 +1,118 @@ +// TODO: test DecimalPlaces return proper decimals + +use cosmwasm_std::{Decimal as StdDecimal, Uint128}; + +use crate::{ + curves::{Constant, Linear, SquareRoot}, + utils::decimal, + Curve, DecimalPlaces, +}; + +#[test] +fn constant_curve() { + // supply is nstep (9), reserve is uatom (6) + let normalize = DecimalPlaces::new(9, 6); + let curve = Constant::new(decimal(15u128, 1), normalize); + + // do some sanity checks.... + // spot price is always 1.5 ATOM + assert_eq!( + StdDecimal::percent(150), + curve.spot_price(Uint128::new(123)) + ); + + // if we have 30 STEP, we should have 45 ATOM + let reserve = curve.reserve(Uint128::new(30_000_000_000)); + assert_eq!(Uint128::new(45_000_000), reserve); + + // if we have 36 ATOM, we should have 24 STEP + let supply = curve.supply(Uint128::new(36_000_000)); + assert_eq!(Uint128::new(24_000_000_000), supply); +} + +#[test] +fn linear_curve() { + // supply is usdt (2), reserve is btc (8) + let normalize = DecimalPlaces::new(2, 8); + // slope is 0.1 (eg hits 1.0 after 10btc) + let curve = Linear::new(decimal(1u128, 1), normalize); + + // do some sanity checks.... + // spot price is 0.1 with 1 USDT supply + assert_eq!( + StdDecimal::permille(100), + curve.spot_price(Uint128::new(100)) + ); + // spot price is 1.7 with 17 USDT supply + assert_eq!( + StdDecimal::permille(1700), + curve.spot_price(Uint128::new(1700)) + ); + // spot price is 0.212 with 2.12 USDT supply + assert_eq!( + StdDecimal::permille(212), + curve.spot_price(Uint128::new(212)) + ); + + // if we have 10 USDT, we should have 5 BTC + let reserve = curve.reserve(Uint128::new(1000)); + assert_eq!(Uint128::new(500_000_000), reserve); + // if we have 20 USDT, we should have 20 BTC + let reserve = curve.reserve(Uint128::new(2000)); + assert_eq!(Uint128::new(2_000_000_000), reserve); + + // if we have 1.25 BTC, we should have 5 USDT + let supply = curve.supply(Uint128::new(125_000_000)); + assert_eq!(Uint128::new(500), supply); + // test square root rounding + // TODO: test when supply has many more decimal places than reserve + // if we have 1.11 BTC, we should have 4.7116875957... USDT + let supply = curve.supply(Uint128::new(111_000_000)); + assert_eq!(Uint128::new(471), supply); +} + +#[test] +fn sqrt_curve() { + // supply is utree (6) reserve is chf (2) + let normalize = DecimalPlaces::new(6, 2); + // slope is 0.35 (eg hits 0.35 after 1 chf, 3.5 after 100chf) + let curve = SquareRoot::new(decimal(35u128, 2), normalize); + + // do some sanity checks.... + // spot price is 0.35 with 1 TREE supply + assert_eq!( + StdDecimal::percent(35), + curve.spot_price(Uint128::new(1_000_000)) + ); + // spot price is 3.5 with 100 TREE supply + assert_eq!( + StdDecimal::percent(350), + curve.spot_price(Uint128::new(100_000_000)) + ); + // spot price should be 23.478713763747788 with 4500 TREE supply (test rounding and reporting here) + // rounds off around 8-9 sig figs (note diff for last points) + assert_eq!( + StdDecimal::from_ratio(2347871365u128, 100_000_000u128), + curve.spot_price(Uint128::new(4_500_000_000)) + ); + + // if we have 1 TREE, we should have 0.2333333333333 CHF + let reserve = curve.reserve(Uint128::new(1_000_000)); + assert_eq!(Uint128::new(23), reserve); + // if we have 100 TREE, we should have 233.333333333 CHF + let reserve = curve.reserve(Uint128::new(100_000_000)); + assert_eq!(Uint128::new(23_333), reserve); + // test rounding + // if we have 235 TREE, we should have 840.5790828021146 CHF + let reserve = curve.reserve(Uint128::new(235_000_000)); + assert_eq!(Uint128::new(84_057), reserve); // round down + + // // if we have 0.23 CHF, we should have 0.990453 TREE (round down) + let supply = curve.supply(Uint128::new(23)); + assert_eq!(Uint128::new(990_000), supply); + // if we have 840.58 CHF, we should have 235.000170 TREE (round down) + let supply = curve.supply(Uint128::new(84058)); + assert_eq!(Uint128::new(235_000_000), supply); +} + +// Idea: generic test that curve.supply(curve.reserve(supply)) == supply (or within some small rounding margin) diff --git a/packages/cw-curves/src/utils.rs b/packages/cw-curves/src/utils.rs new file mode 100644 index 000000000..0f8606045 --- /dev/null +++ b/packages/cw-curves/src/utils.rs @@ -0,0 +1,63 @@ +use cosmwasm_std::Decimal as StdDecimal; +use integer_cbrt::IntegerCubeRoot; +use integer_sqrt::IntegerSquareRoot; +use rust_decimal::prelude::ToPrimitive; +use rust_decimal::Decimal; +use std::str::FromStr; + +/// decimal returns an object = num * 10 ^ -scale +/// We use this function in contract.rs rather than call the crate constructor +/// itself, in case we want to swap out the implementation, we can do it only in this file. +pub fn decimal>(num: T, scale: u32) -> Decimal { + Decimal::from_i128_with_scale(num.into() as i128, scale) +} + +/// StdDecimal stores as a u128 with 18 decimal points of precision +pub fn decimal_to_std(x: Decimal) -> StdDecimal { + // this seems straight-forward (if inefficient), converting via string representation + // TODO: execute errors better? Result? + StdDecimal::from_str(&x.to_string()).unwrap() + + // // maybe a better approach doing math, not sure about rounding + // + // // try to preserve decimal points, max 9 + // let digits = min(x.scale(), 9); + // let multiplier = 10u128.pow(digits); + // + // // we multiply up before we round off to u128, + // // let StdDecimal do its best to keep these decimal places + // let nominator = (x * decimal(multiplier, 0)).to_u128().unwrap(); + // StdDecimal::from_ratio(nominator, multiplier) +} + +// we multiply by 10^18, turn to int, take square root, then divide by 10^9 as we convert back to decimal +pub(crate) fn square_root(square: Decimal) -> Decimal { + // must be even + // TODO: this can overflow easily at 18... what is a good value? + const EXTRA_DIGITS: u32 = 12; + let multiplier = 10u128.saturating_pow(EXTRA_DIGITS); + + // multiply by 10^18 and turn to u128 + let extended = square * decimal(multiplier, 0); + let extended = extended.floor().to_u128().unwrap(); + + // take square root, and build a decimal again + let root = extended.integer_sqrt(); + decimal(root, EXTRA_DIGITS / 2) +} + +// we multiply by 10^9, turn to int, take cube root, then divide by 10^3 as we convert back to decimal +pub(crate) fn cube_root(cube: Decimal) -> Decimal { + // must be multiple of 3 + // TODO: what is a good value? + const EXTRA_DIGITS: u32 = 9; + let multiplier = 10u128.saturating_pow(EXTRA_DIGITS); + + // multiply out and turn to u128 + let extended = cube * decimal(multiplier, 0); + let extended = extended.floor().to_u128().unwrap(); + + // take cube root, and build a decimal again + let root = extended.integer_cbrt(); + decimal(root, EXTRA_DIGITS / 3) +} diff --git a/packages/cw-paginate-storage/src/lib.rs b/packages/cw-paginate-storage/src/lib.rs index 61f420f3a..ec1edc410 100644 --- a/packages/cw-paginate-storage/src/lib.rs +++ b/packages/cw-paginate-storage/src/lib.rs @@ -114,7 +114,7 @@ where } /// Same as `paginate_map` but only returns the keys. For use with -/// `SnaphotMap`. +/// `SnapshotMap`. pub fn paginate_snapshot_map_keys<'a, 'b, K, V, R: 'static>( deps: Deps, map: &SnapshotMap<'a, K, V>, diff --git a/packages/dao-interface/src/msg.rs b/packages/dao-interface/src/msg.rs index 969288433..accf36bc9 100644 --- a/packages/dao-interface/src/msg.rs +++ b/packages/dao-interface/src/msg.rs @@ -174,7 +174,7 @@ pub enum QueryMsg { /// Gets the address associated with an item key. #[returns(crate::query::GetItemResponse)] GetItem { key: String }, - /// Lists all of the items associted with the contract. For + /// Lists all of the items associated with the contract. For /// example, given the items `{ "group": "foo", "subdao": "bar"}` /// this query would return `[("group", "foo"), ("subdao", /// "bar")]`. diff --git a/packages/dao-testing/Cargo.toml b/packages/dao-testing/Cargo.toml index 51d0685b4..a86b65db6 100644 --- a/packages/dao-testing/Cargo.toml +++ b/packages/dao-testing/Cargo.toml @@ -34,6 +34,7 @@ rand = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +cw-abc = { workspace = true } cw-core-v1 = { workspace = true, features = ["library"] } cw-hooks = { workspace = true } cw-proposal-single-v1 = { workspace = true } @@ -42,6 +43,7 @@ cw20-stake = { workspace = true } cw721-base = { workspace = true } cw721-roles = { workspace = true } cw-tokenfactory-issuer = { workspace = true } +dao-abc-factory = { workspace = true } dao-dao-core = { workspace = true, features = ["library"] } dao-interface = { workspace = true } dao-pre-propose-multiple = { workspace = true } diff --git a/packages/dao-testing/src/test_tube/cw_abc.rs b/packages/dao-testing/src/test_tube/cw_abc.rs new file mode 100644 index 000000000..c0b17e29d --- /dev/null +++ b/packages/dao-testing/src/test_tube/cw_abc.rs @@ -0,0 +1,181 @@ +use cosmwasm_std::Coin; +use cw_abc::{ + msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, + ContractError, +}; +use osmosis_test_tube::{ + osmosis_std::types::cosmwasm::wasm::v1::{ + MsgExecuteContractResponse, MsgMigrateContract, MsgMigrateContractResponse, + }, + Account, Module, OsmosisTestApp, Runner, RunnerError, RunnerExecuteResult, SigningAccount, + Wasm, +}; +use serde::de::DeserializeOwned; +use std::fmt::Debug; +use std::path::PathBuf; + +#[derive(Debug)] +pub struct CwAbc<'a> { + pub app: &'a OsmosisTestApp, + pub code_id: u64, + pub contract_addr: String, +} + +impl<'a> CwAbc<'a> { + pub fn new( + app: &'a OsmosisTestApp, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + ) -> Result { + let wasm = Wasm::new(app); + let token_creation_fee = Coin::new(10000000, "uosmo"); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + let contract_addr = wasm + .instantiate( + code_id, + &instantiate_msg, + Some(&signer.address()), + None, + &[token_creation_fee], + signer, + )? + .data + .address; + + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + pub fn new_with_values( + app: &'a OsmosisTestApp, + code_id: u64, + contract_addr: String, + ) -> Result { + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + /// uploads contract and returns a code ID + pub fn upload(app: &OsmosisTestApp, signer: &SigningAccount) -> Result { + let wasm = Wasm::new(app); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + Ok(code_id) + } + + pub fn instantiate( + app: &'a OsmosisTestApp, + code_id: u64, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + ) -> Result { + let wasm = Wasm::new(app); + let contract_addr = wasm + .instantiate( + code_id, + &instantiate_msg, + Some(&signer.address()), + None, + &[], + signer, + )? + .data + .address; + + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + // executes + pub fn execute( + &self, + execute_msg: &ExecuteMsg, + funds: &[Coin], + signer: &SigningAccount, + ) -> RunnerExecuteResult { + let wasm = Wasm::new(self.app); + wasm.execute(&self.contract_addr, execute_msg, funds, signer) + } + + // queries + pub fn query(&self, query_msg: &QueryMsg) -> Result + where + T: DeserializeOwned, + { + let wasm = Wasm::new(self.app); + wasm.query(&self.contract_addr, query_msg) + } + + pub fn migrate( + &self, + testdata: &str, + signer: &SigningAccount, + ) -> RunnerExecuteResult { + let wasm = Wasm::new(self.app); + let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let wasm_byte_code = + std::fs::read(manifest_path.join("tests").join("testdata").join(testdata)).unwrap(); + + let code_id = wasm.store_code(&wasm_byte_code, None, signer)?.data.code_id; + self.app.execute( + MsgMigrateContract { + sender: signer.address(), + contract: self.contract_addr.clone(), + code_id, + msg: serde_json::to_vec(&MigrateMsg {}).unwrap(), + }, + "/cosmwasm.wasm.v1.MsgMigrateContract", + signer, + ) + } + + fn get_wasm_byte_code() -> Vec { + let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let byte_code = std::fs::read( + manifest_path + .join("..") + .join("..") + .join("artifacts") + .join("cw_abc.wasm"), + ); + match byte_code { + Ok(byte_code) => byte_code, + // On arm processors, the above path is not found, so we try the following path + Err(_) => std::fs::read( + manifest_path + .join("..") + .join("..") + .join("artifacts") + .join("cw_abc-aarch64.wasm"), + ) + .unwrap(), + } + } + + pub fn execute_error(err: ContractError) -> RunnerError { + RunnerError::ExecuteError { + msg: format!( + "failed to execute message; message index: 0: {}: execute wasm contract failed", + err + ), + } + } +} diff --git a/packages/dao-testing/src/test_tube/dao_abc_factory.rs b/packages/dao-testing/src/test_tube/dao_abc_factory.rs new file mode 100644 index 000000000..d000548c4 --- /dev/null +++ b/packages/dao-testing/src/test_tube/dao_abc_factory.rs @@ -0,0 +1,129 @@ +use cosmwasm_std::Coin; +use dao_abc_factory::{ + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + ContractError, +}; +use osmosis_test_tube::{ + osmosis_std::types::cosmwasm::wasm::v1::MsgExecuteContractResponse, Account, Module, + OsmosisTestApp, RunnerError, RunnerExecuteResult, SigningAccount, Wasm, +}; +use serde::de::DeserializeOwned; +use std::fmt::Debug; +use std::path::PathBuf; + +#[derive(Debug)] +pub struct AbcFactoryContract<'a> { + pub app: &'a OsmosisTestApp, + pub code_id: u64, + pub contract_addr: String, +} + +impl<'a> AbcFactoryContract<'a> { + pub fn new( + app: &'a OsmosisTestApp, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + ) -> Result { + let wasm = Wasm::new(app); + let token_creation_fee = Coin::new(10000000, "uosmo"); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + let contract_addr = wasm + .instantiate( + code_id, + &instantiate_msg, + Some(&signer.address()), + None, + &[token_creation_fee], + signer, + )? + .data + .address; + + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + pub fn new_with_values( + app: &'a OsmosisTestApp, + code_id: u64, + contract_addr: String, + ) -> Result { + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + /// uploads contract and returns a code ID + pub fn upload(app: &OsmosisTestApp, signer: &SigningAccount) -> Result { + let wasm = Wasm::new(app); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + Ok(code_id) + } + + // executes + pub fn execute( + &self, + execute_msg: &ExecuteMsg, + funds: &[Coin], + signer: &SigningAccount, + ) -> RunnerExecuteResult { + let wasm = Wasm::new(self.app); + wasm.execute(&self.contract_addr, execute_msg, funds, signer) + } + + // queries + pub fn query(&self, query_msg: &QueryMsg) -> Result + where + T: DeserializeOwned, + { + let wasm = Wasm::new(self.app); + wasm.query(&self.contract_addr, query_msg) + } + + fn get_wasm_byte_code() -> Vec { + let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let byte_code = std::fs::read( + manifest_path + .join("..") + .join("..") + .join("artifacts") + .join("dao_abc_factory.wasm"), + ); + match byte_code { + Ok(byte_code) => byte_code, + // On arm processors, the above path is not found, so we try the following path + Err(_) => std::fs::read( + manifest_path + .join("..") + .join("..") + .join("artifacts") + .join("dao_abc_factory-aarch64.wasm"), + ) + .unwrap(), + } + } + + pub fn execute_error(err: ContractError) -> RunnerError { + RunnerError::ExecuteError { + msg: format!( + "failed to execute message; message index: 0: {}: execute wasm contract failed", + err + ), + } + } +} diff --git a/packages/dao-testing/src/test_tube/dao_voting_token_staked.rs b/packages/dao-testing/src/test_tube/dao_voting_token_staked.rs new file mode 100644 index 000000000..df1f6b7cc --- /dev/null +++ b/packages/dao-testing/src/test_tube/dao_voting_token_staked.rs @@ -0,0 +1,129 @@ +use cosmwasm_std::Coin; +use dao_voting_token_staked::{ + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + ContractError, +}; +use osmosis_test_tube::{ + osmosis_std::types::cosmwasm::wasm::v1::MsgExecuteContractResponse, Account, Module, + OsmosisTestApp, RunnerError, RunnerExecuteResult, SigningAccount, Wasm, +}; +use serde::de::DeserializeOwned; +use std::fmt::Debug; +use std::path::PathBuf; + +#[derive(Debug)] +pub struct TokenVotingContract<'a> { + pub app: &'a OsmosisTestApp, + pub code_id: u64, + pub contract_addr: String, +} + +impl<'a> TokenVotingContract<'a> { + pub fn new( + app: &'a OsmosisTestApp, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + ) -> Result { + let wasm = Wasm::new(app); + let token_creation_fee = Coin::new(10000000, "uosmo"); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + let contract_addr = wasm + .instantiate( + code_id, + &instantiate_msg, + Some(&signer.address()), + None, + &[token_creation_fee], + signer, + )? + .data + .address; + + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + pub fn new_with_values( + app: &'a OsmosisTestApp, + code_id: u64, + contract_addr: String, + ) -> Result { + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + /// uploads contract and returns a code ID + pub fn upload(app: &OsmosisTestApp, signer: &SigningAccount) -> Result { + let wasm = Wasm::new(app); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + Ok(code_id) + } + + // executes + pub fn execute( + &self, + execute_msg: &ExecuteMsg, + funds: &[Coin], + signer: &SigningAccount, + ) -> RunnerExecuteResult { + let wasm = Wasm::new(self.app); + wasm.execute(&self.contract_addr, execute_msg, funds, signer) + } + + // queries + pub fn query(&self, query_msg: &QueryMsg) -> Result + where + T: DeserializeOwned, + { + let wasm = Wasm::new(self.app); + wasm.query(&self.contract_addr, query_msg) + } + + fn get_wasm_byte_code() -> Vec { + let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let byte_code = std::fs::read( + manifest_path + .join("..") + .join("..") + .join("artifacts") + .join("dao_voting_token_staked.wasm"), + ); + match byte_code { + Ok(byte_code) => byte_code, + // On arm processors, the above path is not found, so we try the following path + Err(_) => std::fs::read( + manifest_path + .join("..") + .join("..") + .join("artifacts") + .join("dao_voting_token_staked-aarch64.wasm"), + ) + .unwrap(), + } + } + + pub fn execute_error(err: ContractError) -> RunnerError { + RunnerError::ExecuteError { + msg: format!( + "failed to execute message; message index: 0: {}: execute wasm contract failed", + err + ), + } + } +} diff --git a/packages/dao-testing/src/test_tube/mod.rs b/packages/dao-testing/src/test_tube/mod.rs index 0af2999a4..f7e1cb743 100644 --- a/packages/dao-testing/src/test_tube/mod.rs +++ b/packages/dao-testing/src/test_tube/mod.rs @@ -2,15 +2,21 @@ // and also, tarpaulin will not be able read coverage out of wasm binary anyway #![cfg(not(tarpaulin))] -// Integrationg tests using an actual chain binary, requires +// Integration tests using an actual chain binary, requires // the "test-tube" feature to be enabled // cargo test --features test-tube +#[cfg(feature = "test-tube")] +pub mod cw_abc; + #[cfg(feature = "test-tube")] pub mod cw_tokenfactory_issuer; #[cfg(feature = "test-tube")] pub mod cw721_base; +#[cfg(feature = "test-tube")] +pub mod dao_abc_factory; + #[cfg(feature = "test-tube")] pub mod dao_dao_core; @@ -19,3 +25,6 @@ pub mod dao_proposal_single; #[cfg(feature = "test-tube")] pub mod dao_test_custom_factory; + +#[cfg(feature = "test-tube")] +pub mod dao_voting_token_staked; diff --git a/packages/dao-voting/src/voting.rs b/packages/dao-voting/src/voting.rs index 2aabafc2e..48be99a49 100644 --- a/packages/dao-voting/src/voting.rs +++ b/packages/dao-voting/src/voting.rs @@ -179,10 +179,10 @@ impl Votes { /// Computes the total number of votes cast. /// - /// NOTE: The total number of votes avaliable from a voting module + /// NOTE: The total number of votes available from a voting module /// is a `Uint128`. As it is not possible to vote twice we know /// that the sum of votes must be <= 2^128 and can safely return a - /// `Uint128` from this function. A missbehaving voting power + /// `Uint128` from this function. A misbehaving voting power /// module may break this invariant. pub fn total(&self) -> Uint128 { self.yes + self.no + self.abstain