Repository: public-awesome/launchpad
Pull request #481 (2022-12-13 05:32:08)

Add executor to splits
An executor can be a member of the group, any address, or `None`. A zero weight member can also be an executor. This is useful for bots. i.e: A bot tied to a cron job that calls `distribute()` periodically.



Patch:
diff --git a/contracts/splits/.editorconfig b/contracts/splits/.editorconfig
deleted file mode 100644
index 3d36f20b..00000000
--- a/contracts/splits/.editorconfig
+++ /dev/null
@@ -1,11 +0,0 @@
-root = true
-
-[*]
-indent_style = space
-indent_size = 2
-charset = utf-8
-trim_trailing_whitespace = true
-insert_final_newline = true
-
-[*.rs]
-indent_size = 4
diff --git a/contracts/splits/.gitignore b/contracts/splits/.gitignore
deleted file mode 100644
index dfdaaa6b..00000000
--- a/contracts/splits/.gitignore
+++ /dev/null
@@ -1,15 +0,0 @@
-# Build results
-/target
-
-# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327)
-.cargo-ok
-
-# Text file backups
-**/*.rs.bk
-
-# macOS
-.DS_Store
-
-# IDEs
-*.iml
-.idea
diff --git a/contracts/splits/Cargo.toml b/contracts/splits/Cargo.toml
index fa367c92..cb2ae5f7 100644
--- a/contracts/splits/Cargo.toml
+++ b/contracts/splits/Cargo.toml
@@ -25,13 +25,6 @@ backtraces = ["cosmwasm-std/backtraces"]
 # use library feature to disable all instantiate/execute/query exports
 library = []
 
-[package.metadata.scripts]
-optimize = """docker run --rm -v "$(pwd)":/code \
-  --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
-  --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
-  cosmwasm/rust-optimizer:0.12.9
-"""
-
 [dependencies]
 cosmwasm-schema   = { workspace = true }
 cosmwasm-std      = { workspace = true }
diff --git a/contracts/splits/src/contract.rs b/contracts/splits/src/contract.rs
index fa0a7556..f28d3f25 100644
--- a/contracts/splits/src/contract.rs
+++ b/contracts/splits/src/contract.rs
@@ -10,7 +10,7 @@ use sg_std::NATIVE_DENOM;
 
 use crate::error::ContractError;
 use crate::msg::{ConfigResponse, ExecuteMsg, InstantiateMsg, QueryMsg};
-use crate::state::{Config, CONFIG};
+use crate::state::{Config, Executor, CONFIG};
 
 // version info for migration info
 pub const CONTRACT_NAME: &str = "crates.io:sg-splits";
@@ -35,7 +35,14 @@ pub fn instantiate(
 
     set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
 
-    let cfg = Config { group_addr };
+    if let Some(Executor::Only(ref addr)) = msg.executor {
+        deps.api.addr_validate(addr.as_ref())?;
+    }
+
+    let cfg = Config {
+        group_addr,
+        executor: msg.executor,
+    };
     CONFIG.save(deps.storage, &cfg)?;
 
     Ok(Response::default())
@@ -60,16 +67,15 @@ pub fn execute_distribute(
 ) -> Result<Response, ContractError> {
     let config = CONFIG.load(deps.storage)?;
 
-    // only a member can distribute funds
-    let weight = config
-        .group_addr
-        .is_member(&deps.querier, &info.sender, None)?
-        .ok_or(ContractError::Unauthorized {})?;
-    if weight == 0 {
-        return Err(ContractError::InvalidWeight { weight });
-    }
+    config.authorize(&deps.querier, &info.sender)?;
 
     let total_weight = config.group_addr.total_weight(&deps.querier)?;
+    if total_weight == 0 {
+        return Err(ContractError::InvalidWeight {
+            weight: total_weight,
+        });
+    }
+
     let members = config.group_addr.list_members(&deps.querier, None, None)?;
 
     let funds = deps
diff --git a/contracts/splits/src/error.rs b/contracts/splits/src/error.rs
index 9a2f514f..d252336f 100644
--- a/contracts/splits/src/error.rs
+++ b/contracts/splits/src/error.rs
@@ -17,4 +17,7 @@ pub enum ContractError {
 
     #[error("Group contract invalid total weight '{weight}'")]
     InvalidWeight { weight: u64 },
+
+    #[error("Invalid executor '{addr}'")]
+    InvalidExecutor { addr: String },
 }
diff --git a/contracts/splits/src/msg.rs b/contracts/splits/src/msg.rs
index 8706c88a..f2bd2bf4 100644
--- a/contracts/splits/src/msg.rs
+++ b/contracts/splits/src/msg.rs
@@ -1,11 +1,14 @@
 use cosmwasm_schema::cw_serde;
 
-use crate::state::Config;
+use crate::state::{Config, Executor};
 
 #[cw_serde]
 pub struct InstantiateMsg {
     // this is the group contract that contains the member list
     pub group_addr: String,
+    // Who is able to call distribute
+    // None means that anyone can call distribute
+    pub executor: Option<Executor>,
 }
 
 #[cw_serde]
diff --git a/contracts/splits/src/state.rs b/contracts/splits/src/state.rs
index 07bc64b1..54ca2840 100644
--- a/contracts/splits/src/state.rs
+++ b/contracts/splits/src/state.rs
@@ -1,14 +1,51 @@
+use cosmwasm_schema::cw_serde;
+use cosmwasm_std::{Addr, QuerierWrapper};
 use cw4::Cw4Contract;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
 
 use cw_storage_plus::Item;
 
-#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
+use crate::ContractError;
+
+#[cw_serde]
+pub enum Executor {
+    /// Any member of the voting group, even with 0 points
+    Member,
+    /// Only the given address
+    Only(Addr),
+}
+
+#[cw_serde]
 pub struct Config {
     // Total weight and members are queried from this contract
     pub group_addr: Cw4Contract,
+    // Who is able to call distribute
+    // None means that anyone can call distribute
+    pub executor: Option<Executor>,
 }
 
 // unique items
 pub const CONFIG: Item<Config> = Item::new("config");
+
+impl Config {
+    // Executor can be set in 3 ways:
+    // - Member: any member of the voting group is authorized
+    // - Only: only passed address is authorized
+    // - None: Everyone are authorized
+    pub fn authorize(&self, querier: &QuerierWrapper, sender: &Addr) -> Result<(), ContractError> {
+        if let Some(executor) = &self.executor {
+            match executor {
+                Executor::Member => {
+                    self.group_addr
+                        .is_member(querier, sender, None)?
+                        .ok_or(ContractError::Unauthorized {})?;
+                }
+                Executor::Only(addr) => {
+                    if addr != sender {
+                        return Err(ContractError::Unauthorized {});
+                    }
+                }
+            }
+        }
+        Ok(())
+    }
+}


Test patch:
diff --git a/contracts/splits/src/integration_tests.rs b/contracts/splits/src/integration_tests.rs
index a6117ccf..8b8ef350 100644
--- a/contracts/splits/src/integration_tests.rs
+++ b/contracts/splits/src/integration_tests.rs
@@ -2,12 +2,15 @@
 mod tests {
     use crate::{
         msg::{InstantiateMsg, QueryMsg},
+        state::Executor,
         ContractError,
     };
     use cosmwasm_std::{Addr, Coin, Empty};
     use cw2::{query_contract_info, ContractVersion};
     use cw4::{Cw4ExecuteMsg, Member, MemberListResponse};
-    use cw_multi_test::{next_block, App, AppBuilder, Contract, ContractWrapper, Executor};
+    use cw_multi_test::{
+        next_block, App, AppBuilder, Contract, ContractWrapper, Executor as TestExecutor,
+    };
 
     fn member<T: Into<String>>(addr: T, weight: u64) -> Member {
         Member {
@@ -60,10 +63,11 @@ mod tests {
     }
 
     #[track_caller]
-    fn instantiate_splits(app: &mut App, group: Addr) -> Addr {
+    fn instantiate_splits(app: &mut App, group: Addr, executor: Option<Executor>) -> Addr {
         let flex_id = app.store_code(contract_splits());
         let msg = crate::msg::InstantiateMsg {
             group_addr: group.to_string(),
+            executor,
         };
         app.instantiate_contract(flex_id, Addr::unchecked(OWNER), &msg, &[], "splits", None)
             .unwrap()
@@ -74,6 +78,7 @@ mod tests {
         app: &mut App,
         init_funds: Vec<Coin>,
         multisig_as_group_admin: bool,
+        executor: Option<Executor>,
     ) -> (Addr, Addr) {
         // 1. Instantiate group contract with members (and OWNER as admin)
         let members = vec![
@@ -86,7 +91,7 @@ mod tests {
         app.update_block(next_block);
 
         // 2. Set up Splits backed by this group
-        let splits_addr = instantiate_splits(app, group_addr.clone());
+        let splits_addr = instantiate_splits(app, group_addr.clone(), executor);
         app.update_block(next_block);
 
         // 3. (Optional) Set the multisig as the group owner
@@ -123,6 +128,7 @@ mod tests {
         // Zero weight fails
         let instantiate_msg = InstantiateMsg {
             group_addr: group_addr.to_string(),
+            executor: Some(Executor::Only(Addr::unchecked(OWNER))),
         };
         let err = app
             .instantiate_contract(
@@ -144,6 +150,7 @@ mod tests {
 
         let instantiate_msg = InstantiateMsg {
             group_addr: group_addr.to_string(),
+            executor: None,
         };
         let splits_addr = app
             .instantiate_contract(
@@ -196,7 +203,7 @@ mod tests {
         fn distribute_zero_funds() {
             let mut app = mock_app(&[]);
 
-            let (splits_addr, _) = setup_test_case(&mut app, vec![], false);
+            let (splits_addr, _) = setup_test_case(&mut app, vec![], false, None);
 
             let msg = ExecuteMsg::Distribute {};
 
@@ -212,7 +219,8 @@ mod tests {
             let init_funds = coins(100, DENOM);
             let mut app = mock_app(&init_funds);
 
-            let (splits_addr, _) = setup_test_case(&mut app, init_funds, false);
+            let (splits_addr, _) =
+                setup_test_case(&mut app, init_funds, false, Some(Executor::Member));
 
             let msg = ExecuteMsg::Distribute {};
 
@@ -231,7 +239,7 @@ mod tests {
             let init_funds = coins(100, DENOM);
             let mut app = mock_app(&init_funds);
 
-            let (splits_addr, _) = setup_test_case(&mut app, init_funds, false);
+            let (splits_addr, _) = setup_test_case(&mut app, init_funds, false, None);
 
             let msg = ExecuteMsg::Distribute {};
 
diff --git a/contracts/vending-minter/src/integration_tests.rs b/contracts/vending-minter/src/integration_tests.rs
index 80750056..451c7fd8 100644
--- a/contracts/vending-minter/src/integration_tests.rs
+++ b/contracts/vending-minter/src/integration_tests.rs
@@ -297,6 +297,7 @@ fn instantiate_splits(app: &mut StargazeApp, group: Addr) -> Addr {
     println!("splits_id: {}", splits_id);
     let msg = sg_splits::msg::InstantiateMsg {
         group_addr: group.to_string(),
+        executor: None,
     };
     app.instantiate_contract(splits_id, Addr::unchecked(OWNER), &msg, &[], "splits", None)
         .unwrap()