Repository: public-awesome/launchpad
Pull request #463 (2022-11-16 00:44:48)

Move generic whitelist to launchpad
We need a whitelist for the Levana contract that can add and remove addresses.  This version works with strings instead of adddresses since we need to do this for Ethereum addresses that can't be validated in CosmWasm. 

I previously implemented this in names, copying the name minter whitelist. I wanted to move this to Launchpad but couldn't do it bc of too many conflicts. Now with more knowledge of Rust and the codebase was able to implement it.  


Patch:
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index e92cdef0..68f997d5 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -20,5 +20,5 @@ jobs:
       - name: Upload to codecov.io
         uses: codecov/codecov-action@v2
         with:
-          token: ${{secrets.CODECOV_TOKEN}}
+          # token: ${{secrets.CODECOV_TOKEN}}
           fail_ci_if_error: true
diff --git a/Cargo.lock b/Cargo.lock
index 37fac6b2..98d9db11 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1984,7 +1984,7 @@ dependencies = [
  "thiserror",
  "vending-factory",
  "vending-minter",
- "whitelist-generic",
+ "whitelist-generic 0.12.0",
 ]
 
 [[package]]
@@ -2118,6 +2118,18 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "sg-whitelist-basic"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf0670b0de4489224faa3c18ceb28af7251441118473f454110739a69df587b9"
+dependencies = [
+ "cosmwasm-schema",
+ "cosmwasm-std",
+ "schemars",
+ "serde",
+]
+
 [[package]]
 name = "sg-whitelist-basic"
 version = "0.12.0"
@@ -2742,7 +2754,26 @@ dependencies = [
  "serde",
  "sg-name-minter",
  "sg-std 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "sg-whitelist-basic",
+ "sg-whitelist-basic 0.12.0 (git+https://github.com/public-awesome/names.git?rev=91256f9ccfcec66fa8d4c2040acc9e10415c711a)",
+ "thiserror",
+]
+
+[[package]]
+name = "whitelist-generic"
+version = "0.21.1"
+dependencies = [
+ "cosmwasm-schema",
+ "cosmwasm-std",
+ "cw-controllers",
+ "cw-multi-test",
+ "cw-storage-plus",
+ "cw-utils",
+ "cw2",
+ "schemars",
+ "serde",
+ "sg-multi-test 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "sg-std 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "sg-whitelist-basic 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "thiserror",
 ]
 
diff --git a/Cargo.toml b/Cargo.toml
index 735114e9..42a4ee29 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,42 +3,43 @@ members = ["packages/*", "contracts/*"]
 resolver = "2"
 
 [workspace.package]
-version    = "0.21.1"
-edition    = "2021"
-homepage   = "https://stargaze.zone"
+version = "0.21.1"
+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.1", path = "contracts/base-factory" }
-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.1"
-sg2               = "0.21.1"
-sg4               = "0.21.1"
-sg721             = "0.21.1"
-sg721-base        = { version = "0.21.1", path = "contracts/sg721-base" }
-sg-metadata       = "0.21.1"
-sg-multi-test     = "0.21.1"
-sg-splits         = { version = "0.21.1", path = "contracts/splits" }
-sg-std            = "0.21.1"
-sg-whitelist      = { version = "0.21.1", path = "contracts/whitelist" }
-thiserror         = "1.0.31"
-url               = "2.2.2"
-sha2            = {version = "0.10.2", default-features = false }
-rand_core       = { version = "0.6.4", default-features = false }
-vending-factory   = { version = "0.21.1", path = "contracts/vending-factory" }
-vending-minter  = { version = "0.21.1", path = "contracts/vending-minter" }
+base-factory = { version = "0.21.1", path = "contracts/base-factory" }
+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.1"
+sg2 = "0.21.1"
+sg4 = "0.21.1"
+sg721 = "0.21.1"
+sg721-base = { version = "0.21.1", path = "contracts/sg721-base" }
+sg-metadata = "0.21.1"
+sg-multi-test = "0.21.1"
+sg-splits = { version = "0.21.1", path = "contracts/splits" }
+sg-std = "0.21.1"
+sg-whitelist = { version = "0.21.1", path = "contracts/whitelist" }
+thiserror = "1.0.31"
+url = "2.2.2"
+sha2 = { version = "0.10.2", default-features = false }
+rand_core = { version = "0.6.4", default-features = false }
+vending-factory = { version = "0.21.1", path = "contracts/vending-factory" }
+vending-minter = { version = "0.21.1", path = "contracts/vending-minter" }
+sg-whitelist-basic = { version = "0.12.0" }
 
 [profile.release.package.sg721]
 codegen-units = 1
diff --git a/contracts/whitelist-generic/.cargo/config b/contracts/whitelist-generic/.cargo/config
new file mode 100644
index 00000000..d11240ad
--- /dev/null
+++ b/contracts/whitelist-generic/.cargo/config
@@ -0,0 +1,4 @@
+[alias]
+wasm = "build --release --target wasm32-unknown-unknown --lib"
+unit-test = "test --lib"
+schema = "run --example schema"
diff --git a/contracts/whitelist-generic/.gitignore b/contracts/whitelist-generic/.gitignore
new file mode 100644
index 00000000..dfdaaa6b
--- /dev/null
+++ b/contracts/whitelist-generic/.gitignore
@@ -0,0 +1,15 @@
+# Build results
+/target
+
+# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327)
+.cargo-ok
+
+# Text file backups
+**/*.rs.bk
+
+# macOS
+.DS_Store
+
+# IDEs
+*.iml
+.idea
diff --git a/contracts/whitelist-generic/Cargo.toml b/contracts/whitelist-generic/Cargo.toml
new file mode 100644
index 00000000..83b0c5e0
--- /dev/null
+++ b/contracts/whitelist-generic/Cargo.toml
@@ -0,0 +1,60 @@
+[package]
+name = "whitelist-generic"
+authors = ["Michael S<m@publicawesome.com>"]
+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.
+  "contract.wasm",
+  "hash.txt",
+]
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[profile.release]
+opt-level = 3
+debug = false
+rpath = false
+lto = true
+debug-assertions = false
+codegen-units = 1
+panic = 'abort'
+incremental = false
+overflow-checks = true
+
+[features]
+# for more explicit tests, cargo test --features=backtraces
+backtraces = ["cosmwasm-std/backtraces"]
+# use library feature to disable all instantiate/execute/query exports
+library = []
+
+[package.metadata.scripts]
+optimize = """docker run --rm -v "$(pwd)":/code \
+  --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
+  --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
+  cosmwasm/rust-optimizer:0.12.6
+"""
+
+[dependencies]
+cosmwasm-schema = { workspace = true }
+cosmwasm-std = { workspace = true }
+cw-storage-plus = { workspace = true }
+cw-controllers = "0.16.0"
+cw2 = { workspace = true }
+cw-utils = { workspace = true }
+schemars = { workspace = true }
+serde = { workspace = true }
+thiserror = { workspace = true }
+sg-whitelist-basic = { workspace= true }
+sg-std = { workspace = true }
+
+[dev-dependencies]
+cw-multi-test = { workspace = true }
+sg-multi-test = { workspace = true }
\ No newline at end of file
diff --git a/contracts/whitelist-generic/examples/schema.rs b/contracts/whitelist-generic/examples/schema.rs
new file mode 100644
index 00000000..a78660f6
--- /dev/null
+++ b/contracts/whitelist-generic/examples/schema.rs
@@ -0,0 +1,11 @@
+use cosmwasm_schema::write_api;
+
+use whitelist_generic::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
+
+fn main() {
+    write_api! {
+        instantiate: InstantiateMsg,
+        execute: ExecuteMsg,
+        query: QueryMsg,
+    }
+}
diff --git a/contracts/whitelist-generic/schema/whitelist-generic.json b/contracts/whitelist-generic/schema/whitelist-generic.json
new file mode 100644
index 00000000..b59b9d50
--- /dev/null
+++ b/contracts/whitelist-generic/schema/whitelist-generic.json
@@ -0,0 +1,405 @@
+{
+  "contract_name": "whitelist-generic",
+  "contract_version": "0.21.1",
+  "idl_version": "1.0.0",
+  "instantiate": {
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "title": "InstantiateMsg",
+    "type": "object",
+    "required": [
+      "addresses",
+      "per_address_limit"
+    ],
+    "properties": {
+      "addresses": {
+        "type": "array",
+        "items": {
+          "type": "string"
+        }
+      },
+      "mint_discount_bps": {
+        "type": [
+          "integer",
+          "null"
+        ],
+        "format": "uint64",
+        "minimum": 0.0
+      },
+      "per_address_limit": {
+        "type": "integer",
+        "format": "uint32",
+        "minimum": 0.0
+      }
+    },
+    "additionalProperties": false
+  },
+  "execute": {
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "title": "ExecuteMsg",
+    "oneOf": [
+      {
+        "type": "object",
+        "required": [
+          "update_admin"
+        ],
+        "properties": {
+          "update_admin": {
+            "type": "object",
+            "required": [
+              "new_admin"
+            ],
+            "properties": {
+              "new_admin": {
+                "type": "string"
+              }
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "type": "object",
+        "required": [
+          "add_addresses"
+        ],
+        "properties": {
+          "add_addresses": {
+            "type": "object",
+            "required": [
+              "addresses"
+            ],
+            "properties": {
+              "addresses": {
+                "type": "array",
+                "items": {
+                  "type": "string"
+                }
+              }
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "type": "object",
+        "required": [
+          "remove_addresses"
+        ],
+        "properties": {
+          "remove_addresses": {
+            "type": "object",
+            "required": [
+              "addresses"
+            ],
+            "properties": {
+              "addresses": {
+                "type": "array",
+                "items": {
+                  "type": "string"
+                }
+              }
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "description": "Only callable by minter contract. Increment mint count on whitelist map.",
+        "type": "object",
+        "required": [
+          "process_address"
+        ],
+        "properties": {
+          "process_address": {
+            "type": "object",
+            "required": [
+              "address"
+            ],
+            "properties": {
+              "address": {
+                "type": "string"
+              }
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "type": "object",
+        "required": [
+          "update_per_address_limit"
+        ],
+        "properties": {
+          "update_per_address_limit": {
+            "type": "object",
+            "required": [
+              "limit"
+            ],
+            "properties": {
+              "limit": {
+                "type": "integer",
+                "format": "uint32",
+                "minimum": 0.0
+              }
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "type": "object",
+        "required": [
+          "purge"
+        ],
+        "properties": {
+          "purge": {
+            "type": "object",
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      }
+    ]
+  },
+  "query": {
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "title": "QueryMsg",
+    "oneOf": [
+      {
+        "type": "object",
+        "required": [
+          "config"
+        ],
+        "properties": {
+          "config": {
+            "type": "object",
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "type": "object",
+        "required": [
+          "includes_address"
+        ],
+        "properties": {
+          "includes_address": {
+            "type": "object",
+            "required": [
+              "address"
+            ],
+            "properties": {
+              "address": {
+                "type": "string"
+              }
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "type": "object",
+        "required": [
+          "mint_count"
+        ],
+        "properties": {
+          "mint_count": {
+            "type": "object",
+            "required": [
+              "address"
+            ],
+            "properties": {
+              "address": {
+                "type": "string"
+              }
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "description": "Avoid processing addresses that will fail. Includes address and under per address limit",
+        "type": "object",
+        "required": [
+          "is_processable"
+        ],
+        "properties": {
+          "is_processable": {
+            "type": "object",
+            "required": [
+              "address"
+            ],
+            "properties": {
+              "address": {
+                "type": "string"
+              }
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "type": "object",
+        "required": [
+          "admin"
+        ],
+        "properties": {
+          "admin": {
+            "type": "object",
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "type": "object",
+        "required": [
+          "address_count"
+        ],
+        "properties": {
+          "address_count": {
+            "type": "object",
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "type": "object",
+        "required": [
+          "per_address_limit"
+        ],
+        "properties": {
+          "per_address_limit": {
+            "type": "object",
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      }
+    ]
+  },
+  "migrate": null,
+  "sudo": null,
+  "responses": {
+    "address_count": {
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "title": "uint64",
+      "type": "integer",
+      "format": "uint64",
+      "minimum": 0.0
+    },
+    "admin": {
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "title": "AdminResponse",
+      "description": "Returned from Admin.query_admin()",
+      "type": "object",
+      "properties": {
+        "admin": {
+          "type": [
+            "string",
+            "null"
+          ]
+        }
+      },
+      "additionalProperties": false
+    },
+    "config": {
+      "$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": [
+            "admin",
+            "per_address_limit"
+          ],
+          "properties": {
+            "admin": {
+              "$ref": "#/definitions/Addr"
+            },
+            "mint_discount_bps": {
+              "type": [
+                "integer",
+                "null"
+              ],
+              "format": "uint64",
+              "minimum": 0.0
+            },
+            "per_address_limit": {
+              "type": "integer",
+              "format": "uint32",
+              "minimum": 0.0
+            }
+          },
+          "additionalProperties": false
+        }
+      }
+    },
+    "includes_address": {
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "title": "Boolean",
+      "type": "boolean"
+    },
+    "is_processable": {
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "title": "IsProcessableResponse",
+      "type": "object",
+      "required": [
+        "processable"
+      ],
+      "properties": {
+        "processable": {
+          "type": "boolean"
+        }
+      },
+      "additionalProperties": false
+    },
+    "mint_count": {
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "title": "uint64",
+      "type": "integer",
+      "format": "uint64",
+      "minimum": 0.0
+    },
+    "per_address_limit": {
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "title": "PerAddressLimitResponse",
+      "type": "object",
+      "required": [
+        "limit"
+      ],
+      "properties": {
+        "limit": {
+          "type": "integer",
+          "format": "uint64",
+          "minimum": 0.0
+        }
+      },
+      "additionalProperties": false
+    }
+  }
+}
diff --git a/contracts/whitelist-generic/src/contract.rs b/contracts/whitelist-generic/src/contract.rs
new file mode 100644
index 00000000..21d0668a
--- /dev/null
+++ b/contracts/whitelist-generic/src/contract.rs
@@ -0,0 +1,274 @@
+use crate::state::{Config, CONFIG, TOTAL_ADDRESS_COUNT, WHITELIST};
+#[cfg(not(feature = "library"))]
+use cosmwasm_std::entry_point;
+use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, Event, MessageInfo, Order, StdResult};
+use cw2::set_contract_version;
+
+use crate::error::ContractError;
+use crate::msg::{ConfigResponse, ExecuteMsg, InstantiateMsg, QueryMsg};
+use cw_utils::nonpayable;
+use sg_std::{Response, StargazeMsgWrapper};
+
+// version info for migration info
+const CONTRACT_NAME: &str = "crates.io:whitelist-updatable";
+const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn instantiate(
+    deps: DepsMut,
+    _env: Env,
+    info: MessageInfo,
+    mut msg: InstantiateMsg,
+) -> Result<cosmwasm_std::Response<StargazeMsgWrapper>, ContractError> {
+    nonpayable(&info)?;
+    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
+    let config = Config {
+        admin: info.sender,
+        per_address_limit: msg.per_address_limit,
+        /// 1% = 100, 50% = 5000
+        mint_discount_bps: msg.mint_discount_bps,
+    };
+
+    // remove duplicate addresses
+    msg.addresses.sort_unstable();
+    msg.addresses.dedup();
+
+    let mut count = 0u64;
+    for address in msg.addresses.into_iter() {
+        WHITELIST.save(deps.storage, &address, &0u32)?;
+        count += 1;
+    }
+
+    TOTAL_ADDRESS_COUNT.save(deps.storage, &count)?;
+    CONFIG.save(deps.storage, &config)?;
+
+    Ok(Response::default()
+        .add_attribute("action", "instantiate")
+        .add_attribute("contract_name", CONTRACT_NAME)
+        .add_attribute("contract_version", CONTRACT_VERSION))
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn execute(
+    deps: DepsMut,
+    _env: Env,
+    info: MessageInfo,
+    msg: ExecuteMsg,
+) -> Result<cosmwasm_std::Response<StargazeMsgWrapper>, ContractError> {
+    match msg {
+        ExecuteMsg::UpdateAdmin { new_admin } => execute_update_admin(deps, info, new_admin),
+        ExecuteMsg::AddAddresses { addresses } => execute_add_addresses(deps, info, addresses),
+        ExecuteMsg::RemoveAddresses { addresses } => {
+            execute_remove_addresses(deps, info, addresses)
+        }
+        ExecuteMsg::ProcessAddress { address } => execute_process_address(deps, info, address),
+        ExecuteMsg::UpdatePerAddressLimit { limit } => {
+            execute_update_per_address_limit(deps, info, limit)
+        }
+        ExecuteMsg::Purge {} => execute_purge(deps, info),
+    }
+}
+
+pub fn execute_update_admin(
+    deps: DepsMut,
+    info: MessageInfo,
+    new_admin: String,
+) -> Result<cosmwasm_std::Response<StargazeMsgWrapper>, ContractError> {
+    nonpayable(&info)?;
+    let mut config = CONFIG.load(deps.storage)?;
+    if config.admin != info.sender {
+        return Err(ContractError::Unauthorized {});
+    }
+
+    config.admin = deps.api.addr_validate(&new_admin)?;
+    CONFIG.save(deps.storage, &config)?;
+    let event = Event::new("update_admin")
+        .add_attribute("new_admin", config.admin)
+        .add_attribute("sender", info.sender);
+    Ok(Response::new().add_event(event))
+}
+
+pub fn execute_add_addresses(
+    deps: DepsMut,
+    info: MessageInfo,
+    mut addresses: Vec<String>,
+) -> Result<cosmwasm_std::Response<StargazeMsgWrapper>, ContractError> {
+    let config = CONFIG.load(deps.storage)?;
+    let mut count = TOTAL_ADDRESS_COUNT.load(deps.storage)?;
+    if config.admin != info.sender {
+        return Err(ContractError::Unauthorized {});
+    }
+
+    // dedupe
+    addresses.sort_unstable();
+    addresses.dedup();
+
+    for address in addresses.into_iter() {
+        if WHITELIST.has(deps.storage, &address.clone()) {
+            return Err(ContractError::AddressAlreadyExists { addr: address });
+        } else {
+            WHITELIST.save(deps.storage, &address, &0u32)?;
+            count += 1;
+        }
+    }
+
+    TOTAL_ADDRESS_COUNT.save(deps.storage, &count)?;
+
+    let event = Event::new("add_addresses")
+        .add_attribute("new-count", count.to_string())
+        .add_attribute("sender", info.sender);
+    Ok(Response::new().add_event(event))
+}
+
+pub fn execute_remove_addresses(
+    deps: DepsMut,
+    info: MessageInfo,
+    mut addresses: Vec<String>,
+) -> Result<cosmwasm_std::Response<StargazeMsgWrapper>, ContractError> {
+    nonpayable(&info)?;
+    let config = CONFIG.load(deps.storage)?;
+    let mut count = TOTAL_ADDRESS_COUNT.load(deps.storage)?;
+    if config.admin != info.sender {
+        return Err(ContractError::Unauthorized {});
+    }
+
+    // dedupe
+    addresses.sort_unstable();
+    addresses.dedup();
+
+    for address in addresses.into_iter() {
+        if WHITELIST.has(deps.storage, &address) {
+            WHITELIST.remove(deps.storage, &address);
+            count -= 1;
+        } else {
+            return Err(ContractError::AddressNotFound { addr: address });
+        }
+    }
+
+    TOTAL_ADDRESS_COUNT.save(deps.storage, &count)?;
+    let event = Event::new("remove_addresses")
+        .add_attribute("new-count", count.to_string())
+        .add_attribute("sender", info.sender);
+    Ok(Response::new().add_event(event))
+}
+
+pub fn execute_process_address(
+    deps: DepsMut,
+    info: MessageInfo,
+    address: String,
+) -> Result<cosmwasm_std::Response<StargazeMsgWrapper>, ContractError> {
+    nonpayable(&info)?;
+    let config = CONFIG.load(deps.storage)?;
+    if config.admin != info.sender {
+        return Err(ContractError::Unauthorized {});
+    }
+    if !WHITELIST.has(deps.storage, &address) {
+        return Err(ContractError::AddressNotFound { addr: address });
+    }
+
+    if WHITELIST.load(deps.storage, &address)? >= config.per_address_limit {
+        return Err(ContractError::OverPerAddressLimit {});
+    }
+
+    let count = WHITELIST.load(deps.storage, &address)?;
+    WHITELIST.save(deps.storage, &address, &(count + 1))?;
+
+    let event = Event::new("process_address")
+        .add_attribute("address", address)
+        .add_attribute("mint-count", (count + 1).to_string())
+        .add_attribute("sender", info.sender);
+    Ok(Response::new().add_event(event))
+}
+
+pub fn execute_update_per_address_limit(
+    deps: DepsMut,
+    info: MessageInfo,
+    limit: u32,
+) -> Result<cosmwasm_std::Response<StargazeMsgWrapper>, ContractError> {
+    nonpayable(&info)?;
+    let mut config = CONFIG.load(deps.storage)?;
+    if config.admin != info.sender {
+        return Err(ContractError::Unauthorized {});
+    }
+
+    config.per_address_limit = limit;
+    CONFIG.save(deps.storage, &config)?;
+
+    let event = Event::new("update_per_address_limit")
+        .add_attribute("new-limit", limit.to_string())
+        .add_attribute("sender", info.sender);
+    Ok(Response::new().add_event(event))
+}
+
+pub fn execute_purge(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
+    nonpayable(&info)?;
+    let config = CONFIG.load(deps.storage)?;
+    if config.admin != info.sender {
+        return Err(ContractError::Unauthorized {});
+    }
+
+    let keys = WHITELIST
+        .keys(deps.as_ref().storage, None, None, Order::Ascending)
+        .map(|x| x.unwrap())
+        .collect::<Vec<_>>();
+
+    for key in keys {
+        WHITELIST.remove(deps.storage, &key);
+    }
+
+    TOTAL_ADDRESS_COUNT.save(deps.storage, &0u64)?;
+
+    let event = Event::new("purge").add_attribute("sender", info.sender);
+    Ok(Response::new().add_event(event))
+}
+
+#[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::IncludesAddress { address } => to_binary(&query_includes_address(deps, address)?),
+        QueryMsg::MintCount { address } => to_binary(&query_mint_count(deps, address)?),
+        QueryMsg::Admin {} => to_binary(&query_admin(deps)?),
+        QueryMsg::AddressCount {} => to_binary(&query_address_count(deps)?),
+        QueryMsg::PerAddressLimit {} => to_binary(&query_per_address_limit(deps)?),
+        QueryMsg::IsProcessable { address } => to_binary(&query_is_processable(deps, address)?),
+    }
+}
+
+pub fn query_config(deps: Deps) -> StdResult<ConfigResponse> {
+    let config = CONFIG.load(deps.storage)?;
+    Ok(ConfigResponse { config })
+}
+
+pub fn query_includes_address(deps: Deps, address: String) -> StdResult<bool> {
+    Ok(WHITELIST.has(deps.storage, &address))
+}
+
+pub fn query_mint_count(deps: Deps, address: String) -> StdResult<u32> {
+    WHITELIST.load(deps.storage, &address)
+}
+
+pub fn query_admin(deps: Deps) -> StdResult<String> {
+    let config = CONFIG.load(deps.storage)?;
+    Ok(config.admin.to_string())
+}
+
+pub fn query_address_count(deps: Deps) -> StdResult<u64> {
+    TOTAL_ADDRESS_COUNT.load(deps.storage)
+}
+
+pub fn query_per_address_limit(deps: Deps) -> StdResult<u32> {
+    let config = CONFIG.load(deps.storage)?;
+    Ok(config.per_address_limit)
+}
+
+pub fn query_is_processable(deps: Deps, address: String) -> StdResult<bool> {
+    // address not in whitelist, it's not processable
+    if !WHITELIST.has(deps.storage, &address) {
+        return Ok(false);
+    }
+    // compare addr mint count to per address limit
+    let count = WHITELIST.load(deps.storage, &address)?;
+    let config = CONFIG.load(deps.storage)?;
+    Ok(count < config.per_address_limit)
+}
diff --git a/contracts/whitelist-generic/src/error.rs b/contracts/whitelist-generic/src/error.rs
new file mode 100644
index 00000000..b0c80bfe
--- /dev/null
+++ b/contracts/whitelist-generic/src/error.rs
@@ -0,0 +1,26 @@
+use cosmwasm_std::StdError;
+use cw_utils::PaymentError;
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum ContractError {
+    #[error("{0}")]
+    Std(#[from] StdError),
+
+    #[error("{0}")]
+    PaymentError(#[from] PaymentError),
+
+    #[error("Unauthorized")]
+    Unauthorized {},
+
+    #[error("AddressNotFound {addr}")]
+    AddressNotFound { addr: String },
+
+    #[error("OverPerAddressLimit")]
+    OverPerAddressLimit {},
+
+    #[error("AddressAlreadyExists {addr}")]
+    AddressAlreadyExists { addr: String },
+    // Add any other custom errors you like here.
+    // Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details.
+}
diff --git a/contracts/whitelist-generic/src/helpers.rs b/contracts/whitelist-generic/src/helpers.rs
new file mode 100644
index 00000000..5e4cdd15
--- /dev/null
+++ b/contracts/whitelist-generic/src/helpers.rs
@@ -0,0 +1,54 @@
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+use cosmwasm_std::{to_binary, Addr, QuerierWrapper, QueryRequest, StdResult, WasmMsg, WasmQuery};
+use sg_std::CosmosMsg;
+
+use crate::{
+    msg::{ConfigResponse, ExecuteMsg, QueryMsg},
+    state::Config,
+};
+
+/// CwTemplateContract is a wrapper around Addr that provides a lot of helpers
+/// for working with this.
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
+pub struct WhitelistGenericContract(pub Addr);
+
+impl WhitelistGenericContract {
+    pub fn addr(&self) -> Addr {
+        self.0.clone()
+    }
+
+    pub fn call<T: Into<ExecuteMsg>>(&self, msg: T) -> StdResult<CosmosMsg> {
+        let msg = to_binary(&msg.into())?;
+        Ok(WasmMsg::Execute {
+            contract_addr: self.addr().into(),
+            msg,
+            funds: vec![],
+        }
+        .into())
+    }
+
+    pub fn process_address(&self, address: &str) -> StdResult<CosmosMsg> {
+        self.call(ExecuteMsg::ProcessAddress {
+            address: address.to_string(),
+        })
+    }
+
+    pub fn includes(&self, querier: &QuerierWrapper, address: String) -> StdResult<bool> {
+        let includes: bool = querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
+            contract_addr: self.addr().into(),
+            msg: to_binary(&QueryMsg::IncludesAddress { address })?,
+        }))?;
+        Ok(includes)
+    }
+
+    pub fn config(&self, querier: &QuerierWrapper) -> StdResult<Config> {
+        let res: ConfigResponse = querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
+            contract_addr: self.addr().into(),
+            msg: to_binary(&QueryMsg::Config {})?,
+        }))?;
+
+        Ok(res.config)
+    }
+}
diff --git a/contracts/whitelist-generic/src/lib.rs b/contracts/whitelist-generic/src/lib.rs
new file mode 100644
index 00000000..d6185c4e
--- /dev/null
+++ b/contracts/whitelist-generic/src/lib.rs
@@ -0,0 +1,8 @@
+pub mod contract;
+mod error;
+pub mod helpers;
+pub mod integration_tests;
+pub mod msg;
+pub mod state;
+
+pub use crate::error::ContractError;
diff --git a/contracts/whitelist-generic/src/msg.rs b/contracts/whitelist-generic/src/msg.rs
new file mode 100644
index 00000000..bdd1ad2b
--- /dev/null
+++ b/contracts/whitelist-generic/src/msg.rs
@@ -0,0 +1,66 @@
+use cosmwasm_schema::{cw_serde, QueryResponses};
+
+use crate::state::Config;
+
+#[cw_serde]
+pub struct InstantiateMsg {
+    pub addresses: Vec<String>,
+    pub per_address_limit: u32,
+    pub mint_discount_bps: Option<u64>,
+}
+
+#[cw_serde]
+pub enum ExecuteMsg {
+    UpdateAdmin {
+        new_admin: String,
+    },
+    AddAddresses {
+        addresses: Vec<String>,
+    },
+    RemoveAddresses {
+        addresses: Vec<String>,
+    },
+    /// Only callable by minter contract. Increment mint count on whitelist map.
+    ProcessAddress {
+        address: String,
+    },
+    UpdatePerAddressLimit {
+        limit: u32,
+    },
+    Purge {},
+}
+
+#[cw_serde]
+#[derive(QueryResponses)]
+pub enum QueryMsg {
+    #[returns(ConfigResponse)]
+    Config {},
+    #[returns(bool)]
+    IncludesAddress { address: String },
+    #[returns(u64)]
+    MintCount { address: String },
+    /// Avoid processing addresses that will fail. Includes address and under per address limit
+    #[returns(IsProcessableResponse)]
+    IsProcessable { address: String },
+    #[returns(cw_controllers::AdminResponse)]
+    Admin {},
+    #[returns(u64)]
+    AddressCount {},
+    #[returns(PerAddressLimitResponse)]
+    PerAddressLimit {},
+}
+
+#[cw_serde]
+pub struct ConfigResponse {
+    pub config: Config,
+}
+
+#[cw_serde]
+pub struct PerAddressLimitResponse {
+    pub limit: u64,
+}
+
+#[cw_serde]
+pub struct IsProcessableResponse {
+    pub processable: bool,
+}
diff --git a/contracts/whitelist-generic/src/state.rs b/contracts/whitelist-generic/src/state.rs
new file mode 100644
index 00000000..29134543
--- /dev/null
+++ b/contracts/whitelist-generic/src/state.rs
@@ -0,0 +1,22 @@
+use cosmwasm_schema::cw_serde;
+use cosmwasm_std::{Addr, Decimal, Uint128};
+use cw_storage_plus::{Item, Map};
+
+#[cw_serde]
+pub struct Config {
+    pub admin: Addr,
+    pub per_address_limit: u32,
+    pub mint_discount_bps: Option<u64>,
+}
+
+impl Config {
+    pub fn mint_discount(&self) -> Option<Decimal> {
+        self.mint_discount_bps
+            .map(|v| Decimal::percent(v) / Uint128::from(100u128))
+    }
+}
+
+pub const CONFIG: Item<Config> = Item::new("config");
+pub const TOTAL_ADDRESS_COUNT: Item<u64> = Item::new("total_address_count");
+// Holds all addresses and mint count
+pub const WHITELIST: Map<&str, u32> = Map::new("wl");


Test patch:
diff --git a/contracts/whitelist-generic/src/integration_tests.rs b/contracts/whitelist-generic/src/integration_tests.rs
new file mode 100644
index 00000000..71b26a32
--- /dev/null
+++ b/contracts/whitelist-generic/src/integration_tests.rs
@@ -0,0 +1,372 @@
+#[cfg(test)]
+mod tests {
+    use crate::msg::*;
+
+    use cosmwasm_std::Addr;
+    use sg_std::StargazeMsgWrapper;
+
+    use cw_multi_test::{Contract, ContractWrapper, Executor};
+
+    use sg_multi_test::StargazeApp;
+
+    const CREATOR: &str = "creator";
+    const OTHER_ADMIN: &str = "other_admin";
+    const PER_ADDRESS_LIMIT: u32 = 10;
+
+    fn custom_mock_app() -> StargazeApp {
+        StargazeApp::default()
+    }
+
+    pub fn wl_contract() -> Box<dyn Contract<StargazeMsgWrapper>> {
+        let contract = ContractWrapper::new(
+            crate::contract::execute,
+            crate::contract::instantiate,
+            crate::contract::query,
+        );
+        Box::new(contract)
+    }
+
+    fn get_init_address_list() -> Vec<String> {
+        vec![
+            "addr0001".to_string(),
+            "addr0002".to_string(),
+            "addr0003".to_string(),
+            "addr0004".to_string(),
+            "addr0005".to_string(),
+        ]
+    }
+
+    pub fn instantiate_with_addresses(app: &mut StargazeApp) -> Addr {
+        let addrs = get_init_address_list();
+        let msg = InstantiateMsg {
+            per_address_limit: PER_ADDRESS_LIMIT,
+            addresses: addrs,
+            mint_discount_bps: None,
+        };
+        let wl_id = app.store_code(wl_contract());
+        app.instantiate_contract(
+            wl_id,
+            Addr::unchecked(CREATOR),
+            &msg,
+            &[],
+            "wl-contract".to_string(),
+            None,
+        )
+        .unwrap()
+    }
+
+    #[test]
+    pub fn test_instantiate_with_addresses() {
+        let mut app = custom_mock_app();
+        instantiate_with_addresses(&mut app);
+    }
+
+    #[test]
+    pub fn test_wl_contract_initial_state_query() {
+        let mut app = custom_mock_app();
+        let wl_addr = instantiate_with_addresses(&mut app);
+        let addrs = get_init_address_list();
+
+        let admin: String = app
+            .wrap()
+            .query_wasm_smart(&wl_addr, &QueryMsg::Admin {})
+            .unwrap();
+        assert_eq!(admin, CREATOR.to_string());
+
+        let count: u64 = app
+            .wrap()
+            .query_wasm_smart(&wl_addr, &QueryMsg::AddressCount {})
+            .unwrap();
+        assert_eq!(count, addrs.len() as u64);
+
+        let includes: bool = app
+            .wrap()
+            .query_wasm_smart(
+                &wl_addr,
+                &QueryMsg::IncludesAddress {
+                    address: addrs[0].clone(),
+                },
+            )
+            .unwrap();
+        assert!(includes);
+
+        let count: u32 = app
+            .wrap()
+            .query_wasm_smart(
+                &wl_addr,
+                &QueryMsg::MintCount {
+                    address: addrs[0].clone(),
+                },
+            )
+            .unwrap();
+        assert_eq!(count, 0);
+
+        let limit: u32 = app
+            .wrap()
+            .query_wasm_smart(&wl_addr, &QueryMsg::PerAddressLimit {})
+            .unwrap();
+        assert_eq!(limit, 10);
+    }
+
+    #[test]
+    pub fn test_process_address() {
+        let mut app = custom_mock_app();
+        let wl_addr = instantiate_with_addresses(&mut app);
+        let addrs = get_init_address_list();
+
+        let msg = ExecuteMsg::ProcessAddress {
+            address: addrs[0].clone(),
+        };
+        let res = app.execute_contract(Addr::unchecked(CREATOR), wl_addr.clone(), &msg, &[]);
+        assert!(res.is_ok());
+        let res: u32 = app
+            .wrap()
+            .query_wasm_smart(
+                &wl_addr,
+                &QueryMsg::MintCount {
+                    address: addrs[0].clone(),
+                },
+            )
+            .unwrap();
+        assert_eq!(res, 1);
+    }
+
+    #[test]
+    fn test_update_admin() {
+        let mut app = custom_mock_app();
+        let wl_addr = instantiate_with_addresses(&mut app);
+
+        let msg = ExecuteMsg::UpdateAdmin {
+            new_admin: OTHER_ADMIN.to_string(),
+        };
+        let res = app.execute_contract(Addr::unchecked(CREATOR), wl_addr.clone(), &msg, &[]);
+        assert!(res.is_ok());
+        let res: String = app
+            .wrap()
+            .query_wasm_smart(&wl_addr, &QueryMsg::Admin {})
+            .unwrap();
+        assert_eq!(res, OTHER_ADMIN.to_string());
+    }
+    #[test]
+    pub fn test_add_existing_addresses_fail() {
+        let mut app = custom_mock_app();
+        let wl_addr = instantiate_with_addresses(&mut app);
+
+        // add addresses
+        let msg = ExecuteMsg::AddAddresses {
+            addresses: vec![
+                "addr0001".to_string(),
+                "addr0002".to_string(),
+                "addr0003".to_string(),
+                "addr0004".to_string(),
+                "addr0006".to_string(),
+            ],
+        };
+        let res = app.execute_contract(Addr::unchecked(OTHER_ADMIN), wl_addr.clone(), &msg, &[]);
+        assert!(res.is_err());
+        let res: bool = app
+            .wrap()
+            .query_wasm_smart(
+                &wl_addr,
+                &QueryMsg::IncludesAddress {
+                    address: "addr0006".to_string(),
+                },
+            )
+            .unwrap();
+        assert!(!res);
+        let res: u64 = app
+            .wrap()
+            .query_wasm_smart(&wl_addr, &QueryMsg::AddressCount {})
+            .unwrap();
+        assert_eq!(res, 5);
+    }
+
+    pub fn add_addresses(app: &mut StargazeApp, wl_addr: Addr, addresses: Vec<String>) {
+        let msg = ExecuteMsg::AddAddresses { addresses };
+        let res = app.execute_contract(Addr::unchecked(CREATOR), wl_addr, &msg, &[]);
+        assert!(res.is_ok());
+    }
+
+    #[test]
+    pub fn add_addresses_success() {
+        let mut app = custom_mock_app();
+        let wl_addr = instantiate_with_addresses(&mut app);
+        let addresses = vec!["addr0007".to_string(), "addr0006".to_string()];
+        add_addresses(&mut app, wl_addr, addresses);
+    }
+
+    #[test]
+    pub fn remove_invalid_address_list_fail() {
+        let mut app = custom_mock_app();
+        let wl_addr = instantiate_with_addresses(&mut app);
+        let addresses = vec!["addr0007".to_string(), "addr0006".to_string()];
+        add_addresses(&mut app, wl_addr.clone(), addresses);
+
+        let res: bool = app
+            .wrap()
+            .query_wasm_smart(
+                &wl_addr,
+                &QueryMsg::IncludesAddress {
+                    address: "addr0006".to_string(),
+                },
+            )
+            .unwrap();
+        assert!(res);
+        let res: u64 = app
+            .wrap()
+            .query_wasm_smart(&wl_addr, &QueryMsg::AddressCount {})
+            .unwrap();
+        assert_eq!(res, 7);
+
+        // remove addresses
+        let msg = ExecuteMsg::RemoveAddresses {
+            addresses: vec![
+                "addr0000".to_string(),
+                "addr0001".to_string(),
+                "addr0002".to_string(),
+                "addr0003".to_string(),
+                "addr0004".to_string(),
+                "addr0006".to_string(),
+            ],
+        };
+        let res = app.execute_contract(Addr::unchecked(CREATOR), wl_addr, &msg, &[]);
+        assert!(res.is_err());
+    }
+    #[test]
+    pub fn test_remove_addresses_success() {
+        let mut app = custom_mock_app();
+        let wl_addr = instantiate_with_addresses(&mut app);
+        let addresses = vec!["addr0007".to_string(), "addr0006".to_string()];
+        add_addresses(&mut app, wl_addr.clone(), addresses);
+
+        let msg = ExecuteMsg::RemoveAddresses {
+            addresses: vec![
+                "addr0001".to_string(),
+                "addr0002".to_string(),
+                "addr0003".to_string(),
+                "addr0004".to_string(),
+                "addr0006".to_string(),
+            ],
+        };
+        let res = app.execute_contract(Addr::unchecked(CREATOR), wl_addr.clone(), &msg, &[]);
+        assert!(res.is_ok());
+        let res: bool = app
+            .wrap()
+            .query_wasm_smart(
+                &wl_addr,
+                &QueryMsg::IncludesAddress {
+                    address: "addr0006".to_string(),
+                },
+            )
+            .unwrap();
+        assert!(!res);
+        let res: u64 = app
+            .wrap()
+            .query_wasm_smart(&wl_addr, &QueryMsg::AddressCount {})
+            .unwrap();
+        assert_eq!(res, 2);
+    }
+
+    pub fn update_per_address_limit(app: &mut StargazeApp, wl_addr: Addr) {
+        // per address limit
+        let new_per_address_limit = 1;
+        let msg = ExecuteMsg::UpdatePerAddressLimit {
+            limit: new_per_address_limit,
+        };
+        let res = app.execute_contract(Addr::unchecked(CREATOR), wl_addr.clone(), &msg, &[]);
+        assert!(res.is_ok());
+        let res: u32 = app
+            .wrap()
+            .query_wasm_smart(&wl_addr, &QueryMsg::PerAddressLimit {})
+            .unwrap();
+        assert_eq!(res, 1);
+    }
+    #[test]
+    pub fn test_update_per_address_limit() {
+        let mut app = custom_mock_app();
+        let wl_addr = instantiate_with_addresses(&mut app);
+        let addresses = vec!["addr0006".to_string()];
+        add_addresses(&mut app, wl_addr.clone(), addresses);
+        update_per_address_limit(&mut app, wl_addr)
+    }
+    #[test]
+    pub fn test_surpass_process_limit() {
+        let mut app = custom_mock_app();
+        let wl_addr = instantiate_with_addresses(&mut app);
+        let addresses = vec!["addr0007".to_string()];
+        add_addresses(&mut app, wl_addr.clone(), addresses);
+        update_per_address_limit(&mut app, wl_addr.clone());
+
+        // surpass limit
+        let res: bool = app
+            .wrap()
+            .query_wasm_smart(
+                &wl_addr,
+                &QueryMsg::IsProcessable {
+                    address: "addr0007".to_string(),
+                },
+            )
+            .unwrap();
+        assert!(res);
+        let msg = ExecuteMsg::ProcessAddress {
+            address: "addr0007".to_string(),
+        };
+        let res = app.execute_contract(Addr::unchecked(CREATOR), wl_addr.clone(), &msg, &[]);
+
+        assert!(res.is_ok());
+        let res: bool = app
+            .wrap()
+            .query_wasm_smart(
+                &wl_addr,
+                &QueryMsg::IsProcessable {
+                    address: "addr0007".to_string(),
+                },
+            )
+            .unwrap();
+
+        assert!(!res);
+        let msg = ExecuteMsg::ProcessAddress {
+            address: "addr0007".to_string(),
+        };
+        let res = app.execute_contract(Addr::unchecked(CREATOR), wl_addr, &msg, &[]);
+        assert!(res.is_err());
+    }
+
+    #[test]
+    pub fn test_purge_whitelist() {
+        let mut app = custom_mock_app();
+        let wl_addr = instantiate_with_addresses(&mut app);
+        let addresses = vec!["addr0007".to_string()];
+        add_addresses(&mut app, wl_addr.clone(), addresses);
+        update_per_address_limit(&mut app, wl_addr.clone());
+
+        // purge
+        let msg = ExecuteMsg::Purge {};
+        let res = app.execute_contract(Addr::unchecked(CREATOR), wl_addr.clone(), &msg, &[]);
+        assert!(res.is_ok());
+        let res: u32 = app
+            .wrap()
+            .query_wasm_smart(&wl_addr, &QueryMsg::AddressCount {})
+            .unwrap();
+        assert_eq!(res, 0);
+        // does not include addr0007
+        let res: bool = app
+            .wrap()
+            .query_wasm_smart(
+                &wl_addr,
+                &QueryMsg::IncludesAddress {
+                    address: "addr0007".to_string(),
+                },
+            )
+            .unwrap();
+        assert!(!res);
+
+        // query config
+        let res: ConfigResponse = app
+            .wrap()
+            .query_wasm_smart(&wl_addr, &QueryMsg::Config {})
+            .unwrap();
+        assert_eq!(res.config.admin, Addr::unchecked(CREATOR).to_string());
+        assert_eq!(res.config.per_address_limit, 1);
+    }
+}