Repository: public-awesome/launchpad
Pull request #462 (2022-11-15 00:51:20)

Multiple Admins Using CW Plus Method
This is to change the multiple operators with SudoParams implementation that was following Marketplace to the multiple admins implementation that is used in CW Plus. 


Patch:
diff --git a/contracts/sg-eth-airdrop/src/helpers/build_msg.rs b/contracts/sg-eth-airdrop/src/helpers/build_msg.rs
index 51670120..77187ef1 100644
--- a/contracts/sg-eth-airdrop/src/helpers/build_msg.rs
+++ b/contracts/sg-eth-airdrop/src/helpers/build_msg.rs
@@ -4,7 +4,7 @@ use crate::msg::InstantiateMsg;
 use cosmwasm_std::{coins, Addr, BankMsg};
 use cosmwasm_std::{to_binary, DepsMut, Env, MessageInfo, StdResult, WasmMsg};
 use sg_std::{CosmosMsg, StargazeMsgWrapper, SubMsg};
-use sg_whitelist::helpers::CollectionWhitelistContract;
+use sg_whitelist::interface::CollectionWhitelistContract;
 use sg_whitelist::msg::AddMembersMsg;
 use sg_whitelist::msg::ExecuteMsg as CollectionWhitelistExecuteMsg;
 use whitelist_generic::helpers::WhitelistGenericContract;
diff --git a/contracts/whitelist/examples/schema.rs b/contracts/whitelist/examples/schema.rs
index 61747daf..a38e1842 100644
--- a/contracts/whitelist/examples/schema.rs
+++ b/contracts/whitelist/examples/schema.rs
@@ -4,8 +4,8 @@ use std::fs::create_dir_all;
 use cosmwasm_schema::{export_schema, remove_schemas, schema_for};
 
 use sg_whitelist::msg::{
-    ConfigResponse, ExecuteMsg, HasEndedResponse, HasMemberResponse, HasStartedResponse,
-    InstantiateMsg, IsActiveResponse, MembersResponse, QueryMsg,
+    AdminListResponse, ConfigResponse, ExecuteMsg, HasEndedResponse, HasMemberResponse,
+    HasStartedResponse, InstantiateMsg, IsActiveResponse, MembersResponse, QueryMsg,
 };
 use sg_whitelist::state::Config;
 
@@ -25,4 +25,5 @@ fn main() {
     export_schema(&schema_for!(IsActiveResponse), &out_dir);
     export_schema(&schema_for!(MembersResponse), &out_dir);
     export_schema(&schema_for!(QueryMsg), &out_dir);
+    export_schema(&schema_for!(AdminListResponse), &out_dir);
 }
diff --git a/contracts/whitelist/schema/admin_list_response.json b/contracts/whitelist/schema/admin_list_response.json
new file mode 100644
index 00000000..3b04e955
--- /dev/null
+++ b/contracts/whitelist/schema/admin_list_response.json
@@ -0,0 +1,21 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "AdminListResponse",
+  "type": "object",
+  "required": [
+    "admins",
+    "mutable"
+  ],
+  "properties": {
+    "admins": {
+      "type": "array",
+      "items": {
+        "type": "string"
+      }
+    },
+    "mutable": {
+      "type": "boolean"
+    }
+  },
+  "additionalProperties": false
+}
diff --git a/contracts/whitelist/schema/config.json b/contracts/whitelist/schema/config.json
index 0bc8ad94..0bf69711 100644
--- a/contracts/whitelist/schema/config.json
+++ b/contracts/whitelist/schema/config.json
@@ -3,7 +3,6 @@
   "title": "Config",
   "type": "object",
   "required": [
-    "admin",
     "end_time",
     "member_limit",
     "mint_price",
@@ -12,9 +11,6 @@
     "start_time"
   ],
   "properties": {
-    "admin": {
-      "$ref": "#/definitions/Addr"
-    },
     "end_time": {
       "$ref": "#/definitions/Timestamp"
     },
@@ -42,10 +38,6 @@
   },
   "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"
-    },
     "Coin": {
       "type": "object",
       "required": [
diff --git a/contracts/whitelist/schema/execute_msg.json b/contracts/whitelist/schema/execute_msg.json
index 2dd80f74..4d95a597 100644
--- a/contracts/whitelist/schema/execute_msg.json
+++ b/contracts/whitelist/schema/execute_msg.json
@@ -77,6 +77,43 @@
         }
       },
       "additionalProperties": false
+    },
+    {
+      "type": "object",
+      "required": [
+        "update_admins"
+      ],
+      "properties": {
+        "update_admins": {
+          "type": "object",
+          "required": [
+            "admins"
+          ],
+          "properties": {
+            "admins": {
+              "type": "array",
+              "items": {
+                "type": "string"
+              }
+            }
+          },
+          "additionalProperties": false
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "type": "object",
+      "required": [
+        "freeze"
+      ],
+      "properties": {
+        "freeze": {
+          "type": "object",
+          "additionalProperties": false
+        }
+      },
+      "additionalProperties": false
     }
   ],
   "definitions": {
diff --git a/contracts/whitelist/schema/instantiate_msg.json b/contracts/whitelist/schema/instantiate_msg.json
index 45d83666..830e6cff 100644
--- a/contracts/whitelist/schema/instantiate_msg.json
+++ b/contracts/whitelist/schema/instantiate_msg.json
@@ -3,6 +3,8 @@
   "title": "InstantiateMsg",
   "type": "object",
   "required": [
+    "admins",
+    "admins_mutable",
     "end_time",
     "member_limit",
     "members",
@@ -11,6 +13,15 @@
     "start_time"
   ],
   "properties": {
+    "admins": {
+      "type": "array",
+      "items": {
+        "type": "string"
+      }
+    },
+    "admins_mutable": {
+      "type": "boolean"
+    },
     "end_time": {
       "$ref": "#/definitions/Timestamp"
     },
diff --git a/contracts/whitelist/schema/query_msg.json b/contracts/whitelist/schema/query_msg.json
index a086ed56..72b50bcd 100644
--- a/contracts/whitelist/schema/query_msg.json
+++ b/contracts/whitelist/schema/query_msg.json
@@ -103,6 +103,502 @@
         }
       },
       "additionalProperties": false
+    },
+    {
+      "type": "object",
+      "required": [
+        "admin_list"
+      ],
+      "properties": {
+        "admin_list": {
+          "type": "object",
+          "additionalProperties": false
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "type": "object",
+      "required": [
+        "can_execute"
+      ],
+      "properties": {
+        "can_execute": {
+          "type": "object",
+          "required": [
+            "msg",
+            "sender"
+          ],
+          "properties": {
+            "msg": {
+              "$ref": "#/definitions/CosmosMsg_for_Empty"
+            },
+            "sender": {
+              "type": "string"
+            }
+          },
+          "additionalProperties": false
+        }
+      },
+      "additionalProperties": false
+    }
+  ],
+  "definitions": {
+    "BankMsg": {
+      "description": "The message types of the bank module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto",
+      "oneOf": [
+        {
+          "description": "Sends native tokens from the contract to the given address.\n\nThis is translated to a [MsgSend](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto#L19-L28). `from_address` is automatically filled with the current contract's address.",
+          "type": "object",
+          "required": [
+            "send"
+          ],
+          "properties": {
+            "send": {
+              "type": "object",
+              "required": [
+                "amount",
+                "to_address"
+              ],
+              "properties": {
+                "amount": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/definitions/Coin"
+                  }
+                },
+                "to_address": {
+                  "type": "string"
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "This will burn the given coins from the contract's account. There is no Cosmos SDK message that performs this, but it can be done by calling the bank keeper. Important if a contract controls significant token supply that must be retired.",
+          "type": "object",
+          "required": [
+            "burn"
+          ],
+          "properties": {
+            "burn": {
+              "type": "object",
+              "required": [
+                "amount"
+              ],
+              "properties": {
+                "amount": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/definitions/Coin"
+                  }
+                }
+              }
+            }
+          },
+          "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"
+    },
+    "Coin": {
+      "type": "object",
+      "required": [
+        "amount",
+        "denom"
+      ],
+      "properties": {
+        "amount": {
+          "$ref": "#/definitions/Uint128"
+        },
+        "denom": {
+          "type": "string"
+        }
+      }
+    },
+    "CosmosMsg_for_Empty": {
+      "oneOf": [
+        {
+          "type": "object",
+          "required": [
+            "bank"
+          ],
+          "properties": {
+            "bank": {
+              "$ref": "#/definitions/BankMsg"
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "type": "object",
+          "required": [
+            "custom"
+          ],
+          "properties": {
+            "custom": {
+              "$ref": "#/definitions/Empty"
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "type": "object",
+          "required": [
+            "staking"
+          ],
+          "properties": {
+            "staking": {
+              "$ref": "#/definitions/StakingMsg"
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "type": "object",
+          "required": [
+            "distribution"
+          ],
+          "properties": {
+            "distribution": {
+              "$ref": "#/definitions/DistributionMsg"
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "type": "object",
+          "required": [
+            "wasm"
+          ],
+          "properties": {
+            "wasm": {
+              "$ref": "#/definitions/WasmMsg"
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
+    "DistributionMsg": {
+      "description": "The message types of the distribution module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto",
+      "oneOf": [
+        {
+          "description": "This is translated to a [MsgSetWithdrawAddress](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L29-L37). `delegator_address` is automatically filled with the current contract's address.",
+          "type": "object",
+          "required": [
+            "set_withdraw_address"
+          ],
+          "properties": {
+            "set_withdraw_address": {
+              "type": "object",
+              "required": [
+                "address"
+              ],
+              "properties": {
+                "address": {
+                  "description": "The `withdraw_address`",
+                  "type": "string"
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "This is translated to a [[MsgWithdrawDelegatorReward](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L42-L50). `delegator_address` is automatically filled with the current contract's address.",
+          "type": "object",
+          "required": [
+            "withdraw_delegator_reward"
+          ],
+          "properties": {
+            "withdraw_delegator_reward": {
+              "type": "object",
+              "required": [
+                "validator"
+              ],
+              "properties": {
+                "validator": {
+                  "description": "The `validator_address`",
+                  "type": "string"
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
+    "Empty": {
+      "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)",
+      "type": "object"
+    },
+    "StakingMsg": {
+      "description": "The message types of the staking module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto",
+      "oneOf": [
+        {
+          "description": "This is translated to a [MsgDelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L81-L90). `delegator_address` is automatically filled with the current contract's address.",
+          "type": "object",
+          "required": [
+            "delegate"
+          ],
+          "properties": {
+            "delegate": {
+              "type": "object",
+              "required": [
+                "amount",
+                "validator"
+              ],
+              "properties": {
+                "amount": {
+                  "$ref": "#/definitions/Coin"
+                },
+                "validator": {
+                  "type": "string"
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "This is translated to a [MsgUndelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L112-L121). `delegator_address` is automatically filled with the current contract's address.",
+          "type": "object",
+          "required": [
+            "undelegate"
+          ],
+          "properties": {
+            "undelegate": {
+              "type": "object",
+              "required": [
+                "amount",
+                "validator"
+              ],
+              "properties": {
+                "amount": {
+                  "$ref": "#/definitions/Coin"
+                },
+                "validator": {
+                  "type": "string"
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "This is translated to a [MsgBeginRedelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L95-L105). `delegator_address` is automatically filled with the current contract's address.",
+          "type": "object",
+          "required": [
+            "redelegate"
+          ],
+          "properties": {
+            "redelegate": {
+              "type": "object",
+              "required": [
+                "amount",
+                "dst_validator",
+                "src_validator"
+              ],
+              "properties": {
+                "amount": {
+                  "$ref": "#/definitions/Coin"
+                },
+                "dst_validator": {
+                  "type": "string"
+                },
+                "src_validator": {
+                  "type": "string"
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
+    "Uint128": {
+      "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
+      "type": "string"
+    },
+    "WasmMsg": {
+      "description": "The message types of the wasm module.\n\nSee https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto",
+      "oneOf": [
+        {
+          "description": "Dispatches a call to another contract at a known address (with known ABI).\n\nThis is translated to a [MsgExecuteContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L68-L78). `sender` is automatically filled with the current contract's address.",
+          "type": "object",
+          "required": [
+            "execute"
+          ],
+          "properties": {
+            "execute": {
+              "type": "object",
+              "required": [
+                "contract_addr",
+                "funds",
+                "msg"
+              ],
+              "properties": {
+                "contract_addr": {
+                  "type": "string"
+                },
+                "funds": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/definitions/Coin"
+                  }
+                },
+                "msg": {
+                  "description": "msg is the json-encoded ExecuteMsg struct (as raw Binary)",
+                  "allOf": [
+                    {
+                      "$ref": "#/definitions/Binary"
+                    }
+                  ]
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "Instantiates a new contracts from previously uploaded Wasm code.\n\nThis is translated to a [MsgInstantiateContract](https://github.com/CosmWasm/wasmd/blob/v0.16.0-alpha1/x/wasm/internal/types/tx.proto#L47-L61). `sender` is automatically filled with the current contract's address.",
+          "type": "object",
+          "required": [
+            "instantiate"
+          ],
+          "properties": {
+            "instantiate": {
+              "type": "object",
+              "required": [
+                "code_id",
+                "funds",
+                "label",
+                "msg"
+              ],
+              "properties": {
+                "admin": {
+                  "type": [
+                    "string",
+                    "null"
+                  ]
+                },
+                "code_id": {
+                  "type": "integer",
+                  "format": "uint64",
+                  "minimum": 0.0
+                },
+                "funds": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/definitions/Coin"
+                  }
+                },
+                "label": {
+                  "description": "A human-readbale label for the contract",
+                  "type": "string"
+                },
+                "msg": {
+                  "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)",
+                  "allOf": [
+                    {
+                      "$ref": "#/definitions/Binary"
+                    }
+                  ]
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "Migrates a given contracts to use new wasm code. Passes a MigrateMsg to allow us to customize behavior.\n\nOnly the contract admin (as defined in wasmd), if any, is able to make this call.\n\nThis is translated to a [MsgMigrateContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L86-L96). `sender` is automatically filled with the current contract's address.",
+          "type": "object",
+          "required": [
+            "migrate"
+          ],
+          "properties": {
+            "migrate": {
+              "type": "object",
+              "required": [
+                "contract_addr",
+                "msg",
+                "new_code_id"
+              ],
+              "properties": {
+                "contract_addr": {
+                  "type": "string"
+                },
+                "msg": {
+                  "description": "msg is the json-encoded MigrateMsg struct that will be passed to the new code",
+                  "allOf": [
+                    {
+                      "$ref": "#/definitions/Binary"
+                    }
+                  ]
+                },
+                "new_code_id": {
+                  "description": "the code_id of the new logic to place in the given contract",
+                  "type": "integer",
+                  "format": "uint64",
+                  "minimum": 0.0
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "Sets a new admin (for migrate) on the given contract. Fails if this contract is not currently admin of the target contract.",
+          "type": "object",
+          "required": [
+            "update_admin"
+          ],
+          "properties": {
+            "update_admin": {
+              "type": "object",
+              "required": [
+                "admin",
+                "contract_addr"
+              ],
+              "properties": {
+                "admin": {
+                  "type": "string"
+                },
+                "contract_addr": {
+                  "type": "string"
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "Clears the admin on the given contract, so no more migration possible. Fails if this contract is not currently admin of the target contract.",
+          "type": "object",
+          "required": [
+            "clear_admin"
+          ],
+          "properties": {
+            "clear_admin": {
+              "type": "object",
+              "required": [
+                "contract_addr"
+              ],
+              "properties": {
+                "contract_addr": {
+                  "type": "string"
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
     }
-  ]
+  }
 }
diff --git a/contracts/whitelist/src/admin.rs b/contracts/whitelist/src/admin.rs
new file mode 100644
index 00000000..cbb9d9b3
--- /dev/null
+++ b/contracts/whitelist/src/admin.rs
@@ -0,0 +1,67 @@
+use cosmwasm_std::{Addr, Deps, DepsMut, Env, MessageInfo, StdResult};
+use sg_std::Response;
+
+use crate::{
+    msg::{AdminListResponse, CanExecuteResponse},
+    state::ADMIN_LIST,
+    validators::map_validate,
+    ContractError,
+};
+
+pub fn execute_update_admins(
+    deps: DepsMut,
+    _env: Env,
+    info: MessageInfo,
+    admins: Vec<String>,
+) -> Result<Response, ContractError> {
+    let mut cfg = ADMIN_LIST.load(deps.storage)?;
+    if !cfg.can_modify(info.sender.as_ref()) {
+        Err(ContractError::Unauthorized {})
+    } else {
+        cfg.admins = map_validate(deps.api, &admins)?;
+        ADMIN_LIST.save(deps.storage, &cfg)?;
+
+        let res = Response::new().add_attribute("action", "update_admins");
+        Ok(res)
+    }
+}
+
+pub fn can_execute(deps: &DepsMut, sender: Addr) -> Result<Addr, ContractError> {
+    let cfg = ADMIN_LIST.load(deps.storage)?;
+    let can = cfg.is_admin(&sender);
+    if !can {
+        return Err(ContractError::Unauthorized {});
+    }
+    Ok(sender)
+}
+
+pub fn execute_freeze(
+    deps: DepsMut,
+    _env: Env,
+    info: MessageInfo,
+) -> Result<Response, ContractError> {
+    let mut cfg = ADMIN_LIST.load(deps.storage)?;
+    if !cfg.can_modify(info.sender.as_ref()) {
+        Err(ContractError::Unauthorized {})
+    } else {
+        cfg.mutable = false;
+        ADMIN_LIST.save(deps.storage, &cfg)?;
+
+        let res = Response::new().add_attribute("action", "freeze");
+        Ok(res)
+    }
+}
+
+pub fn query_admin_list(deps: Deps) -> StdResult<AdminListResponse> {
+    let cfg = ADMIN_LIST.load(deps.storage)?;
+    Ok(AdminListResponse {
+        admins: cfg.admins.into_iter().map(|a| a.into()).collect(),
+        mutable: cfg.mutable,
+    })
+}
+
+pub fn query_can_execute(deps: Deps, sender: &str) -> StdResult<CanExecuteResponse> {
+    let cfg = ADMIN_LIST.load(deps.storage)?;
+    let can = cfg.is_admin(deps.api.addr_validate(sender)?);
+    Ok(CanExecuteResponse { can_execute: can })
+}
diff --git a/contracts/whitelist/src/contract.rs b/contracts/whitelist/src/contract.rs
index 690dca25..61fa70eb 100644
--- a/contracts/whitelist/src/contract.rs
+++ b/contracts/whitelist/src/contract.rs
@@ -1,11 +1,13 @@
+use crate::admin::{
+    can_execute, execute_freeze, execute_update_admins, query_admin_list, query_can_execute,
+};
 use crate::error::ContractError;
 use crate::msg::{
     AddMembersMsg, ConfigResponse, ExecuteMsg, HasEndedResponse, HasMemberResponse,
     HasStartedResponse, InstantiateMsg, IsActiveResponse, MembersResponse, QueryMsg,
     RemoveMembersMsg,
 };
-use crate::state::{Config, SudoParams, CONFIG, SUDO_PARAMS, WHITELIST};
-use crate::sudo::only_operator;
+use crate::state::{AdminList, Config, ADMIN_LIST, CONFIG, WHITELIST};
 use crate::validators::map_validate;
 #[cfg(not(feature = "library"))]
 use cosmwasm_std::entry_point;
@@ -75,11 +77,6 @@ pub fn instantiate(
         });
     }
 
-    let params = SudoParams {
-        operators: map_validate(deps.api, &msg.operators)?,
-    };
-    SUDO_PARAMS.save(deps.storage, &params)?;
-
     let creation_fee = Decimal::new(msg.member_limit.into(), 3)
         .ceil()
         .to_u128()
@@ -107,6 +104,12 @@ pub fn instantiate(
     };
     CONFIG.save(deps.storage, &config)?;
 
+    let admin_config = AdminList {
+        admins: map_validate(deps.api, &msg.admins)?,
+        mutable: msg.admins_mutable,
+    };
+    ADMIN_LIST.save(deps.storage, &admin_config)?;
+
     if msg.start_time > msg.end_time {
         return Err(ContractError::InvalidStartTime(
             msg.start_time,
@@ -169,6 +172,8 @@ pub fn execute(
         ExecuteMsg::IncreaseMemberLimit(member_limit) => {
             execute_increase_member_limit(deps, info, member_limit)
         }
+        ExecuteMsg::UpdateAdmins { admins } => execute_update_admins(deps, env, info, admins),
+        ExecuteMsg::Freeze {} => execute_freeze(deps, env, info),
     }
 }
 
@@ -179,7 +184,7 @@ pub fn execute_update_start_time(
     start_time: Timestamp,
 ) -> Result<Response, ContractError> {
     let mut config = CONFIG.load(deps.storage)?;
-    only_operator(deps.storage, &info)?;
+    can_execute(&deps, info.sender.clone())?;
 
     // don't allow updating start time if whitelist is active
     if env.block.time >= config.start_time {
@@ -212,7 +217,7 @@ pub fn execute_update_end_time(
     end_time: Timestamp,
 ) -> Result<Response, ContractError> {
     let mut config = CONFIG.load(deps.storage)?;
-    only_operator(deps.storage, &info)?;
+    can_execute(&deps, info.sender.clone())?;
 
     // don't allow updating end time if whitelist is active
     if env.block.time >= config.start_time {
@@ -238,7 +243,7 @@ pub fn execute_add_members(
     mut msg: AddMembersMsg,
 ) -> Result<Response, ContractError> {
     let mut config = CONFIG.load(deps.storage)?;
-    only_operator(deps.storage, &info)?;
+    can_execute(&deps, info.sender.clone())?;
     // remove duplicate members
     msg.to_add.sort_unstable();
     msg.to_add.dedup();
@@ -272,7 +277,7 @@ pub fn execute_remove_members(
     msg: RemoveMembersMsg,
 ) -> Result<Response, ContractError> {
     let mut config = CONFIG.load(deps.storage)?;
-    only_operator(deps.storage, &info)?;
+    can_execute(&deps, info.sender.clone())?;
 
     if env.block.time >= config.start_time {
         return Err(ContractError::AlreadyStarted {});
@@ -300,7 +305,7 @@ pub fn execute_update_per_address_limit(
     per_address_limit: u32,
 ) -> Result<Response, ContractError> {
     let mut config = CONFIG.load(deps.storage)?;
-    only_operator(deps.storage, &info)?;
+    can_execute(&deps, info.sender)?;
 
     if per_address_limit > MAX_PER_ADDRESS_LIMIT {
         return Err(ContractError::InvalidPerAddressLimit {
@@ -331,7 +336,7 @@ pub fn execute_increase_member_limit(
         });
     }
 
-    // if new limit crosses 1,000 members, requires upgrade fee. Otherwise, free upgrade.
+    // if new limit crosses 1,000 members, requires upgrade fee. Otherwise,  upgrade.
     let old_limit = Decimal::new(config.member_limit.into(), 3).ceil();
     let new_limit = Decimal::new(member_limit.into(), 3).ceil();
     let upgrade_fee: u128 = if new_limit > old_limit {
@@ -371,6 +376,8 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
         QueryMsg::IsActive {} => to_binary(&query_is_active(deps, env)?),
         QueryMsg::HasMember { member } => to_binary(&query_has_member(deps, member)?),
         QueryMsg::Config {} => to_binary(&query_config(deps, env)?),
+        QueryMsg::AdminList {} => to_binary(&query_admin_list(deps)?),
+        QueryMsg::CanExecute { sender, .. } => to_binary(&query_can_execute(deps, &sender)?),
     }
 }
 
diff --git a/contracts/whitelist/src/error.rs b/contracts/whitelist/src/error.rs
index 6c30ff8d..4959574d 100644
--- a/contracts/whitelist/src/error.rs
+++ b/contracts/whitelist/src/error.rs
@@ -53,12 +53,6 @@ pub enum ContractError {
     #[error("{0}")]
     PaymentError(#[from] PaymentError),
 
-    #[error("Given operator address already registered as an operator")]
-    OperatorAlreadyRegistered {},
-
-    #[error("Given operator address is not registered as an operator")]
-    OperatorNotRegistered {},
-
-    #[error("UnauthorizedOperator")]
-    UnauthorizedOperator {},
+    #[error("UnauthorizedAdmin")]
+    UnauthorizedAdmin {},
 }
diff --git a/contracts/whitelist/src/helpers.rs b/contracts/whitelist/src/helpers/interface.rs
similarity index 100%
rename from contracts/whitelist/src/helpers.rs
rename to contracts/whitelist/src/helpers/interface.rs
diff --git a/contracts/whitelist/src/helpers/validators.rs b/contracts/whitelist/src/helpers/validators.rs
index d9930df5..b5ffe298 100644
--- a/contracts/whitelist/src/helpers/validators.rs
+++ b/contracts/whitelist/src/helpers/validators.rs
@@ -1,11 +1,5 @@
 use cosmwasm_std::{Addr, Api, StdResult};
 
-pub fn map_validate(api: &dyn Api, addresses: &[String]) -> StdResult<Vec<Addr>> {
-    let mut validated_addresses = addresses
-        .iter()
-        .map(|addr| api.addr_validate(addr))
-        .collect::<StdResult<Vec<_>>>()?;
-    validated_addresses.sort();
-    validated_addresses.dedup();
-    Ok(validated_addresses)
+pub fn map_validate(api: &dyn Api, admins: &[String]) -> StdResult<Vec<Addr>> {
+    admins.iter().map(|addr| api.addr_validate(addr)).collect()
 }
diff --git a/contracts/whitelist/src/lib.rs b/contracts/whitelist/src/lib.rs
index 2c2293b1..95aa31f3 100644
--- a/contracts/whitelist/src/lib.rs
+++ b/contracts/whitelist/src/lib.rs
@@ -1,16 +1,17 @@
 pub mod contract;
 mod error;
-pub mod helpers;
 #[path = "./tests/integration_tests.rs"]
 #[cfg(test)]
 pub mod integration_tests;
 pub mod msg;
 pub mod state;
-pub mod sudo;
 #[path = "./tests/unit_tests.rs"]
 #[cfg(test)]
 pub mod unit_tests;
 
+pub mod admin;
+#[path = "./helpers/interface.rs"]
+pub mod interface;
 #[path = "./helpers/validators.rs"]
 pub mod validators;
 pub use crate::error::ContractError;
diff --git a/contracts/whitelist/src/msg.rs b/contracts/whitelist/src/msg.rs
index e1c443b7..15c78b7b 100644
--- a/contracts/whitelist/src/msg.rs
+++ b/contracts/whitelist/src/msg.rs
@@ -1,5 +1,5 @@
 use cosmwasm_schema::cw_serde;
-use cosmwasm_std::{Coin, Timestamp};
+use cosmwasm_std::{Coin, CosmosMsg, Empty, Timestamp};
 
 #[cw_serde]
 pub struct InstantiateMsg {
@@ -9,7 +9,8 @@ pub struct InstantiateMsg {
     pub mint_price: Coin,
     pub per_address_limit: u32,
     pub member_limit: u32,
-    pub operators: Vec<String>,
+    pub admins: Vec<String>,
+    pub admins_mutable: bool,
 }
 
 #[cw_serde]
@@ -20,6 +21,14 @@ pub enum ExecuteMsg {
     RemoveMembers(RemoveMembersMsg),
     UpdatePerAddressLimit(u32),
     IncreaseMemberLimit(u32),
+    UpdateAdmins { admins: Vec<String> },
+    Freeze {},
+}
+
+#[cw_serde]
+pub struct AdminListResponse {
+    pub admins: Vec<String>,
+    pub mutable: bool,
 }
 
 #[cw_serde]
@@ -45,6 +54,13 @@ pub enum QueryMsg {
         member: String,
     },
     Config {},
+
+    AdminList {},
+
+    CanExecute {
+        sender: String,
+        msg: CosmosMsg<Empty>,
+    },
 }
 
 #[cw_serde]
@@ -95,3 +111,8 @@ pub enum SudoMsg {
     /// Remove operator
     RemoveOperator { operator: String },
 }
+
+#[cw_serde]
+pub struct CanExecuteResponse {
+    pub can_execute: bool,
+}
diff --git a/contracts/whitelist/src/state.rs b/contracts/whitelist/src/state.rs
index fc03c418..3f430aa9 100644
--- a/contracts/whitelist/src/state.rs
+++ b/contracts/whitelist/src/state.rs
@@ -13,11 +13,23 @@ pub struct Config {
 }
 
 #[cw_serde]
-pub struct SudoParams {
-    pub operators: Vec<Addr>,
+pub struct AdminList {
+    pub admins: Vec<Addr>,
+    pub mutable: bool,
 }
 
-pub const SUDO_PARAMS: Item<SudoParams> = Item::new("sudo-params");
+impl AdminList {
+    pub fn is_admin(&self, addr: impl AsRef<str>) -> bool {
+        let addr = addr.as_ref();
+        self.admins.iter().any(|a| a.as_ref() == addr)
+    }
+
+    pub fn can_modify(&self, addr: &str) -> bool {
+        self.mutable && self.is_admin(addr)
+    }
+}
+
+pub const ADMIN_LIST: Item<AdminList> = Item::new("admin_list");
 
 pub const CONFIG: Item<Config> = Item::new("config");
 pub const WHITELIST: Map<Addr, bool> = Map::new("wl");
diff --git a/contracts/whitelist/src/sudo.rs b/contracts/whitelist/src/sudo.rs
deleted file mode 100644
index 026047d5..00000000
--- a/contracts/whitelist/src/sudo.rs
+++ /dev/null
@@ -1,57 +0,0 @@
-use crate::error::ContractError;
-use crate::msg::SudoMsg;
-use crate::state::SUDO_PARAMS;
-use cosmwasm_std::{entry_point, Addr, DepsMut, Env, MessageInfo, Storage};
-use sg_std::Response;
-
-#[cfg_attr(not(feature = "library"), entry_point)]
-pub fn sudo(deps: DepsMut, _env: Env, msg: SudoMsg) -> Result<Response, ContractError> {
-    let api = deps.api;
-
-    match msg {
-        SudoMsg::AddOperator { operator } => sudo_add_operator(deps, api.addr_validate(&operator)?),
-        SudoMsg::RemoveOperator { operator } => {
-            sudo_remove_operator(deps, api.addr_validate(&operator)?)
-        }
-    }
-}
-
-pub fn sudo_add_operator(deps: DepsMut, operator: Addr) -> Result<Response, ContractError> {
-    let mut params = SUDO_PARAMS.load(deps.storage)?;
-    if !params.operators.iter().any(|o| o == &operator) {
-        params.operators.push(operator.clone());
-    } else {
-        return Err(ContractError::OperatorAlreadyRegistered {});
-    }
-    SUDO_PARAMS.save(deps.storage, &params)?;
-    let res = Response::new()
-        .add_attribute("action", "add_operator")
-        .add_attribute("operator", operator);
-    Ok(res)
-}
-
-pub fn sudo_remove_operator(deps: DepsMut, operator: Addr) -> Result<Response, ContractError> {
-    let mut params = SUDO_PARAMS.load(deps.storage)?;
-    if let Some(i) = params.operators.iter().position(|o| o == &operator) {
-        params.operators.remove(i);
-    } else {
-        return Err(ContractError::OperatorNotRegistered {});
-    }
-    SUDO_PARAMS.save(deps.storage, &params)?;
-    let res = Response::new()
-        .add_attribute("action", "remove_operator")
-        .add_attribute("operator", operator);
-    Ok(res)
-}
-
-pub fn only_operator(store: &dyn Storage, info: &MessageInfo) -> Result<Addr, ContractError> {
-    let params = SUDO_PARAMS.load(store)?;
-    if !params
-        .operators
-        .iter()
-        .any(|a| a.as_ref() == info.sender.as_ref())
-    {
-        return Err(ContractError::UnauthorizedOperator {});
-    }
-    Ok(info.sender.clone())
-}


Test patch:
diff --git a/contracts/sg-eth-airdrop/src/testing/setup/setup_collection_whitelist.rs b/contracts/sg-eth-airdrop/src/testing/setup/setup_collection_whitelist.rs
index 7c5f17c6..351eb7be 100644
--- a/contracts/sg-eth-airdrop/src/testing/setup/setup_collection_whitelist.rs
+++ b/contracts/sg-eth-airdrop/src/testing/setup/setup_collection_whitelist.rs
@@ -18,7 +18,8 @@ pub fn setup_whitelist_contract(router: &mut StargazeApp, creator: &Addr) -> Add
         mint_price: coin(WHITELIST_AMOUNT, NATIVE_DENOM),
         per_address_limit: WL_PER_ADDRESS_LIMIT,
         member_limit: 1000,
-        operators: vec![creator.to_string()],
+        admins: vec![creator.to_string()],
+        admins_mutable: true,
     };
     router
         .instantiate_contract(
diff --git a/contracts/vending-minter/src/integration_tests.rs b/contracts/vending-minter/src/integration_tests.rs
index d8db1582..80750056 100644
--- a/contracts/vending-minter/src/integration_tests.rs
+++ b/contracts/vending-minter/src/integration_tests.rs
@@ -111,7 +111,8 @@ fn setup_whitelist_contract(router: &mut StargazeApp, creator: &Addr) -> Addr {
         mint_price: coin(WHITELIST_AMOUNT, NATIVE_DENOM),
         per_address_limit: WL_PER_ADDRESS_LIMIT,
         member_limit: 1000,
-        operators: vec![creator.to_string()],
+        admins: vec![creator.to_string()],
+        admins_mutable: true,
     };
     router
         .instantiate_contract(
diff --git a/contracts/whitelist/src/tests/integration_tests.rs b/contracts/whitelist/src/tests/integration_tests.rs
index 9962a43f..356339c1 100644
--- a/contracts/whitelist/src/tests/integration_tests.rs
+++ b/contracts/whitelist/src/tests/integration_tests.rs
@@ -3,13 +3,14 @@ use cw_multi_test::{BankSudo, Contract, ContractWrapper, Executor, SudoMsg as CW
 use sg_multi_test::StargazeApp;
 use sg_std::{StargazeMsgWrapper, GENESIS_MINT_START_TIME, NATIVE_DENOM};
 
-use crate::msg::{
-    AddMembersMsg, ExecuteMsg, InstantiateMsg, MembersResponse, QueryMsg, RemoveMembersMsg, SudoMsg,
+use crate::{
+    msg::{AddMembersMsg, ExecuteMsg, InstantiateMsg, MembersResponse, QueryMsg, RemoveMembersMsg},
+    state::AdminList,
 };
 
 const COLLECTION_WHITELIST_ADDR: &str = "contract0";
 const ADMIN: &str = "admin";
-const SECOND_OPERATOR: &str = "second_operator";
+const SECOND_ADMIN: &str = "second_admin";
 const UNIT_AMOUNT: u128 = 100_000_000;
 
 const GENESIS_START_TIME: Timestamp = Timestamp::from_nanos(GENESIS_MINT_START_TIME);
@@ -24,8 +25,7 @@ pub fn collection_whitelist() -> Box<dyn Contract<StargazeMsgWrapper>> {
         crate::contract::execute,
         crate::contract::instantiate,
         crate::contract::query,
-    )
-    .with_sudo(crate::sudo::sudo);
+    );
     Box::new(contract)
 }
 
@@ -41,7 +41,8 @@ fn instantiate_contract(admin_account: &str, app: &mut StargazeApp) {
         mint_price: coin(UNIT_AMOUNT, NATIVE_DENOM),
         per_address_limit: 1,
         member_limit: 1000,
-        operators: vec![ADMIN.to_string(), SECOND_OPERATOR.to_string()],
+        admins: vec![ADMIN.to_string(), SECOND_ADMIN.to_string()],
+        admins_mutable: true,
     };
     app.sudo(CWSudoMsg::Bank({
         BankSudo::Mint {
@@ -63,12 +64,8 @@ fn instantiate_contract(admin_account: &str, app: &mut StargazeApp) {
     );
 }
 
-fn add_members_with_specified_operator(
-    operator: &str,
-    members: Vec<String>,
-    app: &mut StargazeApp,
-) {
-    let operator_addr = Addr::unchecked(operator);
+fn add_members_with_specified_admin(admin: &str, members: Vec<String>, app: &mut StargazeApp) {
+    let admin_addr = Addr::unchecked(admin);
     let collection_whitelist_contract = Addr::unchecked("contract0");
     let initial_members = vec!["member0".to_string()];
 
@@ -89,12 +86,7 @@ fn add_members_with_specified_operator(
         to_add: members.clone(),
     };
     let msg = ExecuteMsg::AddMembers(add_msg);
-    let res = app.execute_contract(
-        operator_addr,
-        collection_whitelist_contract.clone(),
-        &msg,
-        &[],
-    );
+    let res = app.execute_contract(admin_addr, collection_whitelist_contract.clone(), &msg, &[]);
     assert_eq!(res.unwrap().events.len(), 2);
 
     let query_msg = QueryMsg::Members {
@@ -111,12 +103,8 @@ fn add_members_with_specified_operator(
     assert_eq!(query_result, expected_result);
 }
 
-fn remove_members_with_specified_operator(
-    operator: &str,
-    members: Vec<String>,
-    app: &mut StargazeApp,
-) {
-    let operator_addr = Addr::unchecked(operator);
+fn remove_members_with_specified_admin(admin: &str, members: Vec<String>, app: &mut StargazeApp) {
+    let admin_addr = Addr::unchecked(admin);
     let collection_whitelist_contract = Addr::unchecked("contract0");
     let initial_members = vec![
         "member0".to_string(),
@@ -139,12 +127,7 @@ fn remove_members_with_specified_operator(
 
     let remove_msg = RemoveMembersMsg { to_remove: members };
     let msg = ExecuteMsg::RemoveMembers(remove_msg);
-    let res = app.execute_contract(
-        operator_addr,
-        collection_whitelist_contract.clone(),
-        &msg,
-        &[],
-    );
+    let res = app.execute_contract(admin_addr, collection_whitelist_contract.clone(), &msg, &[]);
     assert_eq!(res.unwrap().events.len(), 2);
 
     let query_msg = QueryMsg::Members {
@@ -161,8 +144,8 @@ fn remove_members_with_specified_operator(
     assert_eq!(query_result, expected_result);
 }
 
-fn add_members_blocked(operator: &str, members: Vec<String>, app: &mut StargazeApp) {
-    let operator_addr = Addr::unchecked(operator);
+fn add_members_blocked(admin: &str, members: Vec<String>, app: &mut StargazeApp) {
+    let admin_addr = Addr::unchecked(admin);
     let collection_whitelist_contract = Addr::unchecked("contract0");
     let initial_members = vec!["member0".to_string()];
 
@@ -181,16 +164,8 @@ fn add_members_blocked(operator: &str, members: Vec<String>, app: &mut StargazeA
 
     let add_msg = AddMembersMsg { to_add: members };
     let msg = ExecuteMsg::AddMembers(add_msg);
-    let res = app.execute_contract(
-        operator_addr,
-        collection_whitelist_contract.clone(),
-        &msg,
-        &[],
-    );
-    assert_eq!(
-        res.unwrap_err().root_cause().to_string(),
-        "UnauthorizedOperator"
-    );
+    let res = app.execute_contract(admin_addr, collection_whitelist_contract.clone(), &msg, &[]);
+    assert_eq!(res.unwrap_err().root_cause().to_string(), "Unauthorized");
 
     let query_result: MembersResponse = app
         .wrap()
@@ -207,34 +182,89 @@ fn test_instantiate() {
 }
 
 #[test]
-fn test_add_operator() {
+fn test_add_admin() {
     let mut app = custom_mock_app();
     instantiate_contract(ADMIN, &mut app);
     let collection_whitelist_addr = Addr::unchecked(COLLECTION_WHITELIST_ADDR);
 
-    let new_operator: &str = "new_operator";
-    let add_msg = SudoMsg::AddOperator {
-        operator: new_operator.to_string(),
+    let new_admin: &str = "new_admin";
+    let update_admins_message = ExecuteMsg::UpdateAdmins {
+        admins: vec![ADMIN.to_string(), new_admin.to_string()],
     };
-    let res = app.wasm_sudo(collection_whitelist_addr, &add_msg);
-    println!("res is {:?}", res);
+    let _ = app.execute_contract(
+        Addr::unchecked(ADMIN),
+        Addr::unchecked(collection_whitelist_addr),
+        &update_admins_message,
+        &[],
+    );
     let members = vec!["member1".to_string(), "member2".to_string()];
 
-    add_members_with_specified_operator(new_operator, members.clone(), &mut app);
-    remove_members_with_specified_operator(new_operator, members, &mut app);
+    add_members_with_specified_admin(new_admin, members.clone(), &mut app);
+    remove_members_with_specified_admin(new_admin, members, &mut app);
+}
+
+#[test]
+fn test_remove_admin() {
+    let mut app = custom_mock_app();
+    instantiate_contract(ADMIN, &mut app);
+    let collection_whitelist_addr = Addr::unchecked(COLLECTION_WHITELIST_ADDR);
+
+    let update_admin_message = ExecuteMsg::UpdateAdmins {
+        admins: vec![ADMIN.to_string()],
+    };
+
+    let _ = app.execute_contract(
+        Addr::unchecked(ADMIN),
+        Addr::unchecked(collection_whitelist_addr),
+        &update_admin_message,
+        &[],
+    );
+
+    let members = vec!["member1".to_string()];
+    add_members_blocked(SECOND_ADMIN, members, &mut app);
 }
 
 #[test]
-fn test_remove_operator() {
+fn test_query_admin_list() {
+    let mut app = custom_mock_app();
+    instantiate_contract(ADMIN, &mut app);
+    let collection_whitelist_contract = Addr::unchecked(COLLECTION_WHITELIST_ADDR);
+
+    let query_msg = QueryMsg::AdminList {};
+    let query_result: AdminList = app
+        .wrap()
+        .query_wasm_smart(collection_whitelist_contract, &query_msg)
+        .unwrap();
+    let expected_result = AdminList {
+        admins: vec![Addr::unchecked("admin"), Addr::unchecked("second_admin")],
+        mutable: true,
+    };
+    assert_eq!(query_result, expected_result);
+}
+
+#[test]
+fn test_freeze_admins() {
     let mut app = custom_mock_app();
     instantiate_contract(ADMIN, &mut app);
     let collection_whitelist_addr = Addr::unchecked(COLLECTION_WHITELIST_ADDR);
 
-    let remove_msg = SudoMsg::RemoveOperator {
-        operator: SECOND_OPERATOR.to_string(),
+    let freeze_admins_msg = ExecuteMsg::Freeze {};
+    let _ = app.execute_contract(
+        Addr::unchecked(ADMIN),
+        Addr::unchecked(collection_whitelist_addr.clone()),
+        &freeze_admins_msg,
+        &[],
+    );
+
+    let update_admin_message = ExecuteMsg::UpdateAdmins {
+        admins: vec![ADMIN.to_string()],
     };
-    let res = app.wasm_sudo(collection_whitelist_addr, &remove_msg);
-    println!("res is {:?}", res);
-    let members = vec!["member0".to_string()];
-    add_members_blocked(SECOND_OPERATOR, members, &mut app);
+
+    let res = app.execute_contract(
+        Addr::unchecked(ADMIN),
+        Addr::unchecked(collection_whitelist_addr),
+        &update_admin_message,
+        &[],
+    );
+    assert_eq!(res.unwrap_err().root_cause().to_string(), "Unauthorized");
 }
diff --git a/contracts/whitelist/src/tests/unit_tests.rs b/contracts/whitelist/src/tests/unit_tests.rs
index f44ffa37..1aa77571 100644
--- a/contracts/whitelist/src/tests/unit_tests.rs
+++ b/contracts/whitelist/src/tests/unit_tests.rs
@@ -13,8 +13,8 @@ use cosmwasm_std::{
 };
 
 const ADMIN: &str = "admin";
-const NOT_OPERATOR: &str = "not_operator";
-const SECOND_OPERATOR: &str = "second_operator";
+const NOT_ADMIN: &str = "not_admin";
+const SECOND_ADMIN: &str = "second_admin";
 const UNIT_AMOUNT: u128 = 100_000_000;
 
 const GENESIS_START_TIME: Timestamp = Timestamp::from_nanos(GENESIS_MINT_START_TIME);
@@ -28,7 +28,8 @@ fn setup_contract(deps: DepsMut) {
         mint_price: coin(UNIT_AMOUNT, NATIVE_DENOM),
         per_address_limit: 1,
         member_limit: 1000,
-        operators: vec![ADMIN.to_string(), SECOND_OPERATOR.to_string()],
+        admins: vec![ADMIN.to_string(), SECOND_ADMIN.to_string()],
+        admins_mutable: true,
     };
     let info = mock_info(ADMIN, &[coin(100_000_000, "ustars")]);
     let res = instantiate(deps, mock_env(), info, msg).unwrap();
@@ -51,7 +52,8 @@ fn improper_initialization() {
         mint_price: coin(1, NATIVE_DENOM),
         per_address_limit: 1,
         member_limit: 1000,
-        operators: vec![ADMIN.to_string()],
+        admins: vec![ADMIN.to_string()],
+        admins_mutable: true,
     };
     let info = mock_info(ADMIN, &[coin(100_000_000, "ustars")]);
     instantiate(deps.as_mut(), mock_env(), info, msg).unwrap_err();
@@ -67,7 +69,8 @@ fn improper_initialization_invalid_denom() {
         mint_price: coin(UNIT_AMOUNT, "not_ustars"),
         per_address_limit: 1,
         member_limit: 1000,
-        operators: vec![ADMIN.to_string()],
+        admins: vec![ADMIN.to_string()],
+        admins_mutable: true,
     };
     let info = mock_info(ADMIN, &[coin(100_000_000, "ustars")]);
     let err = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap_err();
@@ -84,7 +87,8 @@ fn improper_initialization_invalid_creation_fee() {
         mint_price: coin(UNIT_AMOUNT, "ustars"),
         per_address_limit: 1,
         member_limit: 3000,
-        operators: vec![ADMIN.to_string()],
+        admins: vec![ADMIN.to_string()],
+        admins_mutable: true,
     };
     let info = mock_info(ADMIN, &[coin(100_000_000, "ustars")]);
     let err = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap_err();
@@ -108,7 +112,8 @@ fn improper_initialization_dedup() {
         mint_price: coin(UNIT_AMOUNT, NATIVE_DENOM),
         per_address_limit: 1,
         member_limit: 1000,
-        operators: vec![ADMIN.to_string()],
+        admins: vec![ADMIN.to_string()],
+        admins_mutable: true,
     };
     let info = mock_info(ADMIN, &[coin(100_000_000, "ustars")]);
     let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap();
@@ -125,7 +130,8 @@ fn check_start_time_after_end_time() {
         mint_price: coin(UNIT_AMOUNT, NATIVE_DENOM),
         per_address_limit: 1,
         member_limit: 1000,
-        operators: vec![ADMIN.to_string()],
+        admins: vec![ADMIN.to_string()],
+        admins_mutable: true,
     };
     let info = mock_info(ADMIN, &[coin(100_000_000, "ustars")]);
     let mut deps = mock_dependencies();
@@ -254,7 +260,8 @@ fn query_members_pagination() {
         mint_price: coin(UNIT_AMOUNT, NATIVE_DENOM),
         per_address_limit: 1,
         member_limit: 1000,
-        operators: vec![ADMIN.to_string()],
+        admins: vec![ADMIN.to_string()],
+        admins_mutable: true,
     };
     let info = mock_info(ADMIN, &[coin(100_000_000, "ustars")]);
     let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap();
@@ -377,13 +384,13 @@ fn cant_update_members_non_admin() {
         to_add: vec!["adsfsa1".to_string(), "adsfsa1".to_string()],
     };
     let msg = ExecuteMsg::AddMembers(add_msg);
-    let info = mock_info(NOT_OPERATOR, &[]);
+    let info = mock_info(NOT_ADMIN, &[]);
     let res = execute(deps.as_mut(), mock_env(), info, msg);
 
-    assert_eq!(res.unwrap_err().to_string(), "UnauthorizedOperator");
+    assert_eq!(res.unwrap_err().to_string(), "Unauthorized")
 }
 
-fn add_members_with_specified_operator(operator: &str) {
+fn add_members_with_specified_admin(admin: &str) {
     let mut deps = mock_dependencies();
     setup_contract(deps.as_mut());
 
@@ -392,7 +399,7 @@ fn add_members_with_specified_operator(operator: &str) {
         to_add: vec!["adsfsa1".to_string(), "adsfsa1".to_string()],
     };
     let msg = ExecuteMsg::AddMembers(add_msg);
-    let info = mock_info(operator, &[]);
+    let info = mock_info(admin, &[]);
     let res = execute(deps.as_mut(), mock_env(), info.clone(), msg.clone()).unwrap();
     assert_eq!(res.attributes.len(), 2);
     let res = query_members(deps.as_ref(), None, None).unwrap();
@@ -411,6 +418,6 @@ fn add_members_with_specified_operator(operator: &str) {
 }
 
 #[test]
-fn second_operator_can_add_members() {
-    add_members_with_specified_operator(SECOND_OPERATOR);
+fn second_admin_can_add_members() {
+    add_members_with_specified_admin(SECOND_ADMIN);
 }