From 31b4134a0219ccb6f435789ec45dec2cdf0281be Mon Sep 17 00:00:00 2001 From: ekez Date: Tue, 13 Dec 2022 15:38:56 -0800 Subject: [PATCH] Add feature flag for cw20 support in dao-core. --- .github/workflows/basic.yml | 16 +++++++ contracts/dao-core/Cargo.toml | 3 ++ contracts/dao-core/README.md | 72 ++++++++++++++++++++++++++++++ contracts/dao-core/src/contract.rs | 20 +++++++-- contracts/dao-core/src/msg.rs | 5 +++ contracts/dao-core/src/query.rs | 5 ++- contracts/dao-core/src/state.rs | 6 ++- contracts/dao-core/src/tests.rs | 56 ++++++++++++++++------- 8 files changed, 161 insertions(+), 22 deletions(-) diff --git a/.github/workflows/basic.yml b/.github/workflows/basic.yml index ee3475659..04d49c8fb 100644 --- a/.github/workflows/basic.yml +++ b/.github/workflows/basic.yml @@ -29,6 +29,15 @@ jobs: env: RUST_BACKTRACE: 1 + - name: Run tests w/o default features + uses: actions-rs/cargo@v1 + with: + toolchain: nightly + command: unit-test + args: --locked --no-default-features + env: + RUST_BACKTRACE: 1 + - name: Compile WASM contract uses: actions-rs/cargo@v1 with: @@ -67,6 +76,13 @@ jobs: command: clippy args: --all-targets -- -D warnings + - name: Run cargo clippy w/o default features + uses: actions-rs/cargo@v1 + with: + toolchain: nightly + command: clippy + args: --all-targets --no-default-features -- -D warnings + - name: Generate Schema run: ./scripts/schema.sh diff --git a/contracts/dao-core/Cargo.toml b/contracts/dao-core/Cargo.toml index 73e9eb962..561ce6164 100644 --- a/contracts/dao-core/Cargo.toml +++ b/contracts/dao-core/Cargo.toml @@ -12,8 +12,11 @@ crate-type = ["cdylib", "rlib"] [features] # for more explicit tests, cargo test --features=backtraces backtraces = ["cosmwasm-std/backtraces"] +# enables cw20 treasury management +cw20 = [] # use library feature to disable all instantiate/execute/query exports library = [] +default = ["cw20"] [dependencies] cosmwasm-std = { workspace = true, features = ["ibc3"] } diff --git a/contracts/dao-core/README.md b/contracts/dao-core/README.md index 72e1bfbd9..d4d1cac31 100644 --- a/contracts/dao-core/README.md +++ b/contracts/dao-core/README.md @@ -11,3 +11,75 @@ In additon to the wiki spec this contract may also pause. To do so a `Pause` message must by executed by a proposal module. Pausing the core module will stop all actions on the module for the duration of the pause. + +## Treasury management + +For management of non-native assets this contract maintains a list of +[cw20](https://github.com/CosmWasm/cw-plus/tree/1568d9f7796ef93747e5e5e45484447fddbea80b/packages/cw20) +and +[cw721](https://github.com/CosmWasm/cw-nfts/tree/c7be7aba9fb270abefee5a3696be62f2736592a0/packages/cw721) +tokens who's balances the DAO would like to track. This allows +frontends to list these tokens in the DAO's treasury. This tracking is +needed as, for non-native tokens, there is no on-chain data source for +all of the cw20 and cw721 tokens owned by a DAO. It may also help +reduce spam as random shitcoins sent to the DAO won't be displayed in +treasury listings, unless the DAO approves them. + +For native tokens we do not need this additional tracking step, as +native token balances are stored in the [bank +module](https://github.com/cosmos/cosmos-sdk/tree/main/x/bank). Thus, +for those tokens frontends can query the chain directly to discover +which tokens the DAO owns. + +### Managing the treasury + +There are two ways that a non-native token may be added to the DAO +treasury. + +If `automatically_add_[cw20s|cw721s]` is set to true in the [DAO's +config](https://github.com/DA0-DA0/dao-contracts/blob/74bd3881fdd86829e5e8b132b9952dd64f2d0737/contracts/dao-core/src/state.rs#L16-L21), +the DAO will add the token to the treasury upon receiving the token +via cw20's `Send` method and cw721's `SendNft` method. + +``` +pub enum ExecuteMsg { + /// Executed when the contract receives a cw20 token. Depending on + /// the contract's configuration the contract will automatically + /// add the token to its treasury. + #[cfg(feature = "cw20")] + Receive(cw20::Cw20ReceiveMsg), + /// Executed when the contract receives a cw721 token. Depending + /// on the contract's configuration the contract will + /// automatically add the token to its treasury. + ReceiveNft(cw721::Cw721ReceiveMsg), + // ... +} +``` + +The DAO may always add or remove non-native tokens via the +`UpdateCw20List` and `UpdateCw721List` methods: + +```rust +pub enum ExecuteMsg { + /// Updates the list of cw20 tokens this contract has registered. + #[cfg(feature = "cw20")] + UpdateCw20List { + to_add: Vec, + to_remove: Vec, + }, + /// Updates the list of cw721 tokens this contract has registered. + UpdateCw721List { + to_add: Vec, + to_remove: Vec, + }, + // ... +} +``` + +### Disabling cw20 support + +There is an effort on CosmWasm chains to depreciate the cw20 standard +in favor of native tokens [generated by the TokenFactory +module](https://hackmd.io/@reecepbcups/cw20-to-tokenfactory). By +default, this contract has cw20 support. It may be disabled by +disabling default features (ex. `cargo build --no-default-features`). diff --git a/contracts/dao-core/src/contract.rs b/contracts/dao-core/src/contract.rs index 90abc6dd1..73612f842 100644 --- a/contracts/dao-core/src/contract.rs +++ b/contracts/dao-core/src/contract.rs @@ -13,13 +13,17 @@ use dao_interface::{voting, ModuleInstantiateCallback, ModuleInstantiateInfo}; use crate::error::ContractError; use crate::msg::{ExecuteMsg, InitialItem, InstantiateMsg, MigrateMsg, QueryMsg}; +#[cfg(feature = "cw20")] +use crate::query::Cw20BalanceResponse; use crate::query::{ - AdminNominationResponse, Cw20BalanceResponse, DaoURIResponse, DumpStateResponse, - GetItemResponse, PauseInfoResponse, SubDao, + AdminNominationResponse, DaoURIResponse, DumpStateResponse, GetItemResponse, PauseInfoResponse, + SubDao, }; +#[cfg(feature = "cw20")] +use crate::state::CW20_LIST; use crate::state::{ Config, ProposalModule, ProposalModuleStatus, ACTIVE_PROPOSAL_MODULE_COUNT, ADMIN, CONFIG, - CW20_LIST, CW721_LIST, ITEMS, NOMINATED_ADMIN, PAUSED, PROPOSAL_MODULES, SUBDAO_LIST, + CW721_LIST, ITEMS, NOMINATED_ADMIN, PAUSED, PROPOSAL_MODULES, SUBDAO_LIST, TOTAL_PROPOSAL_MODULE_COUNT, VOTING_MODULE, }; @@ -43,6 +47,7 @@ pub fn instantiate( name: msg.name, description: msg.description, image_url: msg.image_url, + #[cfg(feature = "cw20")] automatically_add_cw20s: msg.automatically_add_cw20s, automatically_add_cw721s: msg.automatically_add_cw721s, dao_uri: msg.dao_uri, @@ -109,6 +114,7 @@ pub fn execute( execute_proposal_hook(deps.as_ref(), info.sender, msgs) } ExecuteMsg::Pause { duration } => execute_pause(deps, env, info.sender, duration), + #[cfg(feature = "cw20")] ExecuteMsg::Receive(_) => execute_receive_cw20(deps, info.sender), ExecuteMsg::ReceiveNft(_) => execute_receive_cw721(deps, info.sender), ExecuteMsg::RemoveItem { key } => execute_remove_item(deps, env, info.sender, key), @@ -116,6 +122,7 @@ pub fn execute( ExecuteMsg::UpdateConfig { config } => { execute_update_config(deps, env, info.sender, config) } + #[cfg(feature = "cw20")] ExecuteMsg::UpdateCw20List { to_add, to_remove } => { execute_update_cw20_list(deps, env, info.sender, to_add, to_remove) } @@ -397,6 +404,7 @@ fn do_update_addr_list( Ok(()) } +#[cfg(feature = "cw20")] pub fn execute_update_cw20_list( deps: DepsMut, env: Env, @@ -504,6 +512,7 @@ pub fn execute_update_sub_daos_list( .add_attribute("sender", sender)) } +#[cfg(feature = "cw20")] pub fn execute_receive_cw20(deps: DepsMut, sender: Addr) -> Result { let config = CONFIG.load(deps.storage)?; if !config.automatically_add_cw20s { @@ -534,7 +543,9 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { QueryMsg::Admin {} => query_admin(deps), QueryMsg::AdminNomination {} => query_admin_nomination(deps), QueryMsg::Config {} => query_config(deps), + #[cfg(feature = "cw20")] QueryMsg::Cw20TokenList { start_after, limit } => query_cw20_list(deps, start_after, limit), + #[cfg(feature = "cw20")] QueryMsg::Cw20Balances { start_after, limit } => { query_cw20_balances(deps, env, start_after, limit) } @@ -727,6 +738,7 @@ pub fn query_list_items( )?) } +#[cfg(feature = "cw20")] pub fn query_cw20_list( deps: Deps, start_after: Option, @@ -759,6 +771,7 @@ pub fn query_cw721_list( )?) } +#[cfg(feature = "cw20")] pub fn query_cw20_balances( deps: Deps, env: Env, @@ -866,6 +879,7 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result, to_remove: Vec, @@ -145,6 +148,7 @@ pub enum QueryMsg { Config {}, /// Gets the token balance for each cw20 registered with the /// contract. + #[cfg(feature = "cw20")] #[returns(crate::query::Cw20BalanceResponse)] Cw20Balances { start_after: Option, @@ -152,6 +156,7 @@ pub enum QueryMsg { }, /// Lists the addresses of the cw20 tokens in this contract's /// treasury. + #[cfg(feature = "cw20")] #[returns(Vec)] Cw20TokenList { start_after: Option, diff --git a/contracts/dao-core/src/query.rs b/contracts/dao-core/src/query.rs index e3a43f02c..49af9c149 100644 --- a/contracts/dao-core/src/query.rs +++ b/contracts/dao-core/src/query.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Uint128}; +use cosmwasm_std::Addr; use cw2::ContractVersion; use cw_utils::Expiration; @@ -45,11 +45,12 @@ pub struct GetItemResponse { /// Returned by the `Cw20Balances` query. #[cw_serde] +#[cfg(feature = "cw20")] pub struct Cw20BalanceResponse { /// The address of the token. pub addr: Addr, /// The contract's balance. - pub balance: Uint128, + pub balance: cosmwasm_std::Uint128, } /// Returned by the `AdminNomination` query. diff --git a/contracts/dao-core/src/state.rs b/contracts/dao-core/src/state.rs index 3bc0bdfbc..43b88370f 100644 --- a/contracts/dao-core/src/state.rs +++ b/contracts/dao-core/src/state.rs @@ -15,6 +15,7 @@ pub struct Config { pub image_url: Option, /// If true the contract will automatically add received cw20 /// tokens to its treasury. + #[cfg(feature = "cw20")] pub automatically_add_cw20s: bool, /// If true the contract will automatically add received cw721 /// tokens to its treasury. @@ -24,8 +25,8 @@ pub struct Config { pub dao_uri: Option, } -#[cw_serde] /// Top level type describing a proposal module. +#[cw_serde] pub struct ProposalModule { /// The address of the proposal module. pub address: Addr, @@ -36,8 +37,8 @@ pub struct ProposalModule { pub status: ProposalModuleStatus, } -#[cw_serde] /// The status of a proposal module. +#[cw_serde] pub enum ProposalModuleStatus { Enabled, Disabled, @@ -86,6 +87,7 @@ pub const ITEMS: Map = Map::new("items"); /// Set of cw20 tokens that have been registered with this contract's /// treasury. +#[cfg(feature = "cw20")] pub const CW20_LIST: Map = Map::new("cw20s"); /// Set of cw721 tokens that have been registered with this contract's /// treasury. diff --git a/contracts/dao-core/src/tests.rs b/contracts/dao-core/src/tests.rs index e50f409f6..d5f31217d 100644 --- a/contracts/dao-core/src/tests.rs +++ b/contracts/dao-core/src/tests.rs @@ -13,12 +13,15 @@ use dao_interface::{ Admin, ModuleInstantiateInfo, }; +#[cfg(feature = "cw20")] +use crate::query::Cw20BalanceResponse; + use crate::{ contract::{derive_proposal_module_prefix, migrate, CONTRACT_NAME, CONTRACT_VERSION}, msg::{ExecuteMsg, InitialItem, InstantiateMsg, MigrateMsg, QueryMsg}, query::{ - AdminNominationResponse, Cw20BalanceResponse, DaoURIResponse, DumpStateResponse, - GetItemResponse, PauseInfoResponse, SubDao, + AdminNominationResponse, DaoURIResponse, DumpStateResponse, GetItemResponse, + PauseInfoResponse, SubDao, }, state::{Config, ProposalModule, ProposalModuleStatus, PROPOSAL_MODULES}, ContractError, @@ -113,6 +116,7 @@ fn test_instantiate_with_n_gov_modules(n: usize) { name: "DAO DAO".to_string(), description: "A DAO that builds DAOs.".to_string(), image_url: None, + #[cfg(feature = "cw20")] automatically_add_cw20s: true, automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { @@ -145,6 +149,7 @@ fn test_instantiate_with_n_gov_modules(n: usize) { name: "DAO DAO".to_string(), description: "A DAO that builds DAOs.".to_string(), image_url: None, + #[cfg(feature = "cw20")] automatically_add_cw20s: true, automatically_add_cw721s: true, } @@ -216,6 +221,7 @@ makes wickedness." name: "DAO DAO".to_string(), description: "A DAO that builds DAOs.".to_string(), image_url: None, + #[cfg(feature = "cw20")] automatically_add_cw20s: true, automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { @@ -246,6 +252,7 @@ fn test_update_config() { name: "DAO DAO".to_string(), description: "A DAO that builds DAOs.".to_string(), image_url: None, + #[cfg(feature = "cw20")] automatically_add_cw20s: true, automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { @@ -291,6 +298,7 @@ fn test_update_config() { name: "Root DAO".to_string(), description: "We love trees and sudo.".to_string(), image_url: Some("https://moonphase.is/image.svg".to_string()), + #[cfg(feature = "cw20")] automatically_add_cw20s: false, automatically_add_cw721s: true, dao_uri: Some("https://daostar.one/EIP".to_string()), @@ -343,6 +351,7 @@ fn test_swap_governance(swaps: Vec<(u32, u32)>) { name: "DAO DAO".to_string(), description: "A DAO that builds DAOs.".to_string(), image_url: None, + #[cfg(feature = "cw20")] automatically_add_cw20s: true, automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { @@ -495,6 +504,7 @@ fn test_removed_modules_can_not_execute() { name: "DAO DAO".to_string(), description: "A DAO that builds DAOs.".to_string(), image_url: None, + #[cfg(feature = "cw20")] automatically_add_cw20s: true, automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { @@ -652,6 +662,7 @@ fn test_module_already_disabled() { name: "DAO DAO".to_string(), description: "A DAO that builds DAOs.".to_string(), image_url: None, + #[cfg(feature = "cw20")] automatically_add_cw20s: true, automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { @@ -751,6 +762,7 @@ fn test_swap_voting_module() { name: "DAO DAO".to_string(), description: "A DAO that builds DAOs.".to_string(), image_url: None, + #[cfg(feature = "cw20")] automatically_add_cw20s: true, automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { @@ -867,6 +879,7 @@ fn test_permissions() { label: "governance module".to_string(), }], initial_items: None, + #[cfg(feature = "cw20")] automatically_add_cw20s: true, automatically_add_cw721s: true, }; @@ -913,6 +926,7 @@ fn test_permissions() { name: "Evil config.".to_string(), description: "👿".to_string(), image_url: None, + #[cfg(feature = "cw20")] automatically_add_cw20s: true, automatically_add_cw721s: true, }, @@ -951,6 +965,7 @@ fn do_standard_instantiate(auto_add: bool, admin: Option) -> (Addr, App) name: "DAO DAO".to_string(), description: "A DAO that builds DAOs.".to_string(), image_url: None, + #[cfg(feature = "cw20")] automatically_add_cw20s: auto_add, automatically_add_cw721s: auto_add, voting_module_instantiate_info: ModuleInstantiateInfo { @@ -1583,6 +1598,7 @@ fn test_list_items() { name: "DAO DAO".to_string(), description: "A DAO that builds DAOs.".to_string(), image_url: None, + #[cfg(feature = "cw20")] automatically_add_cw20s: true, automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { @@ -1700,6 +1716,7 @@ fn test_instantiate_with_items() { name: "DAO DAO".to_string(), description: "A DAO that builds DAOs.".to_string(), image_url: None, + #[cfg(feature = "cw20")] automatically_add_cw20s: true, automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { @@ -1760,6 +1777,7 @@ fn test_instantiate_with_items() { assert_eq!(item1_value, Some("item1_value".to_string())) } +#[cfg(feature = "cw20")] #[test] fn test_cw20_receive_auto_add() { let (gov_addr, mut app) = do_standard_instantiate(true, None); @@ -1795,19 +1813,21 @@ fn test_cw20_receive_auto_add() { ) .unwrap(); - // Check that the balances query works with no tokens. - let cw20_balances: Vec = app - .wrap() - .query_wasm_smart( - gov_addr.clone(), - &QueryMsg::Cw20Balances { - start_after: None, - limit: None, - }, - ) - .unwrap(); - assert_eq!(cw20_balances, vec![]); - + #[cfg(feature = "cw20")] + { + // Check that the balances query works with no tokens. + let cw20_balances: Vec = app + .wrap() + .query_wasm_smart( + gov_addr.clone(), + &QueryMsg::Cw20Balances { + start_after: None, + limit: None, + }, + ) + .unwrap(); + assert_eq!(cw20_balances, vec![]); + } // Send a gov token to the governance contract. app.execute_contract( Addr::unchecked(CREATOR_ADDR), @@ -1907,6 +1927,7 @@ fn test_cw20_receive_auto_add() { assert_eq!(cw20_list, vec![another_cw20]); } +#[cfg(feature = "cw20")] #[test] fn test_cw20_receive_no_auto_add() { let (gov_addr, mut app) = do_standard_instantiate(false, None); @@ -2273,6 +2294,7 @@ fn test_pause() { name: "The Empire Strikes Back".to_string(), description: "haha lol we have pwned your DAO".to_string(), image_url: None, + #[cfg(feature = "cw20")] automatically_add_cw20s: true, automatically_add_cw721s: true, }, @@ -2349,6 +2371,7 @@ fn test_pause() { name: "The Empire Strikes Back Again".to_string(), description: "haha lol we have pwned your DAO again".to_string(), image_url: None, + #[cfg(feature = "cw20")] automatically_add_cw20s: true, automatically_add_cw721s: true, }, @@ -2526,6 +2549,7 @@ fn test_migrate_from_compatible() { name: "DAO DAO".to_string(), description: "A DAO that builds DAOs.".to_string(), image_url: None, + #[cfg(feature = "cw20")] automatically_add_cw20s: false, automatically_add_cw721s: false, voting_module_instantiate_info: ModuleInstantiateInfo { @@ -2724,6 +2748,7 @@ fn test_migrate_mock() { assert_eq!(v2_config.name, v1_config.name); assert_eq!(v2_config.description, v1_config.description); assert_eq!(v2_config.image_url, v1_config.image_url); + #[cfg(feature = "cw20")] assert_eq!( v2_config.automatically_add_cw20s, v1_config.automatically_add_cw20s @@ -2782,6 +2807,7 @@ fn test_module_prefixes() { name: "DAO DAO".to_string(), description: "A DAO that builds DAOs.".to_string(), image_url: None, + #[cfg(feature = "cw20")] automatically_add_cw20s: true, automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo {