Repository: CosmWasm/sylvia
Pull request #69 (2022-11-02 21:47:32)

Cw20 contract
Cw20 allowance and base implementation with coverage of the first one

codecov[bot]: # [Codecov](https://codecov.io/gh/CosmWasm/sylvia/pull/69?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm) Report
> Merging [#69](https://codecov.io/gh/CosmWasm/sylvia/pull/69?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm) (ba7d495) into [main](https://codecov.io/gh/CosmWasm/sylvia/commit/27fe3cc626a87a52fcbee0fdf622bc4144854b6e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm) (27fe3cc) will **increase** coverage by `0.22%`.
> The diff coverage is `68.59%`.

```diff
@@            Coverage Diff             @@
##             main      #69      +/-   ##
==========================================
+ Coverage   67.79%   68.02%   +0.22%     
==========================================
  Files          21       27       +6     
  Lines        1003     1401     +398     
==========================================
+ Hits          680      953     +273     
- Misses        323      448     +125     
```


| [Impacted Files](https://codecov.io/gh/CosmWasm/sylvia/pull/69?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm) | Coverage Δ | |
|---|---|---|
| [contracts/cw20-base/src/contract.rs](https://codecov.io/gh/CosmWasm/sylvia/pull/69/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm#diff-Y29udHJhY3RzL2N3MjAtYmFzZS9zcmMvY29udHJhY3QucnM=) | `29.62% <29.62%> (ø)` | |
| [contracts/cw20-base/src/validation.rs](https://codecov.io/gh/CosmWasm/sylvia/pull/69/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm#diff-Y29udHJhY3RzL2N3MjAtYmFzZS9zcmMvdmFsaWRhdGlvbi5ycw==) | `41.17% <41.17%> (ø)` | |
| [contracts/cw20-base/src/multitest.rs](https://codecov.io/gh/CosmWasm/sylvia/pull/69/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm#diff-Y29udHJhY3RzL2N3MjAtYmFzZS9zcmMvbXVsdGl0ZXN0LnJz) | `60.00% <60.00%> (ø)` | |
| [...racts/cw20-base/src/multitest/receiver\_contract.rs](https://codecov.io/gh/CosmWasm/sylvia/pull/69/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm#diff-Y29udHJhY3RzL2N3MjAtYmFzZS9zcmMvbXVsdGl0ZXN0L3JlY2VpdmVyX2NvbnRyYWN0LnJz) | `65.38% <65.38%> (ø)` | |
| [contracts/cw20-base/src/multitest/proxy.rs](https://codecov.io/gh/CosmWasm/sylvia/pull/69/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm#diff-Y29udHJhY3RzL2N3MjAtYmFzZS9zcmMvbXVsdGl0ZXN0L3Byb3h5LnJz) | `97.36% <97.36%> (ø)` | |
| [contracts/cw20-base/src/allowances.rs](https://codecov.io/gh/CosmWasm/sylvia/pull/69/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm#diff-Y29udHJhY3RzL2N3MjAtYmFzZS9zcmMvYWxsb3dhbmNlcy5ycw==) | `98.12% <98.12%> (ø)` | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm)


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index f7763dd..5884f58 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -322,6 +322,27 @@ dependencies = [
  "sylvia",
 ]
 
+[[package]]
+name = "cw20-base"
+version = "0.2.1"
+dependencies = [
+ "anyhow",
+ "assert_matches",
+ "cosmwasm-schema",
+ "cosmwasm-std",
+ "cw-multi-test",
+ "cw-storage-plus",
+ "cw-utils",
+ "cw2",
+ "cw20-allowances",
+ "cw20-marketing",
+ "cw20-minting",
+ "schemars",
+ "serde",
+ "sylvia",
+ "thiserror",
+]
+
 [[package]]
 name = "cw20-marketing"
 version = "0.2.1"
diff --git a/Cargo.toml b/Cargo.toml
index 690f4d8..d65c26b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,6 +7,7 @@ members = [
     "contracts/cw1",
     "contracts/cw4",
     "contracts/cw20-allowances",
+    "contracts/cw20-base",
     "contracts/cw20-minting",
     "contracts/cw20-marketing",
 
diff --git a/contracts/cw20-allowances/src/lib.rs b/contracts/cw20-allowances/src/lib.rs
index 1a177bc..3f01891 100644
--- a/contracts/cw20-allowances/src/lib.rs
+++ b/contracts/cw20-allowances/src/lib.rs
@@ -1,12 +1,14 @@
-mod responses;
+pub mod responses;
 
-use cosmwasm_std::{Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Uint128};
+use cosmwasm_std::{
+    Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Uint128,
+};
 use cw_utils::Expiration;
 use responses::{AllAllowancesResponse, AllSpenderAllowancesResponse, AllowanceResponse};
 use sylvia::{interface, schemars};
 
 #[interface]
-pub trait Cw20Approval {
+pub trait Cw20Allowances {
     type Error: From<StdError>;
 
     /// Allows spender to access an additional amount tokens from the owner's (env.sender) account.
@@ -51,6 +53,7 @@ pub trait Cw20Approval {
         owner: String,
         contract: String,
         amount: Uint128,
+        msg: Binary,
     ) -> Result<Response, Self::Error>;
 
     /// Destroys amount of tokens forever
diff --git a/contracts/cw20-allowances/src/responses.rs b/contracts/cw20-allowances/src/responses.rs
index 21ccec7..9258879 100644
--- a/contracts/cw20-allowances/src/responses.rs
+++ b/contracts/cw20-allowances/src/responses.rs
@@ -4,9 +4,8 @@ use cw_utils::Expiration;
 use serde::{Deserialize, Serialize};
 use sylvia::schemars;
 
-#[cw_serde]
-pub struct SpenderAllowanceInfo {
-    pub owner: String,
+#[derive(Serialize, Deserialize, Clone, PartialEq, schemars::JsonSchema, Debug, Default)]
+pub struct AllowanceResponse {
     pub allowance: Uint128,
     pub expires: Expiration,
 }
@@ -18,17 +17,18 @@ pub struct AllowanceInfo {
     pub expires: Expiration,
 }
 
-#[derive(Serialize, Deserialize, Clone, PartialEq, schemars::JsonSchema, Debug, Default)]
-pub struct AllowanceResponse {
-    pub allowance: Uint128,
-    pub expires: Expiration,
-}
-
 #[derive(Serialize, Deserialize, Clone, PartialEq, schemars::JsonSchema, Debug, Default)]
 pub struct AllAllowancesResponse {
     pub allowances: Vec<AllowanceInfo>,
 }
 
+#[cw_serde]
+pub struct SpenderAllowanceInfo {
+    pub owner: String,
+    pub allowance: Uint128,
+    pub expires: Expiration,
+}
+
 #[derive(Serialize, Deserialize, Clone, PartialEq, schemars::JsonSchema, Debug, Default)]
 pub struct AllSpenderAllowancesResponse {
     pub allowances: Vec<SpenderAllowanceInfo>,
diff --git a/contracts/cw20-base/Cargo.toml b/contracts/cw20-base/Cargo.toml
new file mode 100644
index 0000000..9b922e8
--- /dev/null
+++ b/contracts/cw20-base/Cargo.toml
@@ -0,0 +1,38 @@
+[package]
+name = "cw20-base"
+version = "0.2.1"
+authors = ["Jan Woźniak <jan@confio.gmbh>"]
+edition = "2021"
+description = "Coverage of cw20-base message generation."
+license = "Apache-2.0"
+repository = "https://github.com/CosmWasm/sylvia"
+homepage = "https://cosmwasm.com"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[features]
+library = []
+tests = ["library", "cw-multi-test", "anyhow"]
+
+[dependencies]
+cosmwasm-std = { version = "1.1", features = ["staking"] }
+cosmwasm-schema = "1.1"
+schemars = "0.8"
+serde = { version = "1", default-features = false, features = ["derive"] }
+sylvia = { path = "../../sylvia" }
+cw-storage-plus = "0.16"
+thiserror = { version = "1.0" }
+cw2 = "0.16"
+cw-multi-test = { version = "0.16", optional = true }
+anyhow = { version = "1", optional = true }
+cw20-allowances = { path = "../cw20-allowances" }
+cw20-marketing = { path = "../cw20-marketing" }
+cw20-minting = { path = "../cw20-minting" }
+cw-utils = "0.16"
+
+[dev-dependencies]
+anyhow = "1"
+cw-multi-test = "0.16"
+assert_matches = "1.5.0"
+cw-utils = "0.16"
\ No newline at end of file
diff --git a/contracts/cw20-base/src/allowances.rs b/contracts/cw20-base/src/allowances.rs
new file mode 100644
index 0000000..32e5087
--- /dev/null
+++ b/contracts/cw20-base/src/allowances.rs
@@ -0,0 +1,331 @@
+use cosmwasm_std::{
+    Addr, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, StdError, StdResult, Uint128,
+};
+use cw20_allowances::responses::{
+    AllAllowancesResponse, AllSpenderAllowancesResponse, AllowanceInfo, AllowanceResponse,
+    SpenderAllowanceInfo,
+};
+use cw20_allowances::Cw20Allowances;
+use cw_storage_plus::{Bound, Bounder};
+use cw_utils::Expiration;
+
+use crate::contract::Cw20Base;
+use crate::error::ContractError;
+use crate::responses::Cw20ReceiveMsg;
+
+// settings for pagination
+const MAX_LIMIT: u32 = 30;
+const DEFAULT_LIMIT: u32 = 10;
+
+impl Cw20Allowances for Cw20Base<'_> {
+    type Error = ContractError;
+
+    /// Allows spender to access an additional amount tokens from the owner's (env.sender) account.
+    /// If expires is Some(), overwrites current allowance expiration with this one.
+    fn increase_allowance(
+        &self,
+        ctx: (DepsMut, Env, MessageInfo),
+        spender: String,
+        amount: Uint128,
+        expires: Option<Expiration>,
+    ) -> Result<Response, Self::Error> {
+        let (deps, env, info) = ctx;
+
+        let spender_addr = deps.api.addr_validate(&spender)?;
+        if spender_addr == info.sender {
+            return Err(ContractError::CannotSetOwnAccount {});
+        }
+
+        let update_fn = |allow: Option<AllowanceResponse>| -> Result<_, _> {
+            let allow = allow.unwrap_or_default();
+            let allowance = allow.allowance + amount;
+            match expires {
+                Some(expires) if !expires.is_expired(&env.block) => {
+                    Ok(AllowanceResponse { allowance, expires })
+                }
+                None => Ok(AllowanceResponse { allowance, ..allow }),
+                _ => Err(ContractError::InvalidExpiration {}),
+            }
+        };
+        self.allowances
+            .update(deps.storage, (&info.sender, &spender_addr), update_fn)?;
+        self.allowances_spender
+            .update(deps.storage, (&spender_addr, &info.sender), update_fn)?;
+
+        let res = Response::new()
+            .add_attribute("action", "increase_allowance")
+            .add_attribute("owner", info.sender)
+            .add_attribute("spender", spender)
+            .add_attribute("amount", amount);
+        Ok(res)
+    }
+
+    /// Lowers the spender's access of tokens from the owner's (env.sender) account by amount.
+    /// If expires is Some(), overwrites current allowance expiration with this one.
+    fn decrease_allowance(
+        &self,
+        ctx: (DepsMut, Env, MessageInfo),
+        spender: String,
+        amount: Uint128,
+        expires: Option<Expiration>,
+    ) -> Result<Response, Self::Error> {
+        let (deps, env, info) = ctx;
+
+        let spender_addr = Addr::unchecked(&spender);
+        if spender_addr == info.sender {
+            return Err(ContractError::CannotSetOwnAccount {});
+        }
+
+        let key = (&info.sender, &spender_addr);
+
+        let reverse = |(a, b)| (b, a);
+
+        // load value and delete if it hits 0, or update otherwise
+        let mut allowance = self.allowances.load(deps.storage, key)?;
+        if amount < allowance.allowance {
+            // update the new amount
+            allowance.allowance = allowance
+                .allowance
+                .checked_sub(amount)
+                .map_err(StdError::overflow)?;
+            if let Some(exp) = expires {
+                if exp.is_expired(&env.block) {
+                    return Err(ContractError::InvalidExpiration {});
+                }
+                allowance.expires = exp;
+            }
+            self.allowances.save(deps.storage, key, &allowance)?;
+            self.allowances_spender
+                .save(deps.storage, reverse(key), &allowance)?;
+        } else {
+            self.allowances.remove(deps.storage, key);
+            self.allowances_spender.remove(deps.storage, reverse(key));
+        }
+
+        let res = Response::new()
+            .add_attribute("action", "decrease_allowance")
+            .add_attribute("owner", info.sender)
+            .add_attribute("spender", spender)
+            .add_attribute("amount", amount);
+        Ok(res)
+    }
+
+    /// Transfers amount tokens from owner -> recipient
+    /// if `env.sender` has sufficient pre-approval.
+    fn transfer_from(
+        &self,
+        ctx: (DepsMut, Env, MessageInfo),
+        owner: String,
+        recipient: String,
+        amount: Uint128,
+    ) -> Result<Response, Self::Error> {
+        let (deps, env, info) = ctx;
+
+        let rcpt_addr = deps.api.addr_validate(&recipient)?;
+        let owner_addr = deps.api.addr_validate(&owner)?;
+
+        // Avoid doing state update in case of self to self transfer
+        if rcpt_addr == owner_addr {
+            let resp = Response::new()
+                .add_attribute("action", "transfer_from")
+                .add_attribute("from", owner)
+                .add_attribute("to", recipient)
+                .add_attribute("by", info.sender)
+                .add_attribute("amount", amount);
+            return Ok(resp);
+        }
+
+        if info.sender != owner {
+            // deduct allowance before doing anything else have enough allowance
+            self.deduct_allowance(deps.storage, &owner_addr, &info.sender, &env.block, amount)?;
+        }
+
+        self.balances.update(
+            deps.storage,
+            &owner_addr,
+            |balance: Option<Uint128>| -> StdResult<_> {
+                Ok(balance.unwrap_or_default().checked_sub(amount)?)
+            },
+        )?;
+        self.balances.update(
+            deps.storage,
+            &rcpt_addr,
+            |balance: Option<Uint128>| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) },
+        )?;
+
+        let res = Response::new()
+            .add_attribute("action", "transfer_from")
+            .add_attribute("from", owner)
+            .add_attribute("to", recipient)
+            .add_attribute("by", info.sender)
+            .add_attribute("amount", amount);
+        Ok(res)
+    }
+
+    /// Sends amount tokens from owner -> contract
+    /// if `env.sender` has sufficient pre-approval.
+    fn send_from(
+        &self,
+        ctx: (DepsMut, Env, MessageInfo),
+        owner: String,
+        contract: String,
+        amount: Uint128,
+        msg: Binary,
+    ) -> Result<Response, Self::Error> {
+        let (deps, env, info) = ctx;
+
+        let rcpt_addr = deps.api.addr_validate(&contract)?;
+        let owner_addr = deps.api.addr_validate(&owner)?;
+
+        // deduct allowance before doing anything else have enough allowance
+        self.deduct_allowance(deps.storage, &owner_addr, &info.sender, &env.block, amount)?;
+
+        // move the tokens to the contract
+        self.balances.update(
+            deps.storage,
+            &owner_addr,
+            |balance: Option<Uint128>| -> StdResult<_> {
+                Ok(balance.unwrap_or_default().checked_sub(amount)?)
+            },
+        )?;
+        self.balances.update(
+            deps.storage,
+            &rcpt_addr,
+            |balance: Option<Uint128>| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) },
+        )?;
+
+        let resp = Response::new()
+            .add_attribute("action", "send_from")
+            .add_attribute("from", &owner)
+            .add_attribute("to", &contract)
+            .add_attribute("by", &info.sender)
+            .add_attribute("amount", amount);
+
+        // create a send message
+        let msg = Cw20ReceiveMsg {
+            sender: info.sender.into(),
+            amount,
+            msg,
+        }
+        .into_cosmos_msg(contract)?;
+
+        let resp = resp.add_message(msg);
+        Ok(resp)
+    }
+
+    /// Destroys amount of tokens forever
+    fn burn_from(
+        &self,
+        ctx: (DepsMut, Env, MessageInfo),
+        owner: String,
+        amount: Uint128,
+    ) -> Result<Response, Self::Error> {
+        let (deps, env, info) = ctx;
+
+        let owner_addr = deps.api.addr_validate(&owner)?;
+
+        // deduct allowance before doing anything else have enough allowance
+        self.deduct_allowance(deps.storage, &owner_addr, &info.sender, &env.block, amount)?;
+
+        // lower balance
+        self.balances.update(
+            deps.storage,
+            &owner_addr,
+            |balance: Option<Uint128>| -> StdResult<_> {
+                Ok(balance.unwrap_or_default().checked_sub(amount)?)
+            },
+        )?;
+        // reduce total_supply
+        self.token_info
+            .update(deps.storage, |mut meta| -> StdResult<_> {
+                meta.total_supply = meta.total_supply.checked_sub(amount)?;
+                Ok(meta)
+            })?;
+
+        let res = Response::new()
+            .add_attribute("action", "burn_from")
+            .add_attribute("owner", owner)
+            .add_attribute("spender", info.sender)
+            .add_attribute("amount", amount);
+        Ok(res)
+    }
+
+    /// Returns how much spender can use from owner account, 0 if unset.
+    fn allowance(
+        &self,
+        ctx: (Deps, Env),
+        owner: String,
+        spender: String,
+    ) -> StdResult<AllowanceResponse> {
+        let (deps, _) = ctx;
+
+        let owner_addr = deps.api.addr_validate(&owner)?;
+        let spender_addr = deps.api.addr_validate(&spender)?;
+        let allowance = self
+            .allowances
+            .may_load(deps.storage, (&owner_addr, &spender_addr))?
+            .unwrap_or_default();
+        Ok(allowance)
+    }
+
+    /// Returns all allowances this owner has approved. Supports pagination.
+    fn all_allowances(
+        &self,
+        ctx: (Deps, Env),
+        owner: String,
+        start_after: Option<String>,
+        limit: Option<u32>,
+    ) -> StdResult<AllAllowancesResponse> {
+        let (deps, _) = ctx;
+
+        let owner_addr = deps.api.addr_validate(&owner)?;
+        let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
+        let start = start_after.map(|s| Bound::ExclusiveRaw(s.into_bytes()));
+
+        let allowances = self
+            .allowances
+            .prefix(&owner_addr)
+            .range(deps.storage, start, None, Order::Ascending)
+            .take(limit)
+            .map(|item| {
+                item.map(|(addr, allow)| AllowanceInfo {
+                    spender: addr.into(),
+                    allowance: allow.allowance,
+                    expires: allow.expires,
+                })
+            })
+            .collect::<StdResult<_>>()?;
+        Ok(AllAllowancesResponse { allowances })
+    }
+
+    /// Returns all allowances this spender has been granted. Supports pagination.
+    fn all_spender_allowances(
+        &self,
+        ctx: (Deps, Env),
+        spender: String,
+        start_after: Option<String>,
+        limit: Option<u32>,
+    ) -> StdResult<AllSpenderAllowancesResponse> {
+        let (deps, _) = ctx;
+
+        let spender_addr = deps.api.addr_validate(&spender)?;
+        let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
+        let start_after = start_after.map(Addr::unchecked);
+        let start = start_after.as_ref().and_then(Bounder::exclusive_bound);
+
+        let allowances = self
+            .allowances_spender
+            .prefix(&spender_addr)
+            .range(deps.storage, start, None, Order::Ascending)
+            .take(limit)
+            .map(|item| {
+                item.map(|(addr, allow)| SpenderAllowanceInfo {
+                    owner: addr.into(),
+                    allowance: allow.allowance,
+                    expires: allow.expires,
+                })
+            })
+            .collect::<StdResult<_>>()?;
+        Ok(AllSpenderAllowancesResponse { allowances })
+    }
+}
diff --git a/contracts/cw20-base/src/contract.rs b/contracts/cw20-base/src/contract.rs
new file mode 100644
index 0000000..f1ee9ae
--- /dev/null
+++ b/contracts/cw20-base/src/contract.rs
@@ -0,0 +1,337 @@
+use crate::error::ContractError;
+use crate::responses::{BalanceResponse, Cw20Coin, Cw20ReceiveMsg, TokenInfoResponse};
+use crate::validation::{validate_accounts, validate_msg, verify_logo};
+use cosmwasm_schema::cw_serde;
+use cosmwasm_std::{
+    ensure, Addr, Binary, BlockInfo, Deps, DepsMut, Env, MessageInfo, Response, StdError,
+    StdResult, Storage, Uint128,
+};
+use cw2::set_contract_version;
+use cw20_allowances::responses::AllowanceResponse;
+use cw20_marketing::responses::{LogoInfo, MarketingInfoResponse};
+use cw20_marketing::Logo;
+use cw20_minting::responses::MinterResponse;
+use cw_storage_plus::{Item, Map};
+use sylvia::{contract, schemars};
+
+// version info for migration info
+const CONTRACT_NAME: &str = "crates.io:cw20-base";
+const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
+
+#[cw_serde]
+pub struct TokenInfo {
+    pub name: String,
+    pub symbol: String,
+    pub decimals: u8,
+    pub total_supply: Uint128,
+    pub mint: Option<MinterData>,
+}
+
+#[cw_serde]
+pub struct MinterData {
+    pub minter: Addr,
+    /// cap is how many more tokens can be issued by the minter
+    pub cap: Option<Uint128>,
+}
+
+#[cw_serde]
+pub struct InstantiateMarketingInfo {
+    pub project: Option<String>,
+    pub description: Option<String>,
+    pub marketing: Option<String>,
+    pub logo: Option<Logo>,
+}
+
+#[cw_serde]
+pub struct InstantiateMsgData {
+    pub name: String,
+    pub symbol: String,
+    pub decimals: u8,
+    pub initial_balances: Vec<Cw20Coin>,
+    pub mint: Option<MinterResponse>,
+    pub marketing: Option<InstantiateMarketingInfo>,
+}
+
+pub struct Cw20Base<'a> {
+    pub(crate) token_info: Item<'static, TokenInfo>,
+    pub(crate) marketing_info: Item<'static, MarketingInfoResponse>,
+    pub(crate) logo: Item<'static, Logo>,
+    pub(crate) balances: Map<'static, &'a Addr, Uint128>,
+    pub(crate) allowances: Map<'static, (&'a Addr, &'a Addr), AllowanceResponse>,
+    // TODO: After https://github.com/CosmWasm/cw-plus/issues/670 is implemented, replace this with a `MultiIndex` over `ALLOWANCES`
+    pub(crate) allowances_spender: Map<'static, (&'a Addr, &'a Addr), AllowanceResponse>,
+}
+
+#[contract]
+#[messages(cw20_allowances as Cw20Allowances)]
+impl Cw20Base<'_> {
+    pub const fn new() -> Self {
+        Self {
+            token_info: Item::new("token_info"),
+            marketing_info: Item::new("marketing_info"),
+            logo: Item::new("logo"),
+            balances: Map::new("balances"),
+            allowances: Map::new("allowances"),
+            allowances_spender: Map::new("allowances_spender"),
+        }
+    }
+
+    pub fn create_accounts(
+        &self,
+        deps: &mut DepsMut,
+        accounts: &[Cw20Coin],
+    ) -> Result<Uint128, ContractError> {
+        validate_accounts(accounts)?;
+
+        let mut total_supply = Uint128::zero();
+        for row in accounts {
+            let address = deps.api.addr_validate(&row.address)?;
+            self.balances.save(deps.storage, &address, &row.amount)?;
+            total_supply += row.amount;
+        }
+
+        Ok(total_supply)
+    }
+
+    // this can be used to update a lower allowance - call bucket.update with proper keys
+    pub fn deduct_allowance(
+        &self,
+        storage: &mut dyn Storage,
+        owner: &Addr,
+        spender: &Addr,
+        block: &BlockInfo,
+        amount: Uint128,
+    ) -> Result<AllowanceResponse, ContractError> {
+        let update_fn = |current: Option<AllowanceResponse>| -> _ {
+            let allowance = current.ok_or(ContractError::NoAllowance {})?;
+
+            if !allowance.expires.is_expired(block) {
+                // deduct the allowance if enough
+                let expires = allowance.expires;
+                let allowance = allowance
+                    .allowance
+                    .checked_sub(amount)
+                    .map_err(StdError::overflow)?;
+                Ok(AllowanceResponse { allowance, expires })
+            } else {
+                Err(ContractError::Expired {})
+            }
+        };
+        self.allowances
+            .update(storage, (owner, spender), update_fn)?;
+        self.allowances_spender
+            .update(storage, (spender, owner), update_fn)
+    }
+
+    #[msg(instantiate)]
+    pub fn instantiate(
+        &self,
+        ctx: (DepsMut, Env, MessageInfo),
+        data: InstantiateMsgData,
+    ) -> Result<Response, ContractError> {
+        let (mut deps, ..) = ctx;
+        let InstantiateMsgData {
+            name,
+            symbol,
+            decimals,
+            initial_balances,
+            mint,
+            marketing,
+        } = data;
+        set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
+
+        // check valid token info
+        validate_msg(&name, &symbol, decimals)?;
+        // create initial accounts
+        let total_supply = self.create_accounts(&mut deps, &initial_balances)?;
+
+        ensure!(
+            !matches!(
+                mint.as_ref().and_then(|v| v.cap), Some(limit) if total_supply > limit
+            ),
+            StdError::generic_err("Initial supply greater than cap")
+        );
+
+        let mint = match mint {
+            Some(m) => Some(MinterData {
+                minter: deps.api.addr_validate(&m.minter)?,
+                cap: m.cap,
+            }),
+            None => None,
+        };
+
+        // store token info
+        let data = TokenInfo {
+            name,
+            symbol,
+            decimals,
+            total_supply,
+            mint,
+        };
+        self.token_info.save(deps.storage, &data)?;
+
+        if let Some(marketing) = marketing {
+            let logo = if let Some(logo) = marketing.logo {
+                verify_logo(&logo)?;
+                self.logo.save(deps.storage, &logo)?;
+
+                match logo {
+                    Logo::Url(url) => Some(LogoInfo::Url(url)),
+                    Logo::Embedded(_) => Some(LogoInfo::Embedded),
+                }
+            } else {
+                None
+            };
+
+            let data = MarketingInfoResponse {
+                project: marketing.project,
+                description: marketing.description,
+                marketing: marketing
+                    .marketing
+                    .map(|addr| deps.api.addr_validate(&addr))
+                    .transpose()?,
+                logo,
+            };
+            self.marketing_info.save(deps.storage, &data)?;
+        }
+
+        Ok(Response::default())
+    }
+
+    /// Transfer is a base message to move tokens to another account without triggering actions
+    #[msg(exec)]
+    fn transfer(
+        &self,
+        ctx: (DepsMut, Env, MessageInfo),
+        recipient: String,
+        amount: Uint128,
+    ) -> Result<Response, ContractError> {
+        let (deps, _, info) = ctx;
+
+        ensure!(
+            amount != Uint128::zero(),
+            ContractError::InvalidZeroAmount {}
+        );
+
+        let rcpt_addr = deps.api.addr_validate(&recipient)?;
+
+        self.balances
+            .update(deps.storage, &info.sender, |balance| {
+                Ok::<_, StdError>(balance.unwrap_or_default().checked_sub(amount)?)
+            })?;
+        self.balances.update(deps.storage, &rcpt_addr, |balance| {
+            Ok::<_, StdError>(balance.unwrap_or_default().checked_add(amount)?)
+        })?;
+
+        let res = Response::new()
+            .add_attribute("action", "transfer")
+            .add_attribute("from", info.sender)
+            .add_attribute("to", recipient)
+            .add_attribute("amount", amount);
+        Ok(res)
+    }
+
+    /// Burn is a base message to destroy tokens forever
+    #[msg(exec)]
+    fn burn(
+        &self,
+        ctx: (DepsMut, Env, MessageInfo),
+        amount: Uint128,
+    ) -> Result<Response, ContractError> {
+        let (deps, _, info) = ctx;
+
+        ensure!(
+            amount != Uint128::zero(),
+            ContractError::InvalidZeroAmount {}
+        );
+
+        // lower balance
+        self.balances
+            .update(deps.storage, &info.sender, |balance| {
+                Ok::<_, StdError>(balance.unwrap_or_default().checked_sub(amount)?)
+            })?;
+        // reduce total_supply
+        self.token_info
+            .update(deps.storage, |mut info| -> StdResult<_> {
+                info.total_supply = info.total_supply.checked_sub(amount)?;
+                Ok(info)
+            })?;
+
+        let res = Response::new()
+            .add_attribute("action", "burn")
+            .add_attribute("from", info.sender)
+            .add_attribute("amount", amount);
+        Ok(res)
+    }
+
+    /// Send is a base message to transfer tokens to a contract and trigger an action
+    /// on the receiving contract.
+    #[msg(exec)]
+    fn send(
+        &self,
+        ctx: (DepsMut, Env, MessageInfo),
+        contract: String,
+        amount: Uint128,
+        msg: Binary,
+    ) -> Result<Response, ContractError> {
+        let (deps, _, info) = ctx;
+
+        ensure!(
+            amount != Uint128::zero(),
+            ContractError::InvalidZeroAmount {}
+        );
+
+        let rcpt_addr = deps.api.addr_validate(&contract)?;
+
+        // move the tokens to the contract
+        self.balances
+            .update(deps.storage, &info.sender, |balance| {
+                Ok::<_, StdError>(balance.unwrap_or_default().checked_sub(amount)?)
+            })?;
+        self.balances.update(deps.storage, &rcpt_addr, |balance| {
+            Ok::<_, StdError>(balance.unwrap_or_default().checked_add(amount)?)
+        })?;
+        let res = Response::new()
+            .add_attribute("action", "send")
+            .add_attribute("from", &info.sender)
+            .add_attribute("to", &contract)
+            .add_attribute("amount", amount);
+
+        let msg = Cw20ReceiveMsg {
+            sender: info.sender.into(),
+            amount,
+            msg,
+        }
+        .into_cosmos_msg(contract)?;
+
+        let res = res.add_message(msg);
+        Ok(res)
+    }
+
+    /// Returns the current balance of the given address, 0 if unset.
+    #[msg(query)]
+    fn balance(&self, ctx: (Deps, Env), address: String) -> StdResult<BalanceResponse> {
+        let (deps, _) = ctx;
+        let address = deps.api.addr_validate(&address)?;
+        let balance = self
+            .balances
+            .may_load(deps.storage, &address)?
+            .unwrap_or_default();
+        Ok(BalanceResponse { balance })
+    }
+
+    /// Returns metadata on the contract - name, decimals, supply, etc.
+    #[msg(query)]
+    fn token_info(&self, ctx: (Deps, Env)) -> StdResult<TokenInfoResponse> {
+        let (deps, _) = ctx;
+
+        let info = self.token_info.load(deps.storage)?;
+        let res = TokenInfoResponse {
+            name: info.name,
+            symbol: info.symbol,
+            decimals: info.decimals,
+            total_supply: info.total_supply,
+        };
+        Ok(res)
+    }
+}
diff --git a/contracts/cw20-base/src/error.rs b/contracts/cw20-base/src/error.rs
new file mode 100644
index 0000000..e0fece0
--- /dev/null
+++ b/contracts/cw20-base/src/error.rs
@@ -0,0 +1,41 @@
+use cosmwasm_std::StdError;
+use thiserror::Error;
+
+#[derive(Error, Debug, PartialEq)]
+pub enum ContractError {
+    #[error("{0}")]
+    Std(#[from] StdError),
+
+    #[error("Unauthorized")]
+    Unauthorized,
+
+    #[error("Cannot set to own account")]
+    CannotSetOwnAccount,
+
+    #[error("Invalid zero amount")]
+    InvalidZeroAmount,
+
+    #[error("Allowance is expired")]
+    Expired,
+
+    #[error("No allowance for this account")]
+    NoAllowance,
+
+    #[error("Minting cannot exceed the cap")]
+    CannotExceedCap,
+
+    #[error("Logo binary data exceeds 5KB limit")]
+    LogoTooBig,
+
+    #[error("Invalid xml preamble for SVG")]
+    InvalidXmlPreamble,
+
+    #[error("Invalid png header")]
+    InvalidPngHeader,
+
+    #[error("Invalid expiration value")]
+    InvalidExpiration,
+
+    #[error("Duplicate initial balance addresses")]
+    DuplicateInitialBalanceAddresses,
+}
diff --git a/contracts/cw20-base/src/lib.rs b/contracts/cw20-base/src/lib.rs
new file mode 100644
index 0000000..27d3412
--- /dev/null
+++ b/contracts/cw20-base/src/lib.rs
@@ -0,0 +1,46 @@
+mod allowances;
+mod contract;
+mod error;
+pub mod responses;
+mod validation;
+
+#[cfg(any(test, feature = "tests"))]
+pub mod multitest;
+
+#[cfg(not(feature = "library"))]
+mod entry_points {
+    use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response};
+
+    use crate::contract::{ContractExecMsg, ContractQueryMsg, Cw20Base, InstantiateMsg};
+    use crate::error::ContractError;
+
+    const CONTRACT: Cw20Base = Cw20Base::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/cw20-base/src/responses.rs b/contracts/cw20-base/src/responses.rs
new file mode 100644
index 0000000..3e0ea92
--- /dev/null
+++ b/contracts/cw20-base/src/responses.rs
@@ -0,0 +1,85 @@
+use cosmwasm_schema::cw_serde;
+use cosmwasm_std::{to_binary, Addr, Binary, CosmosMsg, StdResult, Uint128, WasmMsg};
+use std::fmt;
+
+/// Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg
+#[cw_serde]
+pub struct Cw20ReceiveMsg {
+    pub sender: String,
+    pub amount: Uint128,
+    pub msg: Binary,
+}
+
+impl Cw20ReceiveMsg {
+    /// serializes the message
+    pub fn into_binary(self) -> StdResult<Binary> {
+        let msg = ReceiverExecuteMsg::Receive(self);
+        to_binary(&msg)
+    }
+
+    /// creates a cosmos_msg sending this struct to the named contract
+    pub fn into_cosmos_msg<T: Into<String>>(self, contract_addr: T) -> StdResult<CosmosMsg> {
+        let msg = self.into_binary()?;
+        let execute = WasmMsg::Execute {
+            contract_addr: contract_addr.into(),
+            msg,
+            funds: vec![],
+        };
+        Ok(execute.into())
+    }
+}
+
+// This is just a helper to properly serialize the above message
+#[cw_serde]
+enum ReceiverExecuteMsg {
+    Receive(Cw20ReceiveMsg),
+}
+
+#[cw_serde]
+pub struct Cw20Coin {
+    pub address: String,
+    pub amount: Uint128,
+}
+
+impl Cw20Coin {
+    pub fn is_empty(&self) -> bool {
+        self.amount == Uint128::zero()
+    }
+}
+
+impl fmt::Display for Cw20Coin {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "address: {}, amount: {}", self.address, self.amount)
+    }
+}
+
+#[cw_serde]
+pub struct Cw20CoinVerified {
+    pub address: Addr,
+    pub amount: Uint128,
+}
+
+impl Cw20CoinVerified {
+    pub fn is_empty(&self) -> bool {
+        self.amount == Uint128::zero()
+    }
+}
+
+impl fmt::Display for Cw20CoinVerified {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "address: {}, amount: {}", self.address, self.amount)
+    }
+}
+
+#[cw_serde]
+pub struct BalanceResponse {
+    pub balance: Uint128,
+}
+
+#[cw_serde]
+pub struct TokenInfoResponse {
+    pub name: String,
+    pub symbol: String,
+    pub decimals: u8,
+    pub total_supply: Uint128,
+}
diff --git a/contracts/cw20-base/src/validation.rs b/contracts/cw20-base/src/validation.rs
new file mode 100644
index 0000000..17a6b6e
--- /dev/null
+++ b/contracts/cw20-base/src/validation.rs
@@ -0,0 +1,119 @@
+use cosmwasm_std::{StdError, StdResult};
+use cw20_marketing::{EmbeddedLogo, Logo};
+
+use crate::error::ContractError;
+use crate::responses::Cw20Coin;
+
+const LOGO_SIZE_CAP: usize = 5 * 1024;
+
+pub fn validate_msg(name: &str, symbol: &str, decimals: u8) -> StdResult<()> {
+    // Check name, symbol, decimals
+    if !has_valid_name(name) {
+        return Err(StdError::generic_err(
+            "Name is not in the expected format (3-50 UTF-8 bytes)",
+        ));
+    }
+    if !has_valid_symbol(symbol) {
+        return Err(StdError::generic_err(
+            "Ticker symbol is not in expected format [a-zA-Z\\-]{3,12}",
+        ));
+    }
+    if decimals > 18 {
+        return Err(StdError::generic_err("Decimals must not exceed 18"));
+    }
+    Ok(())
+}
+
+pub fn validate_accounts(accounts: &[Cw20Coin]) -> Result<(), ContractError> {
+    let mut addresses = accounts.iter().map(|c| &c.address).collect::<Vec<_>>();
+    addresses.sort();
+    addresses.dedup();
+
+    if addresses.len() != accounts.len() {
+        Err(ContractError::DuplicateInitialBalanceAddresses {})
+    } else {
+        Ok(())
+    }
+}
+
+/// Checks if passed logo is correct, and if not, returns an error
+pub fn verify_logo(logo: &Logo) -> Result<(), ContractError> {
+    match logo {
+        Logo::Embedded(EmbeddedLogo::Svg(logo)) => verify_xml_logo(logo),
+        Logo::Embedded(EmbeddedLogo::Png(logo)) => verify_png_logo(logo),
+        Logo::Url(_) => Ok(()), // Any reasonable url validation would be regex based, probably not worth it
+    }
+}
+
+fn has_valid_name(name: &str) -> bool {
+    let bytes = name.as_bytes();
+    if bytes.len() < 3 || bytes.len() > 50 {
+        return false;
+    }
+    true
+}
+
+fn has_valid_symbol(symbol: &str) -> bool {
+    let bytes = symbol.as_bytes();
+    if bytes.len() < 3 || bytes.len() > 12 {
+        return false;
+    }
+    for byte in bytes.iter() {
+        if (*byte != 45) && (*byte < 65 || *byte > 90) && (*byte < 97 || *byte > 122) {
+            return false;
+        }
+    }
+    true
+}
+
+/// Validates XML logo
+fn verify_xml_logo(logo: &[u8]) -> Result<(), ContractError> {
+    verify_xml_preamble(logo)?;
+
+    if logo.len() > LOGO_SIZE_CAP {
+        Err(ContractError::LogoTooBig {})
+    } else {
+        Ok(())
+    }
+}
+
+/// Validates png logo
+fn verify_png_logo(logo: &[u8]) -> Result<(), ContractError> {
+    // PNG header format:
+    // 0x89 - magic byte, out of ASCII table to fail on 7-bit systems
+    // "PNG" ascii representation
+    // [0x0d, 0x0a] - dos style line ending
+    // 0x1a - dos control character, stop displaying rest of the file
+    // 0x0a - unix style line ending
+    const HEADER: [u8; 8] = [0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a];
+    if logo.len() > LOGO_SIZE_CAP {
+        Err(ContractError::LogoTooBig {})
+    } else if !logo.starts_with(&HEADER) {
+        Err(ContractError::InvalidPngHeader {})
+    } else {
+        Ok(())
+    }
+}
+
+/// Checks if data starts with XML preamble
+fn verify_xml_preamble(data: &[u8]) -> Result<(), ContractError> {
+    // The easiest way to perform this check would be just match on regex, however regex
+    // compilation is heavy and probably not worth it.
+
+    let preamble = data
+        .split_inclusive(|c| *c == b'>')
+        .next()
+        .ok_or(ContractError::InvalidXmlPreamble {})?;
+
+    const PREFIX: &[u8] = b"<?xml ";
+    const POSTFIX: &[u8] = b"?>";
+
+    if !(preamble.starts_with(PREFIX) && preamble.ends_with(POSTFIX)) {
+        Err(ContractError::InvalidXmlPreamble {})
+    } else {
+        Ok(())
+    }
+
+    // Additionally attributes format could be validated as they are well defined, as well as
+    // comments presence inside of preable, but it is probably not worth it.
+}
diff --git a/contracts/cw20-marketing/src/lib.rs b/contracts/cw20-marketing/src/lib.rs
index 6d621c6..c4171a0 100644
--- a/contracts/cw20-marketing/src/lib.rs
+++ b/contracts/cw20-marketing/src/lib.rs
@@ -1,4 +1,4 @@
-mod responses;
+pub mod responses;
 
 use cosmwasm_schema::cw_serde;
 use cosmwasm_std::{
diff --git a/contracts/cw20-minting/src/lib.rs b/contracts/cw20-minting/src/lib.rs
index 8fb313a..f42c399 100644
--- a/contracts/cw20-minting/src/lib.rs
+++ b/contracts/cw20-minting/src/lib.rs
@@ -1,4 +1,4 @@
-mod responses;
+pub mod responses;
 
 use cosmwasm_std::{Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Uint128};
 use responses::MinterResponse;


Test patch:
diff --git a/contracts/cw20-base/src/multitest.rs b/contracts/cw20-base/src/multitest.rs
new file mode 100644
index 0000000..c388f95
--- /dev/null
+++ b/contracts/cw20-base/src/multitest.rs
@@ -0,0 +1,78 @@
+use anyhow::{bail, Result as AnyResult};
+use cosmwasm_std::{from_slice, Empty};
+use cw_multi_test::Contract;
+
+use crate::contract::{ContractExecMsg, ContractQueryMsg, Cw20Base, InstantiateMsg};
+
+#[cfg(test)]
+mod allowances_tests;
+#[cfg(test)]
+mod proxy;
+#[cfg(test)]
+mod receiver;
+#[cfg(test)]
+mod receiver_contract;
+
+impl Contract<Empty> for Cw20Base<'_> {
+    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/cw20-base/src/multitest/allowances_tests.rs b/contracts/cw20-base/src/multitest/allowances_tests.rs
new file mode 100644
index 0000000..bc85153
--- /dev/null
+++ b/contracts/cw20-base/src/multitest/allowances_tests.rs
@@ -0,0 +1,966 @@
+use cosmwasm_std::{Addr, Binary, StdError, Timestamp, Uint128};
+use cw20_allowances::responses::{
+    AllAllowancesResponse, AllSpenderAllowancesResponse, AllowanceInfo, AllowanceResponse,
+    SpenderAllowanceInfo,
+};
+use cw_multi_test::{next_block, App};
+use cw_utils::Expiration;
+
+use crate::contract::InstantiateMsgData;
+use crate::error::ContractError;
+use crate::multitest::receiver_contract::ReceiverContractCodeId;
+use crate::responses::Cw20Coin;
+
+use super::proxy::Cw20BaseCodeId;
+
+#[test]
+fn increase_decrease_allowances() {
+    let mut app = App::default();
+
+    let owner = Addr::unchecked("addr0001");
+    let spender = Addr::unchecked("addr0002");
+
+    let code_id = Cw20BaseCodeId::store_code(&mut app);
+
+    let contract = code_id
+        .instantiate(
+            &mut app,
+            &owner,
+            InstantiateMsgData {
+                name: "Auto Gen".to_string(),
+                symbol: "AUTO".to_string(),
+                decimals: 3,
+                initial_balances: vec![Cw20Coin {
+                    address: owner.clone().into(),
+                    amount: Uint128::new(12340000),
+                }],
+                mint: None,
+                marketing: None,
+            },
+            "Cw20Base contract",
+        )
+        .unwrap();
+
+    // no allowance to start
+    let allowances = contract
+        .allowance(&app, owner.to_string(), spender.to_string())
+        .unwrap();
+    assert_eq!(allowances, AllowanceResponse::default());
+
+    // set allowance with height expiration
+    let allowance = Uint128::new(7777);
+    let expires = Expiration::AtHeight(123_456);
+    contract
+        .increase_allowance(
+            &mut app,
+            &owner,
+            spender.to_string(),
+            allowance,
+            Some(expires),
+        )
+        .unwrap();
+
+    // ensure it looks good
+    let allowance_resp = contract
+        .allowance(&app, owner.to_string(), spender.to_string())
+        .unwrap();
+
+    assert_eq!(allowance_resp, AllowanceResponse { allowance, expires });
+
+    // decrease it a bit with no expire set - stays the same
+    let lower = Uint128::new(4444);
+    let allowance = allowance.checked_sub(lower).unwrap();
+    contract
+        .decrease_allowance(&mut app, &owner, spender.to_string(), lower, None)
+        .unwrap();
+
+    let allowance_resp = contract
+        .allowance(&app, owner.to_string(), spender.to_string())
+        .unwrap();
+
+    assert_eq!(allowance_resp, AllowanceResponse { allowance, expires });
+
+    // increase it some more and override the expires
+    let raise = Uint128::new(87654);
+    let allowance = allowance + raise;
+    let expires = Expiration::AtTime(Timestamp::from_seconds(8888888888));
+    contract
+        .increase_allowance(&mut app, &owner, spender.to_string(), raise, Some(expires))
+        .unwrap();
+    let allowance_resp = contract
+        .allowance(&app, owner.to_string(), spender.to_string())
+        .unwrap();
+    assert_eq!(allowance_resp, AllowanceResponse { allowance, expires });
+
+    // decrease it below 0
+    contract
+        .decrease_allowance(
+            &mut app,
+            &owner,
+            spender.to_string(),
+            Uint128::new(99988647623876347),
+            None,
+        )
+        .unwrap();
+    let allowance_resp = contract
+        .allowance(&app, owner.to_string(), spender.to_string())
+        .unwrap();
+    assert_eq!(allowance_resp, AllowanceResponse::default());
+}
+
+#[test]
+fn allowances_independent() {
+    let mut app = App::default();
+
+    let owner = Addr::unchecked("addr0001");
+    let spender = Addr::unchecked("addr0002");
+    let spender2 = Addr::unchecked("addr0003");
+
+    let code_id = Cw20BaseCodeId::store_code(&mut app);
+
+    let contract = code_id
+        .instantiate(
+            &mut app,
+            &owner,
+            InstantiateMsgData {
+                name: "Auto Gen".to_string(),
+                symbol: "AUTO".to_string(),
+                decimals: 3,
+                initial_balances: vec![Cw20Coin {
+                    address: owner.clone().into(),
+                    amount: Uint128::new(12340000),
+                }],
+                mint: None,
+                marketing: None,
+            },
+            "Cw20Base contract",
+        )
+        .unwrap();
+
+    // no allowance to start
+    let allowance_resp = contract
+        .allowance(&app, owner.to_string(), spender.to_string())
+        .unwrap();
+    assert_eq!(allowance_resp, AllowanceResponse::default());
+
+    let allowance_resp = contract
+        .allowance(&app, owner.to_string(), spender2.to_string())
+        .unwrap();
+    assert_eq!(allowance_resp, AllowanceResponse::default());
+
+    // set allowance with height expiration
+    let allowance = Uint128::new(7777);
+    let expires = Expiration::AtHeight(123_456);
+    contract
+        .increase_allowance(
+            &mut app,
+            &owner,
+            spender.to_string(),
+            allowance,
+            Some(expires),
+        )
+        .unwrap();
+
+    // set other allowance with no expiration
+    let allowance2 = Uint128::new(87654);
+    contract
+        .increase_allowance(&mut app, &owner, spender2.to_string(), allowance2, None)
+        .unwrap();
+
+    // check they are proper
+    let expect_one = AllowanceResponse { allowance, expires };
+    let expect_two = AllowanceResponse {
+        allowance: allowance2,
+        expires: Expiration::Never {},
+    };
+    let allowance_resp = contract
+        .allowance(&app, owner.to_string(), spender.to_string())
+        .unwrap();
+    assert_eq!(allowance_resp, expect_one);
+
+    let allowance_resp = contract
+        .allowance(&app, owner.to_string(), spender2.to_string())
+        .unwrap();
+    assert_eq!(allowance_resp, expect_two);
+
+    // also allow spender -> spender2 with no interference
+    let allowance3 = Uint128::new(1821);
+    let expires3 = Expiration::AtTime(Timestamp::from_seconds(3767626296));
+    contract
+        .increase_allowance(
+            &mut app,
+            &Addr::unchecked(spender.to_string()),
+            spender2.to_string(),
+            allowance3,
+            Some(expires3),
+        )
+        .unwrap();
+
+    let expect_three = AllowanceResponse {
+        allowance: allowance3,
+        expires: expires3,
+    };
+    let allowance_resp = contract
+        .allowance(&app, owner.to_string(), spender.to_string())
+        .unwrap();
+    assert_eq!(allowance_resp, expect_one);
+    let allowance_resp = contract
+        .allowance(&app, owner.to_string(), spender2.to_string())
+        .unwrap();
+    assert_eq!(allowance_resp, expect_two);
+    let allowance_resp = contract
+        .allowance(&app, spender.to_string(), spender2.to_string())
+        .unwrap();
+    assert_eq!(allowance_resp, expect_three);
+}
+
+#[test]
+fn no_self_allowance() {
+    let mut app = App::default();
+
+    let owner = Addr::unchecked("addr0001");
+
+    let code_id = Cw20BaseCodeId::store_code(&mut app);
+
+    let contract = code_id
+        .instantiate(
+            &mut app,
+            &owner,
+            InstantiateMsgData {
+                name: "Auto Gen".to_string(),
+                symbol: "AUTO".to_string(),
+                decimals: 3,
+                initial_balances: vec![Cw20Coin {
+                    address: owner.clone().into(),
+                    amount: Uint128::new(12340000),
+                }],
+                mint: None,
+                marketing: None,
+            },
+            "Cw20Base contract",
+        )
+        .unwrap();
+
+    // self-allowance
+    let err = contract
+        .increase_allowance(
+            &mut app,
+            &Addr::unchecked(owner.clone()),
+            owner.to_string(),
+            Uint128::new(7777),
+            None,
+        )
+        .unwrap_err();
+
+    assert_eq!(err, ContractError::CannotSetOwnAccount {});
+
+    // decrease self-allowance
+    let err = contract
+        .decrease_allowance(
+            &mut app,
+            &Addr::unchecked(owner.clone()),
+            owner.to_string(),
+            Uint128::new(7777),
+            None,
+        )
+        .unwrap_err();
+
+    assert_eq!(err, ContractError::CannotSetOwnAccount {});
+}
+
+#[test]
+fn transfer_from_self_to_self() {
+    let mut app = App::default();
+
+    let owner = Addr::unchecked("addr0001");
+    let amount = Uint128::new(999999);
+
+    let code_id = Cw20BaseCodeId::store_code(&mut app);
+
+    let contract = code_id
+        .instantiate(
+            &mut app,
+            &owner,
+            InstantiateMsgData {
+                name: "Auto Gen".to_string(),
+                symbol: "AUTO".to_string(),
+                decimals: 3,
+                initial_balances: vec![Cw20Coin {
+                    address: owner.clone().into(),
+                    amount,
+                }],
+                mint: None,
+                marketing: None,
+            },
+            "Cw20Base contract",
+        )
+        .unwrap();
+
+    // valid transfer of part of the allowance
+    let transfer = Uint128::new(44444);
+    contract
+        .transfer_from(
+            &mut app,
+            &owner,
+            owner.to_string(),
+            owner.to_string(),
+            transfer,
+        )
+        .unwrap();
+
+    // make sure amount of money is the same
+    let balance_resp = contract.balance(&app, owner.to_string()).unwrap();
+    assert_eq!(balance_resp.balance, amount);
+}
+
+#[test]
+fn transfer_from_owner_requires_no_allowance() {
+    let mut app = App::default();
+
+    let owner = Addr::unchecked("addr0001");
+    let rcpt = Addr::unchecked("addr0003");
+    let start_amount = Uint128::new(999999);
+
+    let code_id = Cw20BaseCodeId::store_code(&mut app);
+
+    let contract = code_id
+        .instantiate(
+            &mut app,
+            &owner,
+            InstantiateMsgData {
+                name: "Auto Gen".to_string(),
+                symbol: "AUTO".to_string(),
+                decimals: 3,
+                initial_balances: vec![Cw20Coin {
+                    address: owner.clone().into(),
+                    amount: start_amount,
+                }],
+                mint: None,
+                marketing: None,
+            },
+            "Cw20Base contract",
+        )
+        .unwrap();
+
+    // valid transfer of part of the allowance
+    let transfer = Uint128::new(44444);
+    contract
+        .transfer_from(
+            &mut app,
+            &owner,
+            owner.to_string(),
+            rcpt.to_string(),
+            transfer,
+        )
+        .unwrap();
+
+    // make sure money arrived
+    let balance_resp = contract.balance(&app, owner.to_string()).unwrap();
+    assert_eq!(
+        balance_resp.balance,
+        start_amount.checked_sub(transfer).unwrap()
+    );
+
+    let balance_resp = contract.balance(&app, rcpt.to_string()).unwrap();
+    assert_eq!(balance_resp.balance, transfer);
+}
+
+#[test]
+fn transfer_from_respects_limits() {
+    let mut app = App::default();
+
+    let owner = Addr::unchecked("addr0001");
+    let spender = Addr::unchecked("addr0002");
+    let rcpt = Addr::unchecked("addr0003");
+    let start_amount = Uint128::new(999999);
+
+    let code_id = Cw20BaseCodeId::store_code(&mut app);
+
+    let contract = code_id
+        .instantiate(
+            &mut app,
+            &owner,
+            InstantiateMsgData {
+                name: "Auto Gen".to_string(),
+                symbol: "AUTO".to_string(),
+                decimals: 3,
+                initial_balances: vec![Cw20Coin {
+                    address: owner.clone().into(),
+                    amount: start_amount,
+                }],
+                mint: None,
+                marketing: None,
+            },
+            "Cw20Base contract",
+        )
+        .unwrap();
+
+    // provide an allowance
+    let allowance = Uint128::new(77777);
+    contract
+        .increase_allowance(&mut app, &owner, spender.to_string(), allowance, None)
+        .unwrap();
+
+    // valid transfer of part of the allowance
+    let transfer = Uint128::new(44444);
+    contract
+        .transfer_from(
+            &mut app,
+            &Addr::unchecked(spender.to_string()),
+            owner.to_string(),
+            rcpt.to_string(),
+            transfer,
+        )
+        .unwrap();
+
+    // make sure money arrived
+    let balance_resp = contract.balance(&app, owner.to_string()).unwrap();
+    assert_eq!(
+        balance_resp.balance,
+        start_amount.checked_sub(transfer).unwrap()
+    );
+
+    let balance_resp = contract.balance(&app, rcpt.to_string()).unwrap();
+    assert_eq!(balance_resp.balance, transfer);
+
+    // ensure it looks good
+    let allowance_resp = contract
+        .allowance(&app, owner.to_string(), spender.to_string())
+        .unwrap();
+    assert_eq!(
+        allowance_resp,
+        AllowanceResponse {
+            allowance: allowance.checked_sub(transfer).unwrap(),
+            expires: Expiration::Never {}
+        }
+    );
+
+    // cannot send more than the allowance
+    let err = contract
+        .transfer_from(
+            &mut app,
+            &Addr::unchecked(spender.to_string()),
+            owner.to_string(),
+            rcpt.to_string(),
+            Uint128::new(33443),
+        )
+        .unwrap_err();
+    assert!(matches!(err, ContractError::Std(StdError::Overflow { .. })));
+
+    // let us increase limit, but set the expiration to expire in the next block
+    let next_block_height = app.block_info().height + 1;
+    contract
+        .increase_allowance(
+            &mut app,
+            &owner,
+            spender.to_string(),
+            Uint128::new(1000),
+            Some(Expiration::AtHeight(next_block_height)),
+        )
+        .unwrap();
+
+    // move to next block
+    app.update_block(next_block);
+
+    // we should now get the expiration error
+    let err = contract
+        .transfer_from(
+            &mut app,
+            &spender,
+            owner.to_string(),
+            rcpt.to_string(),
+            Uint128::new(33443),
+        )
+        .unwrap_err();
+    assert!(matches!(err, ContractError::Expired {}));
+}
+
+#[test]
+fn burn_from_respects_limits() {
+    let mut app = App::default();
+
+    let owner = Addr::unchecked("addr0001");
+    let spender = Addr::unchecked("addr0002");
+    let start_amount = Uint128::new(999999);
+
+    let code_id = Cw20BaseCodeId::store_code(&mut app);
+
+    let contract = code_id
+        .instantiate(
+            &mut app,
+            &owner,
+            InstantiateMsgData {
+                name: "Auto Gen".to_string(),
+                symbol: "AUTO".to_string(),
+                decimals: 3,
+                initial_balances: vec![Cw20Coin {
+                    address: owner.clone().into(),
+                    amount: start_amount,
+                }],
+                mint: None,
+                marketing: None,
+            },
+            "Cw20Base contract",
+        )
+        .unwrap();
+
+    // provide an allowance
+    let allowance = Uint128::new(77777);
+    contract
+        .increase_allowance(&mut app, &owner, spender.to_string(), allowance, None)
+        .unwrap();
+
+    // valid burn of part of the allowance
+    let transfer = Uint128::new(44444);
+    contract
+        .burn_from(&mut app, &spender, owner.to_string(), transfer)
+        .unwrap();
+
+    // make sure money burnt
+    let balance_resp = contract.balance(&app, owner.to_string()).unwrap();
+    assert_eq!(
+        balance_resp.balance,
+        start_amount.checked_sub(transfer).unwrap()
+    );
+
+    // ensure it looks good
+    let allowance_resp = contract
+        .allowance(&app, owner.to_string(), spender.to_string())
+        .unwrap();
+
+    assert_eq!(
+        allowance_resp,
+        AllowanceResponse {
+            allowance: allowance.checked_sub(transfer).unwrap(),
+            expires: Expiration::Never {}
+        }
+    );
+
+    // cannot burn more than the allowance
+    let err = contract
+        .burn_from(&mut app, &spender, owner.to_string(), Uint128::new(33443))
+        .unwrap_err();
+
+    assert!(matches!(err, ContractError::Std(StdError::Overflow { .. })));
+
+    // let us increase limit, but set the expiration to expire in the next block
+    let next_block_height = app.block_info().height + 1;
+    contract
+        .increase_allowance(
+            &mut app,
+            &owner,
+            spender.to_string(),
+            Uint128::new(1000),
+            Some(Expiration::AtHeight(next_block_height)),
+        )
+        .unwrap();
+
+    // move to next block
+    app.update_block(next_block);
+
+    // we should now get the expiration error
+    let err = contract
+        .burn_from(&mut app, &spender, owner.to_string(), Uint128::new(33443))
+        .unwrap_err();
+    assert!(matches!(err, ContractError::Expired {}));
+}
+
+// Ignoring currently due to some issue with unsupported msg being sent in send_from
+#[test]
+fn send_from_respects_limits() {
+    let mut app = App::default();
+
+    let owner = Addr::unchecked("addr0001");
+    let owner2 = Addr::unchecked("addr0003");
+    let spender = Addr::unchecked("addr0002");
+    let send_msg = Binary::from(r#"{"some":123}"#.as_bytes());
+    let start_amount = Uint128::new(999999);
+
+    let code_id = Cw20BaseCodeId::store_code(&mut app);
+    let receiver_code_id = ReceiverContractCodeId::store_code(&mut app);
+
+    let contract = code_id
+        .instantiate(
+            &mut app,
+            &owner,
+            InstantiateMsgData {
+                name: "Auto Gen".to_string(),
+                symbol: "AUTO".to_string(),
+                decimals: 3,
+                initial_balances: vec![Cw20Coin {
+                    address: owner.clone().into(),
+                    amount: start_amount,
+                }],
+                mint: None,
+                marketing: None,
+            },
+            "Cw20Base contract",
+        )
+        .unwrap();
+
+    let receiver_contract = receiver_code_id
+        .instantiate(&mut app, &owner2, "cool-dex")
+        .unwrap();
+
+    // provide an allowance
+    let allowance = Uint128::new(77777);
+    contract
+        .increase_allowance(&mut app, &owner, spender.to_string(), allowance, None)
+        .unwrap();
+
+    // valid send of part of the allowance
+    let transfer = Uint128::new(44444);
+    contract
+        .send_from(
+            &mut app,
+            &spender,
+            owner.to_string(),
+            receiver_contract.addr().to_string(),
+            transfer,
+            send_msg.clone(),
+        )
+        .unwrap();
+
+    // make sure money burnt
+    let balance_resp = contract.balance(&app, owner.to_string()).unwrap();
+    assert_eq!(
+        balance_resp.balance,
+        start_amount.checked_sub(transfer).unwrap()
+    );
+
+    // ensure it looks good
+    let allowance_resp = contract
+        .allowance(&app, owner.to_string(), spender.to_string())
+        .unwrap();
+
+    assert_eq!(
+        allowance_resp,
+        AllowanceResponse {
+            allowance: allowance.checked_sub(transfer).unwrap(),
+            expires: Expiration::Never {}
+        }
+    );
+
+    // cannot send more than the allowance
+    let err = contract
+        .send_from(
+            &mut app,
+            &spender,
+            owner.to_string(),
+            receiver_contract.addr().to_string(),
+            Uint128::new(33443),
+            send_msg.clone(),
+        )
+        .unwrap_err();
+    assert!(matches!(err, ContractError::Std(StdError::Overflow { .. })));
+
+    // let us increase limit, but set the expiration to expire in the next block
+    let next_block_height = app.block_info().height + 1;
+    contract
+        .increase_allowance(
+            &mut app,
+            &owner,
+            spender.to_string(),
+            Uint128::new(1000),
+            Some(Expiration::AtHeight(next_block_height)),
+        )
+        .unwrap();
+
+    // move to next block
+    app.update_block(next_block);
+
+    // we should now get the expiration error
+    let err = contract
+        .send_from(
+            &mut app,
+            &spender,
+            owner.to_string(),
+            receiver_contract.addr().to_string(),
+            Uint128::new(33443),
+            send_msg,
+        )
+        .unwrap_err();
+
+    assert!(matches!(err, ContractError::Expired {}));
+}
+
+#[test]
+fn no_past_expiration() {
+    let mut app = App::default();
+
+    let owner = Addr::unchecked("addr0001");
+    let spender = Addr::unchecked("addr0002");
+    let start_amount = Uint128::new(999999);
+    let allowance = Uint128::new(7777);
+
+    let code_id = Cw20BaseCodeId::store_code(&mut app);
+
+    let contract = code_id
+        .instantiate(
+            &mut app,
+            &owner,
+            InstantiateMsgData {
+                name: "Auto Gen".to_string(),
+                symbol: "AUTO".to_string(),
+                decimals: 3,
+                initial_balances: vec![Cw20Coin {
+                    address: owner.clone().into(),
+                    amount: start_amount,
+                }],
+                mint: None,
+                marketing: None,
+            },
+            "Cw20Base contract",
+        )
+        .unwrap();
+
+    // set allowance with height expiration at current block height
+    let block_height = app.block_info().height;
+    let expires = Expiration::AtHeight(block_height);
+
+    let err = contract
+        .increase_allowance(
+            &mut app,
+            &owner,
+            spender.to_string(),
+            allowance,
+            Some(expires),
+        )
+        .unwrap_err();
+
+    // ensure it is rejected
+    assert_eq!(ContractError::InvalidExpiration {}, err);
+
+    // set allowance with time expiration in the past
+    let block_time = app.block_info().time;
+    let expires = Expiration::AtTime(block_time.minus_seconds(1));
+
+    let err = contract
+        .increase_allowance(
+            &mut app,
+            &owner,
+            spender.to_string(),
+            allowance,
+            Some(expires),
+        )
+        .unwrap_err();
+
+    // ensure it is rejected
+    assert_eq!(ContractError::InvalidExpiration {}, err);
+
+    // set allowance with height expiration at next block height
+    let block_height = app.block_info().height + 1;
+    let expires = Expiration::AtHeight(block_height);
+
+    contract
+        .increase_allowance(
+            &mut app,
+            &owner,
+            spender.to_string(),
+            allowance,
+            Some(expires),
+        )
+        .unwrap();
+
+    // ensure it looks good
+    let allowance_resp = contract
+        .allowance(&app, owner.to_string(), spender.to_string())
+        .unwrap();
+
+    assert_eq!(allowance_resp, AllowanceResponse { allowance, expires });
+
+    // set allowance with time expiration in the future
+    let block_time = app.block_info().time;
+    let expires = Expiration::AtTime(block_time.plus_seconds(10));
+
+    contract
+        .increase_allowance(
+            &mut app,
+            &owner,
+            spender.to_string(),
+            allowance,
+            Some(expires),
+        )
+        .unwrap();
+
+    // ensure it looks good
+    let allowance_resp = contract
+        .allowance(&app, owner.to_string(), spender.to_string())
+        .unwrap();
+
+    assert_eq!(
+        allowance_resp,
+        AllowanceResponse {
+            allowance: allowance + allowance, // we increased twice
+            expires
+        }
+    );
+
+    // decrease with height expiration at current block height
+    let block_height = app.block_info().height;
+    let expires = Expiration::AtHeight(block_height);
+
+    let err = contract
+        .increase_allowance(
+            &mut app,
+            &owner,
+            spender.to_string(),
+            allowance,
+            Some(expires),
+        )
+        .unwrap_err();
+
+    // ensure it is rejected
+    assert_eq!(ContractError::InvalidExpiration {}, err);
+
+    // decrease with height expiration at next block height
+    let block_height = app.block_info().height + 1;
+    let expires = Expiration::AtHeight(block_height);
+
+    contract
+        .decrease_allowance(
+            &mut app,
+            &owner,
+            spender.to_string(),
+            allowance,
+            Some(expires),
+        )
+        .unwrap();
+
+    // ensure it looks good
+    let allowance_resp = contract
+        .allowance(&app, owner.to_string(), spender.to_string())
+        .unwrap();
+
+    assert_eq!(allowance_resp, AllowanceResponse { allowance, expires });
+}
+
+#[test]
+fn query_allowances() {
+    let mut app = App::default();
+
+    let owner = Addr::unchecked("addr0001");
+    let spender = Addr::unchecked("addr0002");
+    let spender2 = Addr::unchecked("addr0003");
+    let start_amount = Uint128::new(999999);
+    let allowance = Uint128::new(7777);
+
+    let code_id = Cw20BaseCodeId::store_code(&mut app);
+
+    let contract = code_id
+        .instantiate(
+            &mut app,
+            &owner,
+            InstantiateMsgData {
+                name: "Auto Gen".to_string(),
+                symbol: "AUTO".to_string(),
+                decimals: 3,
+                initial_balances: vec![Cw20Coin {
+                    address: owner.clone().into(),
+                    amount: start_amount,
+                }],
+                mint: None,
+                marketing: None,
+            },
+            "Cw20Base contract",
+        )
+        .unwrap();
+
+    // check all allowances
+    let all_allowances_resp = contract
+        .all_allowances(&app, owner.to_string(), None, None)
+        .unwrap();
+
+    assert_eq!(
+        all_allowances_resp,
+        AllAllowancesResponse { allowances: vec![] }
+    );
+
+    // increase spender allowances
+    contract
+        .increase_allowance(&mut app, &owner, spender.to_string(), allowance, None)
+        .unwrap();
+
+    // check all allowances
+    let all_allowances_resp = contract
+        .all_allowances(&app, owner.to_string(), None, None)
+        .unwrap();
+
+    assert_eq!(
+        all_allowances_resp,
+        AllAllowancesResponse {
+            allowances: vec![AllowanceInfo {
+                spender: spender.to_string(),
+                allowance,
+                expires: Expiration::Never {}
+            }]
+        }
+    );
+
+    // check spender allowances
+    let all_spender_allowances_resp = contract
+        .all_spender_allowances(&app, spender.to_string(), None, None)
+        .unwrap();
+
+    assert_eq!(
+        all_spender_allowances_resp,
+        AllSpenderAllowancesResponse {
+            allowances: vec![SpenderAllowanceInfo {
+                owner: owner.to_string(),
+                allowance,
+                expires: Expiration::Never {}
+            }]
+        }
+    );
+
+    // increase spender2 allowances
+    let increased_allowances = allowance + allowance;
+    contract
+        .increase_allowance(
+            &mut app,
+            &owner,
+            spender2.to_string(),
+            increased_allowances,
+            None,
+        )
+        .unwrap();
+
+    // check all allowances
+    let all_allowances_resp = contract
+        .all_allowances(&app, owner.to_string(), None, None)
+        .unwrap();
+
+    assert_eq!(
+        all_allowances_resp,
+        AllAllowancesResponse {
+            allowances: vec![
+                AllowanceInfo {
+                    spender: spender.to_string(),
+                    allowance,
+                    expires: Expiration::Never {}
+                },
+                AllowanceInfo {
+                    spender: spender2.to_string(),
+                    allowance: increased_allowances,
+                    expires: Expiration::Never {}
+                }
+            ]
+        }
+    );
+
+    // check all allowances with limit
+    let all_allowances_resp = contract
+        .all_allowances(&app, owner.to_string(), None, Some(1))
+        .unwrap();
+
+    assert_eq!(
+        all_allowances_resp,
+        AllAllowancesResponse {
+            allowances: vec![AllowanceInfo {
+                spender: spender.to_string(),
+                allowance,
+                expires: Expiration::Never {}
+            },]
+        }
+    );
+}
diff --git a/contracts/cw20-base/src/multitest/proxy.rs b/contracts/cw20-base/src/multitest/proxy.rs
new file mode 100644
index 0000000..b9a0267
--- /dev/null
+++ b/contracts/cw20-base/src/multitest/proxy.rs
@@ -0,0 +1,187 @@
+use cosmwasm_std::{Addr, Binary, StdResult, Uint128};
+
+use cw20_allowances::responses::{
+    AllAllowancesResponse, AllSpenderAllowancesResponse, AllowanceResponse,
+};
+use cw_multi_test::{App, Executor};
+use cw_utils::Expiration;
+
+use crate::contract::{Cw20Base, InstantiateMsg, InstantiateMsgData, QueryMsg};
+use crate::error::ContractError;
+use crate::responses::BalanceResponse;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct Cw20BaseCodeId(u64);
+
+impl Cw20BaseCodeId {
+    pub fn store_code(app: &mut App) -> Self {
+        let code_id = app.store_code(Box::new(Cw20Base::new()));
+        Self(code_id)
+    }
+
+    #[track_caller]
+    pub fn instantiate(
+        self,
+        app: &mut App,
+        sender: &Addr,
+        data: InstantiateMsgData,
+        label: &str,
+    ) -> Result<Cw20BaseProxy, ContractError> {
+        let msg = InstantiateMsg { data };
+
+        app.instantiate_contract(self.0, sender.clone(), &msg, &[], label, None)
+            .map_err(|err| err.downcast().unwrap())
+            .map(Cw20BaseProxy)
+    }
+}
+
+pub struct Cw20BaseProxy(Addr);
+
+impl Cw20BaseProxy {
+    pub fn balance(&self, app: &App, address: String) -> StdResult<BalanceResponse> {
+        let msg = QueryMsg::Balance { address };
+
+        app.wrap().query_wasm_smart(self.0.clone(), &msg)
+    }
+
+    // cw20-allowances
+    #[track_caller]
+    pub fn increase_allowance(
+        &self,
+        app: &mut App,
+        sender: &Addr,
+        spender: String,
+        amount: Uint128,
+        expires: Option<Expiration>,
+    ) -> Result<(), ContractError> {
+        let msg = cw20_allowances::ExecMsg::IncreaseAllowance {
+            spender,
+            amount,
+            expires,
+        };
+
+        app.execute_contract(sender.clone(), self.0.clone(), &msg, &[])
+            .map_err(|err| err.downcast().unwrap())
+            .map(|_| ())
+    }
+
+    #[track_caller]
+    pub fn decrease_allowance(
+        &self,
+        app: &mut App,
+        sender: &Addr,
+        spender: String,
+        amount: Uint128,
+        expires: Option<Expiration>,
+    ) -> Result<(), ContractError> {
+        let msg = cw20_allowances::ExecMsg::DecreaseAllowance {
+            spender,
+            amount,
+            expires,
+        };
+
+        app.execute_contract(sender.clone(), self.0.clone(), &msg, &[])
+            .map_err(|err| err.downcast().unwrap())
+            .map(|_| ())
+    }
+
+    #[track_caller]
+    pub fn transfer_from(
+        &self,
+        app: &mut App,
+        sender: &Addr,
+        owner: String,
+        recipient: String,
+        amount: Uint128,
+    ) -> Result<(), ContractError> {
+        let msg = cw20_allowances::ExecMsg::TransferFrom {
+            owner,
+            recipient,
+            amount,
+        };
+
+        app.execute_contract(sender.clone(), self.0.clone(), &msg, &[])
+            .map_err(|err| err.downcast().unwrap())
+            .map(|_| ())
+    }
+
+    #[track_caller]
+    pub fn send_from(
+        &self,
+        app: &mut App,
+        sender: &Addr,
+        owner: String,
+        contract: String,
+        amount: Uint128,
+        msg: Binary,
+    ) -> Result<(), ContractError> {
+        let msg = cw20_allowances::ExecMsg::SendFrom {
+            owner,
+            contract,
+            amount,
+            msg,
+        };
+
+        app.execute_contract(sender.clone(), self.0.clone(), &msg, &[])
+            .map_err(|err| err.downcast().unwrap())
+            .map(|_| ())
+    }
+
+    #[track_caller]
+    pub fn burn_from(
+        &self,
+        app: &mut App,
+        sender: &Addr,
+        owner: String,
+        amount: Uint128,
+    ) -> Result<(), ContractError> {
+        let msg = cw20_allowances::ExecMsg::BurnFrom { owner, amount };
+
+        app.execute_contract(sender.clone(), self.0.clone(), &msg, &[])
+            .map_err(|err| err.downcast().unwrap())
+            .map(|_| ())
+    }
+
+    pub fn allowance(
+        &self,
+        app: &App,
+        owner: String,
+        spender: String,
+    ) -> StdResult<AllowanceResponse> {
+        let msg = cw20_allowances::Cw20AllowancesQueryMsg::Allowance { owner, spender };
+
+        app.wrap().query_wasm_smart(self.0.clone(), &msg)
+    }
+
+    pub fn all_allowances(
+        &self,
+        app: &App,
+        owner: String,
+        start_after: Option<String>,
+        limit: Option<u32>,
+    ) -> StdResult<AllAllowancesResponse> {
+        let msg = cw20_allowances::Cw20AllowancesQueryMsg::AllAllowances {
+            owner,
+            start_after,
+            limit,
+        };
+
+        app.wrap().query_wasm_smart(self.0.clone(), &msg)
+    }
+
+    pub fn all_spender_allowances(
+        &self,
+        app: &App,
+        spender: String,
+        start_after: Option<String>,
+        limit: Option<u32>,
+    ) -> StdResult<AllSpenderAllowancesResponse> {
+        let msg = cw20_allowances::Cw20AllowancesQueryMsg::AllSpenderAllowances {
+            spender,
+            start_after,
+            limit,
+        };
+
+        app.wrap().query_wasm_smart(self.0.clone(), &msg)
+    }
+}
diff --git a/contracts/cw20-base/src/multitest/receiver.rs b/contracts/cw20-base/src/multitest/receiver.rs
new file mode 100644
index 0000000..002d367
--- /dev/null
+++ b/contracts/cw20-base/src/multitest/receiver.rs
@@ -0,0 +1,16 @@
+use cosmwasm_std::{Binary, DepsMut, Env, MessageInfo, Response, StdError, Uint128};
+use sylvia::interface;
+
+#[interface]
+pub trait Receiver {
+    type Error: From<StdError>;
+
+    #[msg(exec)]
+    fn receive(
+        &self,
+        ctx: (DepsMut, Env, MessageInfo),
+        sender: String,
+        amount: Uint128,
+        msg: Binary,
+    ) -> Result<Response, Self::Error>;
+}
diff --git a/contracts/cw20-base/src/multitest/receiver_contract.rs b/contracts/cw20-base/src/multitest/receiver_contract.rs
new file mode 100644
index 0000000..82e4841
--- /dev/null
+++ b/contracts/cw20-base/src/multitest/receiver_contract.rs
@@ -0,0 +1,139 @@
+use anyhow::{bail, Result as AnyResult};
+use cosmwasm_std::{
+    from_slice, Addr, DepsMut, Empty, Env, MessageInfo, Response, StdError, StdResult,
+};
+use cw_multi_test::{App, Contract, Executor};
+use sylvia::contract;
+use thiserror::Error;
+
+use super::receiver::{self, Receiver};
+
+pub struct ReceiverContract {}
+
+#[derive(Error, Debug, PartialEq)]
+pub enum ContractError {
+    #[error("{0}")]
+    Std(#[from] StdError),
+}
+
+#[contract]
+#[messages(receiver as Receiver)]
+impl ReceiverContract {
+    pub const fn new() -> Self {
+        Self {}
+    }
+    #[msg(instantiate)]
+    pub fn instantiate(&self, _ctx: (DepsMut, Env, MessageInfo)) -> StdResult<Response> {
+        Ok(Response::new())
+    }
+}
+
+impl Receiver for ReceiverContract {
+    type Error = ContractError;
+
+    fn receive(
+        &self,
+        _ctx: (DepsMut, Env, MessageInfo),
+        _sender: String,
+        _amount: cosmwasm_std::Uint128,
+        _msg: cosmwasm_std::Binary,
+    ) -> Result<Response, Self::Error> {
+        Ok(Response::default())
+    }
+}
+
+impl Contract<Empty> for ReceiverContract {
+    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")
+    }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct ReceiverContractCodeId(u64);
+
+impl ReceiverContractCodeId {
+    pub fn store_code(app: &mut App) -> Self {
+        let code_id = app.store_code(Box::new(ReceiverContract::new()));
+        Self(code_id)
+    }
+
+    #[track_caller]
+    pub fn instantiate(
+        self,
+        app: &mut App,
+        sender: &Addr,
+        label: &str,
+    ) -> Result<ReceiverContractProxy, ContractError> {
+        let msg = InstantiateMsg {};
+
+        app.instantiate_contract(self.0, sender.clone(), &msg, &[], label, None)
+            .map_err(|err| err.downcast().unwrap())
+            .map(ReceiverContractProxy)
+    }
+}
+
+pub struct ReceiverContractProxy(Addr);
+
+impl ReceiverContractProxy {
+    pub fn addr(&self) -> &Addr {
+        &self.0
+    }
+}