Repository: DA0-DA0/dao-contracts
Pull request #189 (2022-03-06 12:29:51)

Instantiate DAO with existing staking contract
We now have an added enum in instantiate to create a DAO with an existing staking contract.

- Similar to how we handle CW20s.
- If we instantiate a new CW20 the contract will error if the user specifies to use an existing staking contract. As this action is invalid
- Existing CW20s can use both a new staking contract or an older one
- Test cases cover all possible combinations

Feedback appreciated.


Patch:
diff --git a/contracts/cw3-dao/src/contract.rs b/contracts/cw3-dao/src/contract.rs
index 9ab780b1..3484b439 100644
--- a/contracts/cw3-dao/src/contract.rs
+++ b/contracts/cw3-dao/src/contract.rs
@@ -3,7 +3,9 @@ use crate::helpers::{
     get_and_check_limit, get_deposit_message, get_proposal_deposit_refund_message,
     get_staked_balance, get_total_staked_supply, get_voting_power_at_height, map_proposal,
 };
-use crate::msg::{ExecuteMsg, GovTokenMsg, InstantiateMsg, ProposeMsg, QueryMsg, VoteMsg};
+use crate::msg::{
+    ExecuteMsg, GovTokenMsg, InstantiateMsg, ProposeMsg, QueryMsg, StakingContractMsg, VoteMsg,
+};
 use crate::query::{
     ConfigResponse, Cw20BalancesResponse, ProposalListResponse, ProposalResponse,
     ThresholdResponse, TokenListResponse, VoteInfo, VoteListResponse, VoteResponse,
@@ -11,8 +13,7 @@ use crate::query::{
 };
 use crate::state::{
     next_id, Ballot, Config, Proposal, Votes, BALLOTS, CONFIG, DAO_PAUSED, GOV_TOKEN, PROPOSALS,
-    STAKING_CONTRACT, STAKING_CONTRACT_CODE_ID, STAKING_CONTRACT_UNSTAKING_DURATION,
-    TREASURY_TOKENS,
+    STAKING_CONTRACT, STAKING_CONTRACT_DETAILS, TREASURY_TOKENS,
 };
 use cosmwasm_std::{
     entry_point, to_binary, Addr, Binary, CosmosMsg, Decimal, Deps, DepsMut, Empty, Env,
@@ -67,14 +68,14 @@ pub fn instantiate(
 
     let mut msgs: Vec<SubMsg> = vec![];
 
+    let staking_contract = msg.staking_contract;
+
     match msg.gov_token {
         GovTokenMsg::InstantiateNewCw20 {
             cw20_code_id,
-            stake_contract_code_id,
             label,
             initial_dao_balance,
             msg,
-            unstaking_duration,
         } => {
             // Check that someone has an initial balance to be able to vote in the DAO
             if msg.initial_balances.is_empty() {
@@ -92,8 +93,8 @@ pub fn instantiate(
             }
 
             // Save info for use in reply SubMsgs
-            STAKING_CONTRACT_CODE_ID.save(deps.storage, &stake_contract_code_id)?;
-            STAKING_CONTRACT_UNSTAKING_DURATION.save(deps.storage, &unstaking_duration)?;
+
+            STAKING_CONTRACT_DETAILS.save(deps.storage, &staking_contract)?;
 
             // Instantiate new Gov Token with DAO as admin and minter
             let msg = WasmMsg::Instantiate {
@@ -118,12 +119,7 @@ pub fn instantiate(
 
             msgs.append(&mut vec![msg]);
         }
-        GovTokenMsg::UseExistingCw20 {
-            addr,
-            stake_contract_code_id,
-            label,
-            unstaking_duration,
-        } => {
+        GovTokenMsg::UseExistingCw20 { addr, label } => {
             let cw20_addr = Cw20Contract(
                 deps.api
                     .addr_validate(&addr)
@@ -136,22 +132,43 @@ pub fn instantiate(
             // Save gov token
             GOV_TOKEN.save(deps.storage, &cw20_addr.addr())?;
 
-            // Instantiate staking contract with DAO as admin
-            let msg = WasmMsg::Instantiate {
-                code_id: stake_contract_code_id,
-                funds: vec![],
-                admin: Some(env.contract.address.to_string()),
-                label,
-                msg: to_binary(&stake_cw20::msg::InstantiateMsg {
-                    admin: Some(env.contract.address.to_string()),
+            match staking_contract {
+                StakingContractMsg::InstantiateNewStakingContract {
+                    staking_contract_code_id,
                     unstaking_duration,
-                    token_address: cw20_addr.addr().to_string(),
-                })?,
-            };
+                } => {
+                    let msg = WasmMsg::Instantiate {
+                        code_id: staking_contract_code_id,
+                        funds: vec![],
+                        admin: Some(env.contract.address.to_string()),
+                        label,
+                        msg: to_binary(&stake_cw20::msg::InstantiateMsg {
+                            admin: Some(env.contract.address.to_string()),
+                            unstaking_duration,
+                            token_address: cw20_addr.addr().to_string(),
+                        })?,
+                    };
+                    let msg = SubMsg::reply_on_success(msg, INSTANTIATE_STAKING_CONTRACT_REPLY_ID);
 
-            let msg = SubMsg::reply_on_success(msg, INSTANTIATE_STAKING_CONTRACT_REPLY_ID);
+                    msgs.append(&mut vec![msg]);
+                }
+                StakingContractMsg::UseExistingStakingContract { addr } => {
+                    // Validate the address
+                    let staking_contract_addr = deps.api.addr_validate(&addr)?;
 
-            msgs.append(&mut vec![msg]);
+                    let resp: stake_cw20::msg::GetConfigResponse = deps.querier.query_wasm_smart(
+                        &staking_contract_addr,
+                        &stake_cw20::msg::QueryMsg::GetConfig {},
+                    )?;
+
+                    if cw20_addr.addr() != resp.token_address {
+                        return Err(ContractError::StakingContractMismatch {});
+                    }
+
+                    // Save the validated address
+                    STAKING_CONTRACT.save(deps.storage, &staking_contract_addr)?;
+                }
+            }
         }
     };
 
@@ -763,24 +780,35 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result<Response, ContractEr
                     // Save gov token
                     GOV_TOKEN.save(deps.storage, &cw20_addr)?;
 
-                    // Instantiate staking contract with DAO as admin
-                    let code_id = STAKING_CONTRACT_CODE_ID.load(deps.storage)?;
-                    let unstaking_duration =
-                        STAKING_CONTRACT_UNSTAKING_DURATION.load(deps.storage)?;
-                    let msg = WasmMsg::Instantiate {
-                        code_id,
-                        funds: vec![],
-                        admin: Some(env.contract.address.to_string()),
-                        label: env.contract.address.to_string(),
-                        msg: to_binary(&stake_cw20::msg::InstantiateMsg {
-                            admin: Some(env.contract.address.to_string()),
+                    // Load staking contract details
+                    let staking_contract = STAKING_CONTRACT_DETAILS.load(deps.storage)?;
+                    match staking_contract {
+                        // Create a new staking contract
+                        StakingContractMsg::InstantiateNewStakingContract {
+                            staking_contract_code_id,
                             unstaking_duration,
-                            token_address: cw20_addr.to_string(),
-                        })?,
-                    };
-                    let msg = SubMsg::reply_on_success(msg, INSTANTIATE_STAKING_CONTRACT_REPLY_ID);
-
-                    Ok(Response::new().add_submessage(msg))
+                        } => {
+                            let msg = WasmMsg::Instantiate {
+                                code_id: staking_contract_code_id,
+                                funds: vec![],
+                                admin: Some(env.contract.address.to_string()),
+                                label: env.contract.address.to_string(),
+                                msg: to_binary(&stake_cw20::msg::InstantiateMsg {
+                                    admin: Some(env.contract.address.to_string()),
+                                    unstaking_duration,
+                                    token_address: cw20_addr.to_string(),
+                                })?,
+                            };
+                            let msg = SubMsg::reply_on_success(
+                                msg,
+                                INSTANTIATE_STAKING_CONTRACT_REPLY_ID,
+                            );
+                            Ok(Response::new().add_submessage(msg))
+                        }
+                        StakingContractMsg::UseExistingStakingContract { addr: _addr } => {
+                            Err(ContractError::CannotUseExistingStakingContract {})
+                        }
+                    }
                 }
                 Err(_) => Err(ContractError::InstantiateGovTokenError {}),
             }
@@ -792,7 +820,7 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result<Response, ContractEr
                     // Validate contract address
                     let staking_contract_addr = deps.api.addr_validate(&res.contract_address)?;
 
-                    // Save gov token
+                    // Save staking contract addr
                     STAKING_CONTRACT.save(deps.storage, &staking_contract_addr)?;
 
                     Ok(Response::new())
diff --git a/contracts/cw3-dao/src/error.rs b/contracts/cw3-dao/src/error.rs
index 43b9c777..5077f0d0 100644
--- a/contracts/cw3-dao/src/error.rs
+++ b/contracts/cw3-dao/src/error.rs
@@ -53,4 +53,10 @@ pub enum ContractError {
 
     #[error("DAO is paused")]
     Paused {},
+
+    #[error("Cannot use an existing staking contract with a new CW20")]
+    CannotUseExistingStakingContract {},
+
+    #[error("Staking contract CW20 and DAO CW20 do not match")]
+    StakingContractMismatch {},
 }
diff --git a/contracts/cw3-dao/src/msg.rs b/contracts/cw3-dao/src/msg.rs
index 5a01606a..264f0a63 100644
--- a/contracts/cw3-dao/src/msg.rs
+++ b/contracts/cw3-dao/src/msg.rs
@@ -17,6 +17,8 @@ pub struct InstantiateMsg {
     pub description: String,
     /// Set an existing governance token or launch a new one
     pub gov_token: GovTokenMsg,
+    /// Set an existing staking contract or instantiate an new one
+    pub staking_contract: StakingContractMsg,
     /// Voting params configuration
     pub threshold: Threshold,
     /// The amount of time a proposal can be voted on before expiring
@@ -38,19 +40,25 @@ pub enum GovTokenMsg {
     // Instantiate a new cw20 token with the DAO as minter
     InstantiateNewCw20 {
         cw20_code_id: u64,
-        stake_contract_code_id: u64,
         label: String,
         initial_dao_balance: Option<Uint128>,
         msg: GovTokenInstantiateMsg,
-        unstaking_duration: Option<Duration>,
     },
     /// Use an existing cw20 token
-    UseExistingCw20 {
-        addr: String,
-        label: String,
-        stake_contract_code_id: u64,
+    UseExistingCw20 { addr: String, label: String },
+}
+
+#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
+#[serde(rename_all = "snake_case")]
+#[allow(clippy::large_enum_variant)]
+pub enum StakingContractMsg {
+    /// Create a new staking contract
+    InstantiateNewStakingContract {
+        staking_contract_code_id: u64,
         unstaking_duration: Option<Duration>,
     },
+    /// Use an existing already instantiated staking contract
+    UseExistingStakingContract { addr: String },
 }
 
 #[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, PartialEq)]
diff --git a/contracts/cw3-dao/src/state.rs b/contracts/cw3-dao/src/state.rs
index 9d4dc818..1fddb3de 100644
--- a/contracts/cw3-dao/src/state.rs
+++ b/contracts/cw3-dao/src/state.rs
@@ -1,4 +1,4 @@
-use crate::msg::Threshold;
+use crate::msg::{StakingContractMsg, Threshold};
 use cosmwasm_std::{
     Addr, BlockInfo, CosmosMsg, Decimal, Empty, StdError, StdResult, Storage, Uint128,
 };
@@ -180,9 +180,8 @@ pub const STAKING_CONTRACT: Item<Addr> = Item::new("staking_contract");
 pub const GOV_TOKEN: Item<Addr> = Item::new("gov_token");
 
 // Stores staking contract CODE ID and Unbonding time for use in a reply
-pub const STAKING_CONTRACT_CODE_ID: Item<u64> = Item::new("staking_contract_code_id");
-pub const STAKING_CONTRACT_UNSTAKING_DURATION: Item<Option<Duration>> =
-    Item::new("staking_contract_unstaking_duration");
+pub const STAKING_CONTRACT_DETAILS: Item<StakingContractMsg> =
+    Item::new("staking_contract_details");
 
 // Multiple-item map
 pub const BALLOTS: Map<(u64, &Addr), Ballot> = Map::new("votes");


Test patch:
diff --git a/contracts/cw3-dao/src/tests.rs b/contracts/cw3-dao/src/tests.rs
index cb6ec621..83e21f4e 100644
--- a/contracts/cw3-dao/src/tests.rs
+++ b/contracts/cw3-dao/src/tests.rs
@@ -2,7 +2,7 @@ use crate::contract::{CONTRACT_NAME, CONTRACT_VERSION};
 use crate::error::ContractError;
 use crate::msg::{
     ExecuteMsg, GovTokenInstantiateMsg, GovTokenMsg, InstantiateMsg, ProposeMsg, QueryMsg,
-    Threshold, VoteMsg,
+    StakingContractMsg, Threshold, VoteMsg,
 };
 use crate::query::{
     ConfigResponse, Cw20BalancesResponse, ProposalListResponse, ProposalResponse,
@@ -104,8 +104,10 @@ fn instantiate_dao(
         description: "a great DAO!".to_string(),
         gov_token: GovTokenMsg::UseExistingCw20 {
             addr: cw20.to_string(),
-            stake_contract_code_id: staking_code_id,
             label: "dao-dao".to_string(),
+        },
+        staking_contract: StakingContractMsg::InstantiateNewStakingContract {
+            staking_contract_code_id: staking_code_id,
             unstaking_duration: None,
         },
         threshold,
@@ -264,8 +266,10 @@ fn test_instantiate_works() {
         description: "a great DAO!".to_string(),
         gov_token: GovTokenMsg::UseExistingCw20 {
             addr: cw20_addr.to_string(),
-            stake_contract_code_id,
             label: "dao-dao".to_string(),
+        },
+        staking_contract: StakingContractMsg::InstantiateNewStakingContract {
+            staking_contract_code_id: stake_contract_code_id,
             unstaking_duration: None,
         },
         threshold: Threshold::AbsolutePercentage {
@@ -317,7 +321,6 @@ fn instantiate_new_gov_token() {
         description: "a great DAO!".to_string(),
         gov_token: GovTokenMsg::InstantiateNewCw20 {
             cw20_code_id,
-            stake_contract_code_id,
             label: String::from("DAO DAO"),
             initial_dao_balance: Some(Uint128::new(1000000)),
             msg: GovTokenInstantiateMsg {
@@ -327,6 +330,9 @@ fn instantiate_new_gov_token() {
                 initial_balances: vec![],
                 marketing: None,
             },
+        },
+        staking_contract: StakingContractMsg::InstantiateNewStakingContract {
+            staking_contract_code_id: stake_contract_code_id,
             unstaking_duration: None,
         },
         threshold: Threshold::ThresholdQuorum {
@@ -378,7 +384,6 @@ fn instantiate_new_gov_token() {
         description: "a great DAO!".to_string(),
         gov_token: GovTokenMsg::InstantiateNewCw20 {
             cw20_code_id,
-            stake_contract_code_id,
             label: String::from("DAO DAO"),
             initial_dao_balance: None,
             msg: GovTokenInstantiateMsg {
@@ -388,6 +393,9 @@ fn instantiate_new_gov_token() {
                 initial_balances: initial_balances.clone(),
                 marketing: None,
             },
+        },
+        staking_contract: StakingContractMsg::InstantiateNewStakingContract {
+            staking_contract_code_id: stake_contract_code_id,
             unstaking_duration: None,
         },
         threshold: Threshold::ThresholdQuorum {
@@ -519,7 +527,6 @@ fn instantiate_new_gov_token() {
         description: "a great DAO!".to_string(),
         gov_token: GovTokenMsg::InstantiateNewCw20 {
             cw20_code_id,
-            stake_contract_code_id,
             label: String::from("DAO DAO"),
             initial_dao_balance: Some(Uint128::new(1000)),
             msg: GovTokenInstantiateMsg {
@@ -529,6 +536,9 @@ fn instantiate_new_gov_token() {
                 initial_balances,
                 marketing: None,
             },
+        },
+        staking_contract: StakingContractMsg::InstantiateNewStakingContract {
+            staking_contract_code_id: stake_contract_code_id,
             unstaking_duration: None,
         },
         threshold: Threshold::ThresholdQuorum {
@@ -573,6 +583,221 @@ fn instantiate_new_gov_token() {
     );
 }
 
+#[test]
+fn instantiate_staking_contract() {
+    let mut app = mock_app();
+    let dao_code_id = app.store_code(contract_dao());
+    let cw20_code_id = app.store_code(contract_cw20_gov());
+    let staking_code_id = app.store_code(contract_staking());
+
+    let max_voting_period = Duration::Time(1234567);
+    let threshold = Threshold::ThresholdQuorum {
+        threshold: Decimal::percent(51),
+        quorum: Decimal::percent(10),
+    };
+
+    // Setup test case, we want to use the cw20_addr and
+    // staking_addr in later tests.
+    // This also tests the case of using an existing cw20
+    // and instantiating a new staking contract
+    let (_dao_addr, cw20_addr, staking_addr) = setup_test_case(
+        &mut app,
+        threshold,
+        max_voting_period,
+        coins(100, NATIVE_TOKEN_DENOM),
+        None,
+        None,
+    );
+
+    // Instantiate with a new staking contract, with a new CW20
+    let initial_balances = vec![
+        Cw20Coin {
+            address: OWNER.to_string(),
+            amount: Uint128::new(INITIAL_BALANCE),
+        },
+        Cw20Coin {
+            address: VOTER1.to_string(),
+            amount: Uint128::new(INITIAL_BALANCE),
+        },
+        Cw20Coin {
+            address: VOTER2.to_string(),
+            amount: Uint128::new(INITIAL_BALANCE),
+        },
+        Cw20Coin {
+            address: VOTER3.to_string(),
+            amount: Uint128::new(INITIAL_BALANCE * 2),
+        },
+        Cw20Coin {
+            address: POWER_VOTER.to_string(),
+            amount: Uint128::new(INITIAL_BALANCE * 5),
+        },
+    ];
+    let msg = InstantiateMsg {
+        name: "dao-dao".to_string(),
+        description: "a great DAO!".to_string(),
+        gov_token: GovTokenMsg::InstantiateNewCw20 {
+            cw20_code_id,
+            label: String::from("DAO DAO"),
+            initial_dao_balance: Some(Uint128::new(1000000)),
+            msg: GovTokenInstantiateMsg {
+                name: String::from("DAO DAO"),
+                symbol: String::from("DAO"),
+                decimals: 6,
+                initial_balances: initial_balances.clone(),
+                marketing: None,
+            },
+        },
+        staking_contract: StakingContractMsg::InstantiateNewStakingContract {
+            staking_contract_code_id: staking_code_id,
+            unstaking_duration: None,
+        },
+        threshold: Threshold::AbsolutePercentage {
+            percentage: Decimal::percent(51),
+        },
+        max_voting_period,
+        proposal_deposit_amount: Uint128::zero(),
+        refund_failed_proposals: None,
+        image_url: None,
+        only_members_execute: true,
+        automatically_add_cw20s: true,
+    };
+    app.instantiate_contract(
+        dao_code_id,
+        Addr::unchecked(OWNER),
+        &msg,
+        &[],
+        "cw3-dao",
+        None,
+    )
+    .unwrap();
+
+    // Instantiate with an existing staking contract, with a new CW20
+    // Expect an error as this is an invalid combination
+    let msg = InstantiateMsg {
+        name: "dao-dao".to_string(),
+        description: "a great DAO!".to_string(),
+        gov_token: GovTokenMsg::InstantiateNewCw20 {
+            cw20_code_id,
+            label: String::from("DAO DAO"),
+            initial_dao_balance: Some(Uint128::new(1000000)),
+            msg: GovTokenInstantiateMsg {
+                name: String::from("DAO DAO"),
+                symbol: String::from("DAO"),
+                decimals: 6,
+                initial_balances: initial_balances.clone(),
+                marketing: None,
+            },
+        },
+        staking_contract: StakingContractMsg::UseExistingStakingContract {
+            addr: staking_addr.to_string(),
+        },
+        threshold: Threshold::AbsolutePercentage {
+            percentage: Decimal::percent(51),
+        },
+        max_voting_period,
+        proposal_deposit_amount: Uint128::zero(),
+        refund_failed_proposals: None,
+        image_url: None,
+        only_members_execute: true,
+        automatically_add_cw20s: true,
+    };
+    app.instantiate_contract(
+        dao_code_id,
+        Addr::unchecked(OWNER),
+        &msg,
+        &[],
+        "cw3-dao",
+        None,
+    )
+    .unwrap_err();
+
+    // Create another CW20 to use to test for staking contract
+    // mismatch error
+    let msg = cw20_base::msg::InstantiateMsg {
+        name: String::from("Other"),
+        symbol: String::from("OTHER"),
+        decimals: 6,
+        initial_balances,
+        mint: None,
+        marketing: None,
+    };
+    let other_cw20_addr = app
+        .instantiate_contract(
+            cw20_code_id,
+            Addr::unchecked(OWNER),
+            &msg,
+            &[],
+            "cw20",
+            None,
+        )
+        .unwrap();
+
+    // Instantiate a DAO with an existing CW20 (other_cw20_addr) and an existing staking contract
+    // (staking_addr). We expect this to error as the CW20 address does not match the CW20
+    // address in the staking contract (queried by the GetConfig route)
+    let msg = InstantiateMsg {
+        name: "dao-dao".to_string(),
+        description: "a great DAO!".to_string(),
+        gov_token: GovTokenMsg::UseExistingCw20 {
+            addr: other_cw20_addr.to_string(),
+            label: "dao-dao".to_string(),
+        },
+        staking_contract: StakingContractMsg::UseExistingStakingContract {
+            addr: staking_addr.to_string(),
+        },
+        threshold: Threshold::AbsolutePercentage {
+            percentage: Decimal::percent(51),
+        },
+        max_voting_period,
+        proposal_deposit_amount: Uint128::zero(),
+        refund_failed_proposals: None,
+        image_url: None,
+        only_members_execute: true,
+        automatically_add_cw20s: true,
+    };
+    let _err = app
+        .instantiate_contract(
+            dao_code_id,
+            Addr::unchecked(OWNER),
+            &msg,
+            &[],
+            "cw3-dao",
+            None,
+        )
+        .unwrap_err();
+
+    // Now lets use an existing staking contract, using an existing CW20
+    let msg = InstantiateMsg {
+        name: "dao-dao".to_string(),
+        description: "a great DAO!".to_string(),
+        gov_token: GovTokenMsg::UseExistingCw20 {
+            addr: cw20_addr.to_string(),
+            label: "dao-dao".to_string(),
+        },
+        staking_contract: StakingContractMsg::UseExistingStakingContract {
+            addr: staking_addr.to_string(),
+        },
+        threshold: Threshold::AbsolutePercentage {
+            percentage: Decimal::percent(51),
+        },
+        max_voting_period,
+        proposal_deposit_amount: Uint128::zero(),
+        refund_failed_proposals: None,
+        image_url: None,
+        only_members_execute: true,
+        automatically_add_cw20s: true,
+    };
+    app.instantiate_contract(
+        dao_code_id,
+        Addr::unchecked(OWNER),
+        &msg,
+        &[],
+        "cw3-dao",
+        None,
+    )
+    .unwrap();
+}
+
 #[test]
 fn test_propose_works() {
     let mut app = mock_app();