Repository: CosmWasm/sylvia
Pull request #45 (2022-09-14 14:25:17)

Cw1 subkeys migrated


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index e2a0bd5..aaaa0b8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,9 +4,9 @@ version = 3
 
 [[package]]
 name = "anyhow"
-version = "1.0.57"
+version = "1.0.64"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
+checksum = "b9a8f622bcf6ff3df478e9deba3e03e4e04b300f8e6a139e192c05fa3490afc7"
 
 [[package]]
 name = "assert_matches"
@@ -41,6 +41,15 @@ dependencies = [
  "generic-array",
 ]
 
+[[package]]
+name = "block-buffer"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
+dependencies = [
+ "generic-array",
+]
+
 [[package]]
 name = "byteorder"
 version = "1.4.3"
@@ -61,9 +70,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
 name = "const-oid"
-version = "0.7.1"
+version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3"
+checksum = "722e23542a15cea1f65d4a1419c4cfd7a26706c70871a13a04238ca3f40f1661"
 
 [[package]]
 name = "convert_case"
@@ -73,11 +82,11 @@ checksum = "fb4a24b1aaf0fd0ce8b45161144d6f42cd91677fd5940fd431183eb023b3a2b8"
 
 [[package]]
 name = "cosmwasm-crypto"
-version = "1.0.0"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5eb0afef2325df81aadbf9be1233f522ed8f6e91df870c764bc44cca2b1415bd"
+checksum = "16c50d753d44148c7ff3279ac44b87b5b91e790f4c70db0e3900df211310a2bf"
 dependencies = [
- "digest",
+ "digest 0.10.3",
  "ed25519-zebra",
  "k256",
  "rand_core 0.6.3",
@@ -86,22 +95,23 @@ dependencies = [
 
 [[package]]
 name = "cosmwasm-derive"
-version = "1.0.0"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b36e527620a2a3e00e46b6e731ab6c9b68d11069c986f7d7be8eba79ef081a4"
+checksum = "9053ebe2ad85831e9f9e2124fa2b22807528a78b25cc447483ce2a4aadfba394"
 dependencies = [
  "syn",
 ]
 
 [[package]]
 name = "cosmwasm-std"
-version = "1.0.0"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "875994993c2082a6fcd406937bf0fca21c349e4a624f3810253a14fa83a3a195"
+checksum = "eb8da0ae28693d892af2944319b48adc23c42725dc0fe7271b8baa38c10865da"
 dependencies = [
  "base64",
  "cosmwasm-crypto",
  "cosmwasm-derive",
+ "derivative",
  "forward_ref",
  "schemars",
  "serde",
@@ -137,9 +147,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
 
 [[package]]
 name = "crypto-bigint"
-version = "0.3.2"
+version = "0.4.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21"
+checksum = "9f2b443d17d49dad5ef0ede301c3179cc923b8822f3393b4d2c28c269dd4a122"
 dependencies = [
  "generic-array",
  "rand_core 0.6.3",
@@ -148,13 +158,13 @@ dependencies = [
 ]
 
 [[package]]
-name = "crypto-mac"
-version = "0.11.1"
+name = "crypto-common"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
 dependencies = [
  "generic-array",
- "subtle",
+ "typenum",
 ]
 
 [[package]]
@@ -164,7 +174,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61"
 dependencies = [
  "byteorder",
- "digest",
+ "digest 0.9.0",
  "rand_core 0.5.1",
  "subtle",
  "zeroize",
@@ -235,6 +245,24 @@ dependencies = [
  "sylvia",
 ]
 
+[[package]]
+name = "cw1-subkeys"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "cosmwasm-std",
+ "cw-multi-test",
+ "cw-storage-plus 0.14.0",
+ "cw-utils",
+ "cw1",
+ "cw1-whitelist",
+ "cw2",
+ "schemars",
+ "serde",
+ "sylvia",
+ "thiserror",
+]
+
 [[package]]
 name = "cw1-whitelist"
 version = "0.1.2"
@@ -276,11 +304,12 @@ dependencies = [
 
 [[package]]
 name = "der"
-version = "0.5.1"
+version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c"
+checksum = "13dd2ae565c0a381dde7fade45fce95984c568bdcb4700a4fdbe3175e0380b2f"
 dependencies = [
  "const-oid",
+ "zeroize",
 ]
 
 [[package]]
@@ -303,6 +332,17 @@ dependencies = [
  "generic-array",
 ]
 
+[[package]]
+name = "digest"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
+dependencies = [
+ "block-buffer 0.10.3",
+ "crypto-common",
+ "subtle",
+]
+
 [[package]]
 name = "dyn-clone"
 version = "1.0.5"
@@ -311,9 +351,9 @@ checksum = "21e50f3adc76d6a43f5ed73b698a87d0760ca74617f60f7c3b879003536fdd28"
 
 [[package]]
 name = "ecdsa"
-version = "0.13.4"
+version = "0.14.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0d69ae62e0ce582d56380743515fefaf1a8c70cec685d9677636d7e30ae9dc9"
+checksum = "1826508d57f3140a2e8e3c307b19915a266c92a1b8c2f6bb54e29e5d72a394ae"
 dependencies = [
  "der",
  "elliptic-curve",
@@ -331,7 +371,7 @@ dependencies = [
  "hex",
  "rand_core 0.6.3",
  "serde",
- "sha2",
+ "sha2 0.9.9",
  "thiserror",
  "zeroize",
 ]
@@ -344,16 +384,18 @@ checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
 
 [[package]]
 name = "elliptic-curve"
-version = "0.11.12"
+version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25b477563c2bfed38a3b7a60964c49e058b2510ad3f12ba3483fd8f62c2306d6"
+checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3"
 dependencies = [
  "base16ct",
  "crypto-bigint",
  "der",
+ "digest 0.10.3",
  "ff",
  "generic-array",
  "group",
+ "pkcs8",
  "rand_core 0.6.3",
  "sec1",
  "subtle",
@@ -362,9 +404,9 @@ dependencies = [
 
 [[package]]
 name = "ff"
-version = "0.11.1"
+version = "0.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "131655483be284720a17d74ff97592b8e76576dc25563148601df2d7c9080924"
+checksum = "df689201f395c6b90dfe87127685f8dbfc083a5e779e613575d8bd7314300c3e"
 dependencies = [
  "rand_core 0.6.3",
  "subtle",
@@ -410,9 +452,9 @@ dependencies = [
 
 [[package]]
 name = "group"
-version = "0.11.0"
+version = "0.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89"
+checksum = "7391856def869c1c81063a03457c676fbcd419709c3dfb33d8d319de484b154d"
 dependencies = [
  "ff",
  "rand_core 0.6.3",
@@ -427,12 +469,11 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
 
 [[package]]
 name = "hmac"
-version = "0.11.0"
+version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
 dependencies = [
- "crypto-mac",
- "digest",
+ "digest 0.10.3",
 ]
 
 [[package]]
@@ -452,15 +493,14 @@ checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
 
 [[package]]
 name = "k256"
-version = "0.10.4"
+version = "0.11.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19c3a5e0a0b8450278feda242592512e09f61c72e018b8cd5c859482802daf2d"
+checksum = "6db2573d3fd3e4cc741affc9b5ce1a8ce36cf29f09f80f36da4309d0ae6d7854"
 dependencies = [
  "cfg-if",
  "ecdsa",
  "elliptic-curve",
- "sec1",
- "sha2",
+ "sha2 0.10.5",
 ]
 
 [[package]]
@@ -499,13 +539,12 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
 
 [[package]]
 name = "pkcs8"
-version = "0.8.0"
+version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0"
+checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba"
 dependencies = [
  "der",
  "spki",
- "zeroize",
 ]
 
 [[package]]
@@ -603,9 +642,9 @@ dependencies = [
 
 [[package]]
 name = "rfc6979"
-version = "0.1.0"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96ef608575f6392792f9ecf7890c00086591d29a83910939d430753f7c050525"
+checksum = "88c86280f057430a52f4861551b092a01b419b8eacefc7c995eacb9dc132fe32"
 dependencies = [
  "crypto-bigint",
  "hmac",
@@ -644,10 +683,11 @@ dependencies = [
 
 [[package]]
 name = "sec1"
-version = "0.2.1"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08da66b8b0965a5555b6bd6639e68ccba85e1e2506f5fbb089e93f8a04e1a2d1"
+checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928"
 dependencies = [
+ "base16ct",
  "der",
  "generic-array",
  "pkcs8",
@@ -663,9 +703,9 @@ checksum = "93f6841e709003d68bb2deee8c343572bf446003ec20a583e76f7b15cebf3711"
 
 [[package]]
 name = "serde"
-version = "1.0.137"
+version = "1.0.144"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
+checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
 dependencies = [
  "serde_derive",
 ]
@@ -690,9 +730,9 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.137"
+version = "1.0.144"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
+checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -727,28 +767,39 @@ version = "0.9.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
 dependencies = [
- "block-buffer",
+ "block-buffer 0.9.0",
  "cfg-if",
  "cpufeatures",
- "digest",
+ "digest 0.9.0",
  "opaque-debug",
 ]
 
+[[package]]
+name = "sha2"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf9db03534dff993187064c4e0c05a5708d2a9728ace9a8959b77bedf415dac5"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest 0.10.3",
+]
+
 [[package]]
 name = "signature"
-version = "1.4.0"
+version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02658e48d89f2bec991f9a78e69cfa4c316f8d6a6c4ec12fae1aeb263d486788"
+checksum = "f0ea32af43239f0d353a7dd75a22d94c329c8cdaafdcb4c1c1335aa10c298a4a"
 dependencies = [
- "digest",
+ "digest 0.10.3",
  "rand_core 0.6.3",
 ]
 
 [[package]]
 name = "spki"
-version = "0.5.4"
+version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27"
+checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b"
 dependencies = [
  "base64ct",
  "der",
@@ -805,18 +856,18 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "1.0.31"
+version = "1.0.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
+checksum = "8c1b05ca9d106ba7d2e31a9dab4a64e7be2cce415321966ea3132c49a656e252"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.31"
+version = "1.0.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
+checksum = "e8f2591983642de85c921015f3f070c665a197ed69e417af436115e3a1407487"
 dependencies = [
  "proc-macro2",
  "quote",
diff --git a/Cargo.toml b/Cargo.toml
index 220fb5f..d0a834f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,2 +1,13 @@
 [workspace]
-members = ["sylvia", "sylvia-derive", "contracts/cw1", "contracts/cw1-whitelist", "contracts/cw4"]
+members = [
+    "sylvia",
+    "sylvia-derive",
+
+# Contract intefaces
+    "contracts/cw1",
+    "contracts/cw4",
+
+# Contracts
+    "contracts/cw1-whitelist", 
+    "contracts/cw1-subkeys",
+    ]
diff --git a/contracts/cw1-subkeys/Cargo.toml b/contracts/cw1-subkeys/Cargo.toml
new file mode 100644
index 0000000..ad25bc3
--- /dev/null
+++ b/contracts/cw1-subkeys/Cargo.toml
@@ -0,0 +1,28 @@
+[package]
+name = "cw1-subkeys"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[features]
+library = []
+tests = ["library", "cw-multi-test", "anyhow"]
+
+[dependencies]
+anyhow = { version = "1.0.64", optional = true }
+cosmwasm-std = "1.1.0"
+cw-multi-test = { version = "0.14.0", optional = true }
+cw-storage-plus = "0.14.0"
+cw-utils = "0.14.0"
+cw1 = { version = "0.1.2", path = "../cw1" }
+cw1-whitelist = { version = "0.1.2", path = "../cw1-whitelist", features = ["library"] }
+cw2 = "0.14.0"
+schemars = "0.8.10"
+serde = { version = "1.0.144", features = ["derive"] }
+sylvia = { version = "0.1.2", path = "../../sylvia" }
+thiserror = "1.0.34"
+
+[dev-dependencies]
+anyhow = "1.0.64"
+cw-multi-test = "0.14.0"
diff --git a/contracts/cw1-subkeys/src/contract.rs b/contracts/cw1-subkeys/src/contract.rs
new file mode 100644
index 0000000..43858e3
--- /dev/null
+++ b/contracts/cw1-subkeys/src/contract.rs
@@ -0,0 +1,329 @@
+use cosmwasm_std::{
+    ensure, ensure_ne, Addr, BankMsg, Coin, CosmosMsg, Deps, DepsMut, DistributionMsg, Env,
+    MessageInfo, Order, Response, StakingMsg, StdResult,
+};
+use cw1_whitelist::contract::Cw1WhitelistContract;
+use cw2::set_contract_version;
+use cw_storage_plus::{Bound, Map};
+use cw_utils::Expiration;
+use sylvia::contract;
+
+use crate::error::ContractError;
+use crate::responses::{
+    AllAllowancesResponse, AllPermissionsResponse, AllowanceInfo, PermissionsInfo,
+};
+use crate::state::{Allowance, Permissions};
+
+pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME");
+pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
+
+// Default and max limits for pagination
+const MAX_LIMIT: u32 = 30;
+const DEFAULT_LIMIT: u32 = 10;
+
+pub struct Cw1SubkeysContract<'a> {
+    pub(crate) whitelist: Cw1WhitelistContract<'a>,
+    pub(crate) permissions: Map<'static, &'a Addr, Permissions>,
+    pub(crate) allowances: Map<'static, &'a Addr, Allowance>,
+}
+
+#[contract]
+impl Cw1SubkeysContract<'_> {
+    pub const fn new() -> Self {
+        Self {
+            whitelist: Cw1WhitelistContract::new(),
+            permissions: Map::new("permissions"),
+            allowances: Map::new("allowances"),
+        }
+    }
+
+    #[msg(instantiate)]
+    pub fn instantiate(
+        &self,
+        ctx: (DepsMut, Env, MessageInfo),
+        admins: Vec<String>,
+        mutable: bool,
+    ) -> Result<Response, ContractError> {
+        let (mut deps, env, info) = ctx;
+        let result = self
+            .whitelist
+            .instantiate((deps.branch(), env, info), admins, mutable)?;
+        set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
+        Ok(result)
+    }
+
+    #[msg(exec)]
+    pub fn increase_allowance(
+        &self,
+        ctx: (DepsMut, Env, MessageInfo),
+        spender: String,
+        amount: Coin,
+        expires: Option<Expiration>,
+    ) -> Result<Response, ContractError> {
+        let (deps, env, info) = ctx;
+        ensure!(
+            self.whitelist.is_admin(deps.as_ref(), &info.sender),
+            ContractError::Unauthorized {}
+        );
+
+        let spender = deps.api.addr_validate(&spender)?;
+        ensure_ne!(info.sender, spender, ContractError::CannotSetOwnAccount {});
+
+        self.allowances.update(deps.storage, &spender, |allow| {
+            let prev_expires = allow
+                .as_ref()
+                .map(|allow| allow.expires)
+                .unwrap_or_default();
+
+            let mut allowance = allow
+                .filter(|allow| !allow.expires.is_expired(&env.block))
+                .unwrap_or_default();
+
+            if let Some(exp) = expires {
+                if exp.is_expired(&env.block) {
+                    return Err(ContractError::SettingExpiredAllowance(exp));
+                }
+
+                allowance.expires = exp;
+            } else if prev_expires.is_expired(&env.block) {
+                return Err(ContractError::SettingExpiredAllowance(prev_expires));
+            }
+
+            allowance.balance += amount.clone();
+            Ok(allowance)
+        })?;
+
+        let res = Response::new()
+            .add_attribute("action", "increase_allowance")
+            .add_attribute("owner", info.sender)
+            .add_attribute("spender", spender)
+            .add_attribute("denomination", amount.denom)
+            .add_attribute("amount", amount.amount);
+        Ok(res)
+    }
+
+    #[msg(exec)]
+    pub fn decrease_allowance(
+        &self,
+        ctx: (DepsMut, Env, MessageInfo),
+        spender: String,
+        amount: Coin,
+        expires: Option<Expiration>,
+    ) -> Result<Response, ContractError> {
+        let (deps, env, info) = ctx;
+        ensure!(
+            self.whitelist.is_admin(deps.as_ref(), &info.sender),
+            ContractError::Unauthorized {}
+        );
+
+        let spender = deps.api.addr_validate(&spender)?;
+        ensure_ne!(info.sender, spender, ContractError::CannotSetOwnAccount {});
+
+        let allowance = self.allowances.update(deps.storage, &spender, |allow| {
+            // Fail fast
+            let mut allowance = allow
+                .filter(|allow| !allow.expires.is_expired(&env.block))
+                .ok_or(ContractError::NoAllowance {})?;
+
+            if let Some(exp) = expires {
+                if exp.is_expired(&env.block) {
+                    return Err(ContractError::SettingExpiredAllowance(exp));
+                }
+
+                allowance.expires = exp;
+            }
+
+            allowance.balance = allowance.balance.sub_saturating(amount.clone())?; // Tolerates underflows (amount bigger than balance), but fails if there are no tokens at all for the denom (report potential errors)
+            Ok(allowance)
+        })?;
+
+        if allowance.balance.is_empty() {
+            self.allowances.remove(deps.storage, &spender);
+        }
+
+        let res = Response::new()
+            .add_attribute("action", "decrease_allowance")
+            .add_attribute("owner", info.sender)
+            .add_attribute("spender", spender)
+            .add_attribute("denomination", amount.denom)
+            .add_attribute("amount", amount.amount);
+        Ok(res)
+    }
+
+    #[msg(exec)]
+    pub fn set_permissions(
+        &self,
+        ctx: (DepsMut, Env, MessageInfo),
+        spender: String,
+        permissions: Permissions,
+    ) -> Result<Response, ContractError> {
+        let (deps, _, info) = ctx;
+        ensure!(
+            self.whitelist.is_admin(deps.as_ref(), &info.sender),
+            ContractError::Unauthorized {}
+        );
+
+        let spender = deps.api.addr_validate(&spender)?;
+        ensure_ne!(info.sender, spender, ContractError::CannotSetOwnAccount {});
+        self.permissions
+            .save(deps.storage, &spender, &permissions)?;
+
+        let res = Response::new()
+            .add_attribute("action", "set_permissions")
+            .add_attribute("owner", info.sender)
+            .add_attribute("spender", spender)
+            .add_attribute("permissions", permissions.to_string());
+        Ok(res)
+    }
+
+    #[msg(query)]
+    pub fn allowance(&self, ctx: (Deps, Env), spender: String) -> StdResult<Allowance> {
+        let (deps, env) = ctx;
+
+        // we can use unchecked here as it is a query - bad value means a miss, we never write it
+        let spender = Addr::unchecked(&spender);
+        let allow = self
+            .allowances
+            .may_load(deps.storage, &spender)?
+            .filter(|allow| !allow.expires.is_expired(&env.block))
+            .unwrap_or_default();
+
+        Ok(allow)
+    }
+
+    #[msg(query)]
+    pub fn permissions(&self, ctx: (Deps, Env), spender: String) -> StdResult<Permissions> {
+        let (deps, _) = ctx;
+
+        let spender = Addr::unchecked(&spender);
+        let permissions = self
+            .permissions
+            .may_load(deps.storage, &spender)?
+            .unwrap_or_default();
+
+        Ok(permissions)
+    }
+
+    #[msg(query)]
+    pub fn all_allowances(
+        &self,
+        ctx: (Deps, Env),
+        start_after: Option<String>,
+        limit: Option<u32>,
+    ) -> StdResult<AllAllowancesResponse> {
+        let (deps, env) = ctx;
+
+        let limit = calc_limit(limit);
+        // we use raw addresses here....
+        let start = start_after.map(|s| Bound::ExclusiveRaw(s.into()));
+
+        let allowances: StdResult<_> = self
+            .allowances
+            .range(deps.storage, start, None, Order::Ascending)
+            .filter(|item| {
+                if let Ok((_, allow)) = item {
+                    !allow.expires.is_expired(&env.block)
+                } else {
+                    true
+                }
+            })
+            .take(limit)
+            .map(|item| {
+                item.map(|(addr, allow)| AllowanceInfo {
+                    spender: addr,
+                    balance: allow.balance,
+                    expires: allow.expires,
+                })
+            })
+            .collect();
+
+        Ok(AllAllowancesResponse {
+            allowances: allowances?,
+        })
+    }
+
+    #[msg(query)]
+    pub fn all_permissions(
+        &self,
+        ctx: (Deps, Env),
+        start_after: Option<String>,
+        limit: Option<u32>,
+    ) -> StdResult<AllPermissionsResponse> {
+        let (deps, _) = ctx;
+
+        let limit = calc_limit(limit);
+        let start = start_after.map(|s| Bound::ExclusiveRaw(s.into()));
+
+        let permissions: StdResult<_> = self
+            .permissions
+            .range(deps.storage, start, None, Order::Ascending)
+            .take(limit)
+            .map(|item| {
+                item.map(|(addr, perm)| PermissionsInfo {
+                    spender: addr,
+                    permissions: perm,
+                })
+            })
+            .collect();
+
+        Ok(AllPermissionsResponse {
+            permissions: permissions?,
+        })
+    }
+
+    pub fn is_authorized(
+        &self,
+        deps: Deps,
+        env: &Env,
+        sender: &Addr,
+        msg: &CosmosMsg,
+    ) -> StdResult<bool> {
+        if self.whitelist.is_admin(deps, sender) {
+            return Ok(true);
+        }
+
+        match msg {
+            CosmosMsg::Bank(BankMsg::Send { amount, .. }) => {
+                // now we check if there is enough allowance for this message
+                let allowance = self.allowances.may_load(deps.storage, sender)?;
+                match allowance {
+                    // if there is an allowance, we subtract the requested amount to ensure it is covered (error on underflow)
+                    Some(allow) => Ok(!allow.expires.is_expired(&env.block)
+                        && (allow.balance - amount.clone()).is_ok()),
+                    None => Ok(false),
+                }
+            }
+            CosmosMsg::Staking(staking_msg) => {
+                let permissions = match self.permissions.may_load(deps.storage, sender)? {
+                    Some(permissions) => permissions,
+                    None => return Ok(false),
+                };
+
+                let delegate =
+                    matches!(staking_msg, StakingMsg::Delegate { .. } if permissions.delegate);
+                let undelegate =
+                    matches!(staking_msg, StakingMsg::Undelegate { .. } if permissions.undelegate);
+                let redelegate =
+                    matches!(staking_msg, StakingMsg::Redelegate { .. } if permissions.redelegate);
+
+                Ok(delegate || undelegate || redelegate)
+            }
+            CosmosMsg::Distribution(distribution_msg) => {
+                let permissions = match self.permissions.may_load(deps.storage, sender)? {
+                    Some(permissions) => permissions,
+                    None => return Ok(false),
+                };
+
+                let set_withdraw_addr = matches!(distribution_msg, DistributionMsg::SetWithdrawAddress { .. } if permissions.withdraw);
+                let withdraw_perm = matches!(distribution_msg, DistributionMsg::WithdrawDelegatorReward { .. } if permissions.withdraw);
+
+                Ok(set_withdraw_addr || withdraw_perm)
+            }
+            _ => Ok(false),
+        }
+    }
+}
+
+fn calc_limit(request: Option<u32>) -> usize {
+    request.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize
+}
diff --git a/contracts/cw1-subkeys/src/cw1.rs b/contracts/cw1-subkeys/src/cw1.rs
new file mode 100644
index 0000000..8e607af
--- /dev/null
+++ b/contracts/cw1-subkeys/src/cw1.rs
@@ -0,0 +1,41 @@
+use cosmwasm_std::{ensure, Addr, DepsMut, Env, MessageInfo, Response, StdResult};
+use cw1::Cw1;
+
+use crate::contract::Cw1SubkeysContract;
+use crate::error::ContractError;
+
+impl Cw1 for Cw1SubkeysContract<'_> {
+    type Error = ContractError;
+
+    fn execute(
+        &self,
+        ctx: (DepsMut, Env, MessageInfo),
+        msgs: Vec<cosmwasm_std::CosmosMsg>,
+    ) -> Result<cosmwasm_std::Response, Self::Error> {
+        let (deps, env, info) = ctx;
+        let authorized: StdResult<_> = msgs.iter().fold(Ok(true), |acc, msg| {
+            Ok(acc? & self.is_authorized(deps.as_ref(), &env, &info.sender, msg)?)
+        });
+
+        ensure!(authorized?, ContractError::Unauthorized {});
+
+        let res = Response::new()
+            .add_messages(msgs)
+            .add_attribute("action", "execute")
+            .add_attribute("owner", info.sender);
+        Ok(res)
+    }
+
+    fn can_execute(
+        &self,
+        ctx: (cosmwasm_std::Deps, cosmwasm_std::Env),
+        sender: String,
+        msg: cosmwasm_std::CosmosMsg,
+    ) -> StdResult<cw1::CanExecuteResp> {
+        let (deps, env) = ctx;
+        let sender = Addr::unchecked(&sender);
+
+        self.is_authorized(deps, &env, &sender, &msg)
+            .map(|can| cw1::CanExecuteResp { can_execute: can })
+    }
+}
diff --git a/contracts/cw1-subkeys/src/error.rs b/contracts/cw1-subkeys/src/error.rs
new file mode 100644
index 0000000..56b636c
--- /dev/null
+++ b/contracts/cw1-subkeys/src/error.rs
@@ -0,0 +1,35 @@
+use cosmwasm_std::StdError;
+use cw1_whitelist::error::ContractError as WhitelistError;
+use cw_utils::Expiration;
+use thiserror::Error;
+
+#[derive(Error, Debug, PartialEq)]
+pub enum ContractError {
+    #[error("{0}")]
+    Std(#[from] StdError),
+
+    #[error("Unauthorized")]
+    Unauthorized {},
+
+    #[error("Contract is frozen")]
+    ContractFrozen {},
+
+    #[error("Cannot set to own account")]
+    CannotSetOwnAccount {},
+
+    #[error("No allowance for this account")]
+    NoAllowance {},
+
+    #[error("Allowance already expired while setting: {0}")]
+    SettingExpiredAllowance(Expiration),
+}
+
+impl From<WhitelistError> for ContractError {
+    fn from(err: WhitelistError) -> Self {
+        match err {
+            WhitelistError::Unauthorized {} => ContractError::Unauthorized {},
+            WhitelistError::ContractFrozen {} => ContractError::ContractFrozen {},
+            WhitelistError::Std(err) => ContractError::Std(err),
+        }
+    }
+}
diff --git a/contracts/cw1-subkeys/src/lib.rs b/contracts/cw1-subkeys/src/lib.rs
new file mode 100644
index 0000000..11df766
--- /dev/null
+++ b/contracts/cw1-subkeys/src/lib.rs
@@ -0,0 +1,46 @@
+pub mod contract;
+mod cw1;
+pub mod error;
+#[cfg(any(test, feature = "tests"))]
+pub mod multitest;
+pub mod responses;
+pub mod state;
+mod whitelist;
+
+#[cfg(not(feature = "library"))]
+mod entry_points {
+    use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response};
+
+    use crate::contract::{ContractExecMsg, ContractQueryMsg, Cw1SubkeysContract, InstantiateMsg};
+    use crate::error::ContractError;
+
+    const CONTRACT: Cw1SubkeysContract = Cw1SubkeysContract::new();
+
+    #[entry_point]
+    pub fn instantiate(
+        deps: DepsMut,
+        env: Env,
+        info: MessageInfo,
+        msg: InstantiateMsg,
+    ) -> Result<Response, ContractError> {
+        msg.dispatch(&CONTRACT, (deps, env, info))
+    }
+
+    #[entry_point]
+    pub fn execute(
+        deps: DepsMut,
+        env: Env,
+        info: MessageInfo,
+        msg: ContractExecMsg,
+    ) -> Result<Response, ContractError> {
+        msg.dispatch(&CONTRACT, (deps, env, info))
+    }
+
+    #[entry_point]
+    pub fn query(deps: Deps, env: Env, msg: ContractQueryMsg) -> Result<Binary, ContractError> {
+        msg.dispatch(&CONTRACT, (deps, env))
+    }
+}
+
+#[cfg(not(feature = "library"))]
+pub use crate::entry_points::*;
diff --git a/contracts/cw1-subkeys/src/responses.rs b/contracts/cw1-subkeys/src/responses.rs
new file mode 100644
index 0000000..f6973b1
--- /dev/null
+++ b/contracts/cw1-subkeys/src/responses.rs
@@ -0,0 +1,123 @@
+use cosmwasm_std::Addr;
+use cw_utils::{Expiration, NativeBalance};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+use crate::state::Permissions;
+
+#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug, Default)]
+pub struct AllAllowancesResponse {
+    pub allowances: Vec<AllowanceInfo>,
+}
+
+#[cfg(test)]
+impl AllAllowancesResponse {
+    pub fn canonical(mut self) -> Self {
+        self.allowances = self
+            .allowances
+            .into_iter()
+            .map(AllowanceInfo::canonical)
+            .collect();
+        self.allowances.sort_by(AllowanceInfo::cmp_by_spender);
+        self
+    }
+}
+
+#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
+pub struct AllowanceInfo {
+    pub spender: Addr,
+    pub balance: NativeBalance,
+    pub expires: Expiration,
+}
+
+#[cfg(test)]
+impl AllowanceInfo {
+    /// Utility function providing some ordering to be used with `slice::sort_by`.
+    ///
+    /// Note, that this doesn't implement full ordering - items with same spender but differing on
+    /// permissions, would be considered equal, however as spender is a unique key in any valid
+    /// state this is enough for testing purposes.
+    ///
+    /// Example:
+    ///
+    /// ```
+    /// # use cw_utils::{Expiration, NativeBalance};
+    /// # use cw1_subkeys::msg::AllowanceInfo;
+    /// # use cosmwasm_schema::{cw_serde, QueryResponses};
+    /// # use cosmwasm_std::coin;
+    ///
+    /// let mut allows = vec![Allowance {
+    ///   spender: "spender2".to_owned(),
+    ///   balance: NativeBalance(vec![coin(1, "token1")]),
+    ///   expires: Expiration::Never {},
+    /// }, Allowance {
+    ///   spender: "spender1".to_owned(),
+    ///   balance: NativeBalance(vec![coin(2, "token2")]),
+    ///   expires: Expiration::Never {},
+    /// }];
+    ///
+    /// allows.sort_by(Allowance::cmp_by_spender);
+    ///
+    /// assert_eq!(
+    ///   allows.into_iter().map(|allow| allow.spender).collect::<Vec<_>>(),
+    ///   vec!["spender1".to_owned(), "spender2".to_owned()]
+    /// );
+    /// ```
+    pub fn cmp_by_spender(left: &Self, right: &Self) -> std::cmp::Ordering {
+        left.spender.cmp(&right.spender)
+    }
+
+    pub fn canonical(mut self) -> Self {
+        self.balance.normalize();
+        self
+    }
+}
+
+#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug, Default)]
+pub struct AllPermissionsResponse {
+    pub permissions: Vec<PermissionsInfo>,
+}
+
+#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
+pub struct PermissionsInfo {
+    pub spender: Addr,
+    pub permissions: Permissions,
+}
+
+#[cfg(any(test, feature = "test-utils"))]
+impl PermissionsInfo {
+    /// Utility function providing some ordering to be used with `slice::sort_by`.
+    ///
+    /// Note, that this doesn't implement full ordering - items with same spender but differing on
+    /// permissions, would be considered equal, however as spender is a unique key in any valid
+    /// state this is enough for testing purposes.
+    ///
+    /// Example:
+    ///
+    /// ```
+    /// # use cw1_subkeys::msg::PermissionsInfo;
+    /// # use cw1_subkeys::state::Permissions;
+    ///
+    /// let mut perms = vec![PermissionsInfo {
+    ///   spender: "spender2".to_owned(),
+    ///   permissions: Permissions::default(),
+    /// }, PermissionsInfo {
+    ///   spender: "spender1".to_owned(),
+    ///   permissions: Permissions::default(),
+    /// }];
+    ///
+    /// perms.sort_by(PermissionsInfo::cmp_by_spender);
+    ///
+    /// assert_eq!(
+    ///   perms.into_iter().map(|perm| perm.spender).collect::<Vec<_>>(),
+    ///   vec!["spender1".to_owned(), "spender2".to_owned()]
+    /// );
+    /// ```
+    pub fn cmp_by_spender(left: &Self, right: &Self) -> std::cmp::Ordering {
+        left.spender.cmp(&right.spender)
+    }
+
+    pub fn spender(&self) -> &str {
+        self.spender.as_ref()
+    }
+}
diff --git a/contracts/cw1-subkeys/src/state.rs b/contracts/cw1-subkeys/src/state.rs
new file mode 100644
index 0000000..5db9224
--- /dev/null
+++ b/contracts/cw1-subkeys/src/state.rs
@@ -0,0 +1,67 @@
+use core::fmt;
+
+use cw_utils::{Expiration, NativeBalance};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+// Permissions struct defines users message execution permissions.
+// Could have implemented permissions for each cosmos module(StakingPermissions, GovPermissions etc...)
+// But that meant a lot of code for each module. Keeping the permissions inside one struct is more
+// optimal. Define other modules permissions here.
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, Default, Copy)]
+pub struct Permissions {
+    pub delegate: bool,
+    pub redelegate: bool,
+    pub undelegate: bool,
+    pub withdraw: bool,
+}
+
+impl fmt::Display for Permissions {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(
+            f,
+            "staking: {{ delegate: {}, redelegate: {}, undelegate: {}, withdraw: {} }}",
+            self.delegate, self.redelegate, self.undelegate, self.withdraw
+        )
+    }
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, Default)]
+pub struct Allowance {
+    pub balance: NativeBalance,
+    pub expires: Expiration,
+}
+
+#[cfg(test)]
+impl Allowance {
+    /// Utility function for converting message to its canonical form, so two messages with
+    /// different representation but same semantic meaning can be easily compared.
+    ///
+    /// It could be encapsulated in custom `PartialEq` implementation, but `PartialEq` is expected
+    /// to be fast, so it seems to be reasonable to keep it as representation-equality, and
+    /// canonicalize message only when it is needed
+    ///
+    /// Example:
+    ///
+    /// ```
+    /// # use cw_utils::{Expiration, NativeBalance};
+    /// # use cw1_subkeys::state::Allowance;
+    /// # use cosmwasm_std::coin;
+    ///
+    /// let allow1 = Allowance {
+    ///   balance: NativeBalance(vec![coin(1, "token1"), coin(0, "token2"), coin(2, "token1"), coin(3, "token3")]),
+    ///   expires: Expiration::Never {},
+    /// };
+    ///
+    /// let allow2 = Allowance {
+    ///   balance: NativeBalance(vec![coin(3, "token3"), coin(3, "token1")]),
+    ///   expires: Expiration::Never {},
+    /// };
+    ///
+    /// assert_eq!(allow1.canonical(), allow2.canonical());
+    /// ```
+    pub fn canonical(mut self) -> Self {
+        self.balance.normalize();
+        self
+    }
+}
diff --git a/contracts/cw1-subkeys/src/whitelist.rs b/contracts/cw1-subkeys/src/whitelist.rs
new file mode 100644
index 0000000..7c9c83a
--- /dev/null
+++ b/contracts/cw1-subkeys/src/whitelist.rs
@@ -0,0 +1,40 @@
+use cw1_whitelist::whitelist::Whitelist;
+
+use crate::contract::Cw1SubkeysContract;
+use crate::error::ContractError;
+
+impl Whitelist for Cw1SubkeysContract<'_> {
+    type Error = ContractError;
+
+    fn freeze(
+        &self,
+        ctx: (
+            cosmwasm_std::DepsMut,
+            cosmwasm_std::Env,
+            cosmwasm_std::MessageInfo,
+        ),
+    ) -> Result<cosmwasm_std::Response, Self::Error> {
+        self.whitelist.freeze(ctx).map_err(From::from)
+    }
+
+    fn update_admins(
+        &self,
+        ctx: (
+            cosmwasm_std::DepsMut,
+            cosmwasm_std::Env,
+            cosmwasm_std::MessageInfo,
+        ),
+        admins: Vec<String>,
+    ) -> Result<cosmwasm_std::Response, Self::Error> {
+        self.whitelist
+            .update_admins(ctx, admins)
+            .map_err(From::from)
+    }
+
+    fn admin_list(
+        &self,
+        ctx: (cosmwasm_std::Deps, cosmwasm_std::Env),
+    ) -> cosmwasm_std::StdResult<cw1_whitelist::responses::AdminListResponse> {
+        self.whitelist.admin_list(ctx)
+    }
+}
diff --git a/contracts/cw1-whitelist/Cargo.toml b/contracts/cw1-whitelist/Cargo.toml
index 7bb5d7c..b8b5356 100644
--- a/contracts/cw1-whitelist/Cargo.toml
+++ b/contracts/cw1-whitelist/Cargo.toml
@@ -13,7 +13,7 @@ crate-type = ["cdylib", "rlib"]
 
 [features]
 library = []
-tests = ["library", "cw-multi-test"]
+tests = ["library", "cw-multi-test", "anyhow"]
 
 [dependencies]
 cosmwasm-std = { version = "1.0.0", features = ["staking"] }
@@ -25,6 +25,7 @@ cw-storage-plus = "0.14.0"
 thiserror = { version = "1.0.23" }
 cw2 = "0.14.0"
 cw-multi-test = { version = "0.14.0", optional = true }
+anyhow = { version = "1", optional = true }
 
 [dev-dependencies]
 anyhow = "1"
diff --git a/contracts/cw1-whitelist/src/contract.rs b/contracts/cw1-whitelist/src/contract.rs
index 4d0ed84..c1a9bb5 100644
--- a/contracts/cw1-whitelist/src/contract.rs
+++ b/contracts/cw1-whitelist/src/contract.rs
@@ -1,6 +1,6 @@
 use crate::error::ContractError;
-use crate::responses::AdminListResponse;
-use cosmwasm_std::{Addr, Deps, DepsMut, Empty, Env, MessageInfo, Order, Response, StdResult};
+use crate::whitelist;
+use cosmwasm_std::{Addr, Deps, DepsMut, Empty, Env, MessageInfo, Response};
 
 use cw2::set_contract_version;
 use cw_storage_plus::{Item, Map};
@@ -16,6 +16,7 @@ pub struct Cw1WhitelistContract<'a> {
 
 #[contract]
 #[messages(cw1 as Cw1)]
+#[messages(whitelist as Whitelist)]
 impl Cw1WhitelistContract<'_> {
     pub const fn new() -> Self {
         Self {
@@ -44,98 +45,6 @@ impl Cw1WhitelistContract<'_> {
         Ok(Response::new())
     }
 
-    #[msg(exec)]
-    pub fn freeze(&self, ctx: (DepsMut, Env, MessageInfo)) -> Result<Response, ContractError> {
-        let (deps, _, info) = ctx;
-
-        if !self.is_admin(deps.as_ref(), &info.sender) {
-            return Err(ContractError::Unauthorized {});
-        }
-
-        self.mutable.save(deps.storage, &false)?;
-
-        let resp = Response::new().add_attribute("action", "freeze");
-        Ok(resp)
-    }
-
-    #[msg(exec)]
-    pub fn update_admins(
-        &self,
-        ctx: (DepsMut, Env, MessageInfo),
-        mut admins: Vec<String>,
-    ) -> Result<Response, ContractError> {
-        let (deps, _, info) = ctx;
-
-        if !self.is_admin(deps.as_ref(), &info.sender) {
-            return Err(ContractError::Unauthorized {});
-        }
-
-        if !self.mutable.load(deps.storage)? {
-            return Err(ContractError::ContractFrozen {});
-        }
-
-        admins.sort_unstable();
-        let mut low_idx = 0;
-
-        let to_remove: Vec<_> = self
-            .admins
-            .keys(deps.storage, None, None, Order::Ascending)
-            .filter(|addr| {
-                // This is a bit of optimization basing on the fact that both `admins` and queried
-                // keys range are sorted. Binary search would always return the index which is at
-                // most as big as searched item, so for next item there is no point in looking at
-                // lower indices. On the other hand - if we reached and of the sequence, we want to
-                // remove all following keys.
-                addr.as_ref()
-                    .map(|addr| {
-                        if low_idx >= admins.len() {
-                            return true;
-                        }
-
-                        match admins[low_idx..].binary_search(&addr.into()) {
-                            Ok(idx) => {
-                                low_idx = idx;
-                                false
-                            }
-                            Err(idx) => {
-                                low_idx = idx;
-                                true
-                            }
-                        }
-                    })
-                    .unwrap_or(true)
-            })
-            .collect::<Result<_, _>>()?;
-
-        for addr in to_remove {
-            self.admins.remove(deps.storage, &addr);
-        }
-
-        for admin in admins {
-            let admin = deps.api.addr_validate(&admin)?;
-            self.admins.save(deps.storage, &admin, &Empty {})?;
-        }
-
-        let resp = Response::new().add_attribute("action", "update_admins");
-        Ok(resp)
-    }
-
-    #[msg(query)]
-    pub fn admin_list(&self, ctx: (Deps, Env)) -> StdResult<AdminListResponse> {
-        let (deps, _) = ctx;
-
-        let admins: Result<_, _> = self
-            .admins
-            .keys(deps.storage, None, None, Order::Ascending)
-            .map(|addr| addr.map(String::from))
-            .collect();
-
-        Ok(AdminListResponse {
-            admins: admins?,
-            mutable: self.mutable.load(deps.storage)?,
-        })
-    }
-
     pub fn is_admin(&self, deps: Deps, addr: &Addr) -> bool {
         self.admins.has(deps.storage, addr)
     }
@@ -144,6 +53,8 @@ impl Cw1WhitelistContract<'_> {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::responses::AdminListResponse;
+    use crate::whitelist::{self, Whitelist};
     use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
     use cosmwasm_std::{coin, coins, to_binary, BankMsg, CosmosMsg, StakingMsg, SubMsg, WasmMsg};
     use cw1::Cw1;
@@ -253,7 +164,7 @@ mod tests {
             )
             .unwrap();
 
-        let freeze = ExecMsg::Freeze {};
+        let freeze = whitelist::ExecMsg::Freeze {};
         let msgs = vec![
             BankMsg::Send {
                 to_address: bob.to_string(),
@@ -352,20 +263,19 @@ mod tests {
     }
 
     mod msgs {
+        use super::*;
+
         use cosmwasm_std::{from_binary, from_slice, to_binary, BankMsg};
 
-        use crate::contract::{ContractExecMsg, ContractQueryMsg, ExecMsg, QueryMsg};
+        use crate::contract::{ContractExecMsg, ContractQueryMsg};
 
         #[test]
         fn freeze() {
-            let original = ExecMsg::Freeze {};
+            let original = whitelist::ExecMsg::Freeze {};
             let serialized = to_binary(&original).unwrap();
             let deserialized = from_binary(&serialized).unwrap();
 
-            assert_eq!(
-                ContractExecMsg::Cw1WhitelistContract(original),
-                deserialized
-            );
+            assert_eq!(ContractExecMsg::Whitelist(original), deserialized);
 
             let json = br#"{
                 "freeze": {}
@@ -373,23 +283,20 @@ mod tests {
             let deserialized = from_slice(json).unwrap();
 
             assert_eq!(
-                ContractExecMsg::Cw1WhitelistContract(ExecMsg::Freeze {}),
+                ContractExecMsg::Whitelist(whitelist::ExecMsg::Freeze {}),
                 deserialized
             );
         }
 
         #[test]
         fn update_admins() {
-            let original = ExecMsg::UpdateAdmins {
+            let original = whitelist::ExecMsg::UpdateAdmins {
                 admins: vec!["admin1".to_owned(), "admin2".to_owned()],
             };
             let serialized = to_binary(&original).unwrap();
             let deserialized = from_binary(&serialized).unwrap();
 
-            assert_eq!(
-                ContractExecMsg::Cw1WhitelistContract(original),
-                deserialized
-            );
+            assert_eq!(ContractExecMsg::Whitelist(original), deserialized);
 
             let json = br#"{
                 "update_admins": {
@@ -399,7 +306,7 @@ mod tests {
             let deserialized = from_slice(json).unwrap();
 
             assert_eq!(
-                ContractExecMsg::Cw1WhitelistContract(ExecMsg::UpdateAdmins {
+                ContractExecMsg::Whitelist(whitelist::ExecMsg::UpdateAdmins {
                     admins: vec!["admin1".to_owned(), "admin3".to_owned()]
                 }),
                 deserialized
@@ -408,14 +315,11 @@ mod tests {
 
         #[test]
         fn admin_list() {
-            let original = QueryMsg::AdminList {};
+            let original = whitelist::QueryMsg::AdminList {};
             let serialized = to_binary(&original).unwrap();
             let deserialized = from_binary(&serialized).unwrap();
 
-            assert_eq!(
-                ContractQueryMsg::Cw1WhitelistContract(original),
-                deserialized
-            );
+            assert_eq!(ContractQueryMsg::Whitelist(original), deserialized);
 
             let json = br#"{
                 "admin_list": {}
@@ -423,7 +327,7 @@ mod tests {
             let deserialized = from_slice(json).unwrap();
 
             assert_eq!(
-                ContractQueryMsg::Cw1WhitelistContract(QueryMsg::AdminList {}),
+                ContractQueryMsg::Whitelist(whitelist::QueryMsg::AdminList {}),
                 deserialized
             );
         }
diff --git a/contracts/cw1-whitelist/src/cw1.rs b/contracts/cw1-whitelist/src/cw1.rs
index 2b25fee..2080c99 100644
--- a/contracts/cw1-whitelist/src/cw1.rs
+++ b/contracts/cw1-whitelist/src/cw1.rs
@@ -1,4 +1,4 @@
-use cosmwasm_std::{Addr, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response};
+use cosmwasm_std::{Addr, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
 use cw1::{CanExecuteResp, Cw1};
 
 use crate::contract::Cw1WhitelistContract;
@@ -29,7 +29,7 @@ impl Cw1 for Cw1WhitelistContract<'_> {
         ctx: (Deps, Env),
         sender: String,
         _msg: CosmosMsg,
-    ) -> Result<cw1::CanExecuteResp, Self::Error> {
+    ) -> StdResult<cw1::CanExecuteResp> {
         let (deps, _) = ctx;
 
         let resp = CanExecuteResp {
diff --git a/contracts/cw1-whitelist/src/lib.rs b/contracts/cw1-whitelist/src/lib.rs
index 6d26f03..c063402 100644
--- a/contracts/cw1-whitelist/src/lib.rs
+++ b/contracts/cw1-whitelist/src/lib.rs
@@ -4,9 +4,11 @@ pub mod error;
 #[cfg(any(test, feature = "tests"))]
 pub mod multitest;
 pub mod responses;
+pub mod whitelist;
 
 #[cfg(not(feature = "library"))]
 mod entry_points {
+    use contract::{ContractExecMsg, ContractQueryMsg, InstantiateMsg};
     use cosmwasm_std::{
         entry_point, from_slice, Binary, Deps, DepsMut, Env, MessageInfo, Response,
     };
@@ -23,9 +25,9 @@ mod entry_points {
         deps: DepsMut,
         env: Env,
         info: MessageInfo,
-        msg: Binary,
+        msg: InstantiateMsg,
     ) -> Result<Response, ContractError> {
-        from_slice::<InstantiateMsg>(&msg)?.dispatch(&CONTRACT, (deps, env, info))
+        msg.dispatch(&CONTRACT, (deps, env, info))
     }
 
     #[entry_point]
@@ -33,14 +35,14 @@ mod entry_points {
         deps: DepsMut,
         env: Env,
         info: MessageInfo,
-        msg: Binary,
+        msg: ContractExecMsg,
     ) -> Result<Response, ContractError> {
-        from_slice::<ContractExecMsg>(&msg)?.dispatch(&CONTRACT, (deps, env, info))
+        msg.dispatch(&CONTRACT, (deps, env, info))
     }
 
     #[entry_point]
-    pub fn query(deps: Deps, env: Env, msg: Binary) -> Result<Binary, ContractError> {
-        from_slice::<ContractQueryMsg>(&msg)?.dispatch(&CONTRACT, (deps, env))
+    pub fn query(deps: Deps, env: Env, msg: ContractQueryMsg) -> Result<Binary, ContractError> {
+        msg.dispatch(&CONTRACT, (deps, env))
     }
 }
 
diff --git a/contracts/cw1-whitelist/src/whitelist.rs b/contracts/cw1-whitelist/src/whitelist.rs
new file mode 100644
index 0000000..f9a9e88
--- /dev/null
+++ b/contracts/cw1-whitelist/src/whitelist.rs
@@ -0,0 +1,117 @@
+use cosmwasm_std::{Deps, DepsMut, Empty, Env, MessageInfo, Order, Response, StdResult};
+use sylvia::interface;
+
+use crate::contract::Cw1WhitelistContract;
+use crate::error::ContractError;
+use crate::responses::AdminListResponse;
+
+#[interface]
+pub trait Whitelist {
+    type Error: From<cosmwasm_std::StdError>;
+
+    #[msg(exec)]
+    fn freeze(&self, ctx: (DepsMut, Env, MessageInfo)) -> Result<Response, Self::Error>;
+
+    #[msg(exec)]
+    fn update_admins(
+        &self,
+        ctx: (DepsMut, Env, MessageInfo),
+        admins: Vec<String>,
+    ) -> Result<Response, Self::Error>;
+
+    #[msg(query)]
+    fn admin_list(&self, ctx: (Deps, Env)) -> StdResult<AdminListResponse>;
+}
+
+impl Whitelist for Cw1WhitelistContract<'_> {
+    type Error = ContractError;
+
+    fn freeze(&self, ctx: (DepsMut, Env, MessageInfo)) -> Result<Response, ContractError> {
+        let (deps, _, info) = ctx;
+
+        if !self.is_admin(deps.as_ref(), &info.sender) {
+            return Err(ContractError::Unauthorized {});
+        }
+
+        self.mutable.save(deps.storage, &false)?;
+
+        let resp = Response::new().add_attribute("action", "freeze");
+        Ok(resp)
+    }
+
+    fn update_admins(
+        &self,
+        ctx: (DepsMut, Env, MessageInfo),
+        mut admins: Vec<String>,
+    ) -> Result<Response, ContractError> {
+        let (deps, _, info) = ctx;
+
+        if !self.is_admin(deps.as_ref(), &info.sender) {
+            return Err(ContractError::Unauthorized {});
+        }
+
+        if !self.mutable.load(deps.storage)? {
+            return Err(ContractError::ContractFrozen {});
+        }
+
+        admins.sort_unstable();
+        let mut low_idx = 0;
+
+        let to_remove: Vec<_> = self
+            .admins
+            .keys(deps.storage, None, None, Order::Ascending)
+            .filter(|addr| {
+                // This is a bit of optimization basing on the fact that both `admins` and queried
+                // keys range are sorted. Binary search would always return the index which is at
+                // most as big as searched item, so for next item there is no point in looking at
+                // lower indices. On the other hand - if we reached and of the sequence, we want to
+                // remove all following keys.
+                addr.as_ref()
+                    .map(|addr| {
+                        if low_idx >= admins.len() {
+                            return true;
+                        }
+
+                        match admins[low_idx..].binary_search(&addr.into()) {
+                            Ok(idx) => {
+                                low_idx = idx;
+                                false
+                            }
+                            Err(idx) => {
+                                low_idx = idx;
+                                true
+                            }
+                        }
+                    })
+                    .unwrap_or(true)
+            })
+            .collect::<Result<_, _>>()?;
+
+        for addr in to_remove {
+            self.admins.remove(deps.storage, &addr);
+        }
+
+        for admin in admins {
+            let admin = deps.api.addr_validate(&admin)?;
+            self.admins.save(deps.storage, &admin, &Empty {})?;
+        }
+
+        let resp = Response::new().add_attribute("action", "update_admins");
+        Ok(resp)
+    }
+
+    fn admin_list(&self, ctx: (Deps, Env)) -> StdResult<AdminListResponse> {
+        let (deps, _) = ctx;
+
+        let admins: Result<_, _> = self
+            .admins
+            .keys(deps.storage, None, None, Order::Ascending)
+            .map(|addr| addr.map(String::from))
+            .collect();
+
+        Ok(AdminListResponse {
+            admins: admins?,
+            mutable: self.mutable.load(deps.storage)?,
+        })
+    }
+}
diff --git a/contracts/cw1/src/lib.rs b/contracts/cw1/src/lib.rs
index 5f7c458..bbf230f 100644
--- a/contracts/cw1/src/lib.rs
+++ b/contracts/cw1/src/lib.rs
@@ -1,4 +1,4 @@
-use cosmwasm_std::{CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, StdError};
+use cosmwasm_std::{CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use sylvia::interface;
@@ -31,7 +31,7 @@ pub trait Cw1 {
         ctx: (Deps, Env),
         sender: String,
         msg: CosmosMsg,
-    ) -> Result<CanExecuteResp, Self::Error>;
+    ) -> StdResult<CanExecuteResp>;
 }
 
 #[cfg(test)]


Test patch:
diff --git a/contracts/cw1-subkeys/src/multitest.rs b/contracts/cw1-subkeys/src/multitest.rs
new file mode 100644
index 0000000..07640e6
--- /dev/null
+++ b/contracts/cw1-subkeys/src/multitest.rs
@@ -0,0 +1,73 @@
+use anyhow::{bail, Result as AnyResult};
+use cosmwasm_std::{from_slice, Empty};
+use cw_multi_test::Contract;
+
+use crate::contract::{ContractExecMsg, ContractQueryMsg, Cw1SubkeysContract, InstantiateMsg};
+
+pub mod proxy;
+#[cfg(test)]
+mod tests;
+
+impl Contract<Empty> for Cw1SubkeysContract<'_> {
+    fn execute(
+        &self,
+        deps: cosmwasm_std::DepsMut<Empty>,
+        env: cosmwasm_std::Env,
+        info: cosmwasm_std::MessageInfo,
+        msg: Vec<u8>,
+    ) -> AnyResult<cosmwasm_std::Response<Empty>> {
+        from_slice::<ContractExecMsg>(&msg)?
+            .dispatch(self, (deps, env, info))
+            .map_err(Into::into)
+    }
+
+    fn instantiate(
+        &self,
+        deps: cosmwasm_std::DepsMut<Empty>,
+        env: cosmwasm_std::Env,
+        info: cosmwasm_std::MessageInfo,
+        msg: Vec<u8>,
+    ) -> AnyResult<cosmwasm_std::Response<Empty>> {
+        from_slice::<InstantiateMsg>(&msg)?
+            .dispatch(self, (deps, env, info))
+            .map_err(Into::into)
+    }
+
+    fn query(
+        &self,
+        deps: cosmwasm_std::Deps<Empty>,
+        env: cosmwasm_std::Env,
+        msg: Vec<u8>,
+    ) -> AnyResult<cosmwasm_std::Binary> {
+        from_slice::<ContractQueryMsg>(&msg)?
+            .dispatch(self, (deps, env))
+            .map_err(Into::into)
+    }
+
+    fn sudo(
+        &self,
+        _deps: cosmwasm_std::DepsMut<Empty>,
+        _env: cosmwasm_std::Env,
+        _msg: Vec<u8>,
+    ) -> AnyResult<cosmwasm_std::Response<Empty>> {
+        bail!("sudo not implemented for contract")
+    }
+
+    fn reply(
+        &self,
+        _deps: cosmwasm_std::DepsMut<Empty>,
+        _env: cosmwasm_std::Env,
+        _msg: cosmwasm_std::Reply,
+    ) -> AnyResult<cosmwasm_std::Response<Empty>> {
+        bail!("reply not implemented for contract")
+    }
+
+    fn migrate(
+        &self,
+        _deps: cosmwasm_std::DepsMut<Empty>,
+        _env: cosmwasm_std::Env,
+        _msg: Vec<u8>,
+    ) -> AnyResult<cosmwasm_std::Response<Empty>> {
+        bail!("migrate not implemented for contract")
+    }
+}
diff --git a/contracts/cw1-subkeys/src/multitest/proxy.rs b/contracts/cw1-subkeys/src/multitest/proxy.rs
new file mode 100644
index 0000000..c00dc68
--- /dev/null
+++ b/contracts/cw1-subkeys/src/multitest/proxy.rs
@@ -0,0 +1,125 @@
+use cosmwasm_std::{Addr, Coin, StdResult};
+use cw_multi_test::{App, Executor};
+use cw_utils::Expiration;
+
+use crate::contract::{Cw1SubkeysContract, ExecMsg, InstantiateMsg, QueryMsg};
+use crate::error::ContractError;
+use crate::responses::{AllAllowancesResponse, AllPermissionsResponse};
+use crate::state::{Allowance, Permissions};
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct Cw1SubkeysCodeId(u64);
+
+impl Cw1SubkeysCodeId {
+    pub fn store_code(app: &mut App) -> Self {
+        let code_id = app.store_code(Box::new(Cw1SubkeysContract::new()));
+        Self(code_id)
+    }
+
+    #[track_caller]
+    pub fn instantiate(
+        self,
+        app: &mut App,
+        sender: &Addr,
+        admins: &[&Addr],
+        mutable: bool,
+        label: &str,
+    ) -> Result<Cw1SubkeysProxy, ContractError> {
+        let admins = admins.iter().map(|a| a.to_string()).collect();
+        let msg = InstantiateMsg { admins, mutable };
+
+        app.instantiate_contract(self.0, sender.clone(), &msg, &[], label, None)
+            .map_err(|err| err.downcast().unwrap())
+            .map(Cw1SubkeysProxy)
+    }
+}
+
+pub struct Cw1SubkeysProxy(Addr);
+
+impl Cw1SubkeysProxy {
+    pub fn addr(&self) -> &Addr {
+        &self.0
+    }
+
+    #[track_caller]
+    pub fn increase_allowance(
+        &self,
+        app: &mut App,
+        sender: &Addr,
+        spender: &Addr,
+        amount: Coin,
+        expires: impl Into<Option<Expiration>>,
+    ) -> Result<(), ContractError> {
+        let msg = ExecMsg::IncreaseAllowance {
+            spender: spender.to_string(),
+            amount,
+            expires: expires.into(),
+        };
+
+        app.execute_contract(sender.clone(), self.0.clone(), &msg, &[])
+            .map_err(|err| err.downcast().unwrap())
+            .map(|_| ())
+    }
+
+    #[track_caller]
+    pub fn set_permission(
+        &self,
+        app: &mut App,
+        sender: &Addr,
+        spender: &Addr,
+        permissions: Permissions,
+    ) -> Result<(), ContractError> {
+        let msg = ExecMsg::SetPermissions {
+            spender: spender.to_string(),
+            permissions,
+        };
+
+        app.execute_contract(sender.clone(), self.0.clone(), &msg, &[])
+            .map_err(|err| err.downcast().unwrap())
+            .map(|_| ())
+    }
+
+    pub fn allowance(&self, app: &App, spender: &Addr) -> StdResult<Allowance> {
+        let msg = QueryMsg::Allowance {
+            spender: spender.to_string(),
+        };
+
+        app.wrap().query_wasm_smart(self.0.clone(), &msg)
+    }
+
+    pub fn all_allowances<'a>(
+        &self,
+        app: &App,
+        start_after: impl Into<Option<&'a Addr>>,
+        limit: impl Into<Option<u32>>,
+    ) -> StdResult<AllAllowancesResponse> {
+        let msg = QueryMsg::AllAllowances {
+            start_after: start_after.into().map(|a| a.to_string()),
+            limit: limit.into(),
+        };
+
+        app.wrap().query_wasm_smart(self.0.clone(), &msg)
+    }
+
+    pub fn permissions(&self, app: &App, spender: &Addr) -> StdResult<Permissions> {
+        let msg = QueryMsg::Permissions {
+            spender: spender.to_string(),
+        };
+
+        app.wrap().query_wasm_smart(self.0.clone(), &msg)
+    }
+
+    pub fn all_permissions<'a>(
+        &self,
+        app: &App,
+        start_after: impl Into<Option<&'a Addr>>,
+        limit: impl Into<Option<u32>>,
+    ) -> StdResult<AllPermissionsResponse> {
+        let msg = QueryMsg::AllPermissions {
+            start_after: start_after.into().map(|a| a.to_string()),
+            limit: limit.into(),
+        };
+
+        app.wrap().query_wasm_smart(self.0.clone(), &msg)
+    }
+}
diff --git a/contracts/cw1-subkeys/src/multitest/tests.rs b/contracts/cw1-subkeys/src/multitest/tests.rs
new file mode 100644
index 0000000..c706301
--- /dev/null
+++ b/contracts/cw1-subkeys/src/multitest/tests.rs
@@ -0,0 +1,342 @@
+use cosmwasm_std::{coin, coins, Addr};
+use cw2::{query_contract_info, ContractVersion};
+use cw_multi_test::{next_block, App};
+use cw_utils::{Expiration, NativeBalance};
+
+use crate::contract::{CONTRACT_NAME, CONTRACT_VERSION};
+
+use super::proxy::Cw1SubkeysCodeId;
+
+const ATOM: &str = "atom";
+
+/// Helper function for comparing vectors or another slice-like object as they would represent
+/// set with duplications. Compares sets by first sorting elements using provided ordering.
+/// This functions reshufless elements inplace, as it should never matter as compared
+/// containers should represent same value regardless of ordering, and making this inplace just
+/// safes obsolete copying.
+///
+/// This is implemented as a macro instead of function to throw panic in the place of macro
+/// usage instead of from function called inside test.
+macro_rules! assert_sorted_eq {
+    ($left:expr, $right:expr, $cmp:expr $(,)?) => {
+        let mut left = $left;
+        left.sort_by(&$cmp);
+
+        let mut right = $right;
+        right.sort_by($cmp);
+
+        assert_eq!(left, right);
+    };
+}
+
+#[test]
+fn get_contract_version_works() {
+    let mut app = App::default();
+
+    let owner = Addr::unchecked("owner");
+
+    let code_id = Cw1SubkeysCodeId::store_code(&mut app);
+
+    let contract = code_id
+        .instantiate(&mut app, &owner, &[], true, "Sublist contract")
+        .unwrap();
+
+    let version: ContractVersion = query_contract_info(&app, contract.addr().clone()).unwrap();
+
+    assert_eq!(
+        ContractVersion {
+            contract: CONTRACT_NAME.to_string(),
+            version: CONTRACT_VERSION.to_string(),
+        },
+        version
+    );
+}
+
+mod allowance {
+    use crate::responses::AllowanceInfo;
+    use crate::state::Allowance;
+
+    use super::*;
+
+    #[test]
+    fn query() {
+        let mut app = App::default();
+
+        let owner = Addr::unchecked("owner");
+        let spender1 = Addr::unchecked("spender1");
+        let spender2 = Addr::unchecked("spender2");
+        let spender3 = Addr::unchecked("spender3");
+
+        let code_id = Cw1SubkeysCodeId::store_code(&mut app);
+
+        let contract = code_id
+            .instantiate(&mut app, &owner, &[&owner], false, "Sublist contract")
+            .unwrap();
+
+        contract
+            .increase_allowance(&mut app, &owner, &spender1, coin(1, ATOM), None)
+            .unwrap();
+
+        contract
+            .increase_allowance(&mut app, &owner, &spender2, coin(2, ATOM), None)
+            .unwrap();
+
+        assert_eq!(
+            Allowance {
+                balance: NativeBalance(coins(1, ATOM)),
+                expires: Expiration::Never {},
+            },
+            contract.allowance(&app, &spender1).unwrap()
+        );
+
+        assert_eq!(
+            Allowance {
+                balance: NativeBalance(coins(2, ATOM)),
+                expires: Expiration::Never {},
+            },
+            contract.allowance(&app, &spender2).unwrap()
+        );
+
+        assert_eq!(
+            Allowance::default(),
+            contract.allowance(&app, &spender3).unwrap()
+        );
+    }
+
+    #[test]
+    fn query_expired() {
+        let mut app = App::default();
+
+        let owner = Addr::unchecked("owner");
+        let spender = Addr::unchecked("spender");
+
+        let code_id = Cw1SubkeysCodeId::store_code(&mut app);
+
+        let contract = code_id
+            .instantiate(&mut app, &owner, &[&owner], false, "Sublist contract")
+            .unwrap();
+
+        let height = app.block_info().height;
+        contract
+            .increase_allowance(
+                &mut app,
+                &owner,
+                &spender,
+                coin(1, ATOM),
+                Expiration::AtHeight(height + 1),
+            )
+            .unwrap();
+
+        app.update_block(next_block);
+
+        // Check allowances work for accounts with balances
+        assert_eq!(
+            Allowance {
+                balance: NativeBalance(vec![]),
+                expires: Expiration::Never {},
+            },
+            contract.allowance(&app, &spender).unwrap()
+        );
+    }
+
+    #[test]
+    fn query_all() {
+        let mut app = App::default();
+
+        let owner = Addr::unchecked("owner");
+        let spender1 = Addr::unchecked("spender1");
+        let spender2 = Addr::unchecked("spender2");
+        let spender3 = Addr::unchecked("spender3");
+        let spender4 = Addr::unchecked("spender4");
+
+        let code_id = Cw1SubkeysCodeId::store_code(&mut app);
+
+        let contract = code_id
+            .instantiate(&mut app, &owner, &[&owner], false, "Sublist contract")
+            .unwrap();
+
+        let height = app.block_info().height;
+        contract
+            .increase_allowance(&mut app, &owner, &spender1, coin(1234, ATOM), None)
+            .unwrap();
+
+        contract
+            .increase_allowance(
+                &mut app,
+                &owner,
+                &spender2,
+                coin(2345, ATOM),
+                Expiration::Never {},
+            )
+            .unwrap();
+
+        contract
+            .increase_allowance(
+                &mut app,
+                &owner,
+                &spender3,
+                coin(3456, ATOM),
+                Expiration::AtHeight(height + 2),
+            )
+            .unwrap();
+
+        contract
+            .increase_allowance(
+                &mut app,
+                &owner,
+                &spender4,
+                coin(2222, ATOM),
+                Expiration::AtHeight(height + 1),
+            )
+            .unwrap();
+
+        app.update_block(next_block);
+
+        let batch1 = contract.all_allowances(&app, None, 2).unwrap().allowances;
+        assert_eq!(2, batch1.len());
+
+        let batch2 = contract
+            .all_allowances(&app, &batch1.last().unwrap().spender, 2)
+            .unwrap()
+            .allowances;
+        assert_eq!(1, batch2.len());
+
+        let height = app.block_info().height;
+        let expected = vec![
+            AllowanceInfo {
+                spender: spender1,
+                balance: NativeBalance(coins(1234, ATOM)),
+                expires: Expiration::Never {}, // Not set, expected default
+            },
+            AllowanceInfo {
+                spender: spender2,
+                balance: NativeBalance(coins(2345, ATOM)),
+                expires: Expiration::Never {},
+            },
+            AllowanceInfo {
+                spender: spender3,
+                balance: NativeBalance(coins(3456, ATOM)),
+                expires: Expiration::AtHeight(height + 1),
+            },
+        ];
+
+        // Check allowances work for accounts with balances
+        assert_sorted_eq!(
+            expected,
+            [batch1, batch2].concat(),
+            AllowanceInfo::cmp_by_spender
+        );
+    }
+}
+
+mod permissions {
+    use crate::responses::PermissionsInfo;
+    use crate::state::Permissions;
+
+    use super::*;
+
+    const ALL_PERMS: Permissions = Permissions {
+        delegate: true,
+        redelegate: true,
+        undelegate: true,
+        withdraw: true,
+    };
+
+    const NO_PERMS: Permissions = Permissions {
+        delegate: false,
+        redelegate: false,
+        undelegate: false,
+        withdraw: false,
+    };
+
+    #[test]
+    fn query() {
+        let mut app = App::default();
+
+        let owner = Addr::unchecked("owner");
+        let spender1 = Addr::unchecked("spender1");
+        let spender2 = Addr::unchecked("spender2");
+        let spender3 = Addr::unchecked("spender2");
+
+        let code_id = Cw1SubkeysCodeId::store_code(&mut app);
+
+        let contract = code_id
+            .instantiate(&mut app, &owner, &[&owner], false, "Subkeys contract")
+            .unwrap();
+
+        contract
+            .set_permission(&mut app, &owner, &spender1, ALL_PERMS)
+            .unwrap();
+
+        contract
+            .set_permission(&mut app, &owner, &spender2, NO_PERMS)
+            .unwrap();
+
+        assert_eq!(ALL_PERMS, contract.permissions(&app, &spender1).unwrap());
+        assert_eq!(NO_PERMS, contract.permissions(&app, &spender2).unwrap());
+        assert_eq!(NO_PERMS, contract.permissions(&app, &spender3).unwrap());
+    }
+
+    #[test]
+    fn query_all() {
+        let mut app = App::default();
+
+        let owner = Addr::unchecked("owner");
+        let spender1 = Addr::unchecked("spender1");
+        let spender2 = Addr::unchecked("spender2");
+        let spender3 = Addr::unchecked("spender3");
+
+        let code_id = Cw1SubkeysCodeId::store_code(&mut app);
+
+        let contract = code_id
+            .instantiate(&mut app, &owner, &[&owner], false, "Subkeys contract")
+            .unwrap();
+
+        contract
+            .set_permission(&mut app, &owner, &spender1, ALL_PERMS)
+            .unwrap();
+
+        contract
+            .set_permission(&mut app, &owner, &spender2, NO_PERMS)
+            .unwrap();
+
+        contract
+            .set_permission(&mut app, &owner, &spender3, NO_PERMS)
+            .unwrap();
+
+        assert_eq!(ALL_PERMS, contract.permissions(&app, &spender1).unwrap());
+        assert_eq!(NO_PERMS, contract.permissions(&app, &spender2).unwrap());
+        assert_eq!(NO_PERMS, contract.permissions(&app, &spender3).unwrap());
+
+        let batch1 = contract.all_permissions(&app, None, 2).unwrap().permissions;
+        assert_eq!(2, batch1.len());
+
+        let batch2 = contract
+            .all_permissions(&app, &batch1.last().unwrap().spender, 2)
+            .unwrap()
+            .permissions;
+        assert_eq!(1, batch2.len());
+
+        let expected = vec![
+            PermissionsInfo {
+                spender: spender1,
+                permissions: ALL_PERMS,
+            },
+            PermissionsInfo {
+                spender: spender2,
+                permissions: NO_PERMS,
+            },
+            PermissionsInfo {
+                spender: spender3,
+                permissions: NO_PERMS,
+            },
+        ];
+
+        // Check allowances work for accounts with balances
+        assert_sorted_eq!(
+            expected,
+            [batch1, batch2].concat(),
+            PermissionsInfo::cmp_by_spender
+        );
+    }
+}
diff --git a/contracts/cw1-whitelist/src/multitest.rs b/contracts/cw1-whitelist/src/multitest.rs
index f09db5f..4c573c9 100644
--- a/contracts/cw1-whitelist/src/multitest.rs
+++ b/contracts/cw1-whitelist/src/multitest.rs
@@ -53,8 +53,8 @@ mod test {
     use cosmwasm_std::{to_binary, Addr, WasmMsg};
     use cw_multi_test::{App, Executor};
 
-    use crate::contract::{ExecMsg, QueryMsg};
     use crate::responses::AdminListResponse;
+    use crate::whitelist;
     use assert_matches::assert_matches;
 
     use super::*;
@@ -96,7 +96,7 @@ mod test {
             .unwrap();
         assert_ne!(second_contract, first_contract);
 
-        let freeze = ExecMsg::Freeze {};
+        let freeze = whitelist::ExecMsg::Freeze {};
         let freeze = WasmMsg::Execute {
             contract_addr: second_contract.to_string(),
             msg: to_binary(&freeze).unwrap(),
@@ -114,7 +114,7 @@ mod test {
 
         let resp = app
             .wrap()
-            .query_wasm_smart(second_contract, &QueryMsg::AdminList {})
+            .query_wasm_smart(second_contract, &whitelist::QueryMsg::AdminList {})
             .unwrap();
 
         assert_matches!(