Repository: public-awesome/launchpad
Pull request #492 (2023-01-14 01:54:07)

[SG-203] Allow creating splits with an existing group
Splits can now be created by passing in the address of a group, or it can create it's own group given the group's `code_id`. This makes it more flexible, and provides a better UX for frontend for easily creating splits.

Also introduces a `init` controller, some convenient code for use when a contract instantiates another.


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index e04f136b..50cf4725 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2193,11 +2193,13 @@ dependencies = [
  "cosmwasm-std",
  "cw-multi-test",
  "cw-storage-plus",
+ "cw-utils",
  "cw2",
  "cw4",
  "cw4-group",
  "schemars",
  "serde",
+ "sg-controllers",
  "sg-multi-test 0.21.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "sg-std 0.21.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "thiserror",
@@ -2601,6 +2603,7 @@ dependencies = [
  "rlp",
  "schemars",
  "serde",
+ "sg-controllers",
  "sg-eth-airdrop",
  "sg-multi-test 0.21.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "sg-splits",
diff --git a/Cargo.toml b/Cargo.toml
index 05a94bc4..c87410cd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,127 +1,128 @@
 [workspace]
-members = ["packages/*", "contracts/*", "test-suite/"]
+members  = ["packages/*", "contracts/*", "test-suite/"]
 resolver = "2"
 
 [workspace.package]
-version = "0.21.8"
-edition = "2021"
-homepage = "https://stargaze.zone"
+version    = "0.21.8"
+edition    = "2021"
+homepage   = "https://stargaze.zone"
 repository = "https://github.com/public-awesome/launchpad"
-license = "Apache-2.0"
+license    = "Apache-2.0"
 
 [workspace.dependencies]
-base-factory = { version = "0.21.8", path = "contracts/base-factory" }
-base-minter = { version = "0.21.8", path = "contracts/base-minter" }
-cosmwasm-schema = "1.1.5"
-cosmwasm-std = "1.1.5"
-cw2 = "0.16.0"
-cw4 = "0.16.0"
-cw4-group = "0.16.0"
-cw721 = "0.16.0"
-cw721-base = "0.16.0"
-cw-multi-test = "0.16.0"
-cw-storage-plus = "0.16.0"
-cw-utils = "0.16.0"
-schemars = "0.8.11"
-serde = { version = "1.0.145", default-features = false, features = ["derive"] }
-sg1 = "0.21.8"
-sg2 = "0.21.8"
-sg4 = "0.21.8"
-sg721 = "0.21.8"
-sg721-base = { version = "0.21.8", path = "contracts/sg721-base" }
-sg721-nt = { version = "0.21.8", path = "contracts/sg721-nt" }
-sg-metadata = "0.21.8"
-sg-multi-test = "0.21.8"
-sg-splits = { version = "0.21.8", path = "contracts/splits" }
-sg-std = "0.21.8"
-sg-whitelist = { version = "0.21.8", path = "contracts/whitelist" }
-thiserror = "1.0.31"
-url = "2.2.2"
-sha2 = { version = "0.10.2", default-features = false }
-vending-factory = { version = "0.21.8", path = "contracts/vending-factory" }
-vending-minter = { version = "0.21.8", path = "contracts/vending-minter" }
+base-factory        = { version = "0.21.8", path = "contracts/base-factory" }
+base-minter         = { version = "0.21.8", path = "contracts/base-minter" }
+cosmwasm-schema     = "1.1.5"
+cosmwasm-std        = "1.1.5"
+cw2                 = "0.16.0"
+cw4                 = "0.16.0"
+cw4-group           = "0.16.0"
+cw721               = "0.16.0"
+cw721-base          = "0.16.0"
+cw-multi-test       = "0.16.0"
+cw-storage-plus     = "0.16.0"
+cw-utils            = "0.16.0"
+schemars            = "0.8.11"
+serde               = { version = "1.0.145", default-features = false, features = ["derive"] }
+sg1                 = "0.21.8"
+sg2                 = "0.21.8"
+sg4                 = "0.21.8"
+sg721               = "0.21.8"
+sg721-base          = { version = "0.21.8", path = "contracts/sg721-base" }
+sg721-nt            = { version = "0.21.8", path = "contracts/sg721-nt" }
+sg-controllers      = { version = "0.21.8", path = "packages/controllers" }
+sg-metadata         = "0.21.8"
+sg-multi-test       = "0.21.8"
+sg-splits           = { version = "0.21.8", path = "contracts/splits" }
+sg-std              = "0.21.8"
+sg-whitelist        = { version = "0.21.8", path = "contracts/whitelist" }
+thiserror           = "1.0.31"
+url                 = "2.2.2"
+sha2                = { version = "0.10.2", default-features = false }
+vending-factory     = { version = "0.21.8", path = "contracts/vending-factory" }
+vending-minter      = { version = "0.21.8", path = "contracts/vending-minter" }
 whitelist-immutable = { version = "0.21.8", path = "contracts/whitelist-immutable" }
-sg-whitelist-basic = { version = "0.12.0" }
-ethereum-verify = { version = "0.21.8", path = "packages/ethereum-verify" }
-sg-eth-airdrop = {version = "0.21.8", path = "contracts/sg-eth-airdrop" }
-test-suite = { version = "0.21.8", path = "test-suite" }
+sg-whitelist-basic  = { version = "0.12.0" }
+ethereum-verify     = { version = "0.21.8", path = "packages/ethereum-verify" }
+sg-eth-airdrop      = { version = "0.21.8", path = "contracts/sg-eth-airdrop" }
+test-suite          = { version = "0.21.8", path = "test-suite" }
 
 [profile.release.package.sg721]
 codegen-units = 1
-incremental = false
+incremental   = false
 
 [profile.release.package.sg1]
 codegen-units = 1
-incremental = false
+incremental   = false
 
 [profile.release.package.sg2]
 codegen-units = 1
-incremental = false
+incremental   = false
 
 [profile.release.package.sg4]
 codegen-units = 1
-incremental = false
+incremental   = false
 
 [profile.release.package.sg721-base]
 codegen-units = 1
-incremental = false
+incremental   = false
 
 [profile.release.package.sg721-nt]
 codegen-units = 1
-incremental = false
+incremental   = false
 
 [profile.release.package.sg-whitelist]
 codegen-units = 1
-incremental = false
+incremental   = false
 
 [profile.release.package.sg-utils]
 codegen-units = 1
-incremental = false
+incremental   = false
 
 [profile.release.package.sg-std]
 codegen-units = 1
-incremental = false
+incremental   = false
 
 [profile.release.package.base-factory]
 codegen-units = 1
-incremental = false
+incremental   = false
 
 [profile.release.package.base-minter]
 codegen-units = 1
-incremental = false
+incremental   = false
 
 [profile.release.package.sg-eth-airdrop]
 codegen-units = 1
-incremental = false
+incremental   = false
 
 [profile.release.package.vending-factory]
 codegen-units = 1
-incremental = false
+incremental   = false
 
 [profile.release.package.vending-minter]
 codegen-units = 1
-incremental = false
+incremental   = false
 
 [profile.release.package.sg-splits]
 codegen-units = 1
-incremental = false
+incremental   = false
 
 [profile.release.package.whitelist-immutable]
 codegen-units = 1
-incremental = false
+incremental   = false
 
 [profile.release.package.ethereum-verify]
 codegen-units = 1
-incremental = false
+incremental   = false
 
 [profile.release.package.test-suite]
 codegen-units = 1
-incremental = false
+incremental   = false
 
 [profile.release]
-rpath = false
-lto = true
-overflow-checks = true
-opt-level = 3
-debug = false
-debug-assertions = false
\ No newline at end of file
+rpath            = false
+lto              = true
+overflow-checks  = true
+opt-level        = 3
+debug            = false
+debug-assertions = false
diff --git a/contracts/splits/.cargo/config b/contracts/splits/.cargo/config
index 336b618a..de992674 100644
--- a/contracts/splits/.cargo/config
+++ b/contracts/splits/.cargo/config
@@ -1,4 +1,5 @@
 [alias]
 wasm = "build --release --target wasm32-unknown-unknown"
+wasm-debug = "build --lib --target wasm32-unknown-unknown"
 unit-test = "test --lib"
-schema = "run --example schema"
+schema = "run --bin schema"
diff --git a/contracts/splits/Cargo.toml b/contracts/splits/Cargo.toml
index cb2ae5f7..c855e916 100644
--- a/contracts/splits/Cargo.toml
+++ b/contracts/splits/Cargo.toml
@@ -26,15 +26,17 @@ backtraces = ["cosmwasm-std/backtraces"]
 library = []
 
 [dependencies]
-cosmwasm-schema   = { workspace = true }
-cosmwasm-std      = { workspace = true }
-cw2               = { workspace = true }
-cw4               = { workspace = true }
-cw-storage-plus   = { workspace = true }
-schemars          = { workspace = true }
-serde             = { workspace = true }
-sg-std            = { workspace = true }
-thiserror         = { workspace = true }
+cosmwasm-schema = { workspace = true }
+cosmwasm-std    = { workspace = true }
+cw2             = { workspace = true }
+cw4             = { workspace = true }
+cw-storage-plus = { workspace = true }
+cw-utils        = { workspace = true }
+schemars        = { workspace = true }
+serde           = { workspace = true }
+sg-controllers  = { workspace = true }
+sg-std          = { workspace = true }
+thiserror       = { workspace = true }
 
 [dev-dependencies]
 cosmwasm-schema = { workspace = true }
diff --git a/contracts/splits/examples/schema.rs b/contracts/splits/examples/schema.rs
deleted file mode 100644
index 4e9095d7..00000000
--- a/contracts/splits/examples/schema.rs
+++ /dev/null
@@ -1,27 +0,0 @@
-use std::env::current_dir;
-use std::fs::create_dir_all;
-
-use cosmwasm_schema::{export_schema, export_schema_with_title, remove_schemas, schema_for};
-
-use cw4::{MemberListResponse, MemberResponse};
-use sg_splits::msg::{ConfigResponse, ExecuteMsg, InstantiateMsg, QueryMsg};
-use sg_splits::state::Config;
-
-fn main() {
-    let mut out_dir = current_dir().unwrap();
-    out_dir.push("schema");
-    create_dir_all(&out_dir).unwrap();
-    remove_schemas(&out_dir).unwrap();
-
-    export_schema(&schema_for!(InstantiateMsg), &out_dir);
-    export_schema(&schema_for!(ExecuteMsg), &out_dir);
-    export_schema(&schema_for!(QueryMsg), &out_dir);
-    export_schema(&schema_for!(Config), &out_dir);
-    export_schema(&schema_for!(ConfigResponse), &out_dir);
-    export_schema(&schema_for!(MemberResponse), &out_dir);
-    export_schema_with_title(
-        &schema_for!(MemberListResponse),
-        &out_dir,
-        "ListMembersResponse",
-    );
-}
diff --git a/contracts/splits/schema/config.json b/contracts/splits/schema/config.json
deleted file mode 100644
index 87675c91..00000000
--- a/contracts/splits/schema/config.json
+++ /dev/null
@@ -1,62 +0,0 @@
-{
-  "$schema": "http://json-schema.org/draft-07/schema#",
-  "title": "Config",
-  "type": "object",
-  "required": [
-    "group_addr"
-  ],
-  "properties": {
-    "executor": {
-      "anyOf": [
-        {
-          "$ref": "#/definitions/Executor"
-        },
-        {
-          "type": "null"
-        }
-      ]
-    },
-    "group_addr": {
-      "$ref": "#/definitions/Cw4Contract"
-    }
-  },
-  "additionalProperties": false,
-  "definitions": {
-    "Addr": {
-      "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
-      "type": "string"
-    },
-    "Cw4Contract": {
-      "description": "Cw4Contract is a wrapper around Addr that provides a lot of helpers for working with cw4 contracts\n\nIf you wish to persist this, convert to Cw4CanonicalContract via .canonical()",
-      "allOf": [
-        {
-          "$ref": "#/definitions/Addr"
-        }
-      ]
-    },
-    "Executor": {
-      "oneOf": [
-        {
-          "description": "Any member of the voting group, even with 0 points",
-          "type": "string",
-          "enum": [
-            "member"
-          ]
-        },
-        {
-          "description": "Only the given address",
-          "type": "object",
-          "required": [
-            "only"
-          ],
-          "properties": {
-            "only": {
-              "$ref": "#/definitions/Addr"
-            }
-          },
-          "additionalProperties": false
-        }
-      ]
-    }
-  }
-}
diff --git a/contracts/splits/schema/config_response.json b/contracts/splits/schema/config_response.json
deleted file mode 100644
index fb1f86ec..00000000
--- a/contracts/splits/schema/config_response.json
+++ /dev/null
@@ -1,74 +0,0 @@
-{
-  "$schema": "http://json-schema.org/draft-07/schema#",
-  "title": "ConfigResponse",
-  "type": "object",
-  "required": [
-    "config"
-  ],
-  "properties": {
-    "config": {
-      "$ref": "#/definitions/Config"
-    }
-  },
-  "additionalProperties": false,
-  "definitions": {
-    "Addr": {
-      "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
-      "type": "string"
-    },
-    "Config": {
-      "type": "object",
-      "required": [
-        "group_addr"
-      ],
-      "properties": {
-        "executor": {
-          "anyOf": [
-            {
-              "$ref": "#/definitions/Executor"
-            },
-            {
-              "type": "null"
-            }
-          ]
-        },
-        "group_addr": {
-          "$ref": "#/definitions/Cw4Contract"
-        }
-      },
-      "additionalProperties": false
-    },
-    "Cw4Contract": {
-      "description": "Cw4Contract is a wrapper around Addr that provides a lot of helpers for working with cw4 contracts\n\nIf you wish to persist this, convert to Cw4CanonicalContract via .canonical()",
-      "allOf": [
-        {
-          "$ref": "#/definitions/Addr"
-        }
-      ]
-    },
-    "Executor": {
-      "oneOf": [
-        {
-          "description": "Any member of the voting group, even with 0 points",
-          "type": "string",
-          "enum": [
-            "member"
-          ]
-        },
-        {
-          "description": "Only the given address",
-          "type": "object",
-          "required": [
-            "only"
-          ],
-          "properties": {
-            "only": {
-              "$ref": "#/definitions/Addr"
-            }
-          },
-          "additionalProperties": false
-        }
-      ]
-    }
-  }
-}
diff --git a/contracts/splits/schema/execute_msg.json b/contracts/splits/schema/execute_msg.json
deleted file mode 100644
index 7708f3a0..00000000
--- a/contracts/splits/schema/execute_msg.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
-  "$schema": "http://json-schema.org/draft-07/schema#",
-  "title": "ExecuteMsg",
-  "oneOf": [
-    {
-      "type": "object",
-      "required": [
-        "distribute"
-      ],
-      "properties": {
-        "distribute": {
-          "type": "object",
-          "additionalProperties": false
-        }
-      },
-      "additionalProperties": false
-    }
-  ]
-}
diff --git a/contracts/splits/schema/instantiate_msg.json b/contracts/splits/schema/instantiate_msg.json
deleted file mode 100644
index 99905e11..00000000
--- a/contracts/splits/schema/instantiate_msg.json
+++ /dev/null
@@ -1,54 +0,0 @@
-{
-  "$schema": "http://json-schema.org/draft-07/schema#",
-  "title": "InstantiateMsg",
-  "type": "object",
-  "required": [
-    "group_addr"
-  ],
-  "properties": {
-    "executor": {
-      "anyOf": [
-        {
-          "$ref": "#/definitions/Executor"
-        },
-        {
-          "type": "null"
-        }
-      ]
-    },
-    "group_addr": {
-      "type": "string"
-    }
-  },
-  "additionalProperties": false,
-  "definitions": {
-    "Addr": {
-      "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
-      "type": "string"
-    },
-    "Executor": {
-      "oneOf": [
-        {
-          "description": "Any member of the voting group, even with 0 points",
-          "type": "string",
-          "enum": [
-            "member"
-          ]
-        },
-        {
-          "description": "Only the given address",
-          "type": "object",
-          "required": [
-            "only"
-          ],
-          "properties": {
-            "only": {
-              "$ref": "#/definitions/Addr"
-            }
-          },
-          "additionalProperties": false
-        }
-      ]
-    }
-  }
-}
diff --git a/contracts/splits/schema/list_members_response.json b/contracts/splits/schema/list_members_response.json
deleted file mode 100644
index ea4f0768..00000000
--- a/contracts/splits/schema/list_members_response.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
-  "$schema": "http://json-schema.org/draft-07/schema#",
-  "title": "ListMembersResponse",
-  "type": "object",
-  "required": [
-    "members"
-  ],
-  "properties": {
-    "members": {
-      "type": "array",
-      "items": {
-        "$ref": "#/definitions/Member"
-      }
-    }
-  },
-  "additionalProperties": false,
-  "definitions": {
-    "Member": {
-      "description": "A group member has a weight associated with them. This may all be equal, or may have meaning in the app that makes use of the group (eg. voting power)",
-      "type": "object",
-      "required": [
-        "addr",
-        "weight"
-      ],
-      "properties": {
-        "addr": {
-          "type": "string"
-        },
-        "weight": {
-          "type": "integer",
-          "format": "uint64",
-          "minimum": 0.0
-        }
-      },
-      "additionalProperties": false
-    }
-  }
-}
diff --git a/contracts/splits/schema/member_response.json b/contracts/splits/schema/member_response.json
deleted file mode 100644
index 0de8fdcf..00000000
--- a/contracts/splits/schema/member_response.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
-  "$schema": "http://json-schema.org/draft-07/schema#",
-  "title": "MemberResponse",
-  "type": "object",
-  "properties": {
-    "weight": {
-      "type": [
-        "integer",
-        "null"
-      ],
-      "format": "uint64",
-      "minimum": 0.0
-    }
-  },
-  "additionalProperties": false
-}
diff --git a/contracts/splits/schema/query_msg.json b/contracts/splits/schema/query_msg.json
deleted file mode 100644
index a672de2b..00000000
--- a/contracts/splits/schema/query_msg.json
+++ /dev/null
@@ -1,72 +0,0 @@
-{
-  "$schema": "http://json-schema.org/draft-07/schema#",
-  "title": "QueryMsg",
-  "oneOf": [
-    {
-      "description": "Returns the config",
-      "type": "object",
-      "required": [
-        "config"
-      ],
-      "properties": {
-        "config": {
-          "type": "object",
-          "additionalProperties": false
-        }
-      },
-      "additionalProperties": false
-    },
-    {
-      "description": "Returns Member",
-      "type": "object",
-      "required": [
-        "member"
-      ],
-      "properties": {
-        "member": {
-          "type": "object",
-          "required": [
-            "address"
-          ],
-          "properties": {
-            "address": {
-              "type": "string"
-            }
-          },
-          "additionalProperties": false
-        }
-      },
-      "additionalProperties": false
-    },
-    {
-      "description": "Returns MemberListResponse",
-      "type": "object",
-      "required": [
-        "list_members"
-      ],
-      "properties": {
-        "list_members": {
-          "type": "object",
-          "properties": {
-            "limit": {
-              "type": [
-                "integer",
-                "null"
-              ],
-              "format": "uint32",
-              "minimum": 0.0
-            },
-            "start_after": {
-              "type": [
-                "string",
-                "null"
-              ]
-            }
-          },
-          "additionalProperties": false
-        }
-      },
-      "additionalProperties": false
-    }
-  ]
-}
diff --git a/contracts/splits/schema/sg-splits.json b/contracts/splits/schema/sg-splits.json
new file mode 100644
index 00000000..373b348f
--- /dev/null
+++ b/contracts/splits/schema/sg-splits.json
@@ -0,0 +1,275 @@
+{
+  "contract_name": "sg-splits",
+  "contract_version": "0.21.8",
+  "idl_version": "1.0.0",
+  "instantiate": {
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "title": "InstantiateMsg",
+    "type": "object",
+    "required": [
+      "group"
+    ],
+    "properties": {
+      "group": {
+        "$ref": "#/definitions/Group"
+      }
+    },
+    "additionalProperties": false,
+    "definitions": {
+      "Admin": {
+        "oneOf": [
+          {
+            "type": "object",
+            "required": [
+              "address"
+            ],
+            "properties": {
+              "address": {
+                "type": "object",
+                "required": [
+                  "addr"
+                ],
+                "properties": {
+                  "addr": {
+                    "type": "string"
+                  }
+                },
+                "additionalProperties": false
+              }
+            },
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "required": [
+              "creator"
+            ],
+            "properties": {
+              "creator": {
+                "type": "object",
+                "additionalProperties": false
+              }
+            },
+            "additionalProperties": false
+          }
+        ]
+      },
+      "Binary": {
+        "description": "Binary is a wrapper around Vec<u8> to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec<u8>. See also <https://github.com/CosmWasm/cosmwasm/blob/main/docs/MESSAGE_TYPES.md>.",
+        "type": "string"
+      },
+      "ContractInstantiateMsg": {
+        "type": "object",
+        "required": [
+          "code_id",
+          "label",
+          "msg"
+        ],
+        "properties": {
+          "admin": {
+            "anyOf": [
+              {
+                "$ref": "#/definitions/Admin"
+              },
+              {
+                "type": "null"
+              }
+            ]
+          },
+          "code_id": {
+            "type": "integer",
+            "format": "uint64",
+            "minimum": 0.0
+          },
+          "label": {
+            "type": "string"
+          },
+          "msg": {
+            "$ref": "#/definitions/Binary"
+          }
+        },
+        "additionalProperties": false
+      },
+      "Group": {
+        "oneOf": [
+          {
+            "type": "object",
+            "required": [
+              "cw4_instantiate"
+            ],
+            "properties": {
+              "cw4_instantiate": {
+                "$ref": "#/definitions/ContractInstantiateMsg"
+              }
+            },
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "required": [
+              "cw4_address"
+            ],
+            "properties": {
+              "cw4_address": {
+                "type": "string"
+              }
+            },
+            "additionalProperties": false
+          }
+        ]
+      }
+    }
+  },
+  "execute": {
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "title": "ExecuteMsg",
+    "oneOf": [
+      {
+        "type": "object",
+        "required": [
+          "distribute"
+        ],
+        "properties": {
+          "distribute": {
+            "type": "object",
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      }
+    ]
+  },
+  "query": {
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "title": "QueryMsg",
+    "oneOf": [
+      {
+        "type": "object",
+        "required": [
+          "group"
+        ],
+        "properties": {
+          "group": {
+            "type": "object",
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "type": "object",
+        "required": [
+          "member"
+        ],
+        "properties": {
+          "member": {
+            "type": "object",
+            "required": [
+              "address"
+            ],
+            "properties": {
+              "address": {
+                "type": "string"
+              }
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "type": "object",
+        "required": [
+          "list_members"
+        ],
+        "properties": {
+          "list_members": {
+            "type": "object",
+            "properties": {
+              "limit": {
+                "type": [
+                  "integer",
+                  "null"
+                ],
+                "format": "uint32",
+                "minimum": 0.0
+              },
+              "start_after": {
+                "type": [
+                  "string",
+                  "null"
+                ]
+              }
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      }
+    ]
+  },
+  "migrate": null,
+  "sudo": null,
+  "responses": {
+    "group": {
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "title": "Addr",
+      "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
+      "type": "string"
+    },
+    "list_members": {
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "title": "MemberListResponse",
+      "type": "object",
+      "required": [
+        "members"
+      ],
+      "properties": {
+        "members": {
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/Member"
+          }
+        }
+      },
+      "additionalProperties": false,
+      "definitions": {
+        "Member": {
+          "description": "A group member has a weight associated with them. This may all be equal, or may have meaning in the app that makes use of the group (eg. voting power)",
+          "type": "object",
+          "required": [
+            "addr",
+            "weight"
+          ],
+          "properties": {
+            "addr": {
+              "type": "string"
+            },
+            "weight": {
+              "type": "integer",
+              "format": "uint64",
+              "minimum": 0.0
+            }
+          },
+          "additionalProperties": false
+        }
+      }
+    },
+    "member": {
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "title": "MemberResponse",
+      "type": "object",
+      "properties": {
+        "weight": {
+          "type": [
+            "integer",
+            "null"
+          ],
+          "format": "uint64",
+          "minimum": 0.0
+        }
+      },
+      "additionalProperties": false
+    }
+  }
+}
diff --git a/contracts/splits/src/bin/schema.rs b/contracts/splits/src/bin/schema.rs
new file mode 100644
index 00000000..ea9c52cc
--- /dev/null
+++ b/contracts/splits/src/bin/schema.rs
@@ -0,0 +1,11 @@
+use cosmwasm_schema::write_api;
+
+use sg_splits::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
+
+fn main() {
+    write_api! {
+        instantiate: InstantiateMsg,
+        execute: ExecuteMsg,
+        query: QueryMsg,
+    }
+}
diff --git a/contracts/splits/src/contract.rs b/contracts/splits/src/contract.rs
index f28d3f25..6a3d5676 100644
--- a/contracts/splits/src/contract.rs
+++ b/contracts/splits/src/contract.rs
@@ -1,51 +1,52 @@
 #[cfg(not(feature = "library"))]
 use cosmwasm_std::entry_point;
 use cosmwasm_std::{
-    coins, to_binary, BankMsg, Binary, Decimal, Deps, DepsMut, Env, MessageInfo, Response,
-    StdResult,
+    coins, to_binary, Addr, BankMsg, Binary, Decimal, Deps, DepsMut, Env, MessageInfo, Reply,
+    Response, StdResult, SubMsg,
 };
 use cw2::set_contract_version;
 use cw4::{Cw4Contract, Member, MemberListResponse, MemberResponse};
+use cw_utils::parse_reply_instantiate_data;
 use sg_std::NATIVE_DENOM;
 
 use crate::error::ContractError;
-use crate::msg::{ConfigResponse, ExecuteMsg, InstantiateMsg, QueryMsg};
-use crate::state::{Config, Executor, CONFIG};
+use crate::msg::{ExecuteMsg, Group, InstantiateMsg, QueryMsg};
+use crate::state::GROUP;
 
 // version info for migration info
 pub const CONTRACT_NAME: &str = "crates.io:sg-splits";
 const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
 
+const INIT_GROUP_REPLY_ID: u64 = 1;
+
 #[cfg_attr(not(feature = "library"), entry_point)]
 pub fn instantiate(
     deps: DepsMut,
-    _env: Env,
+    env: Env,
     _info: MessageInfo,
     msg: InstantiateMsg,
 ) -> Result<Response, ContractError> {
-    let group_addr = Cw4Contract(deps.api.addr_validate(&msg.group_addr).map_err(|_| {
-        ContractError::InvalidGroup {
-            addr: msg.group_addr.clone(),
-        }
-    })?);
-    let weight = group_addr.total_weight(&deps.querier)?;
-    if weight == 0 {
-        return Err(ContractError::InvalidWeight { weight });
-    }
-
     set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
 
-    if let Some(Executor::Only(ref addr)) = msg.executor {
-        deps.api.addr_validate(addr.as_ref())?;
-    }
+    let self_addr = env.contract.address;
 
-    let cfg = Config {
-        group_addr,
-        executor: msg.executor,
-    };
-    CONFIG.save(deps.storage, &cfg)?;
+    match msg.group {
+        Group::Cw4Instantiate(init) => Ok(Response::default().add_submessage(
+            SubMsg::reply_on_success(init.into_wasm_msg(self_addr), INIT_GROUP_REPLY_ID),
+        )),
+        Group::Cw4Address(addr) => {
+            let group = Cw4Contract(
+                deps.api
+                    .addr_validate(&addr)
+                    .map_err(|_| ContractError::InvalidGroup { addr })?,
+            );
 
-    Ok(Response::default())
+            checked_total_weight(&group, deps.as_ref())?;
+
+            GROUP.save(deps.storage, &group)?;
+            Ok(Response::default())
+        }
+    }
 }
 
 #[cfg_attr(not(feature = "library"), entry_point)]
@@ -65,18 +66,17 @@ pub fn execute_distribute(
     env: Env,
     info: MessageInfo,
 ) -> Result<Response, ContractError> {
-    let config = CONFIG.load(deps.storage)?;
-
-    config.authorize(&deps.querier, &info.sender)?;
+    let group = GROUP.load(deps.storage)?;
 
-    let total_weight = config.group_addr.total_weight(&deps.querier)?;
-    if total_weight == 0 {
-        return Err(ContractError::InvalidWeight {
-            weight: total_weight,
-        });
+    // only a member can distribute funds
+    let weight = group
+        .is_member(&deps.querier, &info.sender, None)?
+        .ok_or(ContractError::Unauthorized {})?;
+    if weight == 0 {
+        return Err(ContractError::InvalidWeight { weight });
     }
 
-    let members = config.group_addr.list_members(&deps.querier, None, None)?;
+    let total_weight = checked_total_weight(&group, deps.as_ref())?;
 
     let funds = deps
         .querier
@@ -85,7 +85,8 @@ pub fn execute_distribute(
         return Err(ContractError::NoFunds {});
     }
 
-    let msgs = members
+    let msgs = group
+        .list_members(&deps.querier, None, None)?
         .iter()
         .map(|member| {
             let ratio = Decimal::from_ratio(member.weight, total_weight);
@@ -102,10 +103,19 @@ pub fn execute_distribute(
         .add_messages(msgs))
 }
 
+fn checked_total_weight(group: &Cw4Contract, deps: Deps) -> Result<u64, ContractError> {
+    let weight = group.total_weight(&deps.querier)?;
+    if weight == 0 {
+        return Err(ContractError::InvalidWeight { weight });
+    }
+
+    Ok(weight)
+}
+
 #[cfg_attr(not(feature = "library"), entry_point)]
 pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
     match msg {
-        QueryMsg::Config {} => to_binary(&query_config(deps)?),
+        QueryMsg::Group {} => to_binary(&query_group(deps)?),
         QueryMsg::ListMembers { start_after, limit } => {
             to_binary(&list_members(deps, start_after, limit)?)
         }
@@ -113,16 +123,14 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
     }
 }
 
-fn query_config(deps: Deps) -> StdResult<ConfigResponse> {
-    let config = CONFIG.load(deps.storage)?;
-
-    Ok(ConfigResponse { config })
+fn query_group(deps: Deps) -> StdResult<Addr> {
+    Ok(GROUP.load(deps.storage)?.addr())
 }
 
 fn query_member(deps: Deps, member: String) -> StdResult<MemberResponse> {
-    let cfg = CONFIG.load(deps.storage)?;
+    let group = GROUP.load(deps.storage)?;
     let voter_addr = deps.api.addr_validate(&member)?;
-    let weight = cfg.group_addr.is_member(&deps.querier, &voter_addr, None)?;
+    let weight = group.is_member(&deps.querier, &voter_addr, None)?;
 
     Ok(MemberResponse { weight })
 }
@@ -132,9 +140,8 @@ fn list_members(
     start_after: Option<String>,
     limit: Option<u32>,
 ) -> StdResult<MemberListResponse> {
-    let cfg = CONFIG.load(deps.storage)?;
-    let members = cfg
-        .group_addr
+    let group = GROUP.load(deps.storage)?;
+    let members = group
         .list_members(&deps.querier, start_after, limit)?
         .into_iter()
         .map(|member| Member {
@@ -144,3 +151,27 @@ fn list_members(
         .collect();
     Ok(MemberListResponse { members })
 }
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result<Response, ContractError> {
+    if msg.id != INIT_GROUP_REPLY_ID {
+        return Err(ContractError::InvalidReplyID {});
+    }
+
+    let reply = parse_reply_instantiate_data(msg);
+    match reply {
+        Ok(res) => {
+            let group =
+                Cw4Contract(deps.api.addr_validate(&res.contract_address).map_err(|_| {
+                    ContractError::InvalidGroup {
+                        addr: res.contract_address.clone(),
+                    }
+                })?);
+
+            GROUP.save(deps.storage, &group)?;
+
+            Ok(Response::default().add_attribute("action", "reply_on_success"))
+        }
+        Err(_) => Err(ContractError::ReplyOnSuccess {}),
+    }
+}
diff --git a/contracts/splits/src/error.rs b/contracts/splits/src/error.rs
index d252336f..1d988da3 100644
--- a/contracts/splits/src/error.rs
+++ b/contracts/splits/src/error.rs
@@ -20,4 +20,10 @@ pub enum ContractError {
 
     #[error("Invalid executor '{addr}'")]
     InvalidExecutor { addr: String },
+
+    #[error("Invalid reply ID")]
+    InvalidReplyID {},
+
+    #[error("Reply error")]
+    ReplyOnSuccess {},
 }
diff --git a/contracts/splits/src/msg.rs b/contracts/splits/src/msg.rs
index f2bd2bf4..30631298 100644
--- a/contracts/splits/src/msg.rs
+++ b/contracts/splits/src/msg.rs
@@ -1,14 +1,16 @@
-use cosmwasm_schema::cw_serde;
+use cosmwasm_schema::{cw_serde, QueryResponses};
+use cosmwasm_std::Addr;
+use sg_controllers::ContractInstantiateMsg;
 
-use crate::state::{Config, Executor};
+#[cw_serde]
+pub enum Group {
+    Cw4Instantiate(ContractInstantiateMsg),
+    Cw4Address(String),
+}
 
 #[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>,
+    pub group: Group,
 }
 
 #[cw_serde]
@@ -17,19 +19,17 @@ pub enum ExecuteMsg {
 }
 
 #[cw_serde]
+#[derive(QueryResponses)]
 pub enum QueryMsg {
-    /// Returns the config
-    Config {},
-    /// Returns Member
+    #[returns(Addr)]
+    Group {},
+
+    #[returns(cw4::MemberResponse)]
     Member { address: String },
-    /// Returns MemberListResponse
+
+    #[returns(cw4::MemberListResponse)]
     ListMembers {
         start_after: Option<String>,
         limit: Option<u32>,
     },
 }
-
-#[cw_serde]
-pub struct ConfigResponse {
-    pub config: Config,
-}
diff --git a/contracts/splits/src/state.rs b/contracts/splits/src/state.rs
index 54ca2840..82f59859 100644
--- a/contracts/splits/src/state.rs
+++ b/contracts/splits/src/state.rs
@@ -1,51 +1,5 @@
-use cosmwasm_schema::cw_serde;
-use cosmwasm_std::{Addr, QuerierWrapper};
 use cw4::Cw4Contract;
 
 use cw_storage_plus::Item;
 
-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(())
-    }
-}
+pub const GROUP: Item<Cw4Contract> = Item::new("group");
diff --git a/packages/controllers/src/init.rs b/packages/controllers/src/init.rs
new file mode 100644
index 00000000..77a32077
--- /dev/null
+++ b/packages/controllers/src/init.rs
@@ -0,0 +1,31 @@
+use cosmwasm_schema::cw_serde;
+use cosmwasm_std::{Addr, Binary, WasmMsg};
+
+#[cw_serde]
+pub enum Admin {
+    Address { addr: String },
+    Creator {},
+}
+
+#[cw_serde]
+pub struct ContractInstantiateMsg {
+    pub code_id: u64,
+    pub msg: Binary,
+    pub admin: Option<Admin>,
+    pub label: String,
+}
+
+impl ContractInstantiateMsg {
+    pub fn into_wasm_msg(self, creator: Addr) -> WasmMsg {
+        WasmMsg::Instantiate {
+            admin: self.admin.map(|admin| match admin {
+                Admin::Address { addr } => addr,
+                Admin::Creator {} => creator.into_string(),
+            }),
+            code_id: self.code_id,
+            msg: self.msg,
+            label: self.label,
+            funds: vec![],
+        }
+    }
+}
diff --git a/packages/controllers/src/lib.rs b/packages/controllers/src/lib.rs
index 8715d23b..153d4c1b 100644
--- a/packages/controllers/src/lib.rs
+++ b/packages/controllers/src/lib.rs
@@ -1,3 +1,5 @@
 mod hooks;
+mod init;
 
 pub use hooks::{HookError, Hooks, HooksResponse};
+pub use init::{Admin, ContractInstantiateMsg};
diff --git a/ts/src/Splits.client.ts b/ts/src/Splits.client.ts
index d9a46807..84ef265f 100644
--- a/ts/src/Splits.client.ts
+++ b/ts/src/Splits.client.ts
@@ -6,10 +6,10 @@
 
 import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from "@cosmjs/cosmwasm-stargate";
 import { Coin, StdFee } from "@cosmjs/amino";
-import { Executor, Addr, Cw4Contract, ConfigResponse, Config, ExecuteMsg, InstantiateMsg, ListMembersResponse, Member, MemberResponse, QueryMsg } from "./Splits.types";
+import { Group, Admin, Binary, InstantiateMsg, ContractInstantiateMsg, ExecuteMsg, QueryMsg, Addr, MemberListResponse, Member, MemberResponse } from "./Splits.types";
 export interface SplitsReadOnlyInterface {
   contractAddress: string;
-  config: () => Promise<ConfigResponse>;
+  group: () => Promise<Addr>;
   member: ({
     address
   }: {
@@ -21,7 +21,7 @@ export interface SplitsReadOnlyInterface {
   }: {
     limit?: number;
     startAfter?: string;
-  }) => Promise<ListMembersResponse>;
+  }) => Promise<MemberListResponse>;
 }
 export class SplitsQueryClient implements SplitsReadOnlyInterface {
   client: CosmWasmClient;
@@ -30,14 +30,14 @@ export class SplitsQueryClient implements SplitsReadOnlyInterface {
   constructor(client: CosmWasmClient, contractAddress: string) {
     this.client = client;
     this.contractAddress = contractAddress;
-    this.config = this.config.bind(this);
+    this.group = this.group.bind(this);
     this.member = this.member.bind(this);
     this.listMembers = this.listMembers.bind(this);
   }
 
-  config = async (): Promise<ConfigResponse> => {
+  group = async (): Promise<Addr> => {
     return this.client.queryContractSmart(this.contractAddress, {
-      config: {}
+      group: {}
     });
   };
   member = async ({
@@ -57,7 +57,7 @@ export class SplitsQueryClient implements SplitsReadOnlyInterface {
   }: {
     limit?: number;
     startAfter?: string;
-  }): Promise<ListMembersResponse> => {
+  }): Promise<MemberListResponse> => {
     return this.client.queryContractSmart(this.contractAddress, {
       list_members: {
         limit,
diff --git a/ts/src/Splits.message-composer.ts b/ts/src/Splits.message-composer.ts
index d47d14f3..be8e53bc 100644
--- a/ts/src/Splits.message-composer.ts
+++ b/ts/src/Splits.message-composer.ts
@@ -8,7 +8,7 @@ import { Coin } from "@cosmjs/amino";
 import { MsgExecuteContractEncodeObject } from "cosmwasm";
 import { MsgExecuteContract } from "cosmjs-types/cosmwasm/wasm/v1/tx";
 import { toUtf8 } from "@cosmjs/encoding";
-import { Executor, Addr, Cw4Contract, ConfigResponse, Config, ExecuteMsg, InstantiateMsg, ListMembersResponse, Member, MemberResponse, QueryMsg } from "./Splits.types";
+import { Group, Admin, Binary, InstantiateMsg, ContractInstantiateMsg, ExecuteMsg, QueryMsg, Addr, MemberListResponse, Member, MemberResponse } from "./Splits.types";
 export interface SplitsMessage {
   contractAddress: string;
   sender: string;
diff --git a/ts/src/Splits.types.ts b/ts/src/Splits.types.ts
index 96babe61..22ee4ba8 100644
--- a/ts/src/Splits.types.ts
+++ b/ts/src/Splits.types.ts
@@ -4,37 +4,33 @@
 * and run the @cosmwasm/ts-codegen generate command to regenerate this file.
 */
 
-export type Executor = "member" | {
-  only: Addr;
+export type Group = {
+  cw4_instantiate: ContractInstantiateMsg;
+} | {
+  cw4_address: string;
 };
-export type Addr = string;
-export type Cw4Contract = Addr;
-export interface ConfigResponse {
-  config: Config;
+export type Admin = {
+  address: {
+    addr: string;
+  };
+} | {
+  creator: {};
+};
+export type Binary = string;
+export interface InstantiateMsg {
+  group: Group;
 }
-export interface Config {
-  executor?: Executor | null;
-  group_addr: Cw4Contract;
+export interface ContractInstantiateMsg {
+  admin?: Admin | null;
+  code_id: number;
+  label: string;
+  msg: Binary;
 }
 export type ExecuteMsg = {
   distribute: {};
 };
-export interface InstantiateMsg {
-  executor?: Executor | null;
-  group_addr: string;
-}
-export interface ListMembersResponse {
-  members: Member[];
-}
-export interface Member {
-  addr: string;
-  weight: number;
-}
-export interface MemberResponse {
-  weight?: number | null;
-}
 export type QueryMsg = {
-  config: {};
+  group: {};
 } | {
   member: {
     address: string;
@@ -44,4 +40,15 @@ export type QueryMsg = {
     limit?: number | null;
     start_after?: string | null;
   };
-};
\ No newline at end of file
+};
+export type Addr = string;
+export interface MemberListResponse {
+  members: Member[];
+}
+export interface Member {
+  addr: string;
+  weight: number;
+}
+export interface MemberResponse {
+  weight?: number | null;
+}
\ No newline at end of file


Test patch:
diff --git a/test-suite/Cargo.toml b/test-suite/Cargo.toml
index cd082142..6a91e61b 100644
--- a/test-suite/Cargo.toml
+++ b/test-suite/Cargo.toml
@@ -1,14 +1,12 @@
 [package]
-name = "test-suite"
-authors = [
-  "Michael Scotto <m@publicawesome.com>",
-]
-description = "Stargaze test-suite for launchpad"
-version = { workspace = true }
-edition = { workspace = true }
-homepage = { workspace = true }
-repository = { workspace = true }
-license = { workspace = true }
+name        = "test-suite"
+authors     = ["Michael Scotto <m@publicawesome.com>"]
+description = "Stargaze Test Suite for Launchpad"
+version     = { workspace = true }
+edition     = { workspace = true }
+homepage    = { workspace = true }
+repository  = { workspace = true }
+license     = { workspace = true }
 
 exclude = [
   # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication.
@@ -35,45 +33,46 @@ optimize = """docker run --rm -v "$(pwd)":/code \
 """
 
 [dependencies]
-cosmwasm-schema = { workspace = true }
-sg-std = { workspace = true }
-cosmwasm-std = { workspace = true }
-cw4 = { workspace = true }
-cw-multi-test = { workspace = true }
-sg-multi-test = { workspace = true }
-sg721-base = { workspace = true }
-cw4-group = { workspace = true }
-sg-splits = { workspace = true }
-anyhow = "1.0.57"
-sg-eth-airdrop = {workspace = true}
+cosmwasm-schema     = { workspace = true }
+sg-std              = { workspace = true }
+cosmwasm-std        = { workspace = true }
+cw4                 = { workspace = true }
+cw-multi-test       = { workspace = true }
+sg-multi-test       = { workspace = true }
+sg721-base          = { workspace = true }
+cw4-group           = { workspace = true }
+sg-splits           = { workspace = true }
+anyhow              = "1.0.57"
+sg-eth-airdrop      = { workspace = true }
 whitelist-immutable = { workspace = true, features = ["library"] }
-cw721 = { workspace = true }
-cw721-base = { workspace = true, features = ["library"] }
-sg2 = { workspace = true }
-sg721 = { workspace = true }
-sg-whitelist = { workspace = true, features = ["library"] }
-vending-factory = { workspace = true, features = ["library"] }
-vending-minter = { workspace = true, features = ["library"] }
-base-factory = { workspace = true }
-base-minter = { workspace = true }
-sg721-nt = { workspace = true }
+cw721               = { workspace = true }
+cw721-base          = { workspace = true, features = ["library"] }
+sg2                 = { workspace = true }
+sg721               = { workspace = true }
+sg-whitelist        = { workspace = true, features = ["library"] }
+vending-factory     = { workspace = true, features = ["library"] }
+vending-minter      = { workspace = true, features = ["library"] }
+base-factory        = { workspace = true }
+base-minter         = { workspace = true }
+sg721-nt            = { workspace = true }
+sg-controllers      = { workspace = true }
 
 [dev-dependencies]
-serde = { workspace = true }
-thiserror = { workspace = true }
-async-std = "1.12.0"
-ethers-signers = "1.0.0"
-ethers-core = "1.0.0"
-eyre = "0.6"
-rlp = "0.5"
-cw2 = { workspace = true }
+serde           = { workspace = true }
+thiserror       = { workspace = true }
+async-std       = "1.12.0"
+ethers-signers  = "1.0.0"
+ethers-core     = "1.0.0"
+eyre            = "0.6"
+rlp             = "0.5"
+cw2             = { workspace = true }
 cw-storage-plus = { workspace = true }
-cw-utils = { workspace = true }
-rand_core = { version = "0.6.4", default-features = false }
-rand_xoshiro = { version = "0.6.0", default-features = false }
-schemars = { workspace = true }
-sha2 = { workspace = true }
-shuffle = { git = "https://github.com/webmaster128/shuffle", branch = "rm-getrandom", version = "0.1.7" }
-sg1 = { workspace = true }
-sg4 = { workspace = true }
-url = { workspace = true }
+cw-utils        = { workspace = true }
+rand_core       = { version = "0.6.4", default-features = false }
+rand_xoshiro    = { version = "0.6.0", default-features = false }
+schemars        = { workspace = true }
+sha2            = { workspace = true }
+shuffle         = { git = "https://github.com/webmaster128/shuffle", branch = "rm-getrandom", version = "0.1.7" }
+sg1             = { workspace = true }
+sg4             = { workspace = true }
+url             = { workspace = true }
diff --git a/test-suite/src/common_setup/contract_boxes_empty.rs b/test-suite/src/common_setup/contract_boxes_empty.rs
index 13c0353c..9f56a2fd 100644
--- a/test-suite/src/common_setup/contract_boxes_empty.rs
+++ b/test-suite/src/common_setup/contract_boxes_empty.rs
@@ -15,6 +15,7 @@ pub fn contract_splits() -> Box<dyn Contract<Empty>> {
         sg_splits::contract::execute,
         sg_splits::contract::instantiate,
         sg_splits::contract::query,
-    );
+    )
+    .with_reply(sg_splits::contract::reply);
     Box::new(contract)
 }
diff --git a/test-suite/src/splits/tests/integration_tests.rs b/test-suite/src/splits/tests/integration_tests.rs
index 00895b88..4288a171 100644
--- a/test-suite/src/splits/tests/integration_tests.rs
+++ b/test-suite/src/splits/tests/integration_tests.rs
@@ -4,13 +4,14 @@ mod tests {
     use crate::common_setup::{
         contract_boxes_empty::contract_splits, helpers::mock_app_builder_init_funds,
     };
-    use cosmwasm_std::{Addr, Coin};
+    use cosmwasm_std::{to_binary, Addr, Coin};
     use cw2::{query_contract_info, ContractVersion};
     use cw4::{Cw4ExecuteMsg, Member, MemberListResponse};
     use cw_multi_test::{next_block, App, Executor as TestExecutor};
+    use sg_controllers::ContractInstantiateMsg;
+    use sg_splits::msg::Group;
     use sg_splits::{
         msg::{InstantiateMsg, QueryMsg},
-        state::Executor,
         ContractError,
     };
 
@@ -31,11 +32,35 @@ mod tests {
     }
 
     #[track_caller]
-    fn instantiate_splits(app: &mut App, group: Addr, executor: Option<Executor>) -> Addr {
+    fn instantiate_splits_with_group(app: &mut App, group_addr: Addr) -> Addr {
         let flex_id = app.store_code(contract_splits());
         let msg = sg_splits::msg::InstantiateMsg {
-            group_addr: group.to_string(),
-            executor,
+            group: Group::Cw4Address(group_addr.to_string()),
+        };
+        app.instantiate_contract(flex_id, Addr::unchecked(OWNER), &msg, &[], "splits", None)
+            .unwrap()
+    }
+
+    #[track_caller]
+    fn instantiate_splits(app: &mut App) -> Addr {
+        let flex_id = app.store_code(contract_splits());
+        let group_msg = cw4_group::msg::InstantiateMsg {
+            admin: Some(OWNER.into()),
+            members: vec![
+                member(OWNER, 50),
+                member(MEMBER1, 25),
+                member(MEMBER2, 20),
+                member(MEMBER3, 5),
+            ],
+        };
+
+        let msg = sg_splits::msg::InstantiateMsg {
+            group: Group::Cw4Instantiate(ContractInstantiateMsg {
+                code_id: app.store_code(contract_group()),
+                msg: to_binary(&group_msg).unwrap(),
+                admin: None,
+                label: "cw4-group".to_string(),
+            }),
         };
         app.instantiate_contract(flex_id, Addr::unchecked(OWNER), &msg, &[], "splits", None)
             .unwrap()
@@ -46,7 +71,6 @@ 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![
@@ -59,7 +83,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(), executor);
+        let splits_addr = instantiate_splits_with_group(app, group_addr.clone());
         app.update_block(next_block);
 
         // 3. (Optional) Set the multisig as the group owner
@@ -85,6 +109,26 @@ mod tests {
         (splits_addr, group_addr)
     }
 
+    #[track_caller]
+    fn setup_test_case_with_internal_group(app: &mut App, init_funds: Vec<Coin>) -> (Addr, Addr) {
+        // Set up Splits with internal group
+        let splits_addr = instantiate_splits(app);
+        app.update_block(next_block);
+
+        // Bonus: set some funds on the splits contract for future proposals
+        if !init_funds.is_empty() {
+            app.send_tokens(Addr::unchecked(OWNER), splits_addr.clone(), &init_funds)
+                .unwrap();
+        }
+
+        let group_addr: Addr = app
+            .wrap()
+            .query_wasm_smart(&splits_addr, &QueryMsg::Group {})
+            .unwrap();
+
+        (splits_addr, group_addr)
+    }
+
     #[test]
     fn test_instantiate_works() {
         let mut app = mock_app_builder_init_funds(&[]);
@@ -95,8 +139,7 @@ mod tests {
 
         // Zero weight fails
         let instantiate_msg = InstantiateMsg {
-            group_addr: group_addr.to_string(),
-            executor: Some(Executor::Only(Addr::unchecked(OWNER))),
+            group: Group::Cw4Address(group_addr.to_string()),
         };
         let err = app
             .instantiate_contract(
@@ -117,8 +160,7 @@ mod tests {
         let group_addr = instantiate_group(&mut app, vec![member(OWNER, 1)]);
 
         let instantiate_msg = InstantiateMsg {
-            group_addr: group_addr.to_string(),
-            executor: None,
+            group: Group::Cw4Address(group_addr.to_string()),
         };
         let splits_addr = app
             .instantiate_contract(
@@ -171,7 +213,7 @@ mod tests {
         fn distribute_zero_funds() {
             let mut app = mock_app_builder_init_funds(&[]);
 
-            let (splits_addr, _) = setup_test_case(&mut app, vec![], false, None);
+            let (splits_addr, _) = setup_test_case(&mut app, vec![], false);
 
             let msg = ExecuteMsg::Distribute {};
 
@@ -187,8 +229,7 @@ mod tests {
             let init_funds = coins(100, DENOM);
             let mut app = mock_app_builder_init_funds(&init_funds);
 
-            let (splits_addr, _) =
-                setup_test_case(&mut app, init_funds, false, Some(Executor::Member));
+            let (splits_addr, _) = setup_test_case(&mut app, init_funds, false);
 
             let msg = ExecuteMsg::Distribute {};
 
@@ -207,7 +248,7 @@ mod tests {
             let init_funds = coins(100, DENOM);
             let mut app = mock_app_builder_init_funds(&init_funds);
 
-            let (splits_addr, _) = setup_test_case(&mut app, init_funds, false, None);
+            let (splits_addr, _) = setup_test_case_with_internal_group(&mut app, init_funds);
 
             let msg = ExecuteMsg::Distribute {};
 
diff --git a/test-suite/src/vending_minter/tests/splits.rs b/test-suite/src/vending_minter/tests/splits.rs
index f941e2b3..4998d0ee 100644
--- a/test-suite/src/vending_minter/tests/splits.rs
+++ b/test-suite/src/vending_minter/tests/splits.rs
@@ -12,7 +12,7 @@ use cw4::Member;
 use cw_multi_test::{next_block, Executor};
 use sg2::tests::mock_collection_params_1;
 use sg_multi_test::StargazeApp;
-use sg_splits::msg::ExecuteMsg as SplitsExecuteMsg;
+use sg_splits::msg::{ExecuteMsg as SplitsExecuteMsg, Group};
 use sg_std::{GENESIS_MINT_START_TIME, NATIVE_DENOM};
 
 const OWNER: &str = "admin0001";
@@ -30,12 +30,11 @@ pub fn member<T: Into<String>>(addr: T, weight: u64) -> Member {
 }
 
 #[track_caller]
-fn instantiate_splits(app: &mut StargazeApp, group: Addr) -> Addr {
+fn instantiate_splits(app: &mut StargazeApp, group_addr: Addr) -> Addr {
     let splits_id = app.store_code(contract_splits());
     println!("splits_id: {}", splits_id);
     let msg = sg_splits::msg::InstantiateMsg {
-        group_addr: group.to_string(),
-        executor: None,
+        group: Group::Cw4Address(group_addr.to_string()),
     };
     app.instantiate_contract(splits_id, Addr::unchecked(OWNER), &msg, &[], "splits", None)
         .unwrap()
diff --git a/test-suite/src/whitelist_immutable/tests/integration_tests.rs b/test-suite/src/whitelist_immutable/tests/integration_tests.rs
index 37b4f744..306284e6 100644
--- a/test-suite/src/whitelist_immutable/tests/integration_tests.rs
+++ b/test-suite/src/whitelist_immutable/tests/integration_tests.rs
@@ -59,7 +59,7 @@ mod tests {
     pub fn query_address_count(app: &mut StargazeApp, addrs: Vec<String>, wl_addr: Addr) {
         let count: u64 = app
             .wrap()
-            .query_wasm_smart(&wl_addr, &QueryMsg::AddressCount {})
+            .query_wasm_smart(wl_addr, &QueryMsg::AddressCount {})
             .unwrap();
         assert_eq!(count, addrs.len() as u64);
     }
@@ -67,7 +67,7 @@ mod tests {
     pub fn query_admin(app: &mut StargazeApp, wl_addr: Addr) {
         let admin: String = app
             .wrap()
-            .query_wasm_smart(&wl_addr, &QueryMsg::Admin {})
+            .query_wasm_smart(wl_addr, &QueryMsg::Admin {})
             .unwrap();
         assert_eq!(admin, CREATOR.to_string());
     }
@@ -76,7 +76,7 @@ mod tests {
         let includes: bool = app
             .wrap()
             .query_wasm_smart(
-                &wl_addr,
+                wl_addr,
                 &QueryMsg::IncludesAddress {
                     address: addr_to_check,
                 },
@@ -88,7 +88,7 @@ mod tests {
     pub fn query_per_address_limit(app: &mut StargazeApp, wl_addr: Addr, per_address_limit: u32) {
         let limit: u32 = app
             .wrap()
-            .query_wasm_smart(&wl_addr, &QueryMsg::PerAddressLimit {})
+            .query_wasm_smart(wl_addr, &QueryMsg::PerAddressLimit {})
             .unwrap();
         assert_eq!(limit, per_address_limit);
     }