diff --git a/Cargo.lock b/Cargo.lock index df4cc21e4..ce65e3289 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,12 +37,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "anyhow" version = "1.0.75" @@ -88,17 +82,6 @@ dependencies = [ "syn 2.0.29", ] -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -189,29 +172,6 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" -[[package]] -name = "bindgen" -version = "0.60.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "062dddbc1ba4aca46de6338e2bf87771414c335f7b2f2036e8f3e9befebf88e6" -dependencies = [ - "bitflags 1.3.2", - "cexpr", - "clang-sys", - "clap", - "env_logger 0.9.3", - "lazy_static", - "lazycell", - "log", - "peeking_take_while", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "which", -] - [[package]] name = "bip32" version = "0.4.0" @@ -283,7 +243,7 @@ dependencies = [ "dao-proposal-single", "dao-voting 2.2.0", "dao-voting-cw20-staked", - "env_logger 0.10.0", + "env_logger", "serde", "serde_json", "serde_yaml", @@ -328,66 +288,12 @@ dependencies = [ "libc", ] -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "chrono" -version = "0.4.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" -dependencies = [ - "android-tzdata", - "num-traits", -] - -[[package]] -name = "clang-sys" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "clap" -version = "3.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" -dependencies = [ - "atty", - "bitflags 1.3.2", - "clap_lex", - "indexmap 1.9.3", - "strsim", - "termcolor", - "textwrap", -] - -[[package]] -name = "clap_lex" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", -] - [[package]] name = "config" version = "0.13.3" @@ -452,7 +358,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "596064e3608349aa302eb68b2df8ed3a66bbb51d9b470dbd9afff70843e44642" dependencies = [ "async-trait", - "cosmrs 0.10.0", + "cosmrs", "regex", "schemars", "serde", @@ -461,17 +367,6 @@ dependencies = [ "tonic 0.8.3", ] -[[package]] -name = "cosmos-sdk-proto" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20b42021d8488665b1a0d9748f1f81df7235362d194f44481e2e61bf376b77b4" -dependencies = [ - "prost 0.11.9", - "prost-types", - "tendermint-proto 0.23.9", -] - [[package]] name = "cosmos-sdk-proto" version = "0.15.0" @@ -496,27 +391,6 @@ dependencies = [ "tonic 0.9.2", ] -[[package]] -name = "cosmrs" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3903590099dcf1ea580d9353034c9ba1dbf55d1389a5bd2ade98535c3445d1f9" -dependencies = [ - "bip32", - "cosmos-sdk-proto 0.14.0", - "ecdsa", - "eyre", - "getrandom", - "k256", - "rand_core 0.6.4", - "serde", - "serde_json", - "subtle-encoding", - "tendermint 0.23.9", - "tendermint-rpc 0.23.9", - "thiserror", -] - [[package]] name = "cosmrs" version = "0.10.0" @@ -533,8 +407,8 @@ dependencies = [ "serde", "serde_json", "subtle-encoding", - "tendermint 0.26.0", - "tendermint-rpc 0.26.0", + "tendermint", + "tendermint-rpc", "thiserror", ] @@ -1871,7 +1745,6 @@ dependencies = [ "dao-voting-native-staked", "rand", "thiserror", - "token-bindings 0.10.3 (git+https://github.com/CosmosContracts/token-bindings.git?branch=update-token-bindings-test)", ] [[package]] @@ -1913,7 +1786,6 @@ dependencies = [ "dao-voting-cw721-staked", "dao-voting-native-staked", "thiserror", - "token-bindings-test", ] [[package]] @@ -2113,6 +1985,25 @@ dependencies = [ [[package]] name = "dao-voting-native-staked" version = "2.2.0" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-controllers 1.1.0", + "cw-multi-test", + "cw-paginate-storage 2.2.0", + "cw-storage-plus 1.1.0", + "cw-utils 1.0.1", + "cw2 1.1.0", + "dao-dao-macros", + "dao-interface", + "thiserror", +] + +[[package]] +name = "dao-voting-token-factory-staked" +version = "2.2.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -2128,7 +2019,6 @@ dependencies = [ "dao-dao-macros", "dao-interface", "dao-voting 2.2.0", - "osmosis-test-tube", "thiserror", "token-bindings 0.10.3 (git+https://github.com/CosmosContracts/token-bindings.git?branch=update-token-bindings-test)", "token-bindings-test", @@ -2261,19 +2151,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "env_logger" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - [[package]] name = "env_logger" version = "0.10.0" @@ -2492,12 +2369,6 @@ version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - [[package]] name = "group" version = "0.12.1" @@ -2568,15 +2439,6 @@ dependencies = [ "http", ] -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - [[package]] name = "hermit-abi" version = "0.3.2" @@ -2785,7 +2647,7 @@ dependencies = [ "dao-voting 2.2.0", "dao-voting-cw20-staked", "dao-voting-cw721-staked", - "env_logger 0.10.0", + "env_logger", "once_cell", "rand", "serde", @@ -2799,7 +2661,7 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ - "hermit-abi 0.3.2", + "hermit-abi", "rustix", "windows-sys", ] @@ -2876,28 +2738,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "libc" version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - [[package]] name = "linked-hash-map" version = "0.5.6" @@ -2996,7 +2842,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.2", + "hermit-abi", "libc", ] @@ -3046,75 +2892,6 @@ dependencies = [ "hashbrown 0.12.3", ] -[[package]] -name = "os_str_bytes" -version = "6.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" - -[[package]] -name = "osmosis-std" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75895e4db1a81ca29118e366365744f64314938327e4eedba8e6e462fb15e94f" -dependencies = [ - "chrono", - "cosmwasm-std", - "osmosis-std-derive", - "prost 0.11.9", - "prost-types", - "schemars", - "serde", - "serde-cw-value", -] - -[[package]] -name = "osmosis-std" -version = "0.17.0-rc0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b022b748710ecdf1adc6a124c3bef29f17ef05e7fa1260a08889d1d53f9cc5" -dependencies = [ - "chrono", - "cosmwasm-std", - "osmosis-std-derive", - "prost 0.11.9", - "prost-types", - "schemars", - "serde", - "serde-cw-value", -] - -[[package]] -name = "osmosis-std-derive" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47f0b2f22adb341bb59e5a3a1b464dde033181954bd055b9ae86d6511ba465b" -dependencies = [ - "itertools 0.10.5", - "proc-macro2", - "prost-types", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "osmosis-test-tube" -version = "16.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61443b780ea4a1b585c41671f2eb17054ab641617dc0befde2ed34f699854b7e" -dependencies = [ - "base64 0.13.1", - "bindgen", - "cosmrs 0.9.0", - "cosmwasm-std", - "osmosis-std 0.16.2", - "prost 0.11.9", - "serde", - "serde_json", - "test-tube", - "thiserror", -] - [[package]] name = "paste" version = "1.0.14" @@ -3136,12 +2913,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - [[package]] name = "peg" version = "0.7.0" @@ -3490,12 +3261,6 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustix" version = "0.38.8" @@ -3650,15 +3415,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-cw-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75d32da6b8ed758b7d850b6c3c08f1d7df51a4df3cb201296e63e34a78e99d4" -dependencies = [ - "serde", -] - [[package]] name = "serde-json-wasm" version = "0.5.1" @@ -3779,12 +3535,6 @@ dependencies = [ "keccak", ] -[[package]] -name = "shlex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" - [[package]] name = "signature" version = "1.6.4" @@ -3898,12 +3648,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "subtle" version = "2.5.0" @@ -3947,37 +3691,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" -[[package]] -name = "tendermint" -version = "0.23.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "467f82178deeebcd357e1273a0c0b77b9a8a0313ef7c07074baebe99d87851f4" -dependencies = [ - "async-trait", - "bytes", - "ed25519", - "ed25519-dalek", - "flex-error", - "futures", - "k256", - "num-traits", - "once_cell", - "prost 0.11.9", - "prost-types", - "ripemd160", - "serde", - "serde_bytes", - "serde_json", - "serde_repr", - "sha2 0.9.9", - "signature", - "subtle", - "subtle-encoding", - "tendermint-proto 0.23.9", - "time", - "zeroize", -] - [[package]] name = "tendermint" version = "0.26.0" @@ -4009,20 +3722,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "tendermint-config" -version = "0.23.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d42ee0abc27ef5fc34080cce8d43c189950d331631546e7dfb983b6274fa327" -dependencies = [ - "flex-error", - "serde", - "serde_json", - "tendermint 0.23.9", - "toml", - "url", -] - [[package]] name = "tendermint-config" version = "0.26.0" @@ -4032,29 +3731,11 @@ dependencies = [ "flex-error", "serde", "serde_json", - "tendermint 0.26.0", + "tendermint", "toml", "url", ] -[[package]] -name = "tendermint-proto" -version = "0.23.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ce80bf536476db81ecc9ebab834dc329c9c1509a694f211a73858814bfe023" -dependencies = [ - "bytes", - "flex-error", - "num-derive", - "num-traits", - "prost 0.11.9", - "prost-types", - "serde", - "serde_bytes", - "subtle-encoding", - "time", -] - [[package]] name = "tendermint-proto" version = "0.26.0" @@ -4091,39 +3772,6 @@ dependencies = [ "time", ] -[[package]] -name = "tendermint-rpc" -version = "0.23.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f14aafe3528a0f75e9f3f410b525617b2de16c4b7830a21f717eee62882ec60" -dependencies = [ - "async-trait", - "bytes", - "flex-error", - "futures", - "getrandom", - "http", - "hyper", - "hyper-proxy", - "hyper-rustls", - "peg", - "pin-project", - "serde", - "serde_bytes", - "serde_json", - "subtle-encoding", - "tendermint 0.23.9", - "tendermint-config 0.23.9", - "tendermint-proto 0.23.9", - "thiserror", - "time", - "tokio", - "tracing", - "url", - "uuid", - "walkdir", -] - [[package]] name = "tendermint-rpc" version = "0.26.0" @@ -4146,8 +3794,8 @@ dependencies = [ "serde_json", "subtle", "subtle-encoding", - "tendermint 0.26.0", - "tendermint-config 0.26.0", + "tendermint", + "tendermint-config", "tendermint-proto 0.26.0", "thiserror", "time", @@ -4188,28 +3836,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "test-tube" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b1f7cafdf7738331999fb1465d2d3032f08ac61940e1ef4601dbbef21d6a5e" -dependencies = [ - "base64 0.13.1", - "cosmrs 0.9.0", - "cosmwasm-std", - "osmosis-std 0.17.0-rc0", - "prost 0.11.9", - "serde", - "serde_json", - "thiserror", -] - -[[package]] -name = "textwrap" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" - [[package]] name = "thiserror" version = "1.0.47" @@ -4712,17 +4338,6 @@ dependencies = [ "webpki", ] -[[package]] -name = "which" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" -dependencies = [ - "either", - "libc", - "once_cell", -] - [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 142a447a9..50f646221 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,7 @@ cw721 = "0.18" cw721-base = "0.18" env_logger = "0.10" once_cell = "1.18" -osmosis-test-tube = "16.1.2" +# osmosis-test-tube = "16.1.2" proc-macro2 = "1.0" quote = "1.0" rand = "0.8" @@ -107,6 +107,7 @@ dao-voting-cw4 = { path = "./contracts/voting/dao-voting-cw4", version = "2.2.0" dao-voting-cw721-roles = { path = "./contracts/voting/dao-voting-cw721-roles", version = "*" } dao-voting-cw721-staked = { path = "./contracts/voting/dao-voting-cw721-staked", version = "2.2.0" } dao-voting-native-staked = { path = "./contracts/voting/dao-voting-native-staked", version = "2.2.0" } +dao-voting-token-factory-staked = { path = "./contracts/voting/dao-voting-token-factory-staked", version = "2.2.0" } # v1 dependencies. used for state migrations. cw-core-v1 = { package = "cw-core", version = "0.1.0" } diff --git a/contracts/proposal/dao-proposal-multiple/Cargo.toml b/contracts/proposal/dao-proposal-multiple/Cargo.toml index 8eaa57672..35f200f8e 100644 --- a/contracts/proposal/dao-proposal-multiple/Cargo.toml +++ b/contracts/proposal/dao-proposal-multiple/Cargo.toml @@ -58,4 +58,3 @@ cw721-base = { workspace = true } cw4 = { workspace = true } cw4-group = { workspace = true } rand = { workspace = true } -token-bindings = { workspace = true } diff --git a/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs b/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs index ab1614412..b224694be 100644 --- a/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs +++ b/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs @@ -7,7 +7,7 @@ use dao_pre_propose_multiple as cppm; use dao_testing::contracts::{ cw20_balances_voting_contract, cw20_base_contract, cw20_stake_contract, cw20_staked_balances_voting_contract, cw4_group_contract, cw721_base_contract, - dao_dao_contract, pre_propose_multiple_contract, + dao_dao_contract, native_staked_balances_voting_contract, pre_propose_multiple_contract, }; use dao_voting::{ deposit::{DepositRefundPolicy, UncheckedDepositInfo}, @@ -223,116 +223,114 @@ pub fn _instantiate_with_staked_cw721_governance( core_addr } -// pub fn _instantiate_with_native_staked_balances_governance( -// app: &mut App, -// proposal_module_instantiate: InstantiateMsg, -// initial_balances: Option>, -// ) -> Addr { -// let proposal_module_code_id = app.store_code(proposal_multiple_contract()); - -// let initial_balances = initial_balances.unwrap_or_else(|| { -// vec![Cw20Coin { -// address: CREATOR_ADDR.to_string(), -// amount: Uint128::new(100_000_000), -// }] -// }); - -// // Collapse balances so that we can test double votes. -// let initial_balances: Vec = { -// let mut already_seen = vec![]; -// initial_balances -// .into_iter() -// .filter(|Cw20Coin { address, amount: _ }| { -// if already_seen.contains(address) { -// false -// } else { -// already_seen.push(address.clone()); -// true -// } -// }) -// .collect() -// }; - -// let native_stake_id = app.store_code(native_staked_balances_voting_contract()); -// let core_contract_id = app.store_code(dao_dao_contract()); - -// let instantiate_core = dao_interface::msg::InstantiateMsg { -// admin: None, -// name: "DAO DAO".to_string(), -// description: "A DAO that builds DAOs".to_string(), -// image_url: None, -// automatically_add_cw20s: true, -// automatically_add_cw721s: false, -// voting_module_instantiate_info: ModuleInstantiateInfo { -// code_id: native_stake_id, -// msg: to_binary(&dao_voting_native_staked::msg::InstantiateMsg { -// owner: Some(Admin::CoreModule {}), -// manager: None, -// token_info: dao_voting_native_staked::msg::TokenInfo::Existing { -// denom: "ujuno".to_string(), -// }, -// unstaking_duration: None, -// active_threshold: None, -// }) -// .unwrap(), -// admin: None, -// label: "DAO DAO voting module".to_string(), -// }, -// proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { -// code_id: proposal_module_code_id, -// label: "DAO DAO governance module.".to_string(), -// admin: Some(Admin::CoreModule {}), -// msg: to_binary(&proposal_module_instantiate).unwrap(), -// }], -// initial_items: None, -// dao_uri: None, -// }; - -// let core_addr = app -// .instantiate_contract( -// core_contract_id, -// Addr::unchecked(CREATOR_ADDR), -// &instantiate_core, -// &[], -// "DAO DAO", -// None, -// ) -// .unwrap(); - -// let gov_state: dao_interface::query::DumpStateResponse = app -// .wrap() -// .query_wasm_smart( -// core_addr.clone(), -// &dao_interface::msg::QueryMsg::DumpState {}, -// ) -// .unwrap(); -// let native_staking_addr = gov_state.voting_module; - -// for Cw20Coin { address, amount } in initial_balances { -// app.sudo(SudoMsg::Bank(BankSudo::Mint { -// to_address: address.clone(), -// amount: vec![Coin { -// denom: "ujuno".to_string(), -// amount, -// }], -// })) -// .unwrap(); -// app.execute_contract( -// Addr::unchecked(&address), -// native_staking_addr.clone(), -// &dao_voting_native_staked::msg::ExecuteMsg::Stake {}, -// &[Coin { -// amount, -// denom: "ujuno".to_string(), -// }], -// ) -// .unwrap(); -// } - -// app.update_block(next_block); - -// core_addr -// } +pub fn _instantiate_with_native_staked_balances_governance( + app: &mut App, + proposal_module_instantiate: InstantiateMsg, + initial_balances: Option>, +) -> Addr { + let proposal_module_code_id = app.store_code(proposal_multiple_contract()); + + let initial_balances = initial_balances.unwrap_or_else(|| { + vec![Cw20Coin { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(100_000_000), + }] + }); + + // Collapse balances so that we can test double votes. + let initial_balances: Vec = { + let mut already_seen = vec![]; + initial_balances + .into_iter() + .filter(|Cw20Coin { address, amount: _ }| { + if already_seen.contains(address) { + false + } else { + already_seen.push(address.clone()); + true + } + }) + .collect() + }; + + let native_stake_id = app.store_code(native_staked_balances_voting_contract()); + let core_contract_id = app.store_code(dao_dao_contract()); + + let instantiate_core = dao_interface::msg::InstantiateMsg { + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs".to_string(), + image_url: None, + automatically_add_cw20s: true, + automatically_add_cw721s: false, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: native_stake_id, + msg: to_binary(&dao_voting_native_staked::msg::InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: None, + denom: "ujuno".to_string(), + unstaking_duration: None, + // active_threshold: None, + }) + .unwrap(), + admin: None, + label: "DAO DAO voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_module_code_id, + label: "DAO DAO governance module.".to_string(), + admin: Some(Admin::CoreModule {}), + msg: to_binary(&proposal_module_instantiate).unwrap(), + }], + initial_items: None, + dao_uri: None, + }; + + let core_addr = app + .instantiate_contract( + core_contract_id, + Addr::unchecked(CREATOR_ADDR), + &instantiate_core, + &[], + "DAO DAO", + None, + ) + .unwrap(); + + let gov_state: dao_interface::query::DumpStateResponse = app + .wrap() + .query_wasm_smart( + core_addr.clone(), + &dao_interface::msg::QueryMsg::DumpState {}, + ) + .unwrap(); + let native_staking_addr = gov_state.voting_module; + + for Cw20Coin { address, amount } in initial_balances { + app.sudo(SudoMsg::Bank(BankSudo::Mint { + to_address: address.clone(), + amount: vec![Coin { + denom: "ujuno".to_string(), + amount, + }], + })) + .unwrap(); + app.execute_contract( + Addr::unchecked(&address), + native_staking_addr.clone(), + &dao_voting_native_staked::msg::ExecuteMsg::Stake {}, + &[Coin { + amount, + denom: "ujuno".to_string(), + }], + ) + .unwrap(); + } + + app.update_block(next_block); + + core_addr +} pub fn instantiate_with_cw20_balances_governance( app: &mut App, diff --git a/contracts/proposal/dao-proposal-single/Cargo.toml b/contracts/proposal/dao-proposal-single/Cargo.toml index c7176c16b..fdf6e842d 100644 --- a/contracts/proposal/dao-proposal-single/Cargo.toml +++ b/contracts/proposal/dao-proposal-single/Cargo.toml @@ -57,4 +57,3 @@ cw721-base = { workspace = true } cw4 = { workspace = true } cw4-group = { workspace = true } cw-core-v1 = { workspace = true, features = ["library"] } -token-bindings-test = { workspace = true } diff --git a/contracts/proposal/dao-proposal-single/src/testing/contracts.rs b/contracts/proposal/dao-proposal-single/src/testing/contracts.rs index 3fed85b9a..f27bd7e35 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/contracts.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/contracts.rs @@ -80,14 +80,14 @@ pub(crate) fn cw20_staked_balances_voting_contract() -> Box> Box::new(contract) } -// pub(crate) fn native_staked_balances_voting_contract() -> Box> { -// let contract = ContractWrapper::new( -// dao_voting_native_staked::contract::execute, -// dao_voting_native_staked::contract::instantiate, -// dao_voting_native_staked::contract::query, -// ); -// Box::new(contract) -// } +pub(crate) fn native_staked_balances_voting_contract() -> Box> { + let contract = ContractWrapper::new( + dao_voting_native_staked::contract::execute, + dao_voting_native_staked::contract::instantiate, + dao_voting_native_staked::contract::query, + ); + Box::new(contract) +} pub(crate) fn cw721_stake_contract() -> Box> { let contract = ContractWrapper::new( diff --git a/contracts/proposal/dao-proposal-single/src/testing/do_votes.rs b/contracts/proposal/dao-proposal-single/src/testing/do_votes.rs index fe03e4c5c..ecd72e325 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/do_votes.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/do_votes.rs @@ -51,21 +51,21 @@ pub(crate) fn do_votes_nft_balances( ); } -// pub(crate) fn do_votes_native_staked_balances( -// votes: Vec, -// threshold: Threshold, -// expected_status: Status, -// total_supply: Option, -// ) { -// do_test_votes( -// votes, -// threshold, -// expected_status, -// total_supply, -// None, -// instantiate_with_native_staked_balances_governance, -// ); -// } +pub(crate) fn do_votes_native_staked_balances( + votes: Vec, + threshold: Threshold, + expected_status: Status, + total_supply: Option, +) { + do_test_votes( + votes, + threshold, + expected_status, + total_supply, + None, + instantiate_with_native_staked_balances_governance, + ); +} pub(crate) fn do_votes_cw4_weights( votes: Vec, @@ -279,40 +279,40 @@ fn test_vote_simple() { dao_testing::test_simple_votes(do_votes_cw4_weights); dao_testing::test_simple_votes(do_votes_staked_balances); dao_testing::test_simple_votes(do_votes_nft_balances); - // dao_testing::test_simple_votes(do_votes_native_staked_balances) + dao_testing::test_simple_votes(do_votes_native_staked_balances) } #[test] fn test_simple_vote_no_overflow() { dao_testing::test_simple_vote_no_overflow(do_votes_staked_balances); - // dao_testing::test_simple_vote_no_overflow(do_votes_native_staked_balances); + dao_testing::test_simple_vote_no_overflow(do_votes_native_staked_balances); } #[test] fn test_vote_no_overflow() { dao_testing::test_vote_no_overflow(do_votes_staked_balances); - // dao_testing::test_vote_no_overflow(do_votes_native_staked_balances); + dao_testing::test_vote_no_overflow(do_votes_native_staked_balances); } #[test] fn test_simple_early_rejection() { dao_testing::test_simple_early_rejection(do_votes_cw4_weights); dao_testing::test_simple_early_rejection(do_votes_staked_balances); - // dao_testing::test_simple_early_rejection(do_votes_native_staked_balances); + dao_testing::test_simple_early_rejection(do_votes_native_staked_balances); } #[test] fn test_vote_abstain_only() { dao_testing::test_vote_abstain_only(do_votes_cw4_weights); dao_testing::test_vote_abstain_only(do_votes_staked_balances); - // dao_testing::test_vote_abstain_only(do_votes_native_staked_balances); + dao_testing::test_vote_abstain_only(do_votes_native_staked_balances); } #[test] fn test_tricky_rounding() { dao_testing::test_tricky_rounding(do_votes_cw4_weights); dao_testing::test_tricky_rounding(do_votes_staked_balances); - // dao_testing::test_tricky_rounding(do_votes_native_staked_balances); + dao_testing::test_tricky_rounding(do_votes_native_staked_balances); } #[test] @@ -320,14 +320,14 @@ fn test_no_double_votes() { dao_testing::test_no_double_votes(do_votes_cw4_weights); dao_testing::test_no_double_votes(do_votes_staked_balances); dao_testing::test_no_double_votes(do_votes_nft_balances); - // dao_testing::test_no_double_votes(do_votes_native_staked_balances); + dao_testing::test_no_double_votes(do_votes_native_staked_balances); } #[test] fn test_votes_favor_yes() { dao_testing::test_votes_favor_yes(do_votes_staked_balances); dao_testing::test_votes_favor_yes(do_votes_nft_balances); - // dao_testing::test_votes_favor_yes(do_votes_native_staked_balances); + dao_testing::test_votes_favor_yes(do_votes_native_staked_balances); } #[test] @@ -335,7 +335,7 @@ fn test_votes_low_threshold() { dao_testing::test_votes_low_threshold(do_votes_cw4_weights); dao_testing::test_votes_low_threshold(do_votes_staked_balances); dao_testing::test_votes_low_threshold(do_votes_nft_balances); - // dao_testing::test_votes_low_threshold(do_votes_native_staked_balances); + dao_testing::test_votes_low_threshold(do_votes_native_staked_balances); } #[test] @@ -343,7 +343,7 @@ fn test_majority_vs_half() { dao_testing::test_majority_vs_half(do_votes_cw4_weights); dao_testing::test_majority_vs_half(do_votes_staked_balances); dao_testing::test_majority_vs_half(do_votes_nft_balances); - // dao_testing::test_majority_vs_half(do_votes_native_staked_balances); + dao_testing::test_majority_vs_half(do_votes_native_staked_balances); } #[test] @@ -351,7 +351,7 @@ fn test_pass_threshold_not_quorum() { dao_testing::test_pass_threshold_not_quorum(do_votes_cw4_weights); dao_testing::test_pass_threshold_not_quorum(do_votes_staked_balances); dao_testing::test_pass_threshold_not_quorum(do_votes_nft_balances); - // dao_testing::test_pass_threshold_not_quorum(do_votes_native_staked_balances); + dao_testing::test_pass_threshold_not_quorum(do_votes_native_staked_balances); } #[test] @@ -359,7 +359,7 @@ fn test_pass_threshold_exactly_quorum() { dao_testing::test_pass_exactly_quorum(do_votes_cw4_weights); dao_testing::test_pass_exactly_quorum(do_votes_staked_balances); dao_testing::test_pass_exactly_quorum(do_votes_nft_balances); - // dao_testing::test_pass_exactly_quorum(do_votes_native_staked_balances); + dao_testing::test_pass_exactly_quorum(do_votes_native_staked_balances); } /// Generate some random voting selections and make sure they behave @@ -375,7 +375,7 @@ fn fuzz_voting_staked_balances() { dao_testing::fuzz_voting(do_votes_staked_balances) } -// #[test] -// fn fuzz_voting_native_staked_balances() { -// dao_testing::fuzz_voting(do_votes_native_staked_balances) -// } +#[test] +fn fuzz_voting_native_staked_balances() { + dao_testing::fuzz_voting(do_votes_native_staked_balances) +} diff --git a/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs b/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs index 2f0e1d02c..eb1d18e16 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs @@ -19,7 +19,7 @@ use super::{ contracts::{ cw20_base_contract, cw20_stake_contract, cw20_staked_balances_voting_contract, cw4_group_contract, cw4_voting_contract, cw721_base_contract, cw721_stake_contract, - cw_core_contract, proposal_single_contract, + cw_core_contract, native_staked_balances_voting_contract, proposal_single_contract, }, CREATOR_ADDR, }; @@ -220,116 +220,114 @@ pub(crate) fn instantiate_with_staked_cw721_governance( core_addr } -// pub(crate) fn instantiate_with_native_staked_balances_governance( -// app: &mut App, -// proposal_module_instantiate: InstantiateMsg, -// initial_balances: Option>, -// ) -> Addr { -// let proposal_module_code_id = app.store_code(proposal_single_contract()); - -// let initial_balances = initial_balances.unwrap_or_else(|| { -// vec![Cw20Coin { -// address: CREATOR_ADDR.to_string(), -// amount: Uint128::new(100_000_000), -// }] -// }); - -// // Collapse balances so that we can test double votes. -// let initial_balances: Vec = { -// let mut already_seen = vec![]; -// initial_balances -// .into_iter() -// .filter(|Cw20Coin { address, amount: _ }| { -// if already_seen.contains(address) { -// false -// } else { -// already_seen.push(address.clone()); -// true -// } -// }) -// .collect() -// }; - -// let native_stake_id = app.store_code(native_staked_balances_voting_contract()); -// let core_contract_id = app.store_code(cw_core_contract()); - -// let instantiate_core = dao_interface::msg::InstantiateMsg { -// admin: None, -// name: "DAO DAO".to_string(), -// description: "A DAO that builds DAOs".to_string(), -// dao_uri: None, -// image_url: None, -// automatically_add_cw20s: true, -// automatically_add_cw721s: false, -// voting_module_instantiate_info: ModuleInstantiateInfo { -// code_id: native_stake_id, -// msg: to_binary(&dao_voting_native_staked::msg::InstantiateMsg { -// owner: Some(Admin::CoreModule {}), -// manager: None, -// token_info: dao_voting_native_staked::msg::TokenInfo::Existing { -// denom: "ujuno".to_string(), -// }, -// unstaking_duration: None, -// active_threshold: None, -// }) -// .unwrap(), -// admin: None, -// label: "DAO DAO voting module".to_string(), -// }, -// proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { -// code_id: proposal_module_code_id, -// label: "DAO DAO governance module.".to_string(), -// admin: Some(Admin::CoreModule {}), -// msg: to_binary(&proposal_module_instantiate).unwrap(), -// }], -// initial_items: None, -// }; - -// let core_addr = app -// .instantiate_contract( -// core_contract_id, -// Addr::unchecked(CREATOR_ADDR), -// &instantiate_core, -// &[], -// "DAO DAO", -// None, -// ) -// .unwrap(); - -// let gov_state: dao_interface::query::DumpStateResponse = app -// .wrap() -// .query_wasm_smart( -// core_addr.clone(), -// &dao_interface::msg::QueryMsg::DumpState {}, -// ) -// .unwrap(); -// let native_staking_addr = gov_state.voting_module; - -// for Cw20Coin { address, amount } in initial_balances { -// app.sudo(SudoMsg::Bank(BankSudo::Mint { -// to_address: address.clone(), -// amount: vec![Coin { -// denom: "ujuno".to_string(), -// amount, -// }], -// })) -// .unwrap(); -// app.execute_contract( -// Addr::unchecked(&address), -// native_staking_addr.clone(), -// &dao_voting_native_staked::msg::ExecuteMsg::Stake {}, -// &[Coin { -// amount, -// denom: "ujuno".to_string(), -// }], -// ) -// .unwrap(); -// } - -// app.update_block(next_block); - -// core_addr -// } +pub(crate) fn instantiate_with_native_staked_balances_governance( + app: &mut App, + proposal_module_instantiate: InstantiateMsg, + initial_balances: Option>, +) -> Addr { + let proposal_module_code_id = app.store_code(proposal_single_contract()); + + let initial_balances = initial_balances.unwrap_or_else(|| { + vec![Cw20Coin { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(100_000_000), + }] + }); + + // Collapse balances so that we can test double votes. + let initial_balances: Vec = { + let mut already_seen = vec![]; + initial_balances + .into_iter() + .filter(|Cw20Coin { address, amount: _ }| { + if already_seen.contains(address) { + false + } else { + already_seen.push(address.clone()); + true + } + }) + .collect() + }; + + let native_stake_id = app.store_code(native_staked_balances_voting_contract()); + let core_contract_id = app.store_code(cw_core_contract()); + + let instantiate_core = dao_interface::msg::InstantiateMsg { + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs".to_string(), + dao_uri: None, + image_url: None, + automatically_add_cw20s: true, + automatically_add_cw721s: false, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: native_stake_id, + msg: to_binary(&dao_voting_native_staked::msg::InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: None, + denom: "ujuno".to_string(), + unstaking_duration: None, + // active_threshold: None, + }) + .unwrap(), + admin: None, + label: "DAO DAO voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_module_code_id, + label: "DAO DAO governance module.".to_string(), + admin: Some(Admin::CoreModule {}), + msg: to_binary(&proposal_module_instantiate).unwrap(), + }], + initial_items: None, + }; + + let core_addr = app + .instantiate_contract( + core_contract_id, + Addr::unchecked(CREATOR_ADDR), + &instantiate_core, + &[], + "DAO DAO", + None, + ) + .unwrap(); + + let gov_state: dao_interface::query::DumpStateResponse = app + .wrap() + .query_wasm_smart( + core_addr.clone(), + &dao_interface::msg::QueryMsg::DumpState {}, + ) + .unwrap(); + let native_staking_addr = gov_state.voting_module; + + for Cw20Coin { address, amount } in initial_balances { + app.sudo(SudoMsg::Bank(BankSudo::Mint { + to_address: address.clone(), + amount: vec![Coin { + denom: "ujuno".to_string(), + amount, + }], + })) + .unwrap(); + app.execute_contract( + Addr::unchecked(&address), + native_staking_addr.clone(), + &dao_voting_native_staked::msg::ExecuteMsg::Stake {}, + &[Coin { + amount, + denom: "ujuno".to_string(), + }], + ) + .unwrap(); + } + + app.update_block(next_block); + + core_addr +} pub(crate) fn instantiate_with_staked_balances_governance( app: &mut App, diff --git a/contracts/voting/dao-voting-native-staked/Cargo.toml b/contracts/voting/dao-voting-native-staked/Cargo.toml index a87872608..a73bada2a 100644 --- a/contracts/voting/dao-voting-native-staked/Cargo.toml +++ b/contracts/voting/dao-voting-native-staked/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "dao-voting-native-staked" authors = ["Callum Anderson "] -description = "A DAO DAO voting module based on staked cw721 tokens." +description = "A DAO DAO voting module based on staked native tokens. If your chain uses Token Factory, consider using dao-voting-token-factory-staked for additional functionality including creating new tokens." edition = { workspace = true } license = { workspace = true } repository = { workspace = true } @@ -17,23 +17,19 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -cosmwasm-std = { workspace = true, features = ["cosmwasm_1_1"] } +cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } cosmwasm-storage = { workspace = true } cw-storage-plus = { workspace = true } cw2 = { workspace = true } cw-utils = { workspace = true } cw-controllers = { workspace = true } -cw-hooks = { workspace = true } + thiserror = { workspace = true } dao-dao-macros = { workspace = true } dao-interface = { workspace = true } -dao-voting = { workspace = true } cw-paginate-storage = { workspace = true } -token-bindings = { workspace = true } [dev-dependencies] -anyhow = { workspace = true } cw-multi-test = { workspace = true } -osmosis-test-tube = { workspace = true } -token-bindings-test = { workspace = true } +anyhow = { workspace = true } diff --git a/contracts/voting/dao-voting-native-staked/README.md b/contracts/voting/dao-voting-native-staked/README.md index eacb5d9e8..a9084cc3f 100644 --- a/contracts/voting/dao-voting-native-staked/README.md +++ b/contracts/voting/dao-voting-native-staked/README.md @@ -7,3 +7,4 @@ arbitrary height. This contract implements the interface needed to be a DAO DAO [voting module](https://github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-Contracts-Design#the-voting-module). +If your chain uses Token Factory, consider using `dao-voting-token-factory-staked` for additional functionality including creating new tokens. diff --git a/contracts/voting/dao-voting-native-staked/schema/dao-voting-native-staked.json b/contracts/voting/dao-voting-native-staked/schema/dao-voting-native-staked.json index b40013ef0..f553cbbab 100644 --- a/contracts/voting/dao-voting-native-staked/schema/dao-voting-native-staked.json +++ b/contracts/voting/dao-voting-native-staked/schema/dao-voting-native-staked.json @@ -7,19 +7,11 @@ "title": "InstantiateMsg", "type": "object", "required": [ - "token_info" + "denom" ], "properties": { - "active_threshold": { - "description": "The number or percentage of tokens that must be staked for the DAO to be active", - "anyOf": [ - { - "$ref": "#/definitions/ActiveThreshold" - }, - { - "type": "null" - } - ] + "denom": { + "type": "string" }, "manager": { "type": [ @@ -37,9 +29,6 @@ } ] }, - "token_info": { - "$ref": "#/definitions/TokenInfo" - }, "unstaking_duration": { "anyOf": [ { @@ -53,55 +42,6 @@ }, "additionalProperties": false, "definitions": { - "ActiveThreshold": { - "description": "The threshold of tokens that must be staked in order for this voting module to be active. If this is not reached, this module will response to `is_active` queries with false and proposal modules which respect active thresholds will not allow the creation of proposals.", - "oneOf": [ - { - "description": "The absolute number of tokens that must be staked for the module to be active.", - "type": "object", - "required": [ - "absolute_count" - ], - "properties": { - "absolute_count": { - "type": "object", - "required": [ - "count" - ], - "properties": { - "count": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "The percentage of tokens that must be staked for the module to be active. Computed as `staked / total_supply`.", - "type": "object", - "required": [ - "percentage" - ], - "properties": { - "percentage": { - "type": "object", - "required": [ - "percent" - ], - "properties": { - "percent": { - "$ref": "#/definitions/Decimal" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, "Admin": { "description": "Information about the CosmWasm level admin of a contract. Used in conjunction with `ModuleInstantiateInfo` to instantiate modules.", "oneOf": [ @@ -143,39 +83,6 @@ } ] }, - "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 - }, "Duration": { "description": "Duration is a delta of time. You can add it to a BlockInfo or Expiration to move that further in the future. Note that an height-based Duration and a time-based Expiration cannot be combined", "oneOf": [ @@ -209,154 +116,6 @@ "additionalProperties": false } ] - }, - "InitialBalance": { - "type": "object", - "required": [ - "amount", - "mint_to_address" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "mint_to_address": { - "type": "string" - } - }, - "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 - }, - "NewTokenInfo": { - "type": "object", - "required": [ - "initial_balances", - "subdenom" - ], - "properties": { - "initial_balances": { - "type": "array", - "items": { - "$ref": "#/definitions/InitialBalance" - } - }, - "initial_dao_balance": { - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "metadata": { - "anyOf": [ - { - "$ref": "#/definitions/Metadata" - }, - { - "type": "null" - } - ] - }, - "subdenom": { - "type": "string" - } - }, - "additionalProperties": false - }, - "TokenInfo": { - "oneOf": [ - { - "type": "object", - "required": [ - "existing" - ], - "properties": { - "existing": { - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "description": "Token denom e.g. ujuno, or some ibc denom.", - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "new" - ], - "properties": { - "new": { - "$ref": "#/definitions/NewTokenInfo" - } - }, - "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" } } }, @@ -447,130 +206,9 @@ } }, "additionalProperties": false - }, - { - "description": "Sets the active threshold to a new value. Only the instantiator of this contract (a DAO most likely) may call this method.", - "type": "object", - "required": [ - "update_active_threshold" - ], - "properties": { - "update_active_threshold": { - "type": "object", - "properties": { - "new_threshold": { - "anyOf": [ - { - "$ref": "#/definitions/ActiveThreshold" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "add_hook" - ], - "properties": { - "add_hook": { - "type": "object", - "required": [ - "addr" - ], - "properties": { - "addr": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "remove_hook" - ], - "properties": { - "remove_hook": { - "type": "object", - "required": [ - "addr" - ], - "properties": { - "addr": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false } ], "definitions": { - "ActiveThreshold": { - "description": "The threshold of tokens that must be staked in order for this voting module to be active. If this is not reached, this module will response to `is_active` queries with false and proposal modules which respect active thresholds will not allow the creation of proposals.", - "oneOf": [ - { - "description": "The absolute number of tokens that must be staked for the module to be active.", - "type": "object", - "required": [ - "absolute_count" - ], - "properties": { - "absolute_count": { - "type": "object", - "required": [ - "count" - ], - "properties": { - "count": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "The percentage of tokens that must be staked for the module to be active. Computed as `staked / total_supply`.", - "type": "object", - "required": [ - "percentage" - ], - "properties": { - "percentage": { - "type": "object", - "required": [ - "percent" - ], - "properties": { - "percent": { - "$ref": "#/definitions/Decimal" - } - }, - "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" - }, "Duration": { "description": "Duration is a delta of time. You can add it to a BlockInfo or Expiration to move that further in the future. Note that an height-based Duration and a time-based Expiration cannot be combined", "oneOf": [ @@ -628,19 +266,6 @@ }, "additionalProperties": false }, - { - "type": "object", - "required": [ - "get_denom" - ], - "properties": { - "get_denom": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "type": "object", "required": [ @@ -691,32 +316,6 @@ }, "additionalProperties": false }, - { - "type": "object", - "required": [ - "active_threshold" - ], - "properties": { - "active_threshold": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "get_hooks" - ], - "properties": { - "get_hooks": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "Returns the voting power for an address at a given height.", "type": "object", @@ -798,19 +397,6 @@ } }, "additionalProperties": false - }, - { - "type": "object", - "required": [ - "is_active" - ], - "properties": { - "is_active": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false } ] }, @@ -822,83 +408,6 @@ }, "sudo": null, "responses": { - "active_threshold": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ActiveThresholdResponse", - "type": "object", - "properties": { - "active_threshold": { - "anyOf": [ - { - "$ref": "#/definitions/ActiveThreshold" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "ActiveThreshold": { - "description": "The threshold of tokens that must be staked in order for this voting module to be active. If this is not reached, this module will response to `is_active` queries with false and proposal modules which respect active thresholds will not allow the creation of proposals.", - "oneOf": [ - { - "description": "The absolute number of tokens that must be staked for the module to be active.", - "type": "object", - "required": [ - "absolute_count" - ], - "properties": { - "absolute_count": { - "type": "object", - "required": [ - "count" - ], - "properties": { - "count": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "The percentage of tokens that must be staked for the module to be active. Computed as `staked / total_supply`.", - "type": "object", - "required": [ - "percentage" - ], - "properties": { - "percentage": { - "type": "object", - "required": [ - "percent" - ], - "properties": { - "percent": { - "$ref": "#/definitions/Decimal" - } - }, - "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" - }, - "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" - } - } - }, "claims": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ClaimsResponse", @@ -1007,7 +516,13 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "Config", "type": "object", + "required": [ + "denom" + ], "properties": { + "denom": { + "type": "string" + }, "manager": { "anyOf": [ { @@ -1081,37 +596,6 @@ } } }, - "get_denom": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "DenomResponse", - "type": "object", - "required": [ - "denom" - ], - "properties": { - "denom": { - "type": "string" - } - }, - "additionalProperties": false - }, - "get_hooks": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "GetHooksResponse", - "type": "object", - "required": [ - "hooks" - ], - "properties": { - "hooks": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false - }, "info": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "InfoResponse", @@ -1146,11 +630,6 @@ } } }, - "is_active": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Boolean", - "type": "boolean" - }, "list_stakers": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ListStakersResponse", diff --git a/contracts/voting/dao-voting-native-staked/src/contract.rs b/contracts/voting/dao-voting-native-staked/src/contract.rs index 6d7040f33..eb93fa0fe 100644 --- a/contracts/voting/dao-voting-native-staked/src/contract.rs +++ b/contracts/voting/dao-voting-native-staked/src/contract.rs @@ -1,45 +1,24 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; - use cosmwasm_std::{ - coins, to_binary, BankMsg, BankQuery, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, - MessageInfo, Order, Reply, Response, StdResult, SubMsg, Uint128, Uint256, + coins, to_binary, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, + StdResult, Uint128, }; use cw2::set_contract_version; use cw_controllers::ClaimsResponse; -use cw_storage_plus::Bound; -use cw_utils::{maybe_addr, must_pay, Duration}; +use cw_utils::{must_pay, Duration}; use dao_interface::state::Admin; -use dao_interface::voting::{ - IsActiveResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse, -}; -use dao_voting::threshold::ActiveThreshold; -use token_bindings::{TokenFactoryMsg, TokenFactoryQuery, TokenMsg, TokenQuerier}; +use dao_interface::voting::{TotalPowerAtHeightResponse, VotingPowerAtHeightResponse}; use crate::error::ContractError; -use crate::hooks::{stake_hook_msgs, unstake_hook_msgs}; use crate::msg::{ - ActiveThresholdResponse, DenomResponse, ExecuteMsg, GetHooksResponse, InstantiateMsg, - ListStakersResponse, MigrateMsg, QueryMsg, StakerBalanceResponse, TokenInfo, -}; -use crate::state::{ - Config, ACTIVE_THRESHOLD, CLAIMS, CONFIG, DAO, DENOM, HOOKS, MAX_CLAIMS, STAKED_BALANCES, - STAKED_TOTAL, TOKEN_INSTANTIATION_INFO, + ExecuteMsg, InstantiateMsg, ListStakersResponse, MigrateMsg, QueryMsg, StakerBalanceResponse, }; +use crate::state::{Config, CLAIMS, CONFIG, DAO, MAX_CLAIMS, STAKED_BALANCES, STAKED_TOTAL}; pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-voting-native-staked"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); -// Settings for query pagination -const MAX_LIMIT: u32 = 30; -const DEFAULT_LIMIT: u32 = 10; - -const CREATE_DENOM_REPLY_ID: u64 = 0; - -// We multiply by this when calculating needed power for being active -// when using active threshold with percent -const PRECISION_FACTOR: u128 = 10u128.pow(9); - fn validate_duration(duration: Option) -> Result<(), ContractError> { if let Some(unstaking_duration) = duration { match unstaking_duration { @@ -60,11 +39,11 @@ fn validate_duration(duration: Option) -> Result<(), ContractError> { #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( - deps: DepsMut, + deps: DepsMut, _env: Env, info: MessageInfo, msg: InstantiateMsg, -) -> Result, ContractError> { +) -> Result { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; let owner = msg @@ -85,96 +64,38 @@ pub fn instantiate( let config = Config { owner, manager, + denom: msg.denom, unstaking_duration: msg.unstaking_duration, }; CONFIG.save(deps.storage, &config)?; DAO.save(deps.storage, &info.sender)?; - if let Some(active_threshold) = msg.active_threshold.as_ref() { - if let ActiveThreshold::Percentage { percent } = active_threshold { - if *percent > Decimal::percent(100) || *percent <= Decimal::percent(0) { - return Err(ContractError::InvalidActivePercentage {}); - } - } - ACTIVE_THRESHOLD.save(deps.storage, active_threshold)?; - } - - match msg.token_info { - TokenInfo::Existing { denom } => { - if let Some(ActiveThreshold::AbsoluteCount { count }) = msg.active_threshold { - assert_valid_absolute_count_threshold(deps.as_ref(), &denom, count)?; - } - - DENOM.save(deps.storage, &denom)?; - - Ok(Response::::new() - .add_attribute("action", "instantiate") - .add_attribute("token", "existing_token") - .add_attribute("token_denom", denom) - .add_attribute( - "owner", - config - .owner - .map(|a| a.to_string()) - .unwrap_or_else(|| "None".to_string()), - ) - .add_attribute( - "manager", - config - .manager - .map(|a| a.to_string()) - .unwrap_or_else(|| "None".to_string()), - )) - } - TokenInfo::New(token) => { - // TODO investigate how much validation we need to do - if token.subdenom.eq("") { - // TODO replace with token factory errors - return Err(ContractError::NothingToClaim {}); - } - - // Create Token Factory denom SubMsg - let create_denom_msg = SubMsg::reply_on_success( - TokenMsg::CreateDenom { - subdenom: token.clone().subdenom, - metadata: token.clone().metadata, - }, - CREATE_DENOM_REPLY_ID, - ); - - // Save new token info for use in reply - TOKEN_INSTANTIATION_INFO.save(deps.storage, &token)?; - - Ok(Response::::new() - .add_attribute("method", "create_denom") - .add_submessage(create_denom_msg)) - } - } -} - -pub fn assert_valid_absolute_count_threshold( - deps: Deps, - token_denom: &str, - count: Uint128, -) -> Result<(), ContractError> { - if count.is_zero() { - return Err(ContractError::ZeroActiveCount {}); - } - let supply: Coin = deps.querier.query_supply(token_denom.to_string())?; - if count > supply.amount { - return Err(ContractError::InvalidAbsoluteCount {}); - } - Ok(()) + Ok(Response::new() + .add_attribute("action", "instantiate") + .add_attribute( + "owner", + config + .owner + .map(|a| a.to_string()) + .unwrap_or_else(|| "None".to_string()), + ) + .add_attribute( + "manager", + config + .manager + .map(|a| a.to_string()) + .unwrap_or_else(|| "None".to_string()), + )) } #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( - deps: DepsMut, + deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, -) -> Result, ContractError> { +) -> Result { match msg { ExecuteMsg::Stake {} => execute_stake(deps, env, info), ExecuteMsg::Unstake { amount } => execute_unstake(deps, env, info, amount), @@ -184,21 +105,16 @@ pub fn execute( duration, } => execute_update_config(deps, info, owner, manager, duration), ExecuteMsg::Claim {} => execute_claim(deps, env, info), - ExecuteMsg::UpdateActiveThreshold { new_threshold } => { - execute_update_active_threshold(deps, env, info, new_threshold) - } - ExecuteMsg::AddHook { addr } => execute_add_hook(deps, env, info, addr), - ExecuteMsg::RemoveHook { addr } => execute_remove_hook(deps, env, info, addr), } } pub fn execute_stake( - deps: DepsMut, + deps: DepsMut, env: Env, info: MessageInfo, -) -> Result, ContractError> { - let denom = DENOM.load(deps.storage)?; - let amount = must_pay(&info, &denom)?; +) -> Result { + let config = CONFIG.load(deps.storage)?; + let amount = must_pay(&info, &config.denom)?; STAKED_BALANCES.update( deps.storage, @@ -211,21 +127,19 @@ pub fn execute_stake( env.block.height, |total| -> StdResult { Ok(total.unwrap_or_default().checked_add(amount)?) }, )?; - let hook_msgs = stake_hook_msgs(deps.storage, info.sender.clone(), amount)?; - Ok(Response::::new() - .add_submessages(hook_msgs) + Ok(Response::new() .add_attribute("action", "stake") .add_attribute("amount", amount.to_string()) .add_attribute("from", info.sender)) } pub fn execute_unstake( - deps: DepsMut, + deps: DepsMut, env: Env, info: MessageInfo, amount: Uint128, -) -> Result, ContractError> { +) -> Result { if amount.is_zero() { return Err(ContractError::ZeroUnstake {}); } @@ -251,19 +165,16 @@ pub fn execute_unstake( .map_err(|_e| ContractError::InvalidUnstakeAmount {}) }, )?; - let hook_msgs = unstake_hook_msgs(deps.storage, info.sender.clone(), amount)?; let config = CONFIG.load(deps.storage)?; - let denom = DENOM.load(deps.storage)?; match config.unstaking_duration { None => { let msg = CosmosMsg::Bank(BankMsg::Send { to_address: info.sender.to_string(), - amount: coins(amount.u128(), denom), + amount: coins(amount.u128(), config.denom), }); - Ok(Response::::new() + Ok(Response::new() .add_message(msg) - .add_submessages(hook_msgs) .add_attribute("action", "unstake") .add_attribute("from", info.sender) .add_attribute("amount", amount) @@ -281,8 +192,7 @@ pub fn execute_unstake( amount, duration.after(&env.block), )?; - Ok(Response::::new() - .add_submessages(hook_msgs) + Ok(Response::new() .add_attribute("action", "unstake") .add_attribute("from", info.sender) .add_attribute("amount", amount) @@ -292,12 +202,12 @@ pub fn execute_unstake( } pub fn execute_update_config( - deps: DepsMut, + deps: DepsMut, info: MessageInfo, new_owner: Option, new_manager: Option, duration: Option, -) -> Result, ContractError> { +) -> Result { let mut config: Config = CONFIG.load(deps.storage)?; if Some(info.sender.clone()) != config.owner && Some(info.sender.clone()) != config.manager { return Err(ContractError::Unauthorized {}); @@ -322,7 +232,7 @@ pub fn execute_update_config( config.unstaking_duration = duration; CONFIG.save(deps.storage, &config)?; - Ok(Response::::new() + Ok(Response::new() .add_attribute("action", "update_config") .add_attribute( "owner", @@ -341,97 +251,30 @@ pub fn execute_update_config( } pub fn execute_claim( - deps: DepsMut, + deps: DepsMut, env: Env, info: MessageInfo, -) -> Result, ContractError> { +) -> Result { let release = CLAIMS.claim_tokens(deps.storage, &info.sender, &env.block, None)?; if release.is_zero() { return Err(ContractError::NothingToClaim {}); } - let denom = DENOM.load(deps.storage)?; - let msg = CosmosMsg::::Bank(BankMsg::Send { + let config = CONFIG.load(deps.storage)?; + let msg = CosmosMsg::Bank(BankMsg::Send { to_address: info.sender.to_string(), - amount: coins(release.u128(), denom), + amount: coins(release.u128(), config.denom), }); - Ok(Response::::new() + Ok(Response::new() .add_message(msg) .add_attribute("action", "claim") .add_attribute("from", info.sender) .add_attribute("amount", release)) } -pub fn execute_update_active_threshold( - deps: DepsMut, - _env: Env, - info: MessageInfo, - new_active_threshold: Option, -) -> Result, ContractError> { - let dao = DAO.load(deps.storage)?; - if info.sender != dao { - return Err(ContractError::Unauthorized {}); - } - - if let Some(active_threshold) = new_active_threshold { - match active_threshold { - ActiveThreshold::Percentage { percent } => { - if percent > Decimal::percent(100) || percent.is_zero() { - return Err(ContractError::InvalidActivePercentage {}); - } - } - ActiveThreshold::AbsoluteCount { count } => { - let denom = DENOM.load(deps.storage)?; - assert_valid_absolute_count_threshold(deps.as_ref(), &denom, count)?; - } - } - ACTIVE_THRESHOLD.save(deps.storage, &active_threshold)?; - } else { - ACTIVE_THRESHOLD.remove(deps.storage); - } - - Ok(Response::::new().add_attribute("action", "update_active_threshold")) -} - -pub fn execute_add_hook( - deps: DepsMut, - _env: Env, - info: MessageInfo, - addr: String, -) -> Result, ContractError> { - let config: Config = CONFIG.load(deps.storage)?; - if Some(info.sender.clone()) != config.owner && Some(info.sender) != config.manager { - return Err(ContractError::Unauthorized {}); - } - - let hook = deps.api.addr_validate(&addr)?; - HOOKS.add_hook(deps.storage, hook)?; - Ok(Response::::new() - .add_attribute("action", "add_hook") - .add_attribute("hook", addr)) -} - -pub fn execute_remove_hook( - deps: DepsMut, - _env: Env, - info: MessageInfo, - addr: String, -) -> Result, ContractError> { - let config: Config = CONFIG.load(deps.storage)?; - if Some(info.sender.clone()) != config.owner && Some(info.sender) != config.manager { - return Err(ContractError::Unauthorized {}); - } - - let hook = deps.api.addr_validate(&addr)?; - HOOKS.remove_hook(deps.storage, hook)?; - Ok(Response::::new() - .add_attribute("action", "remove_hook") - .add_attribute("hook", addr)) -} - #[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 { match msg { QueryMsg::VotingPowerAtHeight { address, height } => { to_binary(&query_voting_power_at_height(deps, env, address, height)?) @@ -443,20 +286,14 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResul QueryMsg::Dao {} => query_dao(deps), QueryMsg::Claims { address } => to_binary(&query_claims(deps, address)?), QueryMsg::GetConfig {} => to_binary(&CONFIG.load(deps.storage)?), - QueryMsg::GetDenom {} => to_binary(&DenomResponse { - denom: DENOM.load(deps.storage)?, - }), QueryMsg::ListStakers { start_after, limit } => { query_list_stakers(deps, start_after, limit) } - QueryMsg::IsActive {} => query_is_active(deps), - QueryMsg::ActiveThreshold {} => query_active_threshold(deps), - QueryMsg::GetHooks {} => to_binary(&query_hooks(deps)?), } } pub fn query_voting_power_at_height( - deps: Deps, + deps: Deps, env: Env, address: String, height: Option, @@ -470,7 +307,7 @@ pub fn query_voting_power_at_height( } pub fn query_total_power_at_height( - deps: Deps, + deps: Deps, env: Env, height: Option, ) -> StdResult { @@ -481,195 +318,51 @@ pub fn query_total_power_at_height( Ok(TotalPowerAtHeightResponse { power, height }) } -pub fn query_info(deps: Deps) -> StdResult { +pub fn query_info(deps: Deps) -> StdResult { let info = cw2::get_contract_version(deps.storage)?; to_binary(&dao_interface::voting::InfoResponse { info }) } -pub fn query_dao(deps: Deps) -> StdResult { +pub fn query_dao(deps: Deps) -> StdResult { let dao = DAO.load(deps.storage)?; to_binary(&dao) } -pub fn query_claims(deps: Deps, address: String) -> StdResult { +pub fn query_claims(deps: Deps, address: String) -> StdResult { CLAIMS.query_claims(deps, &deps.api.addr_validate(&address)?) } pub fn query_list_stakers( - deps: Deps, + deps: Deps, start_after: Option, limit: Option, ) -> StdResult { - let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - let addr = maybe_addr(deps.api, start_after)?; - let start = addr.as_ref().map(Bound::exclusive); - - let stakers = STAKED_BALANCES - .range(deps.storage, start, None, Order::Ascending) - .take(limit) - .map(|item| { - item.map(|(address, balance)| StakerBalanceResponse { - address: address.into_string(), - balance, - }) - }) - .collect::>()?; - - to_binary(&ListStakersResponse { stakers }) -} + let start_at = start_after + .map(|addr| deps.api.addr_validate(&addr)) + .transpose()?; -pub fn query_is_active(deps: Deps) -> StdResult { - let threshold = ACTIVE_THRESHOLD.may_load(deps.storage)?; - if let Some(threshold) = threshold { - let denom = DENOM.load(deps.storage)?; - let actual_power = STAKED_TOTAL.may_load(deps.storage)?.unwrap_or_default(); - match threshold { - ActiveThreshold::AbsoluteCount { count } => to_binary(&IsActiveResponse { - active: actual_power >= count, - }), - ActiveThreshold::Percentage { percent } => { - // percent is bounded between [0, 100]. decimal - // represents percents in u128 terms as p * - // 10^15. this bounds percent between [0, 10^17]. - // - // total_potential_power is bounded between [0, 2^128] - // as it tracks the balances of a cw20 token which has - // a max supply of 2^128. - // - // with our precision factor being 10^9: - // - // total_power <= 2^128 * 10^9 <= 2^256 - // - // so we're good to put that in a u256. - // - // multiply_ratio promotes to a u512 under the hood, - // so it won't overflow, multiplying by a percent less - // than 100 is gonna make something the same size or - // smaller, applied + 10^9 <= 2^128 * 10^9 + 10^9 <= - // 2^256, so the top of the round won't overflow, and - // rounding is rounding down, so the whole thing can - // be safely unwrapped at the end of the day thank you - // for coming to my ted talk. - let total_potential_power: cosmwasm_std::SupplyResponse = - deps.querier - .query(&cosmwasm_std::QueryRequest::Bank(BankQuery::Supply { - denom, - }))?; - let total_power = total_potential_power - .amount - .amount - .full_mul(PRECISION_FACTOR); - // under the hood decimals are `atomics / 10^decimal_places`. - // cosmwasm doesn't give us a Decimal * Uint256 - // implementation so we take the decimal apart and - // multiply by the fraction. - let applied = total_power.multiply_ratio( - percent.atomics(), - Uint256::from(10u64).pow(percent.decimal_places()), - ); - let rounded = (applied + Uint256::from(PRECISION_FACTOR) - Uint256::from(1u128)) - / Uint256::from(PRECISION_FACTOR); - let count: Uint128 = rounded.try_into().unwrap(); - to_binary(&IsActiveResponse { - active: actual_power >= count, - }) - } - } - } else { - to_binary(&IsActiveResponse { active: true }) - } -} + let stakers = cw_paginate_storage::paginate_snapshot_map( + deps, + &STAKED_BALANCES, + start_at.as_ref(), + limit, + cosmwasm_std::Order::Ascending, + )?; -pub fn query_active_threshold(deps: Deps) -> StdResult { - to_binary(&ActiveThresholdResponse { - active_threshold: ACTIVE_THRESHOLD.may_load(deps.storage)?, - }) -} + let stakers = stakers + .into_iter() + .map(|(address, balance)| StakerBalanceResponse { + address: address.into_string(), + balance, + }) + .collect(); -pub fn query_hooks(deps: Deps) -> StdResult { - Ok(GetHooksResponse { - hooks: HOOKS.query_hooks(deps)?.hooks, - }) + to_binary(&ListStakersResponse { stakers }) } #[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()) -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn reply( - deps: DepsMut, - env: Env, - msg: Reply, -) -> Result, ContractError> { - match msg.id { - CREATE_DENOM_REPLY_ID => { - // Load info for new token and the DAO's address - let token = TOKEN_INSTANTIATION_INFO.load(deps.storage)?; - let dao = DAO.load(deps.storage)?; - - // Get the new token factory denom - let querier = TokenQuerier::new(&deps.querier); - let denom = querier - .full_denom(env.contract.address.to_string(), token.subdenom)? - .denom; - DENOM.save(deps.storage, &denom)?; - - let mut mint_msgs: Vec = vec![]; - - // Check supply is greater than zero - let initial_supply = token - .initial_balances - .iter() - .fold(Uint128::zero(), |p, n| p + n.amount); - - // Cannot instantiate with no initial token owners because it would - // immediately lock the DAO. - if initial_supply.is_zero() { - return Err(ContractError::InitialBalancesError {}); - } - - // Mint initial balances - token.initial_balances.iter().for_each(|b| { - mint_msgs.push(TokenMsg::MintTokens { - denom: denom.clone(), - amount: b.amount, - mint_to_address: b.mint_to_address.clone(), - }) - }); - - // Add initial DAO balance to initial_balances if nonzero. - if let Some(initial_dao_balance) = token.initial_dao_balance { - if !initial_dao_balance.is_zero() { - mint_msgs.push(TokenMsg::MintTokens { - denom: denom.clone(), - amount: initial_dao_balance, - mint_to_address: dao.to_string(), - }) - } - } - - // Clear up unneeded storage. - TOKEN_INSTANTIATION_INFO.remove(deps.storage); - - // Update token factory denom admin to be the DAO - let update_token_admin_msg = TokenMsg::ChangeAdmin { - denom, - new_admin_address: dao.to_string(), - }; - - // TODO what other info do we want here? - Ok(Response::new() - .add_messages(mint_msgs) - .add_message(update_token_admin_msg)) - } - _ => Err(ContractError::UnknownReplyId { id: msg.id }), - } + Ok(Response::default()) } diff --git a/contracts/voting/dao-voting-native-staked/src/error.rs b/contracts/voting/dao-voting-native-staked/src/error.rs index 148c1ac26..dc8cf9f83 100644 --- a/contracts/voting/dao-voting-native-staked/src/error.rs +++ b/contracts/voting/dao-voting-native-staked/src/error.rs @@ -1,30 +1,15 @@ use cosmwasm_std::StdError; -use cw_utils::{ParseReplyError, PaymentError}; +use cw_utils::PaymentError; use thiserror::Error; -#[derive(Error, Debug, PartialEq)] +#[derive(Error, Debug)] pub enum ContractError { - #[error(transparent)] + #[error("{0}")] Std(#[from] StdError), - #[error(transparent)] + #[error("{0}")] PaymentError(#[from] PaymentError), - #[error(transparent)] - ParseReplyError(#[from] ParseReplyError), - - #[error(transparent)] - HookError(#[from] cw_hooks::HookError), - - #[error("Got a submessage reply with unknown id: {id}")] - UnknownReplyId { id: u64 }, - - #[error("Token factory core contract instantiate error")] - TokenFactoryCoreInstantiateError {}, - - #[error("Initial governance token balances must not be empty")] - InitialBalancesError {}, - #[error("Unauthorized")] Unauthorized {}, @@ -45,16 +30,4 @@ pub enum ContractError { #[error("Amount being unstaked must be non-zero")] ZeroUnstake {}, - - #[error("Active threshold percentage must be greater than 0 and less than 1")] - InvalidActivePercentage {}, - - #[error("Active threshold count must be greater than zero")] - ZeroActiveCount {}, - - #[error("Absolute count threshold cannot be greater than the total token supply")] - InvalidAbsoluteCount {}, - - #[error("Cannot change the contract's token after it has been set")] - DuplicateToken {}, } diff --git a/contracts/voting/dao-voting-native-staked/src/lib.rs b/contracts/voting/dao-voting-native-staked/src/lib.rs index 9d62fa3a3..d1800adbc 100644 --- a/contracts/voting/dao-voting-native-staked/src/lib.rs +++ b/contracts/voting/dao-voting-native-staked/src/lib.rs @@ -2,12 +2,9 @@ pub mod contract; mod error; -pub mod hooks; pub mod msg; pub mod state; -// #[cfg(test)] -// mod test_tube; #[cfg(test)] mod tests; diff --git a/contracts/voting/dao-voting-native-staked/src/msg.rs b/contracts/voting/dao-voting-native-staked/src/msg.rs index f9e984d52..bf836ca12 100644 --- a/contracts/voting/dao-voting-native-staked/src/msg.rs +++ b/contracts/voting/dao-voting-native-staked/src/msg.rs @@ -1,48 +1,19 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::Uint128; use cw_utils::Duration; -use dao_dao_macros::{active_query, voting_module_query}; +use dao_dao_macros::voting_module_query; use dao_interface::state::Admin; -use dao_voting::threshold::ActiveThreshold; -use token_bindings::Metadata; - -#[cw_serde] -pub struct InitialBalance { - pub amount: Uint128, - pub mint_to_address: String, -} - -#[cw_serde] -pub struct NewTokenInfo { - pub subdenom: String, - pub metadata: Option, - pub initial_balances: Vec, - pub initial_dao_balance: Option, -} - -#[cw_serde] -pub enum TokenInfo { - Existing { - /// Token denom e.g. ujuno, or some ibc denom. - denom: String, - }, - New(NewTokenInfo), -} #[cw_serde] pub struct InstantiateMsg { - // TODO replace with cw-ownable // Owner can update all configs including changing the owner. This will generally be a DAO. pub owner: Option, // Manager can update all configs except changing the owner. This will generally be an operations multisig for a DAO. pub manager: Option, - // New or existing native token to use for voting power. - pub token_info: TokenInfo, + // Token denom e.g. ujuno, or some ibc denom + pub denom: String, // How long until the tokens become liquid again pub unstaking_duration: Option, - /// The number or percentage of tokens that must be staked - /// for the DAO to be active - pub active_threshold: Option, } #[cw_serde] @@ -57,29 +28,14 @@ pub enum ExecuteMsg { duration: Option, }, Claim {}, - /// Sets the active threshold to a new value. Only the - /// instantiator of this contract (a DAO most likely) may call this - /// method. - UpdateActiveThreshold { - new_threshold: Option, - }, - AddHook { - addr: String, - }, - RemoveHook { - addr: String, - }, } #[voting_module_query] -#[active_query] #[cw_serde] #[derive(QueryResponses)] pub enum QueryMsg { #[returns(crate::state::Config)] GetConfig {}, - #[returns(DenomResponse)] - GetDenom {}, #[returns(cw_controllers::ClaimsResponse)] Claims { address: String }, #[returns(ListStakersResponse)] @@ -87,10 +43,6 @@ pub enum QueryMsg { start_after: Option, limit: Option, }, - #[returns(ActiveThresholdResponse)] - ActiveThreshold {}, - #[returns(GetHooksResponse)] - GetHooks {}, } #[cw_serde] @@ -106,18 +58,3 @@ pub struct StakerBalanceResponse { pub address: String, pub balance: Uint128, } - -#[cw_serde] -pub struct DenomResponse { - pub denom: String, -} - -#[cw_serde] -pub struct ActiveThresholdResponse { - pub active_threshold: Option, -} - -#[cw_serde] -pub struct GetHooksResponse { - pub hooks: Vec, -} diff --git a/contracts/voting/dao-voting-native-staked/src/state.rs b/contracts/voting/dao-voting-native-staked/src/state.rs index fb45ef473..65849e89e 100644 --- a/contracts/voting/dao-voting-native-staked/src/state.rs +++ b/contracts/voting/dao-voting-native-staked/src/state.rs @@ -1,31 +1,19 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Uint128}; use cw_controllers::Claims; -use cw_hooks::Hooks; use cw_storage_plus::{Item, SnapshotItem, SnapshotMap, Strategy}; use cw_utils::Duration; -use dao_voting::threshold::ActiveThreshold; - -use crate::msg::NewTokenInfo; #[cw_serde] pub struct Config { - // TODO use cw-ownable pub owner: Option, pub manager: Option, + pub denom: String, pub unstaking_duration: Option, } -/// The configuration of this voting contract pub const CONFIG: Item = Item::new("config"); - -/// The native denom associated with this contract -pub const DENOM: Item = Item::new("denom"); - -/// The address of the DAO this voting contract is connected to pub const DAO: Item = Item::new("dao"); - -/// Keeps track of staked balances by address over time pub const STAKED_BALANCES: SnapshotMap<&Addr, Uint128> = SnapshotMap::new( "staked_balances", "staked_balance__checkpoints", @@ -33,7 +21,6 @@ pub const STAKED_BALANCES: SnapshotMap<&Addr, Uint128> = SnapshotMap::new( Strategy::EveryBlock, ); -/// Keeps track of staked total over time pub const STAKED_TOTAL: SnapshotItem = SnapshotItem::new( "total_staked", "total_staked__checkpoints", @@ -45,12 +32,3 @@ pub const STAKED_TOTAL: SnapshotItem = SnapshotItem::new( pub const MAX_CLAIMS: u64 = 100; pub const CLAIMS: Claims = Claims::new("claims"); - -/// The minimum amount of staked tokens for the DAO to be active -pub const ACTIVE_THRESHOLD: Item = Item::new("active_threshold"); - -/// Hooks to contracts that will receive staking and unstaking messages -pub const HOOKS: Hooks = Hooks::new("hooks"); - -/// Temporarily holds token_instantiation_info when creating a new Token Factory denom -pub const TOKEN_INSTANTIATION_INFO: Item = Item::new("token_instantiation_info"); diff --git a/contracts/voting/dao-voting-native-staked/src/test_tube.rs b/contracts/voting/dao-voting-native-staked/src/test_tube.rs deleted file mode 100644 index 1e31a5bc4..000000000 --- a/contracts/voting/dao-voting-native-staked/src/test_tube.rs +++ /dev/null @@ -1,12 +0,0 @@ -use cosmwasm_std::Coin; -use osmosis_test_tube::OsmosisTestApp; - -#[test] -fn test_tube() { - let app = OsmosisTestApp::new(); - - let account = app.init_account(&[ - Coin::new(1_000_000_000_000, "uatom"), - Coin::new(1_000_000_000_000, "uosmo"), - ]); -} diff --git a/contracts/voting/dao-voting-native-staked/src/tests.rs b/contracts/voting/dao-voting-native-staked/src/tests.rs index 39dfc9468..b6973ff33 100644 --- a/contracts/voting/dao-voting-native-staked/src/tests.rs +++ b/contracts/voting/dao-voting-native-staked/src/tests.rs @@ -1,95 +1,86 @@ -use std::marker::PhantomData; - use crate::contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}; use crate::msg::{ - ActiveThresholdResponse, DenomResponse, ExecuteMsg, GetHooksResponse, InitialBalance, - InstantiateMsg, ListStakersResponse, MigrateMsg, NewTokenInfo, QueryMsg, StakerBalanceResponse, - TokenInfo, + ExecuteMsg, InstantiateMsg, ListStakersResponse, MigrateMsg, QueryMsg, StakerBalanceResponse, }; use crate::state::Config; -use crate::ContractError; -use cosmwasm_std::testing::{mock_env, MockApi, MockQuerier, MockStorage}; -use cosmwasm_std::{coins, Addr, Coin, Decimal, OwnedDeps, Uint128}; +use cosmwasm_std::testing::{mock_dependencies, mock_env}; +use cosmwasm_std::{coins, Addr, Coin, Empty, Uint128}; use cw_controllers::ClaimsResponse; use cw_multi_test::{ - next_block, AppResponse, BankSudo, Contract, ContractWrapper, Executor, SudoMsg, + custom_app, next_block, App, AppResponse, Contract, ContractWrapper, Executor, }; use cw_utils::Duration; use dao_interface::state::Admin; use dao_interface::voting::{ - InfoResponse, IsActiveResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse, + InfoResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse, }; -use dao_voting::threshold::ActiveThreshold; -use token_bindings::{Metadata, TokenFactoryMsg, TokenFactoryQuery}; -use token_bindings_test::TokenFactoryApp as App; const DAO_ADDR: &str = "dao"; const ADDR1: &str = "addr1"; const ADDR2: &str = "addr2"; const DENOM: &str = "ujuno"; const INVALID_DENOM: &str = "uinvalid"; -const ODD_DENOM: &str = "uodd"; -fn staking_contract() -> Box> { +fn staking_contract() -> Box> { let contract = ContractWrapper::new( crate::contract::execute, crate::contract::instantiate, crate::contract::query, - ) - .with_reply(crate::contract::reply); + ); Box::new(contract) } fn mock_app() -> App { - let mut app = App::new(); - app.sudo(SudoMsg::Bank(BankSudo::Mint { - to_address: DAO_ADDR.to_string(), - amount: vec![ - Coin { - denom: DENOM.to_string(), - amount: Uint128::new(10000), - }, - Coin { - denom: INVALID_DENOM.to_string(), - amount: Uint128::new(10000), - }, - ], - })) - .unwrap(); - app.sudo(SudoMsg::Bank(BankSudo::Mint { - to_address: ADDR1.to_string(), - amount: vec![ - Coin { - denom: DENOM.to_string(), - amount: Uint128::new(10000), - }, - Coin { - denom: INVALID_DENOM.to_string(), - amount: Uint128::new(10000), - }, - Coin { - denom: ODD_DENOM.to_string(), - amount: Uint128::new(5), - }, - ], - })) - .unwrap(); - app.sudo(SudoMsg::Bank(BankSudo::Mint { - to_address: ADDR2.to_string(), - amount: vec![ - Coin { - denom: DENOM.to_string(), - amount: Uint128::new(10000), - }, - Coin { - denom: INVALID_DENOM.to_string(), - amount: Uint128::new(10000), - }, - ], - })) - .unwrap(); - app.update_block(next_block); - app + custom_app(|r, _a, s| { + r.bank + .init_balance( + s, + &Addr::unchecked(DAO_ADDR), + vec![ + Coin { + denom: DENOM.to_string(), + amount: Uint128::new(10000), + }, + Coin { + denom: INVALID_DENOM.to_string(), + amount: Uint128::new(10000), + }, + ], + ) + .unwrap(); + r.bank + .init_balance( + s, + &Addr::unchecked(ADDR1), + vec![ + Coin { + denom: DENOM.to_string(), + amount: Uint128::new(10000), + }, + Coin { + denom: INVALID_DENOM.to_string(), + amount: Uint128::new(10000), + }, + ], + ) + .unwrap(); + r.bank + .init_balance( + s, + &Addr::unchecked(ADDR2), + vec![ + Coin { + denom: DENOM.to_string(), + amount: Uint128::new(10000), + }, + Coin { + denom: INVALID_DENOM.to_string(), + amount: Uint128::new(10000), + }, + ], + ) + .unwrap(); + }) } fn instantiate_staking(app: &mut App, staking_id: u64, msg: InstantiateMsg) -> Addr { @@ -104,20 +95,6 @@ fn instantiate_staking(app: &mut App, staking_id: u64, msg: InstantiateMsg) -> A .unwrap() } -fn instantiate_staking_error(app: &mut App, staking_id: u64, msg: InstantiateMsg) -> ContractError { - app.instantiate_contract( - staking_id, - Addr::unchecked(DAO_ADDR), - &msg, - &[], - "Staking", - None, - ) - .unwrap_err() - .downcast() - .unwrap() -} - fn stake_tokens( app: &mut App, staking_addr: Addr, @@ -208,12 +185,6 @@ fn get_config(app: &mut App, staking_addr: Addr) -> Config { .unwrap() } -fn get_denom(app: &mut App, staking_addr: Addr) -> DenomResponse { - app.wrap() - .query_wasm_smart(staking_addr, &QueryMsg::GetDenom {}) - .unwrap() -} - fn get_claims(app: &mut App, staking_addr: Addr, address: String) -> ClaimsResponse { app.wrap() .query_wasm_smart(staking_addr, &QueryMsg::Claims { address }) @@ -225,7 +196,7 @@ fn get_balance(app: &mut App, address: &str, denom: &str) -> Uint128 { } #[test] -fn test_instantiate_existing() { +fn test_instantiate() { let mut app = mock_app(); let staking_id = app.store_code(staking_contract()); // Populated fields @@ -237,11 +208,8 @@ fn test_instantiate_existing() { addr: DAO_ADDR.to_string(), }), manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, }, ); @@ -252,105 +220,10 @@ fn test_instantiate_existing() { InstantiateMsg { owner: None, manager: None, - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, - unstaking_duration: None, - active_threshold: None, - }, - ); -} - -#[test] -fn test_instantiate_new_denom() { - let mut app = mock_app(); - let staking_id = app.store_code(staking_contract()); - - // Populated fields - let addr = instantiate_staking( - &mut app, - staking_id, - InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: Some(ADDR1.to_string()), - token_info: TokenInfo::New(NewTokenInfo { - subdenom: DENOM.to_string(), - metadata: Some(Metadata { - description: Some("Awesome token, get it now!".to_string()), - denom_units: vec![], - base: None, - display: Some(DENOM.to_string()), - name: Some(DENOM.to_string()), - symbol: Some(DENOM.to_string()), - }), - initial_balances: vec![InitialBalance { - amount: Uint128::new(100), - mint_to_address: ADDR1.to_string(), - }], - initial_dao_balance: Some(Uint128::new(900)), - }), - unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, - }, - ); - - let denom = get_denom(&mut app, addr.clone()); - - assert_eq!(denom.denom, format!("factory/{}/{}", addr, DENOM)); - - // Non populated fields - instantiate_staking( - &mut app, - staking_id, - InstantiateMsg { - owner: None, - manager: None, - token_info: TokenInfo::New(NewTokenInfo { - subdenom: DENOM.to_string(), - metadata: Some(Metadata { - description: Some("Awesome token, get it now!".to_string()), - denom_units: vec![], - base: None, - display: Some(DENOM.to_string()), - name: Some(DENOM.to_string()), - symbol: Some(DENOM.to_string()), - }), - initial_balances: vec![InitialBalance { - amount: Uint128::new(100), - mint_to_address: ADDR1.to_string(), - }], - initial_dao_balance: None, - }), + denom: DENOM.to_string(), unstaking_duration: None, - active_threshold: None, }, ); - - // No initial balances except DAO. - let err = instantiate_staking_error( - &mut app, - staking_id, - InstantiateMsg { - owner: None, - manager: None, - token_info: TokenInfo::New(NewTokenInfo { - subdenom: DENOM.to_string(), - metadata: Some(Metadata { - description: Some("Awesome token, get it now!".to_string()), - denom_units: vec![], - base: None, - display: Some(DENOM.to_string()), - name: Some(DENOM.to_string()), - symbol: Some(DENOM.to_string()), - }), - initial_balances: vec![], - initial_dao_balance: None, - }), - unstaking_duration: None, - active_threshold: None, - }, - ); - assert_eq!(err, ContractError::InitialBalancesError {}); } #[test] @@ -364,11 +237,8 @@ fn test_instantiate_dao_owner() { InstantiateMsg { owner: Some(Admin::CoreModule {}), manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, }, ); @@ -391,13 +261,8 @@ fn test_instantiate_invalid_unstaking_duration() { addr: DAO_ADDR.to_string(), }), manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(0)), - active_threshold: Some(ActiveThreshold::AbsoluteCount { - count: Uint128::new(1), - }), }, ); @@ -408,11 +273,8 @@ fn test_instantiate_invalid_unstaking_duration() { InstantiateMsg { owner: None, manager: None, - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: None, - active_threshold: None, }, ); } @@ -428,11 +290,8 @@ fn test_stake_invalid_denom() { InstantiateMsg { owner: Some(Admin::CoreModule {}), manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, }, ); @@ -450,11 +309,8 @@ fn test_stake_valid_denom() { InstantiateMsg { owner: Some(Admin::CoreModule {}), manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, }, ); @@ -463,43 +319,6 @@ fn test_stake_valid_denom() { app.update_block(next_block); } -#[test] -fn test_stake_new_denom() { - let mut app = mock_app(); - let staking_id = app.store_code(staking_contract()); - let addr = instantiate_staking( - &mut app, - staking_id, - InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: Some(ADDR1.to_string()), - token_info: TokenInfo::New(NewTokenInfo { - subdenom: DENOM.to_string(), - metadata: Some(Metadata { - description: Some("Awesome token, get it now!".to_string()), - denom_units: vec![], - base: None, - display: Some(DENOM.to_string()), - name: Some(DENOM.to_string()), - symbol: Some(DENOM.to_string()), - }), - initial_balances: vec![InitialBalance { - amount: Uint128::new(100), - mint_to_address: ADDR1.to_string(), - }], - initial_dao_balance: Some(Uint128::new(900)), - }), - unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, - }, - ); - - // Try and stake a valid denom - let denom = get_denom(&mut app, addr.clone()).denom; - stake_tokens(&mut app, addr, ADDR1, 100, &denom).unwrap(); - app.update_block(next_block); -} - #[test] #[should_panic(expected = "Can only unstake less than or equal to the amount you have staked")] fn test_unstake_none_staked() { @@ -511,11 +330,8 @@ fn test_unstake_none_staked() { InstantiateMsg { owner: Some(Admin::CoreModule {}), manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, }, ); @@ -533,11 +349,8 @@ fn test_unstake_zero_tokens() { InstantiateMsg { owner: Some(Admin::CoreModule {}), manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, }, ); @@ -555,11 +368,8 @@ fn test_unstake_invalid_balance() { InstantiateMsg { owner: Some(Admin::CoreModule {}), manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, }, ); @@ -581,11 +391,8 @@ fn test_unstake() { InstantiateMsg { owner: Some(Admin::CoreModule {}), manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, }, ); @@ -619,11 +426,8 @@ fn test_unstake_no_unstaking_duration() { InstantiateMsg { owner: Some(Admin::CoreModule {}), manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: None, - active_threshold: None, }, ); @@ -659,11 +463,8 @@ fn test_claim_no_claims() { InstantiateMsg { owner: Some(Admin::CoreModule {}), manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, }, ); @@ -681,11 +482,8 @@ fn test_claim_claim_not_reached() { InstantiateMsg { owner: Some(Admin::CoreModule {}), manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, }, ); @@ -711,11 +509,8 @@ fn test_claim() { InstantiateMsg { owner: Some(Admin::CoreModule {}), manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, }, ); @@ -765,11 +560,8 @@ fn test_update_config_invalid_sender() { InstantiateMsg { owner: Some(Admin::CoreModule {}), manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, }, ); @@ -796,11 +588,8 @@ fn test_update_config_non_owner_changes_owner() { InstantiateMsg { owner: Some(Admin::CoreModule {}), manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, }, ); @@ -818,11 +607,8 @@ fn test_update_config_as_owner() { InstantiateMsg { owner: Some(Admin::CoreModule {}), manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, }, ); @@ -843,6 +629,7 @@ fn test_update_config_as_owner() { owner: Some(Addr::unchecked(ADDR1)), manager: Some(Addr::unchecked(DAO_ADDR)), unstaking_duration: Some(Duration::Height(10)), + denom: DENOM.to_string(), }, config ); @@ -858,11 +645,8 @@ fn test_update_config_as_manager() { InstantiateMsg { owner: Some(Admin::CoreModule {}), manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, }, ); @@ -883,6 +667,7 @@ fn test_update_config_as_manager() { owner: Some(Addr::unchecked(DAO_ADDR)), manager: Some(Addr::unchecked(ADDR2)), unstaking_duration: Some(Duration::Height(10)), + denom: DENOM.to_string(), }, config ); @@ -899,11 +684,8 @@ fn test_update_config_invalid_duration() { InstantiateMsg { owner: Some(Admin::CoreModule {}), manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, }, ); @@ -929,11 +711,8 @@ fn test_query_dao() { InstantiateMsg { owner: Some(Admin::CoreModule {}), manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, }, ); @@ -952,11 +731,8 @@ fn test_query_info() { InstantiateMsg { owner: Some(Admin::CoreModule {}), manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, }, ); @@ -975,11 +751,8 @@ fn test_query_claims() { InstantiateMsg { owner: Some(Admin::CoreModule {}), manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, }, ); @@ -1014,11 +787,8 @@ fn test_query_get_config() { InstantiateMsg { owner: Some(Admin::CoreModule {}), manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, }, ); @@ -1029,6 +799,7 @@ fn test_query_get_config() { owner: Some(Addr::unchecked(DAO_ADDR)), manager: Some(Addr::unchecked(ADDR1)), unstaking_duration: Some(Duration::Height(5)), + denom: DENOM.to_string(), } ) } @@ -1043,11 +814,8 @@ fn test_voting_power_queries() { InstantiateMsg { owner: Some(Admin::CoreModule {}), manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, }, ); @@ -1152,11 +920,8 @@ fn test_query_list_stakers() { InstantiateMsg { owner: Some(Admin::CoreModule {}), manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, + denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, }, ); @@ -1229,387 +994,9 @@ fn test_query_list_stakers() { assert_eq!(stakers, ListStakersResponse { stakers: vec![] }); } -#[test] -#[should_panic(expected = "Active threshold count must be greater than zero")] -fn test_instantiate_zero_active_threshold_count() { - let mut app = mock_app(); - let staking_id = app.store_code(staking_contract()); - instantiate_staking( - &mut app, - staking_id, - InstantiateMsg { - owner: Some(Admin::Address { - addr: DAO_ADDR.to_string(), - }), - manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, - unstaking_duration: Some(Duration::Height(5)), - active_threshold: Some(ActiveThreshold::AbsoluteCount { - count: Uint128::zero(), - }), - }, - ); -} - -#[test] -fn test_active_threshold_absolute_count() { - let mut app = mock_app(); - let staking_id = app.store_code(staking_contract()); - - let addr = instantiate_staking( - &mut app, - staking_id, - InstantiateMsg { - owner: Some(Admin::Address { - addr: DAO_ADDR.to_string(), - }), - manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, - unstaking_duration: Some(Duration::Height(5)), - active_threshold: Some(ActiveThreshold::AbsoluteCount { - count: Uint128::new(100), - }), - }, - ); - - // Not active as none staked - let is_active: IsActiveResponse = app - .wrap() - .query_wasm_smart(addr.clone(), &QueryMsg::IsActive {}) - .unwrap(); - assert!(!is_active.active); - - // Stake 100 tokens - stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap(); - app.update_block(next_block); - - // Active as enough staked - let is_active: IsActiveResponse = app - .wrap() - .query_wasm_smart(addr, &QueryMsg::IsActive {}) - .unwrap(); - assert!(is_active.active); -} - -#[test] -fn test_active_threshold_percent() { - let mut app = mock_app(); - let staking_id = app.store_code(staking_contract()); - let addr = instantiate_staking( - &mut app, - staking_id, - InstantiateMsg { - owner: Some(Admin::Address { - addr: DAO_ADDR.to_string(), - }), - manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, - unstaking_duration: Some(Duration::Height(5)), - active_threshold: Some(ActiveThreshold::Percentage { - percent: Decimal::percent(20), - }), - }, - ); - - // Not active as none staked - let is_active: IsActiveResponse = app - .wrap() - .query_wasm_smart(addr.clone(), &QueryMsg::IsActive {}) - .unwrap(); - assert!(!is_active.active); - - // Stake 6000 tokens, now active - stake_tokens(&mut app, addr.clone(), ADDR1, 6000, DENOM).unwrap(); - app.update_block(next_block); - - // Active as enough staked - let is_active: IsActiveResponse = app - .wrap() - .query_wasm_smart(addr, &QueryMsg::IsActive {}) - .unwrap(); - assert!(is_active.active); -} - -#[test] -fn test_active_threshold_percent_rounds_up() { - let mut app = mock_app(); - let staking_id = app.store_code(staking_contract()); - let addr = instantiate_staking( - &mut app, - staking_id, - InstantiateMsg { - owner: Some(Admin::Address { - addr: DAO_ADDR.to_string(), - }), - manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: ODD_DENOM.to_string(), - }, - unstaking_duration: Some(Duration::Height(5)), - active_threshold: Some(ActiveThreshold::Percentage { - percent: Decimal::percent(50), - }), - }, - ); - - // Not active as none staked - let is_active: IsActiveResponse = app - .wrap() - .query_wasm_smart(addr.clone(), &QueryMsg::IsActive {}) - .unwrap(); - assert!(!is_active.active); - - // Stake 2 tokens, should not be active. - stake_tokens(&mut app, addr.clone(), ADDR1, 2, ODD_DENOM).unwrap(); - app.update_block(next_block); - - let is_active: IsActiveResponse = app - .wrap() - .query_wasm_smart(addr.clone(), &QueryMsg::IsActive {}) - .unwrap(); - assert!(!is_active.active); - - // Stake 1 more token, should now be active. - stake_tokens(&mut app, addr.clone(), ADDR1, 1, ODD_DENOM).unwrap(); - app.update_block(next_block); - - let is_active: IsActiveResponse = app - .wrap() - .query_wasm_smart(addr, &QueryMsg::IsActive {}) - .unwrap(); - assert!(is_active.active); -} - -#[test] -fn test_active_threshold_none() { - let mut app = App::default(); - let staking_id = app.store_code(staking_contract()); - let addr = instantiate_staking( - &mut app, - staking_id, - InstantiateMsg { - owner: Some(Admin::Address { - addr: DAO_ADDR.to_string(), - }), - manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, - unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, - }, - ); - - // Active as no threshold - let is_active: IsActiveResponse = app - .wrap() - .query_wasm_smart(addr, &QueryMsg::IsActive {}) - .unwrap(); - assert!(is_active.active); -} - -#[test] -fn test_update_active_threshold() { - let mut app = mock_app(); - let staking_id = app.store_code(staking_contract()); - let addr = instantiate_staking( - &mut app, - staking_id, - InstantiateMsg { - owner: Some(Admin::Address { - addr: DAO_ADDR.to_string(), - }), - manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, - unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, - }, - ); - - let resp: ActiveThresholdResponse = app - .wrap() - .query_wasm_smart(addr.clone(), &QueryMsg::ActiveThreshold {}) - .unwrap(); - assert_eq!(resp.active_threshold, None); - - let msg = ExecuteMsg::UpdateActiveThreshold { - new_threshold: Some(ActiveThreshold::AbsoluteCount { - count: Uint128::new(100), - }), - }; - - // Expect failure as sender is not the DAO - app.execute_contract(Addr::unchecked(ADDR1), addr.clone(), &msg, &[]) - .unwrap_err(); - - // Expect success as sender is the DAO - app.execute_contract(Addr::unchecked(DAO_ADDR), addr.clone(), &msg, &[]) - .unwrap(); - - let resp: ActiveThresholdResponse = app - .wrap() - .query_wasm_smart(addr, &QueryMsg::ActiveThreshold {}) - .unwrap(); - assert_eq!( - resp.active_threshold, - Some(ActiveThreshold::AbsoluteCount { - count: Uint128::new(100) - }) - ); -} - -#[test] -#[should_panic(expected = "Active threshold percentage must be greater than 0 and less than 1")] -fn test_active_threshold_percentage_gt_100() { - let mut app = App::default(); - let staking_id = app.store_code(staking_contract()); - instantiate_staking( - &mut app, - staking_id, - InstantiateMsg { - owner: Some(Admin::Address { - addr: DAO_ADDR.to_string(), - }), - manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, - unstaking_duration: Some(Duration::Height(5)), - active_threshold: Some(ActiveThreshold::Percentage { - percent: Decimal::percent(120), - }), - }, - ); -} - -#[test] -#[should_panic(expected = "Active threshold percentage must be greater than 0 and less than 1")] -fn test_active_threshold_percentage_lte_0() { - let mut app = App::default(); - let staking_id = app.store_code(staking_contract()); - instantiate_staking( - &mut app, - staking_id, - InstantiateMsg { - owner: Some(Admin::Address { - addr: DAO_ADDR.to_string(), - }), - manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, - unstaking_duration: Some(Duration::Height(5)), - active_threshold: Some(ActiveThreshold::Percentage { - percent: Decimal::percent(0), - }), - }, - ); -} - -#[test] -#[should_panic(expected = "Absolute count threshold cannot be greater than the total token supply")] -fn test_active_threshold_absolute_count_invalid() { - let mut app = App::default(); - let staking_id = app.store_code(staking_contract()); - instantiate_staking( - &mut app, - staking_id, - InstantiateMsg { - owner: Some(Admin::Address { - addr: DAO_ADDR.to_string(), - }), - manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, - unstaking_duration: Some(Duration::Height(5)), - active_threshold: Some(ActiveThreshold::AbsoluteCount { - count: Uint128::new(30001), - }), - }, - ); -} - -#[test] -fn test_add_remove_hooks() { - let mut app = App::default(); - let staking_id = app.store_code(staking_contract()); - let addr = instantiate_staking( - &mut app, - staking_id, - InstantiateMsg { - owner: Some(Admin::Address { - addr: DAO_ADDR.to_string(), - }), - manager: Some(ADDR1.to_string()), - token_info: TokenInfo::Existing { - denom: DENOM.to_string(), - }, - unstaking_duration: Some(Duration::Height(5)), - active_threshold: None, - }, - ); - - // No hooks exist. - let resp: GetHooksResponse = app - .wrap() - .query_wasm_smart(addr.clone(), &QueryMsg::GetHooks {}) - .unwrap(); - assert_eq!(resp.hooks, Vec::::new()); - - // Add a hook. - app.execute_contract( - Addr::unchecked(DAO_ADDR), - addr.clone(), - &ExecuteMsg::AddHook { - addr: "hook".to_string(), - }, - &[], - ) - .unwrap(); - - // One hook exists. - let resp: GetHooksResponse = app - .wrap() - .query_wasm_smart(addr.clone(), &QueryMsg::GetHooks {}) - .unwrap(); - assert_eq!(resp.hooks, vec!["hook".to_string()]); - - // Remove hook. - app.execute_contract( - Addr::unchecked(DAO_ADDR), - addr.clone(), - &ExecuteMsg::RemoveHook { - addr: "hook".to_string(), - }, - &[], - ) - .unwrap(); - - // No hook exists. - let resp: GetHooksResponse = app - .wrap() - .query_wasm_smart(addr, &QueryMsg::GetHooks {}) - .unwrap(); - assert_eq!(resp.hooks, Vec::::new()); -} - #[test] pub fn test_migrate_update_version() { - let mut deps = OwnedDeps { - storage: MockStorage::default(), - api: MockApi::default(), - querier: MockQuerier::default(), - custom_query_type: PhantomData::, - }; + let mut deps = mock_dependencies(); cw2::set_contract_version(&mut deps.storage, "my-contract", "old-version").unwrap(); migrate(deps.as_mut(), mock_env(), MigrateMsg {}).unwrap(); let version = cw2::get_contract_version(&deps.storage).unwrap(); diff --git a/contracts/voting/dao-voting-token-factory-staked/.cargo/config b/contracts/voting/dao-voting-token-factory-staked/.cargo/config new file mode 100644 index 000000000..336b618a1 --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/.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/voting/dao-voting-token-factory-staked/.gitignore b/contracts/voting/dao-voting-token-factory-staked/.gitignore new file mode 100644 index 000000000..dfdaaa6bc --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/.gitignore @@ -0,0 +1,15 @@ +# Build results +/target + +# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) +.cargo-ok + +# Text file backups +**/*.rs.bk + +# macOS +.DS_Store + +# IDEs +*.iml +.idea diff --git a/contracts/voting/dao-voting-token-factory-staked/Cargo.toml b/contracts/voting/dao-voting-token-factory-staked/Cargo.toml new file mode 100644 index 000000000..d5a08a48a --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "dao-voting-token-factory-staked" +authors = ["Callum Anderson ", "Noah Saso ", "Jake Hartnell "] +description = "A DAO DAO voting module based on staked token factory or native tokens. Only works with chains that support Token Factory." +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, features = ["cosmwasm_1_1"] } +cosmwasm-schema = { workspace = true } +cosmwasm-storage = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +cw-utils = { workspace = true } +cw-controllers = { workspace = true } +cw-hooks = { workspace = true } +thiserror = { workspace = true } +dao-dao-macros = { workspace = true } +dao-interface = { workspace = true } +dao-voting = { workspace = true } +cw-paginate-storage = { workspace = true } +token-bindings = { workspace = true } + +[dev-dependencies] +anyhow = { workspace = true } +cw-multi-test = { workspace = true } +# osmosis-test-tube = { workspace = true } +token-bindings-test = { workspace = true } diff --git a/contracts/voting/dao-voting-token-factory-staked/README.md b/contracts/voting/dao-voting-token-factory-staked/README.md new file mode 100644 index 000000000..f3ca10443 --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/README.md @@ -0,0 +1,11 @@ +# Token Factory Staked Balance Voting + +Simple native or token factory based voting contract which assumes the native denom +provided is not used for staking for securing the network e.g. IBC +denoms or secondary tokens (ION). Staked balances may be queried at an +arbitrary height. This contract implements the interface needed to be a DAO +DAO [voting +module](https://github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-Contracts-Design#the-voting-module). + +This contract requires having the Token Factory module on your chain, which allows the creation of new native tokens. If your chain does not have this module, use `dao-voting-native-staked` instead. + diff --git a/contracts/voting/dao-voting-token-factory-staked/examples/schema.rs b/contracts/voting/dao-voting-token-factory-staked/examples/schema.rs new file mode 100644 index 000000000..2aa85dbff --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/examples/schema.rs @@ -0,0 +1,11 @@ +use cosmwasm_schema::write_api; +use dao_voting_token_factory_staked::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg, + } +} diff --git a/contracts/voting/dao-voting-token-factory-staked/schema/dao-voting-native-staked.json b/contracts/voting/dao-voting-token-factory-staked/schema/dao-voting-native-staked.json new file mode 100644 index 000000000..b40013ef0 --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/schema/dao-voting-native-staked.json @@ -0,0 +1,1246 @@ +{ + "contract_name": "dao-voting-native-staked", + "contract_version": "2.2.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "token_info" + ], + "properties": { + "active_threshold": { + "description": "The number or percentage of tokens that must be staked for the DAO to be active", + "anyOf": [ + { + "$ref": "#/definitions/ActiveThreshold" + }, + { + "type": "null" + } + ] + }, + "manager": { + "type": [ + "string", + "null" + ] + }, + "owner": { + "anyOf": [ + { + "$ref": "#/definitions/Admin" + }, + { + "type": "null" + } + ] + }, + "token_info": { + "$ref": "#/definitions/TokenInfo" + }, + "unstaking_duration": { + "anyOf": [ + { + "$ref": "#/definitions/Duration" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "ActiveThreshold": { + "description": "The threshold of tokens that must be staked in order for this voting module to be active. If this is not reached, this module will response to `is_active` queries with false and proposal modules which respect active thresholds will not allow the creation of proposals.", + "oneOf": [ + { + "description": "The absolute number of tokens that must be staked for the module to be active.", + "type": "object", + "required": [ + "absolute_count" + ], + "properties": { + "absolute_count": { + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "The percentage of tokens that must be staked for the module to be active. Computed as `staked / total_supply`.", + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "type": "object", + "required": [ + "percent" + ], + "properties": { + "percent": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Admin": { + "description": "Information about the CosmWasm level admin of a contract. Used in conjunction with `ModuleInstantiateInfo` to instantiate modules.", + "oneOf": [ + { + "description": "Set the admin to a specified address.", + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Sets the admin as the core module address.", + "type": "object", + "required": [ + "core_module" + ], + "properties": { + "core_module": { + "type": "object", + "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 + }, + "Duration": { + "description": "Duration is a delta of time. You can add it to a BlockInfo or Expiration to move that further in the future. Note that an height-based Duration and a time-based Expiration cannot be combined", + "oneOf": [ + { + "type": "object", + "required": [ + "height" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "Time in seconds", + "type": "object", + "required": [ + "time" + ], + "properties": { + "time": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + }, + "InitialBalance": { + "type": "object", + "required": [ + "amount", + "mint_to_address" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "mint_to_address": { + "type": "string" + } + }, + "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 + }, + "NewTokenInfo": { + "type": "object", + "required": [ + "initial_balances", + "subdenom" + ], + "properties": { + "initial_balances": { + "type": "array", + "items": { + "$ref": "#/definitions/InitialBalance" + } + }, + "initial_dao_balance": { + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/Metadata" + }, + { + "type": "null" + } + ] + }, + "subdenom": { + "type": "string" + } + }, + "additionalProperties": false + }, + "TokenInfo": { + "oneOf": [ + { + "type": "object", + "required": [ + "existing" + ], + "properties": { + "existing": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "description": "Token denom e.g. ujuno, or some ibc denom.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "new" + ], + "properties": { + "new": { + "$ref": "#/definitions/NewTokenInfo" + } + }, + "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": [ + { + "type": "object", + "required": [ + "stake" + ], + "properties": { + "stake": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "unstake" + ], + "properties": { + "unstake": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "properties": { + "duration": { + "anyOf": [ + { + "$ref": "#/definitions/Duration" + }, + { + "type": "null" + } + ] + }, + "manager": { + "type": [ + "string", + "null" + ] + }, + "owner": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "claim" + ], + "properties": { + "claim": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Sets the active threshold to a new value. Only the instantiator of this contract (a DAO most likely) may call this method.", + "type": "object", + "required": [ + "update_active_threshold" + ], + "properties": { + "update_active_threshold": { + "type": "object", + "properties": { + "new_threshold": { + "anyOf": [ + { + "$ref": "#/definitions/ActiveThreshold" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "add_hook" + ], + "properties": { + "add_hook": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "remove_hook" + ], + "properties": { + "remove_hook": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "ActiveThreshold": { + "description": "The threshold of tokens that must be staked in order for this voting module to be active. If this is not reached, this module will response to `is_active` queries with false and proposal modules which respect active thresholds will not allow the creation of proposals.", + "oneOf": [ + { + "description": "The absolute number of tokens that must be staked for the module to be active.", + "type": "object", + "required": [ + "absolute_count" + ], + "properties": { + "absolute_count": { + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "The percentage of tokens that must be staked for the module to be active. Computed as `staked / total_supply`.", + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "type": "object", + "required": [ + "percent" + ], + "properties": { + "percent": { + "$ref": "#/definitions/Decimal" + } + }, + "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" + }, + "Duration": { + "description": "Duration is a delta of time. You can add it to a BlockInfo or Expiration to move that further in the future. Note that an height-based Duration and a time-based Expiration cannot be combined", + "oneOf": [ + { + "type": "object", + "required": [ + "height" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "Time in seconds", + "type": "object", + "required": [ + "time" + ], + "properties": { + "time": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "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": [ + "get_config" + ], + "properties": { + "get_config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_denom" + ], + "properties": { + "get_denom": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "claims" + ], + "properties": { + "claims": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "list_stakers" + ], + "properties": { + "list_stakers": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "active_threshold" + ], + "properties": { + "active_threshold": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_hooks" + ], + "properties": { + "get_hooks": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the voting power for an address at a given height.", + "type": "object", + "required": [ + "voting_power_at_height" + ], + "properties": { + "voting_power_at_height": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + }, + "height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the total voting power at a given block heigh.", + "type": "object", + "required": [ + "total_power_at_height" + ], + "properties": { + "total_power_at_height": { + "type": "object", + "properties": { + "height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the address of the DAO this module belongs to.", + "type": "object", + "required": [ + "dao" + ], + "properties": { + "dao": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns contract version info.", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "is_active" + ], + "properties": { + "is_active": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "migrate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "additionalProperties": false + }, + "sudo": null, + "responses": { + "active_threshold": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ActiveThresholdResponse", + "type": "object", + "properties": { + "active_threshold": { + "anyOf": [ + { + "$ref": "#/definitions/ActiveThreshold" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "ActiveThreshold": { + "description": "The threshold of tokens that must be staked in order for this voting module to be active. If this is not reached, this module will response to `is_active` queries with false and proposal modules which respect active thresholds will not allow the creation of proposals.", + "oneOf": [ + { + "description": "The absolute number of tokens that must be staked for the module to be active.", + "type": "object", + "required": [ + "absolute_count" + ], + "properties": { + "absolute_count": { + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "The percentage of tokens that must be staked for the module to be active. Computed as `staked / total_supply`.", + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "type": "object", + "required": [ + "percent" + ], + "properties": { + "percent": { + "$ref": "#/definitions/Decimal" + } + }, + "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" + }, + "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" + } + } + }, + "claims": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ClaimsResponse", + "type": "object", + "required": [ + "claims" + ], + "properties": { + "claims": { + "type": "array", + "items": { + "$ref": "#/definitions/Claim" + } + } + }, + "additionalProperties": false, + "definitions": { + "Claim": { + "type": "object", + "required": [ + "amount", + "release_at" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "release_at": { + "$ref": "#/definitions/Expiration" + } + }, + "additionalProperties": false + }, + "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" + } + ] + }, + "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" + } + } + }, + "dao": { + "$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" + }, + "get_config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Config", + "type": "object", + "properties": { + "manager": { + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "owner": { + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "unstaking_duration": { + "anyOf": [ + { + "$ref": "#/definitions/Duration" + }, + { + "type": "null" + } + ] + } + }, + "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" + }, + "Duration": { + "description": "Duration is a delta of time. You can add it to a BlockInfo or Expiration to move that further in the future. Note that an height-based Duration and a time-based Expiration cannot be combined", + "oneOf": [ + { + "type": "object", + "required": [ + "height" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "Time in seconds", + "type": "object", + "required": [ + "time" + ], + "properties": { + "time": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + } + } + }, + "get_denom": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "DenomResponse", + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + }, + "get_hooks": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GetHooksResponse", + "type": "object", + "required": [ + "hooks" + ], + "properties": { + "hooks": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "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 + } + } + }, + "is_active": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Boolean", + "type": "boolean" + }, + "list_stakers": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ListStakersResponse", + "type": "object", + "required": [ + "stakers" + ], + "properties": { + "stakers": { + "type": "array", + "items": { + "$ref": "#/definitions/StakerBalanceResponse" + } + } + }, + "additionalProperties": false, + "definitions": { + "StakerBalanceResponse": { + "type": "object", + "required": [ + "address", + "balance" + ], + "properties": { + "address": { + "type": "string" + }, + "balance": { + "$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" + } + } + }, + "total_power_at_height": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TotalPowerAtHeightResponse", + "type": "object", + "required": [ + "height", + "power" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "power": { + "$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" + } + } + }, + "voting_power_at_height": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "VotingPowerAtHeightResponse", + "type": "object", + "required": [ + "height", + "power" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "power": { + "$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" + } + } + } + } +} diff --git a/contracts/voting/dao-voting-token-factory-staked/src/contract.rs b/contracts/voting/dao-voting-token-factory-staked/src/contract.rs new file mode 100644 index 000000000..6d7040f33 --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/src/contract.rs @@ -0,0 +1,675 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; + +use cosmwasm_std::{ + coins, to_binary, BankMsg, BankQuery, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, + MessageInfo, Order, Reply, Response, StdResult, SubMsg, Uint128, Uint256, +}; +use cw2::set_contract_version; +use cw_controllers::ClaimsResponse; +use cw_storage_plus::Bound; +use cw_utils::{maybe_addr, must_pay, Duration}; +use dao_interface::state::Admin; +use dao_interface::voting::{ + IsActiveResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse, +}; +use dao_voting::threshold::ActiveThreshold; +use token_bindings::{TokenFactoryMsg, TokenFactoryQuery, TokenMsg, TokenQuerier}; + +use crate::error::ContractError; +use crate::hooks::{stake_hook_msgs, unstake_hook_msgs}; +use crate::msg::{ + ActiveThresholdResponse, DenomResponse, ExecuteMsg, GetHooksResponse, InstantiateMsg, + ListStakersResponse, MigrateMsg, QueryMsg, StakerBalanceResponse, TokenInfo, +}; +use crate::state::{ + Config, ACTIVE_THRESHOLD, CLAIMS, CONFIG, DAO, DENOM, HOOKS, MAX_CLAIMS, STAKED_BALANCES, + STAKED_TOTAL, TOKEN_INSTANTIATION_INFO, +}; + +pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-voting-native-staked"; +pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +// Settings for query pagination +const MAX_LIMIT: u32 = 30; +const DEFAULT_LIMIT: u32 = 10; + +const CREATE_DENOM_REPLY_ID: u64 = 0; + +// We multiply by this when calculating needed power for being active +// when using active threshold with percent +const PRECISION_FACTOR: u128 = 10u128.pow(9); + +fn validate_duration(duration: Option) -> Result<(), ContractError> { + if let Some(unstaking_duration) = duration { + match unstaking_duration { + Duration::Height(height) => { + if height == 0 { + return Err(ContractError::InvalidUnstakingDuration {}); + } + } + Duration::Time(time) => { + if time == 0 { + return Err(ContractError::InvalidUnstakingDuration {}); + } + } + } + } + Ok(()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result, ContractError> { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + let owner = msg + .owner + .as_ref() + .map(|owner| match owner { + Admin::Address { addr } => deps.api.addr_validate(addr), + Admin::CoreModule {} => Ok(info.sender.clone()), + }) + .transpose()?; + let manager = msg + .manager + .map(|manager| deps.api.addr_validate(&manager)) + .transpose()?; + + validate_duration(msg.unstaking_duration)?; + + let config = Config { + owner, + manager, + unstaking_duration: msg.unstaking_duration, + }; + + CONFIG.save(deps.storage, &config)?; + DAO.save(deps.storage, &info.sender)?; + + if let Some(active_threshold) = msg.active_threshold.as_ref() { + if let ActiveThreshold::Percentage { percent } = active_threshold { + if *percent > Decimal::percent(100) || *percent <= Decimal::percent(0) { + return Err(ContractError::InvalidActivePercentage {}); + } + } + ACTIVE_THRESHOLD.save(deps.storage, active_threshold)?; + } + + match msg.token_info { + TokenInfo::Existing { denom } => { + if let Some(ActiveThreshold::AbsoluteCount { count }) = msg.active_threshold { + assert_valid_absolute_count_threshold(deps.as_ref(), &denom, count)?; + } + + DENOM.save(deps.storage, &denom)?; + + Ok(Response::::new() + .add_attribute("action", "instantiate") + .add_attribute("token", "existing_token") + .add_attribute("token_denom", denom) + .add_attribute( + "owner", + config + .owner + .map(|a| a.to_string()) + .unwrap_or_else(|| "None".to_string()), + ) + .add_attribute( + "manager", + config + .manager + .map(|a| a.to_string()) + .unwrap_or_else(|| "None".to_string()), + )) + } + TokenInfo::New(token) => { + // TODO investigate how much validation we need to do + if token.subdenom.eq("") { + // TODO replace with token factory errors + return Err(ContractError::NothingToClaim {}); + } + + // Create Token Factory denom SubMsg + let create_denom_msg = SubMsg::reply_on_success( + TokenMsg::CreateDenom { + subdenom: token.clone().subdenom, + metadata: token.clone().metadata, + }, + CREATE_DENOM_REPLY_ID, + ); + + // Save new token info for use in reply + TOKEN_INSTANTIATION_INFO.save(deps.storage, &token)?; + + Ok(Response::::new() + .add_attribute("method", "create_denom") + .add_submessage(create_denom_msg)) + } + } +} + +pub fn assert_valid_absolute_count_threshold( + deps: Deps, + token_denom: &str, + count: Uint128, +) -> Result<(), ContractError> { + if count.is_zero() { + return Err(ContractError::ZeroActiveCount {}); + } + let supply: Coin = deps.querier.query_supply(token_denom.to_string())?; + if count > supply.amount { + return Err(ContractError::InvalidAbsoluteCount {}); + } + Ok(()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result, ContractError> { + match msg { + ExecuteMsg::Stake {} => execute_stake(deps, env, info), + ExecuteMsg::Unstake { amount } => execute_unstake(deps, env, info, amount), + ExecuteMsg::UpdateConfig { + owner, + manager, + duration, + } => execute_update_config(deps, info, owner, manager, duration), + ExecuteMsg::Claim {} => execute_claim(deps, env, info), + ExecuteMsg::UpdateActiveThreshold { new_threshold } => { + execute_update_active_threshold(deps, env, info, new_threshold) + } + ExecuteMsg::AddHook { addr } => execute_add_hook(deps, env, info, addr), + ExecuteMsg::RemoveHook { addr } => execute_remove_hook(deps, env, info, addr), + } +} + +pub fn execute_stake( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result, ContractError> { + let denom = DENOM.load(deps.storage)?; + let amount = must_pay(&info, &denom)?; + + STAKED_BALANCES.update( + deps.storage, + &info.sender, + env.block.height, + |balance| -> StdResult { Ok(balance.unwrap_or_default().checked_add(amount)?) }, + )?; + STAKED_TOTAL.update( + deps.storage, + env.block.height, + |total| -> StdResult { Ok(total.unwrap_or_default().checked_add(amount)?) }, + )?; + let hook_msgs = stake_hook_msgs(deps.storage, info.sender.clone(), amount)?; + + Ok(Response::::new() + .add_submessages(hook_msgs) + .add_attribute("action", "stake") + .add_attribute("amount", amount.to_string()) + .add_attribute("from", info.sender)) +} + +pub fn execute_unstake( + deps: DepsMut, + env: Env, + info: MessageInfo, + amount: Uint128, +) -> Result, ContractError> { + if amount.is_zero() { + return Err(ContractError::ZeroUnstake {}); + } + + STAKED_BALANCES.update( + deps.storage, + &info.sender, + env.block.height, + |balance| -> Result { + balance + .unwrap_or_default() + .checked_sub(amount) + .map_err(|_e| ContractError::InvalidUnstakeAmount {}) + }, + )?; + STAKED_TOTAL.update( + deps.storage, + env.block.height, + |total| -> Result { + total + .unwrap_or_default() + .checked_sub(amount) + .map_err(|_e| ContractError::InvalidUnstakeAmount {}) + }, + )?; + let hook_msgs = unstake_hook_msgs(deps.storage, info.sender.clone(), amount)?; + + let config = CONFIG.load(deps.storage)?; + let denom = DENOM.load(deps.storage)?; + match config.unstaking_duration { + None => { + let msg = CosmosMsg::Bank(BankMsg::Send { + to_address: info.sender.to_string(), + amount: coins(amount.u128(), denom), + }); + Ok(Response::::new() + .add_message(msg) + .add_submessages(hook_msgs) + .add_attribute("action", "unstake") + .add_attribute("from", info.sender) + .add_attribute("amount", amount) + .add_attribute("claim_duration", "None")) + } + Some(duration) => { + let outstanding_claims = CLAIMS.query_claims(deps.as_ref(), &info.sender)?.claims; + if outstanding_claims.len() >= MAX_CLAIMS as usize { + return Err(ContractError::TooManyClaims {}); + } + + CLAIMS.create_claim( + deps.storage, + &info.sender, + amount, + duration.after(&env.block), + )?; + Ok(Response::::new() + .add_submessages(hook_msgs) + .add_attribute("action", "unstake") + .add_attribute("from", info.sender) + .add_attribute("amount", amount) + .add_attribute("claim_duration", format!("{duration}"))) + } + } +} + +pub fn execute_update_config( + deps: DepsMut, + info: MessageInfo, + new_owner: Option, + new_manager: Option, + duration: Option, +) -> Result, ContractError> { + let mut config: Config = CONFIG.load(deps.storage)?; + if Some(info.sender.clone()) != config.owner && Some(info.sender.clone()) != config.manager { + return Err(ContractError::Unauthorized {}); + } + + let new_owner = new_owner + .map(|new_owner| deps.api.addr_validate(&new_owner)) + .transpose()?; + let new_manager = new_manager + .map(|new_manager| deps.api.addr_validate(&new_manager)) + .transpose()?; + + validate_duration(duration)?; + + if Some(info.sender) != config.owner && new_owner != config.owner { + return Err(ContractError::OnlyOwnerCanChangeOwner {}); + }; + + config.owner = new_owner; + config.manager = new_manager; + + config.unstaking_duration = duration; + + CONFIG.save(deps.storage, &config)?; + Ok(Response::::new() + .add_attribute("action", "update_config") + .add_attribute( + "owner", + config + .owner + .map(|a| a.to_string()) + .unwrap_or_else(|| "None".to_string()), + ) + .add_attribute( + "manager", + config + .manager + .map(|a| a.to_string()) + .unwrap_or_else(|| "None".to_string()), + )) +} + +pub fn execute_claim( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result, ContractError> { + let release = CLAIMS.claim_tokens(deps.storage, &info.sender, &env.block, None)?; + if release.is_zero() { + return Err(ContractError::NothingToClaim {}); + } + + let denom = DENOM.load(deps.storage)?; + let msg = CosmosMsg::::Bank(BankMsg::Send { + to_address: info.sender.to_string(), + amount: coins(release.u128(), denom), + }); + + Ok(Response::::new() + .add_message(msg) + .add_attribute("action", "claim") + .add_attribute("from", info.sender) + .add_attribute("amount", release)) +} + +pub fn execute_update_active_threshold( + deps: DepsMut, + _env: Env, + info: MessageInfo, + new_active_threshold: Option, +) -> Result, ContractError> { + let dao = DAO.load(deps.storage)?; + if info.sender != dao { + return Err(ContractError::Unauthorized {}); + } + + if let Some(active_threshold) = new_active_threshold { + match active_threshold { + ActiveThreshold::Percentage { percent } => { + if percent > Decimal::percent(100) || percent.is_zero() { + return Err(ContractError::InvalidActivePercentage {}); + } + } + ActiveThreshold::AbsoluteCount { count } => { + let denom = DENOM.load(deps.storage)?; + assert_valid_absolute_count_threshold(deps.as_ref(), &denom, count)?; + } + } + ACTIVE_THRESHOLD.save(deps.storage, &active_threshold)?; + } else { + ACTIVE_THRESHOLD.remove(deps.storage); + } + + Ok(Response::::new().add_attribute("action", "update_active_threshold")) +} + +pub fn execute_add_hook( + deps: DepsMut, + _env: Env, + info: MessageInfo, + addr: String, +) -> Result, ContractError> { + let config: Config = CONFIG.load(deps.storage)?; + if Some(info.sender.clone()) != config.owner && Some(info.sender) != config.manager { + return Err(ContractError::Unauthorized {}); + } + + let hook = deps.api.addr_validate(&addr)?; + HOOKS.add_hook(deps.storage, hook)?; + Ok(Response::::new() + .add_attribute("action", "add_hook") + .add_attribute("hook", addr)) +} + +pub fn execute_remove_hook( + deps: DepsMut, + _env: Env, + info: MessageInfo, + addr: String, +) -> Result, ContractError> { + let config: Config = CONFIG.load(deps.storage)?; + if Some(info.sender.clone()) != config.owner && Some(info.sender) != config.manager { + return Err(ContractError::Unauthorized {}); + } + + let hook = deps.api.addr_validate(&addr)?; + HOOKS.remove_hook(deps.storage, hook)?; + Ok(Response::::new() + .add_attribute("action", "remove_hook") + .add_attribute("hook", addr)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::VotingPowerAtHeight { address, height } => { + to_binary(&query_voting_power_at_height(deps, env, address, height)?) + } + QueryMsg::TotalPowerAtHeight { height } => { + to_binary(&query_total_power_at_height(deps, env, height)?) + } + QueryMsg::Info {} => query_info(deps), + QueryMsg::Dao {} => query_dao(deps), + QueryMsg::Claims { address } => to_binary(&query_claims(deps, address)?), + QueryMsg::GetConfig {} => to_binary(&CONFIG.load(deps.storage)?), + QueryMsg::GetDenom {} => to_binary(&DenomResponse { + denom: DENOM.load(deps.storage)?, + }), + QueryMsg::ListStakers { start_after, limit } => { + query_list_stakers(deps, start_after, limit) + } + QueryMsg::IsActive {} => query_is_active(deps), + QueryMsg::ActiveThreshold {} => query_active_threshold(deps), + QueryMsg::GetHooks {} => to_binary(&query_hooks(deps)?), + } +} + +pub fn query_voting_power_at_height( + deps: Deps, + env: Env, + address: String, + height: Option, +) -> StdResult { + let height = height.unwrap_or(env.block.height); + let address = deps.api.addr_validate(&address)?; + let power = STAKED_BALANCES + .may_load_at_height(deps.storage, &address, height)? + .unwrap_or_default(); + Ok(VotingPowerAtHeightResponse { power, height }) +} + +pub fn query_total_power_at_height( + deps: Deps, + env: Env, + height: Option, +) -> StdResult { + let height = height.unwrap_or(env.block.height); + let power = STAKED_TOTAL + .may_load_at_height(deps.storage, height)? + .unwrap_or_default(); + Ok(TotalPowerAtHeightResponse { power, height }) +} + +pub fn query_info(deps: Deps) -> StdResult { + let info = cw2::get_contract_version(deps.storage)?; + to_binary(&dao_interface::voting::InfoResponse { info }) +} + +pub fn query_dao(deps: Deps) -> StdResult { + let dao = DAO.load(deps.storage)?; + to_binary(&dao) +} + +pub fn query_claims(deps: Deps, address: String) -> StdResult { + CLAIMS.query_claims(deps, &deps.api.addr_validate(&address)?) +} + +pub fn query_list_stakers( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let addr = maybe_addr(deps.api, start_after)?; + let start = addr.as_ref().map(Bound::exclusive); + + let stakers = STAKED_BALANCES + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|item| { + item.map(|(address, balance)| StakerBalanceResponse { + address: address.into_string(), + balance, + }) + }) + .collect::>()?; + + to_binary(&ListStakersResponse { stakers }) +} + +pub fn query_is_active(deps: Deps) -> StdResult { + let threshold = ACTIVE_THRESHOLD.may_load(deps.storage)?; + if let Some(threshold) = threshold { + let denom = DENOM.load(deps.storage)?; + let actual_power = STAKED_TOTAL.may_load(deps.storage)?.unwrap_or_default(); + match threshold { + ActiveThreshold::AbsoluteCount { count } => to_binary(&IsActiveResponse { + active: actual_power >= count, + }), + ActiveThreshold::Percentage { percent } => { + // percent is bounded between [0, 100]. decimal + // represents percents in u128 terms as p * + // 10^15. this bounds percent between [0, 10^17]. + // + // total_potential_power is bounded between [0, 2^128] + // as it tracks the balances of a cw20 token which has + // a max supply of 2^128. + // + // with our precision factor being 10^9: + // + // total_power <= 2^128 * 10^9 <= 2^256 + // + // so we're good to put that in a u256. + // + // multiply_ratio promotes to a u512 under the hood, + // so it won't overflow, multiplying by a percent less + // than 100 is gonna make something the same size or + // smaller, applied + 10^9 <= 2^128 * 10^9 + 10^9 <= + // 2^256, so the top of the round won't overflow, and + // rounding is rounding down, so the whole thing can + // be safely unwrapped at the end of the day thank you + // for coming to my ted talk. + let total_potential_power: cosmwasm_std::SupplyResponse = + deps.querier + .query(&cosmwasm_std::QueryRequest::Bank(BankQuery::Supply { + denom, + }))?; + let total_power = total_potential_power + .amount + .amount + .full_mul(PRECISION_FACTOR); + // under the hood decimals are `atomics / 10^decimal_places`. + // cosmwasm doesn't give us a Decimal * Uint256 + // implementation so we take the decimal apart and + // multiply by the fraction. + let applied = total_power.multiply_ratio( + percent.atomics(), + Uint256::from(10u64).pow(percent.decimal_places()), + ); + let rounded = (applied + Uint256::from(PRECISION_FACTOR) - Uint256::from(1u128)) + / Uint256::from(PRECISION_FACTOR); + let count: Uint128 = rounded.try_into().unwrap(); + to_binary(&IsActiveResponse { + active: actual_power >= count, + }) + } + } + } else { + to_binary(&IsActiveResponse { active: true }) + } +} + +pub fn query_active_threshold(deps: Deps) -> StdResult { + to_binary(&ActiveThresholdResponse { + active_threshold: ACTIVE_THRESHOLD.may_load(deps.storage)?, + }) +} + +pub fn query_hooks(deps: Deps) -> StdResult { + Ok(GetHooksResponse { + hooks: HOOKS.query_hooks(deps)?.hooks, + }) +} + +#[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 { + CREATE_DENOM_REPLY_ID => { + // Load info for new token and the DAO's address + let token = TOKEN_INSTANTIATION_INFO.load(deps.storage)?; + let dao = DAO.load(deps.storage)?; + + // Get the new token factory denom + let querier = TokenQuerier::new(&deps.querier); + let denom = querier + .full_denom(env.contract.address.to_string(), token.subdenom)? + .denom; + DENOM.save(deps.storage, &denom)?; + + let mut mint_msgs: Vec = vec![]; + + // Check supply is greater than zero + let initial_supply = token + .initial_balances + .iter() + .fold(Uint128::zero(), |p, n| p + n.amount); + + // Cannot instantiate with no initial token owners because it would + // immediately lock the DAO. + if initial_supply.is_zero() { + return Err(ContractError::InitialBalancesError {}); + } + + // Mint initial balances + token.initial_balances.iter().for_each(|b| { + mint_msgs.push(TokenMsg::MintTokens { + denom: denom.clone(), + amount: b.amount, + mint_to_address: b.mint_to_address.clone(), + }) + }); + + // Add initial DAO balance to initial_balances if nonzero. + if let Some(initial_dao_balance) = token.initial_dao_balance { + if !initial_dao_balance.is_zero() { + mint_msgs.push(TokenMsg::MintTokens { + denom: denom.clone(), + amount: initial_dao_balance, + mint_to_address: dao.to_string(), + }) + } + } + + // Clear up unneeded storage. + TOKEN_INSTANTIATION_INFO.remove(deps.storage); + + // Update token factory denom admin to be the DAO + let update_token_admin_msg = TokenMsg::ChangeAdmin { + denom, + new_admin_address: dao.to_string(), + }; + + // TODO what other info do we want here? + Ok(Response::new() + .add_messages(mint_msgs) + .add_message(update_token_admin_msg)) + } + _ => Err(ContractError::UnknownReplyId { id: msg.id }), + } +} diff --git a/contracts/voting/dao-voting-token-factory-staked/src/error.rs b/contracts/voting/dao-voting-token-factory-staked/src/error.rs new file mode 100644 index 000000000..148c1ac26 --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/src/error.rs @@ -0,0 +1,60 @@ +use cosmwasm_std::StdError; +use cw_utils::{ParseReplyError, PaymentError}; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error(transparent)] + Std(#[from] StdError), + + #[error(transparent)] + PaymentError(#[from] PaymentError), + + #[error(transparent)] + ParseReplyError(#[from] ParseReplyError), + + #[error(transparent)] + HookError(#[from] cw_hooks::HookError), + + #[error("Got a submessage reply with unknown id: {id}")] + UnknownReplyId { id: u64 }, + + #[error("Token factory core contract instantiate error")] + TokenFactoryCoreInstantiateError {}, + + #[error("Initial governance token balances must not be empty")] + InitialBalancesError {}, + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Invalid unstaking duration, unstaking duration cannot be 0")] + InvalidUnstakingDuration {}, + + #[error("Nothing to claim")] + NothingToClaim {}, + + #[error("Too many outstanding claims. Claim some tokens before unstaking more.")] + TooManyClaims {}, + + #[error("Only owner can change owner")] + OnlyOwnerCanChangeOwner {}, + + #[error("Can only unstake less than or equal to the amount you have staked")] + InvalidUnstakeAmount {}, + + #[error("Amount being unstaked must be non-zero")] + ZeroUnstake {}, + + #[error("Active threshold percentage must be greater than 0 and less than 1")] + InvalidActivePercentage {}, + + #[error("Active threshold count must be greater than zero")] + ZeroActiveCount {}, + + #[error("Absolute count threshold cannot be greater than the total token supply")] + InvalidAbsoluteCount {}, + + #[error("Cannot change the contract's token after it has been set")] + DuplicateToken {}, +} diff --git a/contracts/voting/dao-voting-native-staked/src/hooks.rs b/contracts/voting/dao-voting-token-factory-staked/src/hooks.rs similarity index 100% rename from contracts/voting/dao-voting-native-staked/src/hooks.rs rename to contracts/voting/dao-voting-token-factory-staked/src/hooks.rs diff --git a/contracts/voting/dao-voting-token-factory-staked/src/lib.rs b/contracts/voting/dao-voting-token-factory-staked/src/lib.rs new file mode 100644 index 000000000..9d62fa3a3 --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/src/lib.rs @@ -0,0 +1,14 @@ +#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] + +pub mod contract; +mod error; +pub mod hooks; +pub mod msg; +pub mod state; + +// #[cfg(test)] +// mod test_tube; +#[cfg(test)] +mod tests; + +pub use crate::error::ContractError; diff --git a/contracts/voting/dao-voting-token-factory-staked/src/msg.rs b/contracts/voting/dao-voting-token-factory-staked/src/msg.rs new file mode 100644 index 000000000..f9e984d52 --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/src/msg.rs @@ -0,0 +1,123 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Uint128; +use cw_utils::Duration; +use dao_dao_macros::{active_query, voting_module_query}; +use dao_interface::state::Admin; +use dao_voting::threshold::ActiveThreshold; +use token_bindings::Metadata; + +#[cw_serde] +pub struct InitialBalance { + pub amount: Uint128, + pub mint_to_address: String, +} + +#[cw_serde] +pub struct NewTokenInfo { + pub subdenom: String, + pub metadata: Option, + pub initial_balances: Vec, + pub initial_dao_balance: Option, +} + +#[cw_serde] +pub enum TokenInfo { + Existing { + /// Token denom e.g. ujuno, or some ibc denom. + denom: String, + }, + New(NewTokenInfo), +} + +#[cw_serde] +pub struct InstantiateMsg { + // TODO replace with cw-ownable + // Owner can update all configs including changing the owner. This will generally be a DAO. + pub owner: Option, + // Manager can update all configs except changing the owner. This will generally be an operations multisig for a DAO. + pub manager: Option, + // New or existing native token to use for voting power. + pub token_info: TokenInfo, + // How long until the tokens become liquid again + pub unstaking_duration: Option, + /// The number or percentage of tokens that must be staked + /// for the DAO to be active + pub active_threshold: Option, +} + +#[cw_serde] +pub enum ExecuteMsg { + Stake {}, + Unstake { + amount: Uint128, + }, + UpdateConfig { + owner: Option, + manager: Option, + duration: Option, + }, + Claim {}, + /// Sets the active threshold to a new value. Only the + /// instantiator of this contract (a DAO most likely) may call this + /// method. + UpdateActiveThreshold { + new_threshold: Option, + }, + AddHook { + addr: String, + }, + RemoveHook { + addr: String, + }, +} + +#[voting_module_query] +#[active_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(crate::state::Config)] + GetConfig {}, + #[returns(DenomResponse)] + GetDenom {}, + #[returns(cw_controllers::ClaimsResponse)] + Claims { address: String }, + #[returns(ListStakersResponse)] + ListStakers { + start_after: Option, + limit: Option, + }, + #[returns(ActiveThresholdResponse)] + ActiveThreshold {}, + #[returns(GetHooksResponse)] + GetHooks {}, +} + +#[cw_serde] +pub struct MigrateMsg {} + +#[cw_serde] +pub struct ListStakersResponse { + pub stakers: Vec, +} + +#[cw_serde] +pub struct StakerBalanceResponse { + pub address: String, + pub balance: Uint128, +} + +#[cw_serde] +pub struct DenomResponse { + pub denom: String, +} + +#[cw_serde] +pub struct ActiveThresholdResponse { + pub active_threshold: Option, +} + +#[cw_serde] +pub struct GetHooksResponse { + pub hooks: Vec, +} diff --git a/contracts/voting/dao-voting-token-factory-staked/src/state.rs b/contracts/voting/dao-voting-token-factory-staked/src/state.rs new file mode 100644 index 000000000..fb45ef473 --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/src/state.rs @@ -0,0 +1,56 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Uint128}; +use cw_controllers::Claims; +use cw_hooks::Hooks; +use cw_storage_plus::{Item, SnapshotItem, SnapshotMap, Strategy}; +use cw_utils::Duration; +use dao_voting::threshold::ActiveThreshold; + +use crate::msg::NewTokenInfo; + +#[cw_serde] +pub struct Config { + // TODO use cw-ownable + pub owner: Option, + pub manager: Option, + pub unstaking_duration: Option, +} + +/// The configuration of this voting contract +pub const CONFIG: Item = Item::new("config"); + +/// The native denom associated with this contract +pub const DENOM: Item = Item::new("denom"); + +/// The address of the DAO this voting contract is connected to +pub const DAO: Item = Item::new("dao"); + +/// Keeps track of staked balances by address over time +pub const STAKED_BALANCES: SnapshotMap<&Addr, Uint128> = SnapshotMap::new( + "staked_balances", + "staked_balance__checkpoints", + "staked_balance__changelog", + Strategy::EveryBlock, +); + +/// Keeps track of staked total over time +pub const STAKED_TOTAL: SnapshotItem = SnapshotItem::new( + "total_staked", + "total_staked__checkpoints", + "total_staked__changelog", + Strategy::EveryBlock, +); + +/// The maximum number of claims that may be outstanding. +pub const MAX_CLAIMS: u64 = 100; + +pub const CLAIMS: Claims = Claims::new("claims"); + +/// The minimum amount of staked tokens for the DAO to be active +pub const ACTIVE_THRESHOLD: Item = Item::new("active_threshold"); + +/// Hooks to contracts that will receive staking and unstaking messages +pub const HOOKS: Hooks = Hooks::new("hooks"); + +/// Temporarily holds token_instantiation_info when creating a new Token Factory denom +pub const TOKEN_INSTANTIATION_INFO: Item = Item::new("token_instantiation_info"); diff --git a/contracts/voting/dao-voting-token-factory-staked/src/test_tube.rs b/contracts/voting/dao-voting-token-factory-staked/src/test_tube.rs new file mode 100644 index 000000000..5511cefc3 --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/src/test_tube.rs @@ -0,0 +1,13 @@ +// TODO +// use cosmwasm_std::Coin; +// use osmosis_test_tube::OsmosisTestApp; + +// #[test] +// fn test_tube() { +// let app = OsmosisTestApp::new(); + +// let account = app.init_account(&[ +// Coin::new(1_000_000_000_000, "uatom"), +// Coin::new(1_000_000_000_000, "uosmo"), +// ]); +// } diff --git a/contracts/voting/dao-voting-token-factory-staked/src/tests.rs b/contracts/voting/dao-voting-token-factory-staked/src/tests.rs new file mode 100644 index 000000000..39dfc9468 --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/src/tests.rs @@ -0,0 +1,1618 @@ +use std::marker::PhantomData; + +use crate::contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}; +use crate::msg::{ + ActiveThresholdResponse, DenomResponse, ExecuteMsg, GetHooksResponse, InitialBalance, + InstantiateMsg, ListStakersResponse, MigrateMsg, NewTokenInfo, QueryMsg, StakerBalanceResponse, + TokenInfo, +}; +use crate::state::Config; +use crate::ContractError; +use cosmwasm_std::testing::{mock_env, MockApi, MockQuerier, MockStorage}; +use cosmwasm_std::{coins, Addr, Coin, Decimal, OwnedDeps, Uint128}; +use cw_controllers::ClaimsResponse; +use cw_multi_test::{ + next_block, AppResponse, BankSudo, Contract, ContractWrapper, Executor, SudoMsg, +}; +use cw_utils::Duration; +use dao_interface::state::Admin; +use dao_interface::voting::{ + InfoResponse, IsActiveResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse, +}; +use dao_voting::threshold::ActiveThreshold; +use token_bindings::{Metadata, TokenFactoryMsg, TokenFactoryQuery}; +use token_bindings_test::TokenFactoryApp as App; + +const DAO_ADDR: &str = "dao"; +const ADDR1: &str = "addr1"; +const ADDR2: &str = "addr2"; +const DENOM: &str = "ujuno"; +const INVALID_DENOM: &str = "uinvalid"; +const ODD_DENOM: &str = "uodd"; + +fn staking_contract() -> Box> { + let contract = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + ) + .with_reply(crate::contract::reply); + Box::new(contract) +} + +fn mock_app() -> App { + let mut app = App::new(); + app.sudo(SudoMsg::Bank(BankSudo::Mint { + to_address: DAO_ADDR.to_string(), + amount: vec![ + Coin { + denom: DENOM.to_string(), + amount: Uint128::new(10000), + }, + Coin { + denom: INVALID_DENOM.to_string(), + amount: Uint128::new(10000), + }, + ], + })) + .unwrap(); + app.sudo(SudoMsg::Bank(BankSudo::Mint { + to_address: ADDR1.to_string(), + amount: vec![ + Coin { + denom: DENOM.to_string(), + amount: Uint128::new(10000), + }, + Coin { + denom: INVALID_DENOM.to_string(), + amount: Uint128::new(10000), + }, + Coin { + denom: ODD_DENOM.to_string(), + amount: Uint128::new(5), + }, + ], + })) + .unwrap(); + app.sudo(SudoMsg::Bank(BankSudo::Mint { + to_address: ADDR2.to_string(), + amount: vec![ + Coin { + denom: DENOM.to_string(), + amount: Uint128::new(10000), + }, + Coin { + denom: INVALID_DENOM.to_string(), + amount: Uint128::new(10000), + }, + ], + })) + .unwrap(); + app.update_block(next_block); + app +} + +fn instantiate_staking(app: &mut App, staking_id: u64, msg: InstantiateMsg) -> Addr { + app.instantiate_contract( + staking_id, + Addr::unchecked(DAO_ADDR), + &msg, + &[], + "Staking", + None, + ) + .unwrap() +} + +fn instantiate_staking_error(app: &mut App, staking_id: u64, msg: InstantiateMsg) -> ContractError { + app.instantiate_contract( + staking_id, + Addr::unchecked(DAO_ADDR), + &msg, + &[], + "Staking", + None, + ) + .unwrap_err() + .downcast() + .unwrap() +} + +fn stake_tokens( + app: &mut App, + staking_addr: Addr, + sender: &str, + amount: u128, + denom: &str, +) -> anyhow::Result { + app.execute_contract( + Addr::unchecked(sender), + staking_addr, + &ExecuteMsg::Stake {}, + &coins(amount, denom), + ) +} + +fn unstake_tokens( + app: &mut App, + staking_addr: Addr, + sender: &str, + amount: u128, +) -> anyhow::Result { + app.execute_contract( + Addr::unchecked(sender), + staking_addr, + &ExecuteMsg::Unstake { + amount: Uint128::new(amount), + }, + &[], + ) +} + +fn claim(app: &mut App, staking_addr: Addr, sender: &str) -> anyhow::Result { + app.execute_contract( + Addr::unchecked(sender), + staking_addr, + &ExecuteMsg::Claim {}, + &[], + ) +} + +fn update_config( + app: &mut App, + staking_addr: Addr, + sender: &str, + owner: Option, + manager: Option, + duration: Option, +) -> anyhow::Result { + app.execute_contract( + Addr::unchecked(sender), + staking_addr, + &ExecuteMsg::UpdateConfig { + owner, + manager, + duration, + }, + &[], + ) +} + +fn get_voting_power_at_height( + app: &mut App, + staking_addr: Addr, + address: String, + height: Option, +) -> VotingPowerAtHeightResponse { + app.wrap() + .query_wasm_smart( + staking_addr, + &QueryMsg::VotingPowerAtHeight { address, height }, + ) + .unwrap() +} + +fn get_total_power_at_height( + app: &mut App, + staking_addr: Addr, + height: Option, +) -> TotalPowerAtHeightResponse { + app.wrap() + .query_wasm_smart(staking_addr, &QueryMsg::TotalPowerAtHeight { height }) + .unwrap() +} + +fn get_config(app: &mut App, staking_addr: Addr) -> Config { + app.wrap() + .query_wasm_smart(staking_addr, &QueryMsg::GetConfig {}) + .unwrap() +} + +fn get_denom(app: &mut App, staking_addr: Addr) -> DenomResponse { + app.wrap() + .query_wasm_smart(staking_addr, &QueryMsg::GetDenom {}) + .unwrap() +} + +fn get_claims(app: &mut App, staking_addr: Addr, address: String) -> ClaimsResponse { + app.wrap() + .query_wasm_smart(staking_addr, &QueryMsg::Claims { address }) + .unwrap() +} + +fn get_balance(app: &mut App, address: &str, denom: &str) -> Uint128 { + app.wrap().query_balance(address, denom).unwrap().amount +} + +#[test] +fn test_instantiate_existing() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + // Populated fields + let _addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::Address { + addr: DAO_ADDR.to_string(), + }), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Non populated fields + let _addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: None, + manager: None, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: None, + active_threshold: None, + }, + ); +} + +#[test] +fn test_instantiate_new_denom() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + + // Populated fields + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::New(NewTokenInfo { + subdenom: DENOM.to_string(), + metadata: Some(Metadata { + description: Some("Awesome token, get it now!".to_string()), + denom_units: vec![], + base: None, + display: Some(DENOM.to_string()), + name: Some(DENOM.to_string()), + symbol: Some(DENOM.to_string()), + }), + initial_balances: vec![InitialBalance { + amount: Uint128::new(100), + mint_to_address: ADDR1.to_string(), + }], + initial_dao_balance: Some(Uint128::new(900)), + }), + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + let denom = get_denom(&mut app, addr.clone()); + + assert_eq!(denom.denom, format!("factory/{}/{}", addr, DENOM)); + + // Non populated fields + instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: None, + manager: None, + token_info: TokenInfo::New(NewTokenInfo { + subdenom: DENOM.to_string(), + metadata: Some(Metadata { + description: Some("Awesome token, get it now!".to_string()), + denom_units: vec![], + base: None, + display: Some(DENOM.to_string()), + name: Some(DENOM.to_string()), + symbol: Some(DENOM.to_string()), + }), + initial_balances: vec![InitialBalance { + amount: Uint128::new(100), + mint_to_address: ADDR1.to_string(), + }], + initial_dao_balance: None, + }), + unstaking_duration: None, + active_threshold: None, + }, + ); + + // No initial balances except DAO. + let err = instantiate_staking_error( + &mut app, + staking_id, + InstantiateMsg { + owner: None, + manager: None, + token_info: TokenInfo::New(NewTokenInfo { + subdenom: DENOM.to_string(), + metadata: Some(Metadata { + description: Some("Awesome token, get it now!".to_string()), + denom_units: vec![], + base: None, + display: Some(DENOM.to_string()), + name: Some(DENOM.to_string()), + symbol: Some(DENOM.to_string()), + }), + initial_balances: vec![], + initial_dao_balance: None, + }), + unstaking_duration: None, + active_threshold: None, + }, + ); + assert_eq!(err, ContractError::InitialBalancesError {}); +} + +#[test] +fn test_instantiate_dao_owner() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + // Populated fields + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + let config = get_config(&mut app, addr); + + assert_eq!(config.owner, Some(Addr::unchecked(DAO_ADDR))) +} + +#[test] +#[should_panic(expected = "Invalid unstaking duration, unstaking duration cannot be 0")] +fn test_instantiate_invalid_unstaking_duration() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + // Populated fields + let _addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::Address { + addr: DAO_ADDR.to_string(), + }), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(0)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(1), + }), + }, + ); + + // Non populated fields + let _addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: None, + manager: None, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: None, + active_threshold: None, + }, + ); +} + +#[test] +#[should_panic(expected = "Must send reserve token 'ujuno'")] +fn test_stake_invalid_denom() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Try and stake an invalid denom + stake_tokens(&mut app, addr, ADDR1, 100, INVALID_DENOM).unwrap(); +} + +#[test] +fn test_stake_valid_denom() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Try and stake an valid denom + stake_tokens(&mut app, addr, ADDR1, 100, DENOM).unwrap(); + app.update_block(next_block); +} + +#[test] +fn test_stake_new_denom() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::New(NewTokenInfo { + subdenom: DENOM.to_string(), + metadata: Some(Metadata { + description: Some("Awesome token, get it now!".to_string()), + denom_units: vec![], + base: None, + display: Some(DENOM.to_string()), + name: Some(DENOM.to_string()), + symbol: Some(DENOM.to_string()), + }), + initial_balances: vec![InitialBalance { + amount: Uint128::new(100), + mint_to_address: ADDR1.to_string(), + }], + initial_dao_balance: Some(Uint128::new(900)), + }), + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Try and stake a valid denom + let denom = get_denom(&mut app, addr.clone()).denom; + stake_tokens(&mut app, addr, ADDR1, 100, &denom).unwrap(); + app.update_block(next_block); +} + +#[test] +#[should_panic(expected = "Can only unstake less than or equal to the amount you have staked")] +fn test_unstake_none_staked() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + unstake_tokens(&mut app, addr, ADDR1, 100).unwrap(); +} + +#[test] +#[should_panic(expected = "Amount being unstaked must be non-zero")] +fn test_unstake_zero_tokens() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + unstake_tokens(&mut app, addr, ADDR1, 0).unwrap(); +} + +#[test] +#[should_panic(expected = "Can only unstake less than or equal to the amount you have staked")] +fn test_unstake_invalid_balance() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Stake some tokens + stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap(); + app.update_block(next_block); + + // Try and unstake too many + unstake_tokens(&mut app, addr, ADDR1, 200).unwrap(); +} + +#[test] +fn test_unstake() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Stake some tokens + stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap(); + app.update_block(next_block); + + // Unstake some + unstake_tokens(&mut app, addr.clone(), ADDR1, 75).unwrap(); + + // Query claims + let claims = get_claims(&mut app, addr.clone(), ADDR1.to_string()); + assert_eq!(claims.claims.len(), 1); + app.update_block(next_block); + + // Unstake the rest + unstake_tokens(&mut app, addr.clone(), ADDR1, 25).unwrap(); + + // Query claims + let claims = get_claims(&mut app, addr, ADDR1.to_string()); + assert_eq!(claims.claims.len(), 2); +} + +#[test] +fn test_unstake_no_unstaking_duration() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: None, + active_threshold: None, + }, + ); + + // Stake some tokens + stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap(); + app.update_block(next_block); + + // Unstake some tokens + unstake_tokens(&mut app, addr.clone(), ADDR1, 75).unwrap(); + + app.update_block(next_block); + + let balance = get_balance(&mut app, ADDR1, DENOM); + // 10000 (initial bal) - 100 (staked) + 75 (unstaked) = 9975 + assert_eq!(balance, Uint128::new(9975)); + + // Unstake the rest + unstake_tokens(&mut app, addr, ADDR1, 25).unwrap(); + + let balance = get_balance(&mut app, ADDR1, DENOM); + // 10000 (initial bal) - 100 (staked) + 75 (unstaked 1) + 25 (unstaked 2) = 10000 + assert_eq!(balance, Uint128::new(10000)) +} + +#[test] +#[should_panic(expected = "Nothing to claim")] +fn test_claim_no_claims() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + claim(&mut app, addr, ADDR1).unwrap(); +} + +#[test] +#[should_panic(expected = "Nothing to claim")] +fn test_claim_claim_not_reached() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Stake some tokens + stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap(); + app.update_block(next_block); + + // Unstake them to create the claims + unstake_tokens(&mut app, addr.clone(), ADDR1, 100).unwrap(); + app.update_block(next_block); + + // We have a claim but it isnt reached yet so this will still fail + claim(&mut app, addr, ADDR1).unwrap(); +} + +#[test] +fn test_claim() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Stake some tokens + stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap(); + app.update_block(next_block); + + // Unstake some to create the claims + unstake_tokens(&mut app, addr.clone(), ADDR1, 75).unwrap(); + app.update_block(|b| { + b.height += 5; + b.time = b.time.plus_seconds(25); + }); + + // Claim + claim(&mut app, addr.clone(), ADDR1).unwrap(); + + // Query balance + let balance = get_balance(&mut app, ADDR1, DENOM); + // 10000 (initial bal) - 100 (staked) + 75 (unstaked) = 9975 + assert_eq!(balance, Uint128::new(9975)); + + // Unstake the rest + unstake_tokens(&mut app, addr.clone(), ADDR1, 25).unwrap(); + app.update_block(|b| { + b.height += 10; + b.time = b.time.plus_seconds(50); + }); + + // Claim + claim(&mut app, addr, ADDR1).unwrap(); + + // Query balance + let balance = get_balance(&mut app, ADDR1, DENOM); + // 10000 (initial bal) - 100 (staked) + 75 (unstaked 1) + 25 (unstaked 2) = 10000 + assert_eq!(balance, Uint128::new(10000)); +} + +#[test] +#[should_panic(expected = "Unauthorized")] +fn test_update_config_invalid_sender() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // From ADDR2, so not owner or manager + update_config( + &mut app, + addr, + ADDR2, + Some(ADDR1.to_string()), + Some(DAO_ADDR.to_string()), + Some(Duration::Height(10)), + ) + .unwrap(); +} + +#[test] +#[should_panic(expected = "Only owner can change owner")] +fn test_update_config_non_owner_changes_owner() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // ADDR1 is the manager so cannot change the owner + update_config(&mut app, addr, ADDR1, Some(ADDR2.to_string()), None, None).unwrap(); +} + +#[test] +fn test_update_config_as_owner() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Swap owner and manager, change duration + update_config( + &mut app, + addr.clone(), + DAO_ADDR, + Some(ADDR1.to_string()), + Some(DAO_ADDR.to_string()), + Some(Duration::Height(10)), + ) + .unwrap(); + + let config = get_config(&mut app, addr); + assert_eq!( + Config { + owner: Some(Addr::unchecked(ADDR1)), + manager: Some(Addr::unchecked(DAO_ADDR)), + unstaking_duration: Some(Duration::Height(10)), + }, + config + ); +} + +#[test] +fn test_update_config_as_manager() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Change duration and manager as manager cannot change owner + update_config( + &mut app, + addr.clone(), + ADDR1, + Some(DAO_ADDR.to_string()), + Some(ADDR2.to_string()), + Some(Duration::Height(10)), + ) + .unwrap(); + + let config = get_config(&mut app, addr); + assert_eq!( + Config { + owner: Some(Addr::unchecked(DAO_ADDR)), + manager: Some(Addr::unchecked(ADDR2)), + unstaking_duration: Some(Duration::Height(10)), + }, + config + ); +} + +#[test] +#[should_panic(expected = "Invalid unstaking duration, unstaking duration cannot be 0")] +fn test_update_config_invalid_duration() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Change duration and manager as manager cannot change owner + update_config( + &mut app, + addr, + ADDR1, + Some(DAO_ADDR.to_string()), + Some(ADDR2.to_string()), + Some(Duration::Height(0)), + ) + .unwrap(); +} + +#[test] +fn test_query_dao() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + let msg = QueryMsg::Dao {}; + let dao: Addr = app.wrap().query_wasm_smart(addr, &msg).unwrap(); + assert_eq!(dao, Addr::unchecked(DAO_ADDR)); +} + +#[test] +fn test_query_info() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + let msg = QueryMsg::Info {}; + let resp: InfoResponse = app.wrap().query_wasm_smart(addr, &msg).unwrap(); + assert_eq!(resp.info.contract, "crates.io:dao-voting-native-staked"); +} + +#[test] +fn test_query_claims() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + let claims = get_claims(&mut app, addr.clone(), ADDR1.to_string()); + assert_eq!(claims.claims.len(), 0); + + // Stake some tokens + stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap(); + app.update_block(next_block); + + // Unstake some tokens + unstake_tokens(&mut app, addr.clone(), ADDR1, 25).unwrap(); + app.update_block(next_block); + + let claims = get_claims(&mut app, addr.clone(), ADDR1.to_string()); + assert_eq!(claims.claims.len(), 1); + + unstake_tokens(&mut app, addr.clone(), ADDR1, 25).unwrap(); + app.update_block(next_block); + + let claims = get_claims(&mut app, addr, ADDR1.to_string()); + assert_eq!(claims.claims.len(), 2); +} + +#[test] +fn test_query_get_config() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + let config = get_config(&mut app, addr); + assert_eq!( + config, + Config { + owner: Some(Addr::unchecked(DAO_ADDR)), + manager: Some(Addr::unchecked(ADDR1)), + unstaking_duration: Some(Duration::Height(5)), + } + ) +} + +#[test] +fn test_voting_power_queries() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Total power is 0 + let resp = get_total_power_at_height(&mut app, addr.clone(), None); + assert!(resp.power.is_zero()); + + // ADDR1 has no power, none staked + let resp = get_voting_power_at_height(&mut app, addr.clone(), ADDR1.to_string(), None); + assert!(resp.power.is_zero()); + + // ADDR1 stakes + stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap(); + app.update_block(next_block); + + // Total power is 100 + let resp = get_total_power_at_height(&mut app, addr.clone(), None); + assert_eq!(resp.power, Uint128::new(100)); + + // ADDR1 has 100 power + let resp = get_voting_power_at_height(&mut app, addr.clone(), ADDR1.to_string(), None); + assert_eq!(resp.power, Uint128::new(100)); + + // ADDR2 still has 0 power + let resp = get_voting_power_at_height(&mut app, addr.clone(), ADDR2.to_string(), None); + assert!(resp.power.is_zero()); + + // ADDR2 stakes + stake_tokens(&mut app, addr.clone(), ADDR2, 50, DENOM).unwrap(); + app.update_block(next_block); + let prev_height = app.block_info().height - 1; + + // Query the previous height, total 100, ADDR1 100, ADDR2 0 + // Total power is 100 + let resp = get_total_power_at_height(&mut app, addr.clone(), Some(prev_height)); + assert_eq!(resp.power, Uint128::new(100)); + + // ADDR1 has 100 power + let resp = + get_voting_power_at_height(&mut app, addr.clone(), ADDR1.to_string(), Some(prev_height)); + assert_eq!(resp.power, Uint128::new(100)); + + // ADDR2 still has 0 power + let resp = + get_voting_power_at_height(&mut app, addr.clone(), ADDR2.to_string(), Some(prev_height)); + assert!(resp.power.is_zero()); + + // For current height, total 150, ADDR1 100, ADDR2 50 + // Total power is 150 + let resp = get_total_power_at_height(&mut app, addr.clone(), None); + assert_eq!(resp.power, Uint128::new(150)); + + // ADDR1 has 100 power + let resp = get_voting_power_at_height(&mut app, addr.clone(), ADDR1.to_string(), None); + assert_eq!(resp.power, Uint128::new(100)); + + // ADDR2 now has 50 power + let resp = get_voting_power_at_height(&mut app, addr.clone(), ADDR2.to_string(), None); + assert_eq!(resp.power, Uint128::new(50)); + + // ADDR1 unstakes half + unstake_tokens(&mut app, addr.clone(), ADDR1, 50).unwrap(); + app.update_block(next_block); + let prev_height = app.block_info().height - 1; + + // Query the previous height, total 150, ADDR1 100, ADDR2 50 + // Total power is 100 + let resp = get_total_power_at_height(&mut app, addr.clone(), Some(prev_height)); + assert_eq!(resp.power, Uint128::new(150)); + + // ADDR1 has 100 power + let resp = + get_voting_power_at_height(&mut app, addr.clone(), ADDR1.to_string(), Some(prev_height)); + assert_eq!(resp.power, Uint128::new(100)); + + // ADDR2 still has 0 power + let resp = + get_voting_power_at_height(&mut app, addr.clone(), ADDR2.to_string(), Some(prev_height)); + assert_eq!(resp.power, Uint128::new(50)); + + // For current height, total 100, ADDR1 50, ADDR2 50 + // Total power is 100 + let resp = get_total_power_at_height(&mut app, addr.clone(), None); + assert_eq!(resp.power, Uint128::new(100)); + + // ADDR1 has 50 power + let resp = get_voting_power_at_height(&mut app, addr.clone(), ADDR1.to_string(), None); + assert_eq!(resp.power, Uint128::new(50)); + + // ADDR2 now has 50 power + let resp = get_voting_power_at_height(&mut app, addr, ADDR2.to_string(), None); + assert_eq!(resp.power, Uint128::new(50)); +} + +#[test] +fn test_query_list_stakers() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::CoreModule {}), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // ADDR1 stakes + stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap(); + + // ADDR2 stakes + stake_tokens(&mut app, addr.clone(), ADDR2, 50, DENOM).unwrap(); + + // check entire result set + let stakers: ListStakersResponse = app + .wrap() + .query_wasm_smart( + addr.clone(), + &QueryMsg::ListStakers { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + let test_res = ListStakersResponse { + stakers: vec![ + StakerBalanceResponse { + address: ADDR1.to_string(), + balance: Uint128::new(100), + }, + StakerBalanceResponse { + address: ADDR2.to_string(), + balance: Uint128::new(50), + }, + ], + }; + + assert_eq!(stakers, test_res); + + // skipped 1, check result + let stakers: ListStakersResponse = app + .wrap() + .query_wasm_smart( + addr.clone(), + &QueryMsg::ListStakers { + start_after: Some(ADDR1.to_string()), + limit: None, + }, + ) + .unwrap(); + + let test_res = ListStakersResponse { + stakers: vec![StakerBalanceResponse { + address: ADDR2.to_string(), + balance: Uint128::new(50), + }], + }; + + assert_eq!(stakers, test_res); + + // skipped 2, check result. should be nothing + let stakers: ListStakersResponse = app + .wrap() + .query_wasm_smart( + addr, + &QueryMsg::ListStakers { + start_after: Some(ADDR2.to_string()), + limit: None, + }, + ) + .unwrap(); + + assert_eq!(stakers, ListStakersResponse { stakers: vec![] }); +} + +#[test] +#[should_panic(expected = "Active threshold count must be greater than zero")] +fn test_instantiate_zero_active_threshold_count() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::Address { + addr: DAO_ADDR.to_string(), + }), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::zero(), + }), + }, + ); +} + +#[test] +fn test_active_threshold_absolute_count() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::Address { + addr: DAO_ADDR.to_string(), + }), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(100), + }), + }, + ); + + // Not active as none staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(addr.clone(), &QueryMsg::IsActive {}) + .unwrap(); + assert!(!is_active.active); + + // Stake 100 tokens + stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap(); + app.update_block(next_block); + + // Active as enough staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(addr, &QueryMsg::IsActive {}) + .unwrap(); + assert!(is_active.active); +} + +#[test] +fn test_active_threshold_percent() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::Address { + addr: DAO_ADDR.to_string(), + }), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(20), + }), + }, + ); + + // Not active as none staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(addr.clone(), &QueryMsg::IsActive {}) + .unwrap(); + assert!(!is_active.active); + + // Stake 6000 tokens, now active + stake_tokens(&mut app, addr.clone(), ADDR1, 6000, DENOM).unwrap(); + app.update_block(next_block); + + // Active as enough staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(addr, &QueryMsg::IsActive {}) + .unwrap(); + assert!(is_active.active); +} + +#[test] +fn test_active_threshold_percent_rounds_up() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::Address { + addr: DAO_ADDR.to_string(), + }), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: ODD_DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(50), + }), + }, + ); + + // Not active as none staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(addr.clone(), &QueryMsg::IsActive {}) + .unwrap(); + assert!(!is_active.active); + + // Stake 2 tokens, should not be active. + stake_tokens(&mut app, addr.clone(), ADDR1, 2, ODD_DENOM).unwrap(); + app.update_block(next_block); + + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(addr.clone(), &QueryMsg::IsActive {}) + .unwrap(); + assert!(!is_active.active); + + // Stake 1 more token, should now be active. + stake_tokens(&mut app, addr.clone(), ADDR1, 1, ODD_DENOM).unwrap(); + app.update_block(next_block); + + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(addr, &QueryMsg::IsActive {}) + .unwrap(); + assert!(is_active.active); +} + +#[test] +fn test_active_threshold_none() { + let mut app = App::default(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::Address { + addr: DAO_ADDR.to_string(), + }), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Active as no threshold + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(addr, &QueryMsg::IsActive {}) + .unwrap(); + assert!(is_active.active); +} + +#[test] +fn test_update_active_threshold() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::Address { + addr: DAO_ADDR.to_string(), + }), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + let resp: ActiveThresholdResponse = app + .wrap() + .query_wasm_smart(addr.clone(), &QueryMsg::ActiveThreshold {}) + .unwrap(); + assert_eq!(resp.active_threshold, None); + + let msg = ExecuteMsg::UpdateActiveThreshold { + new_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(100), + }), + }; + + // Expect failure as sender is not the DAO + app.execute_contract(Addr::unchecked(ADDR1), addr.clone(), &msg, &[]) + .unwrap_err(); + + // Expect success as sender is the DAO + app.execute_contract(Addr::unchecked(DAO_ADDR), addr.clone(), &msg, &[]) + .unwrap(); + + let resp: ActiveThresholdResponse = app + .wrap() + .query_wasm_smart(addr, &QueryMsg::ActiveThreshold {}) + .unwrap(); + assert_eq!( + resp.active_threshold, + Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(100) + }) + ); +} + +#[test] +#[should_panic(expected = "Active threshold percentage must be greater than 0 and less than 1")] +fn test_active_threshold_percentage_gt_100() { + let mut app = App::default(); + let staking_id = app.store_code(staking_contract()); + instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::Address { + addr: DAO_ADDR.to_string(), + }), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(120), + }), + }, + ); +} + +#[test] +#[should_panic(expected = "Active threshold percentage must be greater than 0 and less than 1")] +fn test_active_threshold_percentage_lte_0() { + let mut app = App::default(); + let staking_id = app.store_code(staking_contract()); + instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::Address { + addr: DAO_ADDR.to_string(), + }), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(0), + }), + }, + ); +} + +#[test] +#[should_panic(expected = "Absolute count threshold cannot be greater than the total token supply")] +fn test_active_threshold_absolute_count_invalid() { + let mut app = App::default(); + let staking_id = app.store_code(staking_contract()); + instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::Address { + addr: DAO_ADDR.to_string(), + }), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(30001), + }), + }, + ); +} + +#[test] +fn test_add_remove_hooks() { + let mut app = App::default(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + owner: Some(Admin::Address { + addr: DAO_ADDR.to_string(), + }), + manager: Some(ADDR1.to_string()), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // No hooks exist. + let resp: GetHooksResponse = app + .wrap() + .query_wasm_smart(addr.clone(), &QueryMsg::GetHooks {}) + .unwrap(); + assert_eq!(resp.hooks, Vec::::new()); + + // Add a hook. + app.execute_contract( + Addr::unchecked(DAO_ADDR), + addr.clone(), + &ExecuteMsg::AddHook { + addr: "hook".to_string(), + }, + &[], + ) + .unwrap(); + + // One hook exists. + let resp: GetHooksResponse = app + .wrap() + .query_wasm_smart(addr.clone(), &QueryMsg::GetHooks {}) + .unwrap(); + assert_eq!(resp.hooks, vec!["hook".to_string()]); + + // Remove hook. + app.execute_contract( + Addr::unchecked(DAO_ADDR), + addr.clone(), + &ExecuteMsg::RemoveHook { + addr: "hook".to_string(), + }, + &[], + ) + .unwrap(); + + // No hook exists. + let resp: GetHooksResponse = app + .wrap() + .query_wasm_smart(addr, &QueryMsg::GetHooks {}) + .unwrap(); + assert_eq!(resp.hooks, Vec::::new()); +} + +#[test] +pub fn test_migrate_update_version() { + let mut deps = OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MockQuerier::default(), + custom_query_type: PhantomData::, + }; + cw2::set_contract_version(&mut deps.storage, "my-contract", "old-version").unwrap(); + migrate(deps.as_mut(), mock_env(), MigrateMsg {}).unwrap(); + let version = cw2::get_contract_version(&deps.storage).unwrap(); + assert_eq!(version.version, CONTRACT_VERSION); + assert_eq!(version.contract, CONTRACT_NAME); +} diff --git a/packages/dao-testing/src/contracts.rs b/packages/dao-testing/src/contracts.rs index b84f36dd9..e82c2b9c1 100644 --- a/packages/dao-testing/src/contracts.rs +++ b/packages/dao-testing/src/contracts.rs @@ -3,7 +3,6 @@ use cosmwasm_std::Empty; use cw_multi_test::{Contract, ContractWrapper}; use dao_pre_propose_multiple as cppm; use dao_pre_propose_single as cpps; -use token_bindings::{TokenFactoryMsg, TokenFactoryQuery}; pub fn cw20_base_contract() -> Box> { let contract = ContractWrapper::new( @@ -109,15 +108,14 @@ pub fn cw20_balances_voting_contract() -> Box> { Box::new(contract) } -// pub fn native_staked_balances_voting_contract( -// ) -> Box> { -// let contract = ContractWrapper::new( -// dao_voting_native_staked::contract::execute, -// dao_voting_native_staked::contract::instantiate, -// dao_voting_native_staked::contract::query, -// ); -// Box::new(contract) -// } +pub fn native_staked_balances_voting_contract() -> Box> { + let contract = ContractWrapper::new( + dao_voting_native_staked::contract::execute, + dao_voting_native_staked::contract::instantiate, + dao_voting_native_staked::contract::query, + ); + Box::new(contract) +} pub fn voting_cw721_staked_contract() -> Box> { let contract = ContractWrapper::new(