From 088143c2724e28793ba6f5d8e623ba15fcba1fe6 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Wed, 19 Apr 2023 18:55:01 -0700 Subject: [PATCH 01/56] Initial commit --- Cargo.lock | 445 +++++++++++++--- contracts/external/cw-abc/.cargo/config | 6 + contracts/external/cw-abc/Cargo.toml | 34 ++ contracts/external/cw-abc/NOTICE | 15 + contracts/external/cw-abc/README.md | 79 +++ contracts/external/cw-abc/examples/schema.rs | 11 + contracts/external/cw-abc/src/contract.rs | 505 +++++++++++++++++++ contracts/external/cw-abc/src/curves.rs | 356 +++++++++++++ contracts/external/cw-abc/src/error.rs | 18 + contracts/external/cw-abc/src/lib.rs | 7 + contracts/external/cw-abc/src/msg.rs | 92 ++++ contracts/external/cw-abc/src/state.rs | 39 ++ 12 files changed, 1532 insertions(+), 75 deletions(-) create mode 100644 contracts/external/cw-abc/.cargo/config create mode 100644 contracts/external/cw-abc/Cargo.toml create mode 100644 contracts/external/cw-abc/NOTICE create mode 100644 contracts/external/cw-abc/README.md create mode 100644 contracts/external/cw-abc/examples/schema.rs create mode 100644 contracts/external/cw-abc/src/contract.rs create mode 100644 contracts/external/cw-abc/src/curves.rs create mode 100644 contracts/external/cw-abc/src/error.rs create mode 100644 contracts/external/cw-abc/src/lib.rs create mode 100644 contracts/external/cw-abc/src/msg.rs create mode 100644 contracts/external/cw-abc/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index 6128b4f1c..defdef3de 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,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "cw-abc" +version = "0.0.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "integer-cbrt", + "integer-sqrt", + "rust_decimal", + "thiserror", + "token-bindings", +] + [[package]] name = "cw-address-like" version = "1.0.4" @@ -2369,9 +2455,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 +2514,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 +2576,7 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -2656,11 +2748,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 +2768,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 +2797,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 +2812,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2", "tokio", "tower-service", "tracing", @@ -2825,6 +2917,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 +3000,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 +3074,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 +3141,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", @@ -3100,9 +3210,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 +3374,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -3295,7 +3405,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -3343,14 +3453,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 +3553,7 @@ dependencies = [ "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -3431,6 +3574,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 +3603,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 +3674,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 +3739,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 +3789,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 +3819,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 +3863,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 +3919,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 +4026,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -3830,14 +4059,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 +4146,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" @@ -3926,16 +4161,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "socket2" version = "0.5.5" @@ -4064,21 +4289,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 +4495,7 @@ dependencies = [ "tokio", "tracing", "url", - "uuid", + "uuid 0.8.2", "walkdir", ] @@ -4286,7 +4529,7 @@ dependencies = [ "tokio", "tracing", "url", - "uuid", + "uuid 0.8.2", "walkdir", ] @@ -4338,22 +4581,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]] @@ -4388,11 +4631,22 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "token-bindings" +version = "0.9.0" +source = "git+https://github.com/CosmosContracts/token-bindings?rev=1412b94#1412b9498347c3e2a1f80fd106fffd9cec697ca0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "schemars", + "serde", +] + [[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 +4654,7 @@ dependencies = [ "mio", "num_cpus", "pin-project-lite", - "socket2 0.5.5", + "socket2", "tokio-macros", "windows-sys 0.48.0", ] @@ -4423,7 +4677,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -4471,6 +4725,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 +4853,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -4606,9 +4877,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 +4895,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 +4916,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 +4943,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 +5013,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", "wasm-bindgen-shared", ] @@ -4758,7 +5035,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4973,6 +5250,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 +5271,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 +5306,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] 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..399ebf190 --- /dev/null +++ b/contracts/external/cw-abc/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "cw-abc" +version = "0.0.1" +authors = ["Ethan Frey ", "Jake Hartnell"] +edition = { workspace = true } +description = "Implements an Augmented Bonding Curve" +license = "Apache-2.0" +repository = { workspace = true } + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cw-utils = { workspace = true } +cw2 = { workspace = true } +cw-storage-plus = { workspace = true } +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +thiserror = { workspace = true } +rust_decimal = "1.14.3" +integer-sqrt = "0.1.5" +integer-cbrt = "0.1.2" +# TODO publish this +token-bindings = { git = "https://github.com/CosmosContracts/token-bindings", rev = "1412b94" } + +[dev-dependencies] + 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..2c6791349 --- /dev/null +++ b/contracts/external/cw-abc/README.md @@ -0,0 +1,79 @@ +# cw-abc + +Implments an augmented bonding curve. + +Forked from and heavily inspired by the work on [cw20-bonding](https://github.com/cosmwasm/cw-tokens/tree/main/contracts/cw20-bonding). + +## Extended Reading + +- https://medium.com/commonsstack/deep-dive-augmented-bonding-curves-b5ca4fad4436 +- https://tokeneconomy.co/token-bonding-curves-in-practice-3eb904720cb8 + +## TODO + +Taking inspiration from [this article](https://medium.com/commonsstack/deep-dive-augmented-bonding-curves-b5ca4fad4436) on augmented bonding curves: + +- [ ] Implement Hatch Phase to allow projects to raise funds +- [ ] Implement optional Exit Tax +- [ ] Optionally vest tokens during the hatch phase +- [ ] Update `cw-vesting` to allow for partcipating in DAOs? + +## Design + +There are two variants: + +Minting: When the input is sent to the contract via `ExecuteMsg::Buy{}` +those tokens remain on the contract and it issues it's own token to the +sender's account (known as _supply_ token). + +Burning: We override the burn function to not only burn the requested tokens, +but also release a proper number of the input tokens to the account that burnt +the custom token + +Curves: `handle` specifies a bonding function, which is sent to parameterize +`handle_fn` (which does all the work). The curve is set when compiling +the contract. In fact many contracts can just wrap `cw-abc` and +specify the custom curve parameter. + +Read more about [bonding curve math here](https://yos.io/2018/11/10/bonding-curves/) + +Note: the first version only accepts native tokens as the + +### 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)` + +We will only implement these curves to start with, and leave it to others to import this with more complex curves, +such as logarithms. 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/src/contract.rs b/contracts/external/cw-abc/src/contract.rs new file mode 100644 index 000000000..dcd71e32a --- /dev/null +++ b/contracts/external/cw-abc/src/contract.rs @@ -0,0 +1,505 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + attr, coins, to_binary, Addr, BankMsg, Binary, Deps, DepsMut, Env, MessageInfo, Response, + StdError, StdResult, Uint128, +}; +use cw2::set_contract_version; +use token_bindings::{Metadata, TokenFactoryMsg, TokenFactoryQuery, TokenMsg, TokenQuerier}; + +use crate::curves::DecimalPlaces; +use crate::error::ContractError; +use crate::msg::{CurveFn, CurveInfoResponse, ExecuteMsg, InstantiateMsg, QueryMsg}; +use crate::state::{CurveState, CURVE_STATE, CURVE_TYPE, DENOM}; +use cw_utils::{must_pay, nonpayable}; + +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:cw20-bonding"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +// By default, the prefix for token factory tokens is "factory" +const DENOM_PREFIX: &str = "factory"; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result, ContractError> { + nonpayable(&info)?; + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + if msg.subdenom.eq("") { + return Err(ContractError::InvalidSubdenom { + subdenom: msg.subdenom, + }); + } + + // Create denom with metadata + let create_denom_msg = TokenMsg::CreateDenom { + subdenom: msg.subdenom.clone(), + metadata: Some(msg.metadata), + }; + + let places = DecimalPlaces::new(msg.decimals, msg.reserve_decimals); + let supply = CurveState::new(msg.reserve_denom, places); + + // TODO validate denom? + + // Save the denom + DENOM.save( + deps.storage, + &format!( + "{}/{}/{}", + DENOM_PREFIX, + env.contract.address.to_string(), + msg.subdenom + ), + )?; + + CURVE_STATE.save(deps.storage, &supply)?; + + CURVE_TYPE.save(deps.storage, &msg.curve_type)?; + + Ok(Response::default().add_message(create_denom_msg)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result, ContractError> { + // 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_execute(deps, env, info, 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_execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, + curve_fn: CurveFn, +) -> Result, ContractError> { + match msg { + ExecuteMsg::Buy {} => execute_buy(deps, env, info, curve_fn), + ExecuteMsg::Burn { amount } => Ok(execute_sell(deps, env, info, curve_fn, amount)?), + } +} + +pub fn execute_buy( + deps: DepsMut, + env: Env, + info: MessageInfo, + curve_fn: CurveFn, +) -> Result, ContractError> { + let mut state = CURVE_STATE.load(deps.storage)?; + + let denom = DENOM.load(deps.storage)?; + let payment = must_pay(&info, &state.reserve_denom)?; + + // calculate how many tokens can be purchased with this and mint them + let curve = curve_fn(state.clone().decimals); + state.reserve += payment; + let new_supply = curve.supply(state.reserve); + let minted = new_supply + .checked_sub(state.supply) + .map_err(StdError::overflow)?; + state.supply = new_supply; + CURVE_STATE.save(deps.storage, &state)?; + + // mint tf token + let mint_msg = TokenMsg::MintTokens { + denom, + amount: minted, + mint_to_address: info.sender.to_string(), + }; + + Ok(Response::new() + .add_message(mint_msg) + .add_attribute("action", "buy") + .add_attribute("from", info.sender) + .add_attribute("reserve", payment) + .add_attribute("supply", minted)) +} + +pub fn execute_sell( + deps: DepsMut, + env: Env, + info: MessageInfo, + curve_fn: CurveFn, + amount: Uint128, +) -> Result, ContractError> { + let receiver = info.sender.clone(); + + let denom = DENOM.load(deps.storage)?; + let payment = must_pay(&info, &denom)?; + + // calculate how many tokens can be purchased with this and mint them + let mut state = CURVE_STATE.load(deps.storage)?; + let curve = curve_fn(state.clone().decimals); + state.supply = state + .supply + .checked_sub(amount) + .map_err(StdError::overflow)?; + let new_reserve = curve.reserve(state.supply); + let released = state + .reserve + .checked_sub(new_reserve) + .map_err(StdError::overflow)?; + state.reserve = new_reserve; + CURVE_STATE.save(deps.storage, &state)?; + + // Burn the tokens + let burn_msg = TokenMsg::BurnTokens { + denom, + amount: payment, + burn_from_address: info.sender.clone().to_string(), + }; + + // now send the tokens to the sender (TODO: for sell_from we do something else, right???) + let msg = BankMsg::Send { + to_address: receiver.to_string(), + amount: coins(released.u128(), state.reserve_denom), + }; + + Ok(Response::new() + .add_message(msg) + .add_message(burn_msg) + .add_attribute("action", "burn") + .add_attribute("from", info.sender) + .add_attribute("supply", amount) + .add_attribute("reserve", released)) +} + +#[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 InstantitateMsg 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_binary(&query_curve_info(deps, curve_fn)?), + // QueryMsg::GetDenom { + // creator_address, + // subdenom, + // } => to_binary(&get_denom(deps, creator_address, subdenom)), + } +} + +pub fn query_curve_info( + deps: Deps, + curve_fn: CurveFn, +) -> StdResult { + let CurveState { + reserve, + supply, + reserve_denom, + decimals, + } = 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, + spot_price, + reserve_denom, + }) +} + +// // TODO, maybe we don't need this +// pub fn get_denom( +// deps: Deps, +// creator_addr: String, +// subdenom: String, +// ) -> GetDenomResponse { +// let querier = TokenQuerier::new(&deps.querier); +// let response = querier.full_denom(creator_addr, subdenom).unwrap(); + +// GetDenomResponse { +// denom: response.denom, +// } +// } + +// fn validate_denom( +// deps: DepsMut, +// denom: String, +// ) -> Result<(), TokenFactoryError> { +// let denom_to_split = denom.clone(); +// let tokenfactory_denom_parts: Vec<&str> = denom_to_split.split('/').collect(); + +// if tokenfactory_denom_parts.len() != 3 { +// return Result::Err(TokenFactoryError::InvalidDenom { +// denom, +// message: std::format!( +// "denom must have 3 parts separated by /, had {}", +// tokenfactory_denom_parts.len() +// ), +// }); +// } + +// let prefix = tokenfactory_denom_parts[0]; +// let creator_address = tokenfactory_denom_parts[1]; +// let subdenom = tokenfactory_denom_parts[2]; + +// if !prefix.eq_ignore_ascii_case("factory") { +// return Result::Err(TokenFactoryError::InvalidDenom { +// denom, +// message: std::format!("prefix must be 'factory', was {}", prefix), +// }); +// } + +// // Validate denom by attempting to query for full denom +// let response = TokenQuerier::new(&deps.querier) +// .full_denom(String::from(creator_address), String::from(subdenom)); +// if response.is_err() { +// return Result::Err(TokenFactoryError::InvalidDenom { +// denom, +// message: response.err().unwrap().to_string(), +// }); +// } + +// Result::Ok(()) +// } + +// // this is poor mans "skip" flag +// #[cfg(test)] +// mod tests { +// use super::*; +// use crate::msg::CurveType; +// use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; +// use cosmwasm_std::{coin, Decimal, OverflowError, OverflowOperation, StdError, SubMsg}; +// use cw_utils::PaymentError; + +// const DENOM: &str = "satoshi"; +// const CREATOR: &str = "creator"; +// const INVESTOR: &str = "investor"; +// const BUYER: &str = "buyer"; + +// fn default_instantiate( +// decimals: u8, +// reserve_decimals: u8, +// curve_type: CurveType, +// ) -> InstantiateMsg { +// InstantiateMsg { +// name: "Bonded".to_string(), +// symbol: "EPOXY".to_string(), +// decimals, +// reserve_denom: DENOM.to_string(), +// reserve_decimals, +// curve_type, +// } +// } + +// fn get_balance>(deps: Deps, addr: U) -> Uint128 { +// query_balance(deps, addr.into()).unwrap().balance +// } + +// fn setup_test(deps: DepsMut, decimals: u8, reserve_decimals: u8, curve_type: CurveType) { +// // this matches `linear_curve` test case from curves.rs +// let creator = String::from(CREATOR); +// let msg = default_instantiate(decimals, reserve_decimals, curve_type); +// let info = mock_info(&creator, &[]); + +// // make sure we can instantiate with this +// let res = instantiate(deps, mock_env(), info, msg).unwrap(); +// assert_eq!(0, res.messages.len()); +// } + +// #[test] +// fn proper_instantiation() { +// let mut deps = mock_dependencies(); + +// // this matches `linear_curve` test case from curves.rs +// let creator = String::from("creator"); +// let curve_type = CurveType::SquareRoot { +// slope: Uint128::new(1), +// scale: 1, +// }; +// let msg = default_instantiate(2, 8, curve_type.clone()); +// let info = mock_info(&creator, &[]); + +// // make sure we can instantiate with this +// let res = instantiate(deps.as_mut(), mock_env(), info, msg.clone()).unwrap(); +// assert_eq!(0, res.messages.len()); + +// // token info is proper +// let token = query_token_info(deps.as_ref()).unwrap(); +// assert_eq!(&token.name, &msg.name); +// assert_eq!(&token.symbol, &msg.symbol); +// assert_eq!(token.decimals, 2); +// assert_eq!(token.total_supply, Uint128::zero()); + +// // curve state is sensible +// let state = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); +// assert_eq!(state.reserve, Uint128::zero()); +// assert_eq!(state.supply, Uint128::zero()); +// assert_eq!(state.reserve_denom.as_str(), DENOM); +// // spot price 0 as supply is 0 +// assert_eq!(state.spot_price, Decimal::zero()); + +// // curve type is stored properly +// let curve = CURVE_TYPE.load(&deps.storage).unwrap(); +// assert_eq!(curve_type, curve); + +// // no balance +// assert_eq!(get_balance(deps.as_ref(), &creator), Uint128::zero()); +// } + +// #[test] +// fn buy_issues_tokens() { +// let mut deps = mock_dependencies(); +// let curve_type = CurveType::Linear { +// slope: Uint128::new(1), +// scale: 1, +// }; +// setup_test(deps.as_mut(), 2, 8, curve_type.clone()); + +// // succeeds with proper token (5 BTC = 5*10^8 satoshi) +// let info = mock_info(INVESTOR, &coins(500_000_000, DENOM)); +// let buy = ExecuteMsg::Buy {}; +// execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap(); + +// // bob got 1000 EPOXY (10.00) +// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); +// assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::zero()); + +// // send them all to buyer +// let info = mock_info(INVESTOR, &[]); +// let send = ExecuteMsg::Transfer { +// recipient: BUYER.into(), +// amount: Uint128::new(1000), +// }; +// execute(deps.as_mut(), mock_env(), info, send).unwrap(); + +// // ensure balances updated +// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::zero()); +// assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::new(1000)); + +// // second stake needs more to get next 1000 EPOXY +// let info = mock_info(INVESTOR, &coins(1_500_000_000, DENOM)); +// execute(deps.as_mut(), mock_env(), info, buy).unwrap(); + +// // ensure balances updated +// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); +// assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::new(1000)); + +// // check curve info updated +// let curve = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); +// assert_eq!(curve.reserve, Uint128::new(2_000_000_000)); +// assert_eq!(curve.supply, Uint128::new(2000)); +// assert_eq!(curve.spot_price, Decimal::percent(200)); + +// // check token info updated +// let token = query_token_info(deps.as_ref()).unwrap(); +// assert_eq!(token.decimals, 2); +// assert_eq!(token.total_supply, Uint128::new(2000)); +// } + +// #[test] +// fn bonding_fails_with_wrong_denom() { +// let mut deps = mock_dependencies(); +// let curve_type = CurveType::Linear { +// slope: Uint128::new(1), +// scale: 1, +// }; +// setup_test(deps.as_mut(), 2, 8, curve_type); + +// // fails when no tokens sent +// let info = mock_info(INVESTOR, &[]); +// let buy = ExecuteMsg::Buy {}; +// let err = execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err(); +// assert_eq!(err, PaymentError::NoFunds {}.into()); + +// // fails when wrong tokens sent +// let info = mock_info(INVESTOR, &coins(1234567, "wei")); +// let err = execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err(); +// assert_eq!(err, PaymentError::MissingDenom(DENOM.into()).into()); + +// // fails when too many tokens sent +// let info = mock_info(INVESTOR, &[coin(3400022, DENOM), coin(1234567, "wei")]); +// let err = execute(deps.as_mut(), mock_env(), info, buy).unwrap_err(); +// assert_eq!(err, PaymentError::MultipleDenoms {}.into()); +// } + +// #[test] +// fn burning_sends_reserve() { +// let mut deps = mock_dependencies(); +// let curve_type = CurveType::Linear { +// slope: Uint128::new(1), +// scale: 1, +// }; +// setup_test(deps.as_mut(), 2, 8, curve_type.clone()); + +// // succeeds with proper token (20 BTC = 20*10^8 satoshi) +// let info = mock_info(INVESTOR, &coins(2_000_000_000, DENOM)); +// let buy = ExecuteMsg::Buy {}; +// execute(deps.as_mut(), mock_env(), info, buy).unwrap(); + +// // bob got 2000 EPOXY (20.00) +// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(2000)); + +// // cannot burn too much +// let info = mock_info(INVESTOR, &[]); +// let burn = ExecuteMsg::Burn { +// amount: Uint128::new(3000), +// }; +// let err = execute(deps.as_mut(), mock_env(), info, burn).unwrap_err(); +// // TODO check error + +// // burn 1000 EPOXY to get back 15BTC (*10^8) +// let info = mock_info(INVESTOR, &[]); +// let burn = ExecuteMsg::Burn { +// amount: Uint128::new(1000), +// }; +// let res = execute(deps.as_mut(), mock_env(), info, burn).unwrap(); + +// // balance is lower +// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); + +// // ensure we got our money back +// assert_eq!(1, res.messages.len()); +// assert_eq!( +// &res.messages[0], +// &SubMsg::new(BankMsg::Send { +// to_address: INVESTOR.into(), +// amount: coins(1_500_000_000, DENOM), +// }) +// ); + +// // check curve info updated +// let curve = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); +// assert_eq!(curve.reserve, Uint128::new(500_000_000)); +// assert_eq!(curve.supply, Uint128::new(1000)); +// assert_eq!(curve.spot_price, Decimal::percent(100)); + +// // check token info updated +// let token = query_token_info(deps.as_ref()).unwrap(); +// assert_eq!(token.decimals, 2); +// assert_eq!(token.total_supply, Uint128::new(1000)); +// } +// } diff --git a/contracts/external/cw-abc/src/curves.rs b/contracts/external/cw-abc/src/curves.rs new file mode 100644 index 000000000..ed757ce4b --- /dev/null +++ b/contracts/external/cw-abc/src/curves.rs @@ -0,0 +1,356 @@ +use cosmwasm_schema::cw_serde; +use integer_cbrt::IntegerCubeRoot; +use integer_sqrt::IntegerSquareRoot; +use rust_decimal::prelude::ToPrimitive; +use rust_decimal::Decimal; +use std::str::FromStr; + +use cosmwasm_std::{Decimal as StdDecimal, Uint128}; + +/// 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; +} + +/// 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 +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) +} + +/// 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.clone().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.clone().to_supply(supply) + } +} + +/// 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.clone().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.clone().to_supply(supply) + } +} + +/// 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.clone().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.clone().to_supply(supply) + } +} + +// we multiply by 10^18, turn to int, take square root, then divide by 10^9 as we convert back to decimal +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 +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) +} + +/// DecimalPlaces should be passed into curve constructors +#[cw_serde] +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) + } +} + +#[cfg(test)] +mod tests { + use super::*; + // TODO: test DecimalPlaces return proper decimals + + #[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/contracts/external/cw-abc/src/error.rs b/contracts/external/cw-abc/src/error.rs new file mode 100644 index 000000000..b3a8d2e41 --- /dev/null +++ b/contracts/external/cw-abc/src/error.rs @@ -0,0 +1,18 @@ +use cosmwasm_std::StdError; +use cw_utils::PaymentError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + Payment(#[from] PaymentError), + + #[error("Invalid subdenom: {subdenom:?}")] + InvalidSubdenom { subdenom: String }, + + #[error("Unauthorized")] + Unauthorized {}, +} diff --git a/contracts/external/cw-abc/src/lib.rs b/contracts/external/cw-abc/src/lib.rs new file mode 100644 index 000000000..8ecd1fa67 --- /dev/null +++ b/contracts/external/cw-abc/src/lib.rs @@ -0,0 +1,7 @@ +pub mod contract; +pub mod curves; +mod error; +pub mod msg; +pub mod state; + +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..711d2e7f2 --- /dev/null +++ b/contracts/external/cw-abc/src/msg.rs @@ -0,0 +1,92 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Decimal, Uint128}; +use token_bindings::Metadata; + +use crate::curves::{decimal, Constant, Curve, DecimalPlaces, Linear, SquareRoot}; + +#[cw_serde] +pub struct InstantiateMsg { + /// The denom to create + pub subdenom: String, + /// Metadata for the token to create + pub metadata: Metadata, + + /// TODO maybe we don't need this + pub decimals: u8, + + /// this is the reserve token denom (only support native for now) + pub reserve_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 reserve_decimals: u8, + + /// enum to store the curve parameters used for this contract + /// if you want to add a custom Curve, you should make a new contract that imports this one. + /// write a custom `instantiate`, and then dispatch `your::execute` -> `cw20_bonding::do_execute` + /// with your custom curve as a parameter (and same with `query` -> `do_query`) + pub curve_type: CurveType, +} + +pub type CurveFn = Box Box>; + +#[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) + } + } + } +} + +#[cw_serde] +pub enum ExecuteMsg { + /// Buy will attempt to purchase as many supply tokens as possible. + /// You must send only reserve tokens in that message + Buy {}, + /// Implements CW20. Burn is a base message to destroy tokens forever + Burn { amount: Uint128 }, +} + +#[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)] + CurveInfo {}, +} + +#[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, + pub spot_price: Decimal, + pub reserve_denom: 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..ffb91b74e --- /dev/null +++ b/contracts/external/cw-abc/src/state.rs @@ -0,0 +1,39 @@ +use cosmwasm_schema::cw_serde; + +use cosmwasm_std::Uint128; +use cw_storage_plus::Item; + +use crate::curves::DecimalPlaces; +use crate::msg::CurveType; + +/// 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, + /// 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(), + supply: Uint128::zero(), + reserve_denom, + decimals, + } + } +} + +pub const CURVE_STATE: Item = Item::new("curve_state"); + +pub const CURVE_TYPE: Item = Item::new("curve_type"); + +pub const DENOM: Item = Item::new("denom"); From aceb8ba04e065133171fe41f65f418f2f6622e5b Mon Sep 17 00:00:00 2001 From: adairrr <32375605+adairrr@users.noreply.github.com> Date: Sun, 23 Apr 2023 04:52:07 +0300 Subject: [PATCH 02/56] Initial phase integration to cw-abc (#698) * CwAbcResult * Hatch phase configuration and init msg refactor * Initial instantiate test * Implement separate phrases and phrase configs * Remove vesting phase and update funding pool * Separate commands and queries * Update init msg with string configs * Phase config query * MinMax and config query * Add some todos * cw-ownable integration --- Cargo.lock | 79 +++++ contracts/external/cw-abc/Cargo.toml | 6 +- contracts/external/cw-abc/src/abc.rs | 231 ++++++++++++ contracts/external/cw-abc/src/commands.rs | 141 ++++++++ contracts/external/cw-abc/src/contract.rs | 412 ++++++++++------------ contracts/external/cw-abc/src/error.rs | 18 + contracts/external/cw-abc/src/lib.rs | 3 + contracts/external/cw-abc/src/msg.rs | 92 ++--- contracts/external/cw-abc/src/queries.rs | 55 +++ contracts/external/cw-abc/src/state.rs | 22 +- 10 files changed, 780 insertions(+), 279 deletions(-) create mode 100644 contracts/external/cw-abc/src/abc.rs create mode 100644 contracts/external/cw-abc/src/commands.rs create mode 100644 contracts/external/cw-abc/src/queries.rs diff --git a/Cargo.lock b/Cargo.lock index defdef3de..7b72e98d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -728,12 +728,15 @@ version = "0.0.1" dependencies = [ "cosmwasm-schema", "cosmwasm-std", + "cw-address-like", + "cw-ownable", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw2 1.1.2", "integer-cbrt", "integer-sqrt", "rust_decimal", + "speculoos", "thiserror", "token-bindings", ] @@ -3160,6 +3163,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" @@ -3171,6 +3208,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" @@ -4171,6 +4241,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "speculoos" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65881c9270d6157f30a09233305da51bed97eef9192d0ea21e57b1c8f05c3620" +dependencies = [ + "num", +] + [[package]] name = "spin" version = "0.5.2" diff --git a/contracts/external/cw-abc/Cargo.toml b/contracts/external/cw-abc/Cargo.toml index 399ebf190..bb8fc9d06 100644 --- a/contracts/external/cw-abc/Cargo.toml +++ b/contracts/external/cw-abc/Cargo.toml @@ -24,11 +24,15 @@ cw-storage-plus = { workspace = true } cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } thiserror = { workspace = true } +# TODO move to workspace +cw-address-like = "1.0.4" rust_decimal = "1.14.3" integer-sqrt = "0.1.5" integer-cbrt = "0.1.2" # TODO publish this token-bindings = { git = "https://github.com/CosmosContracts/token-bindings", rev = "1412b94" } +cw-ownable = { workspace = true } [dev-dependencies] - +# TODO move to workspace +speculoos = "0.11.0" diff --git a/contracts/external/cw-abc/src/abc.rs b/contracts/external/cw-abc/src/abc.rs new file mode 100644 index 000000000..72f73b383 --- /dev/null +++ b/contracts/external/cw-abc/src/abc.rs @@ -0,0 +1,231 @@ + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Api, Decimal as StdDecimal, ensure, StdResult, Uint128}; +use cw_address_like::AddressLike; +use token_bindings::Metadata; +use crate::curves::{Constant, Curve, decimal, DecimalPlaces, Linear, SquareRoot}; +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: Metadata, + // 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, +} + +#[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 minimium and maximum values +#[cw_serde] +pub struct MinMax { + pub min: Uint128, + pub max: Uint128, +} + +#[cw_serde] +pub struct HatchConfig { + // Initial contributors (Hatchers) allow list + pub allowlist: Option>, + // /// TODO: 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 price (p0) per reserve token + // TODO: initial price is not implemented yet + pub initial_price: Uint128, + // The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool + pub initial_allocation_ratio: StdDecimal, +} + +impl From> for HatchConfig { + fn from(value: HatchConfig) -> Self { + HatchConfig { + allowlist: value.allowlist.map(|addresses| { + addresses.into_iter().map(|addr| addr.to_string()).collect() + }), + initial_raise: value.initial_raise, + initial_price: value.initial_price, + initial_allocation_ratio: value.initial_allocation_ratio, + } + } +} + + +impl HatchConfig { + /// Validate the hatch config + pub fn validate(&self, api: &dyn Api) -> 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.initial_price.is_zero(), + ContractError::HatchPhaseConfigError("Initial price must be greater than zero.".to_string()) + ); + + ensure!( + self.initial_allocation_ratio <= StdDecimal::percent(100u64), + ContractError::HatchPhaseConfigError("Initial allocation percentage must be between 0 and 100.".to_string()) + ); + + let allowlist = self + .allowlist + .as_ref() + .map(|addresses| { + addresses + .iter() + .map(|addr| api.addr_validate(addr)) + .collect::>>() + }) + .transpose()?; + + Ok(HatchConfig { + allowlist, + initial_raise: self.initial_raise.clone(), + initial_price: self.initial_price, + initial_allocation_ratio: self.initial_allocation_ratio, + }) + } +} + +impl HatchConfig { + /// Check if the sender is allowlisted for the hatch phase + pub fn assert_allowlisted(&self, hatcher: &Addr) -> Result<(), ContractError> { + if let Some(allowlist) = &self.allowlist { + ensure!( + allowlist.contains(hatcher), + ContractError::SenderNotAllowlisted { + sender: hatcher.to_string(), + } + ); + } + + Ok(()) + } +} + + +#[cw_serde] +pub struct OpenConfig { + // Percentage of capital put into the Reserve Pool during the Open phase + pub allocation_percentage: StdDecimal, +} + +impl OpenConfig { + /// Validate the open config + pub fn validate(&self) -> Result<(), ContractError> { + + ensure!( + self.allocation_percentage <= StdDecimal::percent(100u64), + ContractError::OpenPhaseConfigError("Reserve percentage must be between 0 and 100.".to_string()) + ); + + Ok(()) + } +} + +#[cw_serde] +pub struct ClosedConfig {} + + +#[cw_serde] +pub struct CommonsPhaseConfig { + // The Hatch phase where initial contributors (Hatchers) participate in a hatch sale. + pub hatch: HatchConfig, + // 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, +} + +// #[derive(Default)] +// #[cw_serde] +// pub struct HatchPhaseState { +// // Initial contributors (Hatchers) +// pub hatchers: HashSet, +// } +// +// // TODO: maybe should be combined with config or just placed in state +// #[cw_serde] +// pub struct CommonsPhaseState { +// pub hatch: HatchPhaseState, +// // Vesting, +// pub open: (), +// // TODO: should we allow for a closed phase? +// pub closed: () +// } + +#[cw_serde] +pub enum CommonsPhase { + Hatch, + Open, + // TODO: should we allow for a closed phase? + Closed +} + +impl CommonsPhaseConfig { + /// Validate that the commons configuration is valid + pub fn validate(&self, api: &dyn Api) -> Result, ContractError> { + let hatch = self.hatch.validate(api)?; + self.open.validate()?; + + Ok(CommonsPhaseConfig { + hatch, + open: self.open.clone(), + closed: self.closed.clone(), + }) + } +} + + +pub type CurveFn = Box Box>; + +#[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..3a6ef858f --- /dev/null +++ b/contracts/external/cw-abc/src/commands.rs @@ -0,0 +1,141 @@ +use cosmwasm_std::{BankMsg, coins, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Uint128}; +use token_bindings::{TokenFactoryQuery, TokenMsg}; +use cw_utils::must_pay; +use crate::abc::{CommonsPhase, CurveFn}; +use crate::ContractError; +use crate::contract::CwAbcResult; + +use crate::state::{CURVE_STATE, HATCHERS, PHASE, PHASE_CONFIG, SUPPLY_DENOM}; + +pub fn execute_buy( + deps: DepsMut, + _env: Env, + info: MessageInfo, + curve_fn: CurveFn, +) -> CwAbcResult { + 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)?; + + let (reserved, funded) = match phase { + CommonsPhase::Hatch => { + let hatch_config = &phase_config.hatch; + + // Check that the potential hatcher is allowlisted + hatch_config.assert_allowlisted(&info.sender)?; + HATCHERS.update(deps.storage, |mut hatchers| -> StdResult<_>{ + hatchers.insert(info.sender.clone()); + Ok(hatchers) + })?; + + // Check if the initial_raise max has been met + if curve_state.reserve + payment >= hatch_config.initial_raise.max { + // Transition to the Open phase, the hatchers' tokens are now vesting + phase = CommonsPhase::Open; + PHASE.save(deps.storage, &phase)?; + } + + // Calculate the number of tokens sent to the funding pool using the initial allocation percentage + // TODO: is it safe to multiply a Decimal with a Uint128? + let funded = payment * hatch_config.initial_allocation_ratio; + // Calculate the number of tokens sent to the reserve + let reserved = payment - funded; + + (reserved, funded) + } + CommonsPhase::Open => { + let hatch_config = &phase_config.open; + + // Calculate the number of tokens sent to the funding pool using the allocation percentage + let funded = payment * hatch_config.allocation_percentage; + // Calculate the number of tokens sent to the reserve + let reserved = payment - funded; + + (reserved, funded) + } + CommonsPhase::Closed => { + // TODO: what to do here? + return Err(ContractError::CommonsClosed {}); + } + }; + + // calculate how many tokens can be purchased with this and mint them + let curve = curve_fn(curve_state.clone().decimals); + curve_state.reserve += reserved; + curve_state.funding += funded; + let new_supply = curve.supply(curve_state.reserve); + let minted = new_supply + .checked_sub(curve_state.supply) + .map_err(StdError::overflow)?; + curve_state.supply = new_supply; + CURVE_STATE.save(deps.storage, &curve_state)?; + + let denom = SUPPLY_DENOM.load(deps.storage)?; + // mint supply token + let mint_msg = TokenMsg::MintTokens { + denom, + amount: minted, + mint_to_address: info.sender.to_string(), + }; + + Ok(Response::new() + .add_message(mint_msg) + .add_attribute("action", "buy") + .add_attribute("from", info.sender) + .add_attribute("reserved", reserved) + .add_attribute("funded", funded) + .add_attribute("supply", minted)) +} + +pub fn execute_sell( + deps: DepsMut, + _env: Env, + info: MessageInfo, + curve_fn: CurveFn, + amount: Uint128, +) -> CwAbcResult { + let receiver = info.sender.clone(); + + let denom = SUPPLY_DENOM.load(deps.storage)?; + let payment = must_pay(&info, &denom)?; + + // calculate how many tokens can be purchased with this and mint them + let mut state = CURVE_STATE.load(deps.storage)?; + let curve = curve_fn(state.clone().decimals); + state.supply = state + .supply + .checked_sub(amount) + .map_err(StdError::overflow)?; + let new_reserve = curve.reserve(state.supply); + let released = state + .reserve + .checked_sub(new_reserve) + .map_err(StdError::overflow)?; + state.reserve = new_reserve; + CURVE_STATE.save(deps.storage, &state)?; + + // Burn the tokens + let burn_msg = TokenMsg::BurnTokens { + denom, + amount: payment, + burn_from_address: info.sender.to_string(), + }; + + // now send the tokens to the sender (TODO: for sell_from we do something else, right???) + let msg = BankMsg::Send { + to_address: receiver.to_string(), + amount: coins(released.u128(), state.reserve_denom), + }; + + Ok(Response::new() + .add_message(msg) + .add_message(burn_msg) + .add_attribute("action", "burn") + .add_attribute("from", info.sender) + .add_attribute("supply", amount) + .add_attribute("reserve", released)) +} \ No newline at end of file diff --git a/contracts/external/cw-abc/src/contract.rs b/contracts/external/cw-abc/src/contract.rs index dcd71e32a..2e21f0d70 100644 --- a/contracts/external/cw-abc/src/contract.rs +++ b/contracts/external/cw-abc/src/contract.rs @@ -1,77 +1,94 @@ +use std::collections::HashSet; +use std::ops::Deref; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use cosmwasm_std::{ - attr, coins, to_binary, Addr, BankMsg, Binary, Deps, DepsMut, Env, MessageInfo, Response, - StdError, StdResult, Uint128, -}; +use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, QuerierWrapper, Response, StdResult, to_binary}; use cw2::set_contract_version; -use token_bindings::{Metadata, TokenFactoryMsg, TokenFactoryQuery, TokenMsg, TokenQuerier}; +use token_bindings::{TokenFactoryMsg, TokenFactoryQuery, TokenMsg}; use crate::curves::DecimalPlaces; use crate::error::ContractError; -use crate::msg::{CurveFn, CurveInfoResponse, ExecuteMsg, InstantiateMsg, QueryMsg}; -use crate::state::{CurveState, CURVE_STATE, CURVE_TYPE, DENOM}; -use cw_utils::{must_pay, nonpayable}; +use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use crate::state::{CURVE_STATE, CURVE_TYPE, CurveState, HATCHERS, PHASE_CONFIG, SUPPLY_DENOM}; +use cw_utils::nonpayable; +use crate::abc::CurveFn; +use crate::{commands, queries}; // version info for migration info -const CONTRACT_NAME: &str = "crates.io:cw20-bonding"; +const CONTRACT_NAME: &str = "crates.io:cw20-abc"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); // By default, the prefix for token factory tokens is "factory" const DENOM_PREFIX: &str = "factory"; +pub type CwAbcResult> = Result; + #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, env: Env, info: MessageInfo, msg: InstantiateMsg, -) -> Result, ContractError> { +) -> CwAbcResult { nonpayable(&info)?; set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - if msg.subdenom.eq("") { - return Err(ContractError::InvalidSubdenom { - subdenom: msg.subdenom, - }); + + let InstantiateMsg { + supply, + reserve, + curve_type, + phase_config, + } = msg; + + + if supply.subdenom.is_empty() { + return Err(ContractError::SupplyTokenError("Token subdenom must not be empty.".to_string())); } - // Create denom with metadata - let create_denom_msg = TokenMsg::CreateDenom { - subdenom: msg.subdenom.clone(), - metadata: Some(msg.metadata), - }; + let phase_config = phase_config.validate(deps.api)?; - let places = DecimalPlaces::new(msg.decimals, msg.reserve_decimals); - let supply = CurveState::new(msg.reserve_denom, places); + // Create supply denom with metadata + let create_supply_denom_msg = TokenMsg::CreateDenom { + subdenom: supply.subdenom.clone(), + metadata: Some(supply.metadata), + }; // TODO validate denom? // Save the denom - DENOM.save( + SUPPLY_DENOM.save( deps.storage, &format!( "{}/{}/{}", DENOM_PREFIX, - env.contract.address.to_string(), - msg.subdenom + env.contract.address.into_string(), + supply.subdenom ), )?; - CURVE_STATE.save(deps.storage, &supply)?; + // Save the curve type and state + let normalization_places = DecimalPlaces::new(supply.decimals, reserve.decimals); + let curve_state = CurveState::new(reserve.denom, normalization_places); + CURVE_STATE.save(deps.storage, &curve_state)?; + CURVE_TYPE.save(deps.storage, &curve_type)?; + HATCHERS.save(deps.storage, &HashSet::new())?; - CURVE_TYPE.save(deps.storage, &msg.curve_type)?; + PHASE_CONFIG.save(deps.storage, &phase_config)?; - Ok(Response::default().add_message(create_denom_msg)) + cw_ownable::initialize_owner(deps.storage, deps.api, Some(info.sender.as_str()))?; + + Ok(Response::default().add_message(create_supply_denom_msg)) } + #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, -) -> Result, ContractError> { +) -> CwAbcResult { // 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)?; @@ -88,97 +105,31 @@ pub fn do_execute( info: MessageInfo, msg: ExecuteMsg, curve_fn: CurveFn, -) -> Result, ContractError> { +) -> CwAbcResult { match msg { - ExecuteMsg::Buy {} => execute_buy(deps, env, info, curve_fn), - ExecuteMsg::Burn { amount } => Ok(execute_sell(deps, env, info, curve_fn, amount)?), + ExecuteMsg::Buy {} => commands::execute_buy(deps, env, info, curve_fn), + ExecuteMsg::Burn { amount } => commands::execute_sell(deps, env, info, curve_fn, amount), + ExecuteMsg::UpdateHatchAllowlist { to_add: _, to_remove: _ } => { + cw_ownable::assert_owner(deps.storage, &info.sender)?; + // commands::execute_update_hatch_allowlist(deps, env, info, to_add, to_remove) + todo!() + } + ExecuteMsg::UpdateHatchConfig { .. } => { + cw_ownable::assert_owner(deps.storage, &info.sender)?; + todo!() + }, + ExecuteMsg::UpdateOwnership(action) => { + 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())) + } } } -pub fn execute_buy( - deps: DepsMut, - env: Env, - info: MessageInfo, - curve_fn: CurveFn, -) -> Result, ContractError> { - let mut state = CURVE_STATE.load(deps.storage)?; - - let denom = DENOM.load(deps.storage)?; - let payment = must_pay(&info, &state.reserve_denom)?; - - // calculate how many tokens can be purchased with this and mint them - let curve = curve_fn(state.clone().decimals); - state.reserve += payment; - let new_supply = curve.supply(state.reserve); - let minted = new_supply - .checked_sub(state.supply) - .map_err(StdError::overflow)?; - state.supply = new_supply; - CURVE_STATE.save(deps.storage, &state)?; - - // mint tf token - let mint_msg = TokenMsg::MintTokens { - denom, - amount: minted, - mint_to_address: info.sender.to_string(), - }; - - Ok(Response::new() - .add_message(mint_msg) - .add_attribute("action", "buy") - .add_attribute("from", info.sender) - .add_attribute("reserve", payment) - .add_attribute("supply", minted)) -} - -pub fn execute_sell( - deps: DepsMut, - env: Env, - info: MessageInfo, - curve_fn: CurveFn, - amount: Uint128, -) -> Result, ContractError> { - let receiver = info.sender.clone(); - - let denom = DENOM.load(deps.storage)?; - let payment = must_pay(&info, &denom)?; - - // calculate how many tokens can be purchased with this and mint them - let mut state = CURVE_STATE.load(deps.storage)?; - let curve = curve_fn(state.clone().decimals); - state.supply = state - .supply - .checked_sub(amount) - .map_err(StdError::overflow)?; - let new_reserve = curve.reserve(state.supply); - let released = state - .reserve - .checked_sub(new_reserve) - .map_err(StdError::overflow)?; - state.reserve = new_reserve; - CURVE_STATE.save(deps.storage, &state)?; - - // Burn the tokens - let burn_msg = TokenMsg::BurnTokens { - denom, - amount: payment, - burn_from_address: info.sender.clone().to_string(), - }; - - // now send the tokens to the sender (TODO: for sell_from we do something else, right???) - let msg = BankMsg::Send { - to_address: receiver.to_string(), - amount: coins(released.u128(), state.reserve_denom), - }; - - Ok(Response::new() - .add_message(msg) - .add_message(burn_msg) - .add_attribute("action", "burn") - .add_attribute("from", info.sender) - .add_attribute("supply", amount) - .add_attribute("reserve", released)) -} #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { @@ -190,7 +141,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResul } /// 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 InstantitateMsg and stored in state, but you may want +/// 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, @@ -200,7 +151,9 @@ pub fn do_query( ) -> StdResult { match msg { // custom queries - QueryMsg::CurveInfo {} => to_binary(&query_curve_info(deps, curve_fn)?), + QueryMsg::CurveInfo {} => to_binary(&queries::query_curve_info(deps, curve_fn)?), + QueryMsg::PhaseConfig {} => to_binary(&queries::query_phase_config(deps)?), + QueryMsg::Ownership {} => to_binary(&cw_ownable::get_ownership(deps.storage)?), // QueryMsg::GetDenom { // creator_address, // subdenom, @@ -208,43 +161,6 @@ pub fn do_query( } } -pub fn query_curve_info( - deps: Deps, - curve_fn: CurveFn, -) -> StdResult { - let CurveState { - reserve, - supply, - reserve_denom, - decimals, - } = 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, - spot_price, - reserve_denom, - }) -} - -// // TODO, maybe we don't need this -// pub fn get_denom( -// deps: Deps, -// creator_addr: String, -// subdenom: String, -// ) -> GetDenomResponse { -// let querier = TokenQuerier::new(&deps.querier); -// let response = querier.full_denom(creator_addr, subdenom).unwrap(); - -// GetDenomResponse { -// denom: response.denom, -// } -// } - // fn validate_denom( // deps: DepsMut, // denom: String, @@ -286,34 +202,70 @@ pub fn query_curve_info( // Result::Ok(()) // } -// // this is poor mans "skip" flag -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::msg::CurveType; -// use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; -// use cosmwasm_std::{coin, Decimal, OverflowError, OverflowOperation, StdError, SubMsg}; -// use cw_utils::PaymentError; - -// const DENOM: &str = "satoshi"; -// const CREATOR: &str = "creator"; -// const INVESTOR: &str = "investor"; -// const BUYER: &str = "buyer"; - -// fn default_instantiate( -// decimals: u8, -// reserve_decimals: u8, -// curve_type: CurveType, -// ) -> InstantiateMsg { -// InstantiateMsg { -// name: "Bonded".to_string(), -// symbol: "EPOXY".to_string(), -// decimals, -// reserve_denom: DENOM.to_string(), -// reserve_decimals, -// curve_type, -// } -// } +// this is poor man's "skip" flag +#[cfg(test)] +mod tests { + use std::marker::PhantomData; + use cosmwasm_std::{CosmosMsg, Decimal, OwnedDeps, testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, Uint128}; + use token_bindings::Metadata; + use crate::abc::{ClosedConfig, CommonsPhaseConfig, CurveType, HatchConfig, MinMax, OpenConfig, ReserveToken, SupplyToken}; + use super::*; + use speculoos::prelude::*; + use crate::queries::query_curve_info; + + const DENOM: &str = "satoshi"; + const CREATOR: &str = "creator"; + const INVESTOR: &str = "investor"; + const BUYER: &str = "buyer"; + + const SUPPLY_DENOM: &str = "subdenom"; + + + + fn default_supply_metadata() -> Metadata { + Metadata { + name: Some("Bonded".to_string()), + symbol: Some("EPOXY".to_string()), + description: None, + denom_units: vec![], + base: None, + display: None, + } + } + + fn default_instantiate( + decimals: u8, + reserve_decimals: u8, + curve_type: CurveType, + ) -> InstantiateMsg { + InstantiateMsg { + supply: SupplyToken { + subdenom: SUPPLY_DENOM.to_string(), + metadata: default_supply_metadata(), + decimals, + }, + reserve: ReserveToken { + denom: DENOM.to_string(), + decimals: reserve_decimals, + }, + phase_config: CommonsPhaseConfig { + hatch: HatchConfig { + initial_raise: MinMax { + min: Uint128::one(), + max: Uint128::from(1000000u128), + }, + initial_price: Uint128::one(), + initial_allocation_ratio: Decimal::percent(10u64), + allowlist: None, + }, + open: OpenConfig { + allocation_percentage: Decimal::percent(10u64), + }, + closed: ClosedConfig {}, + }, + curve_type, + } + } // fn get_balance>(deps: Deps, addr: U) -> Uint128 { // query_balance(deps, addr.into()).unwrap().balance @@ -330,45 +282,63 @@ pub fn query_curve_info( // assert_eq!(0, res.messages.len()); // } -// #[test] -// fn proper_instantiation() { -// let mut deps = mock_dependencies(); - -// // this matches `linear_curve` test case from curves.rs -// let creator = String::from("creator"); -// let curve_type = CurveType::SquareRoot { -// slope: Uint128::new(1), -// scale: 1, -// }; -// let msg = default_instantiate(2, 8, curve_type.clone()); -// let info = mock_info(&creator, &[]); - -// // make sure we can instantiate with this -// let res = instantiate(deps.as_mut(), mock_env(), info, msg.clone()).unwrap(); -// assert_eq!(0, res.messages.len()); + /// Mock token factory querier dependencies + fn mock_tf_dependencies() -> OwnedDeps, TokenFactoryQuery> { + OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MockQuerier::::new(&[]), + custom_query_type: PhantomData::, + } + } -// // token info is proper -// let token = query_token_info(deps.as_ref()).unwrap(); -// assert_eq!(&token.name, &msg.name); -// assert_eq!(&token.symbol, &msg.symbol); -// assert_eq!(token.decimals, 2); -// assert_eq!(token.total_supply, Uint128::zero()); - -// // curve state is sensible -// let state = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); -// assert_eq!(state.reserve, Uint128::zero()); -// assert_eq!(state.supply, Uint128::zero()); -// assert_eq!(state.reserve_denom.as_str(), DENOM); -// // spot price 0 as supply is 0 -// assert_eq!(state.spot_price, Decimal::zero()); - -// // curve type is stored properly -// let curve = CURVE_TYPE.load(&deps.storage).unwrap(); -// assert_eq!(curve_type, curve); - -// // no balance -// assert_eq!(get_balance(deps.as_ref(), &creator), Uint128::zero()); -// } + #[test] + fn proper_instantiation() -> CwAbcResult<()> { + let mut deps = mock_tf_dependencies(); + + // this matches `linear_curve` test case from curves.rs + let creator = String::from("creator"); + let curve_type = CurveType::SquareRoot { + slope: Uint128::new(1), + scale: 1, + }; + let msg = default_instantiate(2, 8, curve_type.clone()); + let info = mock_info(&creator, &[]); + + // make sure we can instantiate with this + let res = instantiate(deps.as_mut(), mock_env(), info, msg)?; + assert_that!(res.messages.len()).is_equal_to(1); + let submsg = res.messages.get(0).unwrap(); + assert_that!(submsg.msg).is_equal_to(CosmosMsg::Custom(TokenFactoryMsg::Token(TokenMsg::CreateDenom { + subdenom: SUPPLY_DENOM.to_string(), + metadata: Some(default_supply_metadata()), + }))); + + // TODO! + // // token info is proper + // let token = query_token_info(deps.as_ref()).unwrap(); + // assert_that!(&token.name, &msg.name); + // assert_that!(&token.symbol, &msg.symbol); + // assert_that!(token.decimals, 2); + // assert_that!(token.total_supply, Uint128::zero()); + + // curve state is sensible + let state = query_curve_info(deps.as_ref(), curve_type.to_curve_fn())?; + assert_that!(state.reserve).is_equal_to(Uint128::zero()); + assert_that!(state.supply).is_equal_to(Uint128::zero()); + assert_that!(state.reserve_denom.as_str()).is_equal_to(DENOM); + // spot price 0 as supply is 0 + assert_that!(state.spot_price).is_equal_to(Decimal::zero()); + + // curve type is stored properly + let curve = CURVE_TYPE.load(&deps.storage).unwrap(); + assert_eq!(curve_type, curve); + + // no balance + // assert_eq!(get_balance(deps.as_ref(), &creator), Uint128::zero()); + + Ok(()) + } // #[test] // fn buy_issues_tokens() { @@ -502,4 +472,4 @@ pub fn query_curve_info( // assert_eq!(token.decimals, 2); // assert_eq!(token.total_supply, Uint128::new(1000)); // } -// } +} diff --git a/contracts/external/cw-abc/src/error.rs b/contracts/external/cw-abc/src/error.rs index b3a8d2e41..69ece1764 100644 --- a/contracts/external/cw-abc/src/error.rs +++ b/contracts/external/cw-abc/src/error.rs @@ -13,6 +13,24 @@ pub enum ContractError { #[error("Invalid subdenom: {subdenom:?}")] InvalidSubdenom { subdenom: String }, + #[error("{0}")] + Ownership(#[from] cw_ownable::OwnershipError), + #[error("Unauthorized")] Unauthorized {}, + + #[error("Hatch phase config error {0}")] + HatchPhaseConfigError(String), + + #[error("Open phase config error {0}")] + OpenPhaseConfigError(String), + + #[error("Supply token error {0}")] + SupplyTokenError(String), + + #[error("Sender {sender:?} is not in the hatcher allowlist.")] + SenderNotAllowlisted { sender: String }, + + #[error("The commons is closed to new contributions")] + CommonsClosed {}, } diff --git a/contracts/external/cw-abc/src/lib.rs b/contracts/external/cw-abc/src/lib.rs index 8ecd1fa67..d96c62646 100644 --- a/contracts/external/cw-abc/src/lib.rs +++ b/contracts/external/cw-abc/src/lib.rs @@ -3,5 +3,8 @@ pub mod curves; mod error; pub mod msg; pub mod state; +pub mod abc; +pub(crate) mod commands; +mod queries; pub use crate::error::ContractError; diff --git a/contracts/external/cw-abc/src/msg.rs b/contracts/external/cw-abc/src/msg.rs index 711d2e7f2..7aac7d1b4 100644 --- a/contracts/external/cw-abc/src/msg.rs +++ b/contracts/external/cw-abc/src/msg.rs @@ -1,69 +1,26 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Decimal, Uint128}; -use token_bindings::Metadata; +use cosmwasm_std::{Addr, Decimal, Uint128, Decimal as StdDecimal}; + +use crate::abc::{CommonsPhaseConfig, CurveType, MinMax, ReserveToken, SupplyToken}; -use crate::curves::{decimal, Constant, Curve, DecimalPlaces, Linear, SquareRoot}; #[cw_serde] pub struct InstantiateMsg { - /// The denom to create - pub subdenom: String, - /// Metadata for the token to create - pub metadata: Metadata, - - /// TODO maybe we don't need this - pub decimals: u8, + // Supply token information + pub supply: SupplyToken, - /// this is the reserve token denom (only support native for now) - pub reserve_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 reserve_decimals: u8, + // Reserve token information + pub reserve: ReserveToken, - /// enum to store the curve parameters used for this contract - /// if you want to add a custom Curve, you should make a new contract that imports this one. - /// write a custom `instantiate`, and then dispatch `your::execute` -> `cw20_bonding::do_execute` - /// with your custom curve as a parameter (and same with `query` -> `do_query`) + // Curve type for this contract pub curve_type: CurveType, -} - -pub type CurveFn = Box Box>; -#[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 }, + // Hatch configuration information + pub phase_config: CommonsPhaseConfig, } -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) - } - } - } -} +#[cw_ownable::cw_ownable_execute] #[cw_serde] pub enum ExecuteMsg { /// Buy will attempt to purchase as many supply tokens as possible. @@ -71,14 +28,31 @@ pub enum ExecuteMsg { Buy {}, /// Implements CW20. Burn is a base message to destroy tokens forever Burn { amount: Uint128 }, + /// Update the hatch phase allowlist + UpdateHatchAllowlist { + to_add: Vec, + to_remove: Vec, + }, + /// Update the hatch phase configuration + /// This can only be called by the admin and only during the hatch phase + UpdateHatchConfig { + initial_raise: Option, + initial_allocation_ratio: Option, + }, } +#[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 the current phase configuration + /// Returns [`CommonsPhaseConfigResponse`] + #[returns(CommonsPhaseConfigResponse)] + PhaseConfig {} } #[cw_serde] @@ -87,6 +61,16 @@ pub struct CurveInfoResponse { 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 CommonsPhaseConfigResponse { + // the phase configuration + pub phase_config: CommonsPhaseConfig, +} diff --git a/contracts/external/cw-abc/src/queries.rs b/contracts/external/cw-abc/src/queries.rs new file mode 100644 index 000000000..d6c655f62 --- /dev/null +++ b/contracts/external/cw-abc/src/queries.rs @@ -0,0 +1,55 @@ +use cosmwasm_std::{Deps, StdResult}; +use token_bindings::TokenFactoryQuery; +use crate::abc::CurveFn; +use crate::msg::{CommonsPhaseConfigResponse, CurveInfoResponse}; +use crate::state::{CURVE_STATE, CurveState}; + +/// 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, + }) +} + +/// Load and return the phase config +/// TODO: the allowlist will need to paged... should it be separate? +pub fn query_phase_config(deps: Deps) -> StdResult { + let phase_config = crate::state::PHASE_CONFIG.load(deps.storage)?; + Ok(CommonsPhaseConfigResponse { + phase_config + }) +} + + +// // TODO, maybe we don't need this +// pub fn get_denom( +// deps: Deps, +// creator_addr: String, +// subdenom: String, +// ) -> GetDenomResponse { +// let querier = TokenQuerier::new(&deps.querier); +// let response = querier.full_denom(creator_addr, subdenom).unwrap(); + +// GetDenomResponse { +// denom: response.denom, +// } +// } diff --git a/contracts/external/cw-abc/src/state.rs b/contracts/external/cw-abc/src/state.rs index ffb91b74e..3006501fb 100644 --- a/contracts/external/cw-abc/src/state.rs +++ b/contracts/external/cw-abc/src/state.rs @@ -1,16 +1,19 @@ +use std::collections::HashSet; use cosmwasm_schema::cw_serde; -use cosmwasm_std::Uint128; +use cosmwasm_std::{Addr, Uint128}; use cw_storage_plus::Item; +use crate::abc::{ CommonsPhaseConfig, CurveType, CommonsPhase}; use crate::curves::DecimalPlaces; -use crate::msg::CurveType; /// 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, @@ -25,6 +28,7 @@ impl CurveState { pub fn new(reserve_denom: String, decimals: DecimalPlaces) -> Self { CurveState { reserve: Uint128::zero(), + funding: Uint128::zero(), supply: Uint128::zero(), reserve_denom, decimals, @@ -36,4 +40,16 @@ pub const CURVE_STATE: Item = Item::new("curve_state"); pub const CURVE_TYPE: Item = Item::new("curve_type"); -pub const DENOM: Item = Item::new("denom"); +/// The denom used for the supply token +pub const SUPPLY_DENOM: Item = Item::new("denom"); + +/// Keep track of who has contributed to the hatch phase +/// TODO: cw-set? +pub static HATCHERS: Item> = Item::new("hatchers"); + +/// 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"); + From 52c6a987450a5bdd7db93d1bf6b4239e7e57387e Mon Sep 17 00:00:00 2001 From: adairrr <32375605+adairrr@users.noreply.github.com> Date: Wed, 26 Apr 2023 00:30:21 +0300 Subject: [PATCH 03/56] cw-abc: Updated hatch phase mechanics, donations, queries (#699) * Separate hatcher allowlist * Donation feature * Initial sell exit tax * Hatchers to amount * Hatch phase exit tax * TokenMsg methods * Format * Hatchers query * Fix bug where float was not taken into account in supply * Buy and sell refactoring * Update hatch phase config * Update phase config enum * Add adairrr to authors * Initial boot integration with custom msgs * Initial testing infrastructure * Abstract-OS to AbstractSDK --- contracts/external/cw-abc/Cargo.toml | 19 +- contracts/external/cw-abc/src/abc.rs | 161 ++++--- contracts/external/cw-abc/src/boot.rs | 43 ++ contracts/external/cw-abc/src/commands.rs | 390 +++++++++++++--- contracts/external/cw-abc/src/contract.rs | 453 +++++++++---------- contracts/external/cw-abc/src/error.rs | 9 + contracts/external/cw-abc/src/integration.rs | 38 ++ contracts/external/cw-abc/src/lib.rs | 108 ++++- contracts/external/cw-abc/src/msg.rs | 82 +++- contracts/external/cw-abc/src/queries.rs | 65 ++- contracts/external/cw-abc/src/state.rs | 21 +- 11 files changed, 973 insertions(+), 416 deletions(-) create mode 100644 contracts/external/cw-abc/src/boot.rs create mode 100644 contracts/external/cw-abc/src/integration.rs diff --git a/contracts/external/cw-abc/Cargo.toml b/contracts/external/cw-abc/Cargo.toml index bb8fc9d06..0223dc1b9 100644 --- a/contracts/external/cw-abc/Cargo.toml +++ b/contracts/external/cw-abc/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "cw-abc" version = "0.0.1" -authors = ["Ethan Frey ", "Jake Hartnell"] +authors = ["Ethan Frey ", "Jake Hartnell", "Adair "] edition = { workspace = true } description = "Implements an Augmented Bonding Curve" license = "Apache-2.0" @@ -16,6 +16,7 @@ crate-type = ["cdylib", "rlib"] backtraces = ["cosmwasm-std/backtraces"] # use library feature to disable all instantiate/execute/query exports library = [] +boot = ["dep:boot-core"] [dependencies] cw-utils = { workspace = true } @@ -32,7 +33,23 @@ integer-cbrt = "0.1.2" # TODO publish this token-bindings = { git = "https://github.com/CosmosContracts/token-bindings", rev = "1412b94" } cw-ownable = { workspace = true } +cw-paginate-storage = { workspace = true } +boot-core = { version = "0.10.0", optional = true, git = "https://github.com/AbstractSDK/BOOT", branch = "fix/custom_binding_contract_wrapper" } [dev-dependencies] # TODO move to workspace speculoos = "0.11.0" +#cw-multi-test = { version = "0.16.0" } +anyhow = { workspace = true } +cw-abc = { path = ".", features = ["boot"] } + +[profile.release] +rpath = false +lto = true +overflow-checks = true +opt-level = 3 +debug = false +debug-assertions = false +codegen-units = 1 +panic = 'abort' +incremental = false diff --git a/contracts/external/cw-abc/src/abc.rs b/contracts/external/cw-abc/src/abc.rs index 72f73b383..b8c49d8d2 100644 --- a/contracts/external/cw-abc/src/abc.rs +++ b/contracts/external/cw-abc/src/abc.rs @@ -1,10 +1,9 @@ - use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Api, Decimal as StdDecimal, ensure, StdResult, Uint128}; -use cw_address_like::AddressLike; -use token_bindings::Metadata; -use crate::curves::{Constant, Curve, decimal, DecimalPlaces, Linear, SquareRoot}; +use cosmwasm_std::{ensure, Decimal as StdDecimal, Uint128}; + +use crate::curves::{decimal, Constant, Curve, DecimalPlaces, Linear, SquareRoot}; use crate::ContractError; +use token_bindings::Metadata; #[cw_serde] pub struct SupplyToken { @@ -34,9 +33,7 @@ pub struct MinMax { } #[cw_serde] -pub struct HatchConfig { - // Initial contributors (Hatchers) allow list - pub allowlist: Option>, +pub struct HatchConfig { // /// TODO: 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 @@ -46,90 +43,70 @@ pub struct HatchConfig { pub initial_price: Uint128, // The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool pub initial_allocation_ratio: StdDecimal, + // Exit tax for the hatch phase + pub exit_tax: StdDecimal, } -impl From> for HatchConfig { - fn from(value: HatchConfig) -> Self { - HatchConfig { - allowlist: value.allowlist.map(|addresses| { - addresses.into_iter().map(|addr| addr.to_string()).collect() - }), - initial_raise: value.initial_raise, - initial_price: value.initial_price, - initial_allocation_ratio: value.initial_allocation_ratio, - } - } -} - - -impl HatchConfig { +impl HatchConfig { /// Validate the hatch config - pub fn validate(&self, api: &dyn Api) -> Result, ContractError> { + 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()) + ContractError::HatchPhaseConfigError( + "Initial raise minimum value must be less than maximum value.".to_string() + ) ); ensure!( !self.initial_price.is_zero(), - ContractError::HatchPhaseConfigError("Initial price must be greater than zero.".to_string()) + ContractError::HatchPhaseConfigError( + "Initial price must be greater than zero.".to_string() + ) ); + // TODO: define better values ensure!( self.initial_allocation_ratio <= StdDecimal::percent(100u64), - ContractError::HatchPhaseConfigError("Initial allocation percentage must be between 0 and 100.".to_string()) + ContractError::HatchPhaseConfigError( + "Initial allocation percentage must be between 0 and 100.".to_string() + ) ); - let allowlist = self - .allowlist - .as_ref() - .map(|addresses| { - addresses - .iter() - .map(|addr| api.addr_validate(addr)) - .collect::>>() - }) - .transpose()?; - - Ok(HatchConfig { - allowlist, - initial_raise: self.initial_raise.clone(), - initial_price: self.initial_price, - initial_allocation_ratio: self.initial_allocation_ratio, - }) - } -} - -impl HatchConfig { - /// Check if the sender is allowlisted for the hatch phase - pub fn assert_allowlisted(&self, hatcher: &Addr) -> Result<(), ContractError> { - if let Some(allowlist) = &self.allowlist { - ensure!( - allowlist.contains(hatcher), - ContractError::SenderNotAllowlisted { - sender: hatcher.to_string(), - } - ); - } + // TODO: define better values + ensure!( + self.exit_tax <= StdDecimal::percent(100u64), + ContractError::HatchPhaseConfigError( + "Exit taxation 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 pub allocation_percentage: StdDecimal, + // Exit taxation ratio + pub exit_tax: StdDecimal, } impl OpenConfig { /// Validate the open config pub fn validate(&self) -> Result<(), ContractError> { - ensure!( self.allocation_percentage <= StdDecimal::percent(100u64), - ContractError::OpenPhaseConfigError("Reserve percentage must be between 0 and 100.".to_string()) + ContractError::OpenPhaseConfigError( + "Reserve percentage must be between 0 and 100.".to_string() + ) + ); + + ensure!( + self.exit_tax <= StdDecimal::percent(100u64), + ContractError::OpenPhaseConfigError( + "Exit taxation percentage must be between 0 and 100.".to_string() + ) ); Ok(()) @@ -139,11 +116,17 @@ impl OpenConfig { #[cw_serde] pub struct ClosedConfig {} +impl ClosedConfig { + /// Validate the closed config + pub fn validate(&self) -> Result<(), ContractError> { + Ok(()) + } +} #[cw_serde] -pub struct CommonsPhaseConfig { +pub struct CommonsPhaseConfig { // The Hatch phase where initial contributors (Hatchers) participate in a hatch sale. - pub hatch: HatchConfig, + pub hatch: HatchConfig, // 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. @@ -174,24 +157,55 @@ pub enum CommonsPhase { Hatch, Open, // TODO: should we allow for a closed phase? - Closed + 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 { +impl CommonsPhaseConfig { /// Validate that the commons configuration is valid - pub fn validate(&self, api: &dyn Api) -> Result, ContractError> { - let hatch = self.hatch.validate(api)?; + pub fn validate(&self) -> Result<(), ContractError> { + self.hatch.validate()?; self.open.validate()?; + self.closed.validate()?; - Ok(CommonsPhaseConfig { - hatch, - open: self.open.clone(), - closed: self.closed.clone(), - }) + Ok(()) } } - pub type CurveFn = Box Box>; #[cw_serde] @@ -228,4 +242,3 @@ impl CurveType { } } } - diff --git a/contracts/external/cw-abc/src/boot.rs b/contracts/external/cw-abc/src/boot.rs new file mode 100644 index 000000000..d8376b0e5 --- /dev/null +++ b/contracts/external/cw-abc/src/boot.rs @@ -0,0 +1,43 @@ +use crate::msg::*; +use boot_core::{contract, Contract, CwEnv}; +#[cfg(feature = "daemon")] +use boot_core::{ArtifactsDir, Daemon, WasmPath}; +use boot_core::{ContractWrapper, Mock, MockState, TxHandler, Uploadable}; +use cosmwasm_std::Empty; +use token_bindings::{TokenFactoryMsg, TokenFactoryQuery}; + +#[contract(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct CwAbc; + +impl CwAbc { + pub fn new(name: &str, chain: Chain) -> Self { + let contract = Contract::new(name, chain); + Self(contract) + } +} + +/// Basic app for the token factory contract +/// TODO: should be in the bindings, along with custom handler for multi-test +pub(crate) type TokenFactoryBasicApp = boot_core::BasicApp; + +type TokenFactoryMock = Mock; + +impl Uploadable for CwAbc { + fn source(&self) -> ::ContractSource { + Box::new(ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + )) + } +} + +#[cfg(feature = "daemon")] +impl Uploadable for CwAbc { + fn source(&self) -> ::ContractSource { + ArtifactsDir::env() + .expect("Expected ARTIFACTS_DIR in env") + .find_wasm_path("cw_abc") + .unwrap() + } +} diff --git a/contracts/external/cw-abc/src/commands.rs b/contracts/external/cw-abc/src/commands.rs index 3a6ef858f..e4a9535cc 100644 --- a/contracts/external/cw-abc/src/commands.rs +++ b/contracts/external/cw-abc/src/commands.rs @@ -1,11 +1,18 @@ -use cosmwasm_std::{BankMsg, coins, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Uint128}; -use token_bindings::{TokenFactoryQuery, TokenMsg}; -use cw_utils::must_pay; -use crate::abc::{CommonsPhase, CurveFn}; -use crate::ContractError; +use crate::abc::{CommonsPhase, CurveFn, MinMax}; use crate::contract::CwAbcResult; +use crate::ContractError; +use cosmwasm_std::{ + coins, ensure, Addr, BankMsg, Decimal as StdDecimal, DepsMut, Env, MessageInfo, QuerierWrapper, + Response, StdError, StdResult, Storage, Uint128, +}; +use cw_utils::must_pay; +use std::collections::HashSet; +use std::ops::Deref; +use token_bindings::{TokenFactoryMsg, TokenFactoryQuery, TokenMsg}; -use crate::state::{CURVE_STATE, HATCHERS, PHASE, PHASE_CONFIG, SUPPLY_DENOM}; +use crate::state::{ + CURVE_STATE, DONATIONS, HATCHERS, HATCHER_ALLOWLIST, PHASE, PHASE_CONFIG, SUPPLY_DENOM, +}; pub fn execute_buy( deps: DepsMut, @@ -21,16 +28,12 @@ pub fn execute_buy( let phase_config = PHASE_CONFIG.load(deps.storage)?; let mut phase = PHASE.load(deps.storage)?; - let (reserved, funded) = match phase { + let (reserved, funded) = match &phase { CommonsPhase::Hatch => { - let hatch_config = &phase_config.hatch; - + let hatch_config = phase_config.hatch; // Check that the potential hatcher is allowlisted - hatch_config.assert_allowlisted(&info.sender)?; - HATCHERS.update(deps.storage, |mut hatchers| -> StdResult<_>{ - hatchers.insert(info.sender.clone()); - Ok(hatchers) - })?; + assert_allowlisted(deps.storage, &info.sender)?; + update_hatcher_contributions(deps.storage, &info.sender, payment)?; // Check if the initial_raise max has been met if curve_state.reserve + payment >= hatch_config.initial_raise.max { @@ -39,26 +42,12 @@ pub fn execute_buy( PHASE.save(deps.storage, &phase)?; } - // Calculate the number of tokens sent to the funding pool using the initial allocation percentage - // TODO: is it safe to multiply a Decimal with a Uint128? - let funded = payment * hatch_config.initial_allocation_ratio; - // Calculate the number of tokens sent to the reserve - let reserved = payment - funded; - - (reserved, funded) + calculate_reserved_and_funded(payment, hatch_config.initial_allocation_ratio) } CommonsPhase::Open => { - let hatch_config = &phase_config.open; - - // Calculate the number of tokens sent to the funding pool using the allocation percentage - let funded = payment * hatch_config.allocation_percentage; - // Calculate the number of tokens sent to the reserve - let reserved = payment - funded; - - (reserved, funded) + calculate_reserved_and_funded(payment, phase_config.open.allocation_percentage) } CommonsPhase::Closed => { - // TODO: what to do here? return Err(ContractError::CommonsClosed {}); } }; @@ -67,6 +56,7 @@ pub fn execute_buy( let curve = curve_fn(curve_state.clone().decimals); curve_state.reserve += reserved; curve_state.funding += funded; + // Calculate the supply based on the reserve let new_supply = curve.supply(curve_state.reserve); let minted = new_supply .checked_sub(curve_state.supply) @@ -74,13 +64,7 @@ pub fn execute_buy( curve_state.supply = new_supply; CURVE_STATE.save(deps.storage, &curve_state)?; - let denom = SUPPLY_DENOM.load(deps.storage)?; - // mint supply token - let mint_msg = TokenMsg::MintTokens { - denom, - amount: minted, - mint_to_address: info.sender.to_string(), - }; + let mint_msg = mint_supply_msg(deps.storage, minted, &info.sender)?; Ok(Response::new() .add_message(mint_msg) @@ -91,51 +75,329 @@ pub fn execute_buy( .add_attribute("supply", minted)) } +/// Build a message to mint the supply token to the sender +fn mint_supply_msg(storage: &dyn Storage, minted: Uint128, minter: &Addr) -> CwAbcResult { + let denom = SUPPLY_DENOM.load(storage)?; + // mint supply token + Ok(TokenMsg::mint_contract_tokens( + denom, + minted, + minter.to_string(), + )) +} + +/// Return the reserved and funded amounts based on the payment and the allocation ratio +fn calculate_reserved_and_funded( + payment: Uint128, + allocation_ratio: StdDecimal, +) -> (Uint128, Uint128) { + let funded = payment * allocation_ratio; + let reserved = payment.checked_sub(funded).unwrap(); // Since allocation_ratio is < 1, this subtraction is safe + (reserved, funded) +} + +/// Add the hatcher's contribution to the total contributions +fn update_hatcher_contributions( + storage: &mut dyn Storage, + hatcher: &Addr, + contribution: Uint128, +) -> StdResult<()> { + HATCHERS.update(storage, hatcher, |amount| -> StdResult<_> { + match amount { + Some(mut amount) => { + amount += contribution; + Ok(amount) + } + None => Ok(contribution), + } + })?; + Ok(()) +} + pub fn execute_sell( deps: DepsMut, _env: Env, info: MessageInfo, curve_fn: CurveFn, - amount: Uint128, ) -> CwAbcResult { - let receiver = info.sender.clone(); + let burner = info.sender.clone(); - let denom = SUPPLY_DENOM.load(deps.storage)?; - let payment = must_pay(&info, &denom)?; + let supply_denom = SUPPLY_DENOM.load(deps.storage)?; + let burn_amount = must_pay(&info, &supply_denom)?; + // Burn the sent supply tokens + let burn_msg = TokenMsg::burn_contract_tokens(supply_denom, burn_amount, burner.to_string()); - // calculate how many tokens can be purchased with this and mint them - let mut state = CURVE_STATE.load(deps.storage)?; - let curve = curve_fn(state.clone().decimals); - state.supply = state + let taxed_amount = calculate_exit_tax(deps.storage, burn_amount)?; + + let mut curve_state = CURVE_STATE.load(deps.storage)?; + let curve = curve_fn(curve_state.clone().decimals); + + // Reduce the supply by the amount burned + curve_state.supply = curve_state .supply - .checked_sub(amount) + .checked_sub(burn_amount) .map_err(StdError::overflow)?; - let new_reserve = curve.reserve(state.supply); - let released = state + + // Calculate the new reserve based on the new supply + let new_reserve = curve.reserve(curve_state.supply); + curve_state.reserve = new_reserve; + curve_state.funding += taxed_amount; + CURVE_STATE.save(deps.storage, &curve_state)?; + + // Calculate how many reserve tokens to release based on the sell amount + let released_reserve = curve_state .reserve .checked_sub(new_reserve) .map_err(StdError::overflow)?; - state.reserve = new_reserve; - CURVE_STATE.save(deps.storage, &state)?; - // Burn the tokens - let burn_msg = TokenMsg::BurnTokens { - denom, - amount: payment, - burn_from_address: info.sender.to_string(), - }; - - // now send the tokens to the sender (TODO: for sell_from we do something else, right???) + // Now send the tokens to the sender let msg = BankMsg::Send { - to_address: receiver.to_string(), - amount: coins(released.u128(), state.reserve_denom), + to_address: burner.to_string(), + amount: coins(released_reserve.u128(), curve_state.reserve_denom), }; Ok(Response::new() .add_message(msg) .add_message(burn_msg) .add_attribute("action", "burn") - .add_attribute("from", info.sender) - .add_attribute("supply", amount) - .add_attribute("reserve", released)) -} \ No newline at end of file + .add_attribute("from", burner) + .add_attribute("amount", burn_amount) + .add_attribute("burned", released_reserve) + .add_attribute("funded", taxed_amount)) +} + +/// Calculate the exit taxation for the sell amount based on the phase +fn calculate_exit_tax(storage: &dyn Storage, sell_amount: Uint128) -> CwAbcResult { + // Load the phase config and phase + let phase = PHASE.load(storage)?; + let phase_config = PHASE_CONFIG.load(storage)?; + + // Calculate the exit tax based on the phase + let exit_tax = match &phase { + CommonsPhase::Hatch => phase_config.hatch.exit_tax, + CommonsPhase::Open => phase_config.open.exit_tax, + CommonsPhase::Closed => return Err(ContractError::CommonsClosed {}), + }; + + debug_assert!( + exit_tax <= StdDecimal::percent(100), + "Exit tax must be <= 100%" + ); + + // This won't ever overflow because it's checked + let taxed_amount = sell_amount * exit_tax; + Ok(taxed_amount) +} + +/// Send a donation to the funding pool +pub fn execute_donate( + deps: DepsMut, + _env: Env, + info: MessageInfo, +) -> CwAbcResult { + let mut curve_state = CURVE_STATE.load(deps.storage)?; + + let payment = must_pay(&info, &curve_state.reserve_denom)?; + curve_state.funding += payment; + CURVE_STATE.save(deps.storage, &curve_state)?; + + // No minting of tokens is necessary, the supply stays the same + DONATIONS.save(deps.storage, &info.sender, &payment)?; + + Ok(Response::new() + .add_attribute("action", "donate") + .add_attribute("donor", info.sender) + .add_attribute("amount", payment)) +} + +/// Check if the sender is allowlisted for the hatch phase +fn assert_allowlisted(storage: &dyn Storage, hatcher: &Addr) -> Result<(), ContractError> { + let allowlist = HATCHER_ALLOWLIST.may_load(storage)?; + if let Some(allowlist) = allowlist { + ensure!( + allowlist.contains(hatcher), + ContractError::SenderNotAllowlisted { + sender: hatcher.to_string(), + } + ); + } + + Ok(()) +} + +/// Add and remove addresses from the hatcher allowlist +pub fn update_hatch_allowlist( + deps: DepsMut, + info: MessageInfo, + to_add: Vec, + to_remove: Vec, +) -> CwAbcResult { + cw_ownable::assert_owner(deps.storage, &info.sender)?; + let mut allowlist = HATCHER_ALLOWLIST.may_load(deps.storage)?; + + if allowlist.is_none() { + allowlist = Some(HashSet::new()); + } + + let allowlist = allowlist.as_mut().unwrap(); + + // Add addresses to the allowlist + for allow in to_add { + let addr = deps.api.addr_validate(allow.as_str())?; + allowlist.insert(addr); + } + + // Remove addresses from the allowlist + for deny in to_remove { + let addr = deps.api.addr_validate(deny.as_str())?; + allowlist.remove(&addr); + } + + HATCHER_ALLOWLIST.save(deps.storage, allowlist)?; + + Ok(Response::new().add_attributes(vec![("action", "update_hatch_allowlist")])) +} + +/// Update the hatch config +pub fn update_hatch_config( + deps: DepsMut, + _env: Env, + info: MessageInfo, + initial_raise: Option, + initial_allocation_ratio: Option, +) -> CwAbcResult { + // Assert that the sender is the contract owner + cw_ownable::assert_owner(deps.storage, &info.sender)?; + + // Ensure we're in the Hatch phase + PHASE.load(deps.storage)?.expect_hatch()?; + + // Load the current phase config + let mut phase_config = PHASE_CONFIG.load(deps.storage)?; + + // Update the hatch config if new values are provided + if let Some(initial_raise) = initial_raise { + phase_config.hatch.initial_raise = initial_raise; + } + if let Some(initial_allocation_ratio) = initial_allocation_ratio { + phase_config.hatch.initial_allocation_ratio = initial_allocation_ratio; + } + + phase_config.hatch.validate()?; + PHASE_CONFIG.save(deps.storage, &phase_config)?; + + Ok(Response::new().add_attribute("action", "update_hatch_config")) +} + +/// Update the ownership of the contract +pub fn update_ownership( + deps: DepsMut, + env: &Env, + info: &MessageInfo, + action: cw_ownable::Action, +) -> Result, ContractError> { + 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; + use cosmwasm_std::coin; + use cw_utils::PaymentError; + + const TEST_DONOR: &str = "donor"; + + fn exec_donate(deps: DepsMut, donation_amount: u128) -> CwAbcResult { + execute_donate( + deps, + mock_env(), + mock_info(TEST_DONOR, &[coin(donation_amount, TEST_RESERVE_DENOM)]), + ) + } + + #[test] + fn should_fail_with_no_funds() -> CwAbcResult<()> { + let mut deps = mock_tf_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() -> CwAbcResult<()> { + let mut deps = mock_tf_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 = execute_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_add_to_funding_pool() -> CwAbcResult<()> { + let mut deps = mock_tf_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)?; + + let donation_amount = 5; + let _res = exec_donate(deps.as_mut(), donation_amount)?; + + // check that the curve's funding has been increased while supply and reserve have not + let curve_state = CURVE_STATE.load(&deps.storage)?; + assert_that!(curve_state.funding).is_equal_to(Uint128::new(donation_amount)); + + // 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(()) + } + } +} diff --git a/contracts/external/cw-abc/src/contract.rs b/contracts/external/cw-abc/src/contract.rs index 2e21f0d70..c36470c33 100644 --- a/contracts/external/cw-abc/src/contract.rs +++ b/contracts/external/cw-abc/src/contract.rs @@ -1,21 +1,23 @@ -use std::collections::HashSet; -use std::ops::Deref; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, QuerierWrapper, Response, StdResult, to_binary}; +use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; use cw2::set_contract_version; +use std::collections::HashSet; + use token_bindings::{TokenFactoryMsg, TokenFactoryQuery, TokenMsg}; +use crate::abc::CurveFn; use crate::curves::DecimalPlaces; use crate::error::ContractError; -use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; -use crate::state::{CURVE_STATE, CURVE_TYPE, CurveState, HATCHERS, PHASE_CONFIG, SUPPLY_DENOM}; -use cw_utils::nonpayable; -use crate::abc::CurveFn; +use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, UpdatePhaseConfigMsg}; +use crate::state::{ + CurveState, CURVE_STATE, CURVE_TYPE, HATCHER_ALLOWLIST, PHASE_CONFIG, SUPPLY_DENOM, +}; use crate::{commands, queries}; +use cw_utils::nonpayable; // version info for migration info -const CONTRACT_NAME: &str = "crates.io:cw20-abc"; +pub(crate) const CONTRACT_NAME: &str = "crates.io:cw20-abc"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); // By default, the prefix for token factory tokens is "factory" @@ -33,20 +35,21 @@ pub fn instantiate( nonpayable(&info)?; set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - let InstantiateMsg { supply, reserve, curve_type, phase_config, + hatcher_allowlist, } = msg; - if supply.subdenom.is_empty() { - return Err(ContractError::SupplyTokenError("Token subdenom must not be empty.".to_string())); + return Err(ContractError::SupplyTokenError( + "Token subdenom must not be empty.".to_string(), + )); } - let phase_config = phase_config.validate(deps.api)?; + phase_config.validate()?; // Create supply denom with metadata let create_supply_denom_msg = TokenMsg::CreateDenom { @@ -72,7 +75,14 @@ pub fn instantiate( let curve_state = CurveState::new(reserve.denom, normalization_places); CURVE_STATE.save(deps.storage, &curve_state)?; CURVE_TYPE.save(deps.storage, &curve_type)?; - HATCHERS.save(deps.storage, &HashSet::new())?; + + if let Some(allowlist) = hatcher_allowlist { + let allowlist = allowlist + .into_iter() + .map(|addr| deps.api.addr_validate(addr.as_str())) + .collect::>>()?; + HATCHER_ALLOWLIST.save(deps.storage, &allowlist)?; + } PHASE_CONFIG.save(deps.storage, &phase_config)?; @@ -81,7 +91,6 @@ pub fn instantiate( Ok(Response::default().add_message(create_supply_denom_msg)) } - #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( deps: DepsMut, @@ -108,29 +117,30 @@ pub fn do_execute( ) -> CwAbcResult { match msg { ExecuteMsg::Buy {} => commands::execute_buy(deps, env, info, curve_fn), - ExecuteMsg::Burn { amount } => commands::execute_sell(deps, env, info, curve_fn, amount), - ExecuteMsg::UpdateHatchAllowlist { to_add: _, to_remove: _ } => { - cw_ownable::assert_owner(deps.storage, &info.sender)?; - // commands::execute_update_hatch_allowlist(deps, env, info, to_add, to_remove) - todo!() + ExecuteMsg::Burn {} => commands::execute_sell(deps, env, info, curve_fn), + ExecuteMsg::Donate {} => commands::execute_donate(deps, env, info), + ExecuteMsg::UpdateHatchAllowlist { to_add, to_remove } => { + commands::update_hatch_allowlist(deps, info, to_add, to_remove) } - ExecuteMsg::UpdateHatchConfig { .. } => { - cw_ownable::assert_owner(deps.storage, &info.sender)?; - todo!() + ExecuteMsg::UpdatePhaseConfig(update) => match update { + UpdatePhaseConfigMsg::Hatch { + initial_raise, + initial_allocation_ratio, + } => commands::update_hatch_config( + deps, + env, + info, + initial_raise, + initial_allocation_ratio, + ), + _ => todo!(), }, ExecuteMsg::UpdateOwnership(action) => { - 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())) + 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 @@ -153,6 +163,12 @@ pub fn do_query( // custom queries QueryMsg::CurveInfo {} => to_binary(&queries::query_curve_info(deps, curve_fn)?), QueryMsg::PhaseConfig {} => to_binary(&queries::query_phase_config(deps)?), + QueryMsg::Donations { start_after, limit } => { + to_binary(&queries::query_donations(deps, start_after, limit)?) + } + QueryMsg::Hatchers { start_after, limit } => { + to_binary(&queries::query_hatchers(deps, start_after, limit)?) + } QueryMsg::Ownership {} => to_binary(&cw_ownable::get_ownership(deps.storage)?), // QueryMsg::GetDenom { // creator_address, @@ -204,93 +220,34 @@ pub fn do_query( // this is poor man's "skip" flag #[cfg(test)] -mod tests { - use std::marker::PhantomData; - use cosmwasm_std::{CosmosMsg, Decimal, OwnedDeps, testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, Uint128}; - use token_bindings::Metadata; - use crate::abc::{ClosedConfig, CommonsPhaseConfig, CurveType, HatchConfig, MinMax, OpenConfig, ReserveToken, SupplyToken}; +pub(crate) mod tests { use super::*; - use speculoos::prelude::*; + use crate::abc::CurveType; use crate::queries::query_curve_info; + use cosmwasm_std::{ + testing::{mock_env, mock_info}, + CosmosMsg, Decimal, Uint128, + }; + use speculoos::prelude::*; - const DENOM: &str = "satoshi"; - const CREATOR: &str = "creator"; - const INVESTOR: &str = "investor"; - const BUYER: &str = "buyer"; - - const SUPPLY_DENOM: &str = "subdenom"; - - - - fn default_supply_metadata() -> Metadata { - Metadata { - name: Some("Bonded".to_string()), - symbol: Some("EPOXY".to_string()), - description: None, - denom_units: vec![], - base: None, - display: None, - } - } - - fn default_instantiate( - decimals: u8, - reserve_decimals: u8, - curve_type: CurveType, - ) -> InstantiateMsg { - InstantiateMsg { - supply: SupplyToken { - subdenom: SUPPLY_DENOM.to_string(), - metadata: default_supply_metadata(), - decimals, - }, - reserve: ReserveToken { - denom: DENOM.to_string(), - decimals: reserve_decimals, - }, - phase_config: CommonsPhaseConfig { - hatch: HatchConfig { - initial_raise: MinMax { - min: Uint128::one(), - max: Uint128::from(1000000u128), - }, - initial_price: Uint128::one(), - initial_allocation_ratio: Decimal::percent(10u64), - allowlist: None, - }, - open: OpenConfig { - allocation_percentage: Decimal::percent(10u64), - }, - closed: ClosedConfig {}, - }, - curve_type, - } - } + use crate::testing::*; -// fn get_balance>(deps: Deps, addr: U) -> Uint128 { -// query_balance(deps, addr.into()).unwrap().balance -// } + // fn get_balance>(deps: Deps, addr: U) -> Uint128 { + // query_balance(deps, addr.into()).unwrap().balance + // } -// fn setup_test(deps: DepsMut, decimals: u8, reserve_decimals: u8, curve_type: CurveType) { -// // this matches `linear_curve` test case from curves.rs -// let creator = String::from(CREATOR); -// let msg = default_instantiate(decimals, reserve_decimals, curve_type); -// let info = mock_info(&creator, &[]); + // fn setup_test(deps: DepsMut, decimals: u8, reserve_decimals: u8, curve_type: CurveType) { + // // this matches `linear_curve` test case from curves.rs + // let creator = String::from(CREATOR); + // let msg = default_instantiate(decimals, reserve_decimals, curve_type); + // let info = mock_info(&creator, &[]); -// // make sure we can instantiate with this -// let res = instantiate(deps, mock_env(), info, msg).unwrap(); -// assert_eq!(0, res.messages.len()); -// } + // // make sure we can instantiate with this + // let res = instantiate(deps, mock_env(), info, msg).unwrap(); + // assert_eq!(0, res.messages.len()); + // } /// Mock token factory querier dependencies - fn mock_tf_dependencies() -> OwnedDeps, TokenFactoryQuery> { - OwnedDeps { - storage: MockStorage::default(), - api: MockApi::default(), - querier: MockQuerier::::new(&[]), - custom_query_type: PhantomData::, - } - } #[test] fn proper_instantiation() -> CwAbcResult<()> { @@ -302,17 +259,19 @@ mod tests { slope: Uint128::new(1), scale: 1, }; - let msg = default_instantiate(2, 8, curve_type.clone()); + let msg = default_instantiate_msg(2, 8, curve_type.clone()); let info = mock_info(&creator, &[]); // make sure we can instantiate with this let res = instantiate(deps.as_mut(), mock_env(), info, msg)?; assert_that!(res.messages.len()).is_equal_to(1); let submsg = res.messages.get(0).unwrap(); - assert_that!(submsg.msg).is_equal_to(CosmosMsg::Custom(TokenFactoryMsg::Token(TokenMsg::CreateDenom { - subdenom: SUPPLY_DENOM.to_string(), - metadata: Some(default_supply_metadata()), - }))); + assert_that!(submsg.msg).is_equal_to(CosmosMsg::Custom(TokenFactoryMsg::Token( + TokenMsg::CreateDenom { + subdenom: TEST_SUPPLY_DENOM.to_string(), + metadata: Some(default_supply_metadata()), + }, + ))); // TODO! // // token info is proper @@ -326,7 +285,7 @@ mod tests { let state = query_curve_info(deps.as_ref(), curve_type.to_curve_fn())?; assert_that!(state.reserve).is_equal_to(Uint128::zero()); assert_that!(state.supply).is_equal_to(Uint128::zero()); - assert_that!(state.reserve_denom.as_str()).is_equal_to(DENOM); + assert_that!(state.reserve_denom.as_str()).is_equal_to(TEST_RESERVE_DENOM); // spot price 0 as supply is 0 assert_that!(state.spot_price).is_equal_to(Decimal::zero()); @@ -340,136 +299,136 @@ mod tests { Ok(()) } -// #[test] -// fn buy_issues_tokens() { -// let mut deps = mock_dependencies(); -// let curve_type = CurveType::Linear { -// slope: Uint128::new(1), -// scale: 1, -// }; -// setup_test(deps.as_mut(), 2, 8, curve_type.clone()); - -// // succeeds with proper token (5 BTC = 5*10^8 satoshi) -// let info = mock_info(INVESTOR, &coins(500_000_000, DENOM)); -// let buy = ExecuteMsg::Buy {}; -// execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap(); - -// // bob got 1000 EPOXY (10.00) -// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); -// assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::zero()); - -// // send them all to buyer -// let info = mock_info(INVESTOR, &[]); -// let send = ExecuteMsg::Transfer { -// recipient: BUYER.into(), -// amount: Uint128::new(1000), -// }; -// execute(deps.as_mut(), mock_env(), info, send).unwrap(); - -// // ensure balances updated -// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::zero()); -// assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::new(1000)); - -// // second stake needs more to get next 1000 EPOXY -// let info = mock_info(INVESTOR, &coins(1_500_000_000, DENOM)); -// execute(deps.as_mut(), mock_env(), info, buy).unwrap(); - -// // ensure balances updated -// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); -// assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::new(1000)); - -// // check curve info updated -// let curve = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); -// assert_eq!(curve.reserve, Uint128::new(2_000_000_000)); -// assert_eq!(curve.supply, Uint128::new(2000)); -// assert_eq!(curve.spot_price, Decimal::percent(200)); - -// // check token info updated -// let token = query_token_info(deps.as_ref()).unwrap(); -// assert_eq!(token.decimals, 2); -// assert_eq!(token.total_supply, Uint128::new(2000)); -// } - -// #[test] -// fn bonding_fails_with_wrong_denom() { -// let mut deps = mock_dependencies(); -// let curve_type = CurveType::Linear { -// slope: Uint128::new(1), -// scale: 1, -// }; -// setup_test(deps.as_mut(), 2, 8, curve_type); - -// // fails when no tokens sent -// let info = mock_info(INVESTOR, &[]); -// let buy = ExecuteMsg::Buy {}; -// let err = execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err(); -// assert_eq!(err, PaymentError::NoFunds {}.into()); - -// // fails when wrong tokens sent -// let info = mock_info(INVESTOR, &coins(1234567, "wei")); -// let err = execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err(); -// assert_eq!(err, PaymentError::MissingDenom(DENOM.into()).into()); - -// // fails when too many tokens sent -// let info = mock_info(INVESTOR, &[coin(3400022, DENOM), coin(1234567, "wei")]); -// let err = execute(deps.as_mut(), mock_env(), info, buy).unwrap_err(); -// assert_eq!(err, PaymentError::MultipleDenoms {}.into()); -// } - -// #[test] -// fn burning_sends_reserve() { -// let mut deps = mock_dependencies(); -// let curve_type = CurveType::Linear { -// slope: Uint128::new(1), -// scale: 1, -// }; -// setup_test(deps.as_mut(), 2, 8, curve_type.clone()); - -// // succeeds with proper token (20 BTC = 20*10^8 satoshi) -// let info = mock_info(INVESTOR, &coins(2_000_000_000, DENOM)); -// let buy = ExecuteMsg::Buy {}; -// execute(deps.as_mut(), mock_env(), info, buy).unwrap(); - -// // bob got 2000 EPOXY (20.00) -// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(2000)); - -// // cannot burn too much -// let info = mock_info(INVESTOR, &[]); -// let burn = ExecuteMsg::Burn { -// amount: Uint128::new(3000), -// }; -// let err = execute(deps.as_mut(), mock_env(), info, burn).unwrap_err(); -// // TODO check error - -// // burn 1000 EPOXY to get back 15BTC (*10^8) -// let info = mock_info(INVESTOR, &[]); -// let burn = ExecuteMsg::Burn { -// amount: Uint128::new(1000), -// }; -// let res = execute(deps.as_mut(), mock_env(), info, burn).unwrap(); - -// // balance is lower -// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); - -// // ensure we got our money back -// assert_eq!(1, res.messages.len()); -// assert_eq!( -// &res.messages[0], -// &SubMsg::new(BankMsg::Send { -// to_address: INVESTOR.into(), -// amount: coins(1_500_000_000, DENOM), -// }) -// ); - -// // check curve info updated -// let curve = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); -// assert_eq!(curve.reserve, Uint128::new(500_000_000)); -// assert_eq!(curve.supply, Uint128::new(1000)); -// assert_eq!(curve.spot_price, Decimal::percent(100)); - -// // check token info updated -// let token = query_token_info(deps.as_ref()).unwrap(); -// assert_eq!(token.decimals, 2); -// assert_eq!(token.total_supply, Uint128::new(1000)); -// } + // #[test] + // fn buy_issues_tokens() { + // let mut deps = mock_dependencies(); + // let curve_type = CurveType::Linear { + // slope: Uint128::new(1), + // scale: 1, + // }; + // setup_test(deps.as_mut(), 2, 8, curve_type.clone()); + + // // succeeds with proper token (5 BTC = 5*10^8 satoshi) + // let info = mock_info(INVESTOR, &coins(500_000_000, DENOM)); + // let buy = ExecuteMsg::Buy {}; + // execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap(); + + // // bob got 1000 EPOXY (10.00) + // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); + // assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::zero()); + + // // send them all to buyer + // let info = mock_info(INVESTOR, &[]); + // let send = ExecuteMsg::Transfer { + // recipient: BUYER.into(), + // amount: Uint128::new(1000), + // }; + // execute(deps.as_mut(), mock_env(), info, send).unwrap(); + + // // ensure balances updated + // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::zero()); + // assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::new(1000)); + + // // second stake needs more to get next 1000 EPOXY + // let info = mock_info(INVESTOR, &coins(1_500_000_000, DENOM)); + // execute(deps.as_mut(), mock_env(), info, buy).unwrap(); + + // // ensure balances updated + // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); + // assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::new(1000)); + + // // check curve info updated + // let curve = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); + // assert_eq!(curve.reserve, Uint128::new(2_000_000_000)); + // assert_eq!(curve.supply, Uint128::new(2000)); + // assert_eq!(curve.spot_price, Decimal::percent(200)); + + // // check token info updated + // let token = query_token_info(deps.as_ref()).unwrap(); + // assert_eq!(token.decimals, 2); + // assert_eq!(token.total_supply, Uint128::new(2000)); + // } + + // #[test] + // fn bonding_fails_with_wrong_denom() { + // let mut deps = mock_dependencies(); + // let curve_type = CurveType::Linear { + // slope: Uint128::new(1), + // scale: 1, + // }; + // setup_test(deps.as_mut(), 2, 8, curve_type); + + // // fails when no tokens sent + // let info = mock_info(INVESTOR, &[]); + // let buy = ExecuteMsg::Buy {}; + // let err = execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err(); + // assert_eq!(err, PaymentError::NoFunds {}.into()); + + // // fails when wrong tokens sent + // let info = mock_info(INVESTOR, &coins(1234567, "wei")); + // let err = execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err(); + // assert_eq!(err, PaymentError::MissingDenom(DENOM.into()).into()); + + // // fails when too many tokens sent + // let info = mock_info(INVESTOR, &[coin(3400022, DENOM), coin(1234567, "wei")]); + // let err = execute(deps.as_mut(), mock_env(), info, buy).unwrap_err(); + // assert_eq!(err, PaymentError::MultipleDenoms {}.into()); + // } + + // #[test] + // fn burning_sends_reserve() { + // let mut deps = mock_dependencies(); + // let curve_type = CurveType::Linear { + // slope: Uint128::new(1), + // scale: 1, + // }; + // setup_test(deps.as_mut(), 2, 8, curve_type.clone()); + + // // succeeds with proper token (20 BTC = 20*10^8 satoshi) + // let info = mock_info(INVESTOR, &coins(2_000_000_000, DENOM)); + // let buy = ExecuteMsg::Buy {}; + // execute(deps.as_mut(), mock_env(), info, buy).unwrap(); + + // // bob got 2000 EPOXY (20.00) + // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(2000)); + + // // cannot burn too much + // let info = mock_info(INVESTOR, &[]); + // let burn = ExecuteMsg::Burn { + // amount: Uint128::new(3000), + // }; + // let err = execute(deps.as_mut(), mock_env(), info, burn).unwrap_err(); + // // TODO check error + + // // burn 1000 EPOXY to get back 15BTC (*10^8) + // let info = mock_info(INVESTOR, &[]); + // let burn = ExecuteMsg::Burn { + // amount: Uint128::new(1000), + // }; + // let res = execute(deps.as_mut(), mock_env(), info, burn).unwrap(); + + // // balance is lower + // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); + + // // ensure we got our money back + // assert_eq!(1, res.messages.len()); + // assert_eq!( + // &res.messages[0], + // &SubMsg::new(BankMsg::Send { + // to_address: INVESTOR.into(), + // amount: coins(1_500_000_000, DENOM), + // }) + // ); + + // // check curve info updated + // let curve = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); + // assert_eq!(curve.reserve, Uint128::new(500_000_000)); + // assert_eq!(curve.supply, Uint128::new(1000)); + // assert_eq!(curve.spot_price, Decimal::percent(100)); + + // // check token info updated + // let token = query_token_info(deps.as_ref()).unwrap(); + // assert_eq!(token.decimals, 2); + // assert_eq!(token.total_supply, Uint128::new(1000)); + // } } diff --git a/contracts/external/cw-abc/src/error.rs b/contracts/external/cw-abc/src/error.rs index 69ece1764..2e3989515 100644 --- a/contracts/external/cw-abc/src/error.rs +++ b/contracts/external/cw-abc/src/error.rs @@ -33,4 +33,13 @@ pub enum ContractError { #[error("The commons is closed to new contributions")] CommonsClosed {}, + + #[error("Selling is disabled during the hatch phase")] + HatchSellingDisabled {}, + + #[error("Invalid sell amount")] + MismatchedSellAmount {}, + + #[error("Invalid phase, expected {expected:?}, actual {actual:?}")] + InvalidPhase { expected: String, actual: String }, } diff --git a/contracts/external/cw-abc/src/integration.rs b/contracts/external/cw-abc/src/integration.rs new file mode 100644 index 000000000..d6b23b8bc --- /dev/null +++ b/contracts/external/cw-abc/src/integration.rs @@ -0,0 +1,38 @@ +use crate::{abc::CurveType, boot::CwAbc}; +use boot_core::{BootUpload, Mock}; +use cosmwasm_std::{Addr, Uint128}; + +use crate::testing::prelude::*; + +type AResult = anyhow::Result<()>; // alias for Result<(), anyhow::Error> + +// TODO: we need to make a PR to token factory bindings for the CustomHandler so that messages will actually execute +#[test] +fn instantiate() -> AResult { + let sender = Addr::unchecked(TEST_CREATOR); + let chain = Mock::new(&sender)?; + + let abc = CwAbc::new("cw:abc", chain); + abc.upload()?; + + let curve_type = CurveType::SquareRoot { + slope: Uint128::new(1), + scale: 1, + }; + + let _init_msg = default_instantiate_msg(5u8, 5u8, curve_type); + // abc.instantiate(&init_msg, None, None)?; + // + // let expected_config = msg::CurveInfoResponse { + // reserve: Default::default(), + // supply: Default::default(), + // funding: Default::default(), + // spot_price: Default::default(), + // reserve_denom: "".to_string(), + // }; + // + // let actual_config = abc.curve_info()?; + // + // assert_that!(&actual_config).is_equal_to(&expected_config); + Ok(()) +} diff --git a/contracts/external/cw-abc/src/lib.rs b/contracts/external/cw-abc/src/lib.rs index d96c62646..5070715b8 100644 --- a/contracts/external/cw-abc/src/lib.rs +++ b/contracts/external/cw-abc/src/lib.rs @@ -1,10 +1,112 @@ +pub mod abc; +#[cfg(feature = "boot")] +pub mod boot; +pub(crate) mod commands; pub mod contract; pub mod curves; mod error; +#[cfg(test)] +mod integration; pub mod msg; -pub mod state; -pub mod abc; -pub(crate) mod commands; mod queries; +pub mod state; pub use crate::error::ContractError; + +#[cfg(test)] +pub(crate) mod testing { + use crate::abc::{ + ClosedConfig, CommonsPhaseConfig, CurveType, HatchConfig, MinMax, OpenConfig, ReserveToken, + SupplyToken, + }; + use crate::msg::InstantiateMsg; + use cosmwasm_std::{ + testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, + Decimal, OwnedDeps, Uint128, + }; + + use crate::contract; + use crate::contract::CwAbcResult; + use cosmwasm_std::DepsMut; + use std::marker::PhantomData; + use token_bindings::{Metadata, TokenFactoryQuery}; + + pub(crate) mod prelude { + pub use super::{ + default_instantiate_msg, default_supply_metadata, mock_tf_dependencies, TEST_BUYER, + TEST_CREATOR, TEST_INVESTOR, TEST_RESERVE_DENOM, TEST_SUPPLY_DENOM, + }; + pub use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + 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() -> Metadata { + Metadata { + name: Some("Bonded".to_string()), + symbol: Some("EPOXY".to_string()), + description: None, + denom_units: vec![], + base: None, + display: None, + } + } + + pub fn default_instantiate_msg( + decimals: u8, + reserve_decimals: u8, + curve_type: CurveType, + ) -> InstantiateMsg { + InstantiateMsg { + supply: SupplyToken { + subdenom: TEST_SUPPLY_DENOM.to_string(), + metadata: default_supply_metadata(), + decimals, + }, + reserve: ReserveToken { + denom: TEST_RESERVE_DENOM.to_string(), + decimals: reserve_decimals, + }, + phase_config: CommonsPhaseConfig { + hatch: HatchConfig { + initial_raise: MinMax { + min: Uint128::one(), + max: Uint128::from(1000000u128), + }, + initial_price: Uint128::one(), + initial_allocation_ratio: Decimal::percent(10u64), + exit_tax: Decimal::zero(), + }, + open: OpenConfig { + allocation_percentage: Decimal::percent(10u64), + exit_tax: Decimal::percent(10u64), + }, + closed: ClosedConfig {}, + }, + hatcher_allowlist: None, + curve_type, + } + } + + pub fn mock_init(deps: DepsMut, init_msg: InstantiateMsg) -> CwAbcResult { + let info = mock_info(TEST_CREATOR, &[]); + let env = mock_env(); + contract::instantiate(deps, env, info, init_msg) + } + + pub fn mock_tf_dependencies( + ) -> OwnedDeps, TokenFactoryQuery> { + OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MockQuerier::::new(&[]), + custom_query_type: PhantomData::, + } + } +} diff --git a/contracts/external/cw-abc/src/msg.rs b/contracts/external/cw-abc/src/msg.rs index 7aac7d1b4..c1c30a1ca 100644 --- a/contracts/external/cw-abc/src/msg.rs +++ b/contracts/external/cw-abc/src/msg.rs @@ -1,8 +1,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Decimal, Uint128, Decimal as StdDecimal}; - -use crate::abc::{CommonsPhaseConfig, CurveType, MinMax, ReserveToken, SupplyToken}; +use cosmwasm_std::{Addr, Decimal, Decimal as StdDecimal, Uint128}; +use crate::abc::{CommonsPhase, CommonsPhaseConfig, CurveType, MinMax, ReserveToken, SupplyToken}; #[cw_serde] pub struct InstantiateMsg { @@ -16,18 +15,44 @@ pub struct InstantiateMsg { pub curve_type: CurveType, // Hatch configuration information - pub phase_config: CommonsPhaseConfig, + pub phase_config: CommonsPhaseConfig, + + // Hatcher allowlist + pub hatcher_allowlist: Option>, } +/// Update the phase configurations. +/// These can only be called by the admin and only before or during each phase +#[cw_serde] +pub enum UpdatePhaseConfigMsg { + /// Update the hatch phase configuration + Hatch { + initial_raise: Option, + initial_allocation_ratio: Option, + }, + /// Update the open phase configuration + Open { + exit_tax: Option, + reserve_ratio: Option, + }, + /// Update the closed phase configuration + Closed {}, +} #[cw_ownable::cw_ownable_execute] #[cw_serde] +#[cfg_attr(feature = "boot", derive(boot_core::ExecuteFns))] pub enum ExecuteMsg { /// Buy will attempt to purchase as many supply tokens as possible. /// You must send only reserve tokens in that message + #[payable] Buy {}, - /// Implements CW20. Burn is a base message to destroy tokens forever - Burn { amount: Uint128 }, + /// Burn is a base message to destroy tokens forever + #[payable] + Burn {}, + /// Donate will add reserve tokens to the funding pool + #[payable] + Donate {}, /// Update the hatch phase allowlist UpdateHatchAllowlist { to_add: Vec, @@ -35,15 +60,13 @@ pub enum ExecuteMsg { }, /// Update the hatch phase configuration /// This can only be called by the admin and only during the hatch phase - UpdateHatchConfig { - initial_raise: Option, - initial_allocation_ratio: Option, - }, + UpdatePhaseConfig(UpdatePhaseConfigMsg), } #[cw_ownable::cw_ownable_query] #[cw_serde] #[derive(QueryResponses)] +#[cfg_attr(feature = "boot", derive(boot_core::QueryFns))] pub enum QueryMsg { /// Returns the reserve and supply quantities, as well as the spot price to buy 1 token /// Returns [`CurveInfoResponse`] @@ -52,7 +75,21 @@ pub enum QueryMsg { /// Returns the current phase configuration /// Returns [`CommonsPhaseConfigResponse`] #[returns(CommonsPhaseConfigResponse)] - PhaseConfig {} + PhaseConfig {}, + /// Returns a list of the donors and their donations + /// Returns [`DonationsResponse`] + #[returns(DonationsResponse)] + Donations { + start_after: Option, + limit: Option, + }, + /// List the hatchers and their contributions + /// Returns [`HatchersResponse`] + #[returns(HatchersResponse)] + Hatchers { + start_after: Option, + limit: Option, + }, } #[cw_serde] @@ -69,8 +106,29 @@ pub struct CurveInfoResponse { pub reserve_denom: String, } +#[cw_serde] +pub struct HatcherAllowlistResponse { + // hatcher allowlist + pub allowlist: Option>, +} + #[cw_serde] pub struct CommonsPhaseConfigResponse { // the phase configuration - pub phase_config: CommonsPhaseConfig, + 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)>, } diff --git a/contracts/external/cw-abc/src/queries.rs b/contracts/external/cw-abc/src/queries.rs index d6c655f62..c6d12117b 100644 --- a/contracts/external/cw-abc/src/queries.rs +++ b/contracts/external/cw-abc/src/queries.rs @@ -1,8 +1,11 @@ -use cosmwasm_std::{Deps, StdResult}; -use token_bindings::TokenFactoryQuery; use crate::abc::CurveFn; -use crate::msg::{CommonsPhaseConfigResponse, CurveInfoResponse}; -use crate::state::{CURVE_STATE, CurveState}; +use crate::msg::{ + CommonsPhaseConfigResponse, CurveInfoResponse, DonationsResponse, HatchersResponse, +}; +use crate::state::{CurveState, CURVE_STATE, DONATIONS, HATCHERS, PHASE, PHASE_CONFIG}; +use cosmwasm_std::{Deps, Order, QuerierWrapper, StdResult}; +use std::ops::Deref; +use token_bindings::TokenFactoryQuery; /// Get the current state of the curve pub fn query_curve_info( @@ -31,15 +34,15 @@ pub fn query_curve_info( } /// Load and return the phase config -/// TODO: the allowlist will need to paged... should it be separate? pub fn query_phase_config(deps: Deps) -> StdResult { - let phase_config = crate::state::PHASE_CONFIG.load(deps.storage)?; + let phase = PHASE.load(deps.storage)?; + let phase_config = PHASE_CONFIG.load(deps.storage)?; Ok(CommonsPhaseConfigResponse { - phase_config + phase_config, + phase, }) } - // // TODO, maybe we don't need this // pub fn get_denom( // deps: Deps, @@ -53,3 +56,49 @@ pub fn query_phase_config(deps: Deps) -> StdResult, + start_aftor: Option, + limit: Option, +) -> StdResult { + let donations = cw_paginate::paginate_map( + Deps { + storage: deps.storage, + api: deps.api, + querier: QuerierWrapper::new(deps.querier.deref()), + }, + &DONATIONS, + start_aftor + .map(|addr| deps.api.addr_validate(&addr)) + .transpose()? + .as_ref(), + limit, + Order::Descending, + )?; + + Ok(DonationsResponse { donations }) +} + +pub fn query_hatchers( + deps: Deps, + start_aftor: Option, + limit: Option, +) -> StdResult { + let hatchers = cw_paginate::paginate_map( + Deps { + storage: deps.storage, + api: deps.api, + querier: QuerierWrapper::new(deps.querier.deref()), + }, + &HATCHERS, + start_aftor + .map(|addr| deps.api.addr_validate(&addr)) + .transpose()? + .as_ref(), + limit, + Order::Descending, + )?; + + Ok(HatchersResponse { hatchers }) +} diff --git a/contracts/external/cw-abc/src/state.rs b/contracts/external/cw-abc/src/state.rs index 3006501fb..cb885cc53 100644 --- a/contracts/external/cw-abc/src/state.rs +++ b/contracts/external/cw-abc/src/state.rs @@ -1,9 +1,9 @@ -use std::collections::HashSet; use cosmwasm_schema::cw_serde; +use std::collections::HashSet; +use crate::abc::{CommonsPhase, CommonsPhaseConfig, CurveType}; use cosmwasm_std::{Addr, Uint128}; -use cw_storage_plus::Item; -use crate::abc::{ CommonsPhaseConfig, CurveType, CommonsPhase}; +use cw_storage_plus::{Item, Map}; use crate::curves::DecimalPlaces; @@ -43,13 +43,20 @@ pub const CURVE_TYPE: Item = Item::new("curve_type"); /// The denom used for the supply token pub const SUPPLY_DENOM: Item = Item::new("denom"); +/// Hatcher phase allowlist +/// TODO: we could use the keys for the [`HATCHERS`] map instead setting them to 0 at the beginning, though existing hatchers would not be able to be removed +pub static HATCHER_ALLOWLIST: Item> = Item::new("hatch_allowlist"); + /// Keep track of who has contributed to the hatch phase -/// TODO: cw-set? -pub static HATCHERS: Item> = Item::new("hatchers"); +/// 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"); +pub static PHASE_CONFIG: Item = Item::new("phase_config"); /// The phase state of the Augmented Bonding Curve pub static PHASE: Item = Item::new("phase"); - From 0f390c3070a2a27b7a8a33aaf95e9d9cdbd0cd98 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Tue, 2 May 2023 13:21:49 -0700 Subject: [PATCH 04/56] Comments clean up --- contracts/external/cw-abc/src/abc.rs | 57 ++++++++++------------------ 1 file changed, 20 insertions(+), 37 deletions(-) diff --git a/contracts/external/cw-abc/src/abc.rs b/contracts/external/cw-abc/src/abc.rs index b8c49d8d2..eec4a40bb 100644 --- a/contracts/external/cw-abc/src/abc.rs +++ b/contracts/external/cw-abc/src/abc.rs @@ -7,21 +7,21 @@ use token_bindings::Metadata; #[cw_serde] pub struct SupplyToken { - // The denom to create for the supply token + /// The denom to create for the supply token pub subdenom: String, - // Metadata for the supply token to create + /// Metadata for the supply token to create pub metadata: Metadata, - // 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 + /// Number of decimal places for the supply token, needed for proper curve math. + /// Default for token factory is 6 pub decimals: u8, } #[cw_serde] pub struct ReserveToken { - // Reserve token denom (only support native for now) + /// 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 + /// 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, } @@ -35,15 +35,15 @@ pub struct MinMax { #[cw_serde] pub struct HatchConfig { // /// TODO: 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 contribution_limits: MinMax, + /// The initial raise range (min, max) in the reserve token pub initial_raise: MinMax, - // The initial price (p0) per reserve token - // TODO: initial price is not implemented yet + /// The initial price (p0) per reserve token + /// TODO: initial price is not implemented yet pub initial_price: Uint128, - // The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool + /// The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool pub initial_allocation_ratio: StdDecimal, - // Exit tax for the hatch phase + /// Exit tax for the hatch phase pub exit_tax: StdDecimal, } @@ -86,9 +86,9 @@ impl HatchConfig { #[cw_serde] pub struct OpenConfig { - // Percentage of capital put into the Reserve Pool during the Open phase + /// Percentage of capital put into the Reserve Pool during the Open phase pub allocation_percentage: StdDecimal, - // Exit taxation ratio + /// Exit taxation ratio pub exit_tax: StdDecimal, } @@ -125,33 +125,16 @@ impl ClosedConfig { #[cw_serde] pub struct CommonsPhaseConfig { - // The Hatch phase where initial contributors (Hatchers) participate in a hatch sale. + /// The Hatch phase where initial contributors (Hatchers) participate in a hatch sale. pub hatch: HatchConfig, - // 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. + /// 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. + /// The Closed phase where the Commons is closed to new members. pub closed: ClosedConfig, } -// #[derive(Default)] -// #[cw_serde] -// pub struct HatchPhaseState { -// // Initial contributors (Hatchers) -// pub hatchers: HashSet, -// } -// -// // TODO: maybe should be combined with config or just placed in state -// #[cw_serde] -// pub struct CommonsPhaseState { -// pub hatch: HatchPhaseState, -// // Vesting, -// pub open: (), -// // TODO: should we allow for a closed phase? -// pub closed: () -// } - #[cw_serde] pub enum CommonsPhase { Hatch, From 6097ea4892b39d7b1dd20bcb8f8fd5f41fe6ee3d Mon Sep 17 00:00:00 2001 From: Dat-Andre <114091333+Dat-Andre@users.noreply.github.com> Date: Mon, 8 May 2023 22:17:13 +0100 Subject: [PATCH 05/56] 697 (#702) * update cw-orch dependency * add getrandom dependency to .toml --------- Co-authored-by: Jake Hartnell --- contracts/external/cw-abc/Cargo.toml | 11 ++++++++--- contracts/external/cw-abc/src/boot.rs | 10 +++++----- contracts/external/cw-abc/src/integration.rs | 2 +- contracts/external/cw-abc/src/lib.rs | 8 ++++---- contracts/external/cw-abc/src/msg.rs | 4 ++-- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/contracts/external/cw-abc/Cargo.toml b/contracts/external/cw-abc/Cargo.toml index 0223dc1b9..d5fdc2b5a 100644 --- a/contracts/external/cw-abc/Cargo.toml +++ b/contracts/external/cw-abc/Cargo.toml @@ -1,7 +1,11 @@ [package] name = "cw-abc" version = "0.0.1" -authors = ["Ethan Frey ", "Jake Hartnell", "Adair "] +authors = [ + "Ethan Frey ", + "Jake Hartnell", + "Adair ", +] edition = { workspace = true } description = "Implements an Augmented Bonding Curve" license = "Apache-2.0" @@ -16,7 +20,7 @@ crate-type = ["cdylib", "rlib"] backtraces = ["cosmwasm-std/backtraces"] # use library feature to disable all instantiate/execute/query exports library = [] -boot = ["dep:boot-core"] +boot = ["dep:cw-orch"] [dependencies] cw-utils = { workspace = true } @@ -30,11 +34,12 @@ cw-address-like = "1.0.4" rust_decimal = "1.14.3" integer-sqrt = "0.1.5" integer-cbrt = "0.1.2" +getrandom = { version = "0.2", features = ["js"] } # TODO publish this token-bindings = { git = "https://github.com/CosmosContracts/token-bindings", rev = "1412b94" } cw-ownable = { workspace = true } cw-paginate-storage = { workspace = true } -boot-core = { version = "0.10.0", optional = true, git = "https://github.com/AbstractSDK/BOOT", branch = "fix/custom_binding_contract_wrapper" } +cw-orch = { version = "0.13.3", optional = true, git = "https://github.com/AbstractSDK/cw-orchestrator", branch = "main" } [dev-dependencies] # TODO move to workspace diff --git a/contracts/external/cw-abc/src/boot.rs b/contracts/external/cw-abc/src/boot.rs index d8376b0e5..ad2309b0b 100644 --- a/contracts/external/cw-abc/src/boot.rs +++ b/contracts/external/cw-abc/src/boot.rs @@ -1,9 +1,9 @@ use crate::msg::*; -use boot_core::{contract, Contract, CwEnv}; -#[cfg(feature = "daemon")] -use boot_core::{ArtifactsDir, Daemon, WasmPath}; -use boot_core::{ContractWrapper, Mock, MockState, TxHandler, Uploadable}; use cosmwasm_std::Empty; +use cw_orch::{contract, Contract, CwEnv}; +#[cfg(feature = "daemon")] +use cw_orch::{ArtifactsDir, Daemon, WasmPath}; +use cw_orch::{ContractWrapper, Mock, MockState, TxHandler, Uploadable}; use token_bindings::{TokenFactoryMsg, TokenFactoryQuery}; #[contract(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] @@ -18,7 +18,7 @@ impl CwAbc { /// Basic app for the token factory contract /// TODO: should be in the bindings, along with custom handler for multi-test -pub(crate) type TokenFactoryBasicApp = boot_core::BasicApp; +pub(crate) type TokenFactoryBasicApp = cw_orch::BasicApp; type TokenFactoryMock = Mock; diff --git a/contracts/external/cw-abc/src/integration.rs b/contracts/external/cw-abc/src/integration.rs index d6b23b8bc..767dbdf77 100644 --- a/contracts/external/cw-abc/src/integration.rs +++ b/contracts/external/cw-abc/src/integration.rs @@ -1,6 +1,6 @@ use crate::{abc::CurveType, boot::CwAbc}; -use boot_core::{BootUpload, Mock}; use cosmwasm_std::{Addr, Uint128}; +use cw_orch::{CwOrcUpload, Mock}; use crate::testing::prelude::*; diff --git a/contracts/external/cw-abc/src/lib.rs b/contracts/external/cw-abc/src/lib.rs index 5070715b8..24ac48a7d 100644 --- a/contracts/external/cw-abc/src/lib.rs +++ b/contracts/external/cw-abc/src/lib.rs @@ -33,8 +33,8 @@ pub(crate) mod testing { pub(crate) mod prelude { pub use super::{ - default_instantiate_msg, default_supply_metadata, mock_tf_dependencies, TEST_BUYER, - TEST_CREATOR, TEST_INVESTOR, TEST_RESERVE_DENOM, TEST_SUPPLY_DENOM, + default_instantiate_msg, default_supply_metadata, mock_tf_dependencies, TEST_CREATOR, + TEST_RESERVE_DENOM, TEST_SUPPLY_DENOM, _TEST_BUYER, _TEST_INVESTOR, }; pub use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; pub use speculoos::prelude::*; @@ -42,8 +42,8 @@ pub(crate) mod testing { 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_INVESTOR: &str = "investor"; + pub const _TEST_BUYER: &str = "buyer"; pub const TEST_SUPPLY_DENOM: &str = "subdenom"; diff --git a/contracts/external/cw-abc/src/msg.rs b/contracts/external/cw-abc/src/msg.rs index c1c30a1ca..92de30f70 100644 --- a/contracts/external/cw-abc/src/msg.rs +++ b/contracts/external/cw-abc/src/msg.rs @@ -41,7 +41,7 @@ pub enum UpdatePhaseConfigMsg { #[cw_ownable::cw_ownable_execute] #[cw_serde] -#[cfg_attr(feature = "boot", derive(boot_core::ExecuteFns))] +#[cfg_attr(feature = "boot", derive(cw_orch::ExecuteFns))] pub enum ExecuteMsg { /// Buy will attempt to purchase as many supply tokens as possible. /// You must send only reserve tokens in that message @@ -66,7 +66,7 @@ pub enum ExecuteMsg { #[cw_ownable::cw_ownable_query] #[cw_serde] #[derive(QueryResponses)] -#[cfg_attr(feature = "boot", derive(boot_core::QueryFns))] +#[cfg_attr(feature = "boot", derive(cw_orch::QueryFns))] pub enum QueryMsg { /// Returns the reserve and supply quantities, as well as the spot price to buy 1 token /// Returns [`CurveInfoResponse`] From 84244ed8aa0cdc01d8289da77d0a48c77cb2e5c4 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Sun, 9 Jul 2023 18:24:19 +0200 Subject: [PATCH 06/56] Remove boot / cw-orch, get cw-multi-test working with custom messages --- Cargo.lock | 4 + contracts/external/cw-abc/Cargo.toml | 9 +- contracts/external/cw-abc/src/boot.rs | 43 ---- contracts/external/cw-abc/src/contract.rs | 9 +- contracts/external/cw-abc/src/integration.rs | 226 ++++++++++++++++--- contracts/external/cw-abc/src/lib.rs | 2 - contracts/external/cw-abc/src/msg.rs | 8 +- contracts/external/cw-abc/src/queries.rs | 4 +- 8 files changed, 212 insertions(+), 93 deletions(-) delete mode 100644 contracts/external/cw-abc/src/boot.rs diff --git a/Cargo.lock b/Cargo.lock index 7b72e98d5..02707ec28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -726,13 +726,17 @@ dependencies = [ name = "cw-abc" version = "0.0.1" dependencies = [ + "anyhow", "cosmwasm-schema", "cosmwasm-std", "cw-address-like", + "cw-multi-test", "cw-ownable", + "cw-paginate-storage 2.3.0", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw2 1.1.2", + "getrandom", "integer-cbrt", "integer-sqrt", "rust_decimal", diff --git a/contracts/external/cw-abc/Cargo.toml b/contracts/external/cw-abc/Cargo.toml index d5fdc2b5a..bd2be85fd 100644 --- a/contracts/external/cw-abc/Cargo.toml +++ b/contracts/external/cw-abc/Cargo.toml @@ -20,7 +20,7 @@ crate-type = ["cdylib", "rlib"] backtraces = ["cosmwasm-std/backtraces"] # use library feature to disable all instantiate/execute/query exports library = [] -boot = ["dep:cw-orch"] +# interface = ["dep:cw-orch"] # Adds the dependency when the feature is enabled [dependencies] cw-utils = { workspace = true } @@ -35,18 +35,17 @@ rust_decimal = "1.14.3" integer-sqrt = "0.1.5" integer-cbrt = "0.1.2" getrandom = { version = "0.2", features = ["js"] } -# TODO publish this +# TODO publish this and move to workspace token-bindings = { git = "https://github.com/CosmosContracts/token-bindings", rev = "1412b94" } cw-ownable = { workspace = true } cw-paginate-storage = { workspace = true } -cw-orch = { version = "0.13.3", optional = true, git = "https://github.com/AbstractSDK/cw-orchestrator", branch = "main" } +# cw-orch = { version = "0.13.3", optional = true } [dev-dependencies] # TODO move to workspace speculoos = "0.11.0" -#cw-multi-test = { version = "0.16.0" } anyhow = { workspace = true } -cw-abc = { path = ".", features = ["boot"] } +cw-multi-test = { workspace = true } [profile.release] rpath = false diff --git a/contracts/external/cw-abc/src/boot.rs b/contracts/external/cw-abc/src/boot.rs deleted file mode 100644 index ad2309b0b..000000000 --- a/contracts/external/cw-abc/src/boot.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::msg::*; -use cosmwasm_std::Empty; -use cw_orch::{contract, Contract, CwEnv}; -#[cfg(feature = "daemon")] -use cw_orch::{ArtifactsDir, Daemon, WasmPath}; -use cw_orch::{ContractWrapper, Mock, MockState, TxHandler, Uploadable}; -use token_bindings::{TokenFactoryMsg, TokenFactoryQuery}; - -#[contract(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] -pub struct CwAbc; - -impl CwAbc { - pub fn new(name: &str, chain: Chain) -> Self { - let contract = Contract::new(name, chain); - Self(contract) - } -} - -/// Basic app for the token factory contract -/// TODO: should be in the bindings, along with custom handler for multi-test -pub(crate) type TokenFactoryBasicApp = cw_orch::BasicApp; - -type TokenFactoryMock = Mock; - -impl Uploadable for CwAbc { - fn source(&self) -> ::ContractSource { - Box::new(ContractWrapper::new( - crate::contract::execute, - crate::contract::instantiate, - crate::contract::query, - )) - } -} - -#[cfg(feature = "daemon")] -impl Uploadable for CwAbc { - fn source(&self) -> ::ContractSource { - ArtifactsDir::env() - .expect("Expected ARTIFACTS_DIR in env") - .find_wasm_path("cw_abc") - .unwrap() - } -} diff --git a/contracts/external/cw-abc/src/contract.rs b/contracts/external/cw-abc/src/contract.rs index c36470c33..f21a9cd33 100644 --- a/contracts/external/cw-abc/src/contract.rs +++ b/contracts/external/cw-abc/src/contract.rs @@ -6,18 +6,18 @@ use std::collections::HashSet; use token_bindings::{TokenFactoryMsg, TokenFactoryQuery, TokenMsg}; -use crate::abc::CurveFn; +use crate::abc::{CommonsPhase, CurveFn}; use crate::curves::DecimalPlaces; use crate::error::ContractError; use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, UpdatePhaseConfigMsg}; use crate::state::{ - CurveState, CURVE_STATE, CURVE_TYPE, HATCHER_ALLOWLIST, PHASE_CONFIG, SUPPLY_DENOM, + CurveState, CURVE_STATE, CURVE_TYPE, HATCHER_ALLOWLIST, PHASE, PHASE_CONFIG, SUPPLY_DENOM, }; use crate::{commands, queries}; use cw_utils::nonpayable; // version info for migration info -pub(crate) const CONTRACT_NAME: &str = "crates.io:cw20-abc"; +pub(crate) const CONTRACT_NAME: &str = "crates.io:cw-abc"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); // By default, the prefix for token factory tokens is "factory" @@ -86,6 +86,9 @@ pub fn instantiate( PHASE_CONFIG.save(deps.storage, &phase_config)?; + // TODO don't hardcode this? + PHASE.save(deps.storage, &CommonsPhase::Hatch)?; + cw_ownable::initialize_owner(deps.storage, deps.api, Some(info.sender.as_str()))?; Ok(Response::default().add_message(create_supply_denom_msg)) diff --git a/contracts/external/cw-abc/src/integration.rs b/contracts/external/cw-abc/src/integration.rs index 767dbdf77..b7de8d3b4 100644 --- a/contracts/external/cw-abc/src/integration.rs +++ b/contracts/external/cw-abc/src/integration.rs @@ -1,38 +1,198 @@ -use crate::{abc::CurveType, boot::CwAbc}; -use cosmwasm_std::{Addr, Uint128}; -use cw_orch::{CwOrcUpload, Mock}; +use cosmwasm_std::{ + coin, coins, + testing::{MockApi, MockStorage}, + Addr, Api, Coin, Decimal, Empty, GovMsg, IbcMsg, IbcQuery, StdResult, Storage, Uint128, +}; +use cw_multi_test::{ + custom_app, + custom_handler::{CachingCustomHandler, CachingCustomHandlerState}, + App, AppBuilder, AppResponse, BankKeeper, BankSudo, Contract, ContractWrapper, + DistributionKeeper, Executor, FailingModule, Router, StakeKeeper, WasmKeeper, +}; +use cw_utils::PaymentError; +use token_bindings::{Metadata, TokenFactoryMsg, TokenFactoryQuery}; -use crate::testing::prelude::*; +use crate::{ + abc::{ + ClosedConfig, CommonsPhaseConfig, HatchConfig, MinMax, OpenConfig, ReserveToken, + SupplyToken, + }, + msg::{CurveInfoResponse, InstantiateMsg}, + ContractError, +}; -type AResult = anyhow::Result<()>; // alias for Result<(), anyhow::Error> +pub struct Test { + pub app: App< + BankKeeper, + MockApi, + MockStorage, + CachingCustomHandler, + WasmKeeper, + StakeKeeper, + DistributionKeeper, + FailingModule, + FailingModule, + >, + pub addr: Addr, + pub owner: Addr, + pub recipient: Addr, + pub custom_handler_state: CachingCustomHandlerState, +} + +impl Test { + pub fn new() -> Self { + let owner = Addr::unchecked("owner"); + let recipient = Addr::unchecked("recipient"); + + let custom_handler = CachingCustomHandler::::new(); + let custom_handler_state = custom_handler.state(); + + let mut app = AppBuilder::new_custom() + .with_custom(custom_handler) + .build(|_, _, _| {}); + + app.sudo(cw_multi_test::SudoMsg::Bank(BankSudo::Mint { + to_address: owner.to_string(), + amount: vec![coin(10000, "ujuno"), coin(10000, "uatom")], + })) + .unwrap(); + app.sudo(cw_multi_test::SudoMsg::Bank(BankSudo::Mint { + to_address: recipient.to_string(), + amount: vec![coin(10000, "ujuno"), coin(10000, "uatom")], + })) + .unwrap(); + + let code_id = app.store_code(abc_countract()); + let addr = app + .instantiate_contract( + code_id, + owner.clone(), + &InstantiateMsg { + supply: SupplyToken { + subdenom: "subdenom".to_string(), + metadata: Metadata { + description: None, + denom_units: vec![], + base: None, + display: None, + name: None, + symbol: None, + }, + decimals: 6, + }, + reserve: ReserveToken { + denom: "ujuno".to_string(), + decimals: 6, + }, + curve_type: crate::abc::CurveType::Linear { + slope: Uint128::new(1), + scale: 2, + }, + phase_config: CommonsPhaseConfig { + hatch: HatchConfig { + initial_raise: MinMax { + min: Uint128::new(100), + max: Uint128::new(1000), + }, + initial_price: Uint128::new(1), + initial_allocation_ratio: Decimal::percent(10), + exit_tax: Decimal::percent(10), + }, + open: OpenConfig { + allocation_percentage: Decimal::percent(10), + exit_tax: Decimal::percent(10), + }, + closed: ClosedConfig {}, + }, + hatcher_allowlist: None, + }, + &[], + "cw-bounties", + None, + ) + .unwrap(); + Self { + app, + addr, + owner, + recipient, + custom_handler_state, + } + } + + pub fn buy(&mut self, amount: Vec) -> Result { + let msg = crate::msg::ExecuteMsg::Buy {}; + let res = + self.app + .execute_contract(self.owner.clone(), self.addr.clone(), &msg, &amount)?; + Ok(res) + } + + pub fn query_curve_info(&self) -> StdResult { + let msg = crate::msg::QueryMsg::CurveInfo {}; + self.app.wrap().query_wasm_smart(&self.addr, &msg) + } +} + +fn abc_countract() -> Box> { + let contract = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + ); + Box::new(contract) +} -// TODO: we need to make a PR to token factory bindings for the CustomHandler so that messages will actually execute #[test] -fn instantiate() -> AResult { - let sender = Addr::unchecked(TEST_CREATOR); - let chain = Mock::new(&sender)?; - - let abc = CwAbc::new("cw:abc", chain); - abc.upload()?; - - let curve_type = CurveType::SquareRoot { - slope: Uint128::new(1), - scale: 1, - }; - - let _init_msg = default_instantiate_msg(5u8, 5u8, curve_type); - // abc.instantiate(&init_msg, None, None)?; - // - // let expected_config = msg::CurveInfoResponse { - // reserve: Default::default(), - // supply: Default::default(), - // funding: Default::default(), - // spot_price: Default::default(), - // reserve_denom: "".to_string(), - // }; - // - // let actual_config = abc.curve_info()?; - // - // assert_that!(&actual_config).is_equal_to(&expected_config); - Ok(()) +pub fn test_happy_path() { + let mut test = Test::new(); + + // Curve has been initialized + let curve_info = test.query_curve_info().unwrap(); + assert_eq!( + curve_info, + CurveInfoResponse { + reserve: Uint128::zero(), + supply: Uint128::zero(), + funding: Uint128::zero(), + spot_price: Decimal::zero(), + reserve_denom: "ujuno".to_string(), + } + ); + + let balance_before = test + .app + .wrap() + .query_balance(test.owner.clone(), "ujuno") + .unwrap(); + + // Buy some coins + test.buy(coins(100, "ujuno")).unwrap(); + + // Curve has been updated + let curve_info = test.query_curve_info().unwrap(); + assert_eq!( + curve_info, + CurveInfoResponse { + reserve: Uint128::new(90), + supply: Uint128::new(134164), + funding: Uint128::new(10), + // TODO investigate why does this take 8 for decimals? + spot_price: Decimal::from_atomics(Uint128::new(134164), 8).unwrap(), + reserve_denom: "ujuno".to_string(), + } + ); + + // assert balance + let balance_after = test + .app + .wrap() + .query_balance(test.owner.clone(), "ujuno") + .unwrap(); + assert!( + balance_before.amount.u128() == (balance_after.amount.u128() + 100), + "before: {}, after: {}", + balance_before.amount.u128(), + balance_after.amount.u128() + ); } diff --git a/contracts/external/cw-abc/src/lib.rs b/contracts/external/cw-abc/src/lib.rs index 24ac48a7d..599b96043 100644 --- a/contracts/external/cw-abc/src/lib.rs +++ b/contracts/external/cw-abc/src/lib.rs @@ -1,6 +1,4 @@ pub mod abc; -#[cfg(feature = "boot")] -pub mod boot; pub(crate) mod commands; pub mod contract; pub mod curves; diff --git a/contracts/external/cw-abc/src/msg.rs b/contracts/external/cw-abc/src/msg.rs index 92de30f70..89b43239c 100644 --- a/contracts/external/cw-abc/src/msg.rs +++ b/contracts/external/cw-abc/src/msg.rs @@ -41,17 +41,13 @@ pub enum UpdatePhaseConfigMsg { #[cw_ownable::cw_ownable_execute] #[cw_serde] -#[cfg_attr(feature = "boot", derive(cw_orch::ExecuteFns))] pub enum ExecuteMsg { /// Buy will attempt to purchase as many supply tokens as possible. /// You must send only reserve tokens in that message - #[payable] Buy {}, /// Burn is a base message to destroy tokens forever - #[payable] Burn {}, /// Donate will add reserve tokens to the funding pool - #[payable] Donate {}, /// Update the hatch phase allowlist UpdateHatchAllowlist { @@ -66,7 +62,6 @@ pub enum ExecuteMsg { #[cw_ownable::cw_ownable_query] #[cw_serde] #[derive(QueryResponses)] -#[cfg_attr(feature = "boot", derive(cw_orch::QueryFns))] pub enum QueryMsg { /// Returns the reserve and supply quantities, as well as the spot price to buy 1 token /// Returns [`CurveInfoResponse`] @@ -132,3 +127,6 @@ pub struct HatchersResponse { // the hatchers mapped to their contribution in the reserve token pub hatchers: Vec<(Addr, Uint128)>, } + +#[cw_serde] +pub enum MigrateMsg {} diff --git a/contracts/external/cw-abc/src/queries.rs b/contracts/external/cw-abc/src/queries.rs index c6d12117b..94670499f 100644 --- a/contracts/external/cw-abc/src/queries.rs +++ b/contracts/external/cw-abc/src/queries.rs @@ -62,7 +62,7 @@ pub fn query_donations( start_aftor: Option, limit: Option, ) -> StdResult { - let donations = cw_paginate::paginate_map( + let donations = cw_paginate_storage::paginate_map( Deps { storage: deps.storage, api: deps.api, @@ -85,7 +85,7 @@ pub fn query_hatchers( start_aftor: Option, limit: Option, ) -> StdResult { - let hatchers = cw_paginate::paginate_map( + let hatchers = cw_paginate_storage::paginate_map( Deps { storage: deps.storage, api: deps.api, From 3b4965abb0da12a3a52f05b9cf36a5c1353d87ab Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Thu, 10 Aug 2023 21:54:37 +0200 Subject: [PATCH 07/56] Attempt to get tests working --- contracts/external/cw-abc/Cargo.toml | 1 + contracts/external/cw-abc/src/integration.rs | 149 +++++++++++++++++-- contracts/external/cw-abc/src/lib.rs | 2 + contracts/external/cw-abc/src/testtube.rs | 122 +++++++++++++++ 4 files changed, 264 insertions(+), 10 deletions(-) create mode 100644 contracts/external/cw-abc/src/testtube.rs diff --git a/contracts/external/cw-abc/Cargo.toml b/contracts/external/cw-abc/Cargo.toml index bd2be85fd..9a8766113 100644 --- a/contracts/external/cw-abc/Cargo.toml +++ b/contracts/external/cw-abc/Cargo.toml @@ -46,6 +46,7 @@ cw-paginate-storage = { workspace = true } speculoos = "0.11.0" anyhow = { workspace = true } cw-multi-test = { workspace = true } +osmosis-test-tube = "16.0.0" [profile.release] rpath = false diff --git a/contracts/external/cw-abc/src/integration.rs b/contracts/external/cw-abc/src/integration.rs index b7de8d3b4..d9c72c985 100644 --- a/contracts/external/cw-abc/src/integration.rs +++ b/contracts/external/cw-abc/src/integration.rs @@ -1,16 +1,19 @@ +use anyhow::Result as AnyResult; +use cosmwasm_schema::{schemars::JsonSchema, serde::de::DeserializeOwned}; use cosmwasm_std::{ coin, coins, testing::{MockApi, MockStorage}, - Addr, Api, Coin, Decimal, Empty, GovMsg, IbcMsg, IbcQuery, StdResult, Storage, Uint128, + Addr, Api, Binary, BlockInfo, Coin, CustomQuery, Decimal, Empty, GovMsg, IbcMsg, IbcQuery, + Querier, StdResult, Storage, Uint128, }; use cw_multi_test::{ custom_app, custom_handler::{CachingCustomHandler, CachingCustomHandlerState}, - App, AppBuilder, AppResponse, BankKeeper, BankSudo, Contract, ContractWrapper, - DistributionKeeper, Executor, FailingModule, Router, StakeKeeper, WasmKeeper, + next_block, App, AppBuilder, AppResponse, BankKeeper, BankSudo, BasicAppBuilder, Contract, + ContractWrapper, CosmosRouter, DistributionKeeper, Executor, FailingModule, Module, + StakeKeeper, SudoMsg, WasmKeeper, }; -use cw_utils::PaymentError; -use token_bindings::{Metadata, TokenFactoryMsg, TokenFactoryQuery}; +use token_bindings::{Metadata, TokenFactoryMsg, TokenFactoryQuery, TokenMsg}; use crate::{ abc::{ @@ -18,15 +21,104 @@ use crate::{ SupplyToken, }, msg::{CurveInfoResponse, InstantiateMsg}, - ContractError, }; +pub struct CustomHandler {} + +impl Module for CustomHandler { + type ExecT = TokenFactoryMsg; + type QueryT = TokenFactoryQuery; + type SudoT = Empty; + + fn execute( + &self, + api: &dyn Api, + storage: &mut dyn Storage, + router: &dyn CosmosRouter, + block: &BlockInfo, + _sender: Addr, + msg: Self::ExecT, + ) -> AnyResult + where + ExecC: std::fmt::Debug + Clone + PartialEq + JsonSchema + DeserializeOwned + 'static, + QueryC: CustomQuery + DeserializeOwned + 'static, + { + println!("msg {:?}", msg); + match msg { + TokenFactoryMsg::Token(TokenMsg::MintTokens { + denom, + amount, + mint_to_address, + }) => { + println!("minting tokens"); + + // mint new tokens + let mint = SudoMsg::Bank(BankSudo::Mint { + to_address: mint_to_address, + amount: vec![Coin { + denom: denom.clone(), + amount: amount.clone(), + }], + }); + return Ok(router.sudo(api, storage, block, mint)?); + } + TokenFactoryMsg::Token(TokenMsg::CreateDenom { subdenom, metadata }) => { + println!("creating denom"); + return Ok(AppResponse::default()); + } + _ => unimplemented!(), + }; + } + + fn sudo( + &self, + _api: &dyn Api, + _storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + _msg: Self::SudoT, + ) -> AnyResult + where + ExecC: std::fmt::Debug + Clone + PartialEq + JsonSchema + DeserializeOwned + 'static, + QueryC: CustomQuery + DeserializeOwned + 'static, + { + unimplemented!() + } + + fn query( + &self, + _api: &dyn Api, + _storage: &dyn Storage, + _querier: &dyn Querier, + _block: &BlockInfo, + _request: Self::QueryT, + ) -> AnyResult { + unimplemented!() + } +} + +// impl CustomHandler { +// // this is a custom initialization method +// pub fn set_payout( +// &self, +// storage: &mut dyn Storage, +// lottery: Coin, +// pity: Coin, +// ) -> AnyResult<()> { +// LOTTERY.save(storage, &lottery)?; +// PITY.save(storage, &pity)?; +// Ok(()) +// } +// } + pub struct Test { pub app: App< BankKeeper, MockApi, MockStorage, - CachingCustomHandler, + // CachingCustomHandler, + // FailingModule, + CustomHandler, WasmKeeper, StakeKeeper, DistributionKeeper, @@ -47,8 +139,19 @@ impl Test { let custom_handler = CachingCustomHandler::::new(); let custom_handler_state = custom_handler.state(); - let mut app = AppBuilder::new_custom() - .with_custom(custom_handler) + // let mut app = AppBuilder::new_custom() + // .with_custom(custom_handler) + // .build(|_, _, _| {}); + + // let mut app = custom_app::(|router, _, storage| { + // router + // .bank + // .init_balance(storage, &owner, coins(10000, "ujuno")) + // .unwrap(); + // }); + + let mut app = BasicAppBuilder::::new_custom() + .with_custom(CustomHandler {}) .build(|_, _, _| {}); app.sudo(cw_multi_test::SudoMsg::Bank(BankSudo::Mint { @@ -128,6 +231,14 @@ impl Test { Ok(res) } + pub fn burn(&mut self, amount: Vec) -> Result { + let msg = crate::msg::ExecuteMsg::Burn {}; + let res = + self.app + .execute_contract(self.owner.clone(), self.addr.clone(), &msg, &amount)?; + Ok(res) + } + pub fn query_curve_info(&self) -> StdResult { let msg = crate::msg::QueryMsg::CurveInfo {}; self.app.wrap().query_wasm_smart(&self.addr, &msg) @@ -169,6 +280,9 @@ pub fn test_happy_path() { // Buy some coins test.buy(coins(100, "ujuno")).unwrap(); + // // Update block + // test.app.update_block(next_block); + // Curve has been updated let curve_info = test.query_curve_info().unwrap(); assert_eq!( @@ -183,7 +297,7 @@ pub fn test_happy_path() { } ); - // assert balance + // Assert balance let balance_after = test .app .wrap() @@ -195,4 +309,19 @@ pub fn test_happy_path() { balance_before.amount.u128(), balance_after.amount.u128() ); + + // TODO get denom + let tf_balance = test + .app + .wrap() + .query_balance(test.addr.clone(), "factory/contract0/subdenom") + .unwrap(); + // TODO how to handle this? + println!("{:?}", tf_balance); + + // // Burn some coins + // test.burn(coins(100, "ujuno")).unwrap(); + + // let curve_info = test.query_curve_info().unwrap(); + // println!("{:?}", curve_info); } diff --git a/contracts/external/cw-abc/src/lib.rs b/contracts/external/cw-abc/src/lib.rs index 599b96043..3191289c5 100644 --- a/contracts/external/cw-abc/src/lib.rs +++ b/contracts/external/cw-abc/src/lib.rs @@ -8,6 +8,8 @@ mod integration; pub mod msg; mod queries; pub mod state; +#[cfg(test)] +mod testtube; pub use crate::error::ContractError; diff --git a/contracts/external/cw-abc/src/testtube.rs b/contracts/external/cw-abc/src/testtube.rs new file mode 100644 index 000000000..d4a32c5ac --- /dev/null +++ b/contracts/external/cw-abc/src/testtube.rs @@ -0,0 +1,122 @@ +use crate::{ + abc::{ + ClosedConfig, CommonsPhaseConfig, HatchConfig, MinMax, OpenConfig, ReserveToken, + SupplyToken, + }, + msg::{CurveInfoResponse, ExecuteMsg, InstantiateMsg, QueryMsg}, +}; +use cosmwasm_std::{Coin, Decimal, Uint128}; +use osmosis_test_tube::{Account, Module, OsmosisTestApp, Wasm}; +use token_bindings::Metadata; + +#[test] +fn test_tf() { + // Atempt to write tests with test-tube: https://github.com/osmosis-labs/test-tube + let app = OsmosisTestApp::new(); + let accs = app + .init_accounts( + &[ + Coin::new(1_000_000_000_000, "uatom"), + Coin::new(1_000_000_000_000, "uosmo"), + ], + 2, + ) + .unwrap(); + let admin = &accs[0]; + let new_admin = &accs[1]; + + let wasm = Wasm::new(&app); + + let wasm_byte_code = std::fs::read("../../../artifacts/cw_abc-aarch64.wasm").unwrap(); + let code_id = wasm + .store_code(&wasm_byte_code, None, admin) + .unwrap() + .data + .code_id; + + // instantiate contract + let contract_addr = wasm + .instantiate( + code_id, + &InstantiateMsg { + supply: SupplyToken { + subdenom: "subdenom".to_string(), + metadata: Metadata { + description: None, + denom_units: vec![], + base: None, + display: None, + name: None, + symbol: None, + }, + decimals: 6, + }, + reserve: ReserveToken { + denom: "ujuno".to_string(), + decimals: 6, + }, + curve_type: crate::abc::CurveType::Linear { + slope: Uint128::new(1), + scale: 2, + }, + phase_config: CommonsPhaseConfig { + hatch: HatchConfig { + initial_raise: MinMax { + min: Uint128::new(100), + max: Uint128::new(1000), + }, + initial_price: Uint128::new(1), + initial_allocation_ratio: Decimal::percent(10), + exit_tax: Decimal::percent(10), + }, + open: OpenConfig { + allocation_percentage: Decimal::percent(10), + exit_tax: Decimal::percent(10), + }, + closed: ClosedConfig {}, + }, + hatcher_allowlist: None, + }, + None, // contract admin used for migration, not the same as cw1_whitelist admin + Some("cw-bounties"), // contract label + &[], // funds + admin, // signer + ) + .unwrap() + .data + .address; + println!("{:?}", contract_addr); + + let curve_info = wasm + .query::(&contract_addr, &QueryMsg::CurveInfo {}) + .unwrap(); + println!("{:?}", curve_info); + + // let admin_list = wasm + // .query::(&contract_addr, &QueryMsg::AdminList {}) + // .unwrap(); + + // assert_eq!(admin_list.admins, init_admins); + // assert!(admin_list.mutable); + + // // ============= NEW CODE ================ + + // // update admin list and rechec the state + // let new_admins = vec![new_admin.address()]; + // wasm.execute::( + // &contract_addr, + // &ExecuteMsg::UpdateAdmins { + // admins: new_admins.clone(), + // }, + // &[], + // admin, + // ) + // .unwrap(); + + // let admin_list = wasm + // .query::(&contract_addr, &QueryMsg::AdminList {}) + // .unwrap(); + + // assert_eq!(admin_list.admins, new_admins); + // assert!(admin_list.mutable); +} From 98b02039761ed9a16f22c8c65dd4c732434f8ebe Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Wed, 30 Aug 2023 14:46:07 -0700 Subject: [PATCH 08/56] Start prepping for refactor and tests --- Cargo.lock | 10 +- Cargo.toml | 2 + contracts/external/cw-abc/Cargo.toml | 8 +- contracts/external/cw-abc/src/commands.rs | 13 +- contracts/external/cw-abc/src/contract.rs | 449 +++++++++--------- contracts/external/cw-abc/src/lib.rs | 6 + .../cw-abc/src/test_tube/integration_tests.rs | 17 + .../external/cw-abc/src/test_tube/mod.rs | 1 + .../external/cw-abc/src/test_tube/test_env.rs | 195 ++++++++ contracts/external/cw-abc/src/testtube.rs | 122 ----- packages/dao-testing/Cargo.toml | 1 + packages/dao-testing/src/test_tube/cw_abc.rs | 155 ++++++ packages/dao-testing/src/test_tube/mod.rs | 3 + 13 files changed, 632 insertions(+), 350 deletions(-) create mode 100644 contracts/external/cw-abc/src/test_tube/integration_tests.rs create mode 100644 contracts/external/cw-abc/src/test_tube/mod.rs create mode 100644 contracts/external/cw-abc/src/test_tube/test_env.rs delete mode 100644 contracts/external/cw-abc/src/testtube.rs create mode 100644 packages/dao-testing/src/test_tube/cw_abc.rs diff --git a/Cargo.lock b/Cargo.lock index 02707ec28..ef6b222c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -732,13 +732,15 @@ dependencies = [ "cw-address-like", "cw-multi-test", "cw-ownable", - "cw-paginate-storage 2.3.0", + "cw-paginate-storage 2.4.1", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw2 1.1.2", + "dao-testing", "getrandom", "integer-cbrt", "integer-sqrt", + "osmosis-test-tube", "rust_decimal", "speculoos", "thiserror", @@ -2050,6 +2052,7 @@ version = "2.4.1" dependencies = [ "cosmwasm-schema", "cosmwasm-std", + "cw-abc", "cw-core", "cw-hooks", "cw-multi-test", @@ -4716,8 +4719,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "token-bindings" -version = "0.9.0" -source = "git+https://github.com/CosmosContracts/token-bindings?rev=1412b94#1412b9498347c3e2a1f80fd106fffd9cec697ca0" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be1c893c90d2993320d9722516ece705460f464616313a62edadb9e71df4502" dependencies = [ "cosmwasm-schema", "cosmwasm-std", diff --git a/Cargo.toml b/Cargo.toml index a6701bf57..ac10933ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,12 +74,14 @@ sg721-base = "3.1.0" syn = { version = "1.0", features = ["derive"] } test-context = "0.1" thiserror = { version = "1.0" } +token-bindings = "0.11.0" wynd-utils = "0.4" # One commit ahead of version 0.3.0. Allows initialization with an # 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-denom = { path = "./packages/cw-denom", version = "2.4.1" } cw-hooks = { path = "./packages/cw-hooks", version = "2.4.1" } diff --git a/contracts/external/cw-abc/Cargo.toml b/contracts/external/cw-abc/Cargo.toml index 9a8766113..643578f83 100644 --- a/contracts/external/cw-abc/Cargo.toml +++ b/contracts/external/cw-abc/Cargo.toml @@ -35,18 +35,18 @@ rust_decimal = "1.14.3" integer-sqrt = "0.1.5" integer-cbrt = "0.1.2" getrandom = { version = "0.2", features = ["js"] } -# TODO publish this and move to workspace -token-bindings = { git = "https://github.com/CosmosContracts/token-bindings", rev = "1412b94" } +token-bindings = { workspace = true } cw-ownable = { workspace = true } cw-paginate-storage = { workspace = true } # cw-orch = { version = "0.13.3", optional = true } [dev-dependencies] -# TODO move to workspace +# TODO move to workspace? speculoos = "0.11.0" anyhow = { workspace = true } cw-multi-test = { workspace = true } -osmosis-test-tube = "16.0.0" +dao-testing = { workspace = true } +osmosis-test-tube = { workspace = true } [profile.release] rpath = false diff --git a/contracts/external/cw-abc/src/commands.rs b/contracts/external/cw-abc/src/commands.rs index e4a9535cc..5963d8f91 100644 --- a/contracts/external/cw-abc/src/commands.rs +++ b/contracts/external/cw-abc/src/commands.rs @@ -8,7 +8,7 @@ use cosmwasm_std::{ use cw_utils::must_pay; use std::collections::HashSet; use std::ops::Deref; -use token_bindings::{TokenFactoryMsg, TokenFactoryQuery, TokenMsg}; +use token_bindings::{TokenFactoryMsg, TokenFactoryQuery}; use crate::state::{ CURVE_STATE, DONATIONS, HATCHERS, HATCHER_ALLOWLIST, PHASE, PHASE_CONFIG, SUPPLY_DENOM, @@ -76,10 +76,14 @@ pub fn execute_buy( } /// Build a message to mint the supply token to the sender -fn mint_supply_msg(storage: &dyn Storage, minted: Uint128, minter: &Addr) -> CwAbcResult { +fn mint_supply_msg( + storage: &dyn Storage, + minted: Uint128, + minter: &Addr, +) -> CwAbcResult { let denom = SUPPLY_DENOM.load(storage)?; // mint supply token - Ok(TokenMsg::mint_contract_tokens( + Ok(TokenFactoryMsg::mint_contract_tokens( denom, minted, minter.to_string(), @@ -125,7 +129,8 @@ pub fn execute_sell( let supply_denom = SUPPLY_DENOM.load(deps.storage)?; let burn_amount = must_pay(&info, &supply_denom)?; // Burn the sent supply tokens - let burn_msg = TokenMsg::burn_contract_tokens(supply_denom, burn_amount, burner.to_string()); + let burn_msg = + TokenFactoryMsg::burn_contract_tokens(supply_denom, burn_amount, burner.to_string()); let taxed_amount = calculate_exit_tax(deps.storage, burn_amount)?; diff --git a/contracts/external/cw-abc/src/contract.rs b/contracts/external/cw-abc/src/contract.rs index f21a9cd33..9d13ab0e5 100644 --- a/contracts/external/cw-abc/src/contract.rs +++ b/contracts/external/cw-abc/src/contract.rs @@ -4,7 +4,7 @@ use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, use cw2::set_contract_version; use std::collections::HashSet; -use token_bindings::{TokenFactoryMsg, TokenFactoryQuery, TokenMsg}; +use token_bindings::{TokenFactoryMsg, TokenFactoryQuery}; use crate::abc::{CommonsPhase, CurveFn}; use crate::curves::DecimalPlaces; @@ -51,13 +51,27 @@ pub fn instantiate( phase_config.validate()?; + // TODO utilize cw-tokenfactory-issuer? // Create supply denom with metadata - let create_supply_denom_msg = TokenMsg::CreateDenom { + let create_supply_denom_msg = TokenFactoryMsg::CreateDenom { subdenom: supply.subdenom.clone(), metadata: Some(supply.metadata), }; - // TODO validate denom? + // Tnstantiate cw-token-factory-issuer contract + // DAO (sender) is set as contract admin + // let issuer_instantiate_msg = SubMsg::reply_always( + // WasmMsg::Instantiate { + // admin: Some(info.sender.to_string()), + // code_id: msg.token_issuer_code_id, + // msg: to_binary(&IssuerInstantiateMsg::NewToken { + // subdenom: supply.subdenom.clone(), + // })?, + // funds: info.funds, + // label: "cw-tokenfactory-issuer".to_string(), + // }, + // INSTANTIATE_TOKEN_FACTORY_ISSUER_REPLY_ID, + // ); // Save the denom SUPPLY_DENOM.save( @@ -173,6 +187,7 @@ pub fn do_query( to_binary(&queries::query_hatchers(deps, start_after, limit)?) } QueryMsg::Ownership {} => to_binary(&cw_ownable::get_ownership(deps.storage)?), + // TODO get token contract // QueryMsg::GetDenom { // creator_address, // subdenom, @@ -221,217 +236,217 @@ pub fn do_query( // Result::Ok(()) // } -// this is poor man's "skip" flag -#[cfg(test)] -pub(crate) mod tests { - use super::*; - use crate::abc::CurveType; - use crate::queries::query_curve_info; - use cosmwasm_std::{ - testing::{mock_env, mock_info}, - CosmosMsg, Decimal, Uint128, - }; - use speculoos::prelude::*; - - use crate::testing::*; - - // fn get_balance>(deps: Deps, addr: U) -> Uint128 { - // query_balance(deps, addr.into()).unwrap().balance - // } - - // fn setup_test(deps: DepsMut, decimals: u8, reserve_decimals: u8, curve_type: CurveType) { - // // this matches `linear_curve` test case from curves.rs - // let creator = String::from(CREATOR); - // let msg = default_instantiate(decimals, reserve_decimals, curve_type); - // let info = mock_info(&creator, &[]); - - // // make sure we can instantiate with this - // let res = instantiate(deps, mock_env(), info, msg).unwrap(); - // assert_eq!(0, res.messages.len()); - // } - - /// Mock token factory querier dependencies - - #[test] - fn proper_instantiation() -> CwAbcResult<()> { - let mut deps = mock_tf_dependencies(); - - // this matches `linear_curve` test case from curves.rs - let creator = String::from("creator"); - let curve_type = CurveType::SquareRoot { - slope: Uint128::new(1), - scale: 1, - }; - let msg = default_instantiate_msg(2, 8, curve_type.clone()); - let info = mock_info(&creator, &[]); - - // make sure we can instantiate with this - let res = instantiate(deps.as_mut(), mock_env(), info, msg)?; - assert_that!(res.messages.len()).is_equal_to(1); - let submsg = res.messages.get(0).unwrap(); - assert_that!(submsg.msg).is_equal_to(CosmosMsg::Custom(TokenFactoryMsg::Token( - TokenMsg::CreateDenom { - subdenom: TEST_SUPPLY_DENOM.to_string(), - metadata: Some(default_supply_metadata()), - }, - ))); - - // TODO! - // // token info is proper - // let token = query_token_info(deps.as_ref()).unwrap(); - // assert_that!(&token.name, &msg.name); - // assert_that!(&token.symbol, &msg.symbol); - // assert_that!(token.decimals, 2); - // assert_that!(token.total_supply, Uint128::zero()); - - // curve state is sensible - let state = query_curve_info(deps.as_ref(), curve_type.to_curve_fn())?; - assert_that!(state.reserve).is_equal_to(Uint128::zero()); - assert_that!(state.supply).is_equal_to(Uint128::zero()); - assert_that!(state.reserve_denom.as_str()).is_equal_to(TEST_RESERVE_DENOM); - // spot price 0 as supply is 0 - assert_that!(state.spot_price).is_equal_to(Decimal::zero()); - - // curve type is stored properly - let curve = CURVE_TYPE.load(&deps.storage).unwrap(); - assert_eq!(curve_type, curve); - - // no balance - // assert_eq!(get_balance(deps.as_ref(), &creator), Uint128::zero()); - - Ok(()) - } - - // #[test] - // fn buy_issues_tokens() { - // let mut deps = mock_dependencies(); - // let curve_type = CurveType::Linear { - // slope: Uint128::new(1), - // scale: 1, - // }; - // setup_test(deps.as_mut(), 2, 8, curve_type.clone()); - - // // succeeds with proper token (5 BTC = 5*10^8 satoshi) - // let info = mock_info(INVESTOR, &coins(500_000_000, DENOM)); - // let buy = ExecuteMsg::Buy {}; - // execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap(); - - // // bob got 1000 EPOXY (10.00) - // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); - // assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::zero()); - - // // send them all to buyer - // let info = mock_info(INVESTOR, &[]); - // let send = ExecuteMsg::Transfer { - // recipient: BUYER.into(), - // amount: Uint128::new(1000), - // }; - // execute(deps.as_mut(), mock_env(), info, send).unwrap(); - - // // ensure balances updated - // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::zero()); - // assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::new(1000)); - - // // second stake needs more to get next 1000 EPOXY - // let info = mock_info(INVESTOR, &coins(1_500_000_000, DENOM)); - // execute(deps.as_mut(), mock_env(), info, buy).unwrap(); - - // // ensure balances updated - // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); - // assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::new(1000)); - - // // check curve info updated - // let curve = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); - // assert_eq!(curve.reserve, Uint128::new(2_000_000_000)); - // assert_eq!(curve.supply, Uint128::new(2000)); - // assert_eq!(curve.spot_price, Decimal::percent(200)); - - // // check token info updated - // let token = query_token_info(deps.as_ref()).unwrap(); - // assert_eq!(token.decimals, 2); - // assert_eq!(token.total_supply, Uint128::new(2000)); - // } - - // #[test] - // fn bonding_fails_with_wrong_denom() { - // let mut deps = mock_dependencies(); - // let curve_type = CurveType::Linear { - // slope: Uint128::new(1), - // scale: 1, - // }; - // setup_test(deps.as_mut(), 2, 8, curve_type); - - // // fails when no tokens sent - // let info = mock_info(INVESTOR, &[]); - // let buy = ExecuteMsg::Buy {}; - // let err = execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err(); - // assert_eq!(err, PaymentError::NoFunds {}.into()); - - // // fails when wrong tokens sent - // let info = mock_info(INVESTOR, &coins(1234567, "wei")); - // let err = execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err(); - // assert_eq!(err, PaymentError::MissingDenom(DENOM.into()).into()); - - // // fails when too many tokens sent - // let info = mock_info(INVESTOR, &[coin(3400022, DENOM), coin(1234567, "wei")]); - // let err = execute(deps.as_mut(), mock_env(), info, buy).unwrap_err(); - // assert_eq!(err, PaymentError::MultipleDenoms {}.into()); - // } - - // #[test] - // fn burning_sends_reserve() { - // let mut deps = mock_dependencies(); - // let curve_type = CurveType::Linear { - // slope: Uint128::new(1), - // scale: 1, - // }; - // setup_test(deps.as_mut(), 2, 8, curve_type.clone()); - - // // succeeds with proper token (20 BTC = 20*10^8 satoshi) - // let info = mock_info(INVESTOR, &coins(2_000_000_000, DENOM)); - // let buy = ExecuteMsg::Buy {}; - // execute(deps.as_mut(), mock_env(), info, buy).unwrap(); - - // // bob got 2000 EPOXY (20.00) - // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(2000)); - - // // cannot burn too much - // let info = mock_info(INVESTOR, &[]); - // let burn = ExecuteMsg::Burn { - // amount: Uint128::new(3000), - // }; - // let err = execute(deps.as_mut(), mock_env(), info, burn).unwrap_err(); - // // TODO check error - - // // burn 1000 EPOXY to get back 15BTC (*10^8) - // let info = mock_info(INVESTOR, &[]); - // let burn = ExecuteMsg::Burn { - // amount: Uint128::new(1000), - // }; - // let res = execute(deps.as_mut(), mock_env(), info, burn).unwrap(); - - // // balance is lower - // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); - - // // ensure we got our money back - // assert_eq!(1, res.messages.len()); - // assert_eq!( - // &res.messages[0], - // &SubMsg::new(BankMsg::Send { - // to_address: INVESTOR.into(), - // amount: coins(1_500_000_000, DENOM), - // }) - // ); - - // // check curve info updated - // let curve = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); - // assert_eq!(curve.reserve, Uint128::new(500_000_000)); - // assert_eq!(curve.supply, Uint128::new(1000)); - // assert_eq!(curve.spot_price, Decimal::percent(100)); - - // // check token info updated - // let token = query_token_info(deps.as_ref()).unwrap(); - // assert_eq!(token.decimals, 2); - // assert_eq!(token.total_supply, Uint128::new(1000)); - // } -} +// // this is poor man's "skip" flag +// #[cfg(test)] +// pub(crate) mod tests { +// use super::*; +// use crate::abc::CurveType; +// use crate::queries::query_curve_info; +// use cosmwasm_std::{ +// testing::{mock_env, mock_info}, +// CosmosMsg, Decimal, Uint128, +// }; +// use speculoos::prelude::*; + +// use crate::testing::*; + +// // fn get_balance>(deps: Deps, addr: U) -> Uint128 { +// // query_balance(deps, addr.into()).unwrap().balance +// // } + +// // fn setup_test(deps: DepsMut, decimals: u8, reserve_decimals: u8, curve_type: CurveType) { +// // // this matches `linear_curve` test case from curves.rs +// // let creator = String::from(CREATOR); +// // let msg = default_instantiate(decimals, reserve_decimals, curve_type); +// // let info = mock_info(&creator, &[]); + +// // // make sure we can instantiate with this +// // let res = instantiate(deps, mock_env(), info, msg).unwrap(); +// // assert_eq!(0, res.messages.len()); +// // } + +// /// Mock token factory querier dependencies + +// // #[test] +// // fn proper_instantiation() -> CwAbcResult<()> { +// // let mut deps = mock_tf_dependencies(); + +// // // this matches `linear_curve` test case from curves.rs +// // let creator = String::from("creator"); +// // let curve_type = CurveType::SquareRoot { +// // slope: Uint128::new(1), +// // scale: 1, +// // }; +// // let msg = default_instantiate_msg(2, 8, curve_type.clone()); +// // let info = mock_info(&creator, &[]); + +// // // make sure we can instantiate with this +// // let res = instantiate(deps.as_mut(), mock_env(), info, msg)?; +// // assert_that!(res.messages.len()).is_equal_to(1); +// // let submsg = res.messages.get(0).unwrap(); +// // assert_that!(submsg.msg).is_equal_to(CosmosMsg::Custom(TokenFactoryMsg::Token( +// // TokenMsg::CreateDenom { +// // subdenom: TEST_SUPPLY_DENOM.to_string(), +// // metadata: Some(default_supply_metadata()), +// // }, +// // ))); + +// // // TODO! +// // // // token info is proper +// // // let token = query_token_info(deps.as_ref()).unwrap(); +// // // assert_that!(&token.name, &msg.name); +// // // assert_that!(&token.symbol, &msg.symbol); +// // // assert_that!(token.decimals, 2); +// // // assert_that!(token.total_supply, Uint128::zero()); + +// // // curve state is sensible +// // let state = query_curve_info(deps.as_ref(), curve_type.to_curve_fn())?; +// // assert_that!(state.reserve).is_equal_to(Uint128::zero()); +// // assert_that!(state.supply).is_equal_to(Uint128::zero()); +// // assert_that!(state.reserve_denom.as_str()).is_equal_to(TEST_RESERVE_DENOM); +// // // spot price 0 as supply is 0 +// // assert_that!(state.spot_price).is_equal_to(Decimal::zero()); + +// // // curve type is stored properly +// // let curve = CURVE_TYPE.load(&deps.storage).unwrap(); +// // assert_eq!(curve_type, curve); + +// // // no balance +// // // assert_eq!(get_balance(deps.as_ref(), &creator), Uint128::zero()); + +// // Ok(()) +// // } + +// // #[test] +// // fn buy_issues_tokens() { +// // let mut deps = mock_dependencies(); +// // let curve_type = CurveType::Linear { +// // slope: Uint128::new(1), +// // scale: 1, +// // }; +// // setup_test(deps.as_mut(), 2, 8, curve_type.clone()); + +// // // succeeds with proper token (5 BTC = 5*10^8 satoshi) +// // let info = mock_info(INVESTOR, &coins(500_000_000, DENOM)); +// // let buy = ExecuteMsg::Buy {}; +// // execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap(); + +// // // bob got 1000 EPOXY (10.00) +// // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); +// // assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::zero()); + +// // // send them all to buyer +// // let info = mock_info(INVESTOR, &[]); +// // let send = ExecuteMsg::Transfer { +// // recipient: BUYER.into(), +// // amount: Uint128::new(1000), +// // }; +// // execute(deps.as_mut(), mock_env(), info, send).unwrap(); + +// // // ensure balances updated +// // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::zero()); +// // assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::new(1000)); + +// // // second stake needs more to get next 1000 EPOXY +// // let info = mock_info(INVESTOR, &coins(1_500_000_000, DENOM)); +// // execute(deps.as_mut(), mock_env(), info, buy).unwrap(); + +// // // ensure balances updated +// // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); +// // assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::new(1000)); + +// // // check curve info updated +// // let curve = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); +// // assert_eq!(curve.reserve, Uint128::new(2_000_000_000)); +// // assert_eq!(curve.supply, Uint128::new(2000)); +// // assert_eq!(curve.spot_price, Decimal::percent(200)); + +// // // check token info updated +// // let token = query_token_info(deps.as_ref()).unwrap(); +// // assert_eq!(token.decimals, 2); +// // assert_eq!(token.total_supply, Uint128::new(2000)); +// // } + +// // #[test] +// // fn bonding_fails_with_wrong_denom() { +// // let mut deps = mock_dependencies(); +// // let curve_type = CurveType::Linear { +// // slope: Uint128::new(1), +// // scale: 1, +// // }; +// // setup_test(deps.as_mut(), 2, 8, curve_type); + +// // // fails when no tokens sent +// // let info = mock_info(INVESTOR, &[]); +// // let buy = ExecuteMsg::Buy {}; +// // let err = execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err(); +// // assert_eq!(err, PaymentError::NoFunds {}.into()); + +// // // fails when wrong tokens sent +// // let info = mock_info(INVESTOR, &coins(1234567, "wei")); +// // let err = execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err(); +// // assert_eq!(err, PaymentError::MissingDenom(DENOM.into()).into()); + +// // // fails when too many tokens sent +// // let info = mock_info(INVESTOR, &[coin(3400022, DENOM), coin(1234567, "wei")]); +// // let err = execute(deps.as_mut(), mock_env(), info, buy).unwrap_err(); +// // assert_eq!(err, PaymentError::MultipleDenoms {}.into()); +// // } + +// // #[test] +// // fn burning_sends_reserve() { +// // let mut deps = mock_dependencies(); +// // let curve_type = CurveType::Linear { +// // slope: Uint128::new(1), +// // scale: 1, +// // }; +// // setup_test(deps.as_mut(), 2, 8, curve_type.clone()); + +// // // succeeds with proper token (20 BTC = 20*10^8 satoshi) +// // let info = mock_info(INVESTOR, &coins(2_000_000_000, DENOM)); +// // let buy = ExecuteMsg::Buy {}; +// // execute(deps.as_mut(), mock_env(), info, buy).unwrap(); + +// // // bob got 2000 EPOXY (20.00) +// // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(2000)); + +// // // cannot burn too much +// // let info = mock_info(INVESTOR, &[]); +// // let burn = ExecuteMsg::Burn { +// // amount: Uint128::new(3000), +// // }; +// // let err = execute(deps.as_mut(), mock_env(), info, burn).unwrap_err(); +// // // TODO check error + +// // // burn 1000 EPOXY to get back 15BTC (*10^8) +// // let info = mock_info(INVESTOR, &[]); +// // let burn = ExecuteMsg::Burn { +// // amount: Uint128::new(1000), +// // }; +// // let res = execute(deps.as_mut(), mock_env(), info, burn).unwrap(); + +// // // balance is lower +// // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); + +// // // ensure we got our money back +// // assert_eq!(1, res.messages.len()); +// // assert_eq!( +// // &res.messages[0], +// // &SubMsg::new(BankMsg::Send { +// // to_address: INVESTOR.into(), +// // amount: coins(1_500_000_000, DENOM), +// // }) +// // ); + +// // // check curve info updated +// // let curve = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); +// // assert_eq!(curve.reserve, Uint128::new(500_000_000)); +// // assert_eq!(curve.supply, Uint128::new(1000)); +// // assert_eq!(curve.spot_price, Decimal::percent(100)); + +// // // check token info updated +// // let token = query_token_info(deps.as_ref()).unwrap(); +// // assert_eq!(token.decimals, 2); +// // assert_eq!(token.total_supply, Uint128::new(1000)); +// // } +// } diff --git a/contracts/external/cw-abc/src/lib.rs b/contracts/external/cw-abc/src/lib.rs index 3191289c5..1b2e1a4b9 100644 --- a/contracts/external/cw-abc/src/lib.rs +++ b/contracts/external/cw-abc/src/lib.rs @@ -8,11 +8,17 @@ mod integration; pub mod msg; mod queries; pub mod state; + +// Integrationg 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 testtube; pub use crate::error::ContractError; +// TODO do we still want these? #[cfg(test)] pub(crate) mod testing { use crate::abc::{ 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..ccfd01e81 --- /dev/null +++ b/contracts/external/cw-abc/src/test_tube/integration_tests.rs @@ -0,0 +1,17 @@ +use crate::{ + abc::{ + ClosedConfig, CommonsPhaseConfig, HatchConfig, MinMax, OpenConfig, ReserveToken, + SupplyToken, + }, + msg::{CurveInfoResponse, ExecuteMsg, InstantiateMsg, QueryMsg}, +}; +use cosmwasm_std::{Coin, Decimal, Uint128}; +use osmosis_test_tube::{Account, Module, OsmosisTestApp, Wasm}; +use token_bindings::Metadata; + +#[test] +fn test_happy_path() { + let app = OsmosisTestApp::new(); + + // TODO +} 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..1ca46c6ff --- /dev/null +++ b/contracts/external/cw-abc/src/test_tube/mod.rs @@ -0,0 +1 @@ +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..7201f046b --- /dev/null +++ b/contracts/external/cw-abc/src/test_tube/test_env.rs @@ -0,0 +1,195 @@ +// 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, InitialBalance, InstantiateMsg, NewTokenInfo, QueryMsg, TokenInfo}, + ContractError, +}; + +use cosmwasm_std::{Coin, Uint128}; +use cw_tokenfactory_issuer::msg::{DenomResponse, DenomUnit}; +use cw_utils::Duration; +use dao_interface::voting::{IsActiveResponse, VotingPowerAtHeightResponse}; +use dao_testing::test_tube::{cw_abc::CwAbc, cw_tokenfactory_issuer::TokenfactoryIssuer}; +use dao_voting::threshold::ActiveThreshold; +use osmosis_std::types::{ + cosmos::bank::v1beta1::QueryAllBalancesRequest, cosmwasm::wasm::v1::MsgExecuteContractResponse, +}; +use osmosis_test_tube::{ + 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"; + +pub struct TestEnv<'a> { + pub app: &'a OsmosisTestApp, + pub abc: TfDaoVotingContract<'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 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 { + pub accounts: Vec, + pub instantiate_msg: Option, +} + +impl TestEnvBuilder { + pub fn new() -> Self { + Self { + accounts: vec![], + instantiate_msg: None, + } + } + + pub fn default_setup(self, app: &'_ OsmosisTestApp) -> TestEnv<'_> { + let accounts = app + .init_accounts(&[Coin::new(1000000000000000u128, "uosmo")], 10) + .unwrap(); + + let initial_balances: Vec = accounts + .iter() + .map(|acc| InitialBalance { + address: acc.address(), + amount: Uint128::new(100), + }) + .collect(); + + let issuer_id = TokenfactoryIssuer::upload(app, &accounts[0]).unwrap(); + + let abc = CwAbc::deploy(app, &InstantiateMsg {}, &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 build(self, app: &'_ OsmosisTestApp) -> TestEnv<'_> { + let accounts = self.accounts; + + let abc = CwAbc::deploy( + app, + self.instantiate_msg + .as_ref() + .expect("instantiate msg not set"), + &accounts[0], + ) + .unwrap(); + + let issuer_addr = CwAbc::query(&abc, &QueryMsg::TokenContract {}).unwrap(); + + let tf_issuer = TokenfactoryIssuer::new_with_values( + app, + self.instantiate_msg + .expect("instantiate msg not set") + .token_issuer_code_id, + issuer_addr, + ) + .unwrap(); + + TestEnv { + app, + abc, + tf_issuer, + accounts, + } + } + + 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 + } +} + +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/external/cw-abc/src/testtube.rs b/contracts/external/cw-abc/src/testtube.rs deleted file mode 100644 index d4a32c5ac..000000000 --- a/contracts/external/cw-abc/src/testtube.rs +++ /dev/null @@ -1,122 +0,0 @@ -use crate::{ - abc::{ - ClosedConfig, CommonsPhaseConfig, HatchConfig, MinMax, OpenConfig, ReserveToken, - SupplyToken, - }, - msg::{CurveInfoResponse, ExecuteMsg, InstantiateMsg, QueryMsg}, -}; -use cosmwasm_std::{Coin, Decimal, Uint128}; -use osmosis_test_tube::{Account, Module, OsmosisTestApp, Wasm}; -use token_bindings::Metadata; - -#[test] -fn test_tf() { - // Atempt to write tests with test-tube: https://github.com/osmosis-labs/test-tube - let app = OsmosisTestApp::new(); - let accs = app - .init_accounts( - &[ - Coin::new(1_000_000_000_000, "uatom"), - Coin::new(1_000_000_000_000, "uosmo"), - ], - 2, - ) - .unwrap(); - let admin = &accs[0]; - let new_admin = &accs[1]; - - let wasm = Wasm::new(&app); - - let wasm_byte_code = std::fs::read("../../../artifacts/cw_abc-aarch64.wasm").unwrap(); - let code_id = wasm - .store_code(&wasm_byte_code, None, admin) - .unwrap() - .data - .code_id; - - // instantiate contract - let contract_addr = wasm - .instantiate( - code_id, - &InstantiateMsg { - supply: SupplyToken { - subdenom: "subdenom".to_string(), - metadata: Metadata { - description: None, - denom_units: vec![], - base: None, - display: None, - name: None, - symbol: None, - }, - decimals: 6, - }, - reserve: ReserveToken { - denom: "ujuno".to_string(), - decimals: 6, - }, - curve_type: crate::abc::CurveType::Linear { - slope: Uint128::new(1), - scale: 2, - }, - phase_config: CommonsPhaseConfig { - hatch: HatchConfig { - initial_raise: MinMax { - min: Uint128::new(100), - max: Uint128::new(1000), - }, - initial_price: Uint128::new(1), - initial_allocation_ratio: Decimal::percent(10), - exit_tax: Decimal::percent(10), - }, - open: OpenConfig { - allocation_percentage: Decimal::percent(10), - exit_tax: Decimal::percent(10), - }, - closed: ClosedConfig {}, - }, - hatcher_allowlist: None, - }, - None, // contract admin used for migration, not the same as cw1_whitelist admin - Some("cw-bounties"), // contract label - &[], // funds - admin, // signer - ) - .unwrap() - .data - .address; - println!("{:?}", contract_addr); - - let curve_info = wasm - .query::(&contract_addr, &QueryMsg::CurveInfo {}) - .unwrap(); - println!("{:?}", curve_info); - - // let admin_list = wasm - // .query::(&contract_addr, &QueryMsg::AdminList {}) - // .unwrap(); - - // assert_eq!(admin_list.admins, init_admins); - // assert!(admin_list.mutable); - - // // ============= NEW CODE ================ - - // // update admin list and rechec the state - // let new_admins = vec![new_admin.address()]; - // wasm.execute::( - // &contract_addr, - // &ExecuteMsg::UpdateAdmins { - // admins: new_admins.clone(), - // }, - // &[], - // admin, - // ) - // .unwrap(); - - // let admin_list = wasm - // .query::(&contract_addr, &QueryMsg::AdminList {}) - // .unwrap(); - - // assert_eq!(admin_list.admins, new_admins); - // assert!(admin_list.mutable); -} diff --git a/packages/dao-testing/Cargo.toml b/packages/dao-testing/Cargo.toml index 51d0685b4..d128f6226 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 } 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..a10f80bfa --- /dev/null +++ b/packages/dao-testing/src/test_tube/cw_abc.rs @@ -0,0 +1,155 @@ +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) + } + + // 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_tokenfactory_issuer.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_tokenfactory_issuer-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..016c51488 100644 --- a/packages/dao-testing/src/test_tube/mod.rs +++ b/packages/dao-testing/src/test_tube/mod.rs @@ -5,6 +5,9 @@ // Integrationg 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; From df6ebad0379a2252ecc8e82855db90e5c5a8dbdb Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Wed, 30 Aug 2023 15:30:44 -0700 Subject: [PATCH 09/56] Refactor cw-abc contract to work with cw-tokenfactory-issuer --- Cargo.lock | 1 + contracts/external/cw-abc/Cargo.toml | 1 + contracts/external/cw-abc/src/abc.rs | 2 +- contracts/external/cw-abc/src/contract.rs | 213 +++++++++++++++------- contracts/external/cw-abc/src/error.rs | 10 +- contracts/external/cw-abc/src/msg.rs | 36 ++-- contracts/external/cw-abc/src/state.rs | 8 +- 7 files changed, 186 insertions(+), 85 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef6b222c1..814550dce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -734,6 +734,7 @@ dependencies = [ "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-testing", diff --git a/contracts/external/cw-abc/Cargo.toml b/contracts/external/cw-abc/Cargo.toml index 643578f83..0e0ec211f 100644 --- a/contracts/external/cw-abc/Cargo.toml +++ b/contracts/external/cw-abc/Cargo.toml @@ -38,6 +38,7 @@ getrandom = { version = "0.2", features = ["js"] } token-bindings = { workspace = true } cw-ownable = { workspace = true } cw-paginate-storage = { workspace = true } +cw-tokenfactory-issuer = { workspace = true, features = ["library"] } # cw-orch = { version = "0.13.3", optional = true } [dev-dependencies] diff --git a/contracts/external/cw-abc/src/abc.rs b/contracts/external/cw-abc/src/abc.rs index eec4a40bb..eef4c7db3 100644 --- a/contracts/external/cw-abc/src/abc.rs +++ b/contracts/external/cw-abc/src/abc.rs @@ -10,7 +10,7 @@ pub struct SupplyToken { /// The denom to create for the supply token pub subdenom: String, /// Metadata for the supply token to create - pub metadata: Metadata, + 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, diff --git a/contracts/external/cw-abc/src/contract.rs b/contracts/external/cw-abc/src/contract.rs index 9d13ab0e5..8a2023d3d 100644 --- a/contracts/external/cw-abc/src/contract.rs +++ b/contracts/external/cw-abc/src/contract.rs @@ -1,25 +1,33 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; +use cosmwasm_std::{ + to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, SubMsg, + Uint128, WasmMsg, +}; use cw2::set_contract_version; +use cw_tokenfactory_issuer::msg::{ + ExecuteMsg as IssuerExecuteMsg, InstantiateMsg as IssuerInstantiateMsg, +}; +use cw_utils::{nonpayable, parse_reply_instantiate_data}; use std::collections::HashSet; - use token_bindings::{TokenFactoryMsg, TokenFactoryQuery}; use crate::abc::{CommonsPhase, CurveFn}; use crate::curves::DecimalPlaces; use crate::error::ContractError; -use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, UpdatePhaseConfigMsg}; +use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, UpdatePhaseConfigMsg}; use crate::state::{ CurveState, CURVE_STATE, CURVE_TYPE, HATCHER_ALLOWLIST, PHASE, PHASE_CONFIG, SUPPLY_DENOM, + TOKEN_INSTANTIATION_INFO, TOKEN_ISSUER_CONTRACT, }; use crate::{commands, queries}; -use cw_utils::nonpayable; // 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; + // By default, the prefix for token factory tokens is "factory" const DENOM_PREFIX: &str = "factory"; @@ -41,6 +49,7 @@ pub fn instantiate( curve_type, phase_config, hatcher_allowlist, + token_issuer_code_id, } = msg; if supply.subdenom.is_empty() { @@ -51,27 +60,23 @@ pub fn instantiate( phase_config.validate()?; - // TODO utilize cw-tokenfactory-issuer? - // Create supply denom with metadata - let create_supply_denom_msg = TokenFactoryMsg::CreateDenom { - subdenom: supply.subdenom.clone(), - metadata: Some(supply.metadata), - }; - // Tnstantiate cw-token-factory-issuer contract // DAO (sender) is set as contract admin - // let issuer_instantiate_msg = SubMsg::reply_always( - // WasmMsg::Instantiate { - // admin: Some(info.sender.to_string()), - // code_id: msg.token_issuer_code_id, - // msg: to_binary(&IssuerInstantiateMsg::NewToken { - // subdenom: supply.subdenom.clone(), - // })?, - // funds: info.funds, - // label: "cw-tokenfactory-issuer".to_string(), - // }, - // INSTANTIATE_TOKEN_FACTORY_ISSUER_REPLY_ID, - // ); + let issuer_instantiate_msg = SubMsg::reply_always( + WasmMsg::Instantiate { + admin: Some(info.sender.to_string()), + code_id: token_issuer_code_id, + msg: to_binary(&IssuerInstantiateMsg::NewToken { + subdenom: supply.subdenom.clone(), + })?, + funds: info.funds, + label: "cw-tokenfactory-issuer".to_string(), + }, + INSTANTIATE_TOKEN_FACTORY_ISSUER_REPLY_ID, + ); + + // Save new token info for use in reply + TOKEN_INSTANTIATION_INFO.save(deps.storage, &supply)?; // Save the denom SUPPLY_DENOM.save( @@ -105,7 +110,7 @@ pub fn instantiate( cw_ownable::initialize_owner(deps.storage, deps.api, Some(info.sender.as_str()))?; - Ok(Response::default().add_message(create_supply_denom_msg)) + Ok(Response::default().add_submessage(issuer_instantiate_msg)) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -195,48 +200,126 @@ pub fn do_query( } } -// fn validate_denom( -// deps: DepsMut, -// denom: String, -// ) -> Result<(), TokenFactoryError> { -// let denom_to_split = denom.clone(); -// let tokenfactory_denom_parts: Vec<&str> = denom_to_split.split('/').collect(); - -// if tokenfactory_denom_parts.len() != 3 { -// return Result::Err(TokenFactoryError::InvalidDenom { -// denom, -// message: std::format!( -// "denom must have 3 parts separated by /, had {}", -// tokenfactory_denom_parts.len() -// ), -// }); -// } - -// let prefix = tokenfactory_denom_parts[0]; -// let creator_address = tokenfactory_denom_parts[1]; -// let subdenom = tokenfactory_denom_parts[2]; - -// if !prefix.eq_ignore_ascii_case("factory") { -// return Result::Err(TokenFactoryError::InvalidDenom { -// denom, -// message: std::format!("prefix must be 'factory', was {}", prefix), -// }); -// } - -// // Validate denom by attempting to query for full denom -// let response = TokenQuerier::new(&deps.querier) -// .full_denom(String::from(creator_address), String::from(subdenom)); -// if response.is_err() { -// return Result::Err(TokenFactoryError::InvalidDenom { -// denom, -// message: response.err().unwrap().to_string(), -// }); -// } - -// Result::Ok(()) -// } +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate( + deps: DepsMut, + _env: Env, + _msg: MigrateMsg, +) -> Result, ContractError> { + // 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, ContractError> { + 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 info for new token and remove temporary data + let token_info = TOKEN_INSTANTIATION_INFO.load(deps.storage)?; + TOKEN_INSTANTIATION_INFO.remove(deps.storage); + + // // Load the DAO address + // let dao = DAO.load(deps.storage)?; + + // Format the denom and save it + let denom = format!("factory/{}/{}", &issuer_addr, token_info.subdenom); + + SUPPLY_DENOM.save(deps.storage, &denom)?; + + // // Check supply is greater than zero, iterate through initial + // // balances and sum them, add DAO balance as well. + // let initial_supply = token + // .initial_balances + // .iter() + // .fold(Uint128::zero(), |previous, new_balance| { + // previous + new_balance.amount + // }); + // let total_supply = initial_supply + token.initial_dao_balance.unwrap_or_default(); + + // // Cannot instantiate with no initial token owners because it would + // // immediately lock the DAO. + // if initial_supply.is_zero() { + // return Err(ContractError::InitialBalancesError {}); + // } + + // Msgs to be executed to finalize setup + let mut msgs: Vec = vec![]; + + // Grant an allowance to mint + msgs.push(WasmMsg::Execute { + contract_addr: issuer_addr.clone(), + msg: to_binary(&IssuerExecuteMsg::SetMinterAllowance { + address: env.contract.address.to_string(), + // TODO let this be capped + allowance: Uint128::MAX, + })?, + funds: vec![], + }); + + // TODO fix metadata + // // If metadata, set it by calling the contract + // if let Some(metadata) = token_info.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![token_info.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_binary(&IssuerExecuteMsg::SetDenomMetadata { + // metadata: Metadata { + // description: metadata.description, + // denom_units, + // base: denom.clone(), + // display: metadata.display, + // name: metadata.name, + // symbol: metadata.symbol, + // }, + // })?, + // funds: vec![], + // }); + // } + + // TODO who should own the token contract? + // // Update issuer contract owner to be the DAO + // msgs.push(WasmMsg::Execute { + // contract_addr: issuer_addr.clone(), + // msg: to_binary(&IssuerExecuteMsg::ChangeContractOwner { + // new_owner: dao.to_string(), + // })?, + // 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 }), + } +} -// // this is poor man's "skip" flag // #[cfg(test)] // pub(crate) mod tests { // use super::*; diff --git a/contracts/external/cw-abc/src/error.rs b/contracts/external/cw-abc/src/error.rs index 2e3989515..eae842541 100644 --- a/contracts/external/cw-abc/src/error.rs +++ b/contracts/external/cw-abc/src/error.rs @@ -1,5 +1,5 @@ use cosmwasm_std::StdError; -use cw_utils::PaymentError; +use cw_utils::{ParseReplyError, PaymentError}; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -7,9 +7,12 @@ pub enum ContractError { #[error("{0}")] Std(#[from] StdError), - #[error("{0}")] + #[error(transparent)] Payment(#[from] PaymentError), + #[error(transparent)] + ParseReplyError(#[from] ParseReplyError), + #[error("Invalid subdenom: {subdenom:?}")] InvalidSubdenom { subdenom: String }, @@ -42,4 +45,7 @@ pub enum ContractError { #[error("Invalid phase, expected {expected:?}, actual {actual:?}")] InvalidPhase { expected: String, actual: String }, + + #[error("Got a submessage reply with unknown id: {id}")] + UnknownReplyId { id: u64 }, } diff --git a/contracts/external/cw-abc/src/msg.rs b/contracts/external/cw-abc/src/msg.rs index 89b43239c..69d79d81c 100644 --- a/contracts/external/cw-abc/src/msg.rs +++ b/contracts/external/cw-abc/src/msg.rs @@ -5,19 +5,22 @@ use crate::abc::{CommonsPhase, CommonsPhaseConfig, CurveType, MinMax, ReserveTok #[cw_serde] pub struct InstantiateMsg { - // Supply token information + /// The code id of the cw-tokenfactory-issuer contract + pub token_issuer_code_id: u64, + + /// Supply token information pub supply: SupplyToken, - // Reserve token information + /// Reserve token information pub reserve: ReserveToken, - // Curve type for this contract + /// Curve type for this contract pub curve_type: CurveType, - // Hatch configuration information + /// Hatch configuration information pub phase_config: CommonsPhaseConfig, - // Hatcher allowlist + /// Hatcher allowlist pub hatcher_allowlist: Option>, } @@ -59,6 +62,7 @@ pub enum ExecuteMsg { UpdatePhaseConfig(UpdatePhaseConfigMsg), } +// TODO token contract query #[cw_ownable::cw_ownable_query] #[cw_serde] #[derive(QueryResponses)] @@ -89,44 +93,44 @@ pub enum QueryMsg { #[cw_serde] pub struct CurveInfoResponse { - // how many reserve tokens have been received + /// How many reserve tokens have been received pub reserve: Uint128, - // how many supply tokens have been issued + /// How many supply tokens have been issued pub supply: Uint128, - // the amount of tokens in the funding pool + /// The amount of tokens in the funding pool pub funding: Uint128, - // current spot price of the token + /// Current spot price of the token pub spot_price: Decimal, - // current reserve denom + /// Current reserve denom pub reserve_denom: String, } #[cw_serde] pub struct HatcherAllowlistResponse { - // hatcher allowlist + /// Hatcher allowlist pub allowlist: Option>, } #[cw_serde] pub struct CommonsPhaseConfigResponse { - // the phase configuration + /// The phase configuration pub phase_config: CommonsPhaseConfig, - // current phase + /// Current phase pub phase: CommonsPhase, } #[cw_serde] pub struct DonationsResponse { - // the donators mapped to their donation in the reserve token + /// 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 + /// The hatchers mapped to their contribution in the reserve token pub hatchers: Vec<(Addr, Uint128)>, } #[cw_serde] -pub enum MigrateMsg {} +pub struct MigrateMsg {} diff --git a/contracts/external/cw-abc/src/state.rs b/contracts/external/cw-abc/src/state.rs index cb885cc53..2af899c43 100644 --- a/contracts/external/cw-abc/src/state.rs +++ b/contracts/external/cw-abc/src/state.rs @@ -1,7 +1,7 @@ use cosmwasm_schema::cw_serde; use std::collections::HashSet; -use crate::abc::{CommonsPhase, CommonsPhaseConfig, CurveType}; +use crate::abc::{CommonsPhase, CommonsPhaseConfig, CurveType, SupplyToken}; use cosmwasm_std::{Addr, Uint128}; use cw_storage_plus::{Item, Map}; @@ -60,3 +60,9 @@ 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 token_instantiation_info when creating a new Token Factory denom +pub const TOKEN_INSTANTIATION_INFO: Item = Item::new("token_instantiation_info"); + +/// The address of the cw-tokenfactory-issuer contract +pub const TOKEN_ISSUER_CONTRACT: Item = Item::new("token_issuer_contract"); From f05541b725e3f5302faf5eafee1b6bb8ddd0eb28 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Wed, 30 Aug 2023 15:31:27 -0700 Subject: [PATCH 10/56] Remove old attempt at fixing cw-multi-test --- contracts/external/cw-abc/src/integration.rs | 327 ------------------- contracts/external/cw-abc/src/lib.rs | 2 - 2 files changed, 329 deletions(-) delete mode 100644 contracts/external/cw-abc/src/integration.rs diff --git a/contracts/external/cw-abc/src/integration.rs b/contracts/external/cw-abc/src/integration.rs deleted file mode 100644 index d9c72c985..000000000 --- a/contracts/external/cw-abc/src/integration.rs +++ /dev/null @@ -1,327 +0,0 @@ -use anyhow::Result as AnyResult; -use cosmwasm_schema::{schemars::JsonSchema, serde::de::DeserializeOwned}; -use cosmwasm_std::{ - coin, coins, - testing::{MockApi, MockStorage}, - Addr, Api, Binary, BlockInfo, Coin, CustomQuery, Decimal, Empty, GovMsg, IbcMsg, IbcQuery, - Querier, StdResult, Storage, Uint128, -}; -use cw_multi_test::{ - custom_app, - custom_handler::{CachingCustomHandler, CachingCustomHandlerState}, - next_block, App, AppBuilder, AppResponse, BankKeeper, BankSudo, BasicAppBuilder, Contract, - ContractWrapper, CosmosRouter, DistributionKeeper, Executor, FailingModule, Module, - StakeKeeper, SudoMsg, WasmKeeper, -}; -use token_bindings::{Metadata, TokenFactoryMsg, TokenFactoryQuery, TokenMsg}; - -use crate::{ - abc::{ - ClosedConfig, CommonsPhaseConfig, HatchConfig, MinMax, OpenConfig, ReserveToken, - SupplyToken, - }, - msg::{CurveInfoResponse, InstantiateMsg}, -}; - -pub struct CustomHandler {} - -impl Module for CustomHandler { - type ExecT = TokenFactoryMsg; - type QueryT = TokenFactoryQuery; - type SudoT = Empty; - - fn execute( - &self, - api: &dyn Api, - storage: &mut dyn Storage, - router: &dyn CosmosRouter, - block: &BlockInfo, - _sender: Addr, - msg: Self::ExecT, - ) -> AnyResult - where - ExecC: std::fmt::Debug + Clone + PartialEq + JsonSchema + DeserializeOwned + 'static, - QueryC: CustomQuery + DeserializeOwned + 'static, - { - println!("msg {:?}", msg); - match msg { - TokenFactoryMsg::Token(TokenMsg::MintTokens { - denom, - amount, - mint_to_address, - }) => { - println!("minting tokens"); - - // mint new tokens - let mint = SudoMsg::Bank(BankSudo::Mint { - to_address: mint_to_address, - amount: vec![Coin { - denom: denom.clone(), - amount: amount.clone(), - }], - }); - return Ok(router.sudo(api, storage, block, mint)?); - } - TokenFactoryMsg::Token(TokenMsg::CreateDenom { subdenom, metadata }) => { - println!("creating denom"); - return Ok(AppResponse::default()); - } - _ => unimplemented!(), - }; - } - - fn sudo( - &self, - _api: &dyn Api, - _storage: &mut dyn Storage, - _router: &dyn CosmosRouter, - _block: &BlockInfo, - _msg: Self::SudoT, - ) -> AnyResult - where - ExecC: std::fmt::Debug + Clone + PartialEq + JsonSchema + DeserializeOwned + 'static, - QueryC: CustomQuery + DeserializeOwned + 'static, - { - unimplemented!() - } - - fn query( - &self, - _api: &dyn Api, - _storage: &dyn Storage, - _querier: &dyn Querier, - _block: &BlockInfo, - _request: Self::QueryT, - ) -> AnyResult { - unimplemented!() - } -} - -// impl CustomHandler { -// // this is a custom initialization method -// pub fn set_payout( -// &self, -// storage: &mut dyn Storage, -// lottery: Coin, -// pity: Coin, -// ) -> AnyResult<()> { -// LOTTERY.save(storage, &lottery)?; -// PITY.save(storage, &pity)?; -// Ok(()) -// } -// } - -pub struct Test { - pub app: App< - BankKeeper, - MockApi, - MockStorage, - // CachingCustomHandler, - // FailingModule, - CustomHandler, - WasmKeeper, - StakeKeeper, - DistributionKeeper, - FailingModule, - FailingModule, - >, - pub addr: Addr, - pub owner: Addr, - pub recipient: Addr, - pub custom_handler_state: CachingCustomHandlerState, -} - -impl Test { - pub fn new() -> Self { - let owner = Addr::unchecked("owner"); - let recipient = Addr::unchecked("recipient"); - - let custom_handler = CachingCustomHandler::::new(); - let custom_handler_state = custom_handler.state(); - - // let mut app = AppBuilder::new_custom() - // .with_custom(custom_handler) - // .build(|_, _, _| {}); - - // let mut app = custom_app::(|router, _, storage| { - // router - // .bank - // .init_balance(storage, &owner, coins(10000, "ujuno")) - // .unwrap(); - // }); - - let mut app = BasicAppBuilder::::new_custom() - .with_custom(CustomHandler {}) - .build(|_, _, _| {}); - - app.sudo(cw_multi_test::SudoMsg::Bank(BankSudo::Mint { - to_address: owner.to_string(), - amount: vec![coin(10000, "ujuno"), coin(10000, "uatom")], - })) - .unwrap(); - app.sudo(cw_multi_test::SudoMsg::Bank(BankSudo::Mint { - to_address: recipient.to_string(), - amount: vec![coin(10000, "ujuno"), coin(10000, "uatom")], - })) - .unwrap(); - - let code_id = app.store_code(abc_countract()); - let addr = app - .instantiate_contract( - code_id, - owner.clone(), - &InstantiateMsg { - supply: SupplyToken { - subdenom: "subdenom".to_string(), - metadata: Metadata { - description: None, - denom_units: vec![], - base: None, - display: None, - name: None, - symbol: None, - }, - decimals: 6, - }, - reserve: ReserveToken { - denom: "ujuno".to_string(), - decimals: 6, - }, - curve_type: crate::abc::CurveType::Linear { - slope: Uint128::new(1), - scale: 2, - }, - phase_config: CommonsPhaseConfig { - hatch: HatchConfig { - initial_raise: MinMax { - min: Uint128::new(100), - max: Uint128::new(1000), - }, - initial_price: Uint128::new(1), - initial_allocation_ratio: Decimal::percent(10), - exit_tax: Decimal::percent(10), - }, - open: OpenConfig { - allocation_percentage: Decimal::percent(10), - exit_tax: Decimal::percent(10), - }, - closed: ClosedConfig {}, - }, - hatcher_allowlist: None, - }, - &[], - "cw-bounties", - None, - ) - .unwrap(); - Self { - app, - addr, - owner, - recipient, - custom_handler_state, - } - } - - pub fn buy(&mut self, amount: Vec) -> Result { - let msg = crate::msg::ExecuteMsg::Buy {}; - let res = - self.app - .execute_contract(self.owner.clone(), self.addr.clone(), &msg, &amount)?; - Ok(res) - } - - pub fn burn(&mut self, amount: Vec) -> Result { - let msg = crate::msg::ExecuteMsg::Burn {}; - let res = - self.app - .execute_contract(self.owner.clone(), self.addr.clone(), &msg, &amount)?; - Ok(res) - } - - pub fn query_curve_info(&self) -> StdResult { - let msg = crate::msg::QueryMsg::CurveInfo {}; - self.app.wrap().query_wasm_smart(&self.addr, &msg) - } -} - -fn abc_countract() -> Box> { - let contract = ContractWrapper::new( - crate::contract::execute, - crate::contract::instantiate, - crate::contract::query, - ); - Box::new(contract) -} - -#[test] -pub fn test_happy_path() { - let mut test = Test::new(); - - // Curve has been initialized - let curve_info = test.query_curve_info().unwrap(); - assert_eq!( - curve_info, - CurveInfoResponse { - reserve: Uint128::zero(), - supply: Uint128::zero(), - funding: Uint128::zero(), - spot_price: Decimal::zero(), - reserve_denom: "ujuno".to_string(), - } - ); - - let balance_before = test - .app - .wrap() - .query_balance(test.owner.clone(), "ujuno") - .unwrap(); - - // Buy some coins - test.buy(coins(100, "ujuno")).unwrap(); - - // // Update block - // test.app.update_block(next_block); - - // Curve has been updated - let curve_info = test.query_curve_info().unwrap(); - assert_eq!( - curve_info, - CurveInfoResponse { - reserve: Uint128::new(90), - supply: Uint128::new(134164), - funding: Uint128::new(10), - // TODO investigate why does this take 8 for decimals? - spot_price: Decimal::from_atomics(Uint128::new(134164), 8).unwrap(), - reserve_denom: "ujuno".to_string(), - } - ); - - // Assert balance - let balance_after = test - .app - .wrap() - .query_balance(test.owner.clone(), "ujuno") - .unwrap(); - assert!( - balance_before.amount.u128() == (balance_after.amount.u128() + 100), - "before: {}, after: {}", - balance_before.amount.u128(), - balance_after.amount.u128() - ); - - // TODO get denom - let tf_balance = test - .app - .wrap() - .query_balance(test.addr.clone(), "factory/contract0/subdenom") - .unwrap(); - // TODO how to handle this? - println!("{:?}", tf_balance); - - // // Burn some coins - // test.burn(coins(100, "ujuno")).unwrap(); - - // let curve_info = test.query_curve_info().unwrap(); - // println!("{:?}", curve_info); -} diff --git a/contracts/external/cw-abc/src/lib.rs b/contracts/external/cw-abc/src/lib.rs index 1b2e1a4b9..a7215cd5f 100644 --- a/contracts/external/cw-abc/src/lib.rs +++ b/contracts/external/cw-abc/src/lib.rs @@ -3,8 +3,6 @@ pub(crate) mod commands; pub mod contract; pub mod curves; mod error; -#[cfg(test)] -mod integration; pub mod msg; mod queries; pub mod state; From 988cf5d571a6585d14c2cd24edfdfd4f3fef8e5e Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Wed, 30 Aug 2023 16:05:01 -0700 Subject: [PATCH 11/56] Refactor --- contracts/external/cw-abc/src/commands.rs | 70 +++++++++------ contracts/external/cw-abc/src/contract.rs | 7 +- contracts/external/cw-abc/src/lib.rs | 100 +--------------------- contracts/external/cw-abc/src/msg.rs | 11 +++ contracts/external/cw-abc/src/queries.rs | 25 +++--- contracts/external/cw-abc/src/testing.rs | 95 ++++++++++++++++++++ 6 files changed, 163 insertions(+), 145 deletions(-) create mode 100644 contracts/external/cw-abc/src/testing.rs diff --git a/contracts/external/cw-abc/src/commands.rs b/contracts/external/cw-abc/src/commands.rs index 5963d8f91..c661ccfb8 100644 --- a/contracts/external/cw-abc/src/commands.rs +++ b/contracts/external/cw-abc/src/commands.rs @@ -1,18 +1,20 @@ -use crate::abc::{CommonsPhase, CurveFn, MinMax}; -use crate::contract::CwAbcResult; -use crate::ContractError; use cosmwasm_std::{ - coins, ensure, Addr, BankMsg, Decimal as StdDecimal, DepsMut, Env, MessageInfo, QuerierWrapper, - Response, StdError, StdResult, Storage, Uint128, + coins, ensure, to_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal as StdDecimal, DepsMut, Env, + MessageInfo, QuerierWrapper, Response, StdError, StdResult, Storage, Uint128, WasmMsg, }; +use cw_tokenfactory_issuer::msg::ExecuteMsg as IssuerExecuteMsg; use cw_utils::must_pay; use std::collections::HashSet; use std::ops::Deref; use token_bindings::{TokenFactoryMsg, TokenFactoryQuery}; +use crate::abc::{CommonsPhase, CurveFn, MinMax}; +use crate::contract::CwAbcResult; use crate::state::{ CURVE_STATE, DONATIONS, HATCHERS, HATCHER_ALLOWLIST, PHASE, PHASE_CONFIG, SUPPLY_DENOM, + TOKEN_ISSUER_CONTRACT, }; +use crate::ContractError; pub fn execute_buy( deps: DepsMut, @@ -64,7 +66,16 @@ pub fn execute_buy( curve_state.supply = new_supply; CURVE_STATE.save(deps.storage, &curve_state)?; - let mint_msg = mint_supply_msg(deps.storage, minted, &info.sender)?; + // Mint tokens for sender by calling mint on the cw-tokenfactory-issuer contract + let issuer_addr = TOKEN_ISSUER_CONTRACT.load(deps.storage)?; + let mint_msg = WasmMsg::Execute { + contract_addr: issuer_addr.to_string(), + msg: to_binary(&IssuerExecuteMsg::Mint { + to_address: info.sender.to_string(), + amount: minted, + })?, + funds: vec![], + }; Ok(Response::new() .add_message(mint_msg) @@ -75,21 +86,6 @@ pub fn execute_buy( .add_attribute("supply", minted)) } -/// Build a message to mint the supply token to the sender -fn mint_supply_msg( - storage: &dyn Storage, - minted: Uint128, - minter: &Addr, -) -> CwAbcResult { - let denom = SUPPLY_DENOM.load(storage)?; - // mint supply token - Ok(TokenFactoryMsg::mint_contract_tokens( - denom, - minted, - minter.to_string(), - )) -} - /// Return the reserved and funded amounts based on the payment and the allocation ratio fn calculate_reserved_and_funded( payment: Uint128, @@ -128,9 +124,29 @@ pub fn execute_sell( let supply_denom = SUPPLY_DENOM.load(deps.storage)?; let burn_amount = must_pay(&info, &supply_denom)?; + + let issuer_addr = TOKEN_ISSUER_CONTRACT.load(deps.storage)?; + // Burn the sent supply tokens - let burn_msg = - TokenFactoryMsg::burn_contract_tokens(supply_denom, burn_amount, burner.to_string()); + 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_binary(&IssuerExecuteMsg::Mint { + to_address: info.sender.to_string(), + amount: burn_amount, + })?, + funds: vec![], + }), + ]; let taxed_amount = calculate_exit_tax(deps.storage, burn_amount)?; @@ -156,14 +172,14 @@ pub fn execute_sell( .map_err(StdError::overflow)?; // Now send the tokens to the sender - let msg = BankMsg::Send { + let msg_send = BankMsg::Send { to_address: burner.to_string(), amount: coins(released_reserve.u128(), curve_state.reserve_denom), }; - Ok(Response::new() - .add_message(msg) - .add_message(burn_msg) + Ok(Response::::new() + .add_messages(burn_msgs) + .add_message(msg_send) .add_attribute("action", "burn") .add_attribute("from", burner) .add_attribute("amount", burn_amount) diff --git a/contracts/external/cw-abc/src/contract.rs b/contracts/external/cw-abc/src/contract.rs index 8a2023d3d..cbb782385 100644 --- a/contracts/external/cw-abc/src/contract.rs +++ b/contracts/external/cw-abc/src/contract.rs @@ -192,11 +192,8 @@ pub fn do_query( to_binary(&queries::query_hatchers(deps, start_after, limit)?) } QueryMsg::Ownership {} => to_binary(&cw_ownable::get_ownership(deps.storage)?), - // TODO get token contract - // QueryMsg::GetDenom { - // creator_address, - // subdenom, - // } => to_binary(&get_denom(deps, creator_address, subdenom)), + QueryMsg::Denom {} => to_binary(&queries::get_denom(deps)?), + QueryMsg::TokenContract {} => to_binary(&TOKEN_ISSUER_CONTRACT.load(deps.storage)?), } } diff --git a/contracts/external/cw-abc/src/lib.rs b/contracts/external/cw-abc/src/lib.rs index a7215cd5f..edc8d4a26 100644 --- a/contracts/external/cw-abc/src/lib.rs +++ b/contracts/external/cw-abc/src/lib.rs @@ -14,103 +14,7 @@ pub mod state; #[cfg(feature = "test-tube")] mod testtube; -pub use crate::error::ContractError; - -// TODO do we still want these? #[cfg(test)] -pub(crate) mod testing { - use crate::abc::{ - ClosedConfig, CommonsPhaseConfig, CurveType, HatchConfig, MinMax, OpenConfig, ReserveToken, - SupplyToken, - }; - use crate::msg::InstantiateMsg; - use cosmwasm_std::{ - testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, - Decimal, OwnedDeps, Uint128, - }; - - use crate::contract; - use crate::contract::CwAbcResult; - use cosmwasm_std::DepsMut; - use std::marker::PhantomData; - use token_bindings::{Metadata, TokenFactoryQuery}; - - pub(crate) mod prelude { - pub use super::{ - default_instantiate_msg, default_supply_metadata, mock_tf_dependencies, TEST_CREATOR, - TEST_RESERVE_DENOM, TEST_SUPPLY_DENOM, _TEST_BUYER, _TEST_INVESTOR, - }; - pub use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; - 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"; +mod testing; - pub const TEST_SUPPLY_DENOM: &str = "subdenom"; - - pub fn default_supply_metadata() -> Metadata { - Metadata { - name: Some("Bonded".to_string()), - symbol: Some("EPOXY".to_string()), - description: None, - denom_units: vec![], - base: None, - display: None, - } - } - - pub fn default_instantiate_msg( - decimals: u8, - reserve_decimals: u8, - curve_type: CurveType, - ) -> InstantiateMsg { - InstantiateMsg { - supply: SupplyToken { - subdenom: TEST_SUPPLY_DENOM.to_string(), - metadata: default_supply_metadata(), - decimals, - }, - reserve: ReserveToken { - denom: TEST_RESERVE_DENOM.to_string(), - decimals: reserve_decimals, - }, - phase_config: CommonsPhaseConfig { - hatch: HatchConfig { - initial_raise: MinMax { - min: Uint128::one(), - max: Uint128::from(1000000u128), - }, - initial_price: Uint128::one(), - initial_allocation_ratio: Decimal::percent(10u64), - exit_tax: Decimal::zero(), - }, - open: OpenConfig { - allocation_percentage: Decimal::percent(10u64), - exit_tax: Decimal::percent(10u64), - }, - closed: ClosedConfig {}, - }, - hatcher_allowlist: None, - curve_type, - } - } - - pub fn mock_init(deps: DepsMut, init_msg: InstantiateMsg) -> CwAbcResult { - let info = mock_info(TEST_CREATOR, &[]); - let env = mock_env(); - contract::instantiate(deps, env, info, init_msg) - } - - pub fn mock_tf_dependencies( - ) -> OwnedDeps, TokenFactoryQuery> { - OwnedDeps { - storage: MockStorage::default(), - api: MockApi::default(), - querier: MockQuerier::::new(&[]), - custom_query_type: PhantomData::, - } - } -} +pub use crate::error::ContractError; diff --git a/contracts/external/cw-abc/src/msg.rs b/contracts/external/cw-abc/src/msg.rs index 69d79d81c..a785fcc3f 100644 --- a/contracts/external/cw-abc/src/msg.rs +++ b/contracts/external/cw-abc/src/msg.rs @@ -89,6 +89,12 @@ pub enum QueryMsg { start_after: Option, limit: Option, }, + /// Returns Token Factory Denom for the supply + #[returns(DenomResponse)] + Denom {}, + /// Returns the address of the cw-tokenfactory-issuer contract + #[returns(::cosmwasm_std::Addr)] + TokenContract {}, } #[cw_serde] @@ -105,6 +111,11 @@ pub struct CurveInfoResponse { pub reserve_denom: String, } +#[cw_serde] +pub struct DenomResponse { + pub denom: String, +} + #[cw_serde] pub struct HatcherAllowlistResponse { /// Hatcher allowlist diff --git a/contracts/external/cw-abc/src/queries.rs b/contracts/external/cw-abc/src/queries.rs index 94670499f..b83cd70a9 100644 --- a/contracts/external/cw-abc/src/queries.rs +++ b/contracts/external/cw-abc/src/queries.rs @@ -1,8 +1,11 @@ use crate::abc::CurveFn; use crate::msg::{ - CommonsPhaseConfigResponse, CurveInfoResponse, DonationsResponse, HatchersResponse, + CommonsPhaseConfigResponse, CurveInfoResponse, DenomResponse, DonationsResponse, + HatchersResponse, +}; +use crate::state::{ + CurveState, CURVE_STATE, DONATIONS, HATCHERS, PHASE, PHASE_CONFIG, SUPPLY_DENOM, }; -use crate::state::{CurveState, CURVE_STATE, DONATIONS, HATCHERS, PHASE, PHASE_CONFIG}; use cosmwasm_std::{Deps, Order, QuerierWrapper, StdResult}; use std::ops::Deref; use token_bindings::TokenFactoryQuery; @@ -43,19 +46,11 @@ pub fn query_phase_config(deps: Deps) -> StdResult, -// creator_addr: String, -// subdenom: String, -// ) -> GetDenomResponse { -// let querier = TokenQuerier::new(&deps.querier); -// let response = querier.full_denom(creator_addr, subdenom).unwrap(); - -// GetDenomResponse { -// denom: response.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, diff --git a/contracts/external/cw-abc/src/testing.rs b/contracts/external/cw-abc/src/testing.rs new file mode 100644 index 000000000..3a54efc7e --- /dev/null +++ b/contracts/external/cw-abc/src/testing.rs @@ -0,0 +1,95 @@ +use crate::abc::{ + ClosedConfig, CommonsPhaseConfig, CurveType, HatchConfig, MinMax, OpenConfig, ReserveToken, + SupplyToken, +}; +use crate::msg::InstantiateMsg; +use cosmwasm_std::{ + testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, + Decimal, OwnedDeps, Uint128, +}; + +use crate::contract; +use crate::contract::CwAbcResult; +use cosmwasm_std::DepsMut; +use std::marker::PhantomData; +use token_bindings::{Metadata, TokenFactoryQuery}; + +pub(crate) mod prelude { + pub use super::{ + default_instantiate_msg, default_supply_metadata, mock_tf_dependencies, TEST_CREATOR, + TEST_RESERVE_DENOM, TEST_SUPPLY_DENOM, _TEST_BUYER, _TEST_INVESTOR, + }; + pub use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + 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() -> Metadata { + Metadata { + name: Some("Bonded".to_string()), + symbol: Some("EPOXY".to_string()), + description: None, + denom_units: vec![], + base: None, + display: None, + } +} + +pub fn default_instantiate_msg( + decimals: u8, + reserve_decimals: u8, + curve_type: CurveType, +) -> InstantiateMsg { + InstantiateMsg { + token_issuer_code_id: 1, + supply: SupplyToken { + subdenom: TEST_SUPPLY_DENOM.to_string(), + metadata: Some(default_supply_metadata()), + decimals, + }, + reserve: ReserveToken { + denom: TEST_RESERVE_DENOM.to_string(), + decimals: reserve_decimals, + }, + phase_config: CommonsPhaseConfig { + hatch: HatchConfig { + initial_raise: MinMax { + min: Uint128::one(), + max: Uint128::from(1000000u128), + }, + initial_price: Uint128::one(), + initial_allocation_ratio: Decimal::percent(10u64), + exit_tax: Decimal::zero(), + }, + open: OpenConfig { + allocation_percentage: Decimal::percent(10u64), + exit_tax: Decimal::percent(10u64), + }, + closed: ClosedConfig {}, + }, + hatcher_allowlist: None, + curve_type, + } +} + +pub fn mock_init(deps: DepsMut, init_msg: InstantiateMsg) -> CwAbcResult { + let info = mock_info(TEST_CREATOR, &[]); + let env = mock_env(); + contract::instantiate(deps, env, info, init_msg) +} + +pub fn mock_tf_dependencies( +) -> OwnedDeps, TokenFactoryQuery> { + OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MockQuerier::::new(&[]), + custom_query_type: PhantomData::, + } +} From a72ad9536f709299b694037ad8be1d6ae2f5b54a Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Wed, 30 Aug 2023 16:22:59 -0700 Subject: [PATCH 12/56] Update schema --- contracts/external/cw-abc/schema/cw-abc.json | 1321 ++++++++++++++++++ 1 file changed, 1321 insertions(+) create mode 100644 contracts/external/cw-abc/schema/cw-abc.json 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..5bc90fd9f --- /dev/null +++ b/contracts/external/cw-abc/schema/cw-abc.json @@ -0,0 +1,1321 @@ +{ + "contract_name": "cw-abc", + "contract_version": "0.0.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" + } + ] + }, + "hatcher_allowlist": { + "description": "Hatcher allowlist", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "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": "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": "This maps to cosmos.bank.v1beta1.DenomUnit protobuf struct", + "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 + } + }, + "additionalProperties": false + }, + "HatchConfig": { + "type": "object", + "required": [ + "exit_tax", + "initial_allocation_ratio", + "initial_price", + "initial_raise" + ], + "properties": { + "exit_tax": { + "description": "Exit tax for the hatch phase", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "initial_allocation_ratio": { + "description": "The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "initial_price": { + "description": "The initial price (p0) per reserve token TODO: initial price is not implemented yet", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "initial_raise": { + "description": "pub contribution_limits: MinMax, The initial raise range (min, max) in the reserve token", + "allOf": [ + { + "$ref": "#/definitions/MinMax" + } + ] + } + }, + "additionalProperties": false + }, + "Metadata": { + "description": "This maps to cosmos.bank.v1beta1.Metadata protobuf struct", + "type": "object", + "required": [ + "denom_units" + ], + "properties": { + "base": { + "description": "base represents the base denom (should be the DenomUnit with exponent = 0).", + "type": [ + "string", + "null" + ] + }, + "denom_units": { + "description": "denom_units represents the list of DenomUnit's for a given coin", + "type": "array", + "items": { + "$ref": "#/definitions/DenomUnit" + } + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "display": { + "description": "display indicates the suggested denom that should be displayed in clients.", + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "name defines the name of the token (eg: Cosmos Atom)", + "type": [ + "string", + "null" + ] + }, + "symbol": { + "description": "symbol is the token symbol usually shown on exchanges (eg: ATOM). This can be the same as the display.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "MinMax": { + "description": "Struct for minimium and maximum values", + "type": "object", + "required": [ + "max", + "min" + ], + "properties": { + "max": { + "$ref": "#/definitions/Uint128" + }, + "min": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, + "OpenConfig": { + "type": "object", + "required": [ + "allocation_percentage", + "exit_tax" + ], + "properties": { + "allocation_percentage": { + "description": "Percentage of capital put into the Reserve Pool during the Open phase", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "exit_tax": { + "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 + }, + "metadata": { + "description": "Metadata for the supply token to create", + "anyOf": [ + { + "$ref": "#/definitions/Metadata" + }, + { + "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" + } + } + }, + "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 in that message", + "type": "object", + "required": [ + "buy" + ], + "properties": { + "buy": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Burn is a base message to destroy tokens forever", + "type": "object", + "required": [ + "burn" + ], + "properties": { + "burn": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Donate will add reserve tokens to the funding pool", + "type": "object", + "required": [ + "donate" + ], + "properties": { + "donate": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Update the hatch phase allowlist", + "type": "object", + "required": [ + "update_hatch_allowlist" + ], + "properties": { + "update_hatch_allowlist": { + "type": "object", + "required": [ + "to_add", + "to_remove" + ], + "properties": { + "to_add": { + "type": "array", + "items": { + "type": "string" + } + }, + "to_remove": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Update the hatch phase configuration This can only be called by the admin and only during the hatch phase", + "type": "object", + "required": [ + "update_phase_config" + ], + "properties": { + "update_phase_config": { + "$ref": "#/definitions/UpdatePhaseConfigMsg" + } + }, + "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" + ] + } + ] + }, + "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 + } + ] + }, + "MinMax": { + "description": "Struct for minimium 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 admin and only before or during each phase", + "oneOf": [ + { + "description": "Update the hatch phase configuration", + "type": "object", + "required": [ + "hatch" + ], + "properties": { + "hatch": { + "type": "object", + "properties": { + "initial_allocation_ratio": { + "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": { + "exit_tax": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "reserve_ratio": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Update the closed phase configuration", + "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 the current phase configuration Returns [`CommonsPhaseConfigResponse`]", + "type": "object", + "required": [ + "phase_config" + ], + "properties": { + "phase_config": { + "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 + }, + { + "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 Token Factory Denom for the supply", + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "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 + } + ] + }, + "migrate": null, + "sudo": null, + "responses": { + "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" + } + } + }, + "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" + } + } + }, + "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" + } + } + }, + "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_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": "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": [ + "exit_tax", + "initial_allocation_ratio", + "initial_price", + "initial_raise" + ], + "properties": { + "exit_tax": { + "description": "Exit tax for the hatch phase", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "initial_allocation_ratio": { + "description": "The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "initial_price": { + "description": "The initial price (p0) per reserve token TODO: initial price is not implemented yet", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "initial_raise": { + "description": "pub contribution_limits: MinMax, The initial raise range (min, max) in the reserve token", + "allOf": [ + { + "$ref": "#/definitions/MinMax" + } + ] + } + }, + "additionalProperties": false + }, + "MinMax": { + "description": "Struct for minimium and maximum values", + "type": "object", + "required": [ + "max", + "min" + ], + "properties": { + "max": { + "$ref": "#/definitions/Uint128" + }, + "min": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, + "OpenConfig": { + "type": "object", + "required": [ + "allocation_percentage", + "exit_tax" + ], + "properties": { + "allocation_percentage": { + "description": "Percentage of capital put into the Reserve Pool during the Open phase", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "exit_tax": { + "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" + } + } + }, + "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" + } + } +} From fc5d5d6847e9679b83d1aa6c4309b92487e5482e Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Wed, 30 Aug 2023 16:23:14 -0700 Subject: [PATCH 13/56] Clean up and notes --- contracts/external/cw-abc/src/contract.rs | 265 ++-------------------- contracts/external/cw-abc/src/testing.rs | 225 +++++++++++++++++- 2 files changed, 232 insertions(+), 258 deletions(-) diff --git a/contracts/external/cw-abc/src/contract.rs b/contracts/external/cw-abc/src/contract.rs index cbb782385..55c4d18cb 100644 --- a/contracts/external/cw-abc/src/contract.rs +++ b/contracts/external/cw-abc/src/contract.rs @@ -60,21 +60,6 @@ pub fn instantiate( phase_config.validate()?; - // Tnstantiate cw-token-factory-issuer contract - // DAO (sender) is set as contract admin - let issuer_instantiate_msg = SubMsg::reply_always( - WasmMsg::Instantiate { - admin: Some(info.sender.to_string()), - code_id: token_issuer_code_id, - msg: to_binary(&IssuerInstantiateMsg::NewToken { - subdenom: supply.subdenom.clone(), - })?, - funds: info.funds, - label: "cw-tokenfactory-issuer".to_string(), - }, - INSTANTIATE_TOKEN_FACTORY_ISSUER_REPLY_ID, - ); - // Save new token info for use in reply TOKEN_INSTANTIATION_INFO.save(deps.storage, &supply)?; @@ -108,8 +93,25 @@ pub fn instantiate( // TODO don't hardcode this? PHASE.save(deps.storage, &CommonsPhase::Hatch)?; + // Initialize owner to sender cw_ownable::initialize_owner(deps.storage, deps.api, Some(info.sender.as_str()))?; + // TODO Potential renounce admin? + // Tnstantiate cw-token-factory-issuer contract + // Sender is set as contract admin + let issuer_instantiate_msg = SubMsg::reply_always( + WasmMsg::Instantiate { + admin: Some(info.sender.to_string()), + code_id: token_issuer_code_id, + msg: to_binary(&IssuerInstantiateMsg::NewToken { + subdenom: supply.subdenom.clone(), + })?, + funds: info.funds, + label: "cw-tokenfactory-issuer".to_string(), + }, + INSTANTIATE_TOKEN_FACTORY_ISSUER_REPLY_ID, + ); + Ok(Response::default().add_submessage(issuer_instantiate_msg)) } @@ -224,30 +226,11 @@ pub fn reply( let token_info = TOKEN_INSTANTIATION_INFO.load(deps.storage)?; TOKEN_INSTANTIATION_INFO.remove(deps.storage); - // // Load the DAO address - // let dao = DAO.load(deps.storage)?; - // Format the denom and save it let denom = format!("factory/{}/{}", &issuer_addr, token_info.subdenom); SUPPLY_DENOM.save(deps.storage, &denom)?; - // // Check supply is greater than zero, iterate through initial - // // balances and sum them, add DAO balance as well. - // let initial_supply = token - // .initial_balances - // .iter() - // .fold(Uint128::zero(), |previous, new_balance| { - // previous + new_balance.amount - // }); - // let total_supply = initial_supply + token.initial_dao_balance.unwrap_or_default(); - - // // Cannot instantiate with no initial token owners because it would - // // immediately lock the DAO. - // if initial_supply.is_zero() { - // return Err(ContractError::InitialBalancesError {}); - // } - // Msgs to be executed to finalize setup let mut msgs: Vec = vec![]; @@ -316,217 +299,3 @@ pub fn reply( _ => Err(ContractError::UnknownReplyId { id: msg.id }), } } - -// #[cfg(test)] -// pub(crate) mod tests { -// use super::*; -// use crate::abc::CurveType; -// use crate::queries::query_curve_info; -// use cosmwasm_std::{ -// testing::{mock_env, mock_info}, -// CosmosMsg, Decimal, Uint128, -// }; -// use speculoos::prelude::*; - -// use crate::testing::*; - -// // fn get_balance>(deps: Deps, addr: U) -> Uint128 { -// // query_balance(deps, addr.into()).unwrap().balance -// // } - -// // fn setup_test(deps: DepsMut, decimals: u8, reserve_decimals: u8, curve_type: CurveType) { -// // // this matches `linear_curve` test case from curves.rs -// // let creator = String::from(CREATOR); -// // let msg = default_instantiate(decimals, reserve_decimals, curve_type); -// // let info = mock_info(&creator, &[]); - -// // // make sure we can instantiate with this -// // let res = instantiate(deps, mock_env(), info, msg).unwrap(); -// // assert_eq!(0, res.messages.len()); -// // } - -// /// Mock token factory querier dependencies - -// // #[test] -// // fn proper_instantiation() -> CwAbcResult<()> { -// // let mut deps = mock_tf_dependencies(); - -// // // this matches `linear_curve` test case from curves.rs -// // let creator = String::from("creator"); -// // let curve_type = CurveType::SquareRoot { -// // slope: Uint128::new(1), -// // scale: 1, -// // }; -// // let msg = default_instantiate_msg(2, 8, curve_type.clone()); -// // let info = mock_info(&creator, &[]); - -// // // make sure we can instantiate with this -// // let res = instantiate(deps.as_mut(), mock_env(), info, msg)?; -// // assert_that!(res.messages.len()).is_equal_to(1); -// // let submsg = res.messages.get(0).unwrap(); -// // assert_that!(submsg.msg).is_equal_to(CosmosMsg::Custom(TokenFactoryMsg::Token( -// // TokenMsg::CreateDenom { -// // subdenom: TEST_SUPPLY_DENOM.to_string(), -// // metadata: Some(default_supply_metadata()), -// // }, -// // ))); - -// // // TODO! -// // // // token info is proper -// // // let token = query_token_info(deps.as_ref()).unwrap(); -// // // assert_that!(&token.name, &msg.name); -// // // assert_that!(&token.symbol, &msg.symbol); -// // // assert_that!(token.decimals, 2); -// // // assert_that!(token.total_supply, Uint128::zero()); - -// // // curve state is sensible -// // let state = query_curve_info(deps.as_ref(), curve_type.to_curve_fn())?; -// // assert_that!(state.reserve).is_equal_to(Uint128::zero()); -// // assert_that!(state.supply).is_equal_to(Uint128::zero()); -// // assert_that!(state.reserve_denom.as_str()).is_equal_to(TEST_RESERVE_DENOM); -// // // spot price 0 as supply is 0 -// // assert_that!(state.spot_price).is_equal_to(Decimal::zero()); - -// // // curve type is stored properly -// // let curve = CURVE_TYPE.load(&deps.storage).unwrap(); -// // assert_eq!(curve_type, curve); - -// // // no balance -// // // assert_eq!(get_balance(deps.as_ref(), &creator), Uint128::zero()); - -// // Ok(()) -// // } - -// // #[test] -// // fn buy_issues_tokens() { -// // let mut deps = mock_dependencies(); -// // let curve_type = CurveType::Linear { -// // slope: Uint128::new(1), -// // scale: 1, -// // }; -// // setup_test(deps.as_mut(), 2, 8, curve_type.clone()); - -// // // succeeds with proper token (5 BTC = 5*10^8 satoshi) -// // let info = mock_info(INVESTOR, &coins(500_000_000, DENOM)); -// // let buy = ExecuteMsg::Buy {}; -// // execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap(); - -// // // bob got 1000 EPOXY (10.00) -// // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); -// // assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::zero()); - -// // // send them all to buyer -// // let info = mock_info(INVESTOR, &[]); -// // let send = ExecuteMsg::Transfer { -// // recipient: BUYER.into(), -// // amount: Uint128::new(1000), -// // }; -// // execute(deps.as_mut(), mock_env(), info, send).unwrap(); - -// // // ensure balances updated -// // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::zero()); -// // assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::new(1000)); - -// // // second stake needs more to get next 1000 EPOXY -// // let info = mock_info(INVESTOR, &coins(1_500_000_000, DENOM)); -// // execute(deps.as_mut(), mock_env(), info, buy).unwrap(); - -// // // ensure balances updated -// // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); -// // assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::new(1000)); - -// // // check curve info updated -// // let curve = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); -// // assert_eq!(curve.reserve, Uint128::new(2_000_000_000)); -// // assert_eq!(curve.supply, Uint128::new(2000)); -// // assert_eq!(curve.spot_price, Decimal::percent(200)); - -// // // check token info updated -// // let token = query_token_info(deps.as_ref()).unwrap(); -// // assert_eq!(token.decimals, 2); -// // assert_eq!(token.total_supply, Uint128::new(2000)); -// // } - -// // #[test] -// // fn bonding_fails_with_wrong_denom() { -// // let mut deps = mock_dependencies(); -// // let curve_type = CurveType::Linear { -// // slope: Uint128::new(1), -// // scale: 1, -// // }; -// // setup_test(deps.as_mut(), 2, 8, curve_type); - -// // // fails when no tokens sent -// // let info = mock_info(INVESTOR, &[]); -// // let buy = ExecuteMsg::Buy {}; -// // let err = execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err(); -// // assert_eq!(err, PaymentError::NoFunds {}.into()); - -// // // fails when wrong tokens sent -// // let info = mock_info(INVESTOR, &coins(1234567, "wei")); -// // let err = execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err(); -// // assert_eq!(err, PaymentError::MissingDenom(DENOM.into()).into()); - -// // // fails when too many tokens sent -// // let info = mock_info(INVESTOR, &[coin(3400022, DENOM), coin(1234567, "wei")]); -// // let err = execute(deps.as_mut(), mock_env(), info, buy).unwrap_err(); -// // assert_eq!(err, PaymentError::MultipleDenoms {}.into()); -// // } - -// // #[test] -// // fn burning_sends_reserve() { -// // let mut deps = mock_dependencies(); -// // let curve_type = CurveType::Linear { -// // slope: Uint128::new(1), -// // scale: 1, -// // }; -// // setup_test(deps.as_mut(), 2, 8, curve_type.clone()); - -// // // succeeds with proper token (20 BTC = 20*10^8 satoshi) -// // let info = mock_info(INVESTOR, &coins(2_000_000_000, DENOM)); -// // let buy = ExecuteMsg::Buy {}; -// // execute(deps.as_mut(), mock_env(), info, buy).unwrap(); - -// // // bob got 2000 EPOXY (20.00) -// // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(2000)); - -// // // cannot burn too much -// // let info = mock_info(INVESTOR, &[]); -// // let burn = ExecuteMsg::Burn { -// // amount: Uint128::new(3000), -// // }; -// // let err = execute(deps.as_mut(), mock_env(), info, burn).unwrap_err(); -// // // TODO check error - -// // // burn 1000 EPOXY to get back 15BTC (*10^8) -// // let info = mock_info(INVESTOR, &[]); -// // let burn = ExecuteMsg::Burn { -// // amount: Uint128::new(1000), -// // }; -// // let res = execute(deps.as_mut(), mock_env(), info, burn).unwrap(); - -// // // balance is lower -// // assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); - -// // // ensure we got our money back -// // assert_eq!(1, res.messages.len()); -// // assert_eq!( -// // &res.messages[0], -// // &SubMsg::new(BankMsg::Send { -// // to_address: INVESTOR.into(), -// // amount: coins(1_500_000_000, DENOM), -// // }) -// // ); - -// // // check curve info updated -// // let curve = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); -// // assert_eq!(curve.reserve, Uint128::new(500_000_000)); -// // assert_eq!(curve.supply, Uint128::new(1000)); -// // assert_eq!(curve.spot_price, Decimal::percent(100)); - -// // // check token info updated -// // let token = query_token_info(deps.as_ref()).unwrap(); -// // assert_eq!(token.decimals, 2); -// // assert_eq!(token.total_supply, Uint128::new(1000)); -// // } -// } diff --git a/contracts/external/cw-abc/src/testing.rs b/contracts/external/cw-abc/src/testing.rs index 3a54efc7e..8efad0961 100644 --- a/contracts/external/cw-abc/src/testing.rs +++ b/contracts/external/cw-abc/src/testing.rs @@ -1,18 +1,24 @@ -use crate::abc::{ - ClosedConfig, CommonsPhaseConfig, CurveType, HatchConfig, MinMax, OpenConfig, ReserveToken, - SupplyToken, -}; -use crate::msg::InstantiateMsg; use cosmwasm_std::{ testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, - Decimal, OwnedDeps, Uint128, + CosmosMsg, Decimal, DepsMut, OwnedDeps, Uint128, }; +use speculoos::prelude::*; +use std::marker::PhantomData; +use token_bindings::{Metadata, TokenFactoryMsg, TokenFactoryQuery}; -use crate::contract; use crate::contract::CwAbcResult; -use cosmwasm_std::DepsMut; -use std::marker::PhantomData; -use token_bindings::{Metadata, TokenFactoryQuery}; +use crate::msg::InstantiateMsg; +use crate::queries::query_curve_info; +use crate::{ + abc::{ + ClosedConfig, CommonsPhaseConfig, CurveType, HatchConfig, MinMax, OpenConfig, ReserveToken, + SupplyToken, + }, + contract::instantiate, +}; +use crate::{contract, state::CURVE_TYPE}; + +const CREATOR: &str = "creator"; pub(crate) mod prelude { pub use super::{ @@ -93,3 +99,202 @@ pub fn mock_tf_dependencies( custom_query_type: PhantomData::, } } + +// fn setup_test( +// deps: DepsMut, +// decimals: u8, +// reserve_decimals: u8, +// curve_type: CurveType, +// ) { +// // this matches `linear_curve` test case from curves.rs +// let creator = String::from(CREATOR); +// let msg = default_instantiate_msg(decimals, reserve_decimals, curve_type); +// let info = mock_info(&creator, &[]); + +// // make sure we can instantiate with this +// let res = instantiate(deps, mock_env(), info, msg).unwrap(); +// assert_eq!(0, res.messages.len()); +// } + +// Mock token factory querier dependencies +#[test] +fn proper_instantiation() -> CwAbcResult<()> { + let mut deps = mock_tf_dependencies(); + + // this matches `linear_curve` test case from curves.rs + let creator = String::from("creator"); + let curve_type = CurveType::SquareRoot { + slope: Uint128::new(1), + scale: 1, + }; + let msg = default_instantiate_msg(2, 8, curve_type.clone()); + let info = mock_info(&creator, &[]); + + // make sure we can instantiate with this + let res = instantiate(deps.as_mut(), mock_env(), info, msg)?; + assert_that!(res.messages.len()).is_equal_to(1); + let submsg = res.messages.get(0).unwrap(); + assert_that!(submsg.msg).is_equal_to(CosmosMsg::Custom(WasmMsg::Execute { + contract_addr: (), + msg: (), + funds: (), + })); + + // TODO! + // // token info is proper + // let token = query_token_info(deps.as_ref()).unwrap(); + // assert_that!(&token.name, &msg.name); + // assert_that!(&token.symbol, &msg.symbol); + // assert_that!(token.decimals, 2); + // assert_that!(token.total_supply, Uint128::zero()); + + // curve state is sensible + let state = query_curve_info(deps.as_ref(), curve_type.to_curve_fn())?; + assert_that!(state.reserve).is_equal_to(Uint128::zero()); + assert_that!(state.supply).is_equal_to(Uint128::zero()); + assert_that!(state.reserve_denom.as_str()).is_equal_to(TEST_RESERVE_DENOM); + // spot price 0 as supply is 0 + assert_that!(state.spot_price).is_equal_to(Decimal::zero()); + + // curve type is stored properly + let curve = CURVE_TYPE.load(&deps.storage).unwrap(); + assert_eq!(curve_type, curve); + + // no balance + // assert_eq!(get_balance(deps.as_ref(), &creator), Uint128::zero()); + + Ok(()) +} + +// #[test] +// fn buy_issues_tokens() { +// let mut deps = mock_dependencies(); +// let curve_type = CurveType::Linear { +// slope: Uint128::new(1), +// scale: 1, +// }; +// setup_test(deps.as_mut(), 2, 8, curve_type.clone()); + +// // succeeds with proper token (5 BTC = 5*10^8 satoshi) +// let info = mock_info(INVESTOR, &coins(500_000_000, DENOM)); +// let buy = ExecuteMsg::Buy {}; +// execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap(); + +// // bob got 1000 EPOXY (10.00) +// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); +// assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::zero()); + +// // send them all to buyer +// let info = mock_info(INVESTOR, &[]); +// let send = ExecuteMsg::Transfer { +// recipient: BUYER.into(), +// amount: Uint128::new(1000), +// }; +// execute(deps.as_mut(), mock_env(), info, send).unwrap(); + +// // ensure balances updated +// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::zero()); +// assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::new(1000)); + +// // second stake needs more to get next 1000 EPOXY +// let info = mock_info(INVESTOR, &coins(1_500_000_000, DENOM)); +// execute(deps.as_mut(), mock_env(), info, buy).unwrap(); + +// // ensure balances updated +// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); +// assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::new(1000)); + +// // check curve info updated +// let curve = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); +// assert_eq!(curve.reserve, Uint128::new(2_000_000_000)); +// assert_eq!(curve.supply, Uint128::new(2000)); +// assert_eq!(curve.spot_price, Decimal::percent(200)); + +// // check token info updated +// let token = query_token_info(deps.as_ref()).unwrap(); +// assert_eq!(token.decimals, 2); +// assert_eq!(token.total_supply, Uint128::new(2000)); +// } + +// #[test] +// fn bonding_fails_with_wrong_denom() { +// let mut deps = mock_dependencies(); +// let curve_type = CurveType::Linear { +// slope: Uint128::new(1), +// scale: 1, +// }; +// setup_test(deps.as_mut(), 2, 8, curve_type); + +// // fails when no tokens sent +// let info = mock_info(INVESTOR, &[]); +// let buy = ExecuteMsg::Buy {}; +// let err = execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err(); +// assert_eq!(err, PaymentError::NoFunds {}.into()); + +// // fails when wrong tokens sent +// let info = mock_info(INVESTOR, &coins(1234567, "wei")); +// let err = execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err(); +// assert_eq!(err, PaymentError::MissingDenom(DENOM.into()).into()); + +// // fails when too many tokens sent +// let info = mock_info(INVESTOR, &[coin(3400022, DENOM), coin(1234567, "wei")]); +// let err = execute(deps.as_mut(), mock_env(), info, buy).unwrap_err(); +// assert_eq!(err, PaymentError::MultipleDenoms {}.into()); +// } + +// #[test] +// fn burning_sends_reserve() { +// let mut deps = mock_dependencies(); +// let curve_type = CurveType::Linear { +// slope: Uint128::new(1), +// scale: 1, +// }; +// setup_test(deps.as_mut(), 2, 8, curve_type.clone()); + +// // succeeds with proper token (20 BTC = 20*10^8 satoshi) +// let info = mock_info(INVESTOR, &coins(2_000_000_000, DENOM)); +// let buy = ExecuteMsg::Buy {}; +// execute(deps.as_mut(), mock_env(), info, buy).unwrap(); + +// // bob got 2000 EPOXY (20.00) +// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(2000)); + +// // cannot burn too much +// let info = mock_info(INVESTOR, &[]); +// let burn = ExecuteMsg::Burn { +// amount: Uint128::new(3000), +// }; +// let err = execute(deps.as_mut(), mock_env(), info, burn).unwrap_err(); +// // TODO check error + +// // burn 1000 EPOXY to get back 15BTC (*10^8) +// let info = mock_info(INVESTOR, &[]); +// let burn = ExecuteMsg::Burn { +// amount: Uint128::new(1000), +// }; +// let res = execute(deps.as_mut(), mock_env(), info, burn).unwrap(); + +// // balance is lower +// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); + +// // ensure we got our money back +// assert_eq!(1, res.messages.len()); +// assert_eq!( +// &res.messages[0], +// &SubMsg::new(BankMsg::Send { +// to_address: INVESTOR.into(), +// amount: coins(1_500_000_000, DENOM), +// }) +// ); + +// // check curve info updated +// let curve = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); +// assert_eq!(curve.reserve, Uint128::new(500_000_000)); +// assert_eq!(curve.supply, Uint128::new(1000)); +// assert_eq!(curve.spot_price, Decimal::percent(100)); + +// // check token info updated +// let token = query_token_info(deps.as_ref()).unwrap(); +// assert_eq!(token.decimals, 2); +// assert_eq!(token.total_supply, Uint128::new(1000)); +// } From 2f6bba46e45c765324030c1ea51a711cf23318d6 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Wed, 30 Aug 2023 17:09:25 -0700 Subject: [PATCH 14/56] Get test-tube tests running --- contracts/external/cw-abc/Cargo.toml | 12 +- contracts/external/cw-abc/src/lib.rs | 2 +- .../cw-abc/src/test_tube/integration_tests.rs | 3 + .../external/cw-abc/src/test_tube/mod.rs | 1 + .../external/cw-abc/src/test_tube/test_env.rs | 235 ++++++++++++++++-- contracts/external/cw-abc/src/testing.rs | 98 ++++---- packages/dao-testing/src/test_tube/cw_abc.rs | 26 ++ 7 files changed, 306 insertions(+), 71 deletions(-) diff --git a/contracts/external/cw-abc/Cargo.toml b/contracts/external/cw-abc/Cargo.toml index 0e0ec211f..7b169adae 100644 --- a/contracts/external/cw-abc/Cargo.toml +++ b/contracts/external/cw-abc/Cargo.toml @@ -20,7 +20,11 @@ crate-type = ["cdylib", "rlib"] backtraces = ["cosmwasm-std/backtraces"] # use library feature to disable all instantiate/execute/query exports library = [] -# interface = ["dep:cw-orch"] # Adds the dependency when the feature is enabled +# 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 } @@ -39,15 +43,17 @@ token-bindings = { workspace = true } cw-ownable = { workspace = true } cw-paginate-storage = { workspace = true } cw-tokenfactory-issuer = { workspace = true, features = ["library"] } -# cw-orch = { version = "0.13.3", optional = true } [dev-dependencies] # TODO move to workspace? speculoos = "0.11.0" anyhow = { workspace = true } cw-multi-test = { workspace = true } -dao-testing = { 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 } [profile.release] rpath = false diff --git a/contracts/external/cw-abc/src/lib.rs b/contracts/external/cw-abc/src/lib.rs index edc8d4a26..d8a18c1cb 100644 --- a/contracts/external/cw-abc/src/lib.rs +++ b/contracts/external/cw-abc/src/lib.rs @@ -12,7 +12,7 @@ pub mod state; // cargo test --features test-tube #[cfg(test)] #[cfg(feature = "test-tube")] -mod testtube; +mod test_tube; #[cfg(test)] mod testing; diff --git a/contracts/external/cw-abc/src/test_tube/integration_tests.rs b/contracts/external/cw-abc/src/test_tube/integration_tests.rs index ccfd01e81..a2f00ed57 100644 --- a/contracts/external/cw-abc/src/test_tube/integration_tests.rs +++ b/contracts/external/cw-abc/src/test_tube/integration_tests.rs @@ -1,3 +1,4 @@ +use super::test_env::{TestEnv, TestEnvBuilder}; use crate::{ abc::{ ClosedConfig, CommonsPhaseConfig, HatchConfig, MinMax, OpenConfig, ReserveToken, @@ -13,5 +14,7 @@ use token_bindings::Metadata; fn test_happy_path() { let app = OsmosisTestApp::new(); + let env = TestEnvBuilder::new(); + let TestEnv { abc, accounts, .. } = env.default_setup(&app); // TODO } diff --git a/contracts/external/cw-abc/src/test_tube/mod.rs b/contracts/external/cw-abc/src/test_tube/mod.rs index 1ca46c6ff..eb0b4f91b 100644 --- a/contracts/external/cw-abc/src/test_tube/mod.rs +++ b/contracts/external/cw-abc/src/test_tube/mod.rs @@ -1 +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 index 7201f046b..c53fb008a 100644 --- a/contracts/external/cw-abc/src/test_tube/test_env.rs +++ b/contracts/external/cw-abc/src/test_tube/test_env.rs @@ -3,24 +3,28 @@ #![allow(dead_code)] use crate::{ - msg::{ExecuteMsg, InitialBalance, InstantiateMsg, NewTokenInfo, QueryMsg, TokenInfo}, + abc::{ + ClosedConfig, CommonsPhaseConfig, CurveType, HatchConfig, MinMax, OpenConfig, ReserveToken, + SupplyToken, + }, + msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, ContractError, }; -use cosmwasm_std::{Coin, Uint128}; +use cosmwasm_std::{Coin, Decimal, Uint128}; use cw_tokenfactory_issuer::msg::{DenomResponse, DenomUnit}; use cw_utils::Duration; -use dao_interface::voting::{IsActiveResponse, VotingPowerAtHeightResponse}; -use dao_testing::test_tube::{cw_abc::CwAbc, cw_tokenfactory_issuer::TokenfactoryIssuer}; -use dao_voting::threshold::ActiveThreshold; -use osmosis_std::types::{ - cosmos::bank::v1beta1::QueryAllBalancesRequest, cosmwasm::wasm::v1::MsgExecuteContractResponse, -}; +use dao_testing::test_tube::cw_tokenfactory_issuer::TokenfactoryIssuer; use osmosis_test_tube::{ + osmosis_std::types::cosmos::bank::v1beta1::QueryAllBalancesRequest, + osmosis_std::types::cosmwasm::wasm::v1::{ + MsgExecuteContractResponse, MsgMigrateContract, MsgMigrateContractResponse, + }, 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"; @@ -28,7 +32,7 @@ pub const JUNO: &str = "ujuno"; pub struct TestEnv<'a> { pub app: &'a OsmosisTestApp, - pub abc: TfDaoVotingContract<'a>, + pub abc: CwAbc<'a>, pub tf_issuer: TokenfactoryIssuer<'a>, pub accounts: Vec, } @@ -105,17 +109,46 @@ impl TestEnvBuilder { .init_accounts(&[Coin::new(1000000000000000u128, "uosmo")], 10) .unwrap(); - let initial_balances: Vec = accounts - .iter() - .map(|acc| InitialBalance { - address: acc.address(), - amount: Uint128::new(100), - }) - .collect(); - let issuer_id = TokenfactoryIssuer::upload(app, &accounts[0]).unwrap(); - let abc = CwAbc::deploy(app, &InstantiateMsg {}, &accounts[0]).unwrap(); + let abc = CwAbc::deploy( + app, + &InstantiateMsg { + token_issuer_code_id: issuer_id, + supply: SupplyToken { + subdenom: DENOM.to_string(), + metadata: None, + decimals: 6, + }, + reserve: ReserveToken { + denom: JUNO.to_string(), + decimals: 6, + }, + phase_config: CommonsPhaseConfig { + hatch: HatchConfig { + initial_raise: MinMax { + min: Uint128::one(), + max: Uint128::from(1000000u128), + }, + initial_price: Uint128::one(), + initial_allocation_ratio: Decimal::percent(10u64), + exit_tax: Decimal::zero(), + }, + open: OpenConfig { + allocation_percentage: Decimal::percent(10u64), + exit_tax: 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(); @@ -193,3 +226,169 @@ pub fn assert_contract_err(expected: ContractError, actual: RunnerError) { _ => 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, "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_tokenfactory_issuer.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_tokenfactory_issuer-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/contracts/external/cw-abc/src/testing.rs b/contracts/external/cw-abc/src/testing.rs index 8efad0961..2a9247557 100644 --- a/contracts/external/cw-abc/src/testing.rs +++ b/contracts/external/cw-abc/src/testing.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, - CosmosMsg, Decimal, DepsMut, OwnedDeps, Uint128, + CosmosMsg, Decimal, DepsMut, OwnedDeps, Uint128, WasmMsg, }; use speculoos::prelude::*; use std::marker::PhantomData; @@ -116,55 +116,55 @@ pub fn mock_tf_dependencies( // assert_eq!(0, res.messages.len()); // } -// Mock token factory querier dependencies -#[test] -fn proper_instantiation() -> CwAbcResult<()> { - let mut deps = mock_tf_dependencies(); +// // Mock token factory querier dependencies +// #[test] +// fn proper_instantiation() -> CwAbcResult<()> { +// let mut deps = mock_tf_dependencies(); - // this matches `linear_curve` test case from curves.rs - let creator = String::from("creator"); - let curve_type = CurveType::SquareRoot { - slope: Uint128::new(1), - scale: 1, - }; - let msg = default_instantiate_msg(2, 8, curve_type.clone()); - let info = mock_info(&creator, &[]); - - // make sure we can instantiate with this - let res = instantiate(deps.as_mut(), mock_env(), info, msg)?; - assert_that!(res.messages.len()).is_equal_to(1); - let submsg = res.messages.get(0).unwrap(); - assert_that!(submsg.msg).is_equal_to(CosmosMsg::Custom(WasmMsg::Execute { - contract_addr: (), - msg: (), - funds: (), - })); - - // TODO! - // // token info is proper - // let token = query_token_info(deps.as_ref()).unwrap(); - // assert_that!(&token.name, &msg.name); - // assert_that!(&token.symbol, &msg.symbol); - // assert_that!(token.decimals, 2); - // assert_that!(token.total_supply, Uint128::zero()); - - // curve state is sensible - let state = query_curve_info(deps.as_ref(), curve_type.to_curve_fn())?; - assert_that!(state.reserve).is_equal_to(Uint128::zero()); - assert_that!(state.supply).is_equal_to(Uint128::zero()); - assert_that!(state.reserve_denom.as_str()).is_equal_to(TEST_RESERVE_DENOM); - // spot price 0 as supply is 0 - assert_that!(state.spot_price).is_equal_to(Decimal::zero()); - - // curve type is stored properly - let curve = CURVE_TYPE.load(&deps.storage).unwrap(); - assert_eq!(curve_type, curve); - - // no balance - // assert_eq!(get_balance(deps.as_ref(), &creator), Uint128::zero()); - - Ok(()) -} +// // this matches `linear_curve` test case from curves.rs +// let creator = String::from("creator"); +// let curve_type = CurveType::SquareRoot { +// slope: Uint128::new(1), +// scale: 1, +// }; +// let msg = default_instantiate_msg(2, 8, curve_type.clone()); +// let info = mock_info(&creator, &[]); + +// // make sure we can instantiate with this +// let res = instantiate(deps.as_mut(), mock_env(), info, msg)?; +// assert_that!(res.messages.len()).is_equal_to(1); +// let submsg = res.messages.get(0).unwrap(); +// assert_that!(submsg.msg).is_equal_to(CosmosMsg::Custom(WasmMsg::Execute { +// contract_addr: (), +// msg: (), +// funds: (), +// })); + +// // TODO! +// // // token info is proper +// // let token = query_token_info(deps.as_ref()).unwrap(); +// // assert_that!(&token.name, &msg.name); +// // assert_that!(&token.symbol, &msg.symbol); +// // assert_that!(token.decimals, 2); +// // assert_that!(token.total_supply, Uint128::zero()); + +// // curve state is sensible +// let state = query_curve_info(deps.as_ref(), curve_type.to_curve_fn())?; +// assert_that!(state.reserve).is_equal_to(Uint128::zero()); +// assert_that!(state.supply).is_equal_to(Uint128::zero()); +// assert_that!(state.reserve_denom.as_str()).is_equal_to(TEST_RESERVE_DENOM); +// // spot price 0 as supply is 0 +// assert_that!(state.spot_price).is_equal_to(Decimal::zero()); + +// // curve type is stored properly +// let curve = CURVE_TYPE.load(&deps.storage).unwrap(); +// assert_eq!(curve_type, curve); + +// // no balance +// // assert_eq!(get_balance(deps.as_ref(), &creator), Uint128::zero()); + +// Ok(()) +// } // #[test] // fn buy_issues_tokens() { diff --git a/packages/dao-testing/src/test_tube/cw_abc.rs b/packages/dao-testing/src/test_tube/cw_abc.rs index a10f80bfa..59e90039d 100644 --- a/packages/dao-testing/src/test_tube/cw_abc.rs +++ b/packages/dao-testing/src/test_tube/cw_abc.rs @@ -78,6 +78,32 @@ impl<'a> CwAbc<'a> { 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, From 7d6b60013cb8ed0b6fcbe467d15597871b67078a Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Wed, 30 Aug 2023 17:39:24 -0700 Subject: [PATCH 15/56] Get instantiation working with test-tube --- Cargo.lock | 3 +++ contracts/external/cw-abc/src/contract.rs | 3 +-- .../external/cw-abc/src/test_tube/integration_tests.rs | 8 -------- contracts/external/cw-abc/src/test_tube/test_env.rs | 6 ++++-- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 814550dce..65af40139 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -741,8 +741,11 @@ dependencies = [ "getrandom", "integer-cbrt", "integer-sqrt", + "osmosis-std 0.16.2", "osmosis-test-tube", "rust_decimal", + "serde", + "serde_json", "speculoos", "thiserror", "token-bindings", diff --git a/contracts/external/cw-abc/src/contract.rs b/contracts/external/cw-abc/src/contract.rs index 55c4d18cb..4a7c489d7 100644 --- a/contracts/external/cw-abc/src/contract.rs +++ b/contracts/external/cw-abc/src/contract.rs @@ -8,7 +8,7 @@ use cw2::set_contract_version; use cw_tokenfactory_issuer::msg::{ ExecuteMsg as IssuerExecuteMsg, InstantiateMsg as IssuerInstantiateMsg, }; -use cw_utils::{nonpayable, parse_reply_instantiate_data}; +use cw_utils::parse_reply_instantiate_data; use std::collections::HashSet; use token_bindings::{TokenFactoryMsg, TokenFactoryQuery}; @@ -40,7 +40,6 @@ pub fn instantiate( info: MessageInfo, msg: InstantiateMsg, ) -> CwAbcResult { - nonpayable(&info)?; set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; let InstantiateMsg { diff --git a/contracts/external/cw-abc/src/test_tube/integration_tests.rs b/contracts/external/cw-abc/src/test_tube/integration_tests.rs index a2f00ed57..f805482c3 100644 --- a/contracts/external/cw-abc/src/test_tube/integration_tests.rs +++ b/contracts/external/cw-abc/src/test_tube/integration_tests.rs @@ -1,12 +1,4 @@ use super::test_env::{TestEnv, TestEnvBuilder}; -use crate::{ - abc::{ - ClosedConfig, CommonsPhaseConfig, HatchConfig, MinMax, OpenConfig, ReserveToken, - SupplyToken, - }, - msg::{CurveInfoResponse, ExecuteMsg, InstantiateMsg, QueryMsg}, -}; -use cosmwasm_std::{Coin, Decimal, Uint128}; use osmosis_test_tube::{Account, Module, OsmosisTestApp, Wasm}; use token_bindings::Metadata; diff --git a/contracts/external/cw-abc/src/test_tube/test_env.rs b/contracts/external/cw-abc/src/test_tube/test_env.rs index c53fb008a..0bd95cb42 100644 --- a/contracts/external/cw-abc/src/test_tube/test_env.rs +++ b/contracts/external/cw-abc/src/test_tube/test_env.rs @@ -364,20 +364,22 @@ impl<'a> CwAbc<'a> { let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let byte_code = std::fs::read( manifest_path + .join("..") .join("..") .join("..") .join("artifacts") - .join("cw_tokenfactory_issuer.wasm"), + .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_tokenfactory_issuer-aarch64.wasm"), + .join("cw_abc-aarch64.wasm"), ) .unwrap(), } From 93f16f68ec35b314e956d4703726ae098b2038da Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Wed, 30 Aug 2023 18:40:08 -0700 Subject: [PATCH 16/56] Fix tests, need to investigate why this broke --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 65af40139..43301950f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -741,7 +741,7 @@ dependencies = [ "getrandom", "integer-cbrt", "integer-sqrt", - "osmosis-std 0.16.2", + "osmosis-std", "osmosis-test-tube", "rust_decimal", "serde", From 50eff274dfb8ba53a794462b4dfc5b6e6cca28a6 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Thu, 31 Aug 2023 09:21:13 -0700 Subject: [PATCH 17/56] Minting tokens works! --- .../cw-abc/src/test_tube/integration_tests.rs | 16 +++++++++++++--- .../external/cw-abc/src/test_tube/test_env.rs | 7 +++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/contracts/external/cw-abc/src/test_tube/integration_tests.rs b/contracts/external/cw-abc/src/test_tube/integration_tests.rs index f805482c3..8a3b7fe25 100644 --- a/contracts/external/cw-abc/src/test_tube/integration_tests.rs +++ b/contracts/external/cw-abc/src/test_tube/integration_tests.rs @@ -1,6 +1,9 @@ +use crate::msg::ExecuteMsg; + use super::test_env::{TestEnv, TestEnvBuilder}; -use osmosis_test_tube::{Account, Module, OsmosisTestApp, Wasm}; -use token_bindings::Metadata; + +use cosmwasm_std::coins; +use osmosis_test_tube::OsmosisTestApp; #[test] fn test_happy_path() { @@ -8,5 +11,12 @@ fn test_happy_path() { let env = TestEnvBuilder::new(); let TestEnv { abc, accounts, .. } = env.default_setup(&app); - // TODO + + // Buy tokens + abc.execute(&ExecuteMsg::Buy {}, &coins(1000000, "uosmo"), &accounts[0]) + .unwrap(); + + // TODO query curve + + // TODO burn } diff --git a/contracts/external/cw-abc/src/test_tube/test_env.rs b/contracts/external/cw-abc/src/test_tube/test_env.rs index 0bd95cb42..d4d8fbd0b 100644 --- a/contracts/external/cw-abc/src/test_tube/test_env.rs +++ b/contracts/external/cw-abc/src/test_tube/test_env.rs @@ -12,8 +12,6 @@ use crate::{ }; use cosmwasm_std::{Coin, Decimal, Uint128}; -use cw_tokenfactory_issuer::msg::{DenomResponse, DenomUnit}; -use cw_utils::Duration; use dao_testing::test_tube::cw_tokenfactory_issuer::TokenfactoryIssuer; use osmosis_test_tube::{ osmosis_std::types::cosmos::bank::v1beta1::QueryAllBalancesRequest, @@ -28,7 +26,8 @@ use std::fmt::Debug; 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, @@ -121,7 +120,7 @@ impl TestEnvBuilder { decimals: 6, }, reserve: ReserveToken { - denom: JUNO.to_string(), + denom: RESERVE.to_string(), decimals: 6, }, phase_config: CommonsPhaseConfig { From fc19b74122eaca51f3685a9f9fa959cd3a9bff02 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Fri, 1 Sep 2023 13:38:46 -0700 Subject: [PATCH 18/56] Fixups, burning tokens still doesn't work --- contracts/external/cw-abc/src/commands.rs | 6 +-- contracts/external/cw-abc/src/contract.rs | 11 +++++ .../cw-abc/src/test_tube/integration_tests.rs | 47 ++++++++++++++++--- 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/contracts/external/cw-abc/src/commands.rs b/contracts/external/cw-abc/src/commands.rs index c661ccfb8..447597bd1 100644 --- a/contracts/external/cw-abc/src/commands.rs +++ b/contracts/external/cw-abc/src/commands.rs @@ -140,8 +140,8 @@ pub fn execute_sell( // Execute burn on the cw-tokenfactory-issuer contract CosmosMsg::::Wasm(WasmMsg::Execute { contract_addr: issuer_addr.to_string(), - msg: to_binary(&IssuerExecuteMsg::Mint { - to_address: info.sender.to_string(), + msg: to_binary(&IssuerExecuteMsg::Burn { + from_address: info.sender.to_string(), amount: burn_amount, })?, funds: vec![], @@ -178,8 +178,8 @@ pub fn execute_sell( }; Ok(Response::::new() - .add_messages(burn_msgs) .add_message(msg_send) + .add_messages(burn_msgs) .add_attribute("action", "burn") .add_attribute("from", burner) .add_attribute("amount", burn_amount) diff --git a/contracts/external/cw-abc/src/contract.rs b/contracts/external/cw-abc/src/contract.rs index 4a7c489d7..e9ce64d4d 100644 --- a/contracts/external/cw-abc/src/contract.rs +++ b/contracts/external/cw-abc/src/contract.rs @@ -244,6 +244,17 @@ pub fn reply( funds: vec![], }); + // Grant an allowance to burn + msgs.push(WasmMsg::Execute { + contract_addr: issuer_addr.clone(), + msg: to_binary(&IssuerExecuteMsg::SetBurnerAllowance { + address: env.contract.address.to_string(), + // TODO let this be capped + allowance: Uint128::MAX, + })?, + funds: vec![], + }); + // TODO fix metadata // // If metadata, set it by calling the contract // if let Some(metadata) = token_info.metadata { diff --git a/contracts/external/cw-abc/src/test_tube/integration_tests.rs b/contracts/external/cw-abc/src/test_tube/integration_tests.rs index 8a3b7fe25..54b1d1c4c 100644 --- a/contracts/external/cw-abc/src/test_tube/integration_tests.rs +++ b/contracts/external/cw-abc/src/test_tube/integration_tests.rs @@ -1,22 +1,57 @@ -use crate::msg::ExecuteMsg; +use crate::msg::{ + CommonsPhaseConfigResponse, CurveInfoResponse, DenomResponse, ExecuteMsg, QueryMsg, +}; use super::test_env::{TestEnv, TestEnvBuilder}; use cosmwasm_std::coins; -use osmosis_test_tube::OsmosisTestApp; +use cw_tokenfactory_issuer::msg::QueryMsg as IssuerQueryMsg; +use osmosis_test_tube::{Account, OsmosisTestApp}; #[test] fn test_happy_path() { let app = OsmosisTestApp::new(); - let env = TestEnvBuilder::new(); - let TestEnv { abc, accounts, .. } = env.default_setup(&app); + let builder = TestEnvBuilder::new(); + let env = builder.default_setup(&app); + let TestEnv { + ref abc, + ref accounts, + ref tf_issuer, + .. + } = env; // Buy tokens abc.execute(&ExecuteMsg::Buy {}, &coins(1000000, "uosmo"), &accounts[0]) .unwrap(); - // TODO query curve + // Query denom + let denom = tf_issuer + .query::(&IssuerQueryMsg::Denom {}) + .unwrap() + .denom; + println!("Denom {:?}", denom); - // TODO burn + // Query balances + let balances = env.bank().query_all_balances( + &osmosis_test_tube::osmosis_std::types::cosmos::bank::v1beta1::QueryAllBalancesRequest { + address: accounts[0].address(), + pagination: None, + }, + ).unwrap(); + println!("{:?}", balances); + + // Query curve + let curve_info: CurveInfoResponse = abc.query(&QueryMsg::CurveInfo {}).unwrap(); + println!("Curve {:?}", curve_info); + + let phase: CommonsPhaseConfigResponse = abc.query(&QueryMsg::PhaseConfig {}).unwrap(); + println!("Phase {:?}", phase); + + // Burn + abc.execute(&ExecuteMsg::Burn {}, &coins(900000, denom), &accounts[0]) + .unwrap(); + + let curve_info: CurveInfoResponse = abc.query(&QueryMsg::CurveInfo {}).unwrap(); + println!("Curve {:?}", curve_info); } From b7247f4b705ea5eb6359872b9563421b80bfb3af Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Mon, 30 Oct 2023 20:39:43 +0100 Subject: [PATCH 19/56] Rebase cleanup, fix up tests --- .../src/testing/test_tube_env.rs | 8 ++++++-- .../src/tests/test_tube/integration_tests.rs | 11 ++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/contracts/voting/dao-voting-cw721-staked/src/testing/test_tube_env.rs b/contracts/voting/dao-voting-cw721-staked/src/testing/test_tube_env.rs index c9d462213..226195f8a 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/testing/test_tube_env.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/testing/test_tube_env.rs @@ -24,8 +24,12 @@ use dao_testing::test_tube::{ }; use dao_voting::threshold::ActiveThreshold; use osmosis_test_tube::{ - osmosis_std::types::cosmwasm::wasm::v1::MsgExecuteContractResponse, Account, Bank, Module, - OsmosisTestApp, RunnerError, RunnerExecuteResult, RunnerResult, SigningAccount, Wasm, + 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; 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] From 6f80580a36eab87192349915a8f53f20c46ed744 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Thu, 2 Nov 2023 18:12:32 +0100 Subject: [PATCH 20/56] More cleanup for clippy --- .../external/cw-abc/src/test_tube/test_env.rs | 9 +++----- contracts/external/cw-abc/src/testing.rs | 21 +++++++------------ .../src/testing/test_tube_env.rs | 8 ++----- 3 files changed, 12 insertions(+), 26 deletions(-) diff --git a/contracts/external/cw-abc/src/test_tube/test_env.rs b/contracts/external/cw-abc/src/test_tube/test_env.rs index d4d8fbd0b..b68f16ac3 100644 --- a/contracts/external/cw-abc/src/test_tube/test_env.rs +++ b/contracts/external/cw-abc/src/test_tube/test_env.rs @@ -7,7 +7,7 @@ use crate::{ ClosedConfig, CommonsPhaseConfig, CurveType, HatchConfig, MinMax, OpenConfig, ReserveToken, SupplyToken, }, - msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, ContractError, }; @@ -15,11 +15,8 @@ use cosmwasm_std::{Coin, Decimal, Uint128}; use dao_testing::test_tube::cw_tokenfactory_issuer::TokenfactoryIssuer; use osmosis_test_tube::{ osmosis_std::types::cosmos::bank::v1beta1::QueryAllBalancesRequest, - osmosis_std::types::cosmwasm::wasm::v1::{ - MsgExecuteContractResponse, MsgMigrateContract, MsgMigrateContractResponse, - }, - Account, Bank, Module, OsmosisTestApp, RunnerError, RunnerExecuteResult, RunnerResult, - SigningAccount, Wasm, + osmosis_std::types::cosmwasm::wasm::v1::MsgExecuteContractResponse, Account, Bank, Module, + OsmosisTestApp, RunnerError, RunnerExecuteResult, SigningAccount, Wasm, }; use serde::de::DeserializeOwned; use std::fmt::Debug; diff --git a/contracts/external/cw-abc/src/testing.rs b/contracts/external/cw-abc/src/testing.rs index 2a9247557..38999f0b5 100644 --- a/contracts/external/cw-abc/src/testing.rs +++ b/contracts/external/cw-abc/src/testing.rs @@ -1,24 +1,17 @@ use cosmwasm_std::{ testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, - CosmosMsg, Decimal, DepsMut, OwnedDeps, Uint128, WasmMsg, + Decimal, DepsMut, OwnedDeps, Uint128, }; -use speculoos::prelude::*; use std::marker::PhantomData; -use token_bindings::{Metadata, TokenFactoryMsg, TokenFactoryQuery}; +use token_bindings::{Metadata, TokenFactoryQuery}; +use crate::abc::{ + ClosedConfig, CommonsPhaseConfig, CurveType, HatchConfig, MinMax, OpenConfig, ReserveToken, + SupplyToken, +}; +use crate::contract; use crate::contract::CwAbcResult; use crate::msg::InstantiateMsg; -use crate::queries::query_curve_info; -use crate::{ - abc::{ - ClosedConfig, CommonsPhaseConfig, CurveType, HatchConfig, MinMax, OpenConfig, ReserveToken, - SupplyToken, - }, - contract::instantiate, -}; -use crate::{contract, state::CURVE_TYPE}; - -const CREATOR: &str = "creator"; pub(crate) mod prelude { pub use super::{ diff --git a/contracts/voting/dao-voting-cw721-staked/src/testing/test_tube_env.rs b/contracts/voting/dao-voting-cw721-staked/src/testing/test_tube_env.rs index 226195f8a..c9d462213 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/testing/test_tube_env.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/testing/test_tube_env.rs @@ -24,12 +24,8 @@ use dao_testing::test_tube::{ }; 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, + osmosis_std::types::cosmwasm::wasm::v1::MsgExecuteContractResponse, Account, Bank, Module, + OsmosisTestApp, RunnerError, RunnerExecuteResult, RunnerResult, SigningAccount, Wasm, }; use serde::de::DeserializeOwned; use std::path::PathBuf; From b5c173b11870239b385d78f2c6fe1f79beaae104 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Mon, 6 Nov 2023 15:57:29 +0100 Subject: [PATCH 21/56] Attempt to fix burn --- contracts/external/cw-abc/src/commands.rs | 21 ++++++++++--------- .../cw-abc/src/test_tube/integration_tests.rs | 11 +++++++++- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/contracts/external/cw-abc/src/commands.rs b/contracts/external/cw-abc/src/commands.rs index 447597bd1..77892388e 100644 --- a/contracts/external/cw-abc/src/commands.rs +++ b/contracts/external/cw-abc/src/commands.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ - coins, ensure, to_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal as StdDecimal, DepsMut, Env, - MessageInfo, QuerierWrapper, Response, StdError, StdResult, Storage, Uint128, WasmMsg, + ensure, to_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal as StdDecimal, DepsMut, Env, + MessageInfo, QuerierWrapper, Response, StdError, StdResult, Storage, SubMsg, Uint128, WasmMsg, }; use cw_tokenfactory_issuer::msg::ExecuteMsg as IssuerExecuteMsg; use cw_utils::must_pay; @@ -120,8 +120,6 @@ pub fn execute_sell( info: MessageInfo, curve_fn: CurveFn, ) -> CwAbcResult { - let burner = info.sender.clone(); - let supply_denom = SUPPLY_DENOM.load(deps.storage)?; let burn_amount = must_pay(&info, &supply_denom)?; @@ -172,16 +170,19 @@ pub fn execute_sell( .map_err(StdError::overflow)?; // Now send the tokens to the sender - let msg_send = BankMsg::Send { - to_address: burner.to_string(), - amount: coins(released_reserve.u128(), curve_state.reserve_denom), - }; + let msg_send = SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: info.sender.to_string(), + amount: vec![Coin { + amount: released_reserve, + denom: curve_state.reserve_denom, + }], + })); Ok(Response::::new() - .add_message(msg_send) .add_messages(burn_msgs) + .add_submessage(msg_send) .add_attribute("action", "burn") - .add_attribute("from", burner) + .add_attribute("from", info.sender) .add_attribute("amount", burn_amount) .add_attribute("burned", released_reserve) .add_attribute("funded", taxed_amount)) diff --git a/contracts/external/cw-abc/src/test_tube/integration_tests.rs b/contracts/external/cw-abc/src/test_tube/integration_tests.rs index 54b1d1c4c..921d8a55b 100644 --- a/contracts/external/cw-abc/src/test_tube/integration_tests.rs +++ b/contracts/external/cw-abc/src/test_tube/integration_tests.rs @@ -48,8 +48,17 @@ fn test_happy_path() { let phase: CommonsPhaseConfigResponse = abc.query(&QueryMsg::PhaseConfig {}).unwrap(); println!("Phase {:?}", phase); + // Contract balances + let balances = env.bank().query_all_balances( + &osmosis_test_tube::osmosis_std::types::cosmos::bank::v1beta1::QueryAllBalancesRequest { + address: abc.contract_addr.to_string(), + pagination: None, + }, + ).unwrap(); + println!("{:?}", balances); + // Burn - abc.execute(&ExecuteMsg::Burn {}, &coins(900000, denom), &accounts[0]) + abc.execute(&ExecuteMsg::Burn {}, &coins(10000, denom), &accounts[0]) .unwrap(); let curve_info: CurveInfoResponse = abc.query(&QueryMsg::CurveInfo {}).unwrap(); From 8985177c1445f6cdcf77835dd2503e5773b4fe6a Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Mon, 6 Nov 2023 15:57:42 +0100 Subject: [PATCH 22/56] Note --- contracts/external/cw-abc/src/abc.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/external/cw-abc/src/abc.rs b/contracts/external/cw-abc/src/abc.rs index eef4c7db3..00c278b44 100644 --- a/contracts/external/cw-abc/src/abc.rs +++ b/contracts/external/cw-abc/src/abc.rs @@ -191,6 +191,7 @@ impl CommonsPhaseConfig { pub type CurveFn = Box Box>; +// TODO add S-curve and taylor series #[cw_serde] pub enum CurveType { /// Constant always returns `value * 10^-scale` as spot price From 4930042b95c777c3a8332117eb0cc76b51c468b0 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Thu, 9 Nov 2023 15:56:33 +0100 Subject: [PATCH 23/56] Clean up, clippy, workspace deps, notes --- Cargo.toml | 9 +++- contracts/external/cw-abc/Cargo.toml | 34 +++++--------- contracts/external/cw-abc/src/abc.rs | 1 - contracts/external/cw-abc/src/contract.rs | 55 +++++++++-------------- contracts/external/cw-abc/src/msg.rs | 1 - 5 files changed, 40 insertions(+), 60 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ac10933ca..f3871aef3 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,6 +54,9 @@ 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" @@ -60,6 +64,8 @@ osmosis-test-tube = "20.1.1" proc-macro2 = "1.0" prost = { version = "0.12.3", features = ["prost-derive"] } prost-types = { version = "0.12.3", default-features = false } +prost = "0.11" +rust_decimal = "1.14.3" quote = "1.0" rand = "0.8" schemars = "0.8" @@ -71,7 +77,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" } token-bindings = "0.11.0" diff --git a/contracts/external/cw-abc/Cargo.toml b/contracts/external/cw-abc/Cargo.toml index 7b169adae..cd2616c95 100644 --- a/contracts/external/cw-abc/Cargo.toml +++ b/contracts/external/cw-abc/Cargo.toml @@ -1,17 +1,16 @@ [package] name = "cw-abc" -version = "0.0.1" authors = [ "Ethan Frey ", "Jake Hartnell", "Adair ", ] -edition = { workspace = true } description = "Implements an Augmented Bonding Curve" +# Inherits license from previous work license = "Apache-2.0" +edition = { workspace = true } repository = { workspace = true } - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +version = "0.0.1" [lib] crate-type = ["cdylib", "rlib"] @@ -24,7 +23,7 @@ library = [] # cargo test --features "test-tube" test-tube = [] # when writing tests you may wish to enable test-tube as a default feature -default = ["test-tube"] +# default = ["test-tube"] [dependencies] cw-utils = { workspace = true } @@ -33,20 +32,18 @@ cw-storage-plus = { workspace = true } cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } thiserror = { workspace = true } -# TODO move to workspace -cw-address-like = "1.0.4" -rust_decimal = "1.14.3" -integer-sqrt = "0.1.5" -integer-cbrt = "0.1.2" -getrandom = { version = "0.2", features = ["js"] } +cw-address-like = { workspace = true } +rust_decimal = { workspace = true } +integer-sqrt = { workspace = true } +integer-cbrt = { workspace = true } +getrandom = { workspace = true, features = ["js"] } token-bindings = { workspace = true } cw-ownable = { workspace = true } cw-paginate-storage = { workspace = true } cw-tokenfactory-issuer = { workspace = true, features = ["library"] } [dev-dependencies] -# TODO move to workspace? -speculoos = "0.11.0" +speculoos = { workspace = true } anyhow = { workspace = true } cw-multi-test = { workspace = true } dao-testing = { workspace = true, features = ["test-tube"] } @@ -54,14 +51,3 @@ osmosis-std = { workspace = true } osmosis-test-tube = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } - -[profile.release] -rpath = false -lto = true -overflow-checks = true -opt-level = 3 -debug = false -debug-assertions = false -codegen-units = 1 -panic = 'abort' -incremental = false diff --git a/contracts/external/cw-abc/src/abc.rs b/contracts/external/cw-abc/src/abc.rs index 00c278b44..55cd3e62b 100644 --- a/contracts/external/cw-abc/src/abc.rs +++ b/contracts/external/cw-abc/src/abc.rs @@ -139,7 +139,6 @@ pub struct CommonsPhaseConfig { pub enum CommonsPhase { Hatch, Open, - // TODO: should we allow for a closed phase? Closed, } diff --git a/contracts/external/cw-abc/src/contract.rs b/contracts/external/cw-abc/src/contract.rs index e9ce64d4d..4eb098cdb 100644 --- a/contracts/external/cw-abc/src/contract.rs +++ b/contracts/external/cw-abc/src/contract.rs @@ -231,29 +231,28 @@ pub fn reply( SUPPLY_DENOM.save(deps.storage, &denom)?; // Msgs to be executed to finalize setup - let mut msgs: Vec = vec![]; - - // Grant an allowance to mint - msgs.push(WasmMsg::Execute { - contract_addr: issuer_addr.clone(), - msg: to_binary(&IssuerExecuteMsg::SetMinterAllowance { - address: env.contract.address.to_string(), - // TODO let this be capped - allowance: Uint128::MAX, - })?, - funds: vec![], - }); - - // Grant an allowance to burn - msgs.push(WasmMsg::Execute { - contract_addr: issuer_addr.clone(), - msg: to_binary(&IssuerExecuteMsg::SetBurnerAllowance { - address: env.contract.address.to_string(), - // TODO let this be capped - allowance: Uint128::MAX, - })?, - funds: vec![], - }); + let msgs: Vec = vec![ + // Grant an allowance to mint + WasmMsg::Execute { + contract_addr: issuer_addr.clone(), + msg: to_binary(&IssuerExecuteMsg::SetMinterAllowance { + address: env.contract.address.to_string(), + // TODO let this be capped + allowance: Uint128::MAX, + })?, + funds: vec![], + }, + // Grant an allowance to burn + WasmMsg::Execute { + contract_addr: issuer_addr.clone(), + msg: to_binary(&IssuerExecuteMsg::SetBurnerAllowance { + address: env.contract.address.to_string(), + // TODO let this be capped + allowance: Uint128::MAX, + })?, + funds: vec![], + }, + ]; // TODO fix metadata // // If metadata, set it by calling the contract @@ -291,16 +290,6 @@ pub fn reply( // }); // } - // TODO who should own the token contract? - // // Update issuer contract owner to be the DAO - // msgs.push(WasmMsg::Execute { - // contract_addr: issuer_addr.clone(), - // msg: to_binary(&IssuerExecuteMsg::ChangeContractOwner { - // new_owner: dao.to_string(), - // })?, - // funds: vec![], - // }); - Ok(Response::new() .add_attribute("cw-tokenfactory-issuer-address", issuer_addr) .add_attribute("denom", denom) diff --git a/contracts/external/cw-abc/src/msg.rs b/contracts/external/cw-abc/src/msg.rs index a785fcc3f..848268288 100644 --- a/contracts/external/cw-abc/src/msg.rs +++ b/contracts/external/cw-abc/src/msg.rs @@ -62,7 +62,6 @@ pub enum ExecuteMsg { UpdatePhaseConfig(UpdatePhaseConfigMsg), } -// TODO token contract query #[cw_ownable::cw_ownable_query] #[cw_serde] #[derive(QueryResponses)] From ecdfb9dd45b9644c1933f3a7ba2414a28abce226 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Sun, 12 Nov 2023 22:19:51 +0100 Subject: [PATCH 24/56] Happy path test, make notes --- contracts/external/cw-abc/src/abc.rs | 2 + contracts/external/cw-abc/src/commands.rs | 5 +- contracts/external/cw-abc/src/contract.rs | 2 +- .../cw-abc/src/test_tube/integration_tests.rs | 156 +++++++++++++++--- .../external/cw-abc/src/test_tube/test_env.rs | 58 ++----- 5 files changed, 149 insertions(+), 74 deletions(-) diff --git a/contracts/external/cw-abc/src/abc.rs b/contracts/external/cw-abc/src/abc.rs index 55cd3e62b..0adb044ce 100644 --- a/contracts/external/cw-abc/src/abc.rs +++ b/contracts/external/cw-abc/src/abc.rs @@ -40,6 +40,7 @@ pub struct HatchConfig { pub initial_raise: MinMax, /// The initial price (p0) per reserve token /// TODO: initial price is not implemented yet + /// TODO: do we need this or is it just calculated? pub initial_price: Uint128, /// The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool pub initial_allocation_ratio: StdDecimal, @@ -86,6 +87,7 @@ impl HatchConfig { #[cw_serde] pub struct OpenConfig { + // TODO isn't this the same as initial_allocation_ratio? Maybe clearer to just call it an entrance fee? /// Percentage of capital put into the Reserve Pool during the Open phase pub allocation_percentage: StdDecimal, /// Exit taxation ratio diff --git a/contracts/external/cw-abc/src/commands.rs b/contracts/external/cw-abc/src/commands.rs index 77892388e..e807091d8 100644 --- a/contracts/external/cw-abc/src/commands.rs +++ b/contracts/external/cw-abc/src/commands.rs @@ -39,7 +39,7 @@ pub fn execute_buy( // Check if the initial_raise max has been met if curve_state.reserve + payment >= hatch_config.initial_raise.max { - // Transition to the Open phase, the hatchers' tokens are now vesting + // Transition to the Open phase phase = CommonsPhase::Open; PHASE.save(deps.storage, &phase)?; } @@ -54,10 +54,11 @@ pub fn execute_buy( } }; - // calculate how many tokens can be purchased with this and mint them + // Calculate how many tokens can be purchased with this and mint them let curve = curve_fn(curve_state.clone().decimals); curve_state.reserve += reserved; curve_state.funding += funded; + // Calculate the supply based on the reserve let new_supply = curve.supply(curve_state.reserve); let minted = new_supply diff --git a/contracts/external/cw-abc/src/contract.rs b/contracts/external/cw-abc/src/contract.rs index 4eb098cdb..8afb920e3 100644 --- a/contracts/external/cw-abc/src/contract.rs +++ b/contracts/external/cw-abc/src/contract.rs @@ -89,7 +89,7 @@ pub fn instantiate( PHASE_CONFIG.save(deps.storage, &phase_config)?; - // TODO don't hardcode this? + // TODO don't hardcode this? Make it configurable? Hatch config can be optional PHASE.save(deps.storage, &CommonsPhase::Hatch)?; // Initialize owner to sender diff --git a/contracts/external/cw-abc/src/test_tube/integration_tests.rs b/contracts/external/cw-abc/src/test_tube/integration_tests.rs index 921d8a55b..0f3b4280f 100644 --- a/contracts/external/cw-abc/src/test_tube/integration_tests.rs +++ b/contracts/external/cw-abc/src/test_tube/integration_tests.rs @@ -1,12 +1,14 @@ -use crate::msg::{ - CommonsPhaseConfigResponse, CurveInfoResponse, DenomResponse, ExecuteMsg, QueryMsg, +use crate::{ + abc::{ClosedConfig, CommonsPhase, CommonsPhaseConfig, HatchConfig, MinMax, OpenConfig}, + msg::{CommonsPhaseConfigResponse, CurveInfoResponse, DenomResponse, ExecuteMsg, QueryMsg}, }; -use super::test_env::{TestEnv, TestEnvBuilder}; +use super::test_env::{TestEnv, TestEnvBuilder, RESERVE}; -use cosmwasm_std::coins; +use cosmwasm_std::{coins, Decimal, Uint128}; use cw_tokenfactory_issuer::msg::QueryMsg as IssuerQueryMsg; -use osmosis_test_tube::{Account, OsmosisTestApp}; +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() { @@ -22,7 +24,7 @@ fn test_happy_path() { } = env; // Buy tokens - abc.execute(&ExecuteMsg::Buy {}, &coins(1000000, "uosmo"), &accounts[0]) + abc.execute(&ExecuteMsg::Buy {}, &coins(1000, RESERVE), &accounts[0]) .unwrap(); // Query denom @@ -30,37 +32,137 @@ fn test_happy_path() { .query::(&IssuerQueryMsg::Denom {}) .unwrap() .denom; - println!("Denom {:?}", denom); // Query balances - let balances = env.bank().query_all_balances( - &osmosis_test_tube::osmosis_std::types::cosmos::bank::v1beta1::QueryAllBalancesRequest { + let user_balance = env + .bank() + .query_balance(&QueryBalanceRequest { address: accounts[0].address(), - pagination: None, - }, - ).unwrap(); - println!("{:?}", balances); + 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: "1000".to_string(), + }) + ); // Query curve let curve_info: CurveInfoResponse = abc.query(&QueryMsg::CurveInfo {}).unwrap(); - println!("Curve {:?}", curve_info); + assert_eq!( + curve_info, + CurveInfoResponse { + reserve: Uint128::new(900), + supply: Uint128::new(9000), + funding: Uint128::new(100), + spot_price: Decimal::percent(10u64), + reserve_denom: RESERVE.to_string(), + } + ); + // Query phase let phase: CommonsPhaseConfigResponse = abc.query(&QueryMsg::PhaseConfig {}).unwrap(); - println!("Phase {:?}", phase); - - // Contract balances - let balances = env.bank().query_all_balances( - &osmosis_test_tube::osmosis_std::types::cosmos::bank::v1beta1::QueryAllBalancesRequest { - address: abc.contract_addr.to_string(), - pagination: None, - }, - ).unwrap(); - println!("{:?}", balances); + assert_eq!(phase.phase, CommonsPhase::Hatch); + assert_eq!( + phase.phase_config, + CommonsPhaseConfig { + hatch: HatchConfig { + initial_raise: MinMax { + min: Uint128::one(), + max: Uint128::from(1000000u128), + }, + initial_price: Uint128::one(), + initial_allocation_ratio: Decimal::percent(10u64), + exit_tax: Decimal::percent(10u64), + }, + open: OpenConfig { + allocation_percentage: Decimal::percent(10u64), + exit_tax: Decimal::percent(10u64), + }, + closed: ClosedConfig {}, + } + ); // Burn - abc.execute(&ExecuteMsg::Burn {}, &coins(10000, denom), &accounts[0]) - .unwrap(); + abc.execute( + &ExecuteMsg::Burn {}, + &coins(100, denom.clone()), + &accounts[0], + ) + .unwrap(); + // Query curve let curve_info: CurveInfoResponse = abc.query(&QueryMsg::CurveInfo {}).unwrap(); - println!("Curve {:?}", curve_info); + assert_eq!( + curve_info, + CurveInfoResponse { + reserve: Uint128::new(890), + supply: Uint128::new(8900), + funding: Uint128::new(110), + 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: "8800".to_string(), + }) + ); + assert_eq!( + contract_balance.balance, + Some(Coin { + denom: RESERVE.to_string(), + amount: "990".to_string(), + }) + ); + + // Buy enough tokens to end the hatch phase + abc.execute( + &ExecuteMsg::Buy {}, + &coins(1000000000, RESERVE), + &accounts[0], + ) + .unwrap(); + + // Contract is now in open phase + let phase: CommonsPhaseConfigResponse = abc.query(&QueryMsg::PhaseConfig {}).unwrap(); + assert_eq!(phase.phase, CommonsPhase::Open); } diff --git a/contracts/external/cw-abc/src/test_tube/test_env.rs b/contracts/external/cw-abc/src/test_tube/test_env.rs index b68f16ac3..2dd020c24 100644 --- a/contracts/external/cw-abc/src/test_tube/test_env.rs +++ b/contracts/external/cw-abc/src/test_tube/test_env.rs @@ -23,6 +23,7 @@ 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"; @@ -87,22 +88,16 @@ impl<'a> TestEnv<'a> { } } -pub struct TestEnvBuilder { - pub accounts: Vec, - pub instantiate_msg: Option, -} +pub struct TestEnvBuilder {} impl TestEnvBuilder { pub fn new() -> Self { - Self { - accounts: vec![], - instantiate_msg: None, - } + Self {} } pub fn default_setup(self, app: &'_ OsmosisTestApp) -> TestEnv<'_> { let accounts = app - .init_accounts(&[Coin::new(1000000000000000u128, "uosmo")], 10) + .init_accounts(&[Coin::new(1000000000000000u128, RESERVE)], 10) .unwrap(); let issuer_id = TokenfactoryIssuer::upload(app, &accounts[0]).unwrap(); @@ -128,7 +123,7 @@ impl TestEnvBuilder { }, initial_price: Uint128::one(), initial_allocation_ratio: Decimal::percent(10u64), - exit_tax: Decimal::zero(), + exit_tax: Decimal::percent(10u64), }, open: OpenConfig { allocation_percentage: Decimal::percent(10u64), @@ -158,28 +153,18 @@ impl TestEnvBuilder { } } - pub fn build(self, app: &'_ OsmosisTestApp) -> TestEnv<'_> { - let accounts = self.accounts; + pub fn setup(self, app: &'_ OsmosisTestApp, msg: InstantiateMsg) -> TestEnv<'_> { + let accounts = app + .init_accounts(&[Coin::new(1000000000000000u128, RESERVE)], 10) + .unwrap(); - let abc = CwAbc::deploy( - app, - self.instantiate_msg - .as_ref() - .expect("instantiate msg not set"), - &accounts[0], - ) - .unwrap(); + let issuer_id = TokenfactoryIssuer::upload(app, &accounts[0]).unwrap(); + + let abc = CwAbc::deploy(app, &msg, &accounts[0]).unwrap(); let issuer_addr = CwAbc::query(&abc, &QueryMsg::TokenContract {}).unwrap(); - let tf_issuer = TokenfactoryIssuer::new_with_values( - app, - self.instantiate_msg - .expect("instantiate msg not set") - .token_issuer_code_id, - issuer_addr, - ) - .unwrap(); + let tf_issuer = TokenfactoryIssuer::new_with_values(app, issuer_id, issuer_addr).unwrap(); TestEnv { app, @@ -192,21 +177,6 @@ impl TestEnvBuilder { 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 - } } pub fn assert_contract_err(expected: ContractError, actual: RunnerError) { @@ -237,7 +207,7 @@ impl<'a> CwAbc<'a> { signer: &SigningAccount, ) -> Result { let wasm = Wasm::new(app); - let token_creation_fee = Coin::new(10000000, "uosmo"); + let token_creation_fee = Coin::new(10000000, RESERVE); let code_id = wasm .store_code(&Self::get_wasm_byte_code(), None, signer)? From 5fc2de805d1c15ced8f0a62d63805e8bf42f7849 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Thu, 16 Nov 2023 15:04:37 +0100 Subject: [PATCH 25/56] Refactor update phase config, fix metadata, clean up --- Cargo.lock | 1 + contracts/external/cw-abc/Cargo.toml | 9 +- contracts/external/cw-abc/src/abc.rs | 7 +- contracts/external/cw-abc/src/commands.rs | 93 +++++++++++------ contracts/external/cw-abc/src/contract.rs | 115 +++++++++------------- contracts/external/cw-abc/src/msg.rs | 3 +- contracts/external/cw-abc/src/testing.rs | 18 ++-- 7 files changed, 128 insertions(+), 118 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 43301950f..0fd7a8ba9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -737,6 +737,7 @@ dependencies = [ "cw-tokenfactory-issuer", "cw-utils 1.0.3", "cw2 1.1.2", + "dao-interface", "dao-testing", "getrandom", "integer-cbrt", diff --git a/contracts/external/cw-abc/Cargo.toml b/contracts/external/cw-abc/Cargo.toml index cd2616c95..1676c2fa5 100644 --- a/contracts/external/cw-abc/Cargo.toml +++ b/contracts/external/cw-abc/Cargo.toml @@ -31,16 +31,17 @@ cw2 = { workspace = true } cw-storage-plus = { workspace = true } cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } -thiserror = { 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 } rust_decimal = { workspace = true } integer-sqrt = { workspace = true } integer-cbrt = { workspace = true } getrandom = { workspace = true, features = ["js"] } +thiserror = { workspace = true } token-bindings = { workspace = true } -cw-ownable = { workspace = true } -cw-paginate-storage = { workspace = true } -cw-tokenfactory-issuer = { workspace = true, features = ["library"] } [dev-dependencies] speculoos = { workspace = true } diff --git a/contracts/external/cw-abc/src/abc.rs b/contracts/external/cw-abc/src/abc.rs index 0adb044ce..3014fbd74 100644 --- a/contracts/external/cw-abc/src/abc.rs +++ b/contracts/external/cw-abc/src/abc.rs @@ -1,16 +1,16 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{ensure, Decimal as StdDecimal, Uint128}; +use dao_interface::token::NewDenomMetadata; use crate::curves::{decimal, Constant, Curve, DecimalPlaces, Linear, SquareRoot}; use crate::ContractError; -use token_bindings::Metadata; #[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, + 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, @@ -87,8 +87,8 @@ impl HatchConfig { #[cw_serde] pub struct OpenConfig { - // TODO isn't this the same as initial_allocation_ratio? Maybe clearer to just call it an entrance fee? /// Percentage of capital put into the Reserve Pool during the Open phase + /// when buying from the curve. pub allocation_percentage: StdDecimal, /// Exit taxation ratio pub exit_tax: StdDecimal, @@ -129,6 +129,7 @@ impl ClosedConfig { 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. diff --git a/contracts/external/cw-abc/src/commands.rs b/contracts/external/cw-abc/src/commands.rs index e807091d8..0c3012a8b 100644 --- a/contracts/external/cw-abc/src/commands.rs +++ b/contracts/external/cw-abc/src/commands.rs @@ -10,18 +10,17 @@ use token_bindings::{TokenFactoryMsg, TokenFactoryQuery}; use crate::abc::{CommonsPhase, CurveFn, MinMax}; use crate::contract::CwAbcResult; +use crate::msg::UpdatePhaseConfigMsg; use crate::state::{ - CURVE_STATE, DONATIONS, HATCHERS, HATCHER_ALLOWLIST, PHASE, PHASE_CONFIG, SUPPLY_DENOM, - TOKEN_ISSUER_CONTRACT, + CURVE_STATE, CURVE_TYPE, DONATIONS, HATCHERS, HATCHER_ALLOWLIST, PHASE, PHASE_CONFIG, + SUPPLY_DENOM, TOKEN_ISSUER_CONTRACT, }; use crate::ContractError; -pub fn execute_buy( - deps: DepsMut, - _env: Env, - info: MessageInfo, - curve_fn: CurveFn, -) -> CwAbcResult { +pub fn execute_buy(deps: DepsMut, _env: Env, info: MessageInfo) -> CwAbcResult { + let curve_type = CURVE_TYPE.load(deps.storage)?; + let curve_fn = curve_type.to_curve_fn(); + let mut curve_state = CURVE_STATE.load(deps.storage)?; let payment = must_pay(&info, &curve_state.reserve_denom)?; @@ -115,12 +114,10 @@ fn update_hatcher_contributions( Ok(()) } -pub fn execute_sell( - deps: DepsMut, - _env: Env, - info: MessageInfo, - curve_fn: CurveFn, -) -> CwAbcResult { +pub fn execute_sell(deps: DepsMut, _env: Env, info: MessageInfo) -> CwAbcResult { + let curve_type = CURVE_TYPE.load(deps.storage)?; + let curve_fn = curve_type.to_curve_fn(); + let supply_denom = SUPPLY_DENOM.load(deps.storage)?; let burn_amount = must_pay(&info, &supply_denom)?; @@ -281,35 +278,71 @@ pub fn update_hatch_allowlist( Ok(Response::new().add_attributes(vec![("action", "update_hatch_allowlist")])) } -/// Update the hatch config -pub fn update_hatch_config( +pub fn update_phase_config( deps: DepsMut, _env: Env, info: MessageInfo, - initial_raise: Option, - initial_allocation_ratio: Option, + update_phase_config_msg: UpdatePhaseConfigMsg, ) -> CwAbcResult { // Assert that the sender is the contract owner cw_ownable::assert_owner(deps.storage, &info.sender)?; - // Ensure we're in the Hatch phase - PHASE.load(deps.storage)?.expect_hatch()?; + // 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)?; - // Update the hatch config if new values are provided - if let Some(initial_raise) = initial_raise { - phase_config.hatch.initial_raise = initial_raise; - } - if let Some(initial_allocation_ratio) = initial_allocation_ratio { - phase_config.hatch.initial_allocation_ratio = initial_allocation_ratio; - } + match update_phase_config_msg { + UpdatePhaseConfigMsg::Hatch { + exit_tax, + initial_raise, + initial_allocation_ratio, + } => { + // Check we are in the hatch phase + phase.expect_hatch()?; + + // Update the hatch config if new values are provided + if let Some(initial_raise) = initial_raise { + phase_config.hatch.initial_raise = initial_raise; + } + if let Some(initial_allocation_ratio) = initial_allocation_ratio { + phase_config.hatch.initial_allocation_ratio = initial_allocation_ratio; + } + if let Some(exit_tax) = exit_tax { + phase_config.hatch.exit_tax = exit_tax; + } - phase_config.hatch.validate()?; - PHASE_CONFIG.save(deps.storage, &phase_config)?; + // Validate config + phase_config.hatch.validate()?; + PHASE_CONFIG.save(deps.storage, &phase_config)?; - Ok(Response::new().add_attribute("action", "update_hatch_config")) + Ok(Response::new().add_attribute("action", "update_hatch_phase_config")) + } + UpdatePhaseConfigMsg::Open { + exit_tax, + allocation_percentage, + } => { + // Check we are in the open phase + phase.expect_open()?; + + // Update the hatch config if new values are provided + if let Some(allocation_percentage) = allocation_percentage { + phase_config.open.allocation_percentage = allocation_percentage; + } + if let Some(exit_tax) = exit_tax { + phase_config.hatch.exit_tax = exit_tax; + } + + // 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 ownership of the contract diff --git a/contracts/external/cw-abc/src/contract.rs b/contracts/external/cw-abc/src/contract.rs index 8afb920e3..ba3dd78ef 100644 --- a/contracts/external/cw-abc/src/contract.rs +++ b/contracts/external/cw-abc/src/contract.rs @@ -6,7 +6,7 @@ use cosmwasm_std::{ }; use cw2::set_contract_version; use cw_tokenfactory_issuer::msg::{ - ExecuteMsg as IssuerExecuteMsg, InstantiateMsg as IssuerInstantiateMsg, + DenomUnit, ExecuteMsg as IssuerExecuteMsg, InstantiateMsg as IssuerInstantiateMsg, Metadata, }; use cw_utils::parse_reply_instantiate_data; use std::collections::HashSet; @@ -120,44 +120,17 @@ pub fn execute( env: Env, info: MessageInfo, msg: ExecuteMsg, -) -> CwAbcResult { - // 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_execute(deps, env, info, 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_execute( - deps: DepsMut, - env: Env, - info: MessageInfo, - msg: ExecuteMsg, - curve_fn: CurveFn, ) -> CwAbcResult { match msg { - ExecuteMsg::Buy {} => commands::execute_buy(deps, env, info, curve_fn), - ExecuteMsg::Burn {} => commands::execute_sell(deps, env, info, curve_fn), + ExecuteMsg::Buy {} => commands::execute_buy(deps, env, info), + ExecuteMsg::Burn {} => commands::execute_sell(deps, env, info), ExecuteMsg::Donate {} => commands::execute_donate(deps, env, info), ExecuteMsg::UpdateHatchAllowlist { to_add, to_remove } => { commands::update_hatch_allowlist(deps, info, to_add, to_remove) } - ExecuteMsg::UpdatePhaseConfig(update) => match update { - UpdatePhaseConfigMsg::Hatch { - initial_raise, - initial_allocation_ratio, - } => commands::update_hatch_config( - deps, - env, - info, - initial_raise, - initial_allocation_ratio, - ), - _ => todo!(), - }, + ExecuteMsg::UpdatePhaseConfig(update_msg) => { + commands::update_phase_config(deps, env, info, update_msg) + } ExecuteMsg::UpdateOwnership(action) => { commands::update_ownership(deps, &env, &info, action) } @@ -231,13 +204,15 @@ pub fn reply( SUPPLY_DENOM.save(deps.storage, &denom)?; // Msgs to be executed to finalize setup - let msgs: Vec = vec![ + let mut msgs: Vec = vec![ // Grant an allowance to mint WasmMsg::Execute { contract_addr: issuer_addr.clone(), msg: to_binary(&IssuerExecuteMsg::SetMinterAllowance { address: env.contract.address.to_string(), - // TODO let this be capped + // 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![], @@ -247,48 +222,46 @@ pub fn reply( contract_addr: issuer_addr.clone(), msg: to_binary(&IssuerExecuteMsg::SetBurnerAllowance { address: env.contract.address.to_string(), - // TODO let this be capped allowance: Uint128::MAX, })?, funds: vec![], }, ]; - // TODO fix metadata - // // If metadata, set it by calling the contract - // if let Some(metadata) = token_info.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![token_info.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_binary(&IssuerExecuteMsg::SetDenomMetadata { - // metadata: Metadata { - // description: metadata.description, - // denom_units, - // base: denom.clone(), - // display: metadata.display, - // name: metadata.name, - // symbol: metadata.symbol, - // }, - // })?, - // funds: vec![], - // }); - // } + // If metadata, set it by calling the contract + if let Some(metadata) = token_info.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![token_info.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_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) diff --git a/contracts/external/cw-abc/src/msg.rs b/contracts/external/cw-abc/src/msg.rs index 848268288..b1b04ce52 100644 --- a/contracts/external/cw-abc/src/msg.rs +++ b/contracts/external/cw-abc/src/msg.rs @@ -30,13 +30,14 @@ pub struct InstantiateMsg { pub enum UpdatePhaseConfigMsg { /// Update the hatch phase configuration Hatch { + exit_tax: Option, initial_raise: Option, initial_allocation_ratio: Option, }, /// Update the open phase configuration Open { exit_tax: Option, - reserve_ratio: Option, + allocation_percentage: Option, }, /// Update the closed phase configuration Closed {}, diff --git a/contracts/external/cw-abc/src/testing.rs b/contracts/external/cw-abc/src/testing.rs index 38999f0b5..f3ebcc943 100644 --- a/contracts/external/cw-abc/src/testing.rs +++ b/contracts/external/cw-abc/src/testing.rs @@ -2,8 +2,9 @@ use cosmwasm_std::{ testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, Decimal, DepsMut, OwnedDeps, Uint128, }; +use dao_interface::token::NewDenomMetadata; use std::marker::PhantomData; -use token_bindings::{Metadata, TokenFactoryQuery}; +use token_bindings::TokenFactoryQuery; use crate::abc::{ ClosedConfig, CommonsPhaseConfig, CurveType, HatchConfig, MinMax, OpenConfig, ReserveToken, @@ -29,14 +30,13 @@ pub const _TEST_BUYER: &str = "buyer"; pub const TEST_SUPPLY_DENOM: &str = "subdenom"; -pub fn default_supply_metadata() -> Metadata { - Metadata { - name: Some("Bonded".to_string()), - symbol: Some("EPOXY".to_string()), - description: None, - denom_units: vec![], - base: None, - display: None, +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, } } From ea21f4f95f73acb51adbee43d5b8421b0164a321 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Thu, 16 Nov 2023 16:47:40 +0100 Subject: [PATCH 26/56] Contribution limits --- contracts/external/cw-abc/src/abc.rs | 56 +++++++++++++- contracts/external/cw-abc/src/commands.rs | 37 +++++++-- contracts/external/cw-abc/src/error.rs | 39 +++++----- contracts/external/cw-abc/src/msg.rs | 3 + .../cw-abc/src/test_tube/integration_tests.rs | 77 ++++++++++++++++--- .../external/cw-abc/src/test_tube/test_env.rs | 29 ++++--- contracts/external/cw-abc/src/testing.rs | 4 + 7 files changed, 196 insertions(+), 49 deletions(-) diff --git a/contracts/external/cw-abc/src/abc.rs b/contracts/external/cw-abc/src/abc.rs index 3014fbd74..67febb7df 100644 --- a/contracts/external/cw-abc/src/abc.rs +++ b/contracts/external/cw-abc/src/abc.rs @@ -14,6 +14,8 @@ pub struct SupplyToken { /// Number of decimal places for the supply token, needed for proper curve math. /// Default for token factory is 6 pub decimals: u8, + // TODO max supply + // pub max_supply: Uint128, } #[cw_serde] @@ -34,8 +36,8 @@ pub struct MinMax { #[cw_serde] pub struct HatchConfig { - // /// TODO: The minimum and maximum contribution amounts (min, max) in the reserve token - /// pub contribution_limits: MinMax, + /// 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 price (p0) per reserve token @@ -58,6 +60,14 @@ impl HatchConfig { ) ); + ensure!( + self.contribution_limits.max <= self.initial_raise.max, + ContractError::HatchPhaseConfigError( + "Max contribution limit cannot be greater than the maximum initial raise." + .to_string() + ) + ); + ensure!( !self.initial_price.is_zero(), ContractError::HatchPhaseConfigError( @@ -66,6 +76,8 @@ impl HatchConfig { ); // TODO: define better values + // Q: is zero valid for initial allocation value? Isn't the whole point of the + // hatch phase to initialize the DAO treasury? ensure!( self.initial_allocation_ratio <= StdDecimal::percent(100u64), ContractError::HatchPhaseConfigError( @@ -73,11 +85,10 @@ impl HatchConfig { ) ); - // TODO: define better values ensure!( self.exit_tax <= StdDecimal::percent(100u64), ContractError::HatchPhaseConfigError( - "Exit taxation percentage must be between 0 and 100.".to_string() + "Exit taxation percentage must be less than or equal to 100.".to_string() ) ); @@ -228,3 +239,40 @@ impl CurveType { } } } + +#[cfg(test)] +mod unit_tests { + use super::*; + + #[test] + fn validate_contribution_limit_not_gt_initial_raise() { + let phase_config = CommonsPhaseConfig { + hatch: HatchConfig { + contribution_limits: MinMax { + min: Uint128::one(), + max: Uint128::MAX, + }, + initial_raise: MinMax { + min: Uint128::one(), + max: Uint128::from(1000000u128), + }, + initial_price: Uint128::one(), + initial_allocation_ratio: StdDecimal::percent(10u64), + exit_tax: StdDecimal::percent(10u64), + }, + open: OpenConfig { + allocation_percentage: StdDecimal::percent(10u64), + exit_tax: StdDecimal::percent(10u64), + }, + closed: ClosedConfig {}, + }; + let err = phase_config.validate().unwrap_err(); + assert_eq!( + err, + ContractError::HatchPhaseConfigError( + "Max contribution limit cannot be greater than the maximum initial raise." + .to_string() + ) + ) + } +} diff --git a/contracts/external/cw-abc/src/commands.rs b/contracts/external/cw-abc/src/commands.rs index 0c3012a8b..3083378dc 100644 --- a/contracts/external/cw-abc/src/commands.rs +++ b/contracts/external/cw-abc/src/commands.rs @@ -34,7 +34,25 @@ pub fn execute_buy(deps: DepsMut, _env: Env, info: MessageInf let hatch_config = phase_config.hatch; // Check that the potential hatcher is allowlisted assert_allowlisted(deps.storage, &info.sender)?; - update_hatcher_contributions(deps.storage, &info.sender, payment)?; + + // Update hatcher contribution + let contribution = update_hatcher_contributions(deps.storage, &info.sender, payment)?; + + // Check contribtuion is above minimum + if contribution < hatch_config.contribution_limits.min { + return Err(ContractError::ContributionLimit { + min: hatch_config.contribution_limits.min, + max: hatch_config.contribution_limits.max, + }); + } + + // Check contribution is below maximum + if 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 curve_state.reserve + payment >= hatch_config.initial_raise.max { @@ -77,6 +95,8 @@ pub fn execute_buy(deps: DepsMut, _env: Env, info: MessageInf funds: vec![], }; + // TODO check that the minted amount has not exceeded the max supply + Ok(Response::new() .add_message(mint_msg) .add_attribute("action", "buy") @@ -101,7 +121,7 @@ fn update_hatcher_contributions( storage: &mut dyn Storage, hatcher: &Addr, contribution: Uint128, -) -> StdResult<()> { +) -> StdResult { HATCHERS.update(storage, hatcher, |amount| -> StdResult<_> { match amount { Some(mut amount) => { @@ -110,8 +130,7 @@ fn update_hatcher_contributions( } None => Ok(contribution), } - })?; - Ok(()) + }) } pub fn execute_sell(deps: DepsMut, _env: Env, info: MessageInfo) -> CwAbcResult { @@ -298,20 +317,24 @@ pub fn update_phase_config( exit_tax, initial_raise, initial_allocation_ratio, + 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(exit_tax) = exit_tax { + phase_config.hatch.exit_tax = exit_tax; + } if let Some(initial_raise) = initial_raise { phase_config.hatch.initial_raise = initial_raise; } if let Some(initial_allocation_ratio) = initial_allocation_ratio { phase_config.hatch.initial_allocation_ratio = initial_allocation_ratio; } - if let Some(exit_tax) = exit_tax { - phase_config.hatch.exit_tax = exit_tax; - } // Validate config phase_config.hatch.validate()?; diff --git a/contracts/external/cw-abc/src/error.rs b/contracts/external/cw-abc/src/error.rs index eae842541..369f2767d 100644 --- a/contracts/external/cw-abc/src/error.rs +++ b/contracts/external/cw-abc/src/error.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::StdError; +use cosmwasm_std::{StdError, Uint128}; use cw_utils::{ParseReplyError, PaymentError}; use thiserror::Error; @@ -13,14 +13,26 @@ pub enum ContractError { #[error(transparent)] ParseReplyError(#[from] ParseReplyError), + #[error("{0}")] + Ownership(#[from] cw_ownable::OwnershipError), + + #[error("The commons is closed to new contributions")] + CommonsClosed {}, + + #[error("Contribution must be less than or equal to {max} and greater than or equal to {min}")] + ContributionLimit { min: Uint128, max: Uint128 }, + + #[error("Selling is disabled during the hatch phase")] + HatchSellingDisabled {}, + #[error("Invalid subdenom: {subdenom:?}")] InvalidSubdenom { subdenom: String }, - #[error("{0}")] - Ownership(#[from] cw_ownable::OwnershipError), + #[error("Invalid phase, expected {expected:?}, actual {actual:?}")] + InvalidPhase { expected: String, actual: String }, - #[error("Unauthorized")] - Unauthorized {}, + #[error("Invalid sell amount")] + MismatchedSellAmount {}, #[error("Hatch phase config error {0}")] HatchPhaseConfigError(String), @@ -28,23 +40,14 @@ pub enum ContractError { #[error("Open phase config error {0}")] OpenPhaseConfigError(String), - #[error("Supply token error {0}")] - SupplyTokenError(String), - #[error("Sender {sender:?} is not in the hatcher allowlist.")] SenderNotAllowlisted { sender: String }, - #[error("The commons is closed to new contributions")] - CommonsClosed {}, - - #[error("Selling is disabled during the hatch phase")] - HatchSellingDisabled {}, - - #[error("Invalid sell amount")] - MismatchedSellAmount {}, + #[error("Supply token error {0}")] + SupplyTokenError(String), - #[error("Invalid phase, expected {expected:?}, actual {actual:?}")] - InvalidPhase { expected: String, actual: String }, + #[error("Unauthorized")] + Unauthorized {}, #[error("Got a submessage reply with unknown id: {id}")] UnknownReplyId { id: u64 }, diff --git a/contracts/external/cw-abc/src/msg.rs b/contracts/external/cw-abc/src/msg.rs index b1b04ce52..2e346ce8f 100644 --- a/contracts/external/cw-abc/src/msg.rs +++ b/contracts/external/cw-abc/src/msg.rs @@ -20,6 +20,8 @@ pub struct InstantiateMsg { /// 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>, } @@ -30,6 +32,7 @@ pub struct InstantiateMsg { pub enum UpdatePhaseConfigMsg { /// Update the hatch phase configuration Hatch { + contribution_limits: Option, exit_tax: Option, initial_raise: Option, initial_allocation_ratio: Option, diff --git a/contracts/external/cw-abc/src/test_tube/integration_tests.rs b/contracts/external/cw-abc/src/test_tube/integration_tests.rs index 0f3b4280f..49aad5b04 100644 --- a/contracts/external/cw-abc/src/test_tube/integration_tests.rs +++ b/contracts/external/cw-abc/src/test_tube/integration_tests.rs @@ -1,19 +1,27 @@ use crate::{ - abc::{ClosedConfig, CommonsPhase, CommonsPhaseConfig, HatchConfig, MinMax, OpenConfig}, - msg::{CommonsPhaseConfigResponse, CurveInfoResponse, DenomResponse, ExecuteMsg, QueryMsg}, + abc::{ + ClosedConfig, CommonsPhase, CommonsPhaseConfig, CurveType, HatchConfig, MinMax, OpenConfig, + ReserveToken, SupplyToken, + }, + msg::{ + CommonsPhaseConfigResponse, CurveInfoResponse, DenomResponse, ExecuteMsg, InstantiateMsg, + QueryMsg, + }, + ContractError, }; -use super::test_env::{TestEnv, TestEnvBuilder, RESERVE}; +use super::test_env::{TestEnv, TestEnvBuilder, DENOM, RESERVE}; use cosmwasm_std::{coins, Decimal, Uint128}; 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}; +use osmosis_test_tube::{ + osmosis_std::types::cosmos::base::v1beta1::Coin, Account, OsmosisTestApp, RunnerError, +}; #[test] fn test_happy_path() { let app = OsmosisTestApp::new(); - let builder = TestEnvBuilder::new(); let env = builder.default_setup(&app); let TestEnv { @@ -85,6 +93,10 @@ fn test_happy_path() { phase.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), @@ -155,14 +167,57 @@ fn test_happy_path() { ); // Buy enough tokens to end the hatch phase - abc.execute( - &ExecuteMsg::Buy {}, - &coins(1000000000, RESERVE), - &accounts[0], - ) - .unwrap(); + abc.execute(&ExecuteMsg::Buy {}, &coins(1000000, RESERVE), &accounts[0]) + .unwrap(); // Contract is now in open phase let phase: CommonsPhaseConfigResponse = abc.query(&QueryMsg::PhaseConfig {}).unwrap(); assert_eq!(phase.phase, CommonsPhase::Open); } + +#[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(1000000000, 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), + }) + ); +} + +// TODO +#[test] +fn test_max_supply() { + // Set a max supply and ensure it does not go over +} diff --git a/contracts/external/cw-abc/src/test_tube/test_env.rs b/contracts/external/cw-abc/src/test_tube/test_env.rs index 2dd020c24..55a20549f 100644 --- a/contracts/external/cw-abc/src/test_tube/test_env.rs +++ b/contracts/external/cw-abc/src/test_tube/test_env.rs @@ -117,8 +117,12 @@ impl TestEnvBuilder { }, phase_config: CommonsPhaseConfig { hatch: HatchConfig { + contribution_limits: MinMax { + min: Uint128::from(10u128), + max: Uint128::from(1000000u128), + }, initial_raise: MinMax { - min: Uint128::one(), + min: Uint128::from(10u128), max: Uint128::from(1000000u128), }, initial_price: Uint128::one(), @@ -153,25 +157,32 @@ impl TestEnvBuilder { } } - pub fn setup(self, app: &'_ OsmosisTestApp, msg: InstantiateMsg) -> TestEnv<'_> { + 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]).unwrap(); + let issuer_id = TokenfactoryIssuer::upload(app, &accounts[0])?; - let abc = CwAbc::deploy(app, &msg, &accounts[0]).unwrap(); + // Override issuer_id + msg.token_issuer_code_id = issuer_id; - let issuer_addr = CwAbc::query(&abc, &QueryMsg::TokenContract {}).unwrap(); + let abc = CwAbc::deploy(app, &msg, &accounts[0])?; - let tf_issuer = TokenfactoryIssuer::new_with_values(app, issuer_id, issuer_addr).unwrap(); + let issuer_addr = CwAbc::query(&abc, &QueryMsg::TokenContract {})?; - TestEnv { + 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 { @@ -351,7 +362,7 @@ impl<'a> CwAbc<'a> { } } - pub fn execute_error(err: ContractError) -> RunnerError { + pub fn execute_error(&self, err: ContractError) -> RunnerError { RunnerError::ExecuteError { msg: format!( "failed to execute message; message index: 0: {}: execute wasm contract failed", diff --git a/contracts/external/cw-abc/src/testing.rs b/contracts/external/cw-abc/src/testing.rs index f3ebcc943..c08a85914 100644 --- a/contracts/external/cw-abc/src/testing.rs +++ b/contracts/external/cw-abc/src/testing.rs @@ -58,6 +58,10 @@ pub fn default_instantiate_msg( }, 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), From 9f39048aec7558306bd0a485e9b691cfd550135b Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Thu, 16 Nov 2023 17:11:11 +0100 Subject: [PATCH 27/56] Max Supply --- contracts/external/cw-abc/src/abc.rs | 4 +-- contracts/external/cw-abc/src/commands.rs | 17 ++++++--- contracts/external/cw-abc/src/contract.rs | 17 +++------ contracts/external/cw-abc/src/error.rs | 10 +++--- contracts/external/cw-abc/src/state.rs | 3 ++ .../cw-abc/src/test_tube/integration_tests.rs | 36 +++++++++++++++---- .../external/cw-abc/src/test_tube/test_env.rs | 1 + contracts/external/cw-abc/src/testing.rs | 1 + 8 files changed, 58 insertions(+), 31 deletions(-) diff --git a/contracts/external/cw-abc/src/abc.rs b/contracts/external/cw-abc/src/abc.rs index 67febb7df..aa5a3c713 100644 --- a/contracts/external/cw-abc/src/abc.rs +++ b/contracts/external/cw-abc/src/abc.rs @@ -14,8 +14,8 @@ pub struct SupplyToken { /// Number of decimal places for the supply token, needed for proper curve math. /// Default for token factory is 6 pub decimals: u8, - // TODO max supply - // pub max_supply: Uint128, + // Optional maximum supply + pub max_supply: Option, } #[cw_serde] diff --git a/contracts/external/cw-abc/src/commands.rs b/contracts/external/cw-abc/src/commands.rs index 3083378dc..4ed60984f 100644 --- a/contracts/external/cw-abc/src/commands.rs +++ b/contracts/external/cw-abc/src/commands.rs @@ -8,12 +8,12 @@ use std::collections::HashSet; use std::ops::Deref; use token_bindings::{TokenFactoryMsg, TokenFactoryQuery}; -use crate::abc::{CommonsPhase, CurveFn, MinMax}; +use crate::abc::CommonsPhase; use crate::contract::CwAbcResult; use crate::msg::UpdatePhaseConfigMsg; use crate::state::{ - CURVE_STATE, CURVE_TYPE, DONATIONS, HATCHERS, HATCHER_ALLOWLIST, PHASE, PHASE_CONFIG, - SUPPLY_DENOM, TOKEN_ISSUER_CONTRACT, + CURVE_STATE, CURVE_TYPE, DONATIONS, HATCHERS, HATCHER_ALLOWLIST, MAX_SUPPLY, PHASE, + PHASE_CONFIG, SUPPLY_DENOM, TOKEN_ISSUER_CONTRACT, }; use crate::ContractError; @@ -81,6 +81,15 @@ pub fn execute_buy(deps: DepsMut, _env: Env, info: MessageInf let minted = new_supply .checked_sub(curve_state.supply) .map_err(StdError::overflow)?; + + // 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 new_supply > max_supply { + return Err(ContractError::CannotExceedMaxSupply { max: max_supply }); + } + } + + // Save the new curve state curve_state.supply = new_supply; CURVE_STATE.save(deps.storage, &curve_state)?; @@ -95,8 +104,6 @@ pub fn execute_buy(deps: DepsMut, _env: Env, info: MessageInf funds: vec![], }; - // TODO check that the minted amount has not exceeded the max supply - Ok(Response::new() .add_message(mint_msg) .add_attribute("action", "buy") diff --git a/contracts/external/cw-abc/src/contract.rs b/contracts/external/cw-abc/src/contract.rs index ba3dd78ef..bf94e9707 100644 --- a/contracts/external/cw-abc/src/contract.rs +++ b/contracts/external/cw-abc/src/contract.rs @@ -17,8 +17,8 @@ use crate::curves::DecimalPlaces; use crate::error::ContractError; use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, UpdatePhaseConfigMsg}; use crate::state::{ - CurveState, CURVE_STATE, CURVE_TYPE, HATCHER_ALLOWLIST, PHASE, PHASE_CONFIG, SUPPLY_DENOM, - TOKEN_INSTANTIATION_INFO, TOKEN_ISSUER_CONTRACT, + CurveState, CURVE_STATE, CURVE_TYPE, HATCHER_ALLOWLIST, MAX_SUPPLY, PHASE, PHASE_CONFIG, + SUPPLY_DENOM, TOKEN_INSTANTIATION_INFO, TOKEN_ISSUER_CONTRACT, }; use crate::{commands, queries}; @@ -62,16 +62,9 @@ pub fn instantiate( // Save new token info for use in reply TOKEN_INSTANTIATION_INFO.save(deps.storage, &supply)?; - // Save the denom - SUPPLY_DENOM.save( - deps.storage, - &format!( - "{}/{}/{}", - DENOM_PREFIX, - env.contract.address.into_string(), - supply.subdenom - ), - )?; + if let Some(max_supply) = supply.max_supply { + MAX_SUPPLY.save(deps.storage, &max_supply)?; + } // Save the curve type and state let normalization_places = DecimalPlaces::new(supply.decimals, reserve.decimals); diff --git a/contracts/external/cw-abc/src/error.rs b/contracts/external/cw-abc/src/error.rs index 369f2767d..d62661331 100644 --- a/contracts/external/cw-abc/src/error.rs +++ b/contracts/external/cw-abc/src/error.rs @@ -16,14 +16,17 @@ pub enum ContractError { #[error("{0}")] Ownership(#[from] cw_ownable::OwnershipError), + #[error("Cannot mint more tokens than the maximum supply of {max}")] + CannotExceedMaxSupply { max: Uint128 }, + #[error("The commons is closed to new contributions")] CommonsClosed {}, #[error("Contribution must be less than or equal to {max} and greater than or equal to {min}")] ContributionLimit { min: Uint128, max: Uint128 }, - #[error("Selling is disabled during the hatch phase")] - HatchSellingDisabled {}, + #[error("Hatch phase config error {0}")] + HatchPhaseConfigError(String), #[error("Invalid subdenom: {subdenom:?}")] InvalidSubdenom { subdenom: String }, @@ -34,9 +37,6 @@ pub enum ContractError { #[error("Invalid sell amount")] MismatchedSellAmount {}, - #[error("Hatch phase config error {0}")] - HatchPhaseConfigError(String), - #[error("Open phase config error {0}")] OpenPhaseConfigError(String), diff --git a/contracts/external/cw-abc/src/state.rs b/contracts/external/cw-abc/src/state.rs index 2af899c43..f3b9b55a3 100644 --- a/contracts/external/cw-abc/src/state.rs +++ b/contracts/external/cw-abc/src/state.rs @@ -43,6 +43,9 @@ pub const CURVE_TYPE: Item = Item::new("curve_type"); /// 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"); + /// Hatcher phase allowlist /// TODO: we could use the keys for the [`HATCHERS`] map instead setting them to 0 at the beginning, though existing hatchers would not be able to be removed pub static HATCHER_ALLOWLIST: Item> = Item::new("hatch_allowlist"); diff --git a/contracts/external/cw-abc/src/test_tube/integration_tests.rs b/contracts/external/cw-abc/src/test_tube/integration_tests.rs index 49aad5b04..25482180a 100644 --- a/contracts/external/cw-abc/src/test_tube/integration_tests.rs +++ b/contracts/external/cw-abc/src/test_tube/integration_tests.rs @@ -1,8 +1,5 @@ use crate::{ - abc::{ - ClosedConfig, CommonsPhase, CommonsPhaseConfig, CurveType, HatchConfig, MinMax, OpenConfig, - ReserveToken, SupplyToken, - }, + abc::{ClosedConfig, CommonsPhase, CommonsPhaseConfig, HatchConfig, MinMax, OpenConfig}, msg::{ CommonsPhaseConfigResponse, CurveInfoResponse, DenomResponse, ExecuteMsg, InstantiateMsg, QueryMsg, @@ -216,8 +213,33 @@ fn test_contribution_limits_enforced() { ); } -// TODO #[test] -fn test_max_supply() { - // Set a max supply and ensure it does not go over +fn test_max_supply_enforced() { + 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(1000000, RESERVE), &accounts[0]) + .unwrap(); + + // Buy enough tokens to trigger a max supply error + let err = abc + .execute( + &ExecuteMsg::Buy {}, + &coins(1000000000, RESERVE), + &accounts[0], + ) + .unwrap_err(); + assert_eq!( + err, + abc.execute_error(ContractError::CannotExceedMaxSupply { + max: Uint128::from(1000000u128) + }) + ); } diff --git a/contracts/external/cw-abc/src/test_tube/test_env.rs b/contracts/external/cw-abc/src/test_tube/test_env.rs index 55a20549f..069a8c2dc 100644 --- a/contracts/external/cw-abc/src/test_tube/test_env.rs +++ b/contracts/external/cw-abc/src/test_tube/test_env.rs @@ -110,6 +110,7 @@ impl TestEnvBuilder { subdenom: DENOM.to_string(), metadata: None, decimals: 6, + max_supply: Some(Uint128::from(1000000000u128)), }, reserve: ReserveToken { denom: RESERVE.to_string(), diff --git a/contracts/external/cw-abc/src/testing.rs b/contracts/external/cw-abc/src/testing.rs index c08a85914..247c99c27 100644 --- a/contracts/external/cw-abc/src/testing.rs +++ b/contracts/external/cw-abc/src/testing.rs @@ -51,6 +51,7 @@ pub fn default_instantiate_msg( subdenom: TEST_SUPPLY_DENOM.to_string(), metadata: Some(default_supply_metadata()), decimals, + max_supply: None, }, reserve: ReserveToken { denom: TEST_RESERVE_DENOM.to_string(), From 30dd7a5e9e07e7746d1c19310007814a7f42d082 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Fri, 17 Nov 2023 14:36:50 +0100 Subject: [PATCH 28/56] Clean up, add notes for future work --- contracts/external/cw-abc/src/abc.rs | 16 +--------------- contracts/external/cw-abc/src/msg.rs | 9 +++++++++ .../cw-abc/src/test_tube/integration_tests.rs | 3 +-- .../external/cw-abc/src/test_tube/test_env.rs | 1 - contracts/external/cw-abc/src/testing.rs | 1 - 5 files changed, 11 insertions(+), 19 deletions(-) diff --git a/contracts/external/cw-abc/src/abc.rs b/contracts/external/cw-abc/src/abc.rs index aa5a3c713..8b6263784 100644 --- a/contracts/external/cw-abc/src/abc.rs +++ b/contracts/external/cw-abc/src/abc.rs @@ -40,10 +40,6 @@ pub struct HatchConfig { pub contribution_limits: MinMax, /// The initial raise range (min, max) in the reserve token pub initial_raise: MinMax, - /// The initial price (p0) per reserve token - /// TODO: initial price is not implemented yet - /// TODO: do we need this or is it just calculated? - pub initial_price: Uint128, /// The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool pub initial_allocation_ratio: StdDecimal, /// Exit tax for the hatch phase @@ -68,16 +64,6 @@ impl HatchConfig { ) ); - ensure!( - !self.initial_price.is_zero(), - ContractError::HatchPhaseConfigError( - "Initial price must be greater than zero.".to_string() - ) - ); - - // TODO: define better values - // Q: is zero valid for initial allocation value? Isn't the whole point of the - // hatch phase to initialize the DAO treasury? ensure!( self.initial_allocation_ratio <= StdDecimal::percent(100u64), ContractError::HatchPhaseConfigError( @@ -204,6 +190,7 @@ impl CommonsPhaseConfig { pub type CurveFn = Box Box>; +// TODO Curve type validation? // TODO add S-curve and taylor series #[cw_serde] pub enum CurveType { @@ -256,7 +243,6 @@ mod unit_tests { min: Uint128::one(), max: Uint128::from(1000000u128), }, - initial_price: Uint128::one(), initial_allocation_ratio: StdDecimal::percent(10u64), exit_tax: StdDecimal::percent(10u64), }, diff --git a/contracts/external/cw-abc/src/msg.rs b/contracts/external/cw-abc/src/msg.rs index 2e346ce8f..d6f29d213 100644 --- a/contracts/external/cw-abc/src/msg.rs +++ b/contracts/external/cw-abc/src/msg.rs @@ -42,6 +42,7 @@ pub enum UpdatePhaseConfigMsg { exit_tax: Option, allocation_percentage: Option, }, + /// TODO include curve type so we know what happens when a DAO dies? /// Update the closed phase configuration Closed {}, } @@ -64,8 +65,16 @@ pub enum ExecuteMsg { /// Update the hatch phase configuration /// This can only be called by the admin and only during the hatch phase UpdatePhaseConfig(UpdatePhaseConfigMsg), + // TODO Close the bonding curve + // Closing the bonding curve means no more buys are enabled and exit tax is set + // to zero. This could be used in the event of a project shutting down for example. + // + // Q: do we allow updating of the curve type? Is it passed in here? + // Close {}, } +// TODO Price queries: +// - Price to buy a certain amount? #[cw_ownable::cw_ownable_query] #[cw_serde] #[derive(QueryResponses)] diff --git a/contracts/external/cw-abc/src/test_tube/integration_tests.rs b/contracts/external/cw-abc/src/test_tube/integration_tests.rs index 25482180a..f7c401e5e 100644 --- a/contracts/external/cw-abc/src/test_tube/integration_tests.rs +++ b/contracts/external/cw-abc/src/test_tube/integration_tests.rs @@ -98,7 +98,6 @@ fn test_happy_path() { min: Uint128::one(), max: Uint128::from(1000000u128), }, - initial_price: Uint128::one(), initial_allocation_ratio: Decimal::percent(10u64), exit_tax: Decimal::percent(10u64), }, @@ -232,7 +231,7 @@ fn test_max_supply_enforced() { let err = abc .execute( &ExecuteMsg::Buy {}, - &coins(1000000000, RESERVE), + &coins(1000000000000000000000000, RESERVE), &accounts[0], ) .unwrap_err(); diff --git a/contracts/external/cw-abc/src/test_tube/test_env.rs b/contracts/external/cw-abc/src/test_tube/test_env.rs index 069a8c2dc..b265225ef 100644 --- a/contracts/external/cw-abc/src/test_tube/test_env.rs +++ b/contracts/external/cw-abc/src/test_tube/test_env.rs @@ -126,7 +126,6 @@ impl TestEnvBuilder { min: Uint128::from(10u128), max: Uint128::from(1000000u128), }, - initial_price: Uint128::one(), initial_allocation_ratio: Decimal::percent(10u64), exit_tax: Decimal::percent(10u64), }, diff --git a/contracts/external/cw-abc/src/testing.rs b/contracts/external/cw-abc/src/testing.rs index 247c99c27..a99861e3f 100644 --- a/contracts/external/cw-abc/src/testing.rs +++ b/contracts/external/cw-abc/src/testing.rs @@ -67,7 +67,6 @@ pub fn default_instantiate_msg( min: Uint128::one(), max: Uint128::from(1000000u128), }, - initial_price: Uint128::one(), initial_allocation_ratio: Decimal::percent(10u64), exit_tax: Decimal::zero(), }, From 9e700570dfd39645ffb878a35e45683362dcc041 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Fri, 17 Nov 2023 16:54:59 +0100 Subject: [PATCH 29/56] Close curve, update curve, set max supply methods --- contracts/external/cw-abc/src/commands.rs | 47 ++++++++++++++++++++++- contracts/external/cw-abc/src/contract.rs | 3 ++ contracts/external/cw-abc/src/msg.rs | 38 +++++++++++------- 3 files changed, 74 insertions(+), 14 deletions(-) diff --git a/contracts/external/cw-abc/src/commands.rs b/contracts/external/cw-abc/src/commands.rs index 4ed60984f..bd351fb9d 100644 --- a/contracts/external/cw-abc/src/commands.rs +++ b/contracts/external/cw-abc/src/commands.rs @@ -8,7 +8,7 @@ use std::collections::HashSet; use std::ops::Deref; use token_bindings::{TokenFactoryMsg, TokenFactoryQuery}; -use crate::abc::CommonsPhase; +use crate::abc::{CommonsPhase, CurveType}; use crate::contract::CwAbcResult; use crate::msg::UpdatePhaseConfigMsg; use crate::state::{ @@ -140,6 +140,7 @@ fn update_hatcher_contributions( }) } +/// Sell tokens on the bonding curve pub fn execute_sell(deps: DepsMut, _env: Env, info: MessageInfo) -> CwAbcResult { let curve_type = CURVE_TYPE.load(deps.storage)?; let curve_fn = curve_type.to_curve_fn(); @@ -235,6 +236,15 @@ fn calculate_exit_tax(storage: &dyn Storage, sell_amount: Uint128) -> CwAbcResul Ok(taxed_amount) } +/// Transitions the bonding curve to a closed phase where only sells are allowed +pub fn execute_close(deps: DepsMut, info: MessageInfo) -> CwAbcResult { + 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 execute_donate( deps: DepsMut, @@ -271,6 +281,25 @@ fn assert_allowlisted(storage: &dyn Storage, hatcher: &Addr) -> Result<(), Contr Ok(()) } +/// Set the maxiumum supply (only callable by owner) +/// If `max_supply` is set to None there will be no limit.` +pub fn set_max_supply( + deps: DepsMut, + info: MessageInfo, + max_supply: Option, +) -> CwAbcResult { + 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", "set_max_supply") + .add_attribute("value", max_supply.unwrap_or(Uint128::MAX).to_string())) +} + /// Add and remove addresses from the hatcher allowlist pub fn update_hatch_allowlist( deps: DepsMut, @@ -304,6 +333,7 @@ pub fn update_hatch_allowlist( Ok(Response::new().add_attributes(vec![("action", "update_hatch_allowlist")])) } +/// Update the configuration of a particular phase pub fn update_phase_config( deps: DepsMut, _env: Env, @@ -375,6 +405,21 @@ pub fn update_phase_config( } } +/// Update the bonding curve. Only callable by the 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, +) -> CwAbcResult { + 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, diff --git a/contracts/external/cw-abc/src/contract.rs b/contracts/external/cw-abc/src/contract.rs index bf94e9707..314e62c71 100644 --- a/contracts/external/cw-abc/src/contract.rs +++ b/contracts/external/cw-abc/src/contract.rs @@ -117,7 +117,10 @@ pub fn execute( match msg { ExecuteMsg::Buy {} => commands::execute_buy(deps, env, info), ExecuteMsg::Burn {} => commands::execute_sell(deps, env, info), + ExecuteMsg::Close {} => commands::execute_close(deps, info), ExecuteMsg::Donate {} => commands::execute_donate(deps, env, info), + ExecuteMsg::SetMaxSupply { max_supply } => commands::set_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, info, to_add, to_remove) } diff --git a/contracts/external/cw-abc/src/msg.rs b/contracts/external/cw-abc/src/msg.rs index d6f29d213..34b131256 100644 --- a/contracts/external/cw-abc/src/msg.rs +++ b/contracts/external/cw-abc/src/msg.rs @@ -27,23 +27,24 @@ pub struct InstantiateMsg { } /// Update the phase configurations. -/// These can only be called by the admin and only before or during each phase +/// These can only be called by the owner. #[cw_serde] pub enum UpdatePhaseConfigMsg { /// Update the hatch phase configuration Hatch { contribution_limits: Option, exit_tax: Option, + // TODO what is the minimum used for? initial_raise: Option, initial_allocation_ratio: Option, }, - /// Update the open phase configuration + /// Update the open phase configuration. Open { exit_tax: Option, allocation_percentage: Option, }, - /// TODO include curve type so we know what happens when a DAO dies? - /// Update the closed phase configuration + /// Update the closed phase configuration. + /// TODO Set the curve type to be used on close? Closed {}, } @@ -57,24 +58,35 @@ pub enum ExecuteMsg { Burn {}, /// Donate will add reserve tokens to the funding pool Donate {}, - /// Update the hatch phase allowlist + /// Sets (or unsets if set to None) the maximum supply + SetMaxSupply { + /// 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. + /// This can only be called by the owner. UpdateHatchAllowlist { + /// Addresses to be added. to_add: Vec, + /// Addresses to be removed. to_remove: Vec, }, - /// Update the hatch phase configuration - /// This can only be called by the admin and only during the hatch phase + /// Update the configuration of a certain phase. + /// This can only be called by the owner. UpdatePhaseConfig(UpdatePhaseConfigMsg), - // TODO Close the bonding curve - // Closing the bonding curve means no more buys are enabled and exit tax is set - // to zero. This could be used in the event of a project shutting down for example. - // - // Q: do we allow updating of the curve type? Is it passed in here? - // Close {}, + /// 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 {}, } // TODO Price queries: // - Price to buy a certain amount? +// - What can be bought for a certain amount? #[cw_ownable::cw_ownable_query] #[cw_serde] #[derive(QueryResponses)] From 638b102f560a90cf93d41fae24cb891036586d0d Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Fri, 17 Nov 2023 16:59:19 +0100 Subject: [PATCH 30/56] Rename methods, cleanup unused variables --- contracts/external/cw-abc/src/commands.rs | 4 ++-- contracts/external/cw-abc/src/contract.rs | 6 ++++-- contracts/external/cw-abc/src/msg.rs | 9 +++++---- .../cw-abc/src/test_tube/integration_tests.rs | 15 +++++---------- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/contracts/external/cw-abc/src/commands.rs b/contracts/external/cw-abc/src/commands.rs index bd351fb9d..f80868d37 100644 --- a/contracts/external/cw-abc/src/commands.rs +++ b/contracts/external/cw-abc/src/commands.rs @@ -283,7 +283,7 @@ fn assert_allowlisted(storage: &dyn Storage, hatcher: &Addr) -> Result<(), Contr /// Set the maxiumum supply (only callable by owner) /// If `max_supply` is set to None there will be no limit.` -pub fn set_max_supply( +pub fn update_max_supply( deps: DepsMut, info: MessageInfo, max_supply: Option, @@ -296,7 +296,7 @@ pub fn set_max_supply( } Ok(Response::new() - .add_attribute("action", "set_max_supply") + .add_attribute("action", "update_max_supply") .add_attribute("value", max_supply.unwrap_or(Uint128::MAX).to_string())) } diff --git a/contracts/external/cw-abc/src/contract.rs b/contracts/external/cw-abc/src/contract.rs index 314e62c71..5170c570c 100644 --- a/contracts/external/cw-abc/src/contract.rs +++ b/contracts/external/cw-abc/src/contract.rs @@ -116,10 +116,12 @@ pub fn execute( ) -> CwAbcResult { match msg { ExecuteMsg::Buy {} => commands::execute_buy(deps, env, info), - ExecuteMsg::Burn {} => commands::execute_sell(deps, env, info), + ExecuteMsg::Sell {} => commands::execute_sell(deps, env, info), ExecuteMsg::Close {} => commands::execute_close(deps, info), ExecuteMsg::Donate {} => commands::execute_donate(deps, env, info), - ExecuteMsg::SetMaxSupply { max_supply } => commands::set_max_supply(deps, info, max_supply), + 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, info, to_add, to_remove) diff --git a/contracts/external/cw-abc/src/msg.rs b/contracts/external/cw-abc/src/msg.rs index 34b131256..f6ee7e0db 100644 --- a/contracts/external/cw-abc/src/msg.rs +++ b/contracts/external/cw-abc/src/msg.rs @@ -52,14 +52,15 @@ pub enum UpdatePhaseConfigMsg { #[cw_serde] pub enum ExecuteMsg { /// Buy will attempt to purchase as many supply tokens as possible. - /// You must send only reserve tokens in that message + /// You must send only reserve tokens. Buy {}, - /// Burn is a base message to destroy tokens forever - Burn {}, + /// Sell burns supply tokens in return for the reserve token. + /// You must send only supply tokens. + Sell {}, /// Donate will add reserve tokens to the funding pool Donate {}, /// Sets (or unsets if set to None) the maximum supply - SetMaxSupply { + UpdateMaxSupply { /// The maximum supply able to be minted. max_supply: Option, }, diff --git a/contracts/external/cw-abc/src/test_tube/integration_tests.rs b/contracts/external/cw-abc/src/test_tube/integration_tests.rs index f7c401e5e..f13c368ba 100644 --- a/contracts/external/cw-abc/src/test_tube/integration_tests.rs +++ b/contracts/external/cw-abc/src/test_tube/integration_tests.rs @@ -1,20 +1,15 @@ use crate::{ abc::{ClosedConfig, CommonsPhase, CommonsPhaseConfig, HatchConfig, MinMax, OpenConfig}, - msg::{ - CommonsPhaseConfigResponse, CurveInfoResponse, DenomResponse, ExecuteMsg, InstantiateMsg, - QueryMsg, - }, + msg::{CommonsPhaseConfigResponse, CurveInfoResponse, DenomResponse, ExecuteMsg, QueryMsg}, ContractError, }; -use super::test_env::{TestEnv, TestEnvBuilder, DENOM, RESERVE}; +use super::test_env::{TestEnv, TestEnvBuilder, RESERVE}; use cosmwasm_std::{coins, Decimal, Uint128}; 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, RunnerError, -}; +use osmosis_test_tube::{osmosis_std::types::cosmos::base::v1beta1::Coin, Account, OsmosisTestApp}; #[test] fn test_happy_path() { @@ -109,9 +104,9 @@ fn test_happy_path() { } ); - // Burn + // Sell abc.execute( - &ExecuteMsg::Burn {}, + &ExecuteMsg::Sell {}, &coins(100, denom.clone()), &accounts[0], ) From 8ef65d8220f66573f63059733e4936b4bd66772d Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Fri, 17 Nov 2023 17:35:11 +0100 Subject: [PATCH 31/56] Update schema --- contracts/external/cw-abc/schema/cw-abc.json | 343 ++++++++++++++----- 1 file changed, 256 insertions(+), 87 deletions(-) diff --git a/contracts/external/cw-abc/schema/cw-abc.json b/contracts/external/cw-abc/schema/cw-abc.json index 5bc90fd9f..fbdd1c117 100644 --- a/contracts/external/cw-abc/schema/cw-abc.json +++ b/contracts/external/cw-abc/schema/cw-abc.json @@ -23,7 +23,7 @@ ] }, "hatcher_allowlist": { - "description": "Hatcher allowlist", + "description": "TODO different ways of doing this, for example DAO members? Using a whitelist contract? Merkle tree? Hatcher allowlist", "type": [ "array", "null" @@ -94,7 +94,7 @@ ] }, "open": { - "description": "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.", + "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" @@ -197,7 +197,7 @@ "type": "string" }, "DenomUnit": { - "description": "This maps to cosmos.bank.v1beta1.DenomUnit protobuf struct", + "description": "DenomUnit represents a struct that describes a given denomination unit of the basic token.", "type": "object", "required": [ "aliases", @@ -222,44 +222,43 @@ "format": "uint32", "minimum": 0.0 } - }, - "additionalProperties": false + } }, "HatchConfig": { "type": "object", "required": [ + "contribution_limits", "exit_tax", "initial_allocation_ratio", - "initial_price", "initial_raise" ], "properties": { - "exit_tax": { - "description": "Exit tax for the hatch phase", + "contribution_limits": { + "description": "The minimum and maximum contribution amounts (min, max) in the reserve token", "allOf": [ { - "$ref": "#/definitions/Decimal" + "$ref": "#/definitions/MinMax" } ] }, - "initial_allocation_ratio": { - "description": "The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool", + "exit_tax": { + "description": "Exit tax for the hatch phase", "allOf": [ { "$ref": "#/definitions/Decimal" } ] }, - "initial_price": { - "description": "The initial price (p0) per reserve token TODO: initial price is not implemented yet", + "initial_allocation_ratio": { + "description": "The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool", "allOf": [ { - "$ref": "#/definitions/Uint128" + "$ref": "#/definitions/Decimal" } ] }, "initial_raise": { - "description": "pub contribution_limits: MinMax, The initial raise range (min, max) in the reserve token", + "description": "The initial raise range (min, max) in the reserve token", "allOf": [ { "$ref": "#/definitions/MinMax" @@ -269,70 +268,57 @@ }, "additionalProperties": false }, - "Metadata": { - "description": "This maps to cosmos.bank.v1beta1.Metadata protobuf struct", + "MinMax": { + "description": "Struct for minimium and maximum values", "type": "object", "required": [ - "denom_units" + "max", + "min" ], "properties": { - "base": { - "description": "base represents the base denom (should be the DenomUnit with exponent = 0).", + "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": [ - "string", + "array", "null" - ] - }, - "denom_units": { - "description": "denom_units represents the list of DenomUnit's for a given coin", - "type": "array", + ], "items": { "$ref": "#/definitions/DenomUnit" } }, "description": { - "type": [ - "string", - "null" - ] + "description": "The description of the token", + "type": "string" }, "display": { - "description": "display indicates the suggested denom that should be displayed in clients.", - "type": [ - "string", - "null" - ] + "description": "The unit commonly used in communication (e.g. \"cat\")", + "type": "string" }, "name": { - "description": "name defines the name of the token (eg: Cosmos Atom)", - "type": [ - "string", - "null" - ] + "description": "The name of the token (e.g. \"Cat Coin\")", + "type": "string" }, "symbol": { - "description": "symbol is the token symbol usually shown on exchanges (eg: ATOM). This can be the same as the display.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - }, - "MinMax": { - "description": "Struct for minimium and maximum values", - "type": "object", - "required": [ - "max", - "min" - ], - "properties": { - "max": { - "$ref": "#/definitions/Uint128" - }, - "min": { - "$ref": "#/definitions/Uint128" + "description": "The ticker symbol of the token (e.g. \"CAT\")", + "type": "string" } }, "additionalProperties": false @@ -345,7 +331,7 @@ ], "properties": { "allocation_percentage": { - "description": "Percentage of capital put into the Reserve Pool during the Open phase", + "description": "Percentage of capital put into the Reserve Pool during the Open phase when buying from the curve.", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -396,11 +382,21 @@ "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/Metadata" + "$ref": "#/definitions/NewDenomMetadata" }, { "type": "null" @@ -425,7 +421,7 @@ "title": "ExecuteMsg", "oneOf": [ { - "description": "Buy will attempt to purchase as many supply tokens as possible. You must send only reserve tokens in that message", + "description": "Buy will attempt to purchase as many supply tokens as possible. You must send only reserve tokens.", "type": "object", "required": [ "buy" @@ -439,13 +435,13 @@ "additionalProperties": false }, { - "description": "Burn is a base message to destroy tokens forever", + "description": "Sell burns supply tokens in return for the reserve token. You must send only supply tokens.", "type": "object", "required": [ - "burn" + "sell" ], "properties": { - "burn": { + "sell": { "type": "object", "additionalProperties": false } @@ -467,7 +463,56 @@ "additionalProperties": false }, { - "description": "Update the hatch phase allowlist", + "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. This can only be called by the owner.", "type": "object", "required": [ "update_hatch_allowlist" @@ -481,12 +526,14 @@ ], "properties": { "to_add": { + "description": "Addresses to be added.", "type": "array", "items": { "type": "string" } }, "to_remove": { + "description": "Addresses to be removed.", "type": "array", "items": { "type": "string" @@ -499,7 +546,7 @@ "additionalProperties": false }, { - "description": "Update the hatch phase configuration This can only be called by the admin and only during the hatch phase", + "description": "Update the configuration of a certain phase. This can only be called by the owner.", "type": "object", "required": [ "update_phase_config" @@ -511,6 +558,20 @@ }, "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", @@ -577,6 +638,94 @@ } ] }, + "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" @@ -662,7 +811,7 @@ "type": "string" }, "UpdatePhaseConfigMsg": { - "description": "Update the phase configurations. These can only be called by the admin and only before or during each phase", + "description": "Update the phase configurations. These can only be called by the owner.", "oneOf": [ { "description": "Update the hatch phase configuration", @@ -674,6 +823,26 @@ "hatch": { "type": "object", "properties": { + "contribution_limits": { + "anyOf": [ + { + "$ref": "#/definitions/MinMax" + }, + { + "type": "null" + } + ] + }, + "exit_tax": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, "initial_allocation_ratio": { "anyOf": [ { @@ -701,7 +870,7 @@ "additionalProperties": false }, { - "description": "Update the open phase configuration", + "description": "Update the open phase configuration.", "type": "object", "required": [ "open" @@ -710,7 +879,7 @@ "open": { "type": "object", "properties": { - "exit_tax": { + "allocation_percentage": { "anyOf": [ { "$ref": "#/definitions/Decimal" @@ -720,7 +889,7 @@ } ] }, - "reserve_ratio": { + "exit_tax": { "anyOf": [ { "$ref": "#/definitions/Decimal" @@ -737,7 +906,7 @@ "additionalProperties": false }, { - "description": "Update the closed phase configuration", + "description": "Update the closed phase configuration. TODO Set the curve type to be used on close?", "type": "object", "required": [ "closed" @@ -1204,7 +1373,7 @@ ] }, "open": { - "description": "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.", + "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" @@ -1221,38 +1390,38 @@ "HatchConfig": { "type": "object", "required": [ + "contribution_limits", "exit_tax", "initial_allocation_ratio", - "initial_price", "initial_raise" ], "properties": { - "exit_tax": { - "description": "Exit tax for the hatch phase", + "contribution_limits": { + "description": "The minimum and maximum contribution amounts (min, max) in the reserve token", "allOf": [ { - "$ref": "#/definitions/Decimal" + "$ref": "#/definitions/MinMax" } ] }, - "initial_allocation_ratio": { - "description": "The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool", + "exit_tax": { + "description": "Exit tax for the hatch phase", "allOf": [ { "$ref": "#/definitions/Decimal" } ] }, - "initial_price": { - "description": "The initial price (p0) per reserve token TODO: initial price is not implemented yet", + "initial_allocation_ratio": { + "description": "The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool", "allOf": [ { - "$ref": "#/definitions/Uint128" + "$ref": "#/definitions/Decimal" } ] }, "initial_raise": { - "description": "pub contribution_limits: MinMax, The initial raise range (min, max) in the reserve token", + "description": "The initial raise range (min, max) in the reserve token", "allOf": [ { "$ref": "#/definitions/MinMax" @@ -1287,7 +1456,7 @@ ], "properties": { "allocation_percentage": { - "description": "Percentage of capital put into the Reserve Pool during the Open phase", + "description": "Percentage of capital put into the Reserve Pool during the Open phase when buying from the curve.", "allOf": [ { "$ref": "#/definitions/Decimal" From 69cd33b500ec0b521e7751b6b75f943bd196d1bd Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Mon, 20 Nov 2023 14:02:37 +0100 Subject: [PATCH 32/56] Fix some tests --- .../external/cw-abc/src/test_tube/integration_tests.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/external/cw-abc/src/test_tube/integration_tests.rs b/contracts/external/cw-abc/src/test_tube/integration_tests.rs index f13c368ba..58c61c138 100644 --- a/contracts/external/cw-abc/src/test_tube/integration_tests.rs +++ b/contracts/external/cw-abc/src/test_tube/integration_tests.rs @@ -86,11 +86,11 @@ fn test_happy_path() { CommonsPhaseConfig { hatch: HatchConfig { contribution_limits: MinMax { - min: Uint128::one(), + min: Uint128::from(10u128), max: Uint128::from(1000000u128), }, initial_raise: MinMax { - min: Uint128::one(), + min: Uint128::from(10u128), max: Uint128::from(1000000u128), }, initial_allocation_ratio: Decimal::percent(10u64), @@ -226,14 +226,14 @@ fn test_max_supply_enforced() { let err = abc .execute( &ExecuteMsg::Buy {}, - &coins(1000000000000000000000000, RESERVE), + &coins(10000000000000, RESERVE), &accounts[0], ) .unwrap_err(); assert_eq!( err, abc.execute_error(ContractError::CannotExceedMaxSupply { - max: Uint128::from(1000000u128) + max: Uint128::from(1000000000u128) }) ); } From 60d7f61f5155b36fac93aa97b0ffdbaa348335e0 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Mon, 20 Nov 2023 14:25:21 +0100 Subject: [PATCH 33/56] More tests! --- .../cw-abc/src/test_tube/integration_tests.rs | 190 +++++++++++++++++- 1 file changed, 186 insertions(+), 4 deletions(-) diff --git a/contracts/external/cw-abc/src/test_tube/integration_tests.rs b/contracts/external/cw-abc/src/test_tube/integration_tests.rs index 58c61c138..5e39218c3 100644 --- a/contracts/external/cw-abc/src/test_tube/integration_tests.rs +++ b/contracts/external/cw-abc/src/test_tube/integration_tests.rs @@ -1,10 +1,16 @@ use crate::{ - abc::{ClosedConfig, CommonsPhase, CommonsPhaseConfig, HatchConfig, MinMax, OpenConfig}, - msg::{CommonsPhaseConfigResponse, CurveInfoResponse, DenomResponse, ExecuteMsg, QueryMsg}, + abc::{ + ClosedConfig, CommonsPhase, CommonsPhaseConfig, CurveType, HatchConfig, MinMax, OpenConfig, + ReserveToken, SupplyToken, + }, + msg::{ + CommonsPhaseConfigResponse, CurveInfoResponse, DenomResponse, ExecuteMsg, InstantiateMsg, + QueryMsg, + }, ContractError, }; -use super::test_env::{TestEnv, TestEnvBuilder, RESERVE}; +use super::test_env::{TestEnv, TestEnvBuilder, DENOM, RESERVE}; use cosmwasm_std::{coins, Decimal, Uint128}; use cw_tokenfactory_issuer::msg::QueryMsg as IssuerQueryMsg; @@ -208,7 +214,7 @@ fn test_contribution_limits_enforced() { } #[test] -fn test_max_supply_enforced() { +fn test_max_supply() { let app = OsmosisTestApp::new(); let builder = TestEnvBuilder::new(); let env = builder.default_setup(&app); @@ -236,4 +242,180 @@ fn test_max_supply_enforced() { 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, + 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), + }, + initial_allocation_ratio: Decimal::percent(10u64), + exit_tax: Decimal::percent(10u64), + }, + open: OpenConfig { + allocation_percentage: Decimal::percent(10u64), + exit_tax: Decimal::percent(10u64), + }, + closed: ClosedConfig {}, + }, + hatcher_allowlist: 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![accounts[0].address(), accounts[1].address()], + to_remove: vec![], + }, + &[], + &accounts[1], + ) + .unwrap_err(); + assert_eq!( + err, + abc.execute_error(ContractError::Ownership( + cw_ownable::OwnershipError::NotOwner + )) + ); + + // Enable the allow list, normally this would be passed in through + // instantiation. + abc.execute( + &ExecuteMsg::UpdateHatchAllowlist { + to_add: vec![accounts[0].address(), accounts[1].address()], + 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(); +} + +#[test] +fn test_update_curve() {} From f3b1216aaa8ef6e1aa943ea546dc176de286f7c8 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Mon, 20 Nov 2023 14:54:14 +0100 Subject: [PATCH 34/56] Attempt at update curve tests --- .../cw-abc/src/test_tube/integration_tests.rs | 96 ++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/contracts/external/cw-abc/src/test_tube/integration_tests.rs b/contracts/external/cw-abc/src/test_tube/integration_tests.rs index 5e39218c3..1bed80c1b 100644 --- a/contracts/external/cw-abc/src/test_tube/integration_tests.rs +++ b/contracts/external/cw-abc/src/test_tube/integration_tests.rs @@ -417,5 +417,99 @@ fn test_close_curve() { .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() {} +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(1000000, 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(), + }) + ); +} From be5cc1e111ae54ce40d8725bf52dae819aaeb67b Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Mon, 4 Dec 2023 17:47:32 -0800 Subject: [PATCH 35/56] Next pass at adding info to readme --- contracts/external/cw-abc/README.md | 55 ++++++++++++++++------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/contracts/external/cw-abc/README.md b/contracts/external/cw-abc/README.md index 2c6791349..443e2d6cf 100644 --- a/contracts/external/cw-abc/README.md +++ b/contracts/external/cw-abc/README.md @@ -2,42 +2,49 @@ Implments an augmented bonding curve. -Forked from and heavily inspired by the work on [cw20-bonding](https://github.com/cosmwasm/cw-tokens/tree/main/contracts/cw20-bonding). +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. -## Extended Reading +NOTE: this contract is unaudited and experimental. NOT RECOMMENDED FOR PRODUCTION USE. -- https://medium.com/commonsstack/deep-dive-augmented-bonding-curves-b5ca4fad4436 -- https://tokeneconomy.co/token-bonding-curves-in-practice-3eb904720cb8 +## Overview +Before we get to the *Augmented* part, we must first describe bonding curves themselves. -## TODO +### Token Bonding Curves -Taking inspiration from [this article](https://medium.com/commonsstack/deep-dive-augmented-bonding-curves-b5ca4fad4436) on augmented 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) -- [ ] Implement Hatch Phase to allow projects to raise funds -- [ ] Implement optional Exit Tax -- [ ] Optionally vest tokens during the hatch phase -- [ ] Update `cw-vesting` to allow for partcipating in DAOs? +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 curency (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 curency is returned. -## Design +It is possible to use this contact as a basic bonding curve, without any of the augmented features. -There are two variants: +#### Benefits -Minting: When the input is sent to the contract via `ExecuteMsg::Buy{}` -those tokens remain on the contract and it issues it's own token to the -sender's account (known as _supply_ token). +There are a number of benefits to bonding curves: +- There is enough liquidity to back the entire supply +- 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. +- DAO treasuries can be funded by entry and exit fees on the curve -Burning: We override the burn function to not only burn the requested tokens, -but also release a proper number of the input tokens to the account that burnt -the custom token +## Extended Reading + +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 -Curves: `handle` specifies a bonding function, which is sent to parameterize -`handle_fn` (which does all the work). The curve is set when compiling -the contract. In fact many contracts can just wrap `cw-abc` and -specify the custom curve parameter. +Read more about [bonding curve math here](https://yos.io/2018/11/10/bonding-curves/). -Read more about [bonding curve math here](https://yos.io/2018/11/10/bonding-curves/) +## Future Work -Note: the first version only accepts native tokens as the +Taking inspiration from [this article](https://medium.com/commonsstack/deep-dive-augmented-bonding-curves-b5ca4fad4436) on augmented bonding curves: + +- [ ] Optionally vest tokens during the hatch phase +- [ ] Update `cw-vesting` to allow for partcipating in DAOs? ### Math From c88246917dbb894e72e6e49e0024c38ac8e4956e Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Mon, 4 Dec 2023 18:33:27 -0800 Subject: [PATCH 36/56] to_binary -> to_json_binary --- contracts/external/cw-abc/src/commands.rs | 6 +++--- contracts/external/cw-abc/src/contract.rs | 24 +++++++++++------------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/contracts/external/cw-abc/src/commands.rs b/contracts/external/cw-abc/src/commands.rs index f80868d37..717ee19a0 100644 --- a/contracts/external/cw-abc/src/commands.rs +++ b/contracts/external/cw-abc/src/commands.rs @@ -1,5 +1,5 @@ use cosmwasm_std::{ - ensure, to_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal as StdDecimal, DepsMut, Env, + ensure, to_json_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal as StdDecimal, DepsMut, Env, MessageInfo, QuerierWrapper, Response, StdError, StdResult, Storage, SubMsg, Uint128, WasmMsg, }; use cw_tokenfactory_issuer::msg::ExecuteMsg as IssuerExecuteMsg; @@ -97,7 +97,7 @@ pub fn execute_buy(deps: DepsMut, _env: Env, info: MessageInf let issuer_addr = TOKEN_ISSUER_CONTRACT.load(deps.storage)?; let mint_msg = WasmMsg::Execute { contract_addr: issuer_addr.to_string(), - msg: to_binary(&IssuerExecuteMsg::Mint { + msg: to_json_binary(&IssuerExecuteMsg::Mint { to_address: info.sender.to_string(), amount: minted, })?, @@ -163,7 +163,7 @@ pub fn execute_sell(deps: DepsMut, _env: Env, info: MessageIn // Execute burn on the cw-tokenfactory-issuer contract CosmosMsg::::Wasm(WasmMsg::Execute { contract_addr: issuer_addr.to_string(), - msg: to_binary(&IssuerExecuteMsg::Burn { + msg: to_json_binary(&IssuerExecuteMsg::Burn { from_address: info.sender.to_string(), amount: burn_amount, })?, diff --git a/contracts/external/cw-abc/src/contract.rs b/contracts/external/cw-abc/src/contract.rs index 5170c570c..bf7a390ff 100644 --- a/contracts/external/cw-abc/src/contract.rs +++ b/contracts/external/cw-abc/src/contract.rs @@ -1,7 +1,7 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, SubMsg, + to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, SubMsg, Uint128, WasmMsg, }; use cw2::set_contract_version; @@ -95,7 +95,7 @@ pub fn instantiate( WasmMsg::Instantiate { admin: Some(info.sender.to_string()), code_id: token_issuer_code_id, - msg: to_binary(&IssuerInstantiateMsg::NewToken { + msg: to_json_binary(&IssuerInstantiateMsg::NewToken { subdenom: supply.subdenom.clone(), })?, funds: info.funds, @@ -155,17 +155,17 @@ pub fn do_query( ) -> StdResult { match msg { // custom queries - QueryMsg::CurveInfo {} => to_binary(&queries::query_curve_info(deps, curve_fn)?), - QueryMsg::PhaseConfig {} => to_binary(&queries::query_phase_config(deps)?), + QueryMsg::CurveInfo {} => to_json_binary(&queries::query_curve_info(deps, curve_fn)?), + QueryMsg::PhaseConfig {} => to_json_binary(&queries::query_phase_config(deps)?), QueryMsg::Donations { start_after, limit } => { - to_binary(&queries::query_donations(deps, start_after, limit)?) + to_json_binary(&queries::query_donations(deps, start_after, limit)?) } QueryMsg::Hatchers { start_after, limit } => { - to_binary(&queries::query_hatchers(deps, start_after, limit)?) + to_json_binary(&queries::query_hatchers(deps, start_after, limit)?) } - QueryMsg::Ownership {} => to_binary(&cw_ownable::get_ownership(deps.storage)?), - QueryMsg::Denom {} => to_binary(&queries::get_denom(deps)?), - QueryMsg::TokenContract {} => to_binary(&TOKEN_ISSUER_CONTRACT.load(deps.storage)?), + QueryMsg::Ownership {} => to_json_binary(&cw_ownable::get_ownership(deps.storage)?), + QueryMsg::Denom {} => to_json_binary(&queries::get_denom(deps)?), + QueryMsg::TokenContract {} => to_json_binary(&TOKEN_ISSUER_CONTRACT.load(deps.storage)?), } } @@ -206,7 +206,7 @@ pub fn reply( // Grant an allowance to mint WasmMsg::Execute { contract_addr: issuer_addr.clone(), - msg: to_binary(&IssuerExecuteMsg::SetMinterAllowance { + 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 @@ -218,7 +218,7 @@ pub fn reply( // Grant an allowance to burn WasmMsg::Execute { contract_addr: issuer_addr.clone(), - msg: to_binary(&IssuerExecuteMsg::SetBurnerAllowance { + msg: to_json_binary(&IssuerExecuteMsg::SetBurnerAllowance { address: env.contract.address.to_string(), allowance: Uint128::MAX, })?, @@ -247,7 +247,7 @@ pub fn reply( msgs.push(WasmMsg::Execute { contract_addr: issuer_addr.clone(), - msg: to_binary(&IssuerExecuteMsg::SetDenomMetadata { + msg: to_json_binary(&IssuerExecuteMsg::SetDenomMetadata { metadata: Metadata { description: metadata.description, denom_units, From 4080c016235956b7842c820f09bb8f9f60421f8a Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Tue, 5 Dec 2023 16:15:38 -0800 Subject: [PATCH 37/56] Bug fixes, implement fees --- Cargo.toml | 2 +- contracts/external/cw-abc/src/commands.rs | 68 +++++++++++++------ contracts/external/cw-abc/src/contract.rs | 16 +++-- contracts/external/cw-abc/src/msg.rs | 3 + contracts/external/cw-abc/src/state.rs | 3 + .../cw-abc/src/test_tube/integration_tests.rs | 9 +-- .../external/cw-abc/src/test_tube/test_env.rs | 4 +- contracts/external/cw-abc/src/testing.rs | 1 + 8 files changed, 74 insertions(+), 32 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f3871aef3..35bd4efb2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,7 +60,7 @@ 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 } diff --git a/contracts/external/cw-abc/src/commands.rs b/contracts/external/cw-abc/src/commands.rs index 717ee19a0..a54594f93 100644 --- a/contracts/external/cw-abc/src/commands.rs +++ b/contracts/external/cw-abc/src/commands.rs @@ -1,9 +1,10 @@ use cosmwasm_std::{ ensure, to_json_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal as StdDecimal, DepsMut, Env, - MessageInfo, QuerierWrapper, Response, StdError, StdResult, Storage, SubMsg, Uint128, WasmMsg, + MessageInfo, QuerierWrapper, Response, StdError, StdResult, Storage, Uint128, WasmMsg, }; use cw_tokenfactory_issuer::msg::ExecuteMsg as IssuerExecuteMsg; use cw_utils::must_pay; +use rust_decimal::Decimal; use std::collections::HashSet; use std::ops::Deref; use token_bindings::{TokenFactoryMsg, TokenFactoryQuery}; @@ -12,8 +13,8 @@ use crate::abc::{CommonsPhase, CurveType}; use crate::contract::CwAbcResult; use crate::msg::UpdatePhaseConfigMsg; use crate::state::{ - CURVE_STATE, CURVE_TYPE, DONATIONS, HATCHERS, HATCHER_ALLOWLIST, MAX_SUPPLY, PHASE, - PHASE_CONFIG, SUPPLY_DENOM, TOKEN_ISSUER_CONTRACT, + CURVE_STATE, CURVE_TYPE, DONATIONS, FEES_RECIPIENT, HATCHERS, HATCHER_ALLOWLIST, MAX_SUPPLY, + PHASE, PHASE_CONFIG, SUPPLY_DENOM, TOKEN_ISSUER_CONTRACT, }; use crate::ContractError; @@ -95,17 +96,29 @@ pub fn execute_buy(deps: DepsMut, _env: Env, info: MessageInf // Mint tokens for sender by calling mint on the cw-tokenfactory-issuer contract let issuer_addr = TOKEN_ISSUER_CONTRACT.load(deps.storage)?; - let mint_msg = WasmMsg::Execute { + 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: minted, })?, funds: vec![], + })]; + + // Send funding to fee recipient + if funded > Uint128::zero() { + let fees_recipient = FEES_RECIPIENT.load(deps.storage)?; + msgs.push(CosmosMsg::Bank(BankMsg::Send { + to_address: fees_recipient.to_string(), + amount: vec![Coin { + amount: funded, + denom: curve_state.reserve_denom, + }], + })) }; Ok(Response::new() - .add_message(mint_msg) + .add_messages(msgs) .add_attribute("action", "buy") .add_attribute("from", info.sender) .add_attribute("reserved", reserved) @@ -164,7 +177,7 @@ pub fn execute_sell(deps: DepsMut, _env: Env, info: MessageIn CosmosMsg::::Wasm(WasmMsg::Execute { contract_addr: issuer_addr.to_string(), msg: to_json_binary(&IssuerExecuteMsg::Burn { - from_address: info.sender.to_string(), + from_address: issuer_addr.to_string(), amount: burn_amount, })?, funds: vec![], @@ -184,9 +197,6 @@ pub fn execute_sell(deps: DepsMut, _env: Env, info: MessageIn // Calculate the new reserve based on the new supply let new_reserve = curve.reserve(curve_state.supply); - curve_state.reserve = new_reserve; - curve_state.funding += taxed_amount; - CURVE_STATE.save(deps.storage, &curve_state)?; // Calculate how many reserve tokens to release based on the sell amount let released_reserve = curve_state @@ -194,18 +204,37 @@ pub fn execute_sell(deps: DepsMut, _env: Env, info: MessageIn .checked_sub(new_reserve) .map_err(StdError::overflow)?; - // Now send the tokens to the sender - let msg_send = SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: info.sender.to_string(), - amount: vec![Coin { - amount: released_reserve, - denom: curve_state.reserve_denom, - }], - })); + // Update the curve state + curve_state.reserve = new_reserve; + curve_state.funding += taxed_amount; + CURVE_STATE.save(deps.storage, &curve_state)?; + + // Now send the tokens to the sender and any fees to the DAO + let mut send_msgs: Vec> = + vec![CosmosMsg::::Bank(BankMsg::Send { + to_address: info.sender.to_string(), + amount: vec![Coin { + // TODO Subtract the taxed amount from the released reserve + amount: released_reserve, + denom: curve_state.reserve_denom.clone(), + }], + })]; + + // Send exit fees to the to the fee recipient + if taxed_amount > Uint128::zero() { + let fees_recipient = FEES_RECIPIENT.load(deps.storage)?; + send_msgs.push(CosmosMsg::Bank(BankMsg::Send { + to_address: fees_recipient.to_string(), + amount: vec![Coin { + amount: taxed_amount, + denom: curve_state.reserve_denom, + }], + })) + } Ok(Response::::new() .add_messages(burn_msgs) - .add_submessage(msg_send) + .add_messages(send_msgs) .add_attribute("action", "burn") .add_attribute("from", info.sender) .add_attribute("amount", burn_amount) @@ -223,9 +252,10 @@ fn calculate_exit_tax(storage: &dyn Storage, sell_amount: Uint128) -> CwAbcResul let exit_tax = match &phase { CommonsPhase::Hatch => phase_config.hatch.exit_tax, CommonsPhase::Open => phase_config.open.exit_tax, - CommonsPhase::Closed => return Err(ContractError::CommonsClosed {}), + CommonsPhase::Closed => return Ok(Uint128::zero()), }; + // TODO more normal check? debug_assert!( exit_tax <= StdDecimal::percent(100), "Exit tax must be <= 100%" diff --git a/contracts/external/cw-abc/src/contract.rs b/contracts/external/cw-abc/src/contract.rs index bf7a390ff..091111936 100644 --- a/contracts/external/cw-abc/src/contract.rs +++ b/contracts/external/cw-abc/src/contract.rs @@ -15,10 +15,10 @@ use token_bindings::{TokenFactoryMsg, TokenFactoryQuery}; use crate::abc::{CommonsPhase, CurveFn}; use crate::curves::DecimalPlaces; use crate::error::ContractError; -use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, UpdatePhaseConfigMsg}; +use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; use crate::state::{ - CurveState, CURVE_STATE, CURVE_TYPE, HATCHER_ALLOWLIST, MAX_SUPPLY, PHASE, PHASE_CONFIG, - SUPPLY_DENOM, TOKEN_INSTANTIATION_INFO, TOKEN_ISSUER_CONTRACT, + CurveState, CURVE_STATE, CURVE_TYPE, FEES_RECIPIENT, HATCHER_ALLOWLIST, MAX_SUPPLY, PHASE, + PHASE_CONFIG, SUPPLY_DENOM, TOKEN_INSTANTIATION_INFO, TOKEN_ISSUER_CONTRACT, }; use crate::{commands, queries}; @@ -28,21 +28,19 @@ const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); const INSTANTIATE_TOKEN_FACTORY_ISSUER_REPLY_ID: u64 = 0; -// By default, the prefix for token factory tokens is "factory" -const DENOM_PREFIX: &str = "factory"; - pub type CwAbcResult> = Result; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, - env: Env, + _env: Env, info: MessageInfo, msg: InstantiateMsg, ) -> CwAbcResult { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; let InstantiateMsg { + fees_recipient, supply, reserve, curve_type, @@ -59,6 +57,9 @@ pub fn instantiate( phase_config.validate()?; + // Validate and store the fees recipient + FEES_RECIPIENT.save(deps.storage, &deps.api.addr_validate(&fees_recipient)?)?; + // Save new token info for use in reply TOKEN_INSTANTIATION_INFO.save(deps.storage, &supply)?; @@ -197,6 +198,7 @@ pub fn reply( TOKEN_INSTANTIATION_INFO.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, token_info.subdenom); SUPPLY_DENOM.save(deps.storage, &denom)?; diff --git a/contracts/external/cw-abc/src/msg.rs b/contracts/external/cw-abc/src/msg.rs index f6ee7e0db..879abfce7 100644 --- a/contracts/external/cw-abc/src/msg.rs +++ b/contracts/external/cw-abc/src/msg.rs @@ -5,6 +5,9 @@ use crate::abc::{CommonsPhase, CommonsPhaseConfig, CurveType, MinMax, ReserveTok #[cw_serde] pub struct InstantiateMsg { + /// The recipient for any fees collected from bonding curve operation + pub fees_recipient: String, + /// The code id of the cw-tokenfactory-issuer contract pub token_issuer_code_id: u64, diff --git a/contracts/external/cw-abc/src/state.rs b/contracts/external/cw-abc/src/state.rs index f3b9b55a3..dc4f6b022 100644 --- a/contracts/external/cw-abc/src/state.rs +++ b/contracts/external/cw-abc/src/state.rs @@ -40,6 +40,9 @@ pub const CURVE_STATE: Item = Item::new("curve_state"); pub const CURVE_TYPE: Item = Item::new("curve_type"); +/// The recipient for fees generated from bonding curve operation +pub const FEES_RECIPIENT: Item = Item::new("fees_recipient"); + /// The denom used for the supply token pub const SUPPLY_DENOM: Item = Item::new("denom"); diff --git a/contracts/external/cw-abc/src/test_tube/integration_tests.rs b/contracts/external/cw-abc/src/test_tube/integration_tests.rs index 1bed80c1b..231baf92f 100644 --- a/contracts/external/cw-abc/src/test_tube/integration_tests.rs +++ b/contracts/external/cw-abc/src/test_tube/integration_tests.rs @@ -67,7 +67,7 @@ fn test_happy_path() { contract_balance.balance, Some(Coin { denom: RESERVE.to_string(), - amount: "1000".to_string(), + amount: "900".to_string(), // Minus 10% to fees_recipient }) ); @@ -152,19 +152,19 @@ fn test_happy_path() { user_balance.balance, Some(Coin { denom: denom.clone(), - amount: "8800".to_string(), + amount: "8900".to_string(), }) ); assert_eq!( contract_balance.balance, Some(Coin { denom: RESERVE.to_string(), - amount: "990".to_string(), + amount: "880".to_string(), }) ); // Buy enough tokens to end the hatch phase - abc.execute(&ExecuteMsg::Buy {}, &coins(1000000, RESERVE), &accounts[0]) + abc.execute(&ExecuteMsg::Buy {}, &coins(999999, RESERVE), &accounts[1]) .unwrap(); // Contract is now in open phase @@ -280,6 +280,7 @@ fn test_allowlist() { let app = OsmosisTestApp::new(); let builder = TestEnvBuilder::new(); let instantiate_msg = InstantiateMsg { + fees_recipient: "replaced to accounts[0]".to_string(), token_issuer_code_id: 0, supply: SupplyToken { subdenom: DENOM.to_string(), diff --git a/contracts/external/cw-abc/src/test_tube/test_env.rs b/contracts/external/cw-abc/src/test_tube/test_env.rs index b265225ef..74f3b092d 100644 --- a/contracts/external/cw-abc/src/test_tube/test_env.rs +++ b/contracts/external/cw-abc/src/test_tube/test_env.rs @@ -105,6 +105,7 @@ impl TestEnvBuilder { let abc = CwAbc::deploy( app, &InstantiateMsg { + fees_recipient: accounts[0].address(), token_issuer_code_id: issuer_id, supply: SupplyToken { subdenom: DENOM.to_string(), @@ -168,8 +169,9 @@ impl TestEnvBuilder { let issuer_id = TokenfactoryIssuer::upload(app, &accounts[0])?; - // Override issuer_id + // Override issuer_id and fees_recipient msg.token_issuer_code_id = issuer_id; + msg.fees_recipient = accounts[0].address(); let abc = CwAbc::deploy(app, &msg, &accounts[0])?; diff --git a/contracts/external/cw-abc/src/testing.rs b/contracts/external/cw-abc/src/testing.rs index a99861e3f..c028f2ec6 100644 --- a/contracts/external/cw-abc/src/testing.rs +++ b/contracts/external/cw-abc/src/testing.rs @@ -46,6 +46,7 @@ pub fn default_instantiate_msg( curve_type: CurveType, ) -> InstantiateMsg { InstantiateMsg { + fees_recipient: TEST_CREATOR.to_string(), token_issuer_code_id: 1, supply: SupplyToken { subdenom: TEST_SUPPLY_DENOM.to_string(), From 9c8768e30cd9f0df3999ef3070241c1f7dcff26c Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Tue, 5 Dec 2023 17:09:04 -0800 Subject: [PATCH 38/56] Fix exit fees --- contracts/external/cw-abc/src/commands.rs | 15 ++++++++++----- .../cw-abc/src/test_tube/integration_tests.rs | 4 ++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/contracts/external/cw-abc/src/commands.rs b/contracts/external/cw-abc/src/commands.rs index a54594f93..fad52f0eb 100644 --- a/contracts/external/cw-abc/src/commands.rs +++ b/contracts/external/cw-abc/src/commands.rs @@ -4,7 +4,6 @@ use cosmwasm_std::{ }; use cw_tokenfactory_issuer::msg::ExecuteMsg as IssuerExecuteMsg; use cw_utils::must_pay; -use rust_decimal::Decimal; use std::collections::HashSet; use std::ops::Deref; use token_bindings::{TokenFactoryMsg, TokenFactoryQuery}; @@ -184,8 +183,6 @@ pub fn execute_sell(deps: DepsMut, _env: Env, info: MessageIn }), ]; - let taxed_amount = calculate_exit_tax(deps.storage, burn_amount)?; - let mut curve_state = CURVE_STATE.load(deps.storage)?; let curve = curve_fn(curve_state.clone().decimals); @@ -204,18 +201,26 @@ pub fn execute_sell(deps: DepsMut, _env: Env, info: MessageIn .checked_sub(new_reserve) .map_err(StdError::overflow)?; + // Calculate the exit tax + let taxed_amount = calculate_exit_tax(deps.storage, released_reserve)?; + // Update the curve state curve_state.reserve = new_reserve; curve_state.funding += taxed_amount; CURVE_STATE.save(deps.storage, &curve_state)?; + // Calculate the amount of tokens to send to the sender + // Subtract the taxed amount from the released amount + let released = released_reserve + .checked_sub(taxed_amount) + .map_err(StdError::overflow)?; + // Now send the tokens to the sender and any fees to the DAO let mut send_msgs: Vec> = vec![CosmosMsg::::Bank(BankMsg::Send { to_address: info.sender.to_string(), amount: vec![Coin { - // TODO Subtract the taxed amount from the released reserve - amount: released_reserve, + amount: released, denom: curve_state.reserve_denom.clone(), }], })]; diff --git a/contracts/external/cw-abc/src/test_tube/integration_tests.rs b/contracts/external/cw-abc/src/test_tube/integration_tests.rs index 231baf92f..6b566af0d 100644 --- a/contracts/external/cw-abc/src/test_tube/integration_tests.rs +++ b/contracts/external/cw-abc/src/test_tube/integration_tests.rs @@ -125,7 +125,7 @@ fn test_happy_path() { CurveInfoResponse { reserve: Uint128::new(890), supply: Uint128::new(8900), - funding: Uint128::new(110), + funding: Uint128::new(101), spot_price: Decimal::percent(10u64), reserve_denom: RESERVE.to_string(), } @@ -159,7 +159,7 @@ fn test_happy_path() { contract_balance.balance, Some(Coin { denom: RESERVE.to_string(), - amount: "880".to_string(), + amount: "890".to_string(), }) ); From 5be336ac84bba94a33ff1b4b46894229e466bf1c Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Tue, 5 Dec 2023 17:16:41 -0800 Subject: [PATCH 39/56] Update schema --- contracts/external/cw-abc/schema/cw-abc.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contracts/external/cw-abc/schema/cw-abc.json b/contracts/external/cw-abc/schema/cw-abc.json index fbdd1c117..4479b78cf 100644 --- a/contracts/external/cw-abc/schema/cw-abc.json +++ b/contracts/external/cw-abc/schema/cw-abc.json @@ -8,6 +8,7 @@ "type": "object", "required": [ "curve_type", + "fees_recipient", "phase_config", "reserve", "supply", @@ -22,6 +23,10 @@ } ] }, + "fees_recipient": { + "description": "The recipient for any fees collected from bonding curve operation", + "type": "string" + }, "hatcher_allowlist": { "description": "TODO different ways of doing this, for example DAO members? Using a whitelist contract? Merkle tree? Hatcher allowlist", "type": [ From 0db6203cb9b159bd1934a0cf000ee0e08f23db95 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Wed, 6 Dec 2023 12:49:34 -0800 Subject: [PATCH 40/56] Add more information to the readme --- contracts/external/cw-abc/README.md | 132 +++++++++++++++++++--------- 1 file changed, 90 insertions(+), 42 deletions(-) diff --git a/contracts/external/cw-abc/README.md b/contracts/external/cw-abc/README.md index 443e2d6cf..9df1bbd85 100644 --- a/contracts/external/cw-abc/README.md +++ b/contracts/external/cw-abc/README.md @@ -1,12 +1,12 @@ # cw-abc -Implments an augmented bonding curve. +Implments 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 unaudited and experimental. NOT RECOMMENDED FOR PRODUCTION USE. -## Overview +## What are Augmented Bonding Curves? Before we get to the *Augmented* part, we must first describe bonding curves themselves. ### Token Bonding Curves @@ -23,40 +23,11 @@ This contract implements two methods: It is possible to use this contact as a basic bonding curve, without any of the augmented features. -#### Benefits - -There are a number of benefits to bonding curves: -- There is enough liquidity to back the entire supply -- 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. -- DAO treasuries can be funded by entry and exit fees on the curve - -## Extended Reading - -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 - -Read more about [bonding curve math here](https://yos.io/2018/11/10/bonding-curves/). - -## Future Work - -Taking inspiration from [this article](https://medium.com/commonsstack/deep-dive-augmented-bonding-curves-b5ca4fad4436) on augmented bonding curves: - -- [ ] Optionally vest tokens during the hatch phase -- [ ] Update `cw-vesting` to allow for partcipating in DAOs? - -### Math +#### 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. +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? +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` @@ -64,13 +35,9 @@ 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. +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. +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) @@ -82,5 +49,86 @@ 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)` -We will only implement these curves to start with, and leave it to others to import this with more complex curves, -such as logarithms. +[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 funcationality: +- Entry and exit fees +- Different phases representing the life cycles of projects + +## Features + +Example Instantiation message: + +``` json +{ + "fees_recipient": "address that recieves 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" + }, + "initial_allocation_ratio": "0.25", + "exit_tax": "0.10" + }, + "open": { + "exit_tax": "0.01", + "allocation_percentage": "0.01" + }, + "closed": {} + }, + hatcher_allowlist: ["allowlist addresses, leave blank for no allowlist"], +} +``` + +- `fees_recipient`: the address that will recieve fees (usually a DAO). +- `token_issuer_code_id`: the CosmWasm code ID for a `cw-tokenfactory_issuer` contract. +- `supply`: infor 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 + From 71832ce82fa832075eb45cbbefd11629b04914f7 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Wed, 6 Dec 2023 13:01:57 -0800 Subject: [PATCH 41/56] Rename variables to make them more consistent and understandable --- contracts/external/cw-abc/README.md | 8 ++-- contracts/external/cw-abc/schema/cw-abc.json | 40 ++++++++--------- contracts/external/cw-abc/src/abc.rs | 24 +++++----- contracts/external/cw-abc/src/commands.rs | 44 +++++++++---------- contracts/external/cw-abc/src/msg.rs | 8 ++-- .../cw-abc/src/test_tube/integration_tests.rs | 16 +++---- .../external/cw-abc/src/test_tube/test_env.rs | 8 ++-- contracts/external/cw-abc/src/testing.rs | 8 ++-- 8 files changed, 77 insertions(+), 79 deletions(-) diff --git a/contracts/external/cw-abc/README.md b/contracts/external/cw-abc/README.md index 9df1bbd85..64342e095 100644 --- a/contracts/external/cw-abc/README.md +++ b/contracts/external/cw-abc/README.md @@ -107,12 +107,12 @@ Example Instantiation message: "min": "10000000", "max": "100000000000" }, - "initial_allocation_ratio": "0.25", - "exit_tax": "0.10" + "entry_fee": "0.25", + "exit_fee": "0.10" }, "open": { - "exit_tax": "0.01", - "allocation_percentage": "0.01" + "exit_fee": "0.01", + "entry_fee": "0.01" }, "closed": {} }, diff --git a/contracts/external/cw-abc/schema/cw-abc.json b/contracts/external/cw-abc/schema/cw-abc.json index 4479b78cf..c703f505e 100644 --- a/contracts/external/cw-abc/schema/cw-abc.json +++ b/contracts/external/cw-abc/schema/cw-abc.json @@ -233,8 +233,8 @@ "type": "object", "required": [ "contribution_limits", - "exit_tax", - "initial_allocation_ratio", + "exit_fee", + "entry_fee", "initial_raise" ], "properties": { @@ -246,7 +246,7 @@ } ] }, - "exit_tax": { + "exit_fee": { "description": "Exit tax for the hatch phase", "allOf": [ { @@ -254,7 +254,7 @@ } ] }, - "initial_allocation_ratio": { + "entry_fee": { "description": "The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool", "allOf": [ { @@ -331,11 +331,11 @@ "OpenConfig": { "type": "object", "required": [ - "allocation_percentage", - "exit_tax" + "entry_fee", + "exit_fee" ], "properties": { - "allocation_percentage": { + "entry_fee": { "description": "Percentage of capital put into the Reserve Pool during the Open phase when buying from the curve.", "allOf": [ { @@ -343,7 +343,7 @@ } ] }, - "exit_tax": { + "exit_fee": { "description": "Exit taxation ratio", "allOf": [ { @@ -838,7 +838,7 @@ } ] }, - "exit_tax": { + "exit_fee": { "anyOf": [ { "$ref": "#/definitions/Decimal" @@ -848,7 +848,7 @@ } ] }, - "initial_allocation_ratio": { + "entry_fee": { "anyOf": [ { "$ref": "#/definitions/Decimal" @@ -884,7 +884,7 @@ "open": { "type": "object", "properties": { - "allocation_percentage": { + "entry_fee": { "anyOf": [ { "$ref": "#/definitions/Decimal" @@ -894,7 +894,7 @@ } ] }, - "exit_tax": { + "exit_fee": { "anyOf": [ { "$ref": "#/definitions/Decimal" @@ -1396,8 +1396,8 @@ "type": "object", "required": [ "contribution_limits", - "exit_tax", - "initial_allocation_ratio", + "exit_fee", + "entry_fee", "initial_raise" ], "properties": { @@ -1409,7 +1409,7 @@ } ] }, - "exit_tax": { + "exit_fee": { "description": "Exit tax for the hatch phase", "allOf": [ { @@ -1417,7 +1417,7 @@ } ] }, - "initial_allocation_ratio": { + "entry_fee": { "description": "The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool", "allOf": [ { @@ -1456,11 +1456,11 @@ "OpenConfig": { "type": "object", "required": [ - "allocation_percentage", - "exit_tax" + "entry_fee", + "exit_fee" ], "properties": { - "allocation_percentage": { + "entry_fee": { "description": "Percentage of capital put into the Reserve Pool during the Open phase when buying from the curve.", "allOf": [ { @@ -1468,7 +1468,7 @@ } ] }, - "exit_tax": { + "exit_fee": { "description": "Exit taxation ratio", "allOf": [ { diff --git a/contracts/external/cw-abc/src/abc.rs b/contracts/external/cw-abc/src/abc.rs index 8b6263784..30f1f430b 100644 --- a/contracts/external/cw-abc/src/abc.rs +++ b/contracts/external/cw-abc/src/abc.rs @@ -41,9 +41,9 @@ pub struct HatchConfig { /// 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 initial_allocation_ratio: StdDecimal, + pub entry_fee: StdDecimal, /// Exit tax for the hatch phase - pub exit_tax: StdDecimal, + pub exit_fee: StdDecimal, } impl HatchConfig { @@ -65,14 +65,14 @@ impl HatchConfig { ); ensure!( - self.initial_allocation_ratio <= StdDecimal::percent(100u64), + self.entry_fee <= StdDecimal::percent(100u64), ContractError::HatchPhaseConfigError( "Initial allocation percentage must be between 0 and 100.".to_string() ) ); ensure!( - self.exit_tax <= StdDecimal::percent(100u64), + self.exit_fee <= StdDecimal::percent(100u64), ContractError::HatchPhaseConfigError( "Exit taxation percentage must be less than or equal to 100.".to_string() ) @@ -86,23 +86,23 @@ impl HatchConfig { pub struct OpenConfig { /// Percentage of capital put into the Reserve Pool during the Open phase /// when buying from the curve. - pub allocation_percentage: StdDecimal, + pub entry_fee: StdDecimal, /// Exit taxation ratio - pub exit_tax: StdDecimal, + pub exit_fee: StdDecimal, } impl OpenConfig { /// Validate the open config pub fn validate(&self) -> Result<(), ContractError> { ensure!( - self.allocation_percentage <= StdDecimal::percent(100u64), + self.entry_fee <= StdDecimal::percent(100u64), ContractError::OpenPhaseConfigError( "Reserve percentage must be between 0 and 100.".to_string() ) ); ensure!( - self.exit_tax <= StdDecimal::percent(100u64), + self.exit_fee <= StdDecimal::percent(100u64), ContractError::OpenPhaseConfigError( "Exit taxation percentage must be between 0 and 100.".to_string() ) @@ -243,12 +243,12 @@ mod unit_tests { min: Uint128::one(), max: Uint128::from(1000000u128), }, - initial_allocation_ratio: StdDecimal::percent(10u64), - exit_tax: StdDecimal::percent(10u64), + entry_fee: StdDecimal::percent(10u64), + exit_fee: StdDecimal::percent(10u64), }, open: OpenConfig { - allocation_percentage: StdDecimal::percent(10u64), - exit_tax: StdDecimal::percent(10u64), + entry_fee: StdDecimal::percent(10u64), + exit_fee: StdDecimal::percent(10u64), }, closed: ClosedConfig {}, }; diff --git a/contracts/external/cw-abc/src/commands.rs b/contracts/external/cw-abc/src/commands.rs index fad52f0eb..c10dd881e 100644 --- a/contracts/external/cw-abc/src/commands.rs +++ b/contracts/external/cw-abc/src/commands.rs @@ -61,11 +61,9 @@ pub fn execute_buy(deps: DepsMut, _env: Env, info: MessageInf PHASE.save(deps.storage, &phase)?; } - calculate_reserved_and_funded(payment, hatch_config.initial_allocation_ratio) - } - CommonsPhase::Open => { - calculate_reserved_and_funded(payment, phase_config.open.allocation_percentage) + calculate_reserved_and_funded(payment, hatch_config.entry_fee) } + CommonsPhase::Open => calculate_reserved_and_funded(payment, phase_config.open.entry_fee), CommonsPhase::Closed => { return Err(ContractError::CommonsClosed {}); } @@ -202,7 +200,7 @@ pub fn execute_sell(deps: DepsMut, _env: Env, info: MessageIn .map_err(StdError::overflow)?; // Calculate the exit tax - let taxed_amount = calculate_exit_tax(deps.storage, released_reserve)?; + let taxed_amount = calculate_exit_fee(deps.storage, released_reserve)?; // Update the curve state curve_state.reserve = new_reserve; @@ -248,26 +246,26 @@ pub fn execute_sell(deps: DepsMut, _env: Env, info: MessageIn } /// Calculate the exit taxation for the sell amount based on the phase -fn calculate_exit_tax(storage: &dyn Storage, sell_amount: Uint128) -> CwAbcResult { +fn calculate_exit_fee(storage: &dyn Storage, sell_amount: Uint128) -> CwAbcResult { // Load the phase config and phase let phase = PHASE.load(storage)?; let phase_config = PHASE_CONFIG.load(storage)?; // Calculate the exit tax based on the phase - let exit_tax = match &phase { - CommonsPhase::Hatch => phase_config.hatch.exit_tax, - CommonsPhase::Open => phase_config.open.exit_tax, + let exit_fee = match &phase { + CommonsPhase::Hatch => phase_config.hatch.exit_fee, + CommonsPhase::Open => phase_config.open.exit_fee, CommonsPhase::Closed => return Ok(Uint128::zero()), }; // TODO more normal check? debug_assert!( - exit_tax <= StdDecimal::percent(100), + exit_fee <= StdDecimal::percent(100), "Exit tax must be <= 100%" ); // This won't ever overflow because it's checked - let taxed_amount = sell_amount * exit_tax; + let taxed_amount = sell_amount * exit_fee; Ok(taxed_amount) } @@ -386,9 +384,9 @@ pub fn update_phase_config( match update_phase_config_msg { UpdatePhaseConfigMsg::Hatch { - exit_tax, + exit_fee, initial_raise, - initial_allocation_ratio, + entry_fee, contribution_limits, } => { // Check we are in the hatch phase @@ -398,14 +396,14 @@ pub fn update_phase_config( if let Some(contribution_limits) = contribution_limits { phase_config.hatch.contribution_limits = contribution_limits; } - if let Some(exit_tax) = exit_tax { - phase_config.hatch.exit_tax = exit_tax; + if let Some(exit_fee) = exit_fee { + phase_config.hatch.exit_fee = exit_fee; } if let Some(initial_raise) = initial_raise { phase_config.hatch.initial_raise = initial_raise; } - if let Some(initial_allocation_ratio) = initial_allocation_ratio { - phase_config.hatch.initial_allocation_ratio = initial_allocation_ratio; + if let Some(entry_fee) = entry_fee { + phase_config.hatch.entry_fee = entry_fee; } // Validate config @@ -415,18 +413,18 @@ pub fn update_phase_config( Ok(Response::new().add_attribute("action", "update_hatch_phase_config")) } UpdatePhaseConfigMsg::Open { - exit_tax, - allocation_percentage, + 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(allocation_percentage) = allocation_percentage { - phase_config.open.allocation_percentage = allocation_percentage; + if let Some(entry_fee) = entry_fee { + phase_config.open.entry_fee = entry_fee; } - if let Some(exit_tax) = exit_tax { - phase_config.hatch.exit_tax = exit_tax; + if let Some(exit_fee) = exit_fee { + phase_config.hatch.exit_fee = exit_fee; } // Validate config diff --git a/contracts/external/cw-abc/src/msg.rs b/contracts/external/cw-abc/src/msg.rs index 879abfce7..1f4ca3519 100644 --- a/contracts/external/cw-abc/src/msg.rs +++ b/contracts/external/cw-abc/src/msg.rs @@ -36,15 +36,15 @@ pub enum UpdatePhaseConfigMsg { /// Update the hatch phase configuration Hatch { contribution_limits: Option, - exit_tax: Option, // TODO what is the minimum used for? initial_raise: Option, - initial_allocation_ratio: Option, + entry_fee: Option, + exit_fee: Option, }, /// Update the open phase configuration. Open { - exit_tax: Option, - allocation_percentage: Option, + exit_fee: Option, + entry_fee: Option, }, /// Update the closed phase configuration. /// TODO Set the curve type to be used on close? diff --git a/contracts/external/cw-abc/src/test_tube/integration_tests.rs b/contracts/external/cw-abc/src/test_tube/integration_tests.rs index 6b566af0d..7f88f5a5d 100644 --- a/contracts/external/cw-abc/src/test_tube/integration_tests.rs +++ b/contracts/external/cw-abc/src/test_tube/integration_tests.rs @@ -99,12 +99,12 @@ fn test_happy_path() { min: Uint128::from(10u128), max: Uint128::from(1000000u128), }, - initial_allocation_ratio: Decimal::percent(10u64), - exit_tax: Decimal::percent(10u64), + entry_fee: Decimal::percent(10u64), + exit_fee: Decimal::percent(10u64), }, open: OpenConfig { - allocation_percentage: Decimal::percent(10u64), - exit_tax: Decimal::percent(10u64), + entry_fee: Decimal::percent(10u64), + exit_fee: Decimal::percent(10u64), }, closed: ClosedConfig {}, } @@ -302,12 +302,12 @@ fn test_allowlist() { min: Uint128::from(10u128), max: Uint128::from(1000000u128), }, - initial_allocation_ratio: Decimal::percent(10u64), - exit_tax: Decimal::percent(10u64), + entry_fee: Decimal::percent(10u64), + exit_fee: Decimal::percent(10u64), }, open: OpenConfig { - allocation_percentage: Decimal::percent(10u64), - exit_tax: Decimal::percent(10u64), + entry_fee: Decimal::percent(10u64), + exit_fee: Decimal::percent(10u64), }, closed: ClosedConfig {}, }, diff --git a/contracts/external/cw-abc/src/test_tube/test_env.rs b/contracts/external/cw-abc/src/test_tube/test_env.rs index 74f3b092d..28dec2752 100644 --- a/contracts/external/cw-abc/src/test_tube/test_env.rs +++ b/contracts/external/cw-abc/src/test_tube/test_env.rs @@ -127,12 +127,12 @@ impl TestEnvBuilder { min: Uint128::from(10u128), max: Uint128::from(1000000u128), }, - initial_allocation_ratio: Decimal::percent(10u64), - exit_tax: Decimal::percent(10u64), + entry_fee: Decimal::percent(10u64), + exit_fee: Decimal::percent(10u64), }, open: OpenConfig { - allocation_percentage: Decimal::percent(10u64), - exit_tax: Decimal::percent(10u64), + entry_fee: Decimal::percent(10u64), + exit_fee: Decimal::percent(10u64), }, closed: ClosedConfig {}, }, diff --git a/contracts/external/cw-abc/src/testing.rs b/contracts/external/cw-abc/src/testing.rs index c028f2ec6..39d483613 100644 --- a/contracts/external/cw-abc/src/testing.rs +++ b/contracts/external/cw-abc/src/testing.rs @@ -68,12 +68,12 @@ pub fn default_instantiate_msg( min: Uint128::one(), max: Uint128::from(1000000u128), }, - initial_allocation_ratio: Decimal::percent(10u64), - exit_tax: Decimal::zero(), + entry_fee: Decimal::percent(10u64), + exit_fee: Decimal::zero(), }, open: OpenConfig { - allocation_percentage: Decimal::percent(10u64), - exit_tax: Decimal::percent(10u64), + entry_fee: Decimal::percent(10u64), + exit_fee: Decimal::percent(10u64), }, closed: ClosedConfig {}, }, From 33927a1f23daf8ce4ac2b64b26a7a76335bad181 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Tue, 12 Dec 2023 14:30:03 -0800 Subject: [PATCH 42/56] More queries, update schema --- contracts/external/cw-abc/schema/cw-abc.json | 210 +++++++++++++++++-- contracts/external/cw-abc/src/contract.rs | 8 +- contracts/external/cw-abc/src/msg.rs | 30 ++- contracts/external/cw-abc/src/queries.rs | 31 +-- 4 files changed, 237 insertions(+), 42 deletions(-) diff --git a/contracts/external/cw-abc/schema/cw-abc.json b/contracts/external/cw-abc/schema/cw-abc.json index c703f505e..0a0187250 100644 --- a/contracts/external/cw-abc/schema/cw-abc.json +++ b/contracts/external/cw-abc/schema/cw-abc.json @@ -233,8 +233,8 @@ "type": "object", "required": [ "contribution_limits", - "exit_fee", "entry_fee", + "exit_fee", "initial_raise" ], "properties": { @@ -246,16 +246,16 @@ } ] }, - "exit_fee": { - "description": "Exit tax for the hatch phase", + "entry_fee": { + "description": "The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool", "allOf": [ { "$ref": "#/definitions/Decimal" } ] }, - "entry_fee": { - "description": "The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool", + "exit_fee": { + "description": "Exit tax for the hatch phase", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -838,7 +838,7 @@ } ] }, - "exit_fee": { + "entry_fee": { "anyOf": [ { "$ref": "#/definitions/Decimal" @@ -848,7 +848,7 @@ } ] }, - "entry_fee": { + "exit_fee": { "anyOf": [ { "$ref": "#/definitions/Decimal" @@ -947,13 +947,27 @@ "additionalProperties": false }, { - "description": "Returns the current phase configuration Returns [`CommonsPhaseConfigResponse`]", + "description": "Returns information about the curve type (i.e. linear, constant, etc.)", "type": "object", "required": [ - "phase_config" + "curve_type" ], "properties": { - "phase_config": { + "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 } @@ -990,6 +1004,20 @@ }, "additionalProperties": false }, + { + "description": "Returns the Fee Recipient for the contract. This is the address that recieves any fees collected from bonding curve operation", + "type": "object", + "required": [ + "fees_recipient" + ], + "properties": { + "fees_recipient": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "List the hatchers and their contributions Returns [`HatchersResponse`]", "type": "object", @@ -1021,13 +1049,41 @@ "additionalProperties": false }, { - "description": "Returns Token Factory Denom for the supply", + "description": "Returns the Maxiumum Supply of the supply token", "type": "object", "required": [ - "denom" + "max_supply" ], "properties": { - "denom": { + "max_supply": { + "type": "object", + "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 } @@ -1128,6 +1184,102 @@ } } }, + "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", @@ -1180,6 +1332,12 @@ } } }, + "fees_recipient": { + "$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" + }, "hatchers": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "HatchersResponse", @@ -1218,6 +1376,12 @@ } } }, + "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", @@ -1313,6 +1477,16 @@ } } }, + "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", @@ -1396,8 +1570,8 @@ "type": "object", "required": [ "contribution_limits", - "exit_fee", "entry_fee", + "exit_fee", "initial_raise" ], "properties": { @@ -1409,16 +1583,16 @@ } ] }, - "exit_fee": { - "description": "Exit tax for the hatch phase", + "entry_fee": { + "description": "The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool", "allOf": [ { "$ref": "#/definitions/Decimal" } ] }, - "entry_fee": { - "description": "The initial allocation (θ), percentage of the initial raise allocated to the Funding Pool", + "exit_fee": { + "description": "Exit tax for the hatch phase", "allOf": [ { "$ref": "#/definitions/Decimal" diff --git a/contracts/external/cw-abc/src/contract.rs b/contracts/external/cw-abc/src/contract.rs index 091111936..968516e54 100644 --- a/contracts/external/cw-abc/src/contract.rs +++ b/contracts/external/cw-abc/src/contract.rs @@ -157,15 +157,19 @@ pub fn do_query( match msg { // custom queries QueryMsg::CurveInfo {} => to_json_binary(&queries::query_curve_info(deps, curve_fn)?), - QueryMsg::PhaseConfig {} => to_json_binary(&queries::query_phase_config(deps)?), + 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::FeesRecipient {} => to_json_binary(&FEES_RECIPIENT.load(deps.storage)?), QueryMsg::Hatchers { start_after, limit } => { to_json_binary(&queries::query_hatchers(deps, start_after, limit)?) } + QueryMsg::MaxSupply {} => to_json_binary(&queries::query_max_supply(deps)?), QueryMsg::Ownership {} => to_json_binary(&cw_ownable::get_ownership(deps.storage)?), - QueryMsg::Denom {} => to_json_binary(&queries::get_denom(deps)?), + 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)?), } } diff --git a/contracts/external/cw-abc/src/msg.rs b/contracts/external/cw-abc/src/msg.rs index 1f4ca3519..c201760e2 100644 --- a/contracts/external/cw-abc/src/msg.rs +++ b/contracts/external/cw-abc/src/msg.rs @@ -88,9 +88,6 @@ pub enum ExecuteMsg { Close {}, } -// TODO Price queries: -// - Price to buy a certain amount? -// - What can be bought for a certain amount? #[cw_ownable::cw_ownable_query] #[cw_serde] #[derive(QueryResponses)] @@ -99,10 +96,12 @@ pub enum QueryMsg { /// Returns [`CurveInfoResponse`] #[returns(CurveInfoResponse)] CurveInfo {}, - /// Returns the current phase configuration - /// Returns [`CommonsPhaseConfigResponse`] - #[returns(CommonsPhaseConfigResponse)] - PhaseConfig {}, + /// 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)] @@ -110,6 +109,10 @@ pub enum QueryMsg { start_after: Option, limit: Option, }, + /// Returns the Fee Recipient for the contract. This is the address that + /// recieves any fees collected from bonding curve operation + #[returns(::cosmwasm_std::Addr)] + FeesRecipient {}, /// List the hatchers and their contributions /// Returns [`HatchersResponse`] #[returns(HatchersResponse)] @@ -117,9 +120,16 @@ pub enum QueryMsg { start_after: Option, limit: Option, }, - /// Returns Token Factory Denom for the supply - #[returns(DenomResponse)] - Denom {}, + /// Returns the Maxiumum Supply of the supply token + #[returns(Uint128)] + MaxSupply {}, + /// 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 {}, diff --git a/contracts/external/cw-abc/src/queries.rs b/contracts/external/cw-abc/src/queries.rs index b83cd70a9..1d113df3d 100644 --- a/contracts/external/cw-abc/src/queries.rs +++ b/contracts/external/cw-abc/src/queries.rs @@ -4,9 +4,9 @@ use crate::msg::{ HatchersResponse, }; use crate::state::{ - CurveState, CURVE_STATE, DONATIONS, HATCHERS, PHASE, PHASE_CONFIG, SUPPLY_DENOM, + CurveState, CURVE_STATE, DONATIONS, HATCHERS, MAX_SUPPLY, PHASE, PHASE_CONFIG, SUPPLY_DENOM, }; -use cosmwasm_std::{Deps, Order, QuerierWrapper, StdResult}; +use cosmwasm_std::{Deps, Order, QuerierWrapper, StdResult, Uint128}; use std::ops::Deref; use token_bindings::TokenFactoryQuery; @@ -36,16 +36,6 @@ pub fn query_curve_info( }) } -/// 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, - }) -} - /// Returns information about the supply Denom pub fn get_denom(deps: Deps) -> StdResult { let denom = SUPPLY_DENOM.load(deps.storage)?; @@ -75,6 +65,7 @@ pub fn query_donations( Ok(DonationsResponse { donations }) } +/// Query hatchers who contributed during the hatch phase pub fn query_hatchers( deps: Deps, start_aftor: Option, @@ -97,3 +88,19 @@ pub fn query_hatchers( Ok(HatchersResponse { hatchers }) } + +/// 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, + }) +} From 84811db889afb3914f143d2fe8fca42f88d898f4 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Tue, 12 Dec 2023 14:41:41 -0800 Subject: [PATCH 43/56] Remove TokenFactoryMsg and TokenFactoryQuery, no token-bindings dep --- Cargo.lock | 1 - contracts/external/cw-abc/Cargo.toml | 1 - contracts/external/cw-abc/src/commands.rs | 80 ++++++++++++----------- contracts/external/cw-abc/src/contract.rs | 34 +++------- contracts/external/cw-abc/src/queries.rs | 16 ++--- contracts/external/cw-abc/src/testing.rs | 42 ++++++------ 6 files changed, 78 insertions(+), 96 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0fd7a8ba9..578b1294c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -749,7 +749,6 @@ dependencies = [ "serde_json", "speculoos", "thiserror", - "token-bindings", ] [[package]] diff --git a/contracts/external/cw-abc/Cargo.toml b/contracts/external/cw-abc/Cargo.toml index 1676c2fa5..43abe028c 100644 --- a/contracts/external/cw-abc/Cargo.toml +++ b/contracts/external/cw-abc/Cargo.toml @@ -41,7 +41,6 @@ integer-sqrt = { workspace = true } integer-cbrt = { workspace = true } getrandom = { workspace = true, features = ["js"] } thiserror = { workspace = true } -token-bindings = { workspace = true } [dev-dependencies] speculoos = { workspace = true } diff --git a/contracts/external/cw-abc/src/commands.rs b/contracts/external/cw-abc/src/commands.rs index c10dd881e..9a3ddf8ca 100644 --- a/contracts/external/cw-abc/src/commands.rs +++ b/contracts/external/cw-abc/src/commands.rs @@ -6,10 +6,8 @@ use cw_tokenfactory_issuer::msg::ExecuteMsg as IssuerExecuteMsg; use cw_utils::must_pay; use std::collections::HashSet; use std::ops::Deref; -use token_bindings::{TokenFactoryMsg, TokenFactoryQuery}; use crate::abc::{CommonsPhase, CurveType}; -use crate::contract::CwAbcResult; use crate::msg::UpdatePhaseConfigMsg; use crate::state::{ CURVE_STATE, CURVE_TYPE, DONATIONS, FEES_RECIPIENT, HATCHERS, HATCHER_ALLOWLIST, MAX_SUPPLY, @@ -17,7 +15,7 @@ use crate::state::{ }; use crate::ContractError; -pub fn execute_buy(deps: DepsMut, _env: Env, info: MessageInfo) -> CwAbcResult { +pub fn execute_buy(deps: DepsMut, _env: Env, info: MessageInfo) -> Result { let curve_type = CURVE_TYPE.load(deps.storage)?; let curve_fn = curve_type.to_curve_fn(); @@ -93,7 +91,7 @@ pub fn execute_buy(deps: DepsMut, _env: Env, info: MessageInf // 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 { + 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(), @@ -151,7 +149,11 @@ fn update_hatcher_contributions( } /// Sell tokens on the bonding curve -pub fn execute_sell(deps: DepsMut, _env: Env, info: MessageInfo) -> CwAbcResult { +pub fn execute_sell( + deps: DepsMut, + _env: Env, + info: MessageInfo, +) -> Result { let curve_type = CURVE_TYPE.load(deps.storage)?; let curve_fn = curve_type.to_curve_fn(); @@ -161,9 +163,9 @@ pub fn execute_sell(deps: DepsMut, _env: Env, info: MessageIn let issuer_addr = TOKEN_ISSUER_CONTRACT.load(deps.storage)?; // Burn the sent supply tokens - let burn_msgs: Vec> = vec![ + let burn_msgs: Vec = vec![ // Send tokens to the issuer contract to be burned - CosmosMsg::::Bank(BankMsg::Send { + CosmosMsg::Bank(BankMsg::Send { to_address: issuer_addr.to_string().clone(), amount: vec![Coin { amount: burn_amount, @@ -171,7 +173,7 @@ pub fn execute_sell(deps: DepsMut, _env: Env, info: MessageIn }], }), // Execute burn on the cw-tokenfactory-issuer contract - CosmosMsg::::Wasm(WasmMsg::Execute { + CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: issuer_addr.to_string(), msg: to_json_binary(&IssuerExecuteMsg::Burn { from_address: issuer_addr.to_string(), @@ -214,14 +216,13 @@ pub fn execute_sell(deps: DepsMut, _env: Env, info: MessageIn .map_err(StdError::overflow)?; // Now send the tokens to the sender and any fees to the DAO - let mut send_msgs: Vec> = - vec![CosmosMsg::::Bank(BankMsg::Send { - to_address: info.sender.to_string(), - amount: vec![Coin { - amount: released, - denom: curve_state.reserve_denom.clone(), - }], - })]; + let mut send_msgs: Vec = vec![CosmosMsg::Bank(BankMsg::Send { + to_address: info.sender.to_string(), + amount: vec![Coin { + amount: released, + denom: curve_state.reserve_denom.clone(), + }], + })]; // Send exit fees to the to the fee recipient if taxed_amount > Uint128::zero() { @@ -235,7 +236,7 @@ pub fn execute_sell(deps: DepsMut, _env: Env, info: MessageIn })) } - Ok(Response::::new() + Ok(Response::new() .add_messages(burn_msgs) .add_messages(send_msgs) .add_attribute("action", "burn") @@ -246,7 +247,10 @@ pub fn execute_sell(deps: DepsMut, _env: Env, info: MessageIn } /// Calculate the exit taxation for the sell amount based on the phase -fn calculate_exit_fee(storage: &dyn Storage, sell_amount: Uint128) -> CwAbcResult { +fn calculate_exit_fee( + storage: &dyn Storage, + sell_amount: Uint128, +) -> Result { // Load the phase config and phase let phase = PHASE.load(storage)?; let phase_config = PHASE_CONFIG.load(storage)?; @@ -270,7 +274,7 @@ fn calculate_exit_fee(storage: &dyn Storage, sell_amount: Uint128) -> CwAbcResul } /// Transitions the bonding curve to a closed phase where only sells are allowed -pub fn execute_close(deps: DepsMut, info: MessageInfo) -> CwAbcResult { +pub fn execute_close(deps: DepsMut, info: MessageInfo) -> Result { cw_ownable::assert_owner(deps.storage, &info.sender)?; PHASE.save(deps.storage, &CommonsPhase::Closed)?; @@ -280,10 +284,10 @@ pub fn execute_close(deps: DepsMut, info: MessageInfo) -> CwA /// Send a donation to the funding pool pub fn execute_donate( - deps: DepsMut, + deps: DepsMut, _env: Env, info: MessageInfo, -) -> CwAbcResult { +) -> Result { let mut curve_state = CURVE_STATE.load(deps.storage)?; let payment = must_pay(&info, &curve_state.reserve_denom)?; @@ -317,10 +321,10 @@ fn assert_allowlisted(storage: &dyn Storage, hatcher: &Addr) -> Result<(), Contr /// Set the maxiumum supply (only callable by owner) /// If `max_supply` is set to None there will be no limit.` pub fn update_max_supply( - deps: DepsMut, + deps: DepsMut, info: MessageInfo, max_supply: Option, -) -> CwAbcResult { +) -> Result { cw_ownable::assert_owner(deps.storage, &info.sender)?; match max_supply { @@ -335,11 +339,11 @@ pub fn update_max_supply( /// Add and remove addresses from the hatcher allowlist pub fn update_hatch_allowlist( - deps: DepsMut, + deps: DepsMut, info: MessageInfo, to_add: Vec, to_remove: Vec, -) -> CwAbcResult { +) -> Result { cw_ownable::assert_owner(deps.storage, &info.sender)?; let mut allowlist = HATCHER_ALLOWLIST.may_load(deps.storage)?; @@ -368,11 +372,11 @@ pub fn update_hatch_allowlist( /// Update the configuration of a particular phase pub fn update_phase_config( - deps: DepsMut, + deps: DepsMut, _env: Env, info: MessageInfo, update_phase_config_msg: UpdatePhaseConfigMsg, -) -> CwAbcResult { +) -> Result { // Assert that the sender is the contract owner cw_ownable::assert_owner(deps.storage, &info.sender)?; @@ -442,10 +446,10 @@ pub fn update_phase_config( /// 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, + deps: DepsMut, info: MessageInfo, curve_type: CurveType, -) -> CwAbcResult { +) -> Result { cw_ownable::assert_owner(deps.storage, &info.sender)?; CURVE_TYPE.save(deps.storage, &curve_type)?; @@ -455,11 +459,11 @@ pub fn update_curve( /// Update the ownership of the contract pub fn update_ownership( - deps: DepsMut, + deps: DepsMut, env: &Env, info: &MessageInfo, action: cw_ownable::Action, -) -> Result, ContractError> { +) -> Result { let ownership = cw_ownable::update_ownership( DepsMut { storage: deps.storage, @@ -489,7 +493,7 @@ mod tests { const TEST_DONOR: &str = "donor"; - fn exec_donate(deps: DepsMut, donation_amount: u128) -> CwAbcResult { + fn exec_donate(deps: DepsMut, donation_amount: u128) -> Result { execute_donate( deps, mock_env(), @@ -498,8 +502,8 @@ mod tests { } #[test] - fn should_fail_with_no_funds() -> CwAbcResult<()> { - let mut deps = mock_tf_dependencies(); + fn should_fail_with_no_funds() -> Result<(), ContractError> { + let mut deps = mock_dependencies(); let curve_type = CurveType::Linear { slope: Uint128::new(1), scale: 1, @@ -516,8 +520,8 @@ mod tests { } #[test] - fn should_fail_with_incorrect_denom() -> CwAbcResult<()> { - let mut deps = mock_tf_dependencies(); + fn should_fail_with_incorrect_denom() -> Result<(), ContractError> { + let mut deps = mock_dependencies(); let curve_type = CurveType::Linear { slope: Uint128::new(1), scale: 1, @@ -540,8 +544,8 @@ mod tests { } #[test] - fn should_add_to_funding_pool() -> CwAbcResult<()> { - let mut deps = mock_tf_dependencies(); + fn should_add_to_funding_pool() -> 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), diff --git a/contracts/external/cw-abc/src/contract.rs b/contracts/external/cw-abc/src/contract.rs index 968516e54..1c3b7e294 100644 --- a/contracts/external/cw-abc/src/contract.rs +++ b/contracts/external/cw-abc/src/contract.rs @@ -10,7 +10,6 @@ use cw_tokenfactory_issuer::msg::{ }; use cw_utils::parse_reply_instantiate_data; use std::collections::HashSet; -use token_bindings::{TokenFactoryMsg, TokenFactoryQuery}; use crate::abc::{CommonsPhase, CurveFn}; use crate::curves::DecimalPlaces; @@ -28,15 +27,13 @@ const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); const INSTANTIATE_TOKEN_FACTORY_ISSUER_REPLY_ID: u64 = 0; -pub type CwAbcResult> = Result; - #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( - deps: DepsMut, + deps: DepsMut, _env: Env, info: MessageInfo, msg: InstantiateMsg, -) -> CwAbcResult { +) -> Result { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; let InstantiateMsg { @@ -110,11 +107,11 @@ pub fn instantiate( #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( - deps: DepsMut, + deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, -) -> CwAbcResult { +) -> Result { match msg { ExecuteMsg::Buy {} => commands::execute_buy(deps, env, info), ExecuteMsg::Sell {} => commands::execute_sell(deps, env, info), @@ -137,7 +134,7 @@ pub fn execute( } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { +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)?; @@ -148,12 +145,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResul /// 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 { +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)?), @@ -175,22 +167,14 @@ pub fn do_query( } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate( - deps: DepsMut, - _env: Env, - _msg: MigrateMsg, -) -> Result, ContractError> { +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()) + Ok(Response::default()) } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn reply( - deps: DepsMut, - env: Env, - msg: Reply, -) -> Result, ContractError> { +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 diff --git a/contracts/external/cw-abc/src/queries.rs b/contracts/external/cw-abc/src/queries.rs index 1d113df3d..d80c4272c 100644 --- a/contracts/external/cw-abc/src/queries.rs +++ b/contracts/external/cw-abc/src/queries.rs @@ -8,13 +8,9 @@ use crate::state::{ }; use cosmwasm_std::{Deps, Order, QuerierWrapper, StdResult, Uint128}; use std::ops::Deref; -use token_bindings::TokenFactoryQuery; /// Get the current state of the curve -pub fn query_curve_info( - deps: Deps, - curve_fn: CurveFn, -) -> StdResult { +pub fn query_curve_info(deps: Deps, curve_fn: CurveFn) -> StdResult { let CurveState { reserve, supply, @@ -37,13 +33,13 @@ pub fn query_curve_info( } /// Returns information about the supply Denom -pub fn get_denom(deps: Deps) -> StdResult { +pub fn get_denom(deps: Deps) -> StdResult { let denom = SUPPLY_DENOM.load(deps.storage)?; Ok(DenomResponse { denom }) } pub fn query_donations( - deps: Deps, + deps: Deps, start_aftor: Option, limit: Option, ) -> StdResult { @@ -67,7 +63,7 @@ pub fn query_donations( /// Query hatchers who contributed during the hatch phase pub fn query_hatchers( - deps: Deps, + deps: Deps, start_aftor: Option, limit: Option, ) -> StdResult { @@ -90,13 +86,13 @@ pub fn query_hatchers( } /// Query the max supply of the supply token -pub fn query_max_supply(deps: Deps) -> StdResult { +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 { +pub fn query_phase_config(deps: Deps) -> StdResult { let phase = PHASE.load(deps.storage)?; let phase_config = PHASE_CONFIG.load(deps.storage)?; Ok(CommonsPhaseConfigResponse { diff --git a/contracts/external/cw-abc/src/testing.rs b/contracts/external/cw-abc/src/testing.rs index 39d483613..8f5214a21 100644 --- a/contracts/external/cw-abc/src/testing.rs +++ b/contracts/external/cw-abc/src/testing.rs @@ -1,23 +1,23 @@ use cosmwasm_std::{ - testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, - Decimal, DepsMut, OwnedDeps, Uint128, + testing::{mock_env, mock_info}, + Decimal, DepsMut, Response, Uint128, }; use dao_interface::token::NewDenomMetadata; -use std::marker::PhantomData; -use token_bindings::TokenFactoryQuery; -use crate::abc::{ - ClosedConfig, CommonsPhaseConfig, CurveType, HatchConfig, MinMax, OpenConfig, ReserveToken, - SupplyToken, -}; use crate::contract; -use crate::contract::CwAbcResult; 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, default_supply_metadata, mock_tf_dependencies, TEST_CREATOR, - TEST_RESERVE_DENOM, TEST_SUPPLY_DENOM, _TEST_BUYER, _TEST_INVESTOR, + default_instantiate_msg, default_supply_metadata, TEST_CREATOR, TEST_RESERVE_DENOM, + TEST_SUPPLY_DENOM, _TEST_BUYER, _TEST_INVESTOR, }; pub use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; pub use speculoos::prelude::*; @@ -82,21 +82,21 @@ pub fn default_instantiate_msg( } } -pub fn mock_init(deps: DepsMut, init_msg: InstantiateMsg) -> CwAbcResult { +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) } -pub fn mock_tf_dependencies( -) -> OwnedDeps, TokenFactoryQuery> { - OwnedDeps { - storage: MockStorage::default(), - api: MockApi::default(), - querier: MockQuerier::::new(&[]), - custom_query_type: PhantomData::, - } -} +// pub fn mock_tf_dependencies( +// ) -> OwnedDeps, TokenFactoryQuery> { +// OwnedDeps { +// storage: MockStorage::default(), +// api: MockApi::default(), +// querier: MockQuerier::::new(&[]), +// custom_query_type: PhantomData::, +// } +// } // fn setup_test( // deps: DepsMut, From 9d069ebcc0043894eb3deb5fddb302da48d9aa24 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Tue, 12 Dec 2023 14:46:50 -0800 Subject: [PATCH 44/56] Remove unused old tests --- contracts/external/cw-abc/src/testing.rs | 209 ----------------------- 1 file changed, 209 deletions(-) diff --git a/contracts/external/cw-abc/src/testing.rs b/contracts/external/cw-abc/src/testing.rs index 8f5214a21..a7bd81888 100644 --- a/contracts/external/cw-abc/src/testing.rs +++ b/contracts/external/cw-abc/src/testing.rs @@ -87,212 +87,3 @@ pub fn mock_init(deps: DepsMut, init_msg: InstantiateMsg) -> Result OwnedDeps, TokenFactoryQuery> { -// OwnedDeps { -// storage: MockStorage::default(), -// api: MockApi::default(), -// querier: MockQuerier::::new(&[]), -// custom_query_type: PhantomData::, -// } -// } - -// fn setup_test( -// deps: DepsMut, -// decimals: u8, -// reserve_decimals: u8, -// curve_type: CurveType, -// ) { -// // this matches `linear_curve` test case from curves.rs -// let creator = String::from(CREATOR); -// let msg = default_instantiate_msg(decimals, reserve_decimals, curve_type); -// let info = mock_info(&creator, &[]); - -// // make sure we can instantiate with this -// let res = instantiate(deps, mock_env(), info, msg).unwrap(); -// assert_eq!(0, res.messages.len()); -// } - -// // Mock token factory querier dependencies -// #[test] -// fn proper_instantiation() -> CwAbcResult<()> { -// let mut deps = mock_tf_dependencies(); - -// // this matches `linear_curve` test case from curves.rs -// let creator = String::from("creator"); -// let curve_type = CurveType::SquareRoot { -// slope: Uint128::new(1), -// scale: 1, -// }; -// let msg = default_instantiate_msg(2, 8, curve_type.clone()); -// let info = mock_info(&creator, &[]); - -// // make sure we can instantiate with this -// let res = instantiate(deps.as_mut(), mock_env(), info, msg)?; -// assert_that!(res.messages.len()).is_equal_to(1); -// let submsg = res.messages.get(0).unwrap(); -// assert_that!(submsg.msg).is_equal_to(CosmosMsg::Custom(WasmMsg::Execute { -// contract_addr: (), -// msg: (), -// funds: (), -// })); - -// // TODO! -// // // token info is proper -// // let token = query_token_info(deps.as_ref()).unwrap(); -// // assert_that!(&token.name, &msg.name); -// // assert_that!(&token.symbol, &msg.symbol); -// // assert_that!(token.decimals, 2); -// // assert_that!(token.total_supply, Uint128::zero()); - -// // curve state is sensible -// let state = query_curve_info(deps.as_ref(), curve_type.to_curve_fn())?; -// assert_that!(state.reserve).is_equal_to(Uint128::zero()); -// assert_that!(state.supply).is_equal_to(Uint128::zero()); -// assert_that!(state.reserve_denom.as_str()).is_equal_to(TEST_RESERVE_DENOM); -// // spot price 0 as supply is 0 -// assert_that!(state.spot_price).is_equal_to(Decimal::zero()); - -// // curve type is stored properly -// let curve = CURVE_TYPE.load(&deps.storage).unwrap(); -// assert_eq!(curve_type, curve); - -// // no balance -// // assert_eq!(get_balance(deps.as_ref(), &creator), Uint128::zero()); - -// Ok(()) -// } - -// #[test] -// fn buy_issues_tokens() { -// let mut deps = mock_dependencies(); -// let curve_type = CurveType::Linear { -// slope: Uint128::new(1), -// scale: 1, -// }; -// setup_test(deps.as_mut(), 2, 8, curve_type.clone()); - -// // succeeds with proper token (5 BTC = 5*10^8 satoshi) -// let info = mock_info(INVESTOR, &coins(500_000_000, DENOM)); -// let buy = ExecuteMsg::Buy {}; -// execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap(); - -// // bob got 1000 EPOXY (10.00) -// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); -// assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::zero()); - -// // send them all to buyer -// let info = mock_info(INVESTOR, &[]); -// let send = ExecuteMsg::Transfer { -// recipient: BUYER.into(), -// amount: Uint128::new(1000), -// }; -// execute(deps.as_mut(), mock_env(), info, send).unwrap(); - -// // ensure balances updated -// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::zero()); -// assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::new(1000)); - -// // second stake needs more to get next 1000 EPOXY -// let info = mock_info(INVESTOR, &coins(1_500_000_000, DENOM)); -// execute(deps.as_mut(), mock_env(), info, buy).unwrap(); - -// // ensure balances updated -// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); -// assert_eq!(get_balance(deps.as_ref(), BUYER), Uint128::new(1000)); - -// // check curve info updated -// let curve = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); -// assert_eq!(curve.reserve, Uint128::new(2_000_000_000)); -// assert_eq!(curve.supply, Uint128::new(2000)); -// assert_eq!(curve.spot_price, Decimal::percent(200)); - -// // check token info updated -// let token = query_token_info(deps.as_ref()).unwrap(); -// assert_eq!(token.decimals, 2); -// assert_eq!(token.total_supply, Uint128::new(2000)); -// } - -// #[test] -// fn bonding_fails_with_wrong_denom() { -// let mut deps = mock_dependencies(); -// let curve_type = CurveType::Linear { -// slope: Uint128::new(1), -// scale: 1, -// }; -// setup_test(deps.as_mut(), 2, 8, curve_type); - -// // fails when no tokens sent -// let info = mock_info(INVESTOR, &[]); -// let buy = ExecuteMsg::Buy {}; -// let err = execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err(); -// assert_eq!(err, PaymentError::NoFunds {}.into()); - -// // fails when wrong tokens sent -// let info = mock_info(INVESTOR, &coins(1234567, "wei")); -// let err = execute(deps.as_mut(), mock_env(), info, buy.clone()).unwrap_err(); -// assert_eq!(err, PaymentError::MissingDenom(DENOM.into()).into()); - -// // fails when too many tokens sent -// let info = mock_info(INVESTOR, &[coin(3400022, DENOM), coin(1234567, "wei")]); -// let err = execute(deps.as_mut(), mock_env(), info, buy).unwrap_err(); -// assert_eq!(err, PaymentError::MultipleDenoms {}.into()); -// } - -// #[test] -// fn burning_sends_reserve() { -// let mut deps = mock_dependencies(); -// let curve_type = CurveType::Linear { -// slope: Uint128::new(1), -// scale: 1, -// }; -// setup_test(deps.as_mut(), 2, 8, curve_type.clone()); - -// // succeeds with proper token (20 BTC = 20*10^8 satoshi) -// let info = mock_info(INVESTOR, &coins(2_000_000_000, DENOM)); -// let buy = ExecuteMsg::Buy {}; -// execute(deps.as_mut(), mock_env(), info, buy).unwrap(); - -// // bob got 2000 EPOXY (20.00) -// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(2000)); - -// // cannot burn too much -// let info = mock_info(INVESTOR, &[]); -// let burn = ExecuteMsg::Burn { -// amount: Uint128::new(3000), -// }; -// let err = execute(deps.as_mut(), mock_env(), info, burn).unwrap_err(); -// // TODO check error - -// // burn 1000 EPOXY to get back 15BTC (*10^8) -// let info = mock_info(INVESTOR, &[]); -// let burn = ExecuteMsg::Burn { -// amount: Uint128::new(1000), -// }; -// let res = execute(deps.as_mut(), mock_env(), info, burn).unwrap(); - -// // balance is lower -// assert_eq!(get_balance(deps.as_ref(), INVESTOR), Uint128::new(1000)); - -// // ensure we got our money back -// assert_eq!(1, res.messages.len()); -// assert_eq!( -// &res.messages[0], -// &SubMsg::new(BankMsg::Send { -// to_address: INVESTOR.into(), -// amount: coins(1_500_000_000, DENOM), -// }) -// ); - -// // check curve info updated -// let curve = query_curve_info(deps.as_ref(), curve_type.to_curve_fn()).unwrap(); -// assert_eq!(curve.reserve, Uint128::new(500_000_000)); -// assert_eq!(curve.supply, Uint128::new(1000)); -// assert_eq!(curve.spot_price, Decimal::percent(100)); - -// // check token info updated -// let token = query_token_info(deps.as_ref()).unwrap(); -// assert_eq!(token.decimals, 2); -// assert_eq!(token.total_supply, Uint128::new(1000)); -// } From 00b80d9af73ef1f49d297695e0e4fb55e040b5fd Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Sat, 23 Dec 2023 13:09:40 -0800 Subject: [PATCH 45/56] Fix cargo file after rebase --- Cargo.lock | 12 ------------ Cargo.toml | 1 - 2 files changed, 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 578b1294c..7bf929a01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4721,18 +4721,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "token-bindings" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be1c893c90d2993320d9722516ece705460f464616313a62edadb9e71df4502" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "schemars", - "serde", -] - [[package]] name = "tokio" version = "1.35.1" diff --git a/Cargo.toml b/Cargo.toml index 35bd4efb2..524fd6d82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,7 +64,6 @@ 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 } -prost = "0.11" rust_decimal = "1.14.3" quote = "1.0" rand = "0.8" From 02e26e06753a3cedd5efbc015b160382ad51ea1f Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Sat, 23 Dec 2023 13:13:06 -0800 Subject: [PATCH 46/56] Remove token-bindings dep --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 524fd6d82..3262df8d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,7 +80,6 @@ speculoos = "0.11.0" syn = {version = "1.0", features = ["derive"]} test-context = "0.1" thiserror = { version = "1.0" } -token-bindings = "0.11.0" wynd-utils = "0.4" # One commit ahead of version 0.3.0. Allows initialization with an From 4d4d70e84f6389d5c7aebf3b26ec2383f187dbe6 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Sat, 23 Dec 2023 14:45:14 -0800 Subject: [PATCH 47/56] dao-abc-factory contract Creates new cw-abc DAOs. --- Cargo.lock | 20 + Cargo.toml | 1 + contracts/external/cw-abc/README.md | 2 +- contracts/external/cw-abc/src/queries.rs | 8 +- .../external/dao-abc-factory/.cargo/config | 4 + contracts/external/dao-abc-factory/Cargo.toml | 35 ++ contracts/external/dao-abc-factory/README | 4 + .../dao-abc-factory/examples/schema.rs | 10 + .../schema/dao-abc-factory.json | 547 ++++++++++++++++++ .../external/dao-abc-factory/src/contract.rs | 146 +++++ .../external/dao-abc-factory/src/error.rs | 28 + contracts/external/dao-abc-factory/src/lib.rs | 5 + contracts/external/dao-abc-factory/src/msg.rs | 23 + 13 files changed, 828 insertions(+), 5 deletions(-) create mode 100644 contracts/external/dao-abc-factory/.cargo/config create mode 100644 contracts/external/dao-abc-factory/Cargo.toml create mode 100644 contracts/external/dao-abc-factory/README create mode 100644 contracts/external/dao-abc-factory/examples/schema.rs create mode 100644 contracts/external/dao-abc-factory/schema/dao-abc-factory.json create mode 100644 contracts/external/dao-abc-factory/src/contract.rs create mode 100644 contracts/external/dao-abc-factory/src/error.rs create mode 100644 contracts/external/dao-abc-factory/src/lib.rs create mode 100644 contracts/external/dao-abc-factory/src/msg.rs diff --git a/Cargo.lock b/Cargo.lock index 7bf929a01..4dcb36da6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1671,6 +1671,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "dao-abc-factory" +version = "2.4.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "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-voting 2.4.0", + "thiserror", +] + [[package]] name = "dao-cw721-extensions" version = "2.4.1" diff --git a/Cargo.toml b/Cargo.toml index 3262df8d6..065e9dadc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,6 +100,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/contracts/external/cw-abc/README.md b/contracts/external/cw-abc/README.md index 64342e095..44ab82194 100644 --- a/contracts/external/cw-abc/README.md +++ b/contracts/external/cw-abc/README.md @@ -4,7 +4,7 @@ Implments an [Augmented Bonding Curve](https://medium.com/commonsstack/deep-dive 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 unaudited and experimental. NOT RECOMMENDED FOR PRODUCTION USE. +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. diff --git a/contracts/external/cw-abc/src/queries.rs b/contracts/external/cw-abc/src/queries.rs index d80c4272c..8bc9138a6 100644 --- a/contracts/external/cw-abc/src/queries.rs +++ b/contracts/external/cw-abc/src/queries.rs @@ -40,7 +40,7 @@ pub fn get_denom(deps: Deps) -> StdResult { pub fn query_donations( deps: Deps, - start_aftor: Option, + start_after: Option, limit: Option, ) -> StdResult { let donations = cw_paginate_storage::paginate_map( @@ -50,7 +50,7 @@ pub fn query_donations( querier: QuerierWrapper::new(deps.querier.deref()), }, &DONATIONS, - start_aftor + start_after .map(|addr| deps.api.addr_validate(&addr)) .transpose()? .as_ref(), @@ -64,7 +64,7 @@ pub fn query_donations( /// Query hatchers who contributed during the hatch phase pub fn query_hatchers( deps: Deps, - start_aftor: Option, + start_after: Option, limit: Option, ) -> StdResult { let hatchers = cw_paginate_storage::paginate_map( @@ -74,7 +74,7 @@ pub fn query_hatchers( querier: QuerierWrapper::new(deps.querier.deref()), }, &HATCHERS, - start_aftor + start_after .map(|addr| deps.api.addr_validate(&addr)) .transpose()? .as_ref(), 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..bfaaf4c1a --- /dev/null +++ b/contracts/external/dao-abc-factory/Cargo.toml @@ -0,0 +1,35 @@ +[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 = [] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cosmwasm-storage = { workspace = true } +cw-abc = { workspace = true } +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] +cw-multi-test = { 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..9c341ce9e --- /dev/null +++ b/contracts/external/dao-abc-factory/schema/dao-abc-factory.json @@ -0,0 +1,547 @@ +{ + "contract_name": "dao-abc-factory", + "contract_version": "2.4.0", + "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": [ + { + "description": "Example Factory Implementation", + "type": "object", + "required": [ + "abc_factory" + ], + "properties": { + "abc_factory": { + "$ref": "#/definitions/InstantiateMsg" + } + }, + "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", + "exit_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" + } + ] + }, + "exit_fee": { + "description": "Exit tax for the hatch phase", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "initial_raise": { + "description": "The initial raise range (min, max) in the reserve token", + "allOf": [ + { + "$ref": "#/definitions/MinMax" + } + ] + } + }, + "additionalProperties": false + }, + "InstantiateMsg": { + "type": "object", + "required": [ + "curve_type", + "fees_recipient", + "phase_config", + "reserve", + "supply", + "token_issuer_code_id" + ], + "properties": { + "curve_type": { + "description": "Curve type for this contract", + "allOf": [ + { + "$ref": "#/definitions/CurveType" + } + ] + }, + "fees_recipient": { + "description": "The recipient for any fees collected from bonding curve operation", + "type": "string" + }, + "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": { + "type": "string" + } + }, + "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 minimium 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" + } + } + }, + "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..ecd909e22 --- /dev/null +++ b/contracts/external/dao-abc-factory/src/contract.rs @@ -0,0 +1,146 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_json_binary, Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Order, Reply, Response, + StdResult, SubMsg, WasmMsg, +}; +use cw2::set_contract_version; +use cw_abc::msg::{InstantiateMsg as AbcInstantiateMsg, QueryMsg as AbcQueryMsg}; +use cw_storage_plus::{Bound, Item, Map}; +use cw_utils::parse_reply_instantiate_data; +use dao_interface::{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 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(msg) => execute_token_factory_factory(deps, env, info, msg), + } +} + +pub fn execute_token_factory_factory( + deps: DepsMut, + _env: Env, + info: MessageInfo, + 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, &Empty {})?; + + // 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.token_issuer_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 => { + // Parse issuer address from instantiate reply + let abc_addr = parse_reply_instantiate_data(msg)?.contract_address; + + // Query for denom + let denom = 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 {})?; + + // Responses for `dao-voting-token-staked` MUST include a + // TokenFactoryCallback. + Ok( + Response::new().set_data(to_json_binary(&TokenFactoryCallback { + denom, + token_contract: Some(token_contract.to_string()), + module_instantiate_callback: None, + })?), + ) + } + _ => 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..3915b791e --- /dev/null +++ b/contracts/external/dao-abc-factory/src/lib.rs @@ -0,0 +1,5 @@ +pub mod contract; +mod error; +pub mod msg; + +pub use crate::error::ContractError; 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..6d6cdf52a --- /dev/null +++ b/contracts/external/dao-abc-factory/src/msg.rs @@ -0,0 +1,23 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cw_abc::msg::InstantiateMsg as AbcInstantiateMsg; + +#[cw_serde] +pub struct InstantiateMsg {} + +#[cw_serde] +pub enum ExecuteMsg { + /// Example Factory Implementation + AbcFactory(AbcInstantiateMsg), +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(dao_interface::voting::InfoResponse)] + Info {}, + #[returns(Vec)] + Daos { + start_after: Option, + limit: Option, + }, +} From 389484f58d02f081ba7bbd6c85f25197ae57b942 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Sat, 23 Dec 2023 15:19:18 -0800 Subject: [PATCH 48/56] Fix wasm build --- contracts/external/dao-abc-factory/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/external/dao-abc-factory/Cargo.toml b/contracts/external/dao-abc-factory/Cargo.toml index bfaaf4c1a..3d5c2fdcf 100644 --- a/contracts/external/dao-abc-factory/Cargo.toml +++ b/contracts/external/dao-abc-factory/Cargo.toml @@ -20,7 +20,7 @@ library = [] cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } cosmwasm-storage = { workspace = true } -cw-abc = { workspace = true } +cw-abc = { workspace = true, features = ["library"] } cw2 = { workspace = true } cw-ownable = { workspace = true } cw-storage-plus = { workspace = true } From 301b45af489506f28695efc1a6b1d10479a33d8b Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Fri, 29 Dec 2023 14:07:42 -0800 Subject: [PATCH 49/56] Basic Integration Tests --- Cargo.lock | 9 + contracts/external/cw-abc/src/contract.rs | 5 +- contracts/external/dao-abc-factory/Cargo.toml | 14 + .../schema/dao-abc-factory.json | 18 +- .../external/dao-abc-factory/src/contract.rs | 69 ++- contracts/external/dao-abc-factory/src/lib.rs | 3 + contracts/external/dao-abc-factory/src/msg.rs | 6 +- .../src/test_tube/integration_tests.rs | 147 +++++ .../dao-abc-factory/src/test_tube/mod.rs | 6 + .../dao-abc-factory/src/test_tube/test_env.rs | 560 ++++++++++++++++++ packages/dao-testing/Cargo.toml | 1 + packages/dao-testing/src/test_tube/cw_abc.rs | 4 +- .../src/test_tube/dao_abc_factory.rs | 129 ++++ .../src/test_tube/dao_voting_token_staked.rs | 129 ++++ packages/dao-testing/src/test_tube/mod.rs | 6 + 15 files changed, 1083 insertions(+), 23 deletions(-) create mode 100644 contracts/external/dao-abc-factory/src/test_tube/integration_tests.rs create mode 100644 contracts/external/dao-abc-factory/src/test_tube/mod.rs create mode 100644 contracts/external/dao-abc-factory/src/test_tube/test_env.rs create mode 100644 packages/dao-testing/src/test_tube/dao_abc_factory.rs create mode 100644 packages/dao-testing/src/test_tube/dao_voting_token_staked.rs diff --git a/Cargo.lock b/Cargo.lock index 4dcb36da6..a3efdd308 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1675,6 +1675,7 @@ dependencies = [ name = "dao-abc-factory" version = "2.4.0" dependencies = [ + "anyhow", "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", @@ -1687,7 +1688,14 @@ dependencies = [ "cw2 1.1.2", "dao-dao-macros", "dao-interface", + "dao-proposal-hook-counter", + "dao-proposal-single", + "dao-testing", "dao-voting 2.4.0", + "dao-voting-token-staked", + "osmosis-std", + "osmosis-test-tube", + "serde", "thiserror", ] @@ -2092,6 +2100,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", diff --git a/contracts/external/cw-abc/src/contract.rs b/contracts/external/cw-abc/src/contract.rs index 1c3b7e294..02dbfd3b0 100644 --- a/contracts/external/cw-abc/src/contract.rs +++ b/contracts/external/cw-abc/src/contract.rs @@ -86,12 +86,11 @@ pub fn instantiate( // Initialize owner to sender cw_ownable::initialize_owner(deps.storage, deps.api, Some(info.sender.as_str()))?; - // TODO Potential renounce admin? // Tnstantiate cw-token-factory-issuer contract - // Sender is set as contract admin + // Contract is immutable, no admin let issuer_instantiate_msg = SubMsg::reply_always( WasmMsg::Instantiate { - admin: Some(info.sender.to_string()), + admin: None, code_id: token_issuer_code_id, msg: to_json_binary(&IssuerInstantiateMsg::NewToken { subdenom: supply.subdenom.clone(), diff --git a/contracts/external/dao-abc-factory/Cargo.toml b/contracts/external/dao-abc-factory/Cargo.toml index 3d5c2fdcf..accf4f3cf 100644 --- a/contracts/external/dao-abc-factory/Cargo.toml +++ b/contracts/external/dao-abc-factory/Cargo.toml @@ -15,6 +15,11 @@ crate-type = ["cdylib", "rlib"] 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 } @@ -32,4 +37,13 @@ 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/schema/dao-abc-factory.json b/contracts/external/dao-abc-factory/schema/dao-abc-factory.json index 9c341ce9e..1e72c7edb 100644 --- a/contracts/external/dao-abc-factory/schema/dao-abc-factory.json +++ b/contracts/external/dao-abc-factory/schema/dao-abc-factory.json @@ -13,14 +13,28 @@ "title": "ExecuteMsg", "oneOf": [ { - "description": "Example Factory Implementation", "type": "object", "required": [ "abc_factory" ], "properties": { "abc_factory": { - "$ref": "#/definitions/InstantiateMsg" + "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 diff --git a/contracts/external/dao-abc-factory/src/contract.rs b/contracts/external/dao-abc-factory/src/contract.rs index ecd909e22..b2836742f 100644 --- a/contracts/external/dao-abc-factory/src/contract.rs +++ b/contracts/external/dao-abc-factory/src/contract.rs @@ -1,14 +1,20 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_json_binary, Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Order, Reply, Response, - StdResult, SubMsg, WasmMsg, + 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::{InstantiateMsg as AbcInstantiateMsg, QueryMsg as AbcQueryMsg}; +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::{token::TokenFactoryCallback, voting::Query as VotingModuleQueryMsg}; +use dao_interface::{ + state::ModuleInstantiateCallback, token::TokenFactoryCallback, + voting::Query as VotingModuleQueryMsg, +}; use crate::{ error::ContractError, @@ -21,6 +27,7 @@ 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)] @@ -43,7 +50,10 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { - ExecuteMsg::AbcFactory(msg) => execute_token_factory_factory(deps, env, info, msg), + ExecuteMsg::AbcFactory { + code_id, + instantiate_msg, + } => execute_token_factory_factory(deps, env, info, code_id, instantiate_msg), } } @@ -51,6 +61,7 @@ pub fn execute_token_factory_factory( deps: DepsMut, _env: Env, info: MessageInfo, + code_id: u64, msg: AbcInstantiateMsg, ) -> Result { // Save voting module address @@ -61,7 +72,8 @@ pub fn execute_token_factory_factory( .querier .query_wasm_smart(info.sender, &VotingModuleQueryMsg::Dao {})?; - DAOS.save(deps.storage, dao, &Empty {})?; + DAOS.save(deps.storage, dao.clone(), &Empty {})?; + CURRENT_DAO.save(deps.storage, &dao)?; // Instantiate new contract, further setup is handled in the // SubMsg reply. @@ -69,7 +81,7 @@ pub fn execute_token_factory_factory( WasmMsg::Instantiate { // No admin as we want the bonding curve contract to be immutable admin: None, - code_id: msg.token_issuer_code_id, + code_id, msg: to_json_binary(&msg)?, funds: vec![], label: "cw_abc".to_string(), @@ -118,11 +130,14 @@ pub fn query_daos( 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 = deps + let denom: DenomResponse = deps .querier .query_wasm_smart(abc_addr.clone(), &AbcQueryMsg::Denom {})?; @@ -131,15 +146,41 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result Err(ContractError::UnknownReplyId { id: msg.id }), } diff --git a/contracts/external/dao-abc-factory/src/lib.rs b/contracts/external/dao-abc-factory/src/lib.rs index 3915b791e..286ec8c24 100644 --- a/contracts/external/dao-abc-factory/src/lib.rs +++ b/contracts/external/dao-abc-factory/src/lib.rs @@ -3,3 +3,6 @@ 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 index 6d6cdf52a..143224330 100644 --- a/contracts/external/dao-abc-factory/src/msg.rs +++ b/contracts/external/dao-abc-factory/src/msg.rs @@ -6,8 +6,10 @@ pub struct InstantiateMsg {} #[cw_serde] pub enum ExecuteMsg { - /// Example Factory Implementation - AbcFactory(AbcInstantiateMsg), + AbcFactory { + instantiate_msg: AbcInstantiateMsg, + code_id: u64, + }, } #[cw_serde] 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..fe51e9fb6 --- /dev/null +++ b/contracts/external/dao-abc-factory/src/test_tube/mod.rs @@ -0,0 +1,6 @@ +// 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))] + +mod integration_tests; +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..a31a88822 --- /dev/null +++ b/contracts/external/dao-abc-factory/src/test_tube/test_env.rs @@ -0,0 +1,560 @@ +// 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 { + fees_recipient: accounts[0].address(), + token_issuer_code_id: issuer_id, + 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), + exit_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 { + fees_recipient: accounts[0].address(), + token_issuer_code_id: issuer_id, + 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), + exit_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/packages/dao-testing/Cargo.toml b/packages/dao-testing/Cargo.toml index d128f6226..a86b65db6 100644 --- a/packages/dao-testing/Cargo.toml +++ b/packages/dao-testing/Cargo.toml @@ -43,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 index 59e90039d..c0b17e29d 100644 --- a/packages/dao-testing/src/test_tube/cw_abc.rs +++ b/packages/dao-testing/src/test_tube/cw_abc.rs @@ -154,7 +154,7 @@ impl<'a> CwAbc<'a> { .join("..") .join("..") .join("artifacts") - .join("cw_tokenfactory_issuer.wasm"), + .join("cw_abc.wasm"), ); match byte_code { Ok(byte_code) => byte_code, @@ -164,7 +164,7 @@ impl<'a> CwAbc<'a> { .join("..") .join("..") .join("artifacts") - .join("cw_tokenfactory_issuer-aarch64.wasm"), + .join("cw_abc-aarch64.wasm"), ) .unwrap(), } 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 016c51488..e609fa47e 100644 --- a/packages/dao-testing/src/test_tube/mod.rs +++ b/packages/dao-testing/src/test_tube/mod.rs @@ -14,6 +14,9 @@ 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; @@ -22,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; From a0eb61992cb5d5b50166b7cdb81bd5ef90e163e3 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Tue, 2 Jan 2024 20:44:12 -0800 Subject: [PATCH 50/56] Fix clippy errors and test errors --- contracts/external/cw-abc/src/testing.rs | 6 +----- contracts/external/dao-abc-factory/src/test_tube/mod.rs | 3 +++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/contracts/external/cw-abc/src/testing.rs b/contracts/external/cw-abc/src/testing.rs index a7bd81888..b324ca8ed 100644 --- a/contracts/external/cw-abc/src/testing.rs +++ b/contracts/external/cw-abc/src/testing.rs @@ -15,11 +15,7 @@ use crate::{ }; pub(crate) mod prelude { - pub use super::{ - default_instantiate_msg, default_supply_metadata, TEST_CREATOR, TEST_RESERVE_DENOM, - TEST_SUPPLY_DENOM, _TEST_BUYER, _TEST_INVESTOR, - }; - pub use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + pub use super::{default_instantiate_msg, TEST_RESERVE_DENOM}; pub use speculoos::prelude::*; } diff --git a/contracts/external/dao-abc-factory/src/test_tube/mod.rs b/contracts/external/dao-abc-factory/src/test_tube/mod.rs index fe51e9fb6..796c18a34 100644 --- a/contracts/external/dao-abc-factory/src/test_tube/mod.rs +++ b/contracts/external/dao-abc-factory/src/test_tube/mod.rs @@ -2,5 +2,8 @@ // 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; From 7e923efb026ae0e470d69e32052865ccd645f582 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Tue, 2 Jan 2024 22:51:51 -0800 Subject: [PATCH 51/56] Bump nightly version in CI --- .github/workflows/integration_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 371d72422c7a38a4375476db56f630cd7e6512b8 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Mon, 8 Jan 2024 15:36:58 -0800 Subject: [PATCH 52/56] Remove unused deps --- Cargo.lock | 1 - contracts/external/dao-abc-factory/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a3efdd308..3eb9c7b49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1678,7 +1678,6 @@ dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", - "cosmwasm-storage", "cw-abc", "cw-multi-test", "cw-ownable", diff --git a/contracts/external/dao-abc-factory/Cargo.toml b/contracts/external/dao-abc-factory/Cargo.toml index accf4f3cf..1966d393b 100644 --- a/contracts/external/dao-abc-factory/Cargo.toml +++ b/contracts/external/dao-abc-factory/Cargo.toml @@ -24,7 +24,6 @@ test-tube = [] [dependencies] cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } -cosmwasm-storage = { workspace = true } cw-abc = { workspace = true, features = ["library"] } cw2 = { workspace = true } cw-ownable = { workspace = true } From 6ab116dbfc9616e2aa4596726f8df12b2321c7da Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Mon, 8 Jan 2024 16:52:52 -0800 Subject: [PATCH 53/56] Clean up small TODO --- contracts/external/cw-abc/src/commands.rs | 6 +++--- contracts/external/cw-abc/src/error.rs | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/contracts/external/cw-abc/src/commands.rs b/contracts/external/cw-abc/src/commands.rs index 9a3ddf8ca..205f94cfc 100644 --- a/contracts/external/cw-abc/src/commands.rs +++ b/contracts/external/cw-abc/src/commands.rs @@ -262,10 +262,10 @@ fn calculate_exit_fee( CommonsPhase::Closed => return Ok(Uint128::zero()), }; - // TODO more normal check? - debug_assert!( + // Ensure the exit fee is not greater than 100% + ensure!( exit_fee <= StdDecimal::percent(100), - "Exit tax must be <= 100%" + ContractError::InvalidExitFee {} ); // This won't ever overflow because it's checked diff --git a/contracts/external/cw-abc/src/error.rs b/contracts/external/cw-abc/src/error.rs index d62661331..6c343f21b 100644 --- a/contracts/external/cw-abc/src/error.rs +++ b/contracts/external/cw-abc/src/error.rs @@ -28,6 +28,9 @@ pub enum ContractError { #[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 }, From 6d4b430c1dcdef97723ec9ccfe295266d5f2e947 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Tue, 26 Mar 2024 14:35:44 +0100 Subject: [PATCH 54/56] Post rebase fixups --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- .../external/dao-abc-factory/schema/dao-abc-factory.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3eb9c7b49..9347da3ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1673,7 +1673,7 @@ dependencies = [ [[package]] name = "dao-abc-factory" -version = "2.4.0" +version = "2.4.1" dependencies = [ "anyhow", "cosmwasm-schema", @@ -1690,7 +1690,7 @@ dependencies = [ "dao-proposal-hook-counter", "dao-proposal-single", "dao-testing", - "dao-voting 2.4.0", + "dao-voting 2.4.1", "dao-voting-token-staked", "osmosis-std", "osmosis-test-tube", diff --git a/Cargo.toml b/Cargo.toml index 065e9dadc..bfdaec930 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,7 +100,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-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/contracts/external/dao-abc-factory/schema/dao-abc-factory.json b/contracts/external/dao-abc-factory/schema/dao-abc-factory.json index 1e72c7edb..db9be42c1 100644 --- a/contracts/external/dao-abc-factory/schema/dao-abc-factory.json +++ b/contracts/external/dao-abc-factory/schema/dao-abc-factory.json @@ -1,6 +1,6 @@ { "contract_name": "dao-abc-factory", - "contract_version": "2.4.0", + "contract_version": "2.4.1", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", From f656e8abc0832e46073b86581c9d4a58b6b6aad8 Mon Sep 17 00:00:00 2001 From: ismellike Date: Tue, 14 May 2024 13:44:13 -0500 Subject: [PATCH 55/56] ABC Improvements (#818) * Make hatcher_allowlist a map Also cleans up state when going from Hatch -> Open Various spell-checking fixes * Allow existing tokens to be used with ABC's I think it's important for an existing token to be supported by an ABC. Token DAO's will be able to set up liquidity more easily by simply allowing mint & burn functionality after instantiating the ABC while holding ownership of it. *Also adds a query for hatcher allowlist *fixes spell checks I ran into *moves TokenInfo from dao-voting-token-staked to dao-interface for use in cw-abc * Complete funding pool logic Renamed fees_recipient to funding_pool_forwarding for better naming Made funding_pool_forwarding optional Allow updating the funding_pool_forwarding by owner Allow withdrawing from the funding pool by owner * Implement a circuit breaker Adds IS_PAUSED state that is checked in execute entry Also fixes naming in commands * Add query for initial supply at ABC creation * Allow donation into the reserve pool Also fixes donation state being lost after multiple donations * Move cw-abc curves to its own package cw-curves * Improve validation for max_supply * Support DAO membership in hatcher allowlist Could use a test here Also fixes some warnings and flag for cw-std * Clippy fix * Only clone decimals from curve_state also reorg buy command to be closer to sell command for readability * QoL improvements Derive copy on DecimalPlaces Catch OverflowError directly in ContractError Do not set initial supply as supply - will further remove allowing initial supplies * Revert support for initial supply * Fix clippy unnecessary clones * Donate only into the funding pool * Add a test for donate and withdraw from funding pool * Add test for dao hatchers + update schemas Also removes unused deps from cw-abc which now live in cw-curves * Update Cargo.toml * Buy & Sell Quotes Let users know the curve state and returned amount after x payment * Complete DAO hatchers w/ priority queue Disallow selling in the hatch phase Do not validate max_contribution against max_raise * Fix priority queue ordering on ties also clippy fixes * Couple more clippy fixes --- Cargo.lock | 20 +- Cargo.toml | 1 + ci/bootstrap-env/src/main.rs | 3 +- ci/integration-tests/src/helpers/chain.rs | 3 +- .../dao-dao-core/schema/dao-dao-core.json | 2 +- contracts/external/cw-abc/Cargo.toml | 11 +- contracts/external/cw-abc/README.md | 42 +- contracts/external/cw-abc/schema/cw-abc.json | 711 ++++++++++++++++-- contracts/external/cw-abc/src/abc.rs | 79 +- contracts/external/cw-abc/src/commands.rs | 671 +++++++++++------ contracts/external/cw-abc/src/contract.rs | 142 ++-- contracts/external/cw-abc/src/curves.rs | 356 --------- contracts/external/cw-abc/src/error.rs | 14 +- contracts/external/cw-abc/src/helpers.rs | 109 +++ contracts/external/cw-abc/src/lib.rs | 4 +- contracts/external/cw-abc/src/msg.rs | 96 ++- contracts/external/cw-abc/src/queries.rs | 76 +- contracts/external/cw-abc/src/state.rs | 105 ++- .../cw-abc/src/test_tube/integration_tests.rs | 318 +++++++- .../external/cw-abc/src/test_tube/test_env.rs | 148 +++- contracts/external/cw-abc/src/testing.rs | 3 +- .../schema/cw-tokenfactory-issuer.json | 16 +- .../cw-tokenfactory-issuer/src/msg.rs | 21 +- .../cw-tokenfactory-issuer/src/queries.rs | 4 +- contracts/external/cw-vesting/README.md | 4 +- .../external/cw721-roles/src/contract.rs | 4 +- .../schema/dao-abc-factory.json | 110 ++- .../dao-abc-factory/src/test_tube/test_env.rs | 6 +- .../schema/dao-proposal-single.json | 2 +- .../proposal/dao-proposal-single/src/msg.rs | 2 +- .../dao-proposal-single/src/proposal.rs | 8 +- .../test/dao-proposal-hook-counter/src/msg.rs | 1 + .../voting/dao-voting-cw20-staked/src/msg.rs | 1 + .../voting/dao-voting-cw721-staked/src/msg.rs | 1 + .../dao-voting-token-staked/src/contract.rs | 8 +- .../voting/dao-voting-token-staked/src/msg.rs | 1 + packages/cw-curves/Cargo.toml | 20 + packages/cw-curves/README.md | 6 + packages/cw-curves/src/curve.rs | 74 ++ packages/cw-curves/src/curves/constant.rs | 39 + packages/cw-curves/src/curves/linear.rs | 44 ++ packages/cw-curves/src/curves/mod.rs | 8 + packages/cw-curves/src/curves/square_root.rs | 44 ++ packages/cw-curves/src/lib.rs | 8 + packages/cw-curves/src/tests.rs | 118 +++ packages/cw-curves/src/utils.rs | 63 ++ packages/cw-paginate-storage/src/lib.rs | 2 +- packages/dao-interface/src/msg.rs | 2 +- packages/dao-testing/src/test_tube/mod.rs | 2 +- packages/dao-voting/src/voting.rs | 4 +- 50 files changed, 2643 insertions(+), 894 deletions(-) delete mode 100644 contracts/external/cw-abc/src/curves.rs create mode 100644 contracts/external/cw-abc/src/helpers.rs create mode 100644 packages/cw-curves/Cargo.toml create mode 100644 packages/cw-curves/README.md create mode 100644 packages/cw-curves/src/curve.rs create mode 100644 packages/cw-curves/src/curves/constant.rs create mode 100644 packages/cw-curves/src/curves/linear.rs create mode 100644 packages/cw-curves/src/curves/mod.rs create mode 100644 packages/cw-curves/src/curves/square_root.rs create mode 100644 packages/cw-curves/src/lib.rs create mode 100644 packages/cw-curves/src/tests.rs create mode 100644 packages/cw-curves/src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 9347da3ba..f72280a3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -724,12 +724,13 @@ dependencies = [ [[package]] name = "cw-abc" -version = "0.0.1" +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", @@ -738,13 +739,13 @@ dependencies = [ "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", - "integer-cbrt", - "integer-sqrt", "osmosis-std", "osmosis-test-tube", - "rust_decimal", "serde", "serde_json", "speculoos", @@ -886,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" diff --git a/Cargo.toml b/Cargo.toml index bfdaec930..0ec1dd330 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,6 +88,7 @@ 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" } 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.toml b/contracts/external/cw-abc/Cargo.toml index 43abe028c..3eb423ba0 100644 --- a/contracts/external/cw-abc/Cargo.toml +++ b/contracts/external/cw-abc/Cargo.toml @@ -4,13 +4,14 @@ 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 = "0.0.1" +version = { workspace = true } [lib] crate-type = ["cdylib", "rlib"] @@ -36,11 +37,9 @@ cw-ownable = { workspace = true } cw-paginate-storage = { workspace = true } cw-tokenfactory-issuer = { workspace = true, features = ["library"] } dao-interface = { workspace = true } -rust_decimal = { workspace = true } -integer-sqrt = { workspace = true } -integer-cbrt = { workspace = true } getrandom = { workspace = true, features = ["js"] } thiserror = { workspace = true } +cw-curves = { workspace = true } [dev-dependencies] speculoos = { workspace = true } @@ -51,3 +50,7 @@ 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/README.md b/contracts/external/cw-abc/README.md index 44ab82194..05d2c175e 100644 --- a/contracts/external/cw-abc/README.md +++ b/contracts/external/cw-abc/README.md @@ -1,6 +1,6 @@ # cw-abc -Implments an [Augmented Bonding Curve](https://medium.com/commonsstack/deep-dive-augmented-bonding-curves-b5ca4fad4436). +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. @@ -18,8 +18,8 @@ Each bonding curve has a pricing function, also known as the price curve (or `cu 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 curency (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 curency is returned. +- `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. @@ -64,7 +64,7 @@ Augmented Bonding Curves are nothing new, some articles that inspired this imple - 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 funcationality: +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 @@ -74,7 +74,7 @@ Example Instantiation message: ``` json { - "fees_recipient": "address that recieves fees", + "fees_recipient": "address that receives fees", "token_issuer_code_id": 0, "supply": { "subdenom": "utokenname", @@ -87,17 +87,17 @@ Example Instantiation message: "decimals": 6, "max_supply": "100000000000000" }, - reserve: { + "reserve": { "denom": "ujuno", "decimals": 6, }, - curve_type: { + "curve_type": { "linear": { "slope": "2", "scale": 1 } }, - phase_config: { + "phase_config": { "hatch": { "contribution_limits": { "min": "10000000", @@ -107,8 +107,7 @@ Example Instantiation message: "min": "10000000", "max": "100000000000" }, - "entry_fee": "0.25", - "exit_fee": "0.10" + "entry_fee": "0.25" }, "open": { "exit_fee": "0.01", @@ -116,13 +115,30 @@ Example Instantiation message: }, "closed": {} }, - hatcher_allowlist: ["allowlist addresses, leave blank for no allowlist"], + "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 recieve fees (usually a DAO). +- `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`: infor about the token that will be minted by the curve. This is the token that is created by the bonding curve. +- `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. diff --git a/contracts/external/cw-abc/schema/cw-abc.json b/contracts/external/cw-abc/schema/cw-abc.json index 0a0187250..ffc6ec4d8 100644 --- a/contracts/external/cw-abc/schema/cw-abc.json +++ b/contracts/external/cw-abc/schema/cw-abc.json @@ -1,6 +1,6 @@ { "contract_name": "cw-abc", - "contract_version": "0.0.1", + "contract_version": "2.4.1", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -8,7 +8,6 @@ "type": "object", "required": [ "curve_type", - "fees_recipient", "phase_config", "reserve", "supply", @@ -23,9 +22,12 @@ } ] }, - "fees_recipient": { - "description": "The recipient for any fees collected from bonding curve operation", - "type": "string" + "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", @@ -34,7 +36,7 @@ "null" ], "items": { - "type": "string" + "$ref": "#/definitions/HatcherAllowlistEntryMsg" } }, "phase_config": { @@ -234,7 +236,6 @@ "required": [ "contribution_limits", "entry_fee", - "exit_fee", "initial_raise" ], "properties": { @@ -254,27 +255,106 @@ } ] }, - "exit_fee": { - "description": "Exit tax for the hatch phase", + "initial_raise": { + "description": "The initial raise range (min, max) in the reserve token", "allOf": [ { - "$ref": "#/definitions/Decimal" + "$ref": "#/definitions/MinMax" } ] - }, - "initial_raise": { - "description": "The initial raise range (min, max) in the reserve token", + } + }, + "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 minimium and maximum values", + "description": "Struct for minimum and maximum values", "type": "object", "required": [ "max", @@ -418,6 +498,10 @@ "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" } } }, @@ -454,7 +538,7 @@ "additionalProperties": false }, { - "description": "Donate will add reserve tokens to the funding pool", + "description": "Donate will donate tokens to the funding pool. You must send only reserve tokens.", "type": "object", "required": [ "donate" @@ -467,6 +551,33 @@ }, "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", @@ -517,7 +628,7 @@ "additionalProperties": false }, { - "description": "Update the hatch phase allowlist. This can only be called by the owner.", + "description": "Update the hatch phase allowlist. Only callable by owner.", "type": "object", "required": [ "update_hatch_allowlist" @@ -534,7 +645,7 @@ "description": "Addresses to be added.", "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/HatcherAllowlistEntryMsg" } }, "to_remove": { @@ -550,6 +661,43 @@ }, "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", @@ -782,8 +930,95 @@ } ] }, + "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 minimium and maximum values", + "description": "Struct for minimum and maximum values", "type": "object", "required": [ "max", @@ -848,16 +1083,6 @@ } ] }, - "exit_fee": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, "initial_raise": { "anyOf": [ { @@ -1005,13 +1230,26 @@ "additionalProperties": false }, { - "description": "Returns the Fee Recipient for the contract. This is the address that recieves any fees collected from bonding curve operation", "type": "object", "required": [ - "fees_recipient" + "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": { - "fees_recipient": { + "funding_pool_forwarding": { "type": "object", "additionalProperties": false } @@ -1049,7 +1287,69 @@ "additionalProperties": false }, { - "description": "Returns the Maxiumum Supply of the supply token", + "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" @@ -1062,6 +1362,50 @@ }, "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", @@ -1118,11 +1462,96 @@ }, "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", @@ -1332,12 +1761,174 @@ } } }, - "fees_recipient": { + "funding_pool_forwarding": { "$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.", + "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", @@ -1376,6 +1967,11 @@ } } }, + "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", @@ -1571,7 +2167,6 @@ "required": [ "contribution_limits", "entry_fee", - "exit_fee", "initial_raise" ], "properties": { @@ -1591,14 +2186,6 @@ } ] }, - "exit_fee": { - "description": "Exit tax for the hatch phase", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, "initial_raise": { "description": "The initial raise range (min, max) in the reserve token", "allOf": [ @@ -1611,7 +2198,7 @@ "additionalProperties": false }, "MinMax": { - "description": "Struct for minimium and maximum values", + "description": "Struct for minimum and maximum values", "type": "object", "required": [ "max", @@ -1659,6 +2246,38 @@ } } }, + "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", diff --git a/contracts/external/cw-abc/src/abc.rs b/contracts/external/cw-abc/src/abc.rs index 30f1f430b..1d86a26cb 100644 --- a/contracts/external/cw-abc/src/abc.rs +++ b/contracts/external/cw-abc/src/abc.rs @@ -1,8 +1,12 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{ensure, Decimal as StdDecimal, Uint128}; +use cosmwasm_std::{ensure, Decimal, Uint128}; +use cw_curves::{ + curves::{Constant, Linear, SquareRoot}, + utils::decimal, + Curve, DecimalPlaces, +}; use dao_interface::token::NewDenomMetadata; -use crate::curves::{decimal, Constant, Curve, DecimalPlaces, Linear, SquareRoot}; use crate::ContractError; #[cw_serde] @@ -27,13 +31,15 @@ pub struct ReserveToken { pub decimals: u8, } -/// Struct for minimium and maximum values +/// 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 @@ -41,11 +47,11 @@ pub struct HatchConfig { /// 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: StdDecimal, - /// Exit tax for the hatch phase - pub exit_fee: StdDecimal, + pub entry_fee: Decimal, } +impl Copy for HatchConfig {} + impl HatchConfig { /// Validate the hatch config pub fn validate(&self) -> Result<(), ContractError> { @@ -57,27 +63,12 @@ impl HatchConfig { ); ensure!( - self.contribution_limits.max <= self.initial_raise.max, - ContractError::HatchPhaseConfigError( - "Max contribution limit cannot be greater than the maximum initial raise." - .to_string() - ) - ); - - ensure!( - self.entry_fee <= StdDecimal::percent(100u64), + self.entry_fee <= Decimal::percent(100u64), ContractError::HatchPhaseConfigError( "Initial allocation percentage must be between 0 and 100.".to_string() ) ); - ensure!( - self.exit_fee <= StdDecimal::percent(100u64), - ContractError::HatchPhaseConfigError( - "Exit taxation percentage must be less than or equal to 100.".to_string() - ) - ); - Ok(()) } } @@ -86,23 +77,23 @@ impl HatchConfig { pub struct OpenConfig { /// Percentage of capital put into the Reserve Pool during the Open phase /// when buying from the curve. - pub entry_fee: StdDecimal, + pub entry_fee: Decimal, /// Exit taxation ratio - pub exit_fee: StdDecimal, + pub exit_fee: Decimal, } impl OpenConfig { /// Validate the open config pub fn validate(&self) -> Result<(), ContractError> { ensure!( - self.entry_fee <= StdDecimal::percent(100u64), + self.entry_fee <= Decimal::percent(100u64), ContractError::OpenPhaseConfigError( "Reserve percentage must be between 0 and 100.".to_string() ) ); ensure!( - self.exit_fee <= StdDecimal::percent(100u64), + self.exit_fee <= Decimal::percent(100u64), ContractError::OpenPhaseConfigError( "Exit taxation percentage must be between 0 and 100.".to_string() ) @@ -226,39 +217,3 @@ impl CurveType { } } } - -#[cfg(test)] -mod unit_tests { - use super::*; - - #[test] - fn validate_contribution_limit_not_gt_initial_raise() { - let phase_config = CommonsPhaseConfig { - hatch: HatchConfig { - contribution_limits: MinMax { - min: Uint128::one(), - max: Uint128::MAX, - }, - initial_raise: MinMax { - min: Uint128::one(), - max: Uint128::from(1000000u128), - }, - entry_fee: StdDecimal::percent(10u64), - exit_fee: StdDecimal::percent(10u64), - }, - open: OpenConfig { - entry_fee: StdDecimal::percent(10u64), - exit_fee: StdDecimal::percent(10u64), - }, - closed: ClosedConfig {}, - }; - let err = phase_config.validate().unwrap_err(); - assert_eq!( - err, - ContractError::HatchPhaseConfigError( - "Max contribution limit cannot be greater than the maximum initial raise." - .to_string() - ) - ) - } -} diff --git a/contracts/external/cw-abc/src/commands.rs b/contracts/external/cw-abc/src/commands.rs index 205f94cfc..6bb825e82 100644 --- a/contracts/external/cw-abc/src/commands.rs +++ b/contracts/external/cw-abc/src/commands.rs @@ -1,24 +1,23 @@ use cosmwasm_std::{ - ensure, to_json_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal as StdDecimal, DepsMut, Env, - MessageInfo, QuerierWrapper, Response, StdError, StdResult, Storage, Uint128, WasmMsg, + 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::collections::HashSet; use std::ops::Deref; -use crate::abc::{CommonsPhase, CurveType}; -use crate::msg::UpdatePhaseConfigMsg; +use crate::abc::{CommonsPhase, CurveType, HatchConfig, MinMax}; +use crate::helpers::{calculate_buy_quote, calculate_sell_quote}; +use crate::msg::{HatcherAllowlistEntryMsg, UpdatePhaseConfigMsg}; use crate::state::{ - CURVE_STATE, CURVE_TYPE, DONATIONS, FEES_RECIPIENT, HATCHERS, HATCHER_ALLOWLIST, MAX_SUPPLY, - PHASE, PHASE_CONFIG, SUPPLY_DENOM, TOKEN_ISSUER_CONTRACT, + 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 execute_buy(deps: DepsMut, _env: Env, info: MessageInfo) -> Result { +pub fn buy(deps: DepsMut, _env: Env, info: MessageInfo) -> Result { let curve_type = CURVE_TYPE.load(deps.storage)?; - let curve_fn = curve_type.to_curve_fn(); - let mut curve_state = CURVE_STATE.load(deps.storage)?; let payment = must_pay(&info, &curve_state.reserve_denom)?; @@ -27,25 +26,30 @@ pub fn execute_buy(deps: DepsMut, _env: Env, info: MessageInfo) -> Result { - let hatch_config = phase_config.hatch; // Check that the potential hatcher is allowlisted - assert_allowlisted(deps.storage, &info.sender)?; + let hatch_config = assert_allowlisted( + deps.querier, + deps.storage, + &info.sender, + &phase_config.hatch, + )?; // Update hatcher contribution - let contribution = update_hatcher_contributions(deps.storage, &info.sender, payment)?; - - // Check contribtuion is above minimum - if contribution < hatch_config.contribution_limits.min { - return Err(ContractError::ContributionLimit { - min: hatch_config.contribution_limits.min, - max: hatch_config.contribution_limits.max, - }); - } - - // Check contribution is below maximum - if contribution > hatch_config.contribution_limits.max { + 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, @@ -53,113 +57,101 @@ pub fn execute_buy(deps: DepsMut, _env: Env, info: MessageInfo) -> Result= hatch_config.initial_raise.max { + 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)?; } - - calculate_reserved_and_funded(payment, hatch_config.entry_fee) } - CommonsPhase::Open => calculate_reserved_and_funded(payment, phase_config.open.entry_fee), + CommonsPhase::Open => {} CommonsPhase::Closed => { return Err(ContractError::CommonsClosed {}); } }; - // Calculate how many tokens can be purchased with this and mint them - let curve = curve_fn(curve_state.clone().decimals); - curve_state.reserve += reserved; - curve_state.funding += funded; - - // Calculate the supply based on the reserve - let new_supply = curve.supply(curve_state.reserve); - let minted = new_supply - .checked_sub(curve_state.supply) - .map_err(StdError::overflow)?; - // 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 new_supply > max_supply { + if buy_quote.new_supply > max_supply { return Err(ContractError::CannotExceedMaxSupply { max: max_supply }); } } - // Save the new curve state - curve_state.supply = new_supply; - CURVE_STATE.save(deps.storage, &curve_state)?; - // 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: minted, + amount: buy_quote.amount, })?, funds: vec![], })]; // Send funding to fee recipient - if funded > Uint128::zero() { - let fees_recipient = FEES_RECIPIENT.load(deps.storage)?; - msgs.push(CosmosMsg::Bank(BankMsg::Send { - to_address: fees_recipient.to_string(), - amount: vec![Coin { - amount: funded, - denom: curve_state.reserve_denom, - }], - })) + 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("reserved", reserved) - .add_attribute("funded", funded) - .add_attribute("supply", minted)) -} - -/// Return the reserved and funded amounts based on the payment and the allocation ratio -fn calculate_reserved_and_funded( - payment: Uint128, - allocation_ratio: StdDecimal, -) -> (Uint128, Uint128) { - let funded = payment * allocation_ratio; - let reserved = payment.checked_sub(funded).unwrap(); // Since allocation_ratio is < 1, this subtraction is safe - (reserved, funded) -} - -/// Add the hatcher's contribution to the total contributions -fn update_hatcher_contributions( - storage: &mut dyn Storage, - hatcher: &Addr, - contribution: Uint128, -) -> StdResult { - HATCHERS.update(storage, hatcher, |amount| -> StdResult<_> { - match amount { - Some(mut amount) => { - amount += contribution; - Ok(amount) - } - None => Ok(contribution), - } - }) + .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 execute_sell( - deps: DepsMut, - _env: Env, - info: MessageInfo, -) -> Result { +pub fn sell(deps: DepsMut, _env: Env, info: MessageInfo) -> Result { let curve_type = CURVE_TYPE.load(deps.storage)?; - let curve_fn = curve_type.to_curve_fn(); - 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 @@ -183,98 +175,40 @@ pub fn execute_sell( }), ]; - let mut curve_state = CURVE_STATE.load(deps.storage)?; - let curve = curve_fn(curve_state.clone().decimals); - - // Reduce the supply by the amount burned - curve_state.supply = curve_state - .supply - .checked_sub(burn_amount) - .map_err(StdError::overflow)?; - - // Calculate the new reserve based on the new supply - let new_reserve = curve.reserve(curve_state.supply); - - // Calculate how many reserve tokens to release based on the sell amount - let released_reserve = curve_state - .reserve - .checked_sub(new_reserve) - .map_err(StdError::overflow)?; - - // Calculate the exit tax - let taxed_amount = calculate_exit_fee(deps.storage, released_reserve)?; + // 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 = new_reserve; - curve_state.funding += taxed_amount; + curve_state.reserve = sell_quote.new_reserve; + curve_state.supply = sell_quote.new_supply; CURVE_STATE.save(deps.storage, &curve_state)?; - // Calculate the amount of tokens to send to the sender - // Subtract the taxed amount from the released amount - let released = released_reserve - .checked_sub(taxed_amount) - .map_err(StdError::overflow)?; - - // Now send the tokens to the sender and any fees to the DAO - let mut send_msgs: Vec = vec![CosmosMsg::Bank(BankMsg::Send { - to_address: info.sender.to_string(), - amount: vec![Coin { - amount: released, - denom: curve_state.reserve_denom.clone(), - }], - })]; - - // Send exit fees to the to the fee recipient - if taxed_amount > Uint128::zero() { - let fees_recipient = FEES_RECIPIENT.load(deps.storage)?; - send_msgs.push(CosmosMsg::Bank(BankMsg::Send { - to_address: fees_recipient.to_string(), - amount: vec![Coin { - amount: taxed_amount, - denom: curve_state.reserve_denom, - }], - })) - } - Ok(Response::new() .add_messages(burn_msgs) .add_messages(send_msgs) - .add_attribute("action", "burn") + .add_attribute("action", "sell") .add_attribute("from", info.sender) .add_attribute("amount", burn_amount) - .add_attribute("burned", released_reserve) - .add_attribute("funded", taxed_amount)) -} - -/// Calculate the exit taxation for the sell amount based on the phase -fn calculate_exit_fee( - storage: &dyn Storage, - sell_amount: Uint128, -) -> Result { - // Load the phase config and phase - let phase = PHASE.load(storage)?; - let phase_config = PHASE_CONFIG.load(storage)?; - - // Calculate the exit tax based on the phase - let exit_fee = match &phase { - CommonsPhase::Hatch => phase_config.hatch.exit_fee, - CommonsPhase::Open => phase_config.open.exit_fee, - CommonsPhase::Closed => return Ok(Uint128::zero()), - }; - - // Ensure the exit fee is not greater than 100% - ensure!( - exit_fee <= StdDecimal::percent(100), - ContractError::InvalidExitFee {} - ); - - // This won't ever overflow because it's checked - let taxed_amount = sell_amount * exit_fee; - Ok(taxed_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 execute_close(deps: DepsMut, info: MessageInfo) -> Result { +pub fn close(deps: DepsMut, info: MessageInfo) -> Result { cw_ownable::assert_owner(deps.storage, &info.sender)?; PHASE.save(deps.storage, &CommonsPhase::Closed)?; @@ -283,42 +217,176 @@ pub fn execute_close(deps: DepsMut, info: MessageInfo) -> Result 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)?; - let payment = must_pay(&info, &curve_state.reserve_denom)?; - curve_state.funding += payment; + // 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)?; - // No minting of tokens is necessary, the supply stays the same - DONATIONS.save(deps.storage, &info.sender, &payment)?; + 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", "donate") - .add_attribute("donor", info.sender) - .add_attribute("amount", payment)) + .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(storage: &dyn Storage, hatcher: &Addr) -> Result<(), ContractError> { - let allowlist = HATCHER_ALLOWLIST.may_load(storage)?; - if let Some(allowlist) = allowlist { - ensure!( - allowlist.contains(hatcher), - ContractError::SenderNotAllowlisted { - sender: hatcher.to_string(), +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(()) + Ok(*hatch_config) } -/// Set the maxiumum supply (only callable by owner) +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, @@ -337,40 +405,127 @@ pub fn update_max_supply( .add_attribute("value", max_supply.unwrap_or(Uint128::MAX).to_string())) } -/// Add and remove addresses from the hatcher allowlist +/// 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) pub fn update_hatch_allowlist( deps: DepsMut, + env: Env, info: MessageInfo, - to_add: Vec, + to_add: Vec, to_remove: Vec, ) -> Result { cw_ownable::assert_owner(deps.storage, &info.sender)?; - let mut allowlist = HATCHER_ALLOWLIST.may_load(deps.storage)?; - - if allowlist.is_none() { - allowlist = Some(HashSet::new()); - } - let allowlist = allowlist.as_mut().unwrap(); + let list = hatcher_allowlist(); // Add addresses to the allowlist for allow in to_add { - let addr = deps.api.addr_validate(allow.as_str())?; - allowlist.insert(addr); + 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())?; - allowlist.remove(&addr); - } - HATCHER_ALLOWLIST.save(deps.storage, allowlist)?; + 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")])) } -/// Update the configuration of a particular phase +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, @@ -388,7 +543,6 @@ pub fn update_phase_config( match update_phase_config_msg { UpdatePhaseConfigMsg::Hatch { - exit_fee, initial_raise, entry_fee, contribution_limits, @@ -400,9 +554,6 @@ pub fn update_phase_config( if let Some(contribution_limits) = contribution_limits { phase_config.hatch.contribution_limits = contribution_limits; } - if let Some(exit_fee) = exit_fee { - phase_config.hatch.exit_fee = exit_fee; - } if let Some(initial_raise) = initial_raise { phase_config.hatch.initial_raise = initial_raise; } @@ -428,7 +579,7 @@ pub fn update_phase_config( phase_config.open.entry_fee = entry_fee; } if let Some(exit_fee) = exit_fee { - phase_config.hatch.exit_fee = exit_fee; + phase_config.open.exit_fee = exit_fee; } // Validate config @@ -442,7 +593,7 @@ pub fn update_phase_config( } } -/// Update the bonding curve. Only callable by the owner. +/// 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( @@ -487,14 +638,14 @@ mod tests { mod donate { use super::*; use crate::abc::CurveType; - use crate::testing::mock_init; + 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 { - execute_donate( + donate( deps, mock_env(), mock_info(TEST_DONOR, &[coin(donation_amount, TEST_RESERVE_DENOM)]), @@ -529,7 +680,7 @@ mod tests { let init_msg = default_instantiate_msg(2, 8, curve_type); mock_init(deps.as_mut(), init_msg)?; - let res = execute_donate( + let res = donate( deps.as_mut(), mock_env(), mock_info(TEST_DONOR, &[coin(1, "fake")]), @@ -544,22 +695,24 @@ mod tests { } #[test] - fn should_add_to_funding_pool() -> Result<(), ContractError> { + 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 init_msg = default_instantiate_msg(2, 8, curve_type); + 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 curve's funding has been increased while supply and reserve have not + // 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::new(donation_amount)); + 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))?; @@ -567,5 +720,89 @@ mod tests { 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 index 02dbfd3b0..c14dfb723 100644 --- a/contracts/external/cw-abc/src/contract.rs +++ b/contracts/external/cw-abc/src/contract.rs @@ -1,23 +1,22 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, SubMsg, - Uint128, WasmMsg, + 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 std::collections::HashSet; use crate::abc::{CommonsPhase, CurveFn}; -use crate::curves::DecimalPlaces; use crate::error::ContractError; use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; use crate::state::{ - CurveState, CURVE_STATE, CURVE_TYPE, FEES_RECIPIENT, HATCHER_ALLOWLIST, MAX_SUPPLY, PHASE, - PHASE_CONFIG, SUPPLY_DENOM, TOKEN_INSTANTIATION_INFO, TOKEN_ISSUER_CONTRACT, + 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}; @@ -30,54 +29,45 @@ const INSTANTIATE_TOKEN_FACTORY_ISSUER_REPLY_ID: u64 = 0; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, - _env: Env, + env: Env, info: MessageInfo, msg: InstantiateMsg, ) -> Result { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; let InstantiateMsg { - fees_recipient, + token_issuer_code_id, + funding_pool_forwarding, supply, reserve, curve_type, phase_config, hatcher_allowlist, - token_issuer_code_id, } = 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(), )); } - phase_config.validate()?; - - // Validate and store the fees recipient - FEES_RECIPIENT.save(deps.storage, &deps.api.addr_validate(&fees_recipient)?)?; - - // Save new token info for use in reply - TOKEN_INSTANTIATION_INFO.save(deps.storage, &supply)?; - if let Some(max_supply) = supply.max_supply { MAX_SUPPLY.save(deps.storage, &max_supply)?; } - // Save the curve type and state - let normalization_places = DecimalPlaces::new(supply.decimals, reserve.decimals); - let curve_state = CurveState::new(reserve.denom, normalization_places); - CURVE_STATE.save(deps.storage, &curve_state)?; + // Save the curve type CURVE_TYPE.save(deps.storage, &curve_type)?; - if let Some(allowlist) = hatcher_allowlist { - let allowlist = allowlist - .into_iter() - .map(|addr| deps.api.addr_validate(addr.as_str())) - .collect::>>()?; - HATCHER_ALLOWLIST.save(deps.storage, &allowlist)?; - } - PHASE_CONFIG.save(deps.storage, &phase_config)?; // TODO don't hardcode this? Make it configurable? Hatch config can be optional @@ -86,14 +76,21 @@ pub fn instantiate( // Initialize owner to sender cw_ownable::initialize_owner(deps.storage, deps.api, Some(info.sender.as_str()))?; - // Tnstantiate cw-token-factory-issuer contract - // Contract is immutable, no admin - let issuer_instantiate_msg = SubMsg::reply_always( + // 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.clone(), + subdenom: supply.subdenom, })?, funds: info.funds, label: "cw-tokenfactory-issuer".to_string(), @@ -101,7 +98,27 @@ pub fn instantiate( INSTANTIATE_TOKEN_FACTORY_ISSUER_REPLY_ID, ); - Ok(Response::default().add_submessage(issuer_instantiate_msg)) + // 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)] @@ -111,18 +128,29 @@ pub fn execute( 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::execute_buy(deps, env, info), - ExecuteMsg::Sell {} => commands::execute_sell(deps, env, info), - ExecuteMsg::Close {} => commands::execute_close(deps, info), - ExecuteMsg::Donate {} => commands::execute_donate(deps, env, info), + 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, info, 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) } @@ -153,15 +181,33 @@ pub fn do_query(deps: Deps, _env: Env, msg: QueryMsg, curve_fn: CurveFn) -> StdR QueryMsg::Donations { start_after, limit } => { to_json_binary(&queries::query_donations(deps, start_after, limit)?) } - QueryMsg::FeesRecipient {} => to_json_binary(&FEES_RECIPIENT.load(deps.storage)?), + 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)?) + } } } @@ -180,13 +226,15 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result Result 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; -} - -/// 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 -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) -} - -/// 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.clone().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.clone().to_supply(supply) - } -} - -/// 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.clone().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.clone().to_supply(supply) - } -} - -/// 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.clone().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.clone().to_supply(supply) - } -} - -// we multiply by 10^18, turn to int, take square root, then divide by 10^9 as we convert back to decimal -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 -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) -} - -/// DecimalPlaces should be passed into curve constructors -#[cw_serde] -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) - } -} - -#[cfg(test)] -mod tests { - use super::*; - // TODO: test DecimalPlaces return proper decimals - - #[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/contracts/external/cw-abc/src/error.rs b/contracts/external/cw-abc/src/error.rs index 6c343f21b..ef59faafc 100644 --- a/contracts/external/cw-abc/src/error.rs +++ b/contracts/external/cw-abc/src/error.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{StdError, Uint128}; +use cosmwasm_std::{CheckedMultiplyFractionError, OverflowError, StdError, Uint128}; use cw_utils::{ParseReplyError, PaymentError}; use thiserror::Error; @@ -7,6 +7,9 @@ pub enum ContractError { #[error("{0}")] Std(#[from] StdError), + #[error("{0}")] + Overflow(#[from] OverflowError), + #[error(transparent)] Payment(#[from] PaymentError), @@ -16,12 +19,18 @@ pub enum ContractError { #[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 }, @@ -54,4 +63,7 @@ pub enum ContractError { #[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 index d8a18c1cb..275d47efd 100644 --- a/contracts/external/cw-abc/src/lib.rs +++ b/contracts/external/cw-abc/src/lib.rs @@ -1,13 +1,13 @@ pub mod abc; pub(crate) mod commands; pub mod contract; -pub mod curves; mod error; +pub(crate) mod helpers; pub mod msg; mod queries; pub mod state; -// 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(test)] diff --git a/contracts/external/cw-abc/src/msg.rs b/contracts/external/cw-abc/src/msg.rs index c201760e2..f8ee0d289 100644 --- a/contracts/external/cw-abc/src/msg.rs +++ b/contracts/external/cw-abc/src/msg.rs @@ -1,16 +1,19 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Decimal, Decimal as StdDecimal, Uint128}; +use cosmwasm_std::{Addr, Decimal, Uint128}; -use crate::abc::{CommonsPhase, CommonsPhaseConfig, CurveType, MinMax, ReserveToken, SupplyToken}; +use crate::{ + abc::{CommonsPhase, CommonsPhaseConfig, CurveType, MinMax, ReserveToken, SupplyToken}, + state::{HatcherAllowlistConfigType, HatcherAllowlistEntry}, +}; #[cw_serde] pub struct InstantiateMsg { - /// The recipient for any fees collected from bonding curve operation - pub fees_recipient: String, - /// 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, @@ -26,7 +29,7 @@ pub struct InstantiateMsg { /// TODO different ways of doing this, for example DAO members? /// Using a whitelist contract? Merkle tree? /// Hatcher allowlist - pub hatcher_allowlist: Option>, + pub hatcher_allowlist: Option>, } /// Update the phase configurations. @@ -38,13 +41,12 @@ pub enum UpdatePhaseConfigMsg { contribution_limits: Option, // TODO what is the minimum used for? initial_raise: Option, - entry_fee: Option, - exit_fee: Option, + entry_fee: Option, }, /// Update the open phase configuration. Open { - exit_fee: Option, - entry_fee: Option, + exit_fee: Option, + entry_fee: Option, }, /// Update the closed phase configuration. /// TODO Set the curve type to be used on close? @@ -60,8 +62,14 @@ pub enum ExecuteMsg { /// Sell burns supply tokens in return for the reserve token. /// You must send only supply tokens. Sell {}, - /// Donate will add reserve tokens to the funding pool + /// 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. @@ -72,13 +80,22 @@ pub enum ExecuteMsg { /// TODO think about other potential limitations on this. UpdateCurve { curve_type: CurveType }, /// Update the hatch phase allowlist. - /// This can only be called by the owner. + /// Only callable by owner. UpdateHatchAllowlist { /// Addresses to be added. - to_add: Vec, + 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), @@ -109,10 +126,12 @@ pub enum QueryMsg { start_after: Option, limit: Option, }, - /// Returns the Fee Recipient for the contract. This is the address that - /// recieves any fees collected from bonding curve operation - #[returns(::cosmwasm_std::Addr)] - FeesRecipient {}, + #[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)] @@ -120,9 +139,26 @@ pub enum QueryMsg { start_after: Option, limit: Option, }, - /// Returns the Maxiumum Supply of the supply token + /// 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 {}, @@ -135,6 +171,20 @@ pub enum QueryMsg { 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 @@ -157,7 +207,7 @@ pub struct DenomResponse { #[cw_serde] pub struct HatcherAllowlistResponse { /// Hatcher allowlist - pub allowlist: Option>, + pub allowlist: Option>, } #[cw_serde] @@ -181,5 +231,13 @@ pub struct HatchersResponse { 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 index 8bc9138a6..0e07a8e28 100644 --- a/contracts/external/cw-abc/src/queries.rs +++ b/contracts/external/cw-abc/src/queries.rs @@ -1,12 +1,15 @@ use crate::abc::CurveFn; +use crate::helpers::{calculate_buy_quote, calculate_sell_quote}; use crate::msg::{ CommonsPhaseConfigResponse, CurveInfoResponse, DenomResponse, DonationsResponse, - HatchersResponse, + HatcherAllowlistResponse, HatchersResponse, QuoteResponse, }; use crate::state::{ - CurveState, CURVE_STATE, DONATIONS, HATCHERS, MAX_SUPPLY, PHASE, PHASE_CONFIG, SUPPLY_DENOM, + hatcher_allowlist, CurveState, HatcherAllowlistConfigType, HatcherAllowlistEntry, CURVE_STATE, + CURVE_TYPE, DONATIONS, HATCHERS, MAX_SUPPLY, PHASE, PHASE_CONFIG, SUPPLY_DENOM, }; -use cosmwasm_std::{Deps, Order, QuerierWrapper, StdResult, Uint128}; +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 @@ -85,6 +88,51 @@ pub fn query_hatchers( 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)?; @@ -100,3 +148,25 @@ pub fn query_phase_config(deps: Deps) -> StdResult { 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 index dc4f6b022..74393cf0a 100644 --- a/contracts/external/cw-abc/src/state.rs +++ b/contracts/external/cw-abc/src/state.rs @@ -1,11 +1,10 @@ -use cosmwasm_schema::cw_serde; -use std::collections::HashSet; - -use crate::abc::{CommonsPhase, CommonsPhaseConfig, CurveType, SupplyToken}; -use cosmwasm_std::{Addr, Uint128}; -use cw_storage_plus::{Item, Map}; +use std::fmt::{self, Display}; -use crate::curves::DecimalPlaces; +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] @@ -17,10 +16,10 @@ pub struct CurveState { /// supply is how many tokens this contract has issued pub supply: Uint128, - // the denom of the reserve token + /// the denom of the reserve token pub reserve_denom: String, - // how to normalize reserve and supply + /// how to normalize reserve and supply pub decimals: DecimalPlaces, } @@ -36,12 +35,88 @@ impl CurveState { } } +/// 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 recipient for fees generated from bonding curve operation -pub const FEES_RECIPIENT: Item = Item::new("fees_recipient"); +/// 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"); @@ -49,10 +124,6 @@ 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"); -/// Hatcher phase allowlist -/// TODO: we could use the keys for the [`HATCHERS`] map instead setting them to 0 at the beginning, though existing hatchers would not be able to be removed -pub static HATCHER_ALLOWLIST: Item> = Item::new("hatch_allowlist"); - /// 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 @@ -67,8 +138,8 @@ 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 token_instantiation_info when creating a new Token Factory denom -pub const TOKEN_INSTANTIATION_INFO: Item = Item::new("token_instantiation_info"); +/// 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 index 7f88f5a5d..0f0681d80 100644 --- a/contracts/external/cw-abc/src/test_tube/integration_tests.rs +++ b/contracts/external/cw-abc/src/test_tube/integration_tests.rs @@ -4,15 +4,17 @@ use crate::{ ReserveToken, SupplyToken, }, msg::{ - CommonsPhaseConfigResponse, CurveInfoResponse, DenomResponse, ExecuteMsg, InstantiateMsg, - QueryMsg, + 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}; +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}; @@ -29,6 +31,22 @@ fn test_happy_path() { .. } = 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(); @@ -78,7 +96,7 @@ fn test_happy_path() { CurveInfoResponse { reserve: Uint128::new(900), supply: Uint128::new(9000), - funding: Uint128::new(100), + funding: Uint128::new(0), spot_price: Decimal::percent(10u64), reserve_denom: RESERVE.to_string(), } @@ -86,7 +104,7 @@ fn test_happy_path() { // Query phase let phase: CommonsPhaseConfigResponse = abc.query(&QueryMsg::PhaseConfig {}).unwrap(); - assert_eq!(phase.phase, CommonsPhase::Hatch); + assert!(matches!(phase.phase, CommonsPhase::Hatch)); assert_eq!( phase.phase_config, CommonsPhaseConfig { @@ -97,10 +115,9 @@ fn test_happy_path() { }, initial_raise: MinMax { min: Uint128::from(10u128), - max: Uint128::from(1000000u128), + max: Uint128::from(900_000u128), }, entry_fee: Decimal::percent(10u64), - exit_fee: Decimal::percent(10u64), }, open: OpenConfig { entry_fee: Decimal::percent(10u64), @@ -110,10 +127,44 @@ fn test_happy_path() { } ); + // 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(100, denom.clone()), + &coins(1000, denom.clone()), &accounts[0], ) .unwrap(); @@ -123,9 +174,9 @@ fn test_happy_path() { assert_eq!( curve_info, CurveInfoResponse { - reserve: Uint128::new(890), - supply: Uint128::new(8900), - funding: Uint128::new(101), + reserve: Uint128::new(900800u128), + supply: Uint128::new(9008000u128), + funding: Uint128::new(0), spot_price: Decimal::percent(10u64), reserve_denom: RESERVE.to_string(), } @@ -152,24 +203,16 @@ fn test_happy_path() { user_balance.balance, Some(Coin { denom: denom.clone(), - amount: "8900".to_string(), + amount: "8000".to_string(), }) ); assert_eq!( contract_balance.balance, Some(Coin { denom: RESERVE.to_string(), - amount: "890".to_string(), + amount: "900800".to_string(), }) ); - - // 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); } #[test] @@ -187,7 +230,7 @@ fn test_contribution_limits_enforced() { let err = abc .execute( &ExecuteMsg::Buy {}, - &coins(1000000000, RESERVE), + &coins(1_000_000_000, RESERVE), &accounts[0], ) .unwrap_err(); @@ -225,8 +268,12 @@ fn test_max_supply() { } = env; // Buy enough tokens to end the hatch phase - abc.execute(&ExecuteMsg::Buy {}, &coins(1000000, RESERVE), &accounts[0]) - .unwrap(); + abc.execute( + &ExecuteMsg::Buy {}, + &coins(1_000_000, RESERVE), + &accounts[0], + ) + .unwrap(); // Buy enough tokens to trigger a max supply error let err = abc @@ -280,8 +327,8 @@ fn test_allowlist() { let app = OsmosisTestApp::new(); let builder = TestEnvBuilder::new(); let instantiate_msg = InstantiateMsg { - fees_recipient: "replaced to accounts[0]".to_string(), token_issuer_code_id: 0, + funding_pool_forwarding: Some("replaced to accounts[0]".to_string()), supply: SupplyToken { subdenom: DENOM.to_string(), metadata: None, @@ -303,7 +350,6 @@ fn test_allowlist() { max: Uint128::from(1000000u128), }, entry_fee: Decimal::percent(10u64), - exit_fee: Decimal::percent(10u64), }, open: OpenConfig { entry_fee: Decimal::percent(10u64), @@ -328,7 +374,22 @@ fn test_allowlist() { let err = abc .execute( &ExecuteMsg::UpdateHatchAllowlist { - to_add: vec![accounts[0].address(), accounts[1].address()], + 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![], }, &[], @@ -346,7 +407,22 @@ fn test_allowlist() { // instantiation. abc.execute( &ExecuteMsg::UpdateHatchAllowlist { - to_add: vec![accounts[0].address(), accounts[1].address()], + 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![], }, &[], @@ -439,8 +515,12 @@ fn test_update_curve() { .denom; // Buy enough tokens to end the hatch phase - abc.execute(&ExecuteMsg::Buy {}, &coins(1000000, RESERVE), &accounts[0]) - .unwrap(); + abc.execute( + &ExecuteMsg::Buy {}, + &coins(1_000_000, RESERVE), + &accounts[0], + ) + .unwrap(); // Only owner can update the curve let err = abc @@ -514,3 +594,181 @@ fn test_update_curve() { }) ); } + +#[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/test_env.rs b/contracts/external/cw-abc/src/test_tube/test_env.rs index 28dec2752..2ee793794 100644 --- a/contracts/external/cw-abc/src/test_tube/test_env.rs +++ b/contracts/external/cw-abc/src/test_tube/test_env.rs @@ -11,12 +11,29 @@ use crate::{ ContractError, }; -use cosmwasm_std::{Coin, Decimal, Uint128}; -use dao_testing::test_tube::cw_tokenfactory_issuer::TokenfactoryIssuer; +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, - osmosis_std::types::cosmwasm::wasm::v1::MsgExecuteContractResponse, Account, Bank, Module, - OsmosisTestApp, RunnerError, RunnerExecuteResult, SigningAccount, Wasm, + 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; @@ -51,6 +68,114 @@ impl<'a> TestEnv<'a> { 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, @@ -105,13 +230,13 @@ impl TestEnvBuilder { let abc = CwAbc::deploy( app, &InstantiateMsg { - fees_recipient: accounts[0].address(), 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)), + max_supply: Some(Uint128::from(1_000_000_000u128)), }, reserve: ReserveToken { denom: RESERVE.to_string(), @@ -121,14 +246,13 @@ impl TestEnvBuilder { hatch: HatchConfig { contribution_limits: MinMax { min: Uint128::from(10u128), - max: Uint128::from(1000000u128), + max: Uint128::from(1_000_000u128), }, initial_raise: MinMax { min: Uint128::from(10u128), - max: Uint128::from(1000000u128), + max: Uint128::from(900_000u128), // 1m - 10% }, entry_fee: Decimal::percent(10u64), - exit_fee: Decimal::percent(10u64), }, open: OpenConfig { entry_fee: Decimal::percent(10u64), @@ -169,9 +293,9 @@ impl TestEnvBuilder { let issuer_id = TokenfactoryIssuer::upload(app, &accounts[0])?; - // Override issuer_id and fees_recipient msg.token_issuer_code_id = issuer_id; - msg.fees_recipient = accounts[0].address(); + + msg.funding_pool_forwarding = Some(accounts[0].address()); let abc = CwAbc::deploy(app, &msg, &accounts[0])?; diff --git a/contracts/external/cw-abc/src/testing.rs b/contracts/external/cw-abc/src/testing.rs index b324ca8ed..4caa65889 100644 --- a/contracts/external/cw-abc/src/testing.rs +++ b/contracts/external/cw-abc/src/testing.rs @@ -42,8 +42,8 @@ pub fn default_instantiate_msg( curve_type: CurveType, ) -> InstantiateMsg { InstantiateMsg { - fees_recipient: TEST_CREATOR.to_string(), token_issuer_code_id: 1, + funding_pool_forwarding: None, supply: SupplyToken { subdenom: TEST_SUPPLY_DENOM.to_string(), metadata: Some(default_supply_metadata()), @@ -65,7 +65,6 @@ pub fn default_instantiate_msg( max: Uint128::from(1000000u128), }, entry_fee: Decimal::percent(10u64), - exit_fee: Decimal::zero(), }, open: OpenConfig { entry_fee: Decimal::percent(10u64), 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/schema/dao-abc-factory.json b/contracts/external/dao-abc-factory/schema/dao-abc-factory.json index db9be42c1..d1560feea 100644 --- a/contracts/external/dao-abc-factory/schema/dao-abc-factory.json +++ b/contracts/external/dao-abc-factory/schema/dao-abc-factory.json @@ -205,7 +205,6 @@ "required": [ "contribution_limits", "entry_fee", - "exit_fee", "initial_raise" ], "properties": { @@ -225,30 +224,108 @@ } ] }, - "exit_fee": { - "description": "Exit tax for the hatch phase", + "initial_raise": { + "description": "The initial raise range (min, max) in the reserve token", "allOf": [ { - "$ref": "#/definitions/Decimal" + "$ref": "#/definitions/MinMax" } ] - }, - "initial_raise": { - "description": "The initial raise range (min, max) in the reserve token", + } + }, + "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", - "fees_recipient", "phase_config", "reserve", "supply", @@ -263,9 +340,12 @@ } ] }, - "fees_recipient": { - "description": "The recipient for any fees collected from bonding curve operation", - "type": "string" + "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", @@ -274,7 +354,7 @@ "null" ], "items": { - "type": "string" + "$ref": "#/definitions/HatcherAllowlistEntryMsg" } }, "phase_config": { @@ -311,7 +391,7 @@ "additionalProperties": false }, "MinMax": { - "description": "Struct for minimium and maximum values", + "description": "Struct for minimum and maximum values", "type": "object", "required": [ "max", @@ -455,6 +535,10 @@ "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" } } }, 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 index a31a88822..d57f6d57b 100644 --- a/contracts/external/dao-abc-factory/src/test_tube/test_env.rs +++ b/contracts/external/dao-abc-factory/src/test_tube/test_env.rs @@ -135,8 +135,8 @@ impl TestEnvBuilder { contract_addr: dao_abc_factory.contract_addr.clone(), msg: to_json_binary(&ExecuteMsg::AbcFactory { instantiate_msg: cw_abc::msg::InstantiateMsg { - fees_recipient: accounts[0].address(), token_issuer_code_id: issuer_id, + funding_pool_forwarding: Some(accounts[0].address()), supply: SupplyToken { subdenom: DENOM.to_string(), metadata: None, @@ -158,7 +158,6 @@ impl TestEnvBuilder { max: Uint128::from(1000000u128), }, entry_fee: Decimal::percent(10u64), - exit_fee: Decimal::percent(10u64), }, open: OpenConfig { entry_fee: Decimal::percent(10u64), @@ -247,8 +246,8 @@ impl TestEnvBuilder { contract_addr: dao_abc_factory.contract_addr.clone(), msg: to_json_binary(&ExecuteMsg::AbcFactory { instantiate_msg: cw_abc::msg::InstantiateMsg { - fees_recipient: accounts[0].address(), token_issuer_code_id: issuer_id, + funding_pool_forwarding: Some(accounts[0].address()), supply: SupplyToken { subdenom: DENOM.to_string(), metadata: None, @@ -270,7 +269,6 @@ impl TestEnvBuilder { max: Uint128::from(1000000u128), }, entry_fee: Decimal::percent(10u64), - exit_fee: Decimal::percent(10u64), }, open: OpenConfig { entry_fee: Decimal::percent(10u64), 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/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/src/test_tube/mod.rs b/packages/dao-testing/src/test_tube/mod.rs index e609fa47e..f7e1cb743 100644 --- a/packages/dao-testing/src/test_tube/mod.rs +++ b/packages/dao-testing/src/test_tube/mod.rs @@ -2,7 +2,7 @@ // 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")] 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 From 455880f57727961f61ed5660c30e0c32b64d64ef Mon Sep 17 00:00:00 2001 From: ismellike Date: Mon, 20 May 2024 13:06:39 -0500 Subject: [PATCH 56/56] Fix issue with instantiating cw-abc with hatchers (#830) allow self to call method --- contracts/external/cw-abc/src/commands.rs | 6 ++++-- .../cw-abc/src/test_tube/integration_tests.rs | 11 ++++++++--- .../external/cw-abc/src/test_tube/test_env.rs | 18 +++++++++++++++++- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/contracts/external/cw-abc/src/commands.rs b/contracts/external/cw-abc/src/commands.rs index 6bb825e82..c929866c2 100644 --- a/contracts/external/cw-abc/src/commands.rs +++ b/contracts/external/cw-abc/src/commands.rs @@ -417,7 +417,7 @@ pub fn toggle_pause(deps: DepsMut, info: MessageInfo) -> Result, to_remove: Vec, ) -> Result { - cw_ownable::assert_owner(deps.storage, &info.sender)?; + if env.contract.address != info.sender { + cw_ownable::assert_owner(deps.storage, &info.sender)?; + } let list = hatcher_allowlist(); diff --git a/contracts/external/cw-abc/src/test_tube/integration_tests.rs b/contracts/external/cw-abc/src/test_tube/integration_tests.rs index 0f0681d80..290f1efcc 100644 --- a/contracts/external/cw-abc/src/test_tube/integration_tests.rs +++ b/contracts/external/cw-abc/src/test_tube/integration_tests.rs @@ -357,7 +357,13 @@ fn test_allowlist() { }, closed: ClosedConfig {}, }, - hatcher_allowlist: None, + 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, @@ -403,8 +409,7 @@ fn test_allowlist() { )) ); - // Enable the allow list, normally this would be passed in through - // instantiation. + // Update the allowlist abc.execute( &ExecuteMsg::UpdateHatchAllowlist { to_add: vec![ diff --git a/contracts/external/cw-abc/src/test_tube/test_env.rs b/contracts/external/cw-abc/src/test_tube/test_env.rs index 2ee793794..87cae932b 100644 --- a/contracts/external/cw-abc/src/test_tube/test_env.rs +++ b/contracts/external/cw-abc/src/test_tube/test_env.rs @@ -234,7 +234,17 @@ impl TestEnvBuilder { funding_pool_forwarding: Some(accounts[0].address()), supply: SupplyToken { subdenom: DENOM.to_string(), - metadata: None, + 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)), }, @@ -297,6 +307,12 @@ impl TestEnvBuilder { 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 {})?;