From ac249f418afd94050bb3ba22d429125b6695b02e Mon Sep 17 00:00:00 2001 From: noah Date: Sat, 6 Jul 2024 23:11:22 -0400 Subject: [PATCH] Improve pre-propose proposal creation permission granularity (and bump versions to v2.5.0) (#843) --- Cargo.lock | 158 ++-- Cargo.toml | 80 +- ci/bootstrap-env/src/main.rs | 7 +- ci/integration-tests/src/helpers/helper.rs | 8 +- .../dao-dao-core/schema/dao-dao-core.json | 2 +- .../schema/cw-fund-distributor.json | 2 +- .../schema/dao-rewards-distributor.json | 2 +- .../schema/cw-admin-factory.json | 2 +- .../schema/cw-payroll-factory.json | 2 +- .../cw-token-swap/schema/cw-token-swap.json | 2 +- .../schema/cw-tokenfactory-issuer.json | 2 +- .../cw-vesting/schema/cw-vesting.json | 2 +- .../cw721-roles/schema/cw721-roles.json | 2 +- .../dao-migrator/schema/dao-migrator.json | 2 +- .../dao-pre-propose-approval-single.json | 354 +++++++- .../src/tests.rs | 780 ++++++++++++++++- .../schema/dao-pre-propose-approver.json | 268 +++++- .../dao-pre-propose-approver/src/contract.rs | 22 +- .../dao-pre-propose-approver/src/tests.rs | 193 +++- .../schema/dao-pre-propose-multiple.json | 354 +++++++- .../dao-pre-propose-multiple/src/contract.rs | 17 +- .../dao-pre-propose-multiple/src/tests.rs | 828 +++++++++++++++++- .../schema/dao-pre-propose-single.json | 354 +++++++- .../dao-pre-propose-single/src/contract.rs | 17 +- .../dao-pre-propose-single/src/tests.rs | 825 ++++++++++++++++- .../schema/dao-proposal-condorcet.json | 2 +- .../schema/dao-proposal-multiple.json | 3 +- .../src/testing/instantiate.rs | 20 +- .../src/testing/tests.rs | 14 +- .../schema/dao-proposal-single.json | 3 +- .../src/testing/instantiate.rs | 15 +- .../dao-proposal-single/src/testing/tests.rs | 14 +- .../schema/cw20-stake-external-rewards.json | 2 +- .../schema/cw20-stake-reward-distributor.json | 2 +- .../staking/cw20-stake/schema/cw20-stake.json | 2 +- .../schema/dao-voting-cw20-staked.json | 2 +- .../dao-voting-cw4/schema/dao-voting-cw4.json | 2 +- .../schema/dao-voting-cw721-roles.json | 2 +- .../schema/dao-voting-cw721-staked.json | 2 +- .../schema/dao-voting-token-staked.json | 2 +- packages/dao-pre-propose-base/src/error.rs | 13 +- packages/dao-pre-propose-base/src/execute.rs | 288 +++++- packages/dao-pre-propose-base/src/msg.rs | 29 +- packages/dao-pre-propose-base/src/state.rs | 8 +- packages/dao-pre-propose-base/src/tests.rs | 4 +- packages/dao-voting/src/pre_propose.rs | 79 ++ 46 files changed, 4470 insertions(+), 323 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 486dc1046..6f6eeed06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -271,12 +271,12 @@ dependencies = [ "cw-admin-factory", "cw-utils 1.0.3", "cw20 1.1.2", - "cw20-stake 2.4.2", + "cw20-stake 2.5.0", "dao-dao-core", "dao-interface", "dao-pre-propose-single", "dao-proposal-single", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "dao-voting-cw20-staked", "env_logger", "serde", @@ -663,7 +663,7 @@ dependencies = [ [[package]] name = "cw-admin-factory" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -789,7 +789,7 @@ dependencies = [ [[package]] name = "cw-denom" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -801,18 +801,18 @@ dependencies = [ [[package]] name = "cw-fund-distributor" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", - "cw-paginate-storage 2.4.2", + "cw-paginate-storage 2.5.0", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw2 1.1.2", "cw20 1.1.2", "cw20-base 1.1.2", - "cw20-stake 2.4.2", + "cw20-stake 2.5.0", "dao-dao-core", "dao-interface", "dao-voting-cw20-staked", @@ -821,7 +821,7 @@ dependencies = [ [[package]] name = "cw-hooks" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -888,7 +888,7 @@ dependencies = [ [[package]] name = "cw-paginate-storage" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-std", "cw-multi-test", @@ -898,7 +898,7 @@ dependencies = [ [[package]] name = "cw-payroll-factory" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -942,7 +942,7 @@ dependencies = [ [[package]] name = "cw-stake-tracker" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -995,7 +995,7 @@ dependencies = [ [[package]] name = "cw-token-swap" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1010,7 +1010,7 @@ dependencies = [ [[package]] name = "cw-tokenfactory-issuer" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1031,7 +1031,7 @@ dependencies = [ [[package]] name = "cw-tokenfactory-types" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1101,7 +1101,7 @@ dependencies = [ [[package]] name = "cw-vesting" -version = "2.4.2" +version = "2.5.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -1124,7 +1124,7 @@ dependencies = [ [[package]] name = "cw-wormhole" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1291,7 +1291,7 @@ dependencies = [ [[package]] name = "cw20-stake" -version = "2.4.2" +version = "2.5.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -1300,7 +1300,7 @@ dependencies = [ "cw-hooks", "cw-multi-test", "cw-ownable", - "cw-paginate-storage 2.4.2", + "cw-paginate-storage 2.5.0", "cw-storage-plus 1.2.0", "cw-utils 0.13.4", "cw-utils 1.0.3", @@ -1309,13 +1309,13 @@ dependencies = [ "cw20-base 1.1.2", "cw20-stake 0.2.6", "dao-hooks", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "thiserror", ] [[package]] name = "cw20-stake-external-rewards" -version = "2.4.2" +version = "2.5.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -1329,7 +1329,7 @@ dependencies = [ "cw20 0.13.4", "cw20 1.1.2", "cw20-base 1.1.2", - "cw20-stake 2.4.2", + "cw20-stake 2.5.0", "dao-hooks", "stake-cw20-external-rewards", "thiserror", @@ -1337,7 +1337,7 @@ dependencies = [ [[package]] name = "cw20-stake-reward-distributor" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1348,7 +1348,7 @@ dependencies = [ "cw2 1.1.2", "cw20 1.1.2", "cw20-base 1.1.2", - "cw20-stake 2.4.2", + "cw20-stake 2.5.0", "stake-cw20-reward-distributor", "thiserror", ] @@ -1541,7 +1541,7 @@ dependencies = [ [[package]] name = "cw721-controllers" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1552,7 +1552,7 @@ dependencies = [ [[package]] name = "cw721-roles" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1574,7 +1574,7 @@ dependencies = [ [[package]] name = "dao-cw721-extensions" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1584,13 +1584,13 @@ dependencies = [ [[package]] name = "dao-dao-core" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-core", "cw-multi-test", - "cw-paginate-storage 2.4.2", + "cw-paginate-storage 2.5.0", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw2 1.1.2", @@ -1607,13 +1607,13 @@ dependencies = [ [[package]] name = "dao-dao-macros" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-hooks", "dao-interface", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "proc-macro2", "quote", "syn 1.0.109", @@ -1621,19 +1621,19 @@ dependencies = [ [[package]] name = "dao-hooks" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-hooks", "cw4 1.1.2", "dao-pre-propose-base", - "dao-voting 2.4.2", + "dao-voting 2.5.0", ] [[package]] name = "dao-interface" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1646,7 +1646,7 @@ dependencies = [ [[package]] name = "dao-migrator" -version = "2.4.2" +version = "2.5.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -1663,7 +1663,7 @@ dependencies = [ "cw20 1.1.2", "cw20-base 1.1.2", "cw20-stake 0.2.6", - "cw20-stake 2.4.2", + "cw20-stake 2.5.0", "cw20-staked-balance-voting", "cw4 0.13.4", "cw4-voting", @@ -1672,7 +1672,7 @@ dependencies = [ "dao-proposal-single", "dao-testing", "dao-voting 0.1.0", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "dao-voting-cw20-staked", "dao-voting-cw4", "thiserror", @@ -1680,13 +1680,13 @@ dependencies = [ [[package]] name = "dao-pre-propose-approval-single" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-denom", "cw-multi-test", - "cw-paginate-storage 2.4.2", + "cw-paginate-storage 2.5.0", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw2 1.1.2", @@ -1699,7 +1699,7 @@ dependencies = [ "dao-pre-propose-base", "dao-proposal-single", "dao-testing", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "dao-voting-cw20-staked", "dao-voting-cw4", "thiserror", @@ -1707,7 +1707,7 @@ dependencies = [ [[package]] name = "dao-pre-propose-approver" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1726,14 +1726,14 @@ dependencies = [ "dao-pre-propose-base", "dao-proposal-single", "dao-testing", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "dao-voting-cw20-staked", "dao-voting-cw4", ] [[package]] name = "dao-pre-propose-base" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1744,14 +1744,14 @@ dependencies = [ "cw-utils 1.0.3", "cw2 1.1.2", "dao-interface", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "serde", "thiserror", ] [[package]] name = "dao-pre-propose-multiple" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1768,14 +1768,14 @@ dependencies = [ "dao-pre-propose-base", "dao-proposal-multiple", "dao-testing", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "dao-voting-cw20-staked", "dao-voting-cw4", ] [[package]] name = "dao-pre-propose-single" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1793,14 +1793,14 @@ dependencies = [ "dao-pre-propose-base", "dao-proposal-single", "dao-testing", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "dao-voting-cw20-staked", "dao-voting-cw4", ] [[package]] name = "dao-proposal-condorcet" -version = "2.4.2" +version = "2.5.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -1815,14 +1815,14 @@ dependencies = [ "dao-dao-macros", "dao-interface", "dao-testing", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "dao-voting-cw4", "thiserror", ] [[package]] name = "dao-proposal-hook-counter" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1837,14 +1837,14 @@ dependencies = [ "dao-hooks", "dao-interface", "dao-proposal-single", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "dao-voting-cw20-balance", "thiserror", ] [[package]] name = "dao-proposal-multiple" -version = "2.4.2" +version = "2.5.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -1857,7 +1857,7 @@ dependencies = [ "cw2 1.1.2", "cw20 1.1.2", "cw20-base 1.1.2", - "cw20-stake 2.4.2", + "cw20-stake 2.5.0", "cw4 1.1.2", "cw4-group 1.1.2", "cw721-base 0.18.0", @@ -1868,7 +1868,7 @@ dependencies = [ "dao-pre-propose-multiple", "dao-testing", "dao-voting 0.1.0", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "dao-voting-cw20-balance", "dao-voting-cw20-staked", "dao-voting-cw4", @@ -1880,7 +1880,7 @@ dependencies = [ [[package]] name = "dao-proposal-single" -version = "2.4.2" +version = "2.5.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -1896,7 +1896,7 @@ dependencies = [ "cw2 1.1.2", "cw20 1.1.2", "cw20-base 1.1.2", - "cw20-stake 2.4.2", + "cw20-stake 2.5.0", "cw4 1.1.2", "cw4-group 1.1.2", "cw721-base 0.18.0", @@ -1908,7 +1908,7 @@ dependencies = [ "dao-pre-propose-single", "dao-testing", "dao-voting 0.1.0", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "dao-voting-cw20-balance", "dao-voting-cw20-staked", "dao-voting-cw4", @@ -1919,7 +1919,7 @@ dependencies = [ [[package]] name = "dao-proposal-sudo" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1933,7 +1933,7 @@ dependencies = [ [[package]] name = "dao-rewards-distributor" -version = "2.4.2" +version = "2.5.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -1946,14 +1946,14 @@ dependencies = [ "cw2 1.1.2", "cw20 1.1.2", "cw20-base 1.1.2", - "cw20-stake 2.4.2", + "cw20-stake 2.5.0", "cw4 1.1.2", "cw4-group 1.1.2", "cw721-base 0.18.0", "dao-hooks", "dao-interface", "dao-testing", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "dao-voting-cw20-staked", "dao-voting-cw4", "dao-voting-cw721-staked", @@ -1963,7 +1963,7 @@ dependencies = [ [[package]] name = "dao-test-custom-factory" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1977,13 +1977,13 @@ dependencies = [ "cw721-base 0.18.0", "dao-dao-macros", "dao-interface", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "thiserror", ] [[package]] name = "dao-testing" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1997,7 +1997,7 @@ dependencies = [ "cw2 1.1.2", "cw20 1.1.2", "cw20-base 1.1.2", - "cw20-stake 2.4.2", + "cw20-stake 2.5.0", "cw4 1.1.2", "cw4-group 1.1.2", "cw721-base 0.18.0", @@ -2010,7 +2010,7 @@ dependencies = [ "dao-proposal-single", "dao-test-custom-factory", "dao-voting 0.1.0", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "dao-voting-cw20-balance", "dao-voting-cw20-staked", "dao-voting-cw4", @@ -2039,7 +2039,7 @@ dependencies = [ [[package]] name = "dao-voting" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -2054,7 +2054,7 @@ dependencies = [ [[package]] name = "dao-voting-cw20-balance" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -2071,7 +2071,7 @@ dependencies = [ [[package]] name = "dao-voting-cw20-staked" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -2081,16 +2081,16 @@ dependencies = [ "cw2 1.1.2", "cw20 1.1.2", "cw20-base 1.1.2", - "cw20-stake 2.4.2", + "cw20-stake 2.5.0", "dao-dao-macros", "dao-interface", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "thiserror", ] [[package]] name = "dao-voting-cw4" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -2107,7 +2107,7 @@ dependencies = [ [[package]] name = "dao-voting-cw721-roles" -version = "2.4.2" +version = "2.5.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -2131,7 +2131,7 @@ dependencies = [ [[package]] name = "dao-voting-cw721-staked" -version = "2.4.2" +version = "2.5.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -2152,7 +2152,7 @@ dependencies = [ "dao-proposal-single", "dao-test-custom-factory", "dao-testing", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "osmosis-std", "osmosis-test-tube", "serde", @@ -2161,7 +2161,7 @@ dependencies = [ [[package]] name = "dao-voting-token-staked" -version = "2.4.2" +version = "2.5.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -2181,7 +2181,7 @@ dependencies = [ "dao-proposal-single", "dao-test-custom-factory", "dao-testing", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "osmosis-std", "osmosis-test-tube", "serde", @@ -2869,7 +2869,7 @@ dependencies = [ "cw-vesting", "cw20 1.1.2", "cw20-base 1.1.2", - "cw20-stake 2.4.2", + "cw20-stake 2.5.0", "cw721 0.18.0", "cw721-base 0.18.0", "cw721-roles", @@ -2878,7 +2878,7 @@ dependencies = [ "dao-pre-propose-single", "dao-proposal-single", "dao-test-custom-factory", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "dao-voting-cw20-staked", "dao-voting-cw721-staked", "env_logger", diff --git a/Cargo.toml b/Cargo.toml index 2f2d2ebe4..be1a87726 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ resolver = "2" edition = "2021" license = "BSD-3-Clause" repository = "https://github.com/DA0-DA0/dao-contracts" -version = "2.4.2" +version = "2.5.0" [profile.release] codegen-units = 1 @@ -81,45 +81,45 @@ wynd-utils = "0.4" # optional owner. cw-ownable = "0.5" -cw-admin-factory = { path = "./contracts/external/cw-admin-factory", version = "2.4.2" } -cw-denom = { path = "./packages/cw-denom", version = "2.4.2" } -cw-fund-distributor = { path = "./contracts/distribution/cw-fund-distributor", version = "2.4.2" } -cw-hooks = { path = "./packages/cw-hooks", version = "2.4.2" } -cw-paginate-storage = { path = "./packages/cw-paginate-storage", version = "2.4.2" } -cw-payroll-factory = { path = "./contracts/external/cw-payroll-factory", version = "2.4.2" } -cw-stake-tracker = { path = "./packages/cw-stake-tracker", version = "2.4.2" } -cw-tokenfactory-issuer = { path = "./contracts/external/cw-tokenfactory-issuer", version = "2.4.2", default-features = false } -cw-tokenfactory-types = { path = "./packages/cw-tokenfactory-types", version = "2.4.2", default-features = false } -cw-vesting = { path = "./contracts/external/cw-vesting", version = "2.4.2" } -cw-wormhole = { path = "./packages/cw-wormhole", version = "2.4.2" } -cw20-stake = { path = "./contracts/staking/cw20-stake", version = "2.4.2" } -cw721-controllers = { path = "./packages/cw721-controllers", version = "2.4.2" } -cw721-roles = { path = "./contracts/external/cw721-roles", version = "2.4.2" } -dao-cw721-extensions = { path = "./packages/dao-cw721-extensions", version = "2.4.2" } -dao-dao-core = { path = "./contracts/dao-dao-core", version = "2.4.2" } -dao-dao-macros = { path = "./packages/dao-dao-macros", version = "2.4.2" } -dao-hooks = { path = "./packages/dao-hooks", version = "2.4.2" } -dao-interface = { path = "./packages/dao-interface", version = "2.4.2" } -dao-pre-propose-approval-single = { path = "./contracts/pre-propose/dao-pre-propose-approval-single", version = "2.4.2" } -dao-pre-propose-approver = { path = "./contracts/pre-propose/dao-pre-propose-approver", version = "2.4.2" } -dao-pre-propose-base = { path = "./packages/dao-pre-propose-base", version = "2.4.2" } -dao-pre-propose-multiple = { path = "./contracts/pre-propose/dao-pre-propose-multiple", version = "2.4.2" } -dao-pre-propose-single = { path = "./contracts/pre-propose/dao-pre-propose-single", version = "2.4.2" } -dao-proposal-condorcet = { path = "./contracts/proposal/dao-proposal-condorcet", version = "2.4.2" } -dao-proposal-hook-counter = { path = "./contracts/test/dao-proposal-hook-counter", version = "2.4.2" } -dao-proposal-multiple = { path = "./contracts/proposal/dao-proposal-multiple", version = "2.4.2" } -dao-proposal-single = { path = "./contracts/proposal/dao-proposal-single", version = "2.4.2" } -dao-proposal-sudo = { path = "./contracts/test/dao-proposal-sudo", version = "2.4.2" } -dao-rewards-distributor = { path = "./contracts/distribution/dao-rewards-distributor", version = "2.4.2" } -dao-test-custom-factory = { path = "./contracts/test/dao-test-custom-factory", version = "2.4.2" } -dao-testing = { path = "./packages/dao-testing", version = "2.4.2" } -dao-voting = { path = "./packages/dao-voting", version = "2.4.2" } -dao-voting-cw20-balance = { path = "./contracts/test/dao-voting-cw20-balance", version = "2.4.2" } -dao-voting-cw20-staked = { path = "./contracts/voting/dao-voting-cw20-staked", version = "2.4.2" } -dao-voting-cw4 = { path = "./contracts/voting/dao-voting-cw4", version = "2.4.2" } -dao-voting-cw721-roles = { path = "./contracts/voting/dao-voting-cw721-roles", version = "2.4.2" } -dao-voting-cw721-staked = { path = "./contracts/voting/dao-voting-cw721-staked", version = "2.4.2" } -dao-voting-token-staked = { path = "./contracts/voting/dao-voting-token-staked", version = "2.4.2" } +cw-admin-factory = { path = "./contracts/external/cw-admin-factory", version = "2.5.0" } +cw-denom = { path = "./packages/cw-denom", version = "2.5.0" } +cw-fund-distributor = { path = "./contracts/distribution/cw-fund-distributor", version = "2.5.0" } +cw-hooks = { path = "./packages/cw-hooks", version = "2.5.0" } +cw-paginate-storage = { path = "./packages/cw-paginate-storage", version = "2.5.0" } +cw-payroll-factory = { path = "./contracts/external/cw-payroll-factory", version = "2.5.0" } +cw-stake-tracker = { path = "./packages/cw-stake-tracker", version = "2.5.0" } +cw-tokenfactory-issuer = { path = "./contracts/external/cw-tokenfactory-issuer", version = "2.5.0", default-features = false } +cw-tokenfactory-types = { path = "./packages/cw-tokenfactory-types", version = "2.5.0", default-features = false } +cw-vesting = { path = "./contracts/external/cw-vesting", version = "2.5.0" } +cw-wormhole = { path = "./packages/cw-wormhole", version = "2.5.0" } +cw20-stake = { path = "./contracts/staking/cw20-stake", version = "2.5.0" } +cw721-controllers = { path = "./packages/cw721-controllers", version = "2.5.0" } +cw721-roles = { path = "./contracts/external/cw721-roles", version = "2.5.0" } +dao-cw721-extensions = { path = "./packages/dao-cw721-extensions", version = "2.5.0" } +dao-dao-core = { path = "./contracts/dao-dao-core", version = "2.5.0" } +dao-dao-macros = { path = "./packages/dao-dao-macros", version = "2.5.0" } +dao-hooks = { path = "./packages/dao-hooks", version = "2.5.0" } +dao-interface = { path = "./packages/dao-interface", version = "2.5.0" } +dao-pre-propose-approval-single = { path = "./contracts/pre-propose/dao-pre-propose-approval-single", version = "2.5.0" } +dao-pre-propose-approver = { path = "./contracts/pre-propose/dao-pre-propose-approver", version = "2.5.0" } +dao-pre-propose-base = { path = "./packages/dao-pre-propose-base", version = "2.5.0" } +dao-pre-propose-multiple = { path = "./contracts/pre-propose/dao-pre-propose-multiple", version = "2.5.0" } +dao-pre-propose-single = { path = "./contracts/pre-propose/dao-pre-propose-single", version = "2.5.0" } +dao-proposal-condorcet = { path = "./contracts/proposal/dao-proposal-condorcet", version = "2.5.0" } +dao-proposal-hook-counter = { path = "./contracts/test/dao-proposal-hook-counter", version = "2.5.0" } +dao-proposal-multiple = { path = "./contracts/proposal/dao-proposal-multiple", version = "2.5.0" } +dao-proposal-single = { path = "./contracts/proposal/dao-proposal-single", version = "2.5.0" } +dao-proposal-sudo = { path = "./contracts/test/dao-proposal-sudo", version = "2.5.0" } +dao-rewards-distributor = { path = "./contracts/distribution/dao-rewards-distributor", version = "2.5.0" } +dao-test-custom-factory = { path = "./contracts/test/dao-test-custom-factory", version = "2.5.0" } +dao-testing = { path = "./packages/dao-testing", version = "2.5.0" } +dao-voting = { path = "./packages/dao-voting", version = "2.5.0" } +dao-voting-cw20-balance = { path = "./contracts/test/dao-voting-cw20-balance", version = "2.5.0" } +dao-voting-cw20-staked = { path = "./contracts/voting/dao-voting-cw20-staked", version = "2.5.0" } +dao-voting-cw4 = { path = "./contracts/voting/dao-voting-cw4", version = "2.5.0" } +dao-voting-cw721-roles = { path = "./contracts/voting/dao-voting-cw721-roles", version = "2.5.0" } +dao-voting-cw721-staked = { path = "./contracts/voting/dao-voting-cw721-staked", version = "2.5.0" } +dao-voting-token-staked = { path = "./contracts/voting/dao-voting-token-staked", version = "2.5.0" } # v1 dependencies. used for state migrations. cw-core-v1 = { package = "cw-core", version = "0.1.0" } diff --git a/ci/bootstrap-env/src/main.rs b/ci/bootstrap-env/src/main.rs index 83d6b82c9..e32e4f558 100644 --- a/ci/bootstrap-env/src/main.rs +++ b/ci/bootstrap-env/src/main.rs @@ -4,6 +4,7 @@ use cosm_orc::{config::cfg::Config, orchestrator::cosm_orc::CosmOrc}; use cosmwasm_std::{to_json_binary, Decimal, Empty, Uint128}; use cw20::Cw20Coin; use dao_interface::state::{Admin, ModuleInstantiateInfo}; +use dao_voting::pre_propose::PreProposeSubmissionPolicy; use dao_voting::{ deposit::{DepositRefundPolicy, DepositToken, UncheckedDepositInfo, VotingModuleTokenType}, pre_propose::PreProposeInfo, @@ -99,7 +100,11 @@ fn main() -> Result<()> { amount: Uint128::new(1000000000), refund_policy: DepositRefundPolicy::OnlyPassed, }), - open_proposal_submission: false, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }, extension: Empty::default(), }) .unwrap(), diff --git a/ci/integration-tests/src/helpers/helper.rs b/ci/integration-tests/src/helpers/helper.rs index e3bb76558..95b0e55bb 100644 --- a/ci/integration-tests/src/helpers/helper.rs +++ b/ci/integration-tests/src/helpers/helper.rs @@ -8,7 +8,7 @@ use dao_interface::query::DumpStateResponse; use dao_interface::state::{Admin, ModuleInstantiateInfo}; use dao_voting::{ deposit::{DepositRefundPolicy, DepositToken, UncheckedDepositInfo, VotingModuleTokenType}, - pre_propose::{PreProposeInfo, ProposalCreationPolicy}, + pre_propose::{PreProposeInfo, PreProposeSubmissionPolicy, ProposalCreationPolicy}, threshold::PercentageThreshold, threshold::Threshold, voting::Vote, @@ -84,7 +84,11 @@ pub fn create_dao( amount: DEPOSIT_AMOUNT, refund_policy: DepositRefundPolicy::OnlyPassed, }), - open_proposal_submission: false, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }, extension: Empty::default(), }) .unwrap(), diff --git a/contracts/dao-dao-core/schema/dao-dao-core.json b/contracts/dao-dao-core/schema/dao-dao-core.json index 3ada83c21..b1cca4f3d 100644 --- a/contracts/dao-dao-core/schema/dao-dao-core.json +++ b/contracts/dao-dao-core/schema/dao-dao-core.json @@ -1,6 +1,6 @@ { "contract_name": "dao-dao-core", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/distribution/cw-fund-distributor/schema/cw-fund-distributor.json b/contracts/distribution/cw-fund-distributor/schema/cw-fund-distributor.json index 02b368e07..63fc4b821 100644 --- a/contracts/distribution/cw-fund-distributor/schema/cw-fund-distributor.json +++ b/contracts/distribution/cw-fund-distributor/schema/cw-fund-distributor.json @@ -1,6 +1,6 @@ { "contract_name": "cw-fund-distributor", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/distribution/dao-rewards-distributor/schema/dao-rewards-distributor.json b/contracts/distribution/dao-rewards-distributor/schema/dao-rewards-distributor.json index e7d00a62b..37f979a11 100644 --- a/contracts/distribution/dao-rewards-distributor/schema/dao-rewards-distributor.json +++ b/contracts/distribution/dao-rewards-distributor/schema/dao-rewards-distributor.json @@ -1,6 +1,6 @@ { "contract_name": "dao-rewards-distributor", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/external/cw-admin-factory/schema/cw-admin-factory.json b/contracts/external/cw-admin-factory/schema/cw-admin-factory.json index a85308e6e..efba62576 100644 --- a/contracts/external/cw-admin-factory/schema/cw-admin-factory.json +++ b/contracts/external/cw-admin-factory/schema/cw-admin-factory.json @@ -1,6 +1,6 @@ { "contract_name": "cw-admin-factory", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/external/cw-payroll-factory/schema/cw-payroll-factory.json b/contracts/external/cw-payroll-factory/schema/cw-payroll-factory.json index 964a41762..d9eb0eb32 100644 --- a/contracts/external/cw-payroll-factory/schema/cw-payroll-factory.json +++ b/contracts/external/cw-payroll-factory/schema/cw-payroll-factory.json @@ -1,6 +1,6 @@ { "contract_name": "cw-payroll-factory", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/external/cw-token-swap/schema/cw-token-swap.json b/contracts/external/cw-token-swap/schema/cw-token-swap.json index 4d5db2501..0875c942f 100644 --- a/contracts/external/cw-token-swap/schema/cw-token-swap.json +++ b/contracts/external/cw-token-swap/schema/cw-token-swap.json @@ -1,6 +1,6 @@ { "contract_name": "cw-token-swap", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json b/contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json index 2ff6b757c..6b5e40a2a 100644 --- a/contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json +++ b/contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json @@ -1,6 +1,6 @@ { "contract_name": "cw-tokenfactory-issuer", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/external/cw-vesting/schema/cw-vesting.json b/contracts/external/cw-vesting/schema/cw-vesting.json index d10586ec4..d26c50748 100644 --- a/contracts/external/cw-vesting/schema/cw-vesting.json +++ b/contracts/external/cw-vesting/schema/cw-vesting.json @@ -1,6 +1,6 @@ { "contract_name": "cw-vesting", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/external/cw721-roles/schema/cw721-roles.json b/contracts/external/cw721-roles/schema/cw721-roles.json index 27d75ef6c..d9a254380 100644 --- a/contracts/external/cw721-roles/schema/cw721-roles.json +++ b/contracts/external/cw721-roles/schema/cw721-roles.json @@ -1,6 +1,6 @@ { "contract_name": "cw721-roles", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/external/dao-migrator/schema/dao-migrator.json b/contracts/external/dao-migrator/schema/dao-migrator.json index 828c241ef..62f9371fc 100644 --- a/contracts/external/dao-migrator/schema/dao-migrator.json +++ b/contracts/external/dao-migrator/schema/dao-migrator.json @@ -1,6 +1,6 @@ { "contract_name": "dao-migrator", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/pre-propose/dao-pre-propose-approval-single/schema/dao-pre-propose-approval-single.json b/contracts/pre-propose/dao-pre-propose-approval-single/schema/dao-pre-propose-approval-single.json index d253aeb3a..c46026da1 100644 --- a/contracts/pre-propose/dao-pre-propose-approval-single/schema/dao-pre-propose-approval-single.json +++ b/contracts/pre-propose/dao-pre-propose-approval-single/schema/dao-pre-propose-approval-single.json @@ -1,6 +1,6 @@ { "contract_name": "dao-pre-propose-approval-single", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -8,7 +8,7 @@ "type": "object", "required": [ "extension", - "open_proposal_submission" + "submission_policy" ], "properties": { "deposit_info": { @@ -30,9 +30,13 @@ } ] }, - "open_proposal_submission": { - "description": "If false, only members (addresses with voting power) may create proposals in the DAO. Otherwise, any address may create a proposal so long as they pay the deposit.", - "type": "boolean" + "submission_policy": { + "description": "The policy dictating who is allowed to submit proposals.", + "allOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + } + ] } }, "additionalProperties": false, @@ -123,6 +127,80 @@ }, "additionalProperties": false }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "dao_members" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" @@ -238,11 +316,9 @@ "properties": { "update_config": { "type": "object", - "required": [ - "open_proposal_submission" - ], "properties": { "deposit_info": { + "description": "If None, will remove the deposit. Backwards compatible.", "anyOf": [ { "$ref": "#/definitions/UncheckedDepositInfo" @@ -252,8 +328,79 @@ } ] }, - "open_proposal_submission": { - "type": "boolean" + "submission_policy": { + "description": "If None, will leave the submission policy in the config as-is.", + "anyOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Perform more granular submission policy updates to allow for atomic operations that don't override others.", + "type": "object", + "required": [ + "update_submission_policy" + ], + "properties": { + "update_submission_policy": { + "type": "object", + "properties": { + "allowlist_add": { + "description": "If using specific policy, optionally add to the allowlist.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "allowlist_remove": { + "description": "If using specific policy, optionally remove from the allowlist.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "denylist_add": { + "description": "Optionally add to the denylist. Works for any submission policy.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "denylist_remove": { + "description": "Optionally remove from denylist. Works for any submission policy.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "set_dao_members": { + "description": "If using specific policy, optionally update the `dao_members` flag.", + "type": [ + "boolean", + "null" + ] } }, "additionalProperties": false @@ -1011,6 +1158,80 @@ } } }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "dao_members" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, "ProposeMessage": { "oneOf": [ { @@ -1611,6 +1832,28 @@ }, "additionalProperties": false }, + { + "description": "Returns whether or not the address can submit proposals.", + "type": "object", + "required": [ + "can_propose" + ], + "properties": { + "can_propose": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Returns list of proposal submitted hooks.", "type": "object", @@ -1918,12 +2161,17 @@ "migrate": null, "sudo": null, "responses": { + "can_propose": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Boolean", + "type": "boolean" + }, "config": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Config", "type": "object", "required": [ - "open_proposal_submission" + "submission_policy" ], "properties": { "deposit_info": { @@ -1937,9 +2185,13 @@ } ] }, - "open_proposal_submission": { - "description": "If false, only members (addresses with voting power) may create proposals in the DAO. Otherwise, any address may create a proposal so long as they pay the deposit.", - "type": "boolean" + "submission_policy": { + "description": "The policy dictating who is allowed to submit proposals.", + "allOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + } + ] } }, "additionalProperties": false, @@ -2040,6 +2292,80 @@ } ] }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "dao_members" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" diff --git a/contracts/pre-propose/dao-pre-propose-approval-single/src/tests.rs b/contracts/pre-propose/dao-pre-propose-approval-single/src/tests.rs index 99b8c0369..0705780fc 100644 --- a/contracts/pre-propose/dao-pre-propose-approval-single/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-approval-single/src/tests.rs @@ -9,6 +9,7 @@ use dao_interface::state::{Admin, ModuleInstantiateInfo}; use dao_pre_propose_base::{error::PreProposeError, msg::DepositInfoResponse, state::Config}; use dao_proposal_single::query::ProposalResponse; use dao_testing::helpers::instantiate_with_cw4_groups_governance; +use dao_voting::pre_propose::{PreProposeSubmissionPolicy, PreProposeSubmissionPolicyError}; use dao_voting::{ deposit::{CheckedDepositInfo, DepositRefundPolicy, DepositToken, UncheckedDepositInfo}, pre_propose::{PreProposeInfo, ProposalCreationPolicy}, @@ -52,6 +53,16 @@ fn get_default_proposal_module_instantiate( ) -> dao_proposal_single::msg::InstantiateMsg { let pre_propose_id = app.store_code(cw_pre_propose_base_proposal_single()); + let submission_policy = if open_proposal_submission { + PreProposeSubmissionPolicy::Anyone { denylist: None } + } else { + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + } + }; + dao_proposal_single::msg::InstantiateMsg { threshold: Threshold::AbsolutePercentage { percentage: PercentageThreshold::Majority {}, @@ -65,7 +76,7 @@ fn get_default_proposal_module_instantiate( code_id: pre_propose_id, msg: to_json_binary(&InstantiateMsg { deposit_info, - open_proposal_submission, + submission_policy, extension: InstantiateExt { approver: "approver".to_string(), }, @@ -298,19 +309,30 @@ fn get_deposit_info(app: &App, module: Addr, id: u64) -> DepositInfoResponse { .unwrap() } +fn query_can_propose(app: &App, module: Addr, address: impl Into) -> bool { + app.wrap() + .query_wasm_smart( + module, + &QueryMsg::CanPropose { + address: address.into(), + }, + ) + .unwrap() +} + fn update_config( app: &mut App, module: Addr, sender: &str, deposit_info: Option, - open_proposal_submission: bool, + submission_policy: PreProposeSubmissionPolicy, ) -> Config { app.execute_contract( Addr::unchecked(sender), module.clone(), &ExecuteMsg::UpdateConfig { deposit_info, - open_proposal_submission, + submission_policy: Some(submission_policy), }, &[], ) @@ -324,14 +346,14 @@ fn update_config_should_fail( module: Addr, sender: &str, deposit_info: Option, - open_proposal_submission: bool, + submission_policy: PreProposeSubmissionPolicy, ) -> PreProposeError { app.execute_contract( Addr::unchecked(sender), module, &ExecuteMsg::UpdateConfig { deposit_info, - open_proposal_submission, + submission_policy: Some(submission_policy), }, &[], ) @@ -1165,7 +1187,10 @@ fn test_permissions() { .unwrap_err() .downcast() .unwrap(); - assert_eq!(err, PreProposeError::NotMember {}); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); } #[test] @@ -1314,7 +1339,10 @@ fn test_no_deposit_required_members_submission() { .unwrap_err() .downcast() .unwrap(); - assert_eq!(err, PreProposeError::NotMember {}); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); let pre_propose_id = make_pre_proposal(&mut app, pre_propose.clone(), "ekez", &[]); @@ -1325,6 +1353,212 @@ fn test_no_deposit_required_members_submission() { assert_eq!(Status::Passed, new_status) } +#[test] +fn test_anyone_denylist() { + let mut app = App::default(); + let DefaultTestSetup { + core_addr, + pre_propose, + .. + } = setup_default_test(&mut app, None, false); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Anyone { denylist: None }, + ); + + let rando = "rando"; + + // Proposal succeeds when anyone can propose. + assert!(query_can_propose(&app, pre_propose.clone(), rando)); + make_pre_proposal(&mut app, pre_propose.clone(), rando, &[]); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Anyone { + denylist: Some(vec![rando.to_string()]), + }, + ); + + // Proposing fails if on denylist. + assert!(!query_can_propose(&app, pre_propose.clone(), rando)); + let err: PreProposeError = app + .execute_contract( + Addr::unchecked(rando), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "I would like to join the DAO".to_string(), + description: "though, I am currently not a member.".to_string(), + msgs: vec![], + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + // Proposing succeeds if not on denylist. + assert!(query_can_propose(&app, pre_propose.clone(), "ekez")); + make_pre_proposal(&mut app, pre_propose, "ekez", &[]); +} + +#[test] +fn test_specific_allowlist_denylist() { + let mut app = App::default(); + let DefaultTestSetup { + core_addr, + pre_propose, + .. + } = setup_default_test(&mut app, None, false); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }, + ); + + // Proposal succeeds for member. + assert!(query_can_propose(&app, pre_propose.clone(), "ekez")); + make_pre_proposal(&mut app, pre_propose.clone(), "ekez", &[]); + + let rando = "rando"; + + // Proposing fails for non-member. + assert!(!query_can_propose(&app, pre_propose.clone(), rando)); + let err: PreProposeError = app + .execute_contract( + Addr::unchecked(rando), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "I would like to join the DAO".to_string(), + description: "though, I am currently not a member.".to_string(), + msgs: vec![], + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: Some(vec![rando.to_string()]), + denylist: None, + }, + ); + + // Proposal succeeds if on allowlist. + assert!(query_can_propose(&app, pre_propose.clone(), rando)); + make_pre_proposal(&mut app, pre_propose.clone(), rando, &[]); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: Some(vec![rando.to_string()]), + denylist: Some(vec!["ekez".to_string()]), + }, + ); + + // Proposing fails if on denylist. + assert!(!query_can_propose(&app, pre_propose.clone(), "ekez")); + let err: PreProposeError = app + .execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "Let me propose!".to_string(), + description: "I am a member!!!".to_string(), + msgs: vec![], + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: Some(vec![rando.to_string()]), + denylist: None, + }, + ); + + // Proposing fails if members not allowed. + assert!(!query_can_propose(&app, pre_propose.clone(), "ekez")); + let err: PreProposeError = app + .execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "Let me propose!".to_string(), + description: "I am a member!!!".to_string(), + msgs: vec![], + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + // Proposal succeeds if on allowlist. + assert!(query_can_propose(&app, pre_propose.clone(), rando)); + make_pre_proposal(&mut app, pre_propose.clone(), rando, &[]); +} + #[test] #[should_panic(expected = "invalid zero deposit. set the deposit to `None` to have no deposit")] fn test_instantiate_with_zero_native_deposit() { @@ -1354,7 +1588,11 @@ fn test_instantiate_with_zero_native_deposit() { amount: Uint128::zero(), refund_policy: DepositRefundPolicy::OnlyPassed, }), - open_proposal_submission: false, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }, extension: InstantiateExt { approver: "approver".to_string(), }, @@ -1419,7 +1657,11 @@ fn test_instantiate_with_zero_cw20_deposit() { amount: Uint128::zero(), refund_policy: DepositRefundPolicy::OnlyPassed, }), - open_proposal_submission: false, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }, extension: InstantiateExt { approver: "approver".to_string(), }, @@ -1467,7 +1709,11 @@ fn test_update_config() { config, Config { deposit_info: None, - open_proposal_submission: false + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None + } } ); @@ -1487,7 +1733,7 @@ fn test_update_config() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Never, }), - true, + PreProposeSubmissionPolicy::Anyone { denylist: None }, ); let config = get_config(&app, pre_propose.clone()); @@ -1499,7 +1745,7 @@ fn test_update_config() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Never }), - open_proposal_submission: true, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: None }, } ); @@ -1549,9 +1795,503 @@ fn test_update_config() { assert_eq!(balance, Uint128::new(0)); // Only the core module can update the config. - let err = - update_config_should_fail(&mut app, pre_propose, proposal_single.as_str(), None, true); + let err = update_config_should_fail( + &mut app, + pre_propose.clone(), + proposal_single.as_str(), + None, + PreProposeSubmissionPolicy::Anyone { denylist: None }, + ); assert_eq!(err, PreProposeError::NotDao {}); + + // Errors when no one is authorized to create proposals. + let err = update_config_should_fail( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: None, + denylist: None, + }, + ); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::NoOneAllowed {}) + ); + + // Errors when allowlist and denylist overlap. + let err = update_config_should_fail( + &mut app, + pre_propose, + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: Some(vec!["ekez".to_string()]), + denylist: Some(vec!["ekez".to_string()]), + }, + ); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::DenylistAllowlistOverlap {} + ) + ); +} + +#[test] +fn test_update_submission_policy() { + let mut app = App::default(); + let DefaultTestSetup { + core_addr, + pre_propose, + .. + } = setup_default_test(&mut app, None, true); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: None }, + } + ); + + // Only the core module can update the submission policy. + let err: PreProposeError = app + .execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, PreProposeError::NotDao {}); + + // Append to denylist, with auto de-dupe. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string(), "ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { + denylist: Some(vec!["ekez".to_string()]), + }, + } + ); + + // Add and remove to/from denylist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["someone".to_string(), "else".to_string()]), + denylist_remove: Some(vec!["ekez".to_string()]), + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { + denylist: Some(vec!["someone".to_string(), "else".to_string()]), + }, + } + ); + + // Remove from denylist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: Some(vec!["someone".to_string(), "else".to_string()]), + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: None }, + } + ); + + // Error if try to change Specific fields when set to Anyone. + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: Some(true), + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::AnyoneInvalidUpdateFields {} + ) + ); + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: Some(vec!["ekez".to_string()]), + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::AnyoneInvalidUpdateFields {} + ) + ); + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: Some(vec!["ekez".to_string()]), + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::AnyoneInvalidUpdateFields {} + ) + ); + + // Change to Specific policy. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateConfig { + deposit_info: None, + submission_policy: Some(PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }), + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }, + } + ); + + // Append to denylist, with auto de-dupe. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string(), "ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: Some(vec!["ekez".to_string()]), + }, + } + ); + + // Add and remove to/from denylist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["someone".to_string(), "else".to_string()]), + denylist_remove: Some(vec!["ekez".to_string()]), + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: Some(vec!["someone".to_string(), "else".to_string()]), + }, + } + ); + + // Remove from denylist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: Some(vec!["someone".to_string(), "else".to_string()]), + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None + }, + } + ); + + // Append to allowlist, with auto de-dupe. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: Some(vec!["ekez".to_string(), "ekez".to_string()]), + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: Some(vec!["ekez".to_string()]), + denylist: None, + }, + } + ); + + // Add and remove to/from allowlist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: Some(vec!["someone".to_string(), "else".to_string()]), + allowlist_remove: Some(vec!["ekez".to_string()]), + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: Some(vec!["someone".to_string(), "else".to_string()]), + denylist: None, + }, + } + ); + + // Remove from allowlist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: Some(vec!["someone".to_string(), "else".to_string()]), + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None + }, + } + ); + + // Setting dao_members to false fails if allowlist is empty. + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: Some(false), + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::NoOneAllowed {}) + ); + + // Set dao_members to false and add allowlist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: Some(false), + allowlist_add: Some(vec!["ekez".to_string()]), + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: Some(vec!["ekez".to_string()]), + denylist: None + }, + } + ); + + // Errors when allowlist and denylist overlap. + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::DenylistAllowlistOverlap {} + ) + ); } #[test] @@ -1595,7 +2335,11 @@ fn test_withdraw() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Always, }), - false, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }, ); // Withdraw with no specified denom - should fall back to the one @@ -1642,7 +2386,11 @@ fn test_withdraw() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Always, }), - false, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }, ); increase_allowance( diff --git a/contracts/pre-propose/dao-pre-propose-approver/schema/dao-pre-propose-approver.json b/contracts/pre-propose/dao-pre-propose-approver/schema/dao-pre-propose-approver.json index ef387da81..769cd499d 100644 --- a/contracts/pre-propose/dao-pre-propose-approver/schema/dao-pre-propose-approver.json +++ b/contracts/pre-propose/dao-pre-propose-approver/schema/dao-pre-propose-approver.json @@ -1,6 +1,6 @@ { "contract_name": "dao-pre-propose-approver", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -51,11 +51,9 @@ "properties": { "update_config": { "type": "object", - "required": [ - "open_proposal_submission" - ], "properties": { "deposit_info": { + "description": "If None, will remove the deposit. Backwards compatible.", "anyOf": [ { "$ref": "#/definitions/UncheckedDepositInfo" @@ -65,8 +63,79 @@ } ] }, - "open_proposal_submission": { - "type": "boolean" + "submission_policy": { + "description": "If None, will leave the submission policy in the config as-is.", + "anyOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Perform more granular submission policy updates to allow for atomic operations that don't override others.", + "type": "object", + "required": [ + "update_submission_policy" + ], + "properties": { + "update_submission_policy": { + "type": "object", + "properties": { + "allowlist_add": { + "description": "If using specific policy, optionally add to the allowlist.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "allowlist_remove": { + "description": "If using specific policy, optionally remove from the allowlist.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "denylist_add": { + "description": "Optionally add to the denylist. Works for any submission policy.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "denylist_remove": { + "description": "Optionally remove from denylist. Works for any submission policy.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "set_dao_members": { + "description": "If using specific policy, optionally update the `dao_members` flag.", + "type": [ + "boolean", + "null" + ] } }, "additionalProperties": false @@ -370,6 +439,80 @@ } ] }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "dao_members" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, "Status": { "oneOf": [ { @@ -607,6 +750,28 @@ }, "additionalProperties": false }, + { + "description": "Returns whether or not the address can submit proposals.", + "type": "object", + "required": [ + "can_propose" + ], + "properties": { + "can_propose": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Returns list of proposal submitted hooks.", "type": "object", @@ -713,12 +878,17 @@ "migrate": null, "sudo": null, "responses": { + "can_propose": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Boolean", + "type": "boolean" + }, "config": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Config", "type": "object", "required": [ - "open_proposal_submission" + "submission_policy" ], "properties": { "deposit_info": { @@ -732,9 +902,13 @@ } ] }, - "open_proposal_submission": { - "description": "If false, only members (addresses with voting power) may create proposals in the DAO. Otherwise, any address may create a proposal so long as they pay the deposit.", - "type": "boolean" + "submission_policy": { + "description": "The policy dictating who is allowed to submit proposals.", + "allOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + } + ] } }, "additionalProperties": false, @@ -835,6 +1009,80 @@ } ] }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "dao_members" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" diff --git a/contracts/pre-propose/dao-pre-propose-approver/src/contract.rs b/contracts/pre-propose/dao-pre-propose-approver/src/contract.rs index 119928320..9dcb23c93 100644 --- a/contracts/pre-propose/dao-pre-propose-approver/src/contract.rs +++ b/contracts/pre-propose/dao-pre-propose-approver/src/contract.rs @@ -11,6 +11,7 @@ use dao_pre_propose_approval_single::msg::{ ApproverProposeMessage, ExecuteExt as ApprovalExt, ExecuteMsg as PreProposeApprovalExecuteMsg, }; use dao_pre_propose_base::{error::PreProposeError, state::PreProposeContract}; +use dao_voting::pre_propose::PreProposeSubmissionPolicy; use dao_voting::status::Status; use crate::msg::{ @@ -33,11 +34,16 @@ pub fn instantiate( info: MessageInfo, msg: InstantiateMsg, ) -> Result { - // This contract does not handle deposits or have open submissions - // Here we hardcode the pre-propose-base instantiate message + // This contract does not handle deposits or allow submission permissions + // since only the approval-single contract can create proposals. Just + // hardcode the pre-propose-base instantiate message. let base_instantiate_msg = BaseInstantiateMsg { deposit_info: None, - open_proposal_submission: false, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }, extension: Empty {}, }; // Default pre-propose-base instantiation @@ -92,6 +98,9 @@ pub fn execute( ExecuteMsg::Extension { msg } => match msg { ExecuteExt::ResetApprover {} => execute_reset_approver(deps, env, info), }, + // Override config updates since they don't apply. + ExecuteMsg::UpdateConfig { .. } => Err(PreProposeError::Unsupported {}), + ExecuteMsg::UpdateSubmissionPolicy { .. } => Err(PreProposeError::Unsupported {}), _ => PrePropose::default().execute(deps, env, info, msg), } } @@ -107,7 +116,7 @@ pub fn execute_propose( return Err(PreProposeError::Unauthorized {}); } - // Get pre_prospose_id, transform proposal for the approver + // Get pre_propose_id, transform proposal for the approver // Here we make sure that there are no messages that can be executed let (pre_propose_id, sanitized_msg) = match msg { ApproverProposeMessage::Propose { @@ -228,6 +237,11 @@ pub fn execute_reset_approver( #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { + QueryMsg::CanPropose { address } => { + let approval_contract = PRE_PROPOSE_APPROVAL_CONTRACT.load(deps.storage)?; + let can_propose = address == approval_contract; + to_json_binary(&can_propose) + } QueryMsg::QueryExtension { msg } => match msg { QueryExt::PreProposeApprovalContract {} => { to_json_binary(&PRE_PROPOSE_APPROVAL_CONTRACT.load(deps.storage)?) diff --git a/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs b/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs index fbef73e87..9cae15ff8 100644 --- a/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs @@ -3,6 +3,7 @@ use cw2::ContractVersion; use cw20::Cw20Coin; use cw_denom::UncheckedDenom; use cw_multi_test::{App, BankSudo, Contract, ContractWrapper, Executor}; +use dao_voting::pre_propose::{PreProposeSubmissionPolicy, PreProposeSubmissionPolicyError}; use dps::query::{ProposalListResponse, ProposalResponse}; use dao_interface::state::ProposalModule; @@ -79,6 +80,16 @@ fn get_proposal_module_approval_single_instantiate( ) -> dps::msg::InstantiateMsg { let pre_propose_id = app.store_code(cw_pre_propose_base_proposal_single()); + let submission_policy = if open_proposal_submission { + PreProposeSubmissionPolicy::Anyone { denylist: None } + } else { + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + } + }; + dps::msg::InstantiateMsg { threshold: Threshold::AbsolutePercentage { percentage: PercentageThreshold::Majority {}, @@ -92,7 +103,7 @@ fn get_proposal_module_approval_single_instantiate( code_id: pre_propose_id, msg: to_json_binary(&InstantiateMsg { deposit_info, - open_proposal_submission, + submission_policy, extension: InstantiateExt { approver: APPROVER.to_string(), }, @@ -446,19 +457,30 @@ fn get_latest_proposal_id(app: &App, module: Addr) -> u64 { props.proposals[props.proposals.len() - 1].id } +fn query_can_propose(app: &App, module: Addr, address: impl Into) -> bool { + app.wrap() + .query_wasm_smart( + module, + &QueryMsg::CanPropose { + address: address.into(), + }, + ) + .unwrap() +} + fn update_config( app: &mut App, module: Addr, sender: &str, deposit_info: Option, - open_proposal_submission: bool, + submission_policy: PreProposeSubmissionPolicy, ) -> Config { app.execute_contract( Addr::unchecked(sender), module.clone(), &ExecuteMsg::UpdateConfig { deposit_info, - open_proposal_submission, + submission_policy: Some(submission_policy), }, &[], ) @@ -472,14 +494,14 @@ fn update_config_should_fail( module: Addr, sender: &str, deposit_info: Option, - open_proposal_submission: bool, + submission_policy: PreProposeSubmissionPolicy, ) -> PreProposeError { app.execute_contract( Addr::unchecked(sender), module, &ExecuteMsg::UpdateConfig { deposit_info, - open_proposal_submission, + submission_policy: Some(submission_policy), }, &[], ) @@ -1160,7 +1182,10 @@ fn test_permissions() { .unwrap_err() .downcast() .unwrap(); - assert_eq!(err, PreProposeError::NotMember {}); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); } #[test] @@ -1316,7 +1341,11 @@ fn test_update_config() { config, Config { deposit_info: None, - open_proposal_submission: false + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None + } } ); @@ -1343,7 +1372,7 @@ fn test_update_config() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Never, }), - true, + PreProposeSubmissionPolicy::Anyone { denylist: None }, ); let config = get_config(&app, pre_propose.clone()); @@ -1355,7 +1384,7 @@ fn test_update_config() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Never }), - open_proposal_submission: true, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: None }, } ); @@ -1407,9 +1436,139 @@ fn test_update_config() { assert_eq!(balance, Uint128::new(0)); // Only the core module can update the config. - let err = - update_config_should_fail(&mut app, pre_propose, proposal_single.as_str(), None, true); + let err = update_config_should_fail( + &mut app, + pre_propose.clone(), + proposal_single.as_str(), + None, + PreProposeSubmissionPolicy::Anyone { denylist: None }, + ); assert_eq!(err, PreProposeError::NotDao {}); + + // Errors when no one is authorized to create proposals. + let err = update_config_should_fail( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: None, + denylist: None, + }, + ); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::NoOneAllowed {}) + ); + + // Errors when allowlist and denylist overlap. + let err = update_config_should_fail( + &mut app, + pre_propose, + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: Some(vec!["ekez".to_string()]), + denylist: Some(vec!["ekez".to_string()]), + }, + ); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::DenylistAllowlistOverlap {} + ) + ); +} + +#[test] +fn test_approver_unsupported_update_config() { + let mut app = App::default(); + + // Need to instantiate this so contract addresses match with cw20 test cases + let _ = instantiate_cw20_base_default(&mut app); + + let DefaultTestSetup { + core_addr, + pre_propose_approver, + .. + } = setup_default_test(&mut app, None, true); + + // Should fail because config is not supported for the approver pre-propose + // contract. + let err = update_config_should_fail( + &mut app, + pre_propose_approver, + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: Some(vec!["ekez".to_string()]), + denylist: None, + }, + ); + assert_eq!(err, PreProposeError::Unsupported {}); +} + +#[test] +fn test_approver_unsupported_update_submission_policy() { + let mut app = App::default(); + + // Need to instantiate this so contract addresses match with cw20 test cases + let _ = instantiate_cw20_base_default(&mut app); + + let DefaultTestSetup { + core_addr, + pre_propose_approver, + .. + } = setup_default_test(&mut app, None, true); + + // Should fail because submission policy is not supported for the approver + // pre-propose contract. + let err: PreProposeError = app + .execute_contract( + core_addr, + pre_propose_approver, + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, PreProposeError::Unsupported {}); +} + +#[test] +fn test_approver_can_propose() { + let mut app = App::default(); + + // Need to instantiate this so contract addresses match with cw20 test cases + let _ = instantiate_cw20_base_default(&mut app); + + let DefaultTestSetup { + pre_propose, + pre_propose_approver, + .. + } = setup_default_test(&mut app, None, true); + + // Only the pre-propose-approval-single contract can propose. + assert!(query_can_propose( + &app, + pre_propose_approver.clone(), + pre_propose + )); + assert!(!query_can_propose( + &app, + pre_propose_approver, + "someone_else" + )); } #[test] @@ -1459,7 +1618,11 @@ fn test_withdraw() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Always, }), - false, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }, ); // Withdraw with no specified denom - should fall back to the one @@ -1508,7 +1671,11 @@ fn test_withdraw() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Always, }), - false, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }, ); increase_allowance( diff --git a/contracts/pre-propose/dao-pre-propose-multiple/schema/dao-pre-propose-multiple.json b/contracts/pre-propose/dao-pre-propose-multiple/schema/dao-pre-propose-multiple.json index 577862b00..f55f6359e 100644 --- a/contracts/pre-propose/dao-pre-propose-multiple/schema/dao-pre-propose-multiple.json +++ b/contracts/pre-propose/dao-pre-propose-multiple/schema/dao-pre-propose-multiple.json @@ -1,6 +1,6 @@ { "contract_name": "dao-pre-propose-multiple", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -8,7 +8,7 @@ "type": "object", "required": [ "extension", - "open_proposal_submission" + "submission_policy" ], "properties": { "deposit_info": { @@ -30,9 +30,13 @@ } ] }, - "open_proposal_submission": { - "description": "If false, only members (addresses with voting power) may create proposals in the DAO. Otherwise, any address may create a proposal so long as they pay the deposit.", - "type": "boolean" + "submission_policy": { + "description": "The policy dictating who is allowed to submit proposals.", + "allOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + } + ] } }, "additionalProperties": false, @@ -115,6 +119,80 @@ "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", "type": "object" }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "dao_members" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" @@ -230,11 +308,9 @@ "properties": { "update_config": { "type": "object", - "required": [ - "open_proposal_submission" - ], "properties": { "deposit_info": { + "description": "If None, will remove the deposit. Backwards compatible.", "anyOf": [ { "$ref": "#/definitions/UncheckedDepositInfo" @@ -244,8 +320,79 @@ } ] }, - "open_proposal_submission": { - "type": "boolean" + "submission_policy": { + "description": "If None, will leave the submission policy in the config as-is.", + "anyOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Perform more granular submission policy updates to allow for atomic operations that don't override others.", + "type": "object", + "required": [ + "update_submission_policy" + ], + "properties": { + "update_submission_policy": { + "type": "object", + "properties": { + "allowlist_add": { + "description": "If using specific policy, optionally add to the allowlist.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "allowlist_remove": { + "description": "If using specific policy, optionally remove from the allowlist.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "denylist_add": { + "description": "Optionally add to the denylist. Works for any submission policy.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "denylist_remove": { + "description": "Optionally remove from denylist. Works for any submission policy.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "set_dao_members": { + "description": "If using specific policy, optionally update the `dao_members` flag.", + "type": [ + "boolean", + "null" + ] } }, "additionalProperties": false @@ -1008,6 +1155,80 @@ }, "additionalProperties": false }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "dao_members" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, "ProposeMessage": { "oneOf": [ { @@ -1556,6 +1777,28 @@ }, "additionalProperties": false }, + { + "description": "Returns whether or not the address can submit proposals.", + "type": "object", + "required": [ + "can_propose" + ], + "properties": { + "can_propose": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Returns list of proposal submitted hooks.", "type": "object", @@ -1603,12 +1846,17 @@ "migrate": null, "sudo": null, "responses": { + "can_propose": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Boolean", + "type": "boolean" + }, "config": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Config", "type": "object", "required": [ - "open_proposal_submission" + "submission_policy" ], "properties": { "deposit_info": { @@ -1622,9 +1870,13 @@ } ] }, - "open_proposal_submission": { - "description": "If false, only members (addresses with voting power) may create proposals in the DAO. Otherwise, any address may create a proposal so long as they pay the deposit.", - "type": "boolean" + "submission_policy": { + "description": "The policy dictating who is allowed to submit proposals.", + "allOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + } + ] } }, "additionalProperties": false, @@ -1725,6 +1977,80 @@ } ] }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "dao_members" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" diff --git a/contracts/pre-propose/dao-pre-propose-multiple/src/contract.rs b/contracts/pre-propose/dao-pre-propose-multiple/src/contract.rs index 30e119fbc..4c1d03126 100644 --- a/contracts/pre-propose/dao-pre-propose-multiple/src/contract.rs +++ b/contracts/pre-propose/dao-pre-propose-multiple/src/contract.rs @@ -87,10 +87,23 @@ pub fn execute( ExecuteMsg::Withdraw { denom } => ExecuteInternal::Withdraw { denom }, ExecuteMsg::UpdateConfig { deposit_info, - open_proposal_submission, + submission_policy, } => ExecuteInternal::UpdateConfig { deposit_info, - open_proposal_submission, + submission_policy, + }, + ExecuteMsg::UpdateSubmissionPolicy { + denylist_add, + denylist_remove, + set_dao_members, + allowlist_add, + allowlist_remove, + } => ExecuteInternal::UpdateSubmissionPolicy { + denylist_add, + denylist_remove, + set_dao_members, + allowlist_add, + allowlist_remove, }, ExecuteMsg::AddProposalSubmittedHook { address } => { ExecuteInternal::AddProposalSubmittedHook { address } diff --git a/contracts/pre-propose/dao-pre-propose-multiple/src/tests.rs b/contracts/pre-propose/dao-pre-propose-multiple/src/tests.rs index 56f310ee2..48a01a2aa 100644 --- a/contracts/pre-propose/dao-pre-propose-multiple/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-multiple/src/tests.rs @@ -10,6 +10,7 @@ use dao_interface::state::{Admin, ModuleInstantiateInfo}; use dao_pre_propose_base::{error::PreProposeError, msg::DepositInfoResponse, state::Config}; use dao_proposal_multiple as cpm; use dao_testing::helpers::instantiate_with_cw4_groups_governance; +use dao_voting::pre_propose::{PreProposeSubmissionPolicy, PreProposeSubmissionPolicyError}; use dao_voting::{ deposit::{CheckedDepositInfo, DepositRefundPolicy, DepositToken, UncheckedDepositInfo}, multiple_choice::{ @@ -54,6 +55,16 @@ fn get_default_proposal_module_instantiate( ) -> cpm::msg::InstantiateMsg { let pre_propose_id = app.store_code(cw_pre_propose_base_proposal_single()); + let submission_policy = if open_proposal_submission { + PreProposeSubmissionPolicy::Anyone { denylist: None } + } else { + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + } + }; + cpm::msg::InstantiateMsg { voting_strategy: VotingStrategy::SingleChoice { quorum: PercentageThreshold::Percent(Decimal::percent(10)), @@ -67,7 +78,7 @@ fn get_default_proposal_module_instantiate( code_id: pre_propose_id, msg: to_json_binary(&InstantiateMsg { deposit_info, - open_proposal_submission, + submission_policy, extension: Empty::default(), }) .unwrap(), @@ -351,19 +362,30 @@ fn get_deposit_info(app: &App, module: Addr, id: u64) -> DepositInfoResponse { .unwrap() } +fn query_can_propose(app: &App, module: Addr, address: impl Into) -> bool { + app.wrap() + .query_wasm_smart( + module, + &QueryMsg::CanPropose { + address: address.into(), + }, + ) + .unwrap() +} + fn update_config( app: &mut App, module: Addr, sender: &str, deposit_info: Option, - open_proposal_submission: bool, + submission_policy: PreProposeSubmissionPolicy, ) -> Config { app.execute_contract( Addr::unchecked(sender), module.clone(), &ExecuteMsg::UpdateConfig { deposit_info, - open_proposal_submission, + submission_policy: Some(submission_policy), }, &[], ) @@ -377,14 +399,14 @@ fn update_config_should_fail( module: Addr, sender: &str, deposit_info: Option, - open_proposal_submission: bool, + submission_policy: PreProposeSubmissionPolicy, ) -> PreProposeError { app.execute_contract( Addr::unchecked(sender), module, &ExecuteMsg::UpdateConfig { deposit_info, - open_proposal_submission, + submission_policy: Some(submission_policy), }, &[], ) @@ -878,7 +900,10 @@ fn test_permissions() { .unwrap_err() .downcast() .unwrap(); - assert_eq!(err, PreProposeError::NotMember {}) + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ) } #[test] @@ -985,7 +1010,10 @@ fn test_no_deposit_required_members_submission() { .unwrap_err() .downcast() .unwrap(); - assert_eq!(err, PreProposeError::NotMember {}); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); let id = make_proposal(&mut app, pre_propose, proposal_single.clone(), "ekez", &[]); let new_status = vote( @@ -998,6 +1026,260 @@ fn test_no_deposit_required_members_submission() { assert_eq!(Status::Passed, new_status) } +#[test] +fn test_anyone_denylist() { + let mut app = App::default(); + let DefaultTestSetup { + core_addr, + proposal_single, + pre_propose, + } = setup_default_test(&mut app, None, false); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Anyone { denylist: None }, + ); + + let rando = "rando"; + + // Proposal succeeds when anyone can propose. + assert!(query_can_propose(&app, pre_propose.clone(), rando)); + make_proposal( + &mut app, + pre_propose.clone(), + proposal_single.clone(), + rando, + &[], + ); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Anyone { + denylist: Some(vec![rando.to_string()]), + }, + ); + + // Proposing fails if on denylist. + assert!(!query_can_propose(&app, pre_propose.clone(), rando)); + let err: PreProposeError = app + .execute_contract( + Addr::unchecked(rando), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "I would like to join the DAO".to_string(), + description: "though, I am currently not a member.".to_string(), + choices: MultipleChoiceOptions { + options: vec![MultipleChoiceOption { + description: "multiple choice option 1".to_string(), + msgs: vec![], + title: "title".to_string(), + }], + }, + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + // Proposing succeeds if not on denylist. + assert!(query_can_propose(&app, pre_propose.clone(), "ekez")); + make_proposal(&mut app, pre_propose, proposal_single.clone(), "ekez", &[]); +} + +#[test] +fn test_specific_allowlist_denylist() { + let mut app = App::default(); + let DefaultTestSetup { + core_addr, + proposal_single, + pre_propose, + } = setup_default_test(&mut app, None, false); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }, + ); + + // Proposal succeeds for member. + assert!(query_can_propose(&app, pre_propose.clone(), "ekez")); + make_proposal( + &mut app, + pre_propose.clone(), + proposal_single.clone(), + "ekez", + &[], + ); + + let rando = "rando"; + + // Proposing fails for non-member. + assert!(!query_can_propose(&app, pre_propose.clone(), rando)); + let err: PreProposeError = app + .execute_contract( + Addr::unchecked(rando), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "I would like to join the DAO".to_string(), + description: "though, I am currently not a member.".to_string(), + choices: MultipleChoiceOptions { + options: vec![MultipleChoiceOption { + description: "multiple choice option 1".to_string(), + msgs: vec![], + title: "title".to_string(), + }], + }, + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: Some(vec![rando.to_string()]), + denylist: None, + }, + ); + + // Proposal succeeds if on allowlist. + assert!(query_can_propose(&app, pre_propose.clone(), rando)); + make_proposal( + &mut app, + pre_propose.clone(), + proposal_single.clone(), + rando, + &[], + ); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: Some(vec![rando.to_string()]), + denylist: Some(vec!["ekez".to_string()]), + }, + ); + + // Proposing fails if on denylist. + assert!(!query_can_propose(&app, pre_propose.clone(), "ekez")); + let err: PreProposeError = app + .execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "Let me propose!".to_string(), + description: "I am a member!!!".to_string(), + choices: MultipleChoiceOptions { + options: vec![MultipleChoiceOption { + description: "multiple choice option 1".to_string(), + msgs: vec![], + title: "title".to_string(), + }], + }, + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: Some(vec![rando.to_string()]), + denylist: None, + }, + ); + + // Proposing fails if members not allowed. + assert!(!query_can_propose(&app, pre_propose.clone(), "ekez")); + let err: PreProposeError = app + .execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "Let me propose!".to_string(), + description: "I am a member!!!".to_string(), + choices: MultipleChoiceOptions { + options: vec![MultipleChoiceOption { + description: "multiple choice option 1".to_string(), + msgs: vec![], + title: "title".to_string(), + }], + }, + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + // Proposal succeeds if on allowlist. + assert!(query_can_propose(&app, pre_propose.clone(), rando)); + make_proposal( + &mut app, + pre_propose.clone(), + proposal_single.clone(), + rando, + &[], + ); +} + #[test] fn test_execute_extension_does_nothing() { let mut app = App::default(); @@ -1059,7 +1341,11 @@ fn test_instantiate_with_zero_native_deposit() { amount: Uint128::zero(), refund_policy: DepositRefundPolicy::OnlyPassed, }), - open_proposal_submission: false, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }, extension: Empty::default(), }) .unwrap(), @@ -1122,7 +1408,11 @@ fn test_instantiate_with_zero_cw20_deposit() { amount: Uint128::zero(), refund_policy: DepositRefundPolicy::OnlyPassed, }), - open_proposal_submission: false, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }, extension: Empty::default(), }) .unwrap(), @@ -1168,7 +1458,11 @@ fn test_update_config() { config, Config { deposit_info: None, - open_proposal_submission: false + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None + } } ); @@ -1191,7 +1485,7 @@ fn test_update_config() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Never, }), - true, + PreProposeSubmissionPolicy::Anyone { denylist: None }, ); let config = get_config(&app, pre_propose.clone()); @@ -1203,7 +1497,7 @@ fn test_update_config() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Never }), - open_proposal_submission: true, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: None }, } ); @@ -1261,9 +1555,503 @@ fn test_update_config() { assert_eq!(balance, Uint128::new(0)); // Only the core module can update the config. - let err = - update_config_should_fail(&mut app, pre_propose, proposal_single.as_str(), None, true); + let err = update_config_should_fail( + &mut app, + pre_propose.clone(), + proposal_single.as_str(), + None, + PreProposeSubmissionPolicy::Anyone { denylist: None }, + ); assert_eq!(err, PreProposeError::NotDao {}); + + // Errors when no one is authorized to create proposals. + let err = update_config_should_fail( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: None, + denylist: None, + }, + ); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::NoOneAllowed {}) + ); + + // Errors when allowlist and denylist overlap. + let err = update_config_should_fail( + &mut app, + pre_propose, + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: Some(vec!["ekez".to_string()]), + denylist: Some(vec!["ekez".to_string()]), + }, + ); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::DenylistAllowlistOverlap {} + ) + ); +} + +#[test] +fn test_update_submission_policy() { + let mut app = App::default(); + let DefaultTestSetup { + core_addr, + pre_propose, + .. + } = setup_default_test(&mut app, None, true); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: None }, + } + ); + + // Only the core module can update the submission policy. + let err: PreProposeError = app + .execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, PreProposeError::NotDao {}); + + // Append to denylist, with auto de-dupe. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string(), "ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { + denylist: Some(vec!["ekez".to_string()]), + }, + } + ); + + // Add and remove to/from denylist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["someone".to_string(), "else".to_string()]), + denylist_remove: Some(vec!["ekez".to_string()]), + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { + denylist: Some(vec!["someone".to_string(), "else".to_string()]), + }, + } + ); + + // Remove from denylist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: Some(vec!["someone".to_string(), "else".to_string()]), + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: None }, + } + ); + + // Error if try to change Specific fields when set to Anyone. + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: Some(true), + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::AnyoneInvalidUpdateFields {} + ) + ); + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: Some(vec!["ekez".to_string()]), + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::AnyoneInvalidUpdateFields {} + ) + ); + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: Some(vec!["ekez".to_string()]), + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::AnyoneInvalidUpdateFields {} + ) + ); + + // Change to Specific policy. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateConfig { + deposit_info: None, + submission_policy: Some(PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }), + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }, + } + ); + + // Append to denylist, with auto de-dupe. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string(), "ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: Some(vec!["ekez".to_string()]), + }, + } + ); + + // Add and remove to/from denylist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["someone".to_string(), "else".to_string()]), + denylist_remove: Some(vec!["ekez".to_string()]), + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: Some(vec!["someone".to_string(), "else".to_string()]), + }, + } + ); + + // Remove from denylist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: Some(vec!["someone".to_string(), "else".to_string()]), + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None + }, + } + ); + + // Append to allowlist, with auto de-dupe. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: Some(vec!["ekez".to_string(), "ekez".to_string()]), + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: Some(vec!["ekez".to_string()]), + denylist: None, + }, + } + ); + + // Add and remove to/from allowlist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: Some(vec!["someone".to_string(), "else".to_string()]), + allowlist_remove: Some(vec!["ekez".to_string()]), + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: Some(vec!["someone".to_string(), "else".to_string()]), + denylist: None, + }, + } + ); + + // Remove from allowlist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: Some(vec!["someone".to_string(), "else".to_string()]), + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None + }, + } + ); + + // Setting dao_members to false fails if allowlist is empty. + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: Some(false), + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::NoOneAllowed {}) + ); + + // Set dao_members to false and add allowlist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: Some(false), + allowlist_add: Some(vec!["ekez".to_string()]), + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: Some(vec!["ekez".to_string()]), + denylist: None + }, + } + ); + + // Errors when allowlist and denylist overlap. + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::DenylistAllowlistOverlap {} + ) + ); } #[test] @@ -1307,7 +2095,11 @@ fn test_withdraw() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Always, }), - false, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }, ); // Withdraw with no specified denom - should fall back to the one @@ -1351,7 +2143,11 @@ fn test_withdraw() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Always, }), - false, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }, ); increase_allowance( diff --git a/contracts/pre-propose/dao-pre-propose-single/schema/dao-pre-propose-single.json b/contracts/pre-propose/dao-pre-propose-single/schema/dao-pre-propose-single.json index bf590180a..578fe5634 100644 --- a/contracts/pre-propose/dao-pre-propose-single/schema/dao-pre-propose-single.json +++ b/contracts/pre-propose/dao-pre-propose-single/schema/dao-pre-propose-single.json @@ -1,6 +1,6 @@ { "contract_name": "dao-pre-propose-single", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -8,7 +8,7 @@ "type": "object", "required": [ "extension", - "open_proposal_submission" + "submission_policy" ], "properties": { "deposit_info": { @@ -30,9 +30,13 @@ } ] }, - "open_proposal_submission": { - "description": "If false, only members (addresses with voting power) may create proposals in the DAO. Otherwise, any address may create a proposal so long as they pay the deposit.", - "type": "boolean" + "submission_policy": { + "description": "The policy dictating who is allowed to submit proposals.", + "allOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + } + ] } }, "additionalProperties": false, @@ -115,6 +119,80 @@ "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", "type": "object" }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "dao_members" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" @@ -230,11 +308,9 @@ "properties": { "update_config": { "type": "object", - "required": [ - "open_proposal_submission" - ], "properties": { "deposit_info": { + "description": "If None, will remove the deposit. Backwards compatible.", "anyOf": [ { "$ref": "#/definitions/UncheckedDepositInfo" @@ -244,8 +320,79 @@ } ] }, - "open_proposal_submission": { - "type": "boolean" + "submission_policy": { + "description": "If None, will leave the submission policy in the config as-is.", + "anyOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Perform more granular submission policy updates to allow for atomic operations that don't override others.", + "type": "object", + "required": [ + "update_submission_policy" + ], + "properties": { + "update_submission_policy": { + "type": "object", + "properties": { + "allowlist_add": { + "description": "If using specific policy, optionally add to the allowlist.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "allowlist_remove": { + "description": "If using specific policy, optionally remove from the allowlist.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "denylist_add": { + "description": "Optionally add to the denylist. Works for any submission policy.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "denylist_remove": { + "description": "Optionally remove from denylist. Works for any submission policy.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "set_dao_members": { + "description": "If using specific policy, optionally update the `dao_members` flag.", + "type": [ + "boolean", + "null" + ] } }, "additionalProperties": false @@ -929,6 +1076,80 @@ } } }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "dao_members" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, "ProposeMessage": { "oneOf": [ { @@ -1530,6 +1751,28 @@ }, "additionalProperties": false }, + { + "description": "Returns whether or not the address can submit proposals.", + "type": "object", + "required": [ + "can_propose" + ], + "properties": { + "can_propose": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Returns list of proposal submitted hooks.", "type": "object", @@ -1577,12 +1820,17 @@ "migrate": null, "sudo": null, "responses": { + "can_propose": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Boolean", + "type": "boolean" + }, "config": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Config", "type": "object", "required": [ - "open_proposal_submission" + "submission_policy" ], "properties": { "deposit_info": { @@ -1596,9 +1844,13 @@ } ] }, - "open_proposal_submission": { - "description": "If false, only members (addresses with voting power) may create proposals in the DAO. Otherwise, any address may create a proposal so long as they pay the deposit.", - "type": "boolean" + "submission_policy": { + "description": "The policy dictating who is allowed to submit proposals.", + "allOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + } + ] } }, "additionalProperties": false, @@ -1699,6 +1951,80 @@ } ] }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "dao_members" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" diff --git a/contracts/pre-propose/dao-pre-propose-single/src/contract.rs b/contracts/pre-propose/dao-pre-propose-single/src/contract.rs index 73b283cac..9f65d7f06 100644 --- a/contracts/pre-propose/dao-pre-propose-single/src/contract.rs +++ b/contracts/pre-propose/dao-pre-propose-single/src/contract.rs @@ -91,10 +91,23 @@ pub fn execute( ExecuteMsg::Withdraw { denom } => ExecuteInternal::Withdraw { denom }, ExecuteMsg::UpdateConfig { deposit_info, - open_proposal_submission, + submission_policy, } => ExecuteInternal::UpdateConfig { deposit_info, - open_proposal_submission, + submission_policy, + }, + ExecuteMsg::UpdateSubmissionPolicy { + denylist_add, + denylist_remove, + set_dao_members, + allowlist_add, + allowlist_remove, + } => ExecuteInternal::UpdateSubmissionPolicy { + denylist_add, + denylist_remove, + set_dao_members, + allowlist_add, + allowlist_remove, }, ExecuteMsg::AddProposalSubmittedHook { address } => { ExecuteInternal::AddProposalSubmittedHook { address } diff --git a/contracts/pre-propose/dao-pre-propose-single/src/tests.rs b/contracts/pre-propose/dao-pre-propose-single/src/tests.rs index 0475d13b6..f579ad398 100644 --- a/contracts/pre-propose/dao-pre-propose-single/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-single/src/tests.rs @@ -9,6 +9,7 @@ use dao_interface::state::{Admin, ModuleInstantiateInfo}; use dao_pre_propose_base::{error::PreProposeError, msg::DepositInfoResponse, state::Config}; use dao_proposal_single as dps; use dao_testing::helpers::instantiate_with_cw4_groups_governance; +use dao_voting::pre_propose::{PreProposeSubmissionPolicy, PreProposeSubmissionPolicyError}; use dao_voting::{ deposit::{CheckedDepositInfo, DepositRefundPolicy, DepositToken, UncheckedDepositInfo}, pre_propose::{PreProposeInfo, ProposalCreationPolicy}, @@ -52,6 +53,16 @@ fn get_default_proposal_module_instantiate( ) -> dps::msg::InstantiateMsg { let pre_propose_id = app.store_code(cw_pre_propose_base_proposal_single()); + let submission_policy = if open_proposal_submission { + PreProposeSubmissionPolicy::Anyone { denylist: None } + } else { + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + } + }; + dps::msg::InstantiateMsg { threshold: Threshold::AbsolutePercentage { percentage: PercentageThreshold::Majority {}, @@ -65,7 +76,7 @@ fn get_default_proposal_module_instantiate( code_id: pre_propose_id, msg: to_json_binary(&InstantiateMsg { deposit_info, - open_proposal_submission, + submission_policy, extension: Empty::default(), }) .unwrap(), @@ -332,19 +343,30 @@ fn get_deposit_info(app: &App, module: Addr, id: u64) -> DepositInfoResponse { .unwrap() } +fn query_can_propose(app: &App, module: Addr, address: impl Into) -> bool { + app.wrap() + .query_wasm_smart( + module, + &QueryMsg::CanPropose { + address: address.into(), + }, + ) + .unwrap() +} + fn update_config( app: &mut App, module: Addr, sender: &str, deposit_info: Option, - open_proposal_submission: bool, + submission_policy: PreProposeSubmissionPolicy, ) -> Config { app.execute_contract( Addr::unchecked(sender), module.clone(), &ExecuteMsg::UpdateConfig { deposit_info, - open_proposal_submission, + submission_policy: Some(submission_policy), }, &[], ) @@ -358,14 +380,14 @@ fn update_config_should_fail( module: Addr, sender: &str, deposit_info: Option, - open_proposal_submission: bool, + submission_policy: PreProposeSubmissionPolicy, ) -> PreProposeError { app.execute_contract( Addr::unchecked(sender), module, &ExecuteMsg::UpdateConfig { deposit_info, - open_proposal_submission, + submission_policy: Some(submission_policy), }, &[], ) @@ -838,7 +860,10 @@ fn test_permissions() { .unwrap_err() .downcast() .unwrap(); - assert_eq!(err, PreProposeError::NotMember {}) + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ) } #[test] @@ -927,13 +952,246 @@ fn test_no_deposit_required_members_submission() { .unwrap_err() .downcast() .unwrap(); - assert_eq!(err, PreProposeError::NotMember {}); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); let id = make_proposal(&mut app, pre_propose, proposal_single.clone(), "ekez", &[]); let new_status = vote(&mut app, proposal_single, "ekez", id, Vote::Yes); assert_eq!(Status::Passed, new_status) } +#[test] +fn test_anyone_denylist() { + let mut app = App::default(); + let DefaultTestSetup { + core_addr, + proposal_single, + pre_propose, + } = setup_default_test(&mut app, None, false); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Anyone { denylist: None }, + ); + + let rando = "rando"; + + // Proposal succeeds when anyone can propose. + assert!(query_can_propose(&app, pre_propose.clone(), rando)); + make_proposal( + &mut app, + pre_propose.clone(), + proposal_single.clone(), + rando, + &[], + ); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Anyone { + denylist: Some(vec![rando.to_string()]), + }, + ); + + // Proposing fails if on denylist. + assert!(!query_can_propose(&app, pre_propose.clone(), rando)); + let err: PreProposeError = app + .execute_contract( + Addr::unchecked(rando), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "I would like to join the DAO".to_string(), + description: "though, I am currently not a member.".to_string(), + msgs: vec![], + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + // Proposing succeeds if not on denylist. + assert!(query_can_propose(&app, pre_propose.clone(), "ekez")); + make_proposal(&mut app, pre_propose, proposal_single.clone(), "ekez", &[]); +} + +#[test] +fn test_specific_allowlist_denylist() { + let mut app = App::default(); + let DefaultTestSetup { + core_addr, + proposal_single, + pre_propose, + } = setup_default_test(&mut app, None, false); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }, + ); + + // Proposal succeeds for member. + assert!(query_can_propose(&app, pre_propose.clone(), "ekez")); + make_proposal( + &mut app, + pre_propose.clone(), + proposal_single.clone(), + "ekez", + &[], + ); + + let rando = "rando"; + + // Proposing fails for non-member. + assert!(!query_can_propose(&app, pre_propose.clone(), rando)); + let err: PreProposeError = app + .execute_contract( + Addr::unchecked(rando), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "I would like to join the DAO".to_string(), + description: "though, I am currently not a member.".to_string(), + msgs: vec![], + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: Some(vec![rando.to_string()]), + denylist: None, + }, + ); + + // Proposal succeeds if on allowlist. + assert!(query_can_propose(&app, pre_propose.clone(), rando)); + make_proposal( + &mut app, + pre_propose.clone(), + proposal_single.clone(), + rando, + &[], + ); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: Some(vec![rando.to_string()]), + denylist: Some(vec!["ekez".to_string()]), + }, + ); + + // Proposing fails if on denylist. + assert!(!query_can_propose(&app, pre_propose.clone(), "ekez")); + let err: PreProposeError = app + .execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "Let me propose!".to_string(), + description: "I am a member!!!".to_string(), + msgs: vec![], + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: Some(vec![rando.to_string()]), + denylist: None, + }, + ); + + // Proposing fails if members not allowed. + assert!(!query_can_propose(&app, pre_propose.clone(), "ekez")); + let err: PreProposeError = app + .execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "Let me propose!".to_string(), + description: "I am a member!!!".to_string(), + msgs: vec![], + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + // Proposal succeeds if on allowlist. + assert!(query_can_propose(&app, pre_propose.clone(), rando)); + make_proposal( + &mut app, + pre_propose.clone(), + proposal_single.clone(), + rando, + &[], + ); +} + #[test] fn test_execute_extension_does_nothing() { let mut app = App::default(); @@ -995,7 +1253,11 @@ fn test_instantiate_with_zero_native_deposit() { amount: Uint128::zero(), refund_policy: DepositRefundPolicy::OnlyPassed, }), - open_proposal_submission: false, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }, extension: Empty::default(), }) .unwrap(), @@ -1058,7 +1320,11 @@ fn test_instantiate_with_zero_cw20_deposit() { amount: Uint128::zero(), refund_policy: DepositRefundPolicy::OnlyPassed, }), - open_proposal_submission: false, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }, extension: Empty::default(), }) .unwrap(), @@ -1104,7 +1370,11 @@ fn test_update_config() { config, Config { deposit_info: None, - open_proposal_submission: false + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None + }, } ); @@ -1127,7 +1397,7 @@ fn test_update_config() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Never, }), - true, + PreProposeSubmissionPolicy::Anyone { denylist: None }, ); let config = get_config(&app, pre_propose.clone()); @@ -1139,7 +1409,7 @@ fn test_update_config() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Never }), - open_proposal_submission: true, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: None }, } ); @@ -1185,9 +1455,524 @@ fn test_update_config() { assert_eq!(balance, Uint128::new(0)); // Only the core module can update the config. - let err = - update_config_should_fail(&mut app, pre_propose, proposal_single.as_str(), None, true); + let err = update_config_should_fail( + &mut app, + pre_propose.clone(), + proposal_single.as_str(), + None, + PreProposeSubmissionPolicy::Anyone { denylist: None }, + ); assert_eq!(err, PreProposeError::NotDao {}); + + // Errors when no one is authorized to create proposals. + let err = update_config_should_fail( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: None, + denylist: None, + }, + ); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::NoOneAllowed {}) + ); + + // Errors when allowlist and denylist overlap. + let err = update_config_should_fail( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: Some(vec!["ekez".to_string()]), + denylist: Some(vec!["ekez".to_string()]), + }, + ); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::DenylistAllowlistOverlap {} + ) + ); + + // Doesn't change submission policy if omitted. + app.execute_contract( + core_addr, + pre_propose.clone(), + &ExecuteMsg::UpdateConfig { + deposit_info: None, + submission_policy: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: None }, + } + ); +} + +#[test] +fn test_update_submission_policy() { + let mut app = App::default(); + let DefaultTestSetup { + core_addr, + pre_propose, + .. + } = setup_default_test(&mut app, None, true); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: None }, + } + ); + + // Only the core module can update the submission policy. + let err: PreProposeError = app + .execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, PreProposeError::NotDao {}); + + // Append to denylist, with auto de-dupe. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string(), "ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { + denylist: Some(vec!["ekez".to_string()]), + }, + } + ); + + // Add and remove to/from denylist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["someone".to_string(), "else".to_string()]), + denylist_remove: Some(vec!["ekez".to_string()]), + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { + denylist: Some(vec!["someone".to_string(), "else".to_string()]), + }, + } + ); + + // Remove from denylist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: Some(vec!["someone".to_string(), "else".to_string()]), + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: None }, + } + ); + + // Error if try to change Specific fields when set to Anyone. + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: Some(true), + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::AnyoneInvalidUpdateFields {} + ) + ); + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: Some(vec!["ekez".to_string()]), + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::AnyoneInvalidUpdateFields {} + ) + ); + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: Some(vec!["ekez".to_string()]), + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::AnyoneInvalidUpdateFields {} + ) + ); + + // Change to Specific policy. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateConfig { + deposit_info: None, + submission_policy: Some(PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }), + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }, + } + ); + + // Append to denylist, with auto de-dupe. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string(), "ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: Some(vec!["ekez".to_string()]), + }, + } + ); + + // Add and remove to/from denylist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["someone".to_string(), "else".to_string()]), + denylist_remove: Some(vec!["ekez".to_string()]), + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: Some(vec!["someone".to_string(), "else".to_string()]), + }, + } + ); + + // Remove from denylist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: Some(vec!["someone".to_string(), "else".to_string()]), + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None + }, + } + ); + + // Append to allowlist, with auto de-dupe. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: Some(vec!["ekez".to_string(), "ekez".to_string()]), + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: Some(vec!["ekez".to_string()]), + denylist: None, + }, + } + ); + + // Add and remove to/from allowlist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: Some(vec!["someone".to_string(), "else".to_string()]), + allowlist_remove: Some(vec!["ekez".to_string()]), + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: Some(vec!["someone".to_string(), "else".to_string()]), + denylist: None, + }, + } + ); + + // Remove from allowlist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: Some(vec!["someone".to_string(), "else".to_string()]), + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None + }, + } + ); + + // Setting dao_members to false fails if allowlist is empty. + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: Some(false), + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::NoOneAllowed {}) + ); + + // Set dao_members to false and add allowlist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: Some(false), + allowlist_add: Some(vec!["ekez".to_string()]), + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: Some(vec!["ekez".to_string()]), + denylist: None + }, + } + ); + + // Errors when allowlist and denylist overlap. + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::DenylistAllowlistOverlap {} + ) + ); } #[test] @@ -1231,7 +2016,11 @@ fn test_withdraw() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Always, }), - false, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }, ); // Withdraw with no specified denom - should fall back to the one @@ -1275,7 +2064,11 @@ fn test_withdraw() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Always, }), - false, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }, ); increase_allowance( diff --git a/contracts/proposal/dao-proposal-condorcet/schema/dao-proposal-condorcet.json b/contracts/proposal/dao-proposal-condorcet/schema/dao-proposal-condorcet.json index 1ffbbb163..ab82db9c5 100644 --- a/contracts/proposal/dao-proposal-condorcet/schema/dao-proposal-condorcet.json +++ b/contracts/proposal/dao-proposal-condorcet/schema/dao-proposal-condorcet.json @@ -1,6 +1,6 @@ { "contract_name": "dao-proposal-condorcet", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/proposal/dao-proposal-multiple/schema/dao-proposal-multiple.json b/contracts/proposal/dao-proposal-multiple/schema/dao-proposal-multiple.json index 415412ea2..5f6a884d0 100644 --- a/contracts/proposal/dao-proposal-multiple/schema/dao-proposal-multiple.json +++ b/contracts/proposal/dao-proposal-multiple/schema/dao-proposal-multiple.json @@ -1,6 +1,6 @@ { "contract_name": "dao-proposal-multiple", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -5173,6 +5173,7 @@ "proposal_creation_policy": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ProposalCreationPolicy", + "description": "The policy configured in a proposal module that determines whether or not a pre-propose module is in use. If so, only the module can create new proposals. Otherwise, there is no restriction on proposal creation.", "oneOf": [ { "description": "Anyone may create a proposal, free of charge.", diff --git a/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs b/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs index 8fd7e0c95..28fdc5847 100644 --- a/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs +++ b/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs @@ -12,8 +12,11 @@ use dao_testing::contracts::{ use dao_voting::{ deposit::{DepositRefundPolicy, UncheckedDepositInfo, VotingModuleTokenType}, multiple_choice::VotingStrategy, - pre_propose::PreProposeInfo, - threshold::{ActiveThreshold, ActiveThreshold::AbsoluteCount, PercentageThreshold}, + pre_propose::{PreProposeInfo, PreProposeSubmissionPolicy}, + threshold::{ + ActiveThreshold::{self, AbsoluteCount}, + PercentageThreshold, + }, }; use dao_voting_cw4::msg::GroupContract; @@ -29,12 +32,23 @@ fn get_pre_propose_info( open_proposal_submission: bool, ) -> PreProposeInfo { let pre_propose_contract = app.store_code(pre_propose_multiple_contract()); + + let submission_policy = if open_proposal_submission { + PreProposeSubmissionPolicy::Anyone { denylist: None } + } else { + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + } + }; + PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { code_id: pre_propose_contract, msg: to_json_binary(&cppm::InstantiateMsg { deposit_info, - open_proposal_submission, + submission_policy, extension: Empty::default(), }) .unwrap(), diff --git a/contracts/proposal/dao-proposal-multiple/src/testing/tests.rs b/contracts/proposal/dao-proposal-multiple/src/testing/tests.rs index d7b51beaa..55b43cc9f 100644 --- a/contracts/proposal/dao-proposal-multiple/src/testing/tests.rs +++ b/contracts/proposal/dao-proposal-multiple/src/testing/tests.rs @@ -9,6 +9,7 @@ use cw_utils::Duration; use dao_interface::state::ProposalModule; use dao_interface::state::{Admin, ModuleInstantiateInfo}; use dao_voting::multiple_choice::MultipleChoiceAutoVote; +use dao_voting::pre_propose::PreProposeSubmissionPolicy; use dao_voting::veto::{VetoConfig, VetoError}; use dao_voting::{ deposit::{ @@ -96,12 +97,23 @@ pub fn get_pre_propose_info( open_proposal_submission: bool, ) -> PreProposeInfo { let pre_propose_contract = app.store_code(pre_propose_multiple_contract()); + + let submission_policy = if open_proposal_submission { + PreProposeSubmissionPolicy::Anyone { denylist: None } + } else { + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + } + }; + PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { code_id: pre_propose_contract, msg: to_json_binary(&cppm::InstantiateMsg { deposit_info, - open_proposal_submission, + submission_policy, extension: Empty::default(), }) .unwrap(), diff --git a/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json b/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json index 2491d27aa..0d247d3cc 100644 --- a/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json +++ b/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json @@ -1,6 +1,6 @@ { "contract_name": "dao-proposal-single", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -5326,6 +5326,7 @@ "proposal_creation_policy": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ProposalCreationPolicy", + "description": "The policy configured in a proposal module that determines whether or not a pre-propose module is in use. If so, only the module can create new proposals. Otherwise, there is no restriction on proposal creation.", "oneOf": [ { "description": "Anyone may create a proposal, free of charge.", diff --git a/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs b/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs index 020154700..42514753d 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs @@ -8,7 +8,7 @@ use dao_pre_propose_single as cppbps; use dao_voting::{ deposit::{DepositRefundPolicy, UncheckedDepositInfo, VotingModuleTokenType}, - pre_propose::PreProposeInfo, + pre_propose::{PreProposeInfo, PreProposeSubmissionPolicy}, threshold::{ActiveThreshold, PercentageThreshold, Threshold::ThresholdQuorum}, }; use dao_voting_cw4::msg::GroupContract; @@ -31,12 +31,23 @@ pub(crate) fn get_pre_propose_info( ) -> PreProposeInfo { let pre_propose_contract = app.store_code(crate::testing::contracts::pre_propose_single_contract()); + + let submission_policy = if open_proposal_submission { + PreProposeSubmissionPolicy::Anyone { denylist: None } + } else { + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + } + }; + PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { code_id: pre_propose_contract, msg: to_json_binary(&cppbps::InstantiateMsg { deposit_info, - open_proposal_submission, + submission_policy, extension: Empty::default(), }) .unwrap(), diff --git a/contracts/proposal/dao-proposal-single/src/testing/tests.rs b/contracts/proposal/dao-proposal-single/src/testing/tests.rs index 4246e5a09..34b591ea0 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/tests.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/tests.rs @@ -19,7 +19,7 @@ use dao_interface::{ use dao_testing::{ShouldExecute, TestSingleChoiceVote}; use dao_voting::{ deposit::{CheckedDepositInfo, UncheckedDepositInfo, VotingModuleTokenType}, - pre_propose::{PreProposeInfo, ProposalCreationPolicy}, + pre_propose::{PreProposeInfo, PreProposeSubmissionPolicy, ProposalCreationPolicy}, proposal::{SingleChoiceProposeMsg as ProposeMsg, MAX_PROPOSAL_SIZE}, reply::{ failed_pre_propose_module_hook_id, mask_proposal_execution_proposal_id, @@ -3955,7 +3955,11 @@ fn test_update_pre_propose_module() { amount: Uint128::new(1), refund_policy: dao_voting::deposit::DepositRefundPolicy::OnlyPassed, }), - open_proposal_submission: false, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None, + }, extension: Empty::default(), }) .unwrap(), @@ -4006,7 +4010,11 @@ fn test_update_pre_propose_module() { amount: Uint128::new(1), refund_policy: dao_voting::deposit::DepositRefundPolicy::OnlyPassed, }), - open_proposal_submission: false, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: None, + denylist: None + }, } ); diff --git a/contracts/staking/cw20-stake-external-rewards/schema/cw20-stake-external-rewards.json b/contracts/staking/cw20-stake-external-rewards/schema/cw20-stake-external-rewards.json index cab9e7c5b..aeec250c4 100644 --- a/contracts/staking/cw20-stake-external-rewards/schema/cw20-stake-external-rewards.json +++ b/contracts/staking/cw20-stake-external-rewards/schema/cw20-stake-external-rewards.json @@ -1,6 +1,6 @@ { "contract_name": "cw20-stake-external-rewards", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/staking/cw20-stake-reward-distributor/schema/cw20-stake-reward-distributor.json b/contracts/staking/cw20-stake-reward-distributor/schema/cw20-stake-reward-distributor.json index bbbd6279b..3acae01e3 100644 --- a/contracts/staking/cw20-stake-reward-distributor/schema/cw20-stake-reward-distributor.json +++ b/contracts/staking/cw20-stake-reward-distributor/schema/cw20-stake-reward-distributor.json @@ -1,6 +1,6 @@ { "contract_name": "cw20-stake-reward-distributor", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/staking/cw20-stake/schema/cw20-stake.json b/contracts/staking/cw20-stake/schema/cw20-stake.json index 5c04738ae..77c19cd17 100644 --- a/contracts/staking/cw20-stake/schema/cw20-stake.json +++ b/contracts/staking/cw20-stake/schema/cw20-stake.json @@ -1,6 +1,6 @@ { "contract_name": "cw20-stake", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/voting/dao-voting-cw20-staked/schema/dao-voting-cw20-staked.json b/contracts/voting/dao-voting-cw20-staked/schema/dao-voting-cw20-staked.json index a2e254e93..17afb6f5c 100644 --- a/contracts/voting/dao-voting-cw20-staked/schema/dao-voting-cw20-staked.json +++ b/contracts/voting/dao-voting-cw20-staked/schema/dao-voting-cw20-staked.json @@ -1,6 +1,6 @@ { "contract_name": "dao-voting-cw20-staked", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/voting/dao-voting-cw4/schema/dao-voting-cw4.json b/contracts/voting/dao-voting-cw4/schema/dao-voting-cw4.json index f5c5a14ec..d8908601f 100644 --- a/contracts/voting/dao-voting-cw4/schema/dao-voting-cw4.json +++ b/contracts/voting/dao-voting-cw4/schema/dao-voting-cw4.json @@ -1,6 +1,6 @@ { "contract_name": "dao-voting-cw4", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/voting/dao-voting-cw721-roles/schema/dao-voting-cw721-roles.json b/contracts/voting/dao-voting-cw721-roles/schema/dao-voting-cw721-roles.json index be8df0318..71f8d7c38 100644 --- a/contracts/voting/dao-voting-cw721-roles/schema/dao-voting-cw721-roles.json +++ b/contracts/voting/dao-voting-cw721-roles/schema/dao-voting-cw721-roles.json @@ -1,6 +1,6 @@ { "contract_name": "dao-voting-cw721-roles", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/voting/dao-voting-cw721-staked/schema/dao-voting-cw721-staked.json b/contracts/voting/dao-voting-cw721-staked/schema/dao-voting-cw721-staked.json index d131f5109..aae82a3f8 100644 --- a/contracts/voting/dao-voting-cw721-staked/schema/dao-voting-cw721-staked.json +++ b/contracts/voting/dao-voting-cw721-staked/schema/dao-voting-cw721-staked.json @@ -1,6 +1,6 @@ { "contract_name": "dao-voting-cw721-staked", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/voting/dao-voting-token-staked/schema/dao-voting-token-staked.json b/contracts/voting/dao-voting-token-staked/schema/dao-voting-token-staked.json index 43c934c01..8f6c9aaba 100644 --- a/contracts/voting/dao-voting-token-staked/schema/dao-voting-token-staked.json +++ b/contracts/voting/dao-voting-token-staked/schema/dao-voting-token-staked.json @@ -1,6 +1,6 @@ { "contract_name": "dao-voting-token-staked", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/packages/dao-pre-propose-base/src/error.rs b/packages/dao-pre-propose-base/src/error.rs index 127996166..762ffa768 100644 --- a/packages/dao-pre-propose-base/src/error.rs +++ b/packages/dao-pre-propose-base/src/error.rs @@ -4,7 +4,9 @@ use cw_utils::ParseReplyError; use thiserror::Error; use cw_hooks::HookError; -use dao_voting::{deposit::DepositError, status::Status}; +use dao_voting::{ + deposit::DepositError, pre_propose::PreProposeSubmissionPolicyError, status::Status, +}; #[derive(Error, Debug, PartialEq)] pub enum PreProposeError { @@ -23,15 +25,15 @@ pub enum PreProposeError { #[error(transparent)] ParseReplyError(#[from] ParseReplyError), + #[error(transparent)] + SubmissionPolicy(#[from] PreProposeSubmissionPolicyError), + #[error("Message sender is not proposal module")] NotModule {}, #[error("Message sender is not dao")] NotDao {}, - #[error("You must be a member of this DAO (have voting power) to create a proposal")] - NotMember {}, - #[error("No denomination for withdrawal. specify a denomination to withdraw")] NoWithdrawalDenom {}, @@ -49,4 +51,7 @@ pub enum PreProposeError { #[error("An unknown reply ID was received.")] UnknownReplyID {}, + + #[error("Unsupported")] + Unsupported {}, } diff --git a/packages/dao-pre-propose-base/src/execute.rs b/packages/dao-pre-propose-base/src/execute.rs index 64e0ed3d6..223e08b2d 100644 --- a/packages/dao-pre-propose-base/src/execute.rs +++ b/packages/dao-pre-propose-base/src/execute.rs @@ -1,7 +1,7 @@ use cosmwasm_schema::schemars::JsonSchema; use cosmwasm_std::{ - to_json_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, SubMsg, - WasmMsg, + to_json_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, + SubMsg, WasmMsg, }; use cw2::set_contract_version; @@ -10,6 +10,7 @@ use cw_denom::UncheckedDenom; use dao_interface::voting::{Query as CwCoreQuery, VotingPowerAtHeightResponse}; use dao_voting::{ deposit::{DepositRefundPolicy, UncheckedDepositInfo}, + pre_propose::{PreProposeSubmissionPolicy, PreProposeSubmissionPolicyError}, status::Status, }; use serde::Serialize; @@ -56,9 +57,11 @@ where .map(|info| info.into_checked(deps.as_ref(), dao.clone())) .transpose()?; + msg.submission_policy.validate()?; + let config = Config { deposit_info, - open_proposal_submission: msg.open_proposal_submission, + submission_policy: msg.submission_policy, }; self.config.save(deps.storage, &config)?; @@ -68,8 +71,8 @@ where .add_attribute("proposal_module", info.sender.into_string()) .add_attribute("deposit_info", format!("{:?}", config.deposit_info)) .add_attribute( - "open_proposal_submission", - config.open_proposal_submission.to_string(), + "submission_policy", + config.submission_policy.human_readable(), ) .add_attribute("dao", dao)) } @@ -85,8 +88,23 @@ where ExecuteMsg::Propose { msg } => self.execute_propose(deps, env, info, msg), ExecuteMsg::UpdateConfig { deposit_info, - open_proposal_submission, - } => self.execute_update_config(deps, info, deposit_info, open_proposal_submission), + submission_policy, + } => self.execute_update_config(deps, info, deposit_info, submission_policy), + ExecuteMsg::UpdateSubmissionPolicy { + denylist_add, + denylist_remove, + set_dao_members, + allowlist_add, + allowlist_remove, + } => self.execute_update_submission_policy( + deps, + info, + denylist_add, + denylist_remove, + set_dao_members, + allowlist_add, + allowlist_remove, + ), ExecuteMsg::Withdraw { denom } => { self.execute_withdraw(deps.as_ref(), env, info, denom) } @@ -171,27 +189,186 @@ where deps: DepsMut, info: MessageInfo, deposit_info: Option, - open_proposal_submission: bool, + submission_policy: Option, ) -> Result { let dao = self.dao.load(deps.storage)?; if info.sender != dao { - Err(PreProposeError::NotDao {}) - } else { - let deposit_info = deposit_info - .map(|d| d.into_checked(deps.as_ref(), dao)) - .transpose()?; - self.config.save( - deps.storage, - &Config { + return Err(PreProposeError::NotDao {}); + } + + let deposit_info = deposit_info + .map(|d| d.into_checked(deps.as_ref(), dao)) + .transpose()?; + + self.config + .update(deps.storage, |prev| -> Result { + let new_submission_policy = if let Some(submission_policy) = submission_policy { + submission_policy.validate()?; + submission_policy + } else { + prev.submission_policy + }; + + Ok(Config { deposit_info, - open_proposal_submission, - }, - )?; + submission_policy: new_submission_policy, + }) + })?; - Ok(Response::default() - .add_attribute("method", "update_config") - .add_attribute("sender", info.sender)) + Ok(Response::default() + .add_attribute("method", "update_config") + .add_attribute("sender", info.sender)) + } + + #[allow(clippy::too_many_arguments)] + pub fn execute_update_submission_policy( + &self, + deps: DepsMut, + info: MessageInfo, + denylist_add: Option>, + denylist_remove: Option>, + set_dao_members: Option, + allowlist_add: Option>, + allowlist_remove: Option>, + ) -> Result { + let dao = self.dao.load(deps.storage)?; + if info.sender != dao { + return Err(PreProposeError::NotDao {}); } + + let mut config = self.config.load(deps.storage)?; + + match config.submission_policy { + PreProposeSubmissionPolicy::Anyone { denylist } => { + // Error if other values that apply to Specific were set. + if set_dao_members.is_some() + || allowlist_add.is_some() + || allowlist_remove.is_some() + { + return Err(PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::AnyoneInvalidUpdateFields {}, + )); + } + + let mut denylist = denylist.unwrap_or_default(); + + // Add to denylist. + if let Some(mut denylist_add) = denylist_add { + // Validate addresses. + denylist_add + .iter() + .map(|addr| deps.api.addr_validate(addr)) + .collect::>>()?; + + denylist.append(&mut denylist_add); + denylist.dedup(); + } + + // Remove from denylist. + if let Some(denylist_remove) = denylist_remove { + // Validate addresses. + denylist_remove + .iter() + .map(|addr| deps.api.addr_validate(addr)) + .collect::>>()?; + + denylist.retain(|a| !denylist_remove.contains(a)); + } + + let denylist = if denylist.is_empty() { + None + } else { + Some(denylist) + }; + + config.submission_policy = PreProposeSubmissionPolicy::Anyone { denylist }; + } + PreProposeSubmissionPolicy::Specific { + dao_members, + allowlist, + denylist, + } => { + let dao_members = if let Some(new_dao_members) = set_dao_members { + new_dao_members + } else { + dao_members + }; + + let mut allowlist = allowlist.unwrap_or_default(); + let mut denylist = denylist.unwrap_or_default(); + + // Add to allowlist. + if let Some(mut allowlist_add) = allowlist_add { + // Validate addresses. + allowlist_add + .iter() + .map(|addr| deps.api.addr_validate(addr)) + .collect::>>()?; + + allowlist.append(&mut allowlist_add); + allowlist.dedup(); + } + + // Remove from allowlist. + if let Some(allowlist_remove) = allowlist_remove { + // Validate addresses. + allowlist_remove + .iter() + .map(|addr| deps.api.addr_validate(addr)) + .collect::>>()?; + + allowlist.retain(|a| !allowlist_remove.contains(a)); + } + + // Add to denylist. + if let Some(mut denylist_add) = denylist_add { + // Validate addresses. + denylist_add + .iter() + .map(|addr| deps.api.addr_validate(addr)) + .collect::>>()?; + + denylist.append(&mut denylist_add); + denylist.dedup(); + } + + // Remove from denylist. + if let Some(denylist_remove) = denylist_remove { + // Validate addresses. + denylist_remove + .iter() + .map(|addr| deps.api.addr_validate(addr)) + .collect::>>()?; + + denylist.retain(|a| !denylist_remove.contains(a)); + } + + // Replace empty vectors with None. + let allowlist = if allowlist.is_empty() { + None + } else { + Some(allowlist) + }; + let denylist = if denylist.is_empty() { + None + } else { + Some(denylist) + }; + + config.submission_policy = PreProposeSubmissionPolicy::Specific { + dao_members, + allowlist, + denylist, + }; + } + } + + config.submission_policy.validate()?; + self.config.save(deps.storage, &config)?; + + Ok(Response::default() + .add_attribute("method", "update_submission_policy") + .add_attribute("sender", info.sender)) } pub fn execute_withdraw( @@ -341,20 +518,47 @@ where pub fn check_can_submit(&self, deps: Deps, who: Addr) -> Result<(), PreProposeError> { let config = self.config.load(deps.storage)?; - if !config.open_proposal_submission { - let dao = self.dao.load(deps.storage)?; - let voting_power: VotingPowerAtHeightResponse = deps.querier.query_wasm_smart( - dao.into_string(), - &CwCoreQuery::VotingPowerAtHeight { - address: who.into_string(), - height: None, - }, - )?; - if voting_power.power.is_zero() { - return Err(PreProposeError::NotMember {}); + match config.submission_policy { + PreProposeSubmissionPolicy::Anyone { denylist } => { + if !denylist.unwrap_or_default().contains(&who.to_string()) { + return Ok(()); + } + } + PreProposeSubmissionPolicy::Specific { + dao_members, + allowlist, + denylist, + } => { + // denylist overrides all other settings + if !denylist.unwrap_or_default().contains(&who.to_string()) { + // if on the allowlist, return early + if allowlist.unwrap_or_default().contains(&who.to_string()) { + return Ok(()); + } + + // check DAO membership only if not on the allowlist + if dao_members { + let dao = self.dao.load(deps.storage)?; + let voting_power: VotingPowerAtHeightResponse = + deps.querier.query_wasm_smart( + dao.into_string(), + &CwCoreQuery::VotingPowerAtHeight { + address: who.into_string(), + height: None, + }, + )?; + if !voting_power.power.is_zero() { + return Ok(()); + } + } + } } } - Ok(()) + + // all other cases are not allowed + Err(PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::Unauthorized {}, + )) } pub fn query(&self, deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { @@ -371,6 +575,22 @@ where proposer, }) } + QueryMsg::CanPropose { address } => { + let addr = deps.api.addr_validate(&address)?; + match self.check_can_submit(deps, addr) { + Ok(_) => to_json_binary(&true), + Err(err) => match err { + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::Unauthorized {}, + ) => to_json_binary(&false), + PreProposeError::Std(err) => Err(err), + _ => Err(StdError::generic_err(format!( + "unexpected error: {:?}", + err + ))), + }, + } + } QueryMsg::ProposalSubmittedHooks {} => { to_json_binary(&self.proposal_submitted_hooks.query_hooks(deps)?) } diff --git a/packages/dao-pre-propose-base/src/msg.rs b/packages/dao-pre-propose-base/src/msg.rs index b2f2cc410..f3391cf71 100644 --- a/packages/dao-pre-propose-base/src/msg.rs +++ b/packages/dao-pre-propose-base/src/msg.rs @@ -2,6 +2,7 @@ use cosmwasm_schema::{cw_serde, schemars::JsonSchema, QueryResponses}; use cw_denom::UncheckedDenom; use dao_voting::{ deposit::{CheckedDepositInfo, UncheckedDepositInfo}, + pre_propose::PreProposeSubmissionPolicy, status::Status, }; @@ -10,10 +11,8 @@ pub struct InstantiateMsg { /// Information about the deposit requirements for this /// module. None if no deposit. pub deposit_info: Option, - /// If false, only members (addresses with voting power) may create - /// proposals in the DAO. Otherwise, any address may create a - /// proposal so long as they pay the deposit. - pub open_proposal_submission: bool, + /// The policy dictating who is allowed to submit proposals. + pub submission_policy: PreProposeSubmissionPolicy, /// Extension for instantiation. The default implementation will /// do nothing with this data. pub extension: InstantiateExt, @@ -30,8 +29,25 @@ pub enum ExecuteMsg { /// will only apply to proposals created after the config is /// updated. Only the DAO may execute this message. UpdateConfig { + /// If None, will remove the deposit. Backwards compatible. deposit_info: Option, - open_proposal_submission: bool, + /// If None, will leave the submission policy in the config as-is. + submission_policy: Option, + }, + + /// Perform more granular submission policy updates to allow for atomic + /// operations that don't override others. + UpdateSubmissionPolicy { + /// Optionally add to the denylist. Works for any submission policy. + denylist_add: Option>, + /// Optionally remove from denylist. Works for any submission policy. + denylist_remove: Option>, + /// If using specific policy, optionally update the `dao_members` flag. + set_dao_members: Option, + /// If using specific policy, optionally add to the allowlist. + allowlist_add: Option>, + /// If using specific policy, optionally remove from the allowlist. + allowlist_remove: Option>, }, /// Withdraws funds inside of this contract to the message @@ -109,6 +125,9 @@ where /// PROPOSAL_ID. #[returns(DepositInfoResponse)] DepositInfo { proposal_id: u64 }, + /// Returns whether or not the address can submit proposals. + #[returns(bool)] + CanPropose { address: String }, /// Returns list of proposal submitted hooks. #[returns(cw_hooks::HooksResponse)] ProposalSubmittedHooks {}, diff --git a/packages/dao-pre-propose-base/src/state.rs b/packages/dao-pre-propose-base/src/state.rs index 26310a5cb..4967e1de9 100644 --- a/packages/dao-pre-propose-base/src/state.rs +++ b/packages/dao-pre-propose-base/src/state.rs @@ -5,17 +5,15 @@ use cosmwasm_std::Addr; use cw_hooks::Hooks; use cw_storage_plus::{Item, Map}; -use dao_voting::deposit::CheckedDepositInfo; +use dao_voting::{deposit::CheckedDepositInfo, pre_propose::PreProposeSubmissionPolicy}; #[cw_serde] pub struct Config { /// Information about the deposit required to create a /// proposal. If `None`, no deposit is required. pub deposit_info: Option, - /// If false, only members (addresses with voting power) may create - /// proposals in the DAO. Otherwise, any address may create a - /// proposal so long as they pay the deposit. - pub open_proposal_submission: bool, + /// The policy dictating who is allowed to submit proposals. + pub submission_policy: PreProposeSubmissionPolicy, } pub struct PreProposeContract { diff --git a/packages/dao-pre-propose-base/src/tests.rs b/packages/dao-pre-propose-base/src/tests.rs index a6b12a74d..89522def2 100644 --- a/packages/dao-pre-propose-base/src/tests.rs +++ b/packages/dao-pre-propose-base/src/tests.rs @@ -4,7 +4,7 @@ use cosmwasm_std::{ to_json_binary, Addr, Binary, ContractResult, Empty, Response, SubMsg, WasmMsg, }; use cw_hooks::HooksResponse; -use dao_voting::status::Status; +use dao_voting::{pre_propose::PreProposeSubmissionPolicy, status::Status}; use crate::{ error::PreProposeError, @@ -87,7 +87,7 @@ fn test_proposal_submitted_hooks() { &mut deps.storage, &Config { deposit_info: None, - open_proposal_submission: true, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: None }, }, ) .unwrap(); diff --git a/packages/dao-voting/src/pre_propose.rs b/packages/dao-voting/src/pre_propose.rs index 482bf9260..680d1f588 100644 --- a/packages/dao-voting/src/pre_propose.rs +++ b/packages/dao-voting/src/pre_propose.rs @@ -4,6 +4,7 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Empty, StdResult, SubMsg}; use dao_interface::state::ModuleInstantiateInfo; +use thiserror::Error; use crate::reply::pre_propose_module_instantiation_id; @@ -16,6 +17,9 @@ pub enum PreProposeInfo { ModuleMayPropose { info: ModuleInstantiateInfo }, } +/// The policy configured in a proposal module that determines whether or not a +/// pre-propose module is in use. If so, only the module can create new +/// proposals. Otherwise, there is no restriction on proposal creation. #[cw_serde] pub enum ProposalCreationPolicy { /// Anyone may create a proposal, free of charge. @@ -58,6 +62,81 @@ impl PreProposeInfo { } } +/// The policy configured in a pre-propose module that determines who can submit +/// proposals. This is the preferred way to restrict proposal creation (as +/// opposed to the ProposalCreationPolicy above) since pre-propose modules +/// support other features, such as proposal deposits. +#[cw_serde] +pub enum PreProposeSubmissionPolicy { + /// Anyone may create proposals, except for those in the denylist. + Anyone { + /// Addresses that may not create proposals. + denylist: Option>, + }, + /// Specific people may create proposals. + Specific { + /// Whether or not DAO members may create proposals. + dao_members: bool, + /// Addresses that may create proposals. + allowlist: Option>, + /// Addresses that may not create proposals, overriding other settings. + denylist: Option>, + }, +} + +#[derive(Error, Debug, PartialEq, Eq)] +pub enum PreProposeSubmissionPolicyError { + #[error("The proposal submission policy doesn't allow anyone to submit proposals")] + NoOneAllowed {}, + + #[error("Denylist cannot contain addresses in the allowlist")] + DenylistAllowlistOverlap {}, + + #[error("You are not allowed to submit proposals")] + Unauthorized {}, + + #[error("The current proposal submission policy (Anyone) only supports a denylist. Change the policy to Specific in order to configure more granular permissions.")] + AnyoneInvalidUpdateFields {}, +} + +impl PreProposeSubmissionPolicy { + /// Validate the policy configuration. + pub fn validate(&self) -> Result<(), PreProposeSubmissionPolicyError> { + if let PreProposeSubmissionPolicy::Specific { + dao_members, + allowlist, + denylist, + } = self + { + let allowlist = allowlist.as_deref().unwrap_or_default(); + let denylist = denylist.as_deref().unwrap_or_default(); + + // prevent allowlist and denylist from overlapping + if denylist.iter().any(|a| allowlist.iter().any(|b| a == b)) { + return Err(PreProposeSubmissionPolicyError::DenylistAllowlistOverlap {}); + } + + // ensure someone is allowed to submit proposals, be it DAO members + // or someone on the allowlist. we can't verify that the denylist + // doesn't contain all DAO members, so this is the best we can do to + // ensure that someone is allowed to submit. + if !dao_members && allowlist.is_empty() { + return Err(PreProposeSubmissionPolicyError::NoOneAllowed {}); + } + } + + Ok(()) + } + + /// Human readable string for use in events. + pub fn human_readable(&self) -> String { + match self { + Self::Anyone { .. } => "anyone".to_string(), + Self::Specific { .. } => "specific".to_string(), + } + } +} + #[cfg(test)] mod tests { use cosmwasm_std::{to_json_binary, WasmMsg};