From 315094e89959d0d9ab766cb1f5caffcd656e31f0 Mon Sep 17 00:00:00 2001 From: Sturdy <91910406+apollo-sturdy@users.noreply.github.com> Date: Wed, 10 Jan 2024 20:54:44 +0100 Subject: [PATCH 1/5] feat: add stargate feature to cw3 --- contracts/cw3-fixed-multisig/Cargo.toml | 2 +- contracts/cw3-flex-multisig/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/cw3-fixed-multisig/Cargo.toml b/contracts/cw3-fixed-multisig/Cargo.toml index fa0cd97f7..367683f72 100644 --- a/contracts/cw3-fixed-multisig/Cargo.toml +++ b/contracts/cw3-fixed-multisig/Cargo.toml @@ -23,7 +23,7 @@ cw-utils = "1.0.1" cw2 = { path = "../../packages/cw2", version = "1.1.2" } cw3 = { path = "../../packages/cw3", version = "1.1.2" } cw-storage-plus = "1.1.0" -cosmwasm-std = { version = "1.4.0" } +cosmwasm-std = { version = "1.4.0", features = ["stargate"] } schemars = "0.8.15" serde = { version = "1.0.188", default-features = false, features = ["derive"] } thiserror = { version = "1.0.49" } diff --git a/contracts/cw3-flex-multisig/Cargo.toml b/contracts/cw3-flex-multisig/Cargo.toml index f25a684b9..4ff11a6c5 100644 --- a/contracts/cw3-flex-multisig/Cargo.toml +++ b/contracts/cw3-flex-multisig/Cargo.toml @@ -28,7 +28,7 @@ cw3-fixed-multisig = { path = "../cw3-fixed-multisig", version = "1.1.2", featur cw4 = { path = "../../packages/cw4", version = "1.1.2" } cw20 = { path = "../../packages/cw20", version = "1.1.2" } cw-storage-plus = "1.1.0" -cosmwasm-std = { version = "1.4.0" } +cosmwasm-std = { version = "1.4.0", features = ["stargate"] } schemars = "0.8.15" serde = { version = "1.0.188", default-features = false, features = ["derive"] } thiserror = { version = "1.0.49" } From 18c80ea049924842f94ae90ec5570b9deff203b5 Mon Sep 17 00:00:00 2001 From: Sturdy <91910406+apollo-sturdy@users.noreply.github.com> Date: Wed, 10 Jan 2024 20:57:42 +0100 Subject: [PATCH 2/5] feat: enable cosmwasm 1.1-1.4 features for cw3 --- contracts/cw3-fixed-multisig/Cargo.toml | 2 +- contracts/cw3-flex-multisig/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/cw3-fixed-multisig/Cargo.toml b/contracts/cw3-fixed-multisig/Cargo.toml index 367683f72..3fde4a1c6 100644 --- a/contracts/cw3-fixed-multisig/Cargo.toml +++ b/contracts/cw3-fixed-multisig/Cargo.toml @@ -23,7 +23,7 @@ cw-utils = "1.0.1" cw2 = { path = "../../packages/cw2", version = "1.1.2" } cw3 = { path = "../../packages/cw3", version = "1.1.2" } cw-storage-plus = "1.1.0" -cosmwasm-std = { version = "1.4.0", features = ["stargate"] } +cosmwasm-std = { version = "1.4.0", features = ["stargate", "cosmwasm_1_1", "cosmwasm_1_2", "cosmwasm_1_3", "cosmwasm_1_4"] } schemars = "0.8.15" serde = { version = "1.0.188", default-features = false, features = ["derive"] } thiserror = { version = "1.0.49" } diff --git a/contracts/cw3-flex-multisig/Cargo.toml b/contracts/cw3-flex-multisig/Cargo.toml index 4ff11a6c5..04276552b 100644 --- a/contracts/cw3-flex-multisig/Cargo.toml +++ b/contracts/cw3-flex-multisig/Cargo.toml @@ -28,7 +28,7 @@ cw3-fixed-multisig = { path = "../cw3-fixed-multisig", version = "1.1.2", featur cw4 = { path = "../../packages/cw4", version = "1.1.2" } cw20 = { path = "../../packages/cw20", version = "1.1.2" } cw-storage-plus = "1.1.0" -cosmwasm-std = { version = "1.4.0", features = ["stargate"] } +cosmwasm-std = { version = "1.4.0", features = ["stargate", "cosmwasm_1_1", "cosmwasm_1_2", "cosmwasm_1_3", "cosmwasm_1_4"] } schemars = "0.8.15" serde = { version = "1.0.188", default-features = false, features = ["derive"] } thiserror = { version = "1.0.49" } From 578efbda0049526219e75897e084039b039d04cc Mon Sep 17 00:00:00 2001 From: Sturdy <91910406+apollo-sturdy@users.noreply.github.com> Date: Wed, 10 Jan 2024 21:02:20 +0100 Subject: [PATCH 3/5] ci: build artifacts on ci --- .github/workflows/artifacts.yml | 35 +++++++++++++++++++++++++++ Makefile.toml | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 .github/workflows/artifacts.yml create mode 100644 Makefile.toml diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml new file mode 100644 index 000000000..4de007642 --- /dev/null +++ b/.github/workflows/artifacts.yml @@ -0,0 +1,35 @@ +name: Artifacts + +on: + push: + branches: + - master + - main + pull_request: + branches: + - master + - main + workflow_dispatch: + +env: + RUST_BACKTRACE: 1 + CARGO_TERM_COLOR: always + +jobs: + artifacts: + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v3 + + - name: Install cargo make + uses: davidB/rust-cargo-make@v1 + + - name: Compile contracts to wasm + run: cargo make rust-optimizer + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: artifacts + path: artifacts/ diff --git a/Makefile.toml b/Makefile.toml new file mode 100644 index 000000000..03c93e69e --- /dev/null +++ b/Makefile.toml @@ -0,0 +1,42 @@ +[config] +default_to_workspace = false +skip_core_tasks = true + +[env] +# If you bump this version, verify RUST_VERSION correctness +RUST_OPTIMIZER_VERSION = "0.13.0" +# Use rust version from rust-optimizer Dockerfile (see https://github.com/CosmWasm/rust-optimizer/blob/main/Dockerfile#L1) +# to be sure that we compile / test against the same version +RUST_VERSION = "1.69.0" +NIGHTLY_VERSION = "nightly-2023-08-29" + +[tasks.install-stable] +script = ''' +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain ${RUST_VERSION} +rustup target add wasm32-unknown-unknown --toolchain ${RUST_VERSION} +rustup component add rustfmt --toolchain ${RUST_VERSION} +rustup component add clippy --toolchain ${RUST_VERSION} +rustup component add llvm-tools-preview --toolchain ${RUST_VERSION} +''' + +[tasks.install-nightly] +script = ''' +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain ${NIGHTLY_VERSION} +rustup target add wasm32-unknown-unknown --toolchain ${NIGHTLY_VERSION} +rustup component add rustfmt --toolchain ${NIGHTLY_VERSION} +rustup component add clippy --toolchain ${NIGHTLY_VERSION} +''' + +[tasks.rust-optimizer] +cwd = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}" +script = """ +if [[ $(arch) == "arm64" ]]; then + image="cosmwasm/workspace-optimizer-arm64:${RUST_OPTIMIZER_VERSION}" +else + image="cosmwasm/workspace-optimizer:${RUST_OPTIMIZER_VERSION}" +fi +docker run --rm -v "$(pwd)":/code \ + --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + ${image} +""" From 13a60d6550c36f213008af37f3dda7a6d77f30e0 Mon Sep 17 00:00:00 2001 From: Sturdy <91910406+apollo-sturdy@users.noreply.github.com> Date: Wed, 10 Jan 2024 21:03:16 +0100 Subject: [PATCH 4/5] feat: delete unused contracts for faster building --- contracts/cw1-subkeys/.cargo/config | 6 - contracts/cw1-subkeys/Cargo.toml | 40 - contracts/cw1-subkeys/README.md | 93 - contracts/cw1-subkeys/src/bin/schema.rs | 13 - contracts/cw1-subkeys/src/contract.rs | 2295 ----------------- contracts/cw1-subkeys/src/error.rs | 63 - contracts/cw1-subkeys/src/lib.rs | 19 - contracts/cw1-subkeys/src/msg.rs | 190 -- contracts/cw1-subkeys/src/state.rs | 72 - contracts/cw1-whitelist/.cargo/config | 6 - contracts/cw1-whitelist/Cargo.toml | 36 - contracts/cw1-whitelist/README.md | 44 - contracts/cw1-whitelist/src/bin/schema.rs | 11 - contracts/cw1-whitelist/src/contract.rs | 312 --- contracts/cw1-whitelist/src/error.rs | 11 - .../cw1-whitelist/src/integration_tests.rs | 120 - contracts/cw1-whitelist/src/lib.rs | 28 - contracts/cw1-whitelist/src/msg.rs | 83 - contracts/cw1-whitelist/src/state.rs | 69 - contracts/cw20-base/.cargo/config | 6 - contracts/cw20-base/Cargo.toml | 33 - contracts/cw20-base/README.md | 48 - contracts/cw20-base/src/allowances.rs | 879 ------- contracts/cw20-base/src/bin/schema.rs | 11 - contracts/cw20-base/src/contract.rs | 2219 ---------------- contracts/cw20-base/src/enumerable.rs | 319 --- contracts/cw20-base/src/error.rs | 43 - contracts/cw20-base/src/lib.rs | 24 - contracts/cw20-base/src/msg.rs | 175 -- contracts/cw20-base/src/state.rs | 36 - contracts/cw20-ics20/.cargo/config | 6 - contracts/cw20-ics20/Cargo.toml | 31 - contracts/cw20-ics20/README.md | 66 - contracts/cw20-ics20/src/amount.rs | 66 - contracts/cw20-ics20/src/bin/schema.rs | 11 - contracts/cw20-ics20/src/contract.rs | 710 ----- contracts/cw20-ics20/src/error.rs | 80 - contracts/cw20-ics20/src/ibc.rs | 675 ----- contracts/cw20-ics20/src/lib.rs | 23 - contracts/cw20-ics20/src/migrations.rs | 86 - contracts/cw20-ics20/src/msg.rs | 129 - contracts/cw20-ics20/src/state.rs | 110 - contracts/cw20-ics20/src/test_helpers.rs | 86 - contracts/cw4-stake/.cargo/config | 5 - contracts/cw4-stake/Cargo.toml | 39 - contracts/cw4-stake/README.md | 77 - contracts/cw4-stake/src/bin/schema.rs | 11 - contracts/cw4-stake/src/contract.rs | 1015 -------- contracts/cw4-stake/src/error.rs | 40 - contracts/cw4-stake/src/lib.rs | 33 - contracts/cw4-stake/src/msg.rs | 82 - contracts/cw4-stake/src/state.rs | 32 - 52 files changed, 10717 deletions(-) delete mode 100644 contracts/cw1-subkeys/.cargo/config delete mode 100644 contracts/cw1-subkeys/Cargo.toml delete mode 100644 contracts/cw1-subkeys/README.md delete mode 100644 contracts/cw1-subkeys/src/bin/schema.rs delete mode 100644 contracts/cw1-subkeys/src/contract.rs delete mode 100644 contracts/cw1-subkeys/src/error.rs delete mode 100644 contracts/cw1-subkeys/src/lib.rs delete mode 100644 contracts/cw1-subkeys/src/msg.rs delete mode 100644 contracts/cw1-subkeys/src/state.rs delete mode 100644 contracts/cw1-whitelist/.cargo/config delete mode 100644 contracts/cw1-whitelist/Cargo.toml delete mode 100644 contracts/cw1-whitelist/README.md delete mode 100644 contracts/cw1-whitelist/src/bin/schema.rs delete mode 100644 contracts/cw1-whitelist/src/contract.rs delete mode 100644 contracts/cw1-whitelist/src/error.rs delete mode 100644 contracts/cw1-whitelist/src/integration_tests.rs delete mode 100644 contracts/cw1-whitelist/src/lib.rs delete mode 100644 contracts/cw1-whitelist/src/msg.rs delete mode 100644 contracts/cw1-whitelist/src/state.rs delete mode 100644 contracts/cw20-base/.cargo/config delete mode 100644 contracts/cw20-base/Cargo.toml delete mode 100644 contracts/cw20-base/README.md delete mode 100644 contracts/cw20-base/src/allowances.rs delete mode 100644 contracts/cw20-base/src/bin/schema.rs delete mode 100644 contracts/cw20-base/src/contract.rs delete mode 100644 contracts/cw20-base/src/enumerable.rs delete mode 100644 contracts/cw20-base/src/error.rs delete mode 100644 contracts/cw20-base/src/lib.rs delete mode 100644 contracts/cw20-base/src/msg.rs delete mode 100644 contracts/cw20-base/src/state.rs delete mode 100644 contracts/cw20-ics20/.cargo/config delete mode 100644 contracts/cw20-ics20/Cargo.toml delete mode 100644 contracts/cw20-ics20/README.md delete mode 100644 contracts/cw20-ics20/src/amount.rs delete mode 100644 contracts/cw20-ics20/src/bin/schema.rs delete mode 100644 contracts/cw20-ics20/src/contract.rs delete mode 100644 contracts/cw20-ics20/src/error.rs delete mode 100644 contracts/cw20-ics20/src/ibc.rs delete mode 100644 contracts/cw20-ics20/src/lib.rs delete mode 100644 contracts/cw20-ics20/src/migrations.rs delete mode 100644 contracts/cw20-ics20/src/msg.rs delete mode 100644 contracts/cw20-ics20/src/state.rs delete mode 100644 contracts/cw20-ics20/src/test_helpers.rs delete mode 100644 contracts/cw4-stake/.cargo/config delete mode 100644 contracts/cw4-stake/Cargo.toml delete mode 100644 contracts/cw4-stake/README.md delete mode 100644 contracts/cw4-stake/src/bin/schema.rs delete mode 100644 contracts/cw4-stake/src/contract.rs delete mode 100644 contracts/cw4-stake/src/error.rs delete mode 100644 contracts/cw4-stake/src/lib.rs delete mode 100644 contracts/cw4-stake/src/msg.rs delete mode 100644 contracts/cw4-stake/src/state.rs diff --git a/contracts/cw1-subkeys/.cargo/config b/contracts/cw1-subkeys/.cargo/config deleted file mode 100644 index f5174787c..000000000 --- a/contracts/cw1-subkeys/.cargo/config +++ /dev/null @@ -1,6 +0,0 @@ -[alias] -wasm = "build --release --lib --target wasm32-unknown-unknown" -wasm-debug = "build --lib --target wasm32-unknown-unknown" -unit-test = "test --lib" -integration-test = "test --test integration" -schema = "run --bin schema" diff --git a/contracts/cw1-subkeys/Cargo.toml b/contracts/cw1-subkeys/Cargo.toml deleted file mode 100644 index 61c56dbe2..000000000 --- a/contracts/cw1-subkeys/Cargo.toml +++ /dev/null @@ -1,40 +0,0 @@ -[package] -name = "cw1-subkeys" -version = "1.1.2" -authors = ["Ethan Frey "] -edition = "2021" -description = "Implement subkeys for authorizing native tokens as a cw1 proxy contract" -license = "Apache-2.0" -repository = "https://github.com/CosmWasm/cw-plus" -homepage = "https://cosmwasm.com" -documentation = "https://docs.cosmwasm.com" - -[lib] -crate-type = ["cdylib", "rlib"] - -[features] -backtraces = ["cosmwasm-std/backtraces"] -# use library feature to disable all instantiate/execute/query exports -library = [] -test-utils = [] - -[dependencies] -cosmwasm-schema = { version = "1.4.0" } -cw-utils = "1.0.1" -cw1 = { path = "../../packages/cw1", version = "1.1.2" } -cw2 = { path = "../../packages/cw2", version = "1.1.2" } -cw1-whitelist = { path = "../cw1-whitelist", version = "1.1.2", features = [ - "library", -] } -cosmwasm-std = { version = "1.4.0", features = ["staking"] } -cw-storage-plus = "1.1.0" -schemars = "0.8.15" -serde = { version = "1.0.188", default-features = false, features = ["derive"] } -thiserror = "1.0.49" -semver = "1" - -[dev-dependencies] -cw1-whitelist = { path = "../cw1-whitelist", version = "1.1.2", features = [ - "library", - "test-utils", -] } diff --git a/contracts/cw1-subkeys/README.md b/contracts/cw1-subkeys/README.md deleted file mode 100644 index dc7d3164a..000000000 --- a/contracts/cw1-subkeys/README.md +++ /dev/null @@ -1,93 +0,0 @@ -# CW1 Subkeys - -This builds on `cw1-whitelist` to provide the first non-trivial solution. -It still works like `cw1-whitelist` with a set of admins (typically 1) -which have full control of the account. However, you can then grant -a number of accounts allowances to send native tokens from this account. - -This was proposed in Summer 2019 for the Cosmos Hub and resembles the -functionality of ERC20 (allowances and transfer from). - -## Details - -Basically, any admin can add an allowance for a `(spender, denom)` pair -(similar to cw20 `IncreaseAllowance` / `DecreaseAllowance`). Any non-admin -account can try to execute a `CosmosMsg::Bank(BankMsg::Send{})` from this -contract and if they have the required allowances, their allowance will be -reduced and the send message relayed. If they don't have sufficient authorization, -or if they try to proxy any other message type, then the attempt will be rejected. -Admin can give permissions to subkeys to relay specific types of messages -(covers _Delegate, Undelegate, Redelegate, Withdraw_ for now). Subkeys have no permission -on creation, it can be setup with `SetupPermission` message. - -### Messages - -This adds 2 messages beyond the `cw1` spec: - -```rust -enum ExecuteMsg { - IncreaseAllowance { - spender: HumanAddr, - denom: String, - amount: Uint128, - expires: Option, - }, - DecreaseAllowance { - spender: HumanAddr, - denom: String, - amount: Uint128, - expires: Option, - }, - SetupPermissions { - spender: HumanAddr, - permissions: Permissions, - } -} -``` - -### Queries - -It also adds one more query type: - -```rust -enum QueryMsg { - Allowance { - spender: HumanAddr, - }, - AllAllowances { - start_after: Option, - limit: Option, - }, -} - -pub struct AllowanceInfo { - pub spender: HumanAddr, - pub balance: Balance, - pub expires: Expiration, - pub permissions: Permissions, -} - -pub struct AllAllowancesResponse { - pub allowances: Vec, -} -``` - -## Running this contract - -You will need Rust 1.44.1+ with `wasm32-unknown-unknown` target installed. - -You can run unit tests on this via: - -`cargo test` - -Once you are happy with the content, you can compile it to wasm via: - -``` -RUSTFLAGS='-C link-arg=-s' cargo wasm -cp ../../target/wasm32-unknown-unknown/release/cw1_subkeys.wasm . -ls -l cw1_subkeys.wasm -sha256sum cw1_subkeys.wasm -``` - -Or for a production-ready (optimized) build, run a build command in the -the repository root: https://github.com/CosmWasm/cw-plus#compiling. diff --git a/contracts/cw1-subkeys/src/bin/schema.rs b/contracts/cw1-subkeys/src/bin/schema.rs deleted file mode 100644 index 1616b5f58..000000000 --- a/contracts/cw1-subkeys/src/bin/schema.rs +++ /dev/null @@ -1,13 +0,0 @@ -use cosmwasm_schema::write_api; - -use cw1_subkeys::msg::{ExecuteMsg, QueryMsg}; - -use cw1_whitelist::msg::InstantiateMsg; - -fn main() { - write_api! { - instantiate: InstantiateMsg, - execute: ExecuteMsg, - query: QueryMsg, - } -} diff --git a/contracts/cw1-subkeys/src/contract.rs b/contracts/cw1-subkeys/src/contract.rs deleted file mode 100644 index 3a88caec2..000000000 --- a/contracts/cw1-subkeys/src/contract.rs +++ /dev/null @@ -1,2295 +0,0 @@ -use schemars::JsonSchema; -use std::fmt; -use std::ops::{AddAssign, Sub}; - -#[cfg(not(feature = "library"))] -use cosmwasm_std::entry_point; -use cosmwasm_std::{ - ensure, ensure_ne, to_binary, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, DistributionMsg, - Empty, Env, MessageInfo, Order, Response, StakingMsg, StdResult, -}; -use cw1::CanExecuteResponse; -use cw1_whitelist::{ - contract::{ - execute_freeze, execute_update_admins, instantiate as whitelist_instantiate, - query_admin_list, - }, - msg::InstantiateMsg, - state::ADMIN_LIST, -}; -use cw2::{get_contract_version, set_contract_version}; -use cw_storage_plus::Bound; -use cw_utils::Expiration; -use semver::Version; - -use crate::error::ContractError; -use crate::msg::{ - AllAllowancesResponse, AllPermissionsResponse, AllowanceInfo, ExecuteMsg, PermissionsInfo, - QueryMsg, -}; -use crate::state::{Allowance, Permissions, ALLOWANCES, PERMISSIONS}; - -// version info for migration info -const CONTRACT_NAME: &str = "crates.io:cw1-subkeys"; -const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn instantiate( - mut deps: DepsMut, - env: Env, - info: MessageInfo, - msg: InstantiateMsg, -) -> StdResult { - let result = whitelist_instantiate(deps.branch(), env, info, msg)?; - set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - Ok(result) -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn execute( - deps: DepsMut, - env: Env, - info: MessageInfo, - // Note: implement this function with different type to add support for custom messages - // and then import the rest of this contract code. - msg: ExecuteMsg, -) -> Result, ContractError> { - match msg { - ExecuteMsg::Execute { msgs } => execute_execute(deps, env, info, msgs), - ExecuteMsg::Freeze {} => Ok(execute_freeze(deps, env, info)?), - ExecuteMsg::UpdateAdmins { admins } => Ok(execute_update_admins(deps, env, info, admins)?), - ExecuteMsg::IncreaseAllowance { - spender, - amount, - expires, - } => execute_increase_allowance(deps, env, info, spender, amount, expires), - ExecuteMsg::DecreaseAllowance { - spender, - amount, - expires, - } => execute_decrease_allowance(deps, env, info, spender, amount, expires), - ExecuteMsg::SetPermissions { - spender, - permissions, - } => execute_set_permissions(deps, env, info, spender, permissions), - } -} - -pub fn execute_execute( - deps: DepsMut, - env: Env, - info: MessageInfo, - msgs: Vec>, -) -> Result, ContractError> -where - T: Clone + fmt::Debug + PartialEq + JsonSchema, -{ - let cfg = ADMIN_LIST.load(deps.storage)?; - - // Not an admin - need to check for permissions - if !cfg.is_admin(info.sender.as_ref()) { - for msg in &msgs { - match msg { - CosmosMsg::Staking(staking_msg) => { - let perm = PERMISSIONS.may_load(deps.storage, &info.sender)?; - let perm = perm.ok_or(ContractError::NotAllowed {})?; - check_staking_permissions(staking_msg, perm)?; - } - CosmosMsg::Distribution(distribution_msg) => { - let perm = PERMISSIONS.may_load(deps.storage, &info.sender)?; - let perm = perm.ok_or(ContractError::NotAllowed {})?; - check_distribution_permissions(distribution_msg, perm)?; - } - CosmosMsg::Bank(BankMsg::Send { - to_address: _, - amount, - }) => { - ALLOWANCES.update::<_, ContractError>(deps.storage, &info.sender, |allow| { - let mut allowance = allow.ok_or(ContractError::NoAllowance {})?; - ensure!( - !allowance.expires.is_expired(&env.block), - ContractError::NoAllowance {} - ); - - // Decrease allowance - allowance.balance = allowance.balance.sub(amount.clone())?; - Ok(allowance) - })?; - } - _ => { - return Err(ContractError::MessageTypeRejected {}); - } - } - } - } - // Relay messages - let res = Response::new() - .add_messages(msgs) - .add_attribute("action", "execute") - .add_attribute("owner", info.sender); - Ok(res) -} - -pub fn check_staking_permissions( - staking_msg: &StakingMsg, - permissions: Permissions, -) -> Result<(), ContractError> { - match staking_msg { - StakingMsg::Delegate { .. } => { - ensure!(permissions.delegate, ContractError::DelegatePerm {}); - } - StakingMsg::Undelegate { .. } => { - ensure!(permissions.undelegate, ContractError::UnDelegatePerm {}); - } - StakingMsg::Redelegate { .. } => { - ensure!(permissions.redelegate, ContractError::ReDelegatePerm {}); - } - _ => return Err(ContractError::UnsupportedMessage {}), - } - Ok(()) -} - -pub fn check_distribution_permissions( - distribution_msg: &DistributionMsg, - permissions: Permissions, -) -> Result<(), ContractError> { - match distribution_msg { - DistributionMsg::SetWithdrawAddress { .. } => { - ensure!(permissions.withdraw, ContractError::WithdrawAddrPerm {}); - } - DistributionMsg::WithdrawDelegatorReward { .. } => { - ensure!(permissions.withdraw, ContractError::WithdrawPerm {}); - } - _ => return Err(ContractError::UnsupportedMessage {}), - } - Ok(()) -} - -pub fn execute_increase_allowance( - deps: DepsMut, - env: Env, - info: MessageInfo, - spender: String, - amount: Coin, - expires: Option, -) -> Result, ContractError> -where - T: Clone + fmt::Debug + PartialEq + JsonSchema, -{ - let cfg = ADMIN_LIST.load(deps.storage)?; - ensure!(cfg.is_admin(&info.sender), ContractError::Unauthorized {}); - - let spender_addr = deps.api.addr_validate(&spender)?; - ensure_ne!( - info.sender, - spender_addr, - ContractError::CannotSetOwnAccount {} - ); - - ALLOWANCES.update::<_, ContractError>(deps.storage, &spender_addr, |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.add_assign(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) -} - -pub fn execute_decrease_allowance( - deps: DepsMut, - env: Env, - info: MessageInfo, - spender: String, - amount: Coin, - expires: Option, -) -> Result, ContractError> -where - T: Clone + fmt::Debug + PartialEq + JsonSchema, -{ - let cfg = ADMIN_LIST.load(deps.storage)?; - ensure!(cfg.is_admin(&info.sender), ContractError::Unauthorized {}); - - let spender_addr = deps.api.addr_validate(&spender)?; - ensure_ne!( - info.sender, - spender_addr, - ContractError::CannotSetOwnAccount {} - ); - - let allowance = - ALLOWANCES.update::<_, ContractError>(deps.storage, &spender_addr, |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() { - ALLOWANCES.remove(deps.storage, &spender_addr); - } - - 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) -} - -pub fn execute_set_permissions( - deps: DepsMut, - _env: Env, - info: MessageInfo, - spender: String, - perm: Permissions, -) -> Result, ContractError> -where - T: Clone + fmt::Debug + PartialEq + JsonSchema, -{ - let cfg = ADMIN_LIST.load(deps.storage)?; - ensure!(cfg.is_admin(&info.sender), ContractError::Unauthorized {}); - - let spender_addr = deps.api.addr_validate(&spender)?; - ensure_ne!( - info.sender, - spender_addr, - ContractError::CannotSetOwnAccount {} - ); - PERMISSIONS.save(deps.storage, &spender_addr, &perm)?; - - let res = Response::new() - .add_attribute("action", "set_permissions") - .add_attribute("owner", info.sender) - .add_attribute("spender", spender) - .add_attribute("permissions", perm.to_string()); - Ok(res) -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { - match msg { - QueryMsg::AdminList {} => to_binary(&query_admin_list(deps)?), - QueryMsg::Allowance { spender } => to_binary(&query_allowance(deps, env, spender)?), - QueryMsg::Permissions { spender } => to_binary(&query_permissions(deps, spender)?), - QueryMsg::CanExecute { sender, msg } => { - to_binary(&query_can_execute(deps, env, sender, msg)?) - } - QueryMsg::AllAllowances { start_after, limit } => { - to_binary(&query_all_allowances(deps, env, start_after, limit)?) - } - QueryMsg::AllPermissions { start_after, limit } => { - to_binary(&query_all_permissions(deps, start_after, limit)?) - } - } -} - -// if the subkey has no allowance, return an empty struct (not an error) -pub fn query_allowance(deps: Deps, env: Env, spender: String) -> StdResult { - // we can use unchecked here as it is a query - bad value means a miss, we never write it - let spender = deps.api.addr_validate(&spender)?; - let allow = ALLOWANCES - .may_load(deps.storage, &spender)? - .filter(|allow| !allow.expires.is_expired(&env.block)) - .unwrap_or_default(); - - Ok(allow) -} - -// if the subkey has no permissions, return an empty struct (not an error) -pub fn query_permissions(deps: Deps, spender: String) -> StdResult { - let spender = deps.api.addr_validate(&spender)?; - let permissions = PERMISSIONS - .may_load(deps.storage, &spender)? - .unwrap_or_default(); - Ok(permissions) -} - -fn query_can_execute( - deps: Deps, - env: Env, - sender: String, - msg: CosmosMsg, -) -> StdResult { - Ok(CanExecuteResponse { - can_execute: can_execute(deps, env, sender, msg)?, - }) -} - -// this can just return booleans and the query_can_execute wrapper creates the struct once, not on every path -fn can_execute(deps: Deps, env: Env, sender: String, msg: CosmosMsg) -> StdResult { - let cfg = ADMIN_LIST.load(deps.storage)?; - if cfg.is_admin(&sender) { - return Ok(true); - } - - let sender = deps.api.addr_validate(&sender)?; - match msg { - CosmosMsg::Bank(BankMsg::Send { amount, .. }) => { - // now we check if there is enough allowance for this message - let allowance = 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.sub(amount).is_ok()) - } - None => Ok(false), - } - } - CosmosMsg::Staking(staking_msg) => { - let perm_opt = PERMISSIONS.may_load(deps.storage, &sender)?; - match perm_opt { - Some(permission) => Ok(check_staking_permissions(&staking_msg, permission).is_ok()), - None => Ok(false), - } - } - CosmosMsg::Distribution(distribution_msg) => { - let perm_opt = PERMISSIONS.may_load(deps.storage, &sender)?; - match perm_opt { - Some(permission) => { - Ok(check_distribution_permissions(&distribution_msg, permission).is_ok()) - } - None => Ok(false), - } - } - _ => Ok(false), - } -} - -const MAX_LIMIT: u32 = 30; -const DEFAULT_LIMIT: u32 = 10; - -fn calc_limit(request: Option) -> usize { - request.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize -} - -// return a list of all allowances here -pub fn query_all_allowances( - deps: Deps, - env: Env, - start_after: Option, - limit: Option, -) -> StdResult { - let limit = calc_limit(limit); - // we use raw addresses here.... - let start = start_after.map(|s| Bound::ExclusiveRaw(s.into())); - - let allowances = 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.into(), - balance: allow.balance, - expires: allow.expires, - }) - }) - .collect::>>()?; - Ok(AllAllowancesResponse { allowances }) -} - -// return a list of all permissions here -pub fn query_all_permissions( - deps: Deps, - start_after: Option, - limit: Option, -) -> StdResult { - let limit = calc_limit(limit); - let start = start_after.map(|s| Bound::ExclusiveRaw(s.into())); - - let permissions = PERMISSIONS - .range(deps.storage, start, None, Order::Ascending) - .take(limit) - .map(|item| { - item.map(|(addr, perm)| PermissionsInfo { - spender: addr.into(), - permissions: perm, - }) - }) - .collect::>>()?; - Ok(AllPermissionsResponse { permissions }) -} - -// Migrate contract if version is lower than current version -#[entry_point] -pub fn migrate(deps: DepsMut, _env: Env, _msg: Empty) -> Result { - let version: Version = CONTRACT_VERSION.parse()?; - let storage_version: Version = get_contract_version(deps.storage)?.version.parse()?; - - if storage_version < version { - set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - - // If state structure changed in any contract version in the way migration is needed, it - // should occur here - } - - Ok(Response::new()) -} - -#[cfg(test)] -mod tests { - use cosmwasm_std::testing::{ - mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage, - }; - use cosmwasm_std::{coin, coins, OwnedDeps, StakingMsg, SubMsg, Timestamp}; - - use cw1_whitelist::msg::AdminListResponse; - use cw2::{get_contract_version, ContractVersion}; - use cw_utils::NativeBalance; - - use crate::state::Permissions; - - use std::collections::HashMap; - - use super::*; - - const OWNER: &str = "owner"; - - const ADMIN1: &str = "admin1"; - const ADMIN2: &str = "admin2"; - - const SPENDER1: &str = "spender1"; - const SPENDER2: &str = "spender2"; - const SPENDER3: &str = "spender3"; - const SPENDER4: &str = "spender4"; - - const TOKEN: &str = "token"; - const TOKEN1: &str = "token1"; - const TOKEN2: &str = "token2"; - - 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, - }; - - // Expiration constant working properly with default `mock_env` - const NON_EXPIRED_HEIGHT: Expiration = Expiration::AtHeight(22_222); - const NON_EXPIRED_TIME: Expiration = - Expiration::AtTime(Timestamp::from_nanos(2_571_797_419_879_305_533)); - - const EXPIRED_HEIGHT: Expiration = Expiration::AtHeight(10); - const EXPIRED_TIME: Expiration = Expiration::AtTime(Timestamp::from_nanos(100)); - - /// Helper structure for Suite configuration - #[derive(Default)] - struct SuiteConfig { - spenders: HashMap<&'static str, Spender>, - admins: Vec<&'static str>, - } - - impl SuiteConfig { - fn new() -> Self { - Self::default() - } - - fn init(self) -> Suite { - Suite::init_with_config(self) - } - - fn with_allowance(mut self, spender: &'static str, allowance: Coin) -> Self { - self.spenders - .entry(spender) - .or_default() - .allowances - .push(allowance); - self - } - - fn expire_allowances(mut self, spender: &'static str, expires: Expiration) -> Self { - let item = self.spenders.entry(spender).or_default(); - assert!( - item.allowances_expire.is_none(), - "Allowances expiration for spender {spender} already configured", - ); - item.allowances_expire = Some(expires); - self - } - - fn with_permissions(mut self, spender: &'static str, permissions: Permissions) -> Self { - let item = self.spenders.entry(spender).or_default(); - assert!( - item.permissions.is_none(), - "Permissions for spender {spender} already configured", - ); - item.permissions = Some(permissions); - self - } - - fn with_admin(mut self, admin: &'static str) -> Self { - self.admins.push(admin); - self - } - } - - #[derive(Default)] - struct Spender { - allowances: Vec, - allowances_expire: Option, - permissions: Option, - } - - /// Test suite helper unifying test initialization, keeping access to created data - struct Suite { - deps: OwnedDeps, - owner: MessageInfo, - } - - impl Suite { - /// Initializes test case using default config - fn init() -> Self { - Self::init_with_config(SuiteConfig::default()) - } - - /// Initialized test case using provided config - fn init_with_config(config: SuiteConfig) -> Self { - let mut deps = mock_dependencies(); - let admins = std::iter::once(OWNER) - .chain(config.admins) - .map(ToOwned::to_owned) - .collect(); - - let instantiate_msg = InstantiateMsg { - admins, - mutable: true, - }; - let owner = mock_info(OWNER, &[]); - - instantiate( - deps.as_mut().branch(), - mock_env(), - owner.clone(), - instantiate_msg, - ) - .unwrap(); - - for (name, spender) in config.spenders { - let Spender { - allowances, - allowances_expire: expires, - permissions, - } = spender; - - for amount in allowances { - let msg = ExecuteMsg::IncreaseAllowance { - spender: name.to_owned(), - amount, - expires, - }; - - // Extend block and time, so all alowances are set, even if expired in normal - // mock_env - let mut env = mock_env(); - env.block.time = Timestamp::from_nanos(0); - env.block.height = 0; - execute(deps.as_mut().branch(), env, owner.clone(), msg).unwrap(); - } - - if let Some(permissions) = permissions { - let msg = ExecuteMsg::SetPermissions { - spender: name.to_owned(), - permissions, - }; - execute(deps.as_mut().branch(), mock_env(), owner.clone(), msg).unwrap(); - } - } - - Self { deps, owner } - } - } - - /// 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 Suite { deps, .. } = Suite::init(); - - assert_eq!( - ContractVersion { - contract: CONTRACT_NAME.to_string(), - version: CONTRACT_VERSION.to_string(), - }, - get_contract_version(&deps.storage).unwrap() - ) - } - - mod allowance { - use super::*; - - #[test] - fn query() { - let Suite { deps, .. } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(1, TOKEN)) - .with_allowance(SPENDER2, coin(2, TOKEN)) - .init(); - - // Check allowances work for accounts with balances - let allowance = - query_allowance(deps.as_ref(), mock_env(), SPENDER1.to_owned()).unwrap(); - assert_eq!( - allowance, - Allowance { - balance: NativeBalance(vec![coin(1, TOKEN)]), - expires: Expiration::Never {}, - } - ); - let allowance = - query_allowance(deps.as_ref(), mock_env(), SPENDER2.to_owned()).unwrap(); - assert_eq!( - allowance, - Allowance { - balance: NativeBalance(vec![coin(2, TOKEN)]), - expires: Expiration::Never {}, - } - ); - - // Check allowances work for accounts with no balance - let allowance = - query_allowance(deps.as_ref(), mock_env(), SPENDER3.to_string()).unwrap(); - assert_eq!(allowance, Allowance::default()); - } - - #[test] - fn query_expired() { - let Suite { deps, .. } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(1, TOKEN)) - .expire_allowances(SPENDER1, EXPIRED_HEIGHT) - .init(); - - // Check allowances work for accounts with balances - let allowance = - query_allowance(deps.as_ref(), mock_env(), SPENDER1.to_owned()).unwrap(); - assert_eq!( - allowance, - Allowance { - balance: NativeBalance(vec![]), - expires: Expiration::Never {}, - } - ); - } - - #[test] - fn query_all() { - let s1_allow = coin(1234, TOKEN); - let s2_allow = coin(2345, TOKEN); - let s3_allow = coin(3456, TOKEN); - - let s2_expire = Expiration::Never {}; - let s3_expire = NON_EXPIRED_HEIGHT; - - let Suite { deps, .. } = SuiteConfig::new() - .with_allowance(SPENDER1, s1_allow.clone()) - .with_allowance(SPENDER2, s2_allow.clone()) - .expire_allowances(SPENDER2, s2_expire) - .with_allowance(SPENDER3, s3_allow.clone()) - .expire_allowances(SPENDER3, s3_expire) - // This allowance is already expired - should not occur in result - .with_allowance(SPENDER4, coin(2222, TOKEN)) - .expire_allowances(SPENDER4, EXPIRED_HEIGHT) - .init(); - - // let's try pagination. - // - // Check is tricky, as there is no guarantee about order expiration are received (as it is - // dependent at least on ordering of insertions), so to check if pagination works, all what - // can we do is to ensure parts are of expected size, and that collectively all allowances - // are returned. - let batch1 = query_all_allowances(deps.as_ref(), mock_env(), None, Some(2)) - .unwrap() - .allowances; - assert_eq!(2, batch1.len()); - - // now continue from after the last one - let batch2 = query_all_allowances( - deps.as_ref(), - mock_env(), - Some(batch1[1].spender.clone()), - Some(2), - ) - .unwrap() - .allowances; - assert_eq!(1, batch2.len()); - - let expected = vec![ - AllowanceInfo { - spender: SPENDER1.to_owned(), - balance: NativeBalance(vec![s1_allow]), - expires: Expiration::Never {}, // Not set, expected default - }, - AllowanceInfo { - spender: SPENDER2.to_owned(), - balance: NativeBalance(vec![s2_allow]), - expires: s2_expire, - }, - AllowanceInfo { - spender: SPENDER3.to_owned(), - balance: NativeBalance(vec![s3_allow]), - expires: s3_expire, - }, - ]; - - assert_sorted_eq!( - expected, - [batch1, batch2].concat(), - AllowanceInfo::cmp_by_spender - ); - } - } - - mod permissions { - use super::*; - - #[test] - fn query() { - let Suite { deps, .. } = SuiteConfig::new() - .with_permissions(SPENDER1, ALL_PERMS) - .with_permissions(SPENDER2, NO_PERMS) - .init(); - - let permissions = query_permissions(deps.as_ref(), SPENDER1.to_string()).unwrap(); - assert_eq!(permissions, ALL_PERMS); - - let permissions = query_permissions(deps.as_ref(), SPENDER2.to_string()).unwrap(); - assert_eq!(permissions, NO_PERMS); - - // no permission is set. should return false - let permissions = query_permissions(deps.as_ref(), SPENDER3.to_string()).unwrap(); - assert_eq!(permissions, NO_PERMS); - } - - #[test] - fn query_all() { - let Suite { deps, .. } = SuiteConfig::new() - .with_permissions(SPENDER1, ALL_PERMS) - .with_permissions(SPENDER2, NO_PERMS) - .with_permissions(SPENDER3, NO_PERMS) - .init(); - - // let's try pagination - let batch1 = query_all_permissions(deps.as_ref(), None, Some(2)) - .unwrap() - .permissions; - assert_eq!(batch1.len(), 2); - - let batch2 = - query_all_permissions(deps.as_ref(), Some(batch1[1].spender.clone()), Some(2)) - .unwrap() - .permissions; - assert_eq!(batch2.len(), 1); - - let expected = vec![ - PermissionsInfo { - spender: SPENDER1.to_owned(), - permissions: ALL_PERMS, - }, - PermissionsInfo { - spender: SPENDER2.to_owned(), - permissions: NO_PERMS, - }, - PermissionsInfo { - spender: SPENDER3.to_owned(), - permissions: NO_PERMS, - }, - ]; - - assert_sorted_eq!( - [batch1, batch2].concat(), - expected, - PermissionsInfo::cmp_by_spender - ); - } - } - - mod admins { - use super::*; - - #[test] - fn query() { - let Suite { deps, .. } = SuiteConfig::new().with_admin(ADMIN1).init(); - - // Verify - assert_eq!( - query_admin_list(deps.as_ref()).unwrap().canonical(), - AdminListResponse { - admins: vec![OWNER.to_owned(), ADMIN1.to_owned()], - mutable: true, - } - .canonical() - ); - } - - #[test] - fn update() { - let Suite { - mut deps, owner, .. - } = SuiteConfig::new().init(); - - let rsp = execute( - deps.as_mut(), - mock_env(), - owner, - ExecuteMsg::UpdateAdmins { - admins: vec![OWNER.to_owned(), ADMIN1.to_owned(), ADMIN2.to_owned()], - }, - ) - .unwrap(); - - assert_eq!(rsp.messages, vec![]); - assert!(rsp.events.is_empty()); - assert_eq!(rsp.data, None); - - assert_eq!( - query_admin_list(deps.as_ref()).unwrap().canonical(), - AdminListResponse { - admins: vec![OWNER.to_owned(), ADMIN1.to_owned(), ADMIN2.to_owned()], - mutable: true, - } - .canonical() - ); - } - - #[test] - fn non_owner_update() { - let Suite { mut deps, .. } = SuiteConfig::new().with_admin(ADMIN1).init(); - let info = mock_info(ADMIN1, &[]); - - let rsp = execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::UpdateAdmins { - admins: vec![OWNER.to_owned(), ADMIN1.to_owned(), ADMIN2.to_owned()], - }, - ) - .unwrap(); - - assert_eq!(rsp.messages, vec![]); - assert!(rsp.events.is_empty()); - assert_eq!(rsp.data, None); - - assert_eq!( - query_admin_list(deps.as_ref()).unwrap().canonical(), - AdminListResponse { - admins: vec![OWNER.to_owned(), ADMIN1.to_owned(), ADMIN2.to_owned()], - mutable: true, - } - .canonical() - ); - } - - #[test] - fn non_admin_fail_to_update() { - let Suite { mut deps, .. } = SuiteConfig::new().init(); - let info = mock_info(ADMIN1, &[]); - - execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::UpdateAdmins { - admins: vec![OWNER.to_owned(), ADMIN1.to_owned(), ADMIN2.to_owned()], - }, - ) - .unwrap_err(); - - assert_eq!( - query_admin_list(deps.as_ref()).unwrap().canonical(), - AdminListResponse { - admins: vec![OWNER.to_owned()], - mutable: true, - } - .canonical() - ); - } - - #[test] - fn removed_owner_fail_to_update() { - let Suite { - mut deps, owner, .. - } = SuiteConfig::new().init(); - - // Exact result not checked as it is verified in another test - execute( - deps.as_mut(), - mock_env(), - owner.clone(), - ExecuteMsg::UpdateAdmins { - admins: vec![ADMIN1.to_owned()], - }, - ) - .unwrap(); - - execute( - deps.as_mut(), - mock_env(), - owner, - ExecuteMsg::UpdateAdmins { - admins: vec![OWNER.to_owned(), ADMIN1.to_owned(), ADMIN2.to_owned()], - }, - ) - .unwrap_err(); - - assert_eq!( - query_admin_list(deps.as_ref()).unwrap().canonical(), - AdminListResponse { - admins: vec![ADMIN1.to_owned()], - mutable: true, - } - .canonical() - ); - } - } - - mod increase_allowance { - // TODO: Add cases with increasing expired allowances, and with adding allowance which is - // already expired - - use super::*; - - #[test] - fn existing_token() { - let Suite { - mut deps, owner, .. - } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(1, TOKEN1)) - .init(); - - let rsp = execute( - deps.as_mut(), - mock_env(), - owner, - ExecuteMsg::IncreaseAllowance { - spender: SPENDER1.to_owned(), - amount: coin(3, TOKEN1), - expires: None, - }, - ) - .unwrap(); - - assert_eq!(rsp.messages, vec![]); - assert!(rsp.events.is_empty()); - assert_eq!(rsp.data, None); - - assert_eq!( - query_all_allowances(deps.as_ref(), mock_env(), None, None) - .unwrap() - .canonical(), - AllAllowancesResponse { - allowances: vec![AllowanceInfo { - spender: SPENDER1.to_owned(), - balance: NativeBalance(vec![coin(4, TOKEN1)]), - expires: Expiration::Never {}, - }] - } - .canonical() - ); - } - - #[test] - fn with_expiration() { - let Suite { - mut deps, owner, .. - } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(1, TOKEN1)) - .init(); - - let mut env = mock_env(); - env.block.height = 2; - - let rsp = execute( - deps.as_mut(), - env, - owner, - ExecuteMsg::IncreaseAllowance { - spender: SPENDER1.to_owned(), - amount: coin(3, TOKEN1), - expires: Some(NON_EXPIRED_HEIGHT), - }, - ) - .unwrap(); - - assert_eq!(rsp.messages, vec![]); - assert!(rsp.events.is_empty()); - assert_eq!(rsp.data, None); - - assert_eq!( - query_all_allowances(deps.as_ref(), mock_env(), None, None) - .unwrap() - .canonical(), - AllAllowancesResponse { - allowances: vec![AllowanceInfo { - spender: SPENDER1.to_owned(), - balance: NativeBalance(vec![coin(4, TOKEN1)]), - expires: NON_EXPIRED_HEIGHT, - }] - } - .canonical() - ); - } - - #[test] - fn new_token_on_existing_spender() { - let Suite { - mut deps, owner, .. - } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(1, TOKEN1)) - .init(); - - let rsp = execute( - deps.as_mut(), - mock_env(), - owner, - ExecuteMsg::IncreaseAllowance { - spender: SPENDER1.to_owned(), - amount: coin(3, TOKEN2), - expires: None, - }, - ) - .unwrap(); - - assert_eq!(rsp.messages, vec![]); - assert!(rsp.events.is_empty()); - assert_eq!(rsp.data, None); - - assert_eq!( - query_all_allowances(deps.as_ref(), mock_env(), None, None) - .unwrap() - .canonical(), - AllAllowancesResponse { - allowances: vec![AllowanceInfo { - spender: SPENDER1.to_owned(), - balance: NativeBalance(vec![coin(1, TOKEN1), coin(3, TOKEN2)]), - expires: Expiration::Never {}, - }] - } - .canonical() - ); - } - - #[test] - fn new_spender() { - let Suite { - mut deps, owner, .. - } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(1, TOKEN1)) - .init(); - - let rsp = execute( - deps.as_mut(), - mock_env(), - owner, - ExecuteMsg::IncreaseAllowance { - spender: SPENDER2.to_owned(), - amount: coin(3, TOKEN1), - expires: None, - }, - ) - .unwrap(); - - assert_eq!(rsp.messages, vec![]); - assert!(rsp.events.is_empty()); - assert_eq!(rsp.data, None); - - assert_eq!( - query_all_allowances(deps.as_ref(), mock_env(), None, None) - .unwrap() - .canonical(), - AllAllowancesResponse { - allowances: vec![ - AllowanceInfo { - spender: SPENDER1.to_owned(), - balance: NativeBalance(vec![coin(1, TOKEN1)]), - expires: Expiration::Never {}, - }, - AllowanceInfo { - spender: SPENDER2.to_owned(), - balance: NativeBalance(vec![coin(3, TOKEN1)]), - expires: Expiration::Never {}, - } - ] - } - .canonical(), - ); - } - - #[test] - fn new_spender_with_expiration() { - let Suite { - mut deps, owner, .. - } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(1, TOKEN1)) - .init(); - - let rsp = execute( - deps.as_mut(), - mock_env(), - owner, - ExecuteMsg::IncreaseAllowance { - spender: SPENDER2.to_owned(), - amount: coin(3, TOKEN1), - expires: Some(NON_EXPIRED_HEIGHT), - }, - ) - .unwrap(); - - assert_eq!(rsp.messages, vec![]); - assert!(rsp.events.is_empty()); - assert_eq!(rsp.data, None); - - assert_eq!( - query_all_allowances(deps.as_ref(), mock_env(), None, None) - .unwrap() - .canonical(), - AllAllowancesResponse { - allowances: vec![ - AllowanceInfo { - spender: SPENDER1.to_owned(), - balance: NativeBalance(vec![coin(1, TOKEN1)]), - expires: Expiration::Never {}, - }, - AllowanceInfo { - spender: SPENDER2.to_owned(), - balance: NativeBalance(vec![coin(3, TOKEN1)]), - expires: NON_EXPIRED_HEIGHT, - } - ] - } - .canonical(), - ); - } - - #[test] - fn previous_expired() { - let Suite { - mut deps, owner, .. - } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(1, TOKEN1)) - .expire_allowances(SPENDER1, EXPIRED_HEIGHT) - .init(); - - let rsp = execute( - deps.as_mut(), - mock_env(), - owner, - ExecuteMsg::IncreaseAllowance { - spender: SPENDER1.to_owned(), - amount: coin(2, TOKEN2), - expires: Some(NON_EXPIRED_TIME), - }, - ) - .unwrap(); - - assert_eq!(rsp.messages, vec![]); - assert!(rsp.events.is_empty()); - assert_eq!(rsp.data, None); - - assert_eq!( - query_all_allowances(deps.as_ref(), mock_env(), None, None) - .unwrap() - .canonical(), - AllAllowancesResponse { - allowances: vec![AllowanceInfo { - spender: SPENDER1.to_owned(), - balance: NativeBalance(vec![coin(2, TOKEN2)]), - expires: NON_EXPIRED_TIME, - }] - } - .canonical(), - ); - } - - #[test] - fn set_expired() { - let Suite { - mut deps, owner, .. - } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(1, TOKEN1)) - .expire_allowances(SPENDER1, NON_EXPIRED_HEIGHT) - .init(); - - let rsp = execute( - deps.as_mut(), - mock_env(), - owner, - ExecuteMsg::IncreaseAllowance { - spender: SPENDER1.to_owned(), - amount: coin(2, TOKEN2), - expires: Some(EXPIRED_TIME), - }, - ); - assert_eq!( - rsp, - Err(ContractError::SettingExpiredAllowance(EXPIRED_TIME)) - ); - - assert_eq!( - query_all_allowances(deps.as_ref(), mock_env(), None, None) - .unwrap() - .canonical(), - AllAllowancesResponse { - allowances: vec![AllowanceInfo { - spender: SPENDER1.to_owned(), - balance: NativeBalance(vec![coin(1, TOKEN1)]), - expires: NON_EXPIRED_HEIGHT, - }] - } - .canonical(), - ); - } - - #[test] - fn update_expired_with_no_expiration() { - let Suite { - mut deps, owner, .. - } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(1, TOKEN1)) - .expire_allowances(SPENDER1, EXPIRED_HEIGHT) - .init(); - - execute( - deps.as_mut(), - mock_env(), - owner, - ExecuteMsg::IncreaseAllowance { - spender: SPENDER1.to_owned(), - amount: coin(2, TOKEN2), - expires: None, - }, - ) - .unwrap_err(); - - assert_eq!( - query_all_allowances(deps.as_ref(), mock_env(), None, None) - .unwrap() - .canonical(), - AllAllowancesResponse { allowances: vec![] }.canonical(), - ); - } - } - - mod decrease_allowances { - // TODO: Add cases with decreasing expired allowances, and with adding allowance which is - // already expired - - use super::*; - - #[test] - fn existing_token_partial() { - let Suite { - mut deps, owner, .. - } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(10, TOKEN1)) - .expire_allowances(SPENDER1, NON_EXPIRED_HEIGHT) - .init(); - - let rsp = execute( - deps.as_mut(), - mock_env(), - owner, - ExecuteMsg::DecreaseAllowance { - spender: SPENDER1.to_owned(), - amount: coin(4, TOKEN1), - expires: None, - }, - ) - .unwrap(); - - assert_eq!(rsp.messages, vec![]); - assert!(rsp.events.is_empty()); - assert_eq!(rsp.data, None); - - assert_eq!( - query_all_allowances(deps.as_ref(), mock_env(), None, None) - .unwrap() - .canonical(), - AllAllowancesResponse { - allowances: vec![AllowanceInfo { - spender: SPENDER1.to_owned(), - balance: NativeBalance(vec![coin(6, TOKEN1)]), - expires: NON_EXPIRED_HEIGHT, - }] - } - .canonical() - ); - } - - #[test] - fn existing_token_whole() { - let Suite { - mut deps, owner, .. - } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(10, TOKEN1)) - .with_allowance(SPENDER1, coin(20, TOKEN2)) - .expire_allowances(SPENDER1, NON_EXPIRED_HEIGHT) - .init(); - - let rsp = execute( - deps.as_mut(), - mock_env(), - owner, - ExecuteMsg::DecreaseAllowance { - spender: SPENDER1.to_owned(), - amount: coin(10, TOKEN1), - expires: None, - }, - ) - .unwrap(); - - assert_eq!(rsp.messages, vec![]); - assert!(rsp.events.is_empty()); - assert_eq!(rsp.data, None); - - assert_eq!( - query_all_allowances(deps.as_ref(), mock_env(), None, None) - .unwrap() - .canonical(), - AllAllowancesResponse { - allowances: vec![AllowanceInfo { - spender: SPENDER1.to_owned(), - balance: NativeBalance(vec![coin(20, TOKEN2)]), - expires: NON_EXPIRED_HEIGHT, - }] - } - .canonical() - ); - } - - #[test] - fn existing_token_underflow() { - let Suite { - mut deps, owner, .. - } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(10, TOKEN1)) - .with_allowance(SPENDER1, coin(20, TOKEN2)) - .expire_allowances(SPENDER1, NON_EXPIRED_HEIGHT) - .init(); - - let rsp = execute( - deps.as_mut(), - mock_env(), - owner, - ExecuteMsg::DecreaseAllowance { - spender: SPENDER1.to_owned(), - amount: coin(15, TOKEN1), - expires: None, - }, - ) - .unwrap(); - - assert_eq!(rsp.messages, vec![]); - assert!(rsp.events.is_empty()); - assert_eq!(rsp.data, None); - - assert_eq!( - query_all_allowances(deps.as_ref(), mock_env(), None, None) - .unwrap() - .canonical(), - AllAllowancesResponse { - allowances: vec![AllowanceInfo { - spender: SPENDER1.to_owned(), - balance: NativeBalance(vec![coin(20, TOKEN2)]), - expires: NON_EXPIRED_HEIGHT, - }] - } - .canonical() - ); - } - - #[test] - fn last_existing_token_whole() { - let Suite { - mut deps, owner, .. - } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(10, TOKEN1)) - .expire_allowances(SPENDER1, NON_EXPIRED_HEIGHT) - .init(); - - let rsp = execute( - deps.as_mut(), - mock_env(), - owner, - ExecuteMsg::DecreaseAllowance { - spender: SPENDER1.to_owned(), - amount: coin(10, TOKEN1), - expires: None, - }, - ) - .unwrap(); - - assert_eq!(rsp.messages, vec![]); - assert!(rsp.events.is_empty()); - assert_eq!(rsp.data, None); - - assert_eq!( - query_all_allowances(deps.as_ref(), mock_env(), None, None) - .unwrap() - .canonical(), - AllAllowancesResponse { allowances: vec![] }.canonical() - ); - } - - #[test] - fn with_expiration() { - let Suite { - mut deps, owner, .. - } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(10, TOKEN1)) - .expire_allowances(SPENDER1, NON_EXPIRED_HEIGHT) - .init(); - - let rsp = execute( - deps.as_mut(), - mock_env(), - owner, - ExecuteMsg::DecreaseAllowance { - spender: SPENDER1.to_owned(), - amount: coin(4, TOKEN1), - expires: Some(NON_EXPIRED_TIME), - }, - ) - .unwrap(); - - assert_eq!(rsp.messages, vec![]); - assert!(rsp.events.is_empty()); - assert_eq!(rsp.data, None); - - assert_eq!( - query_all_allowances(deps.as_ref(), mock_env(), None, None) - .unwrap() - .canonical(), - AllAllowancesResponse { - allowances: vec![AllowanceInfo { - spender: SPENDER1.to_owned(), - balance: NativeBalance(vec![coin(6, TOKEN1)]), - expires: NON_EXPIRED_TIME, - }] - } - .canonical() - ); - } - - #[test] - fn non_exisiting_token() { - let Suite { - mut deps, owner, .. - } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(10, TOKEN1)) - .expire_allowances(SPENDER1, NON_EXPIRED_HEIGHT) - .init(); - - execute( - deps.as_mut(), - mock_env(), - owner, - ExecuteMsg::DecreaseAllowance { - spender: SPENDER1.to_owned(), - amount: coin(4, TOKEN2), - expires: None, - }, - ) - .unwrap_err(); - - assert_eq!( - query_all_allowances(deps.as_ref(), mock_env(), None, None) - .unwrap() - .canonical(), - AllAllowancesResponse { - allowances: vec![AllowanceInfo { - spender: SPENDER1.to_owned(), - balance: NativeBalance(vec![coin(10, TOKEN1)]), - expires: NON_EXPIRED_HEIGHT, - }] - } - .canonical() - ); - } - - #[test] - fn non_exisiting_spender() { - let Suite { - mut deps, owner, .. - } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(10, TOKEN1)) - .init(); - - execute( - deps.as_mut(), - mock_env(), - owner, - ExecuteMsg::DecreaseAllowance { - spender: SPENDER2.to_owned(), - amount: coin(4, TOKEN1), - expires: None, - }, - ) - .unwrap_err(); - - assert_eq!( - query_all_allowances(deps.as_ref(), mock_env(), None, None) - .unwrap() - .canonical(), - AllAllowancesResponse { - allowances: vec![AllowanceInfo { - spender: SPENDER1.to_owned(), - balance: NativeBalance(vec![coin(10, TOKEN1)]), - expires: Expiration::Never {}, - }] - } - .canonical() - ); - } - - #[test] - fn previous_expired() { - let Suite { - mut deps, owner, .. - } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(3, TOKEN1)) - .expire_allowances(SPENDER1, EXPIRED_HEIGHT) - .init(); - - execute( - deps.as_mut(), - mock_env(), - owner, - ExecuteMsg::DecreaseAllowance { - spender: SPENDER1.to_owned(), - amount: coin(2, TOKEN1), - expires: Some(NON_EXPIRED_TIME), - }, - ) - .unwrap_err(); - - assert_eq!( - query_all_allowances(deps.as_ref(), mock_env(), None, None) - .unwrap() - .canonical(), - AllAllowancesResponse { allowances: vec![] }.canonical(), - ); - } - - #[test] - fn set_expired() { - let Suite { - mut deps, owner, .. - } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(3, TOKEN1)) - .expire_allowances(SPENDER1, NON_EXPIRED_HEIGHT) - .init(); - - execute( - deps.as_mut(), - mock_env(), - owner, - ExecuteMsg::IncreaseAllowance { - spender: SPENDER1.to_owned(), - amount: coin(2, TOKEN1), - expires: Some(EXPIRED_TIME), - }, - ) - .unwrap_err(); - - assert_eq!( - query_all_allowances(deps.as_ref(), mock_env(), None, None) - .unwrap() - .canonical(), - AllAllowancesResponse { - allowances: vec![AllowanceInfo { - spender: SPENDER1.to_owned(), - balance: NativeBalance(vec![coin(3, TOKEN1)]), - expires: NON_EXPIRED_HEIGHT, - }] - } - .canonical(), - ); - } - } - - mod spend { - use super::*; - - #[test] - fn with_allowance() { - let Suite { mut deps, .. } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(10, TOKEN1)) - .init(); - - let msgs = vec![BankMsg::Send { - to_address: SPENDER2.to_owned(), - amount: coins(6, TOKEN1), - } - .into()]; - - let info = mock_info(SPENDER1, &[]); - - let rsp = execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::Execute { msgs: msgs.clone() }, - ) - .unwrap(); - - assert_eq!( - rsp.messages, - msgs.into_iter().map(SubMsg::new).collect::>() - ); - assert!(rsp.events.is_empty()); - assert_eq!(rsp.data, None); - - assert_eq!( - query_all_allowances(deps.as_ref(), mock_env(), None, None) - .unwrap() - .canonical(), - AllAllowancesResponse { - allowances: vec![AllowanceInfo { - spender: SPENDER1.to_owned(), - balance: NativeBalance(vec![coin(4, TOKEN1)]), - expires: Expiration::Never {}, - }] - } - .canonical() - ); - } - - #[test] - fn without_allowance() { - let Suite { mut deps, .. } = Suite::init(); - - let msgs = vec![BankMsg::Send { - to_address: SPENDER2.to_owned(), - amount: coins(6, TOKEN1), - } - .into()]; - - let info = mock_info(SPENDER1, &[]); - - execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::Execute { msgs }, - ) - .unwrap_err(); - - assert_eq!( - query_all_allowances(deps.as_ref(), mock_env(), None, None) - .unwrap() - .canonical(), - AllAllowancesResponse { allowances: vec![] }.canonical() - ); - } - - #[test] - fn not_enough_allowance() { - let Suite { mut deps, .. } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(10, TOKEN1)) - .init(); - - let msgs = vec![BankMsg::Send { - to_address: SPENDER2.to_owned(), - amount: coins(20, TOKEN1), - } - .into()]; - - let info = mock_info(SPENDER1, &[]); - - execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::Execute { msgs }, - ) - .unwrap_err(); - - assert_eq!( - query_all_allowances(deps.as_ref(), mock_env(), None, None) - .unwrap() - .canonical(), - AllAllowancesResponse { - allowances: vec![AllowanceInfo { - spender: SPENDER1.to_owned(), - balance: NativeBalance(coins(10, TOKEN1)), - expires: Expiration::Never {} - }] - } - .canonical() - ); - } - - #[test] - fn time_allowance_expired() { - let Suite { mut deps, .. } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(10, TOKEN1)) - .expire_allowances(SPENDER1, EXPIRED_TIME) - .init(); - - let msgs = vec![BankMsg::Send { - to_address: SPENDER2.to_owned(), - amount: coins(6, TOKEN1), - } - .into()]; - - let info = mock_info(SPENDER1, &[]); - execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::Execute { msgs }, - ) - .unwrap_err(); - - assert_eq!( - query_all_allowances(deps.as_ref(), mock_env(), None, None) - .unwrap() - .canonical(), - AllAllowancesResponse { allowances: vec![] }.canonical() - ); - } - - #[test] - fn height_allowance_expired() { - let Suite { mut deps, .. } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(10, TOKEN1)) - .expire_allowances(SPENDER1, EXPIRED_HEIGHT) - .init(); - - let msgs = vec![BankMsg::Send { - to_address: SPENDER2.to_owned(), - amount: coins(6, TOKEN1), - } - .into()]; - - let info = mock_info(SPENDER1, &[]); - execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::Execute { msgs }, - ) - .unwrap_err(); - - assert_eq!( - query_all_allowances(deps.as_ref(), mock_env(), None, None) - .unwrap() - .canonical(), - AllAllowancesResponse { allowances: vec![] }.canonical() - ); - } - - #[test] - fn admin_without_allowance() { - let Suite { mut deps, .. } = SuiteConfig::new().with_admin(ADMIN1).init(); - - let msgs = vec![BankMsg::Send { - to_address: SPENDER2.to_owned(), - amount: coins(20, TOKEN1), - } - .into()]; - - let info = mock_info(ADMIN1, &[]); - - let rsp = execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::Execute { msgs: msgs.clone() }, - ) - .unwrap(); - - assert_eq!( - rsp.messages, - msgs.into_iter().map(SubMsg::new).collect::>() - ); - assert!(rsp.events.is_empty()); - assert_eq!(rsp.data, None); - - assert_eq!( - query_all_allowances(deps.as_ref(), mock_env(), None, None) - .unwrap() - .canonical(), - AllAllowancesResponse { allowances: vec![] }.canonical() - ); - } - } - - mod custom_msg { - use super::*; - - #[test] - fn admin() { - let Suite { mut deps, .. } = SuiteConfig::new().with_admin(ADMIN1).init(); - - let info = mock_info(ADMIN1, &[]); - - let msgs = vec![CosmosMsg::Custom(Empty {})]; - - let rsp = execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::Execute { msgs: msgs.clone() }, - ) - .unwrap(); - - assert_eq!( - rsp.messages, - msgs.into_iter().map(SubMsg::new).collect::>() - ); - assert!(rsp.events.is_empty()); - assert_eq!(rsp.data, None); - } - - #[test] - fn non_admin() { - let Suite { mut deps, .. } = SuiteConfig::new().with_admin(ADMIN1).init(); - - let info = mock_info(SPENDER1, &[]); - - let msgs = vec![CosmosMsg::Custom(Empty {})]; - - let _rsp = execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::Execute { msgs }, - ) - .unwrap_err(); - } - } - - mod staking_permission { - use super::*; - - #[test] - fn allowed() { - let Suite { mut deps, .. } = SuiteConfig::new() - .with_permissions(SPENDER1, ALL_PERMS) - .init(); - - let msgs = vec![ - StakingMsg::Delegate { - validator: "validator1".to_owned(), - amount: coin(10, TOKEN1), - } - .into(), - StakingMsg::Redelegate { - src_validator: "validator1".to_owned(), - dst_validator: "validator2".to_owned(), - amount: coin(15, TOKEN1), - } - .into(), - StakingMsg::Undelegate { - validator: "validator1".to_owned(), - amount: coin(10, TOKEN1), - } - .into(), - DistributionMsg::WithdrawDelegatorReward { - validator: "validator1".to_owned(), - } - .into(), - ]; - - for msg in msgs { - let msgs = vec![msg]; - let rsp = execute( - deps.as_mut(), - mock_env(), - mock_info(SPENDER1, &[]), - ExecuteMsg::Execute { msgs: msgs.clone() }, - ) - .unwrap(); - - assert_eq!( - rsp.messages, - msgs.into_iter().map(SubMsg::new).collect::>() - ); - assert!(rsp.events.is_empty()); - assert_eq!(rsp.data, None); - } - } - - #[test] - fn admin() { - let Suite { mut deps, .. } = SuiteConfig::new().with_admin(ADMIN1).init(); - - let msgs = vec![ - StakingMsg::Delegate { - validator: "validator1".to_owned(), - amount: coin(10, TOKEN1), - } - .into(), - StakingMsg::Redelegate { - src_validator: "validator1".to_owned(), - dst_validator: "validator2".to_owned(), - amount: coin(15, TOKEN1), - } - .into(), - StakingMsg::Undelegate { - validator: "validator1".to_owned(), - amount: coin(10, TOKEN1), - } - .into(), - DistributionMsg::WithdrawDelegatorReward { - validator: "validator1".to_owned(), - } - .into(), - ]; - - for msg in msgs { - let msgs = vec![msg]; - let rsp = execute( - deps.as_mut(), - mock_env(), - mock_info(ADMIN1, &[]), - ExecuteMsg::Execute { msgs: msgs.clone() }, - ) - .unwrap(); - - assert_eq!( - rsp.messages, - msgs.into_iter().map(SubMsg::new).collect::>() - ); - assert!(rsp.events.is_empty()); - assert_eq!(rsp.data, None); - } - } - - #[test] - fn reject() { - let Suite { mut deps, .. } = Suite::init(); - - let msgs = vec![ - StakingMsg::Delegate { - validator: "validator1".to_owned(), - amount: coin(10, TOKEN1), - } - .into(), - StakingMsg::Redelegate { - src_validator: "validator1".to_owned(), - dst_validator: "validator2".to_owned(), - amount: coin(15, TOKEN1), - } - .into(), - StakingMsg::Undelegate { - validator: "validator1".to_owned(), - amount: coin(10, TOKEN1), - } - .into(), - DistributionMsg::WithdrawDelegatorReward { - validator: "validator1".to_owned(), - } - .into(), - ]; - - for msg in msgs { - let msgs = vec![msg]; - execute( - deps.as_mut(), - mock_env(), - mock_info(SPENDER1, &[]), - ExecuteMsg::Execute { msgs }, - ) - .unwrap_err(); - } - } - } - - mod can_execute { - use super::*; - - #[test] - fn allowed() { - let Suite { deps, .. } = SuiteConfig::new() - .with_permissions(SPENDER1, ALL_PERMS) - .with_allowance(SPENDER1, coin(10, TOKEN1)) - .init(); - - let msgs: Vec = vec![ - BankMsg::Send { - to_address: SPENDER2.to_owned(), - amount: coins(5, TOKEN1), - } - .into(), - StakingMsg::Delegate { - validator: SPENDER2.to_owned(), - amount: coin(8, TOKEN), - } - .into(), - DistributionMsg::WithdrawDelegatorReward { - validator: SPENDER2.to_owned(), - } - .into(), - ]; - - for msg in msgs { - let resp = - query_can_execute(deps.as_ref(), mock_env(), SPENDER1.to_owned(), msg.clone()) - .unwrap(); - - assert_eq!( - resp, - CanExecuteResponse { can_execute: true }, - "Original message: {msg:#?}", - ); - } - } - - #[test] - fn not_enough_allowance() { - let Suite { deps, .. } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(10, TOKEN1)) - .init(); - - let msg: CosmosMsg = BankMsg::Send { - to_address: SPENDER2.to_owned(), - amount: coins(16, TOKEN1), - } - .into(); - - let resp = - query_can_execute(deps.as_ref(), mock_env(), SPENDER1.to_owned(), msg).unwrap(); - - assert_eq!(resp, CanExecuteResponse { can_execute: false }); - } - - #[test] - fn expired_allowance() { - let Suite { deps, .. } = SuiteConfig::new() - .with_allowance(SPENDER1, coin(10, TOKEN1)) - .expire_allowances(SPENDER1, EXPIRED_TIME) - .init(); - - let msg: CosmosMsg = BankMsg::Send { - to_address: SPENDER2.to_owned(), - amount: coins(5, TOKEN1), - } - .into(); - - let resp = - query_can_execute(deps.as_ref(), mock_env(), SPENDER1.to_owned(), msg).unwrap(); - - assert_eq!(resp, CanExecuteResponse { can_execute: false }); - } - - #[test] - fn missing_permissions() { - let Suite { deps, .. } = SuiteConfig::new() - .with_permissions(SPENDER1, NO_PERMS) - .init(); - - let msgs: Vec = vec![ - StakingMsg::Delegate { - validator: SPENDER2.to_owned(), - amount: coin(8, TOKEN), - } - .into(), - DistributionMsg::WithdrawDelegatorReward { - validator: SPENDER2.to_owned(), - } - .into(), - ]; - - for msg in msgs { - let resp = - query_can_execute(deps.as_ref(), mock_env(), SPENDER1.to_owned(), msg.clone()) - .unwrap(); - - assert_eq!( - resp, - CanExecuteResponse { can_execute: false }, - "Original message: {msg:#?}", - ); - } - } - - #[test] - fn custom() { - let Suite { deps, .. } = SuiteConfig::new() - .with_permissions(SPENDER1, ALL_PERMS) - .init(); - - let msg: CosmosMsg = CosmosMsg::Custom(Empty {}); - - let resp = - query_can_execute(deps.as_ref(), mock_env(), SPENDER1.to_owned(), msg).unwrap(); - - assert_eq!(resp, CanExecuteResponse { can_execute: false }); - } - - #[test] - fn admin() { - let Suite { deps, .. } = SuiteConfig::new().with_admin(ADMIN1).init(); - - let msgs = vec![ - BankMsg::Send { - to_address: SPENDER2.to_owned(), - amount: coins(5, TOKEN1), - } - .into(), - StakingMsg::Delegate { - validator: SPENDER2.to_owned(), - amount: coin(8, TOKEN), - } - .into(), - DistributionMsg::WithdrawDelegatorReward { - validator: SPENDER2.to_owned(), - } - .into(), - CosmosMsg::Custom(Empty {}), - ]; - - for msg in msgs { - let resp = - query_can_execute(deps.as_ref(), mock_env(), ADMIN1.to_owned(), msg.clone()) - .unwrap(); - - assert_eq!( - resp, - CanExecuteResponse { can_execute: true }, - "Original message: {msg:#?}", - ); - } - } - } - - // tests permissions and allowances are independent features and does not affect each other - #[test] - fn permissions_allowances_independent() { - let mut deps = mock_dependencies(); - - let owner = "admin0001"; - let admins = vec![owner.to_string()]; - - // spender1 has every permission to stake - let spender1 = "spender0001"; - let spender2 = "spender0002"; - let denom = "token1"; - let amount = 10000; - let coin = coin(amount, denom); - - let allow = Allowance { - balance: NativeBalance(vec![coin.clone()]), - expires: Expiration::Never {}, - }; - let perm = Permissions { - delegate: true, - redelegate: false, - undelegate: false, - withdraw: true, - }; - - let info = mock_info(owner, &[]); - // Instantiate a contract with admins - let instantiate_msg = InstantiateMsg { - admins, - mutable: true, - }; - instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); - - // setup permission and then allowance and check if changed - let setup_perm_msg = ExecuteMsg::SetPermissions { - spender: spender1.to_string(), - permissions: perm, - }; - execute(deps.as_mut(), mock_env(), info.clone(), setup_perm_msg).unwrap(); - - let setup_allowance_msg = ExecuteMsg::IncreaseAllowance { - spender: spender1.to_string(), - amount: coin.clone(), - expires: None, - }; - execute(deps.as_mut(), mock_env(), info.clone(), setup_allowance_msg).unwrap(); - - let res_perm = query_permissions(deps.as_ref(), spender1.to_string()).unwrap(); - assert_eq!(perm, res_perm); - let res_allow = query_allowance(deps.as_ref(), mock_env(), spender1.to_string()).unwrap(); - assert_eq!(allow, res_allow); - - // setup allowance and then permission and check if changed - let setup_allowance_msg = ExecuteMsg::IncreaseAllowance { - spender: spender2.to_string(), - amount: coin, - expires: None, - }; - execute(deps.as_mut(), mock_env(), info.clone(), setup_allowance_msg).unwrap(); - - let setup_perm_msg = ExecuteMsg::SetPermissions { - spender: spender2.to_string(), - permissions: perm, - }; - execute(deps.as_mut(), mock_env(), info, setup_perm_msg).unwrap(); - - let res_perm = query_permissions(deps.as_ref(), spender2.to_string()).unwrap(); - assert_eq!(perm, res_perm); - let res_allow = query_allowance(deps.as_ref(), mock_env(), spender2.to_string()).unwrap(); - assert_eq!(allow, res_allow); - } -} diff --git a/contracts/cw1-subkeys/src/error.rs b/contracts/cw1-subkeys/src/error.rs deleted file mode 100644 index acafc9376..000000000 --- a/contracts/cw1-subkeys/src/error.rs +++ /dev/null @@ -1,63 +0,0 @@ -use cosmwasm_std::StdError; -use cw_utils::Expiration; -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("No permissions for this account")] - NotAllowed {}, - - #[error("No allowance for this account")] - NoAllowance {}, - - #[error("Message type rejected")] - MessageTypeRejected {}, - - #[error("Delegate is not allowed")] - DelegatePerm {}, - - #[error("Re-delegate is not allowed")] - ReDelegatePerm {}, - - #[error("Un-delegate is not allowed")] - UnDelegatePerm {}, - - #[error("Withdraw is not allowed")] - WithdrawPerm {}, - - #[error("Set withdraw address is not allowed")] - WithdrawAddrPerm {}, - - #[error("Unsupported message")] - UnsupportedMessage {}, - - #[error("Allowance already expired while setting: {0}")] - SettingExpiredAllowance(Expiration), - - #[error("Semver parsing error: {0}")] - SemVer(String), -} - -impl From for ContractError { - fn from(err: cw1_whitelist::ContractError) -> Self { - match err { - cw1_whitelist::ContractError::Std(error) => ContractError::Std(error), - cw1_whitelist::ContractError::Unauthorized {} => ContractError::Unauthorized {}, - } - } -} - -impl From for ContractError { - fn from(err: semver::Error) -> Self { - Self::SemVer(err.to_string()) - } -} diff --git a/contracts/cw1-subkeys/src/lib.rs b/contracts/cw1-subkeys/src/lib.rs deleted file mode 100644 index 1c0c7c6a4..000000000 --- a/contracts/cw1-subkeys/src/lib.rs +++ /dev/null @@ -1,19 +0,0 @@ -/*! -This builds on [`cw1_whitelist`] to provide the first non-trivial solution. -It still works like [`cw1_whitelist`] with a set of admins (typically 1) -which have full control of the account. However, you can then grant -a number of accounts allowances to send native tokens from this account. - -This was proposed in Summer 2019 for the Cosmos Hub and resembles the -functionality of ERC20 (allowances and transfer from). - -For more information on this contract, please check out the -[README](https://github.com/CosmWasm/cw-plus/blob/main/contracts/cw1-subkeys/README.md). -*/ - -pub mod contract; -mod error; -pub mod msg; -pub mod state; - -pub use crate::error::ContractError; diff --git a/contracts/cw1-subkeys/src/msg.rs b/contracts/cw1-subkeys/src/msg.rs deleted file mode 100644 index a3f17a0cf..000000000 --- a/contracts/cw1-subkeys/src/msg.rs +++ /dev/null @@ -1,190 +0,0 @@ -use schemars::JsonSchema; - -use std::fmt; - -use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Coin, CosmosMsg, Empty}; -use cw_utils::{Expiration, NativeBalance}; - -use crate::state::Permissions; - -#[cw_serde] -pub enum ExecuteMsg -where - T: Clone + fmt::Debug + PartialEq + JsonSchema, -{ - /// Execute requests the contract to re-dispatch all these messages with the - /// contract's address as sender. Every implementation has it's own logic to - /// determine in - Execute { msgs: Vec> }, - /// Freeze will make a mutable contract immutable, must be called by an admin - Freeze {}, - /// UpdateAdmins will change the admin set of the contract, must be called by an existing admin, - /// and only works if the contract is mutable - UpdateAdmins { admins: Vec }, - - /// Add an allowance to a given subkey (subkey must not be admin) - IncreaseAllowance { - spender: String, - amount: Coin, - expires: Option, - }, - /// Decreases an allowance for a given subkey (subkey must not be admin) - DecreaseAllowance { - spender: String, - amount: Coin, - expires: Option, - }, - - // Setups up permissions for a given subkey. - SetPermissions { - spender: String, - permissions: Permissions, - }, -} - -#[cw_serde] -#[derive(QueryResponses)] -pub enum QueryMsg -where - T: Clone + fmt::Debug + PartialEq + JsonSchema, -{ - /// Shows all admins and whether or not it is mutable - #[returns(cw1_whitelist::msg::AdminListResponse)] - AdminList {}, - /// Get the current allowance for the given subkey (how much it can spend) - #[returns(crate::state::Allowance)] - Allowance { spender: String }, - /// Get the current permissions for the given subkey (how much it can spend) - #[returns(PermissionsInfo)] - Permissions { spender: String }, - /// Checks permissions of the caller on this proxy. - /// If CanExecute returns true then a call to `Execute` with the same message, - /// before any further state changes, should also succeed. - #[returns(cw1::CanExecuteResponse)] - CanExecute { sender: String, msg: CosmosMsg }, - /// Gets all Allowances for this contract - #[returns(AllAllowancesResponse)] - AllAllowances { - start_after: Option, - limit: Option, - }, - /// Gets all Permissions for this contract - #[returns(AllPermissionsResponse)] - AllPermissions { - start_after: Option, - limit: Option, - }, -} - -#[cw_serde] -pub struct AllAllowancesResponse { - pub allowances: Vec, -} - -#[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 - } -} - -#[cw_serde] -pub struct AllowanceInfo { - pub spender: String, - 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![AllowanceInfo { - /// spender: "spender2".to_owned(), - /// balance: NativeBalance(vec![coin(1, "token1")]), - /// expires: Expiration::Never {}, - /// }, AllowanceInfo { - /// spender: "spender1".to_owned(), - /// balance: NativeBalance(vec![coin(2, "token2")]), - /// expires: Expiration::Never {}, - /// }]; - /// - /// allows.sort_by(AllowanceInfo::cmp_by_spender); - /// - /// assert_eq!( - /// allows.into_iter().map(|allow| allow.spender).collect::>(), - /// 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 - } -} - -#[cw_serde] -pub struct PermissionsInfo { - pub spender: String, - 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!["spender1".to_owned(), "spender2".to_owned()] - /// ); - /// ``` - pub fn cmp_by_spender(left: &Self, right: &Self) -> std::cmp::Ordering { - left.spender.cmp(&right.spender) - } -} - -#[cw_serde] -pub struct AllPermissionsResponse { - pub permissions: Vec, -} diff --git a/contracts/cw1-subkeys/src/state.rs b/contracts/cw1-subkeys/src/state.rs deleted file mode 100644 index 2b621ea4a..000000000 --- a/contracts/cw1-subkeys/src/state.rs +++ /dev/null @@ -1,72 +0,0 @@ -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::fmt; - -use cosmwasm_std::Addr; -use cw_storage_plus::Map; -use cw_utils::{Expiration, NativeBalance}; - -// 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 - } -} - -pub const PERMISSIONS: Map<&Addr, Permissions> = Map::new("permissions"); -pub const ALLOWANCES: Map<&Addr, Allowance> = Map::new("allowances"); diff --git a/contracts/cw1-whitelist/.cargo/config b/contracts/cw1-whitelist/.cargo/config deleted file mode 100644 index f5174787c..000000000 --- a/contracts/cw1-whitelist/.cargo/config +++ /dev/null @@ -1,6 +0,0 @@ -[alias] -wasm = "build --release --lib --target wasm32-unknown-unknown" -wasm-debug = "build --lib --target wasm32-unknown-unknown" -unit-test = "test --lib" -integration-test = "test --test integration" -schema = "run --bin schema" diff --git a/contracts/cw1-whitelist/Cargo.toml b/contracts/cw1-whitelist/Cargo.toml deleted file mode 100644 index 7f4f3cbf3..000000000 --- a/contracts/cw1-whitelist/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "cw1-whitelist" -version = "1.1.2" -authors = ["Ethan Frey "] -edition = "2021" -description = "Implementation of an proxy contract using a whitelist" -license = "Apache-2.0" -repository = "https://github.com/CosmWasm/cw-plus" -homepage = "https://cosmwasm.com" -documentation = "https://docs.cosmwasm.com" - -[lib] -crate-type = ["cdylib", "rlib"] - -[features] -backtraces = ["cosmwasm-std/backtraces"] -# use library feature to disable all instantiate/execute/query exports -library = [] -test-utils = [] - -[dependencies] -cosmwasm-schema = { version = "1.4.0" } -cw-utils = "1.0.1" -cw1 = { path = "../../packages/cw1", version = "1.1.2" } -cw2 = { path = "../../packages/cw2", version = "1.1.2" } -cosmwasm-std = { version = "1.4.0", features = ["staking"] } -cw-storage-plus = "1.1.0" -schemars = "0.8.15" -serde = { version = "1.0.188", default-features = false, features = ["derive"] } -thiserror = { version = "1.0.49" } - -[dev-dependencies] -anyhow = "1" -assert_matches = "1" -cw-multi-test = "0.16.5" -derivative = "2" diff --git a/contracts/cw1-whitelist/README.md b/contracts/cw1-whitelist/README.md deleted file mode 100644 index 7a2e6d6cb..000000000 --- a/contracts/cw1-whitelist/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# CW1 Whitelist - -This may be the simplest implementation of CW1, a whitelist of addresses. -It contains a set of admins that are defined upon creation. -Any of those admins may `Execute` any message via the contract, -per the CW1 spec. - -To make this slightly less minimalistic, you can allow the admin set -to be mutable or immutable. If it is mutable, then any admin may -(a) change the admin set and (b) freeze it (making it immutable). - -While largely an example contract for CW1, this has various real-world use-cases, -such as a common account that is shared among multiple trusted devices, -or trading an entire account (used as 1 of 1 mutable). Most of the time, -this can be used as a framework to build your own, -more advanced cw1 implementations. - -## Allowing Custom Messages - -By default, this doesn't support `CustomMsg` in order to be fully generic -among blockchains. However, all types are Generic over `T`, and this is only -fixed in `handle`. You can import this contract and just redefine your `handle` -function, setting a different parameter to `ExecuteMsg`, and you can produce -a chain-specific message. - -## Running this contract - -You will need Rust 1.44.1+ with `wasm32-unknown-unknown` target installed. - -You can run unit tests on this via: - -`cargo test` - -Once you are happy with the content, you can compile it to wasm via: - -``` -RUSTFLAGS='-C link-arg=-s' cargo wasm -cp ../../target/wasm32-unknown-unknown/release/cw1_whitelist.wasm . -ls -l cw1_whitelist.wasm -sha256sum cw1_whitelist.wasm -``` - -Or for a production-ready (optimized) build, run a build command in -the repository root: https://github.com/CosmWasm/cw-plus#compiling. diff --git a/contracts/cw1-whitelist/src/bin/schema.rs b/contracts/cw1-whitelist/src/bin/schema.rs deleted file mode 100644 index e72e54623..000000000 --- a/contracts/cw1-whitelist/src/bin/schema.rs +++ /dev/null @@ -1,11 +0,0 @@ -use cosmwasm_schema::write_api; - -use cw1_whitelist::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; - -fn main() { - write_api! { - instantiate: InstantiateMsg, - execute: ExecuteMsg, - query: QueryMsg, - } -} diff --git a/contracts/cw1-whitelist/src/contract.rs b/contracts/cw1-whitelist/src/contract.rs deleted file mode 100644 index b13fea207..000000000 --- a/contracts/cw1-whitelist/src/contract.rs +++ /dev/null @@ -1,312 +0,0 @@ -use schemars::JsonSchema; -use std::fmt; - -#[cfg(not(feature = "library"))] -use cosmwasm_std::entry_point; -use cosmwasm_std::{ - to_binary, Addr, Api, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Response, - StdResult, -}; - -use cw1::CanExecuteResponse; -use cw2::set_contract_version; - -use crate::error::ContractError; -use crate::msg::{AdminListResponse, ExecuteMsg, InstantiateMsg, QueryMsg}; -use crate::state::{AdminList, ADMIN_LIST}; - -// version info for migration info -const CONTRACT_NAME: &str = "crates.io:cw1-whitelist"; -const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn instantiate( - deps: DepsMut, - _env: Env, - _info: MessageInfo, - msg: InstantiateMsg, -) -> StdResult { - set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - let cfg = AdminList { - admins: map_validate(deps.api, &msg.admins)?, - mutable: msg.mutable, - }; - ADMIN_LIST.save(deps.storage, &cfg)?; - Ok(Response::default()) -} - -pub fn map_validate(api: &dyn Api, admins: &[String]) -> StdResult> { - admins.iter().map(|addr| api.addr_validate(addr)).collect() -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn execute( - deps: DepsMut, - env: Env, - info: MessageInfo, - // Note: implement this function with different type to add support for custom messages - // and then import the rest of this contract code. - msg: ExecuteMsg, -) -> Result, ContractError> { - match msg { - ExecuteMsg::Execute { msgs } => execute_execute(deps, env, info, msgs), - ExecuteMsg::Freeze {} => execute_freeze(deps, env, info), - ExecuteMsg::UpdateAdmins { admins } => execute_update_admins(deps, env, info, admins), - } -} - -pub fn execute_execute( - deps: DepsMut, - _env: Env, - info: MessageInfo, - msgs: Vec>, -) -> Result, ContractError> -where - T: Clone + fmt::Debug + PartialEq + JsonSchema, -{ - if !can_execute(deps.as_ref(), info.sender.as_ref())? { - Err(ContractError::Unauthorized {}) - } else { - let res = Response::new() - .add_messages(msgs) - .add_attribute("action", "execute"); - Ok(res) - } -} - -pub fn execute_freeze( - deps: DepsMut, - _env: Env, - info: MessageInfo, -) -> Result { - let mut cfg = ADMIN_LIST.load(deps.storage)?; - if !cfg.can_modify(info.sender.as_ref()) { - Err(ContractError::Unauthorized {}) - } else { - cfg.mutable = false; - ADMIN_LIST.save(deps.storage, &cfg)?; - - let res = Response::new().add_attribute("action", "freeze"); - Ok(res) - } -} - -pub fn execute_update_admins( - deps: DepsMut, - _env: Env, - info: MessageInfo, - admins: Vec, -) -> Result { - let mut cfg = ADMIN_LIST.load(deps.storage)?; - if !cfg.can_modify(info.sender.as_ref()) { - Err(ContractError::Unauthorized {}) - } else { - cfg.admins = map_validate(deps.api, &admins)?; - ADMIN_LIST.save(deps.storage, &cfg)?; - - let res = Response::new().add_attribute("action", "update_admins"); - Ok(res) - } -} - -fn can_execute(deps: Deps, sender: &str) -> StdResult { - let cfg = ADMIN_LIST.load(deps.storage)?; - let can = cfg.is_admin(sender); - Ok(can) -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { - match msg { - QueryMsg::AdminList {} => to_binary(&query_admin_list(deps)?), - QueryMsg::CanExecute { sender, msg } => to_binary(&query_can_execute(deps, sender, msg)?), - } -} - -pub fn query_admin_list(deps: Deps) -> StdResult { - let cfg = ADMIN_LIST.load(deps.storage)?; - Ok(AdminListResponse { - admins: cfg.admins.into_iter().map(|a| a.into()).collect(), - mutable: cfg.mutable, - }) -} - -pub fn query_can_execute( - deps: Deps, - sender: String, - _msg: CosmosMsg, -) -> StdResult { - Ok(CanExecuteResponse { - can_execute: can_execute(deps, &sender)?, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - use cosmwasm_std::{coin, coins, BankMsg, StakingMsg, SubMsg, WasmMsg}; - - #[test] - fn instantiate_and_modify_config() { - let mut deps = mock_dependencies(); - - let alice = "alice"; - let bob = "bob"; - let carl = "carl"; - - let anyone = "anyone"; - - // instantiate the contract - let instantiate_msg = InstantiateMsg { - admins: vec![alice.to_string(), bob.to_string(), carl.to_string()], - mutable: true, - }; - let info = mock_info(anyone, &[]); - instantiate(deps.as_mut(), mock_env(), info, instantiate_msg).unwrap(); - - // ensure expected config - let expected = AdminListResponse { - admins: vec![alice.to_string(), bob.to_string(), carl.to_string()], - mutable: true, - }; - assert_eq!(query_admin_list(deps.as_ref()).unwrap(), expected); - - // anyone cannot modify the contract - let msg = ExecuteMsg::UpdateAdmins { - admins: vec![anyone.to_string()], - }; - let info = mock_info(anyone, &[]); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!(err, ContractError::Unauthorized {}); - - // but alice can kick out carl - let msg = ExecuteMsg::UpdateAdmins { - admins: vec![alice.to_string(), bob.to_string()], - }; - let info = mock_info(alice, &[]); - execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - // ensure expected config - let expected = AdminListResponse { - admins: vec![alice.to_string(), bob.to_string()], - mutable: true, - }; - assert_eq!(query_admin_list(deps.as_ref()).unwrap(), expected); - - // carl cannot freeze it - let info = mock_info(carl, &[]); - let err = execute(deps.as_mut(), mock_env(), info, ExecuteMsg::Freeze {}).unwrap_err(); - assert_eq!(err, ContractError::Unauthorized {}); - - // but bob can - let info = mock_info(bob, &[]); - execute(deps.as_mut(), mock_env(), info, ExecuteMsg::Freeze {}).unwrap(); - let expected = AdminListResponse { - admins: vec![alice.to_string(), bob.to_string()], - mutable: false, - }; - assert_eq!(query_admin_list(deps.as_ref()).unwrap(), expected); - - // and now alice cannot change it again - let msg = ExecuteMsg::UpdateAdmins { - admins: vec![alice.to_string()], - }; - let info = mock_info(alice, &[]); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!(err, ContractError::Unauthorized {}); - } - - #[test] - fn execute_messages_has_proper_permissions() { - let mut deps = mock_dependencies(); - - let alice = "alice"; - let bob = "bob"; - let carl = "carl"; - - // instantiate the contract - let instantiate_msg = InstantiateMsg { - admins: vec![alice.to_string(), carl.to_string()], - mutable: false, - }; - let info = mock_info(bob, &[]); - instantiate(deps.as_mut(), mock_env(), info, instantiate_msg).unwrap(); - - let freeze: ExecuteMsg = ExecuteMsg::Freeze {}; - let msgs = vec![ - BankMsg::Send { - to_address: bob.to_string(), - amount: coins(10000, "DAI"), - } - .into(), - WasmMsg::Execute { - contract_addr: "some contract".into(), - msg: to_binary(&freeze).unwrap(), - funds: vec![], - } - .into(), - ]; - - // make some nice message - let execute_msg = ExecuteMsg::Execute { msgs: msgs.clone() }; - - // bob cannot execute them - let info = mock_info(bob, &[]); - let err = execute(deps.as_mut(), mock_env(), info, execute_msg.clone()).unwrap_err(); - assert_eq!(err, ContractError::Unauthorized {}); - - // but carl can - let info = mock_info(carl, &[]); - let res = execute(deps.as_mut(), mock_env(), info, execute_msg).unwrap(); - assert_eq!( - res.messages, - msgs.into_iter().map(SubMsg::new).collect::>() - ); - assert_eq!(res.attributes, [("action", "execute")]); - } - - #[test] - fn can_execute_query_works() { - let mut deps = mock_dependencies(); - - let alice = "alice"; - let bob = "bob"; - - let anyone = "anyone"; - - // instantiate the contract - let instantiate_msg = InstantiateMsg { - admins: vec![alice.to_string(), bob.to_string()], - mutable: false, - }; - let info = mock_info(anyone, &[]); - instantiate(deps.as_mut(), mock_env(), info, instantiate_msg).unwrap(); - - // let us make some queries... different msg types by owner and by other - let send_msg = CosmosMsg::Bank(BankMsg::Send { - to_address: anyone.to_string(), - amount: coins(12345, "ushell"), - }); - let staking_msg = CosmosMsg::Staking(StakingMsg::Delegate { - validator: anyone.to_string(), - amount: coin(70000, "ureef"), - }); - - // owner can send - let res = query_can_execute(deps.as_ref(), alice.to_string(), send_msg.clone()).unwrap(); - assert!(res.can_execute); - - // owner can stake - let res = query_can_execute(deps.as_ref(), bob.to_string(), staking_msg.clone()).unwrap(); - assert!(res.can_execute); - - // anyone cannot send - let res = query_can_execute(deps.as_ref(), anyone.to_string(), send_msg).unwrap(); - assert!(!res.can_execute); - - // anyone cannot stake - let res = query_can_execute(deps.as_ref(), anyone.to_string(), staking_msg).unwrap(); - assert!(!res.can_execute); - } -} diff --git a/contracts/cw1-whitelist/src/error.rs b/contracts/cw1-whitelist/src/error.rs deleted file mode 100644 index 9a5e44903..000000000 --- a/contracts/cw1-whitelist/src/error.rs +++ /dev/null @@ -1,11 +0,0 @@ -use cosmwasm_std::StdError; -use thiserror::Error; - -#[derive(Error, Debug, PartialEq)] -pub enum ContractError { - #[error("{0}")] - Std(#[from] StdError), - - #[error("Unauthorized")] - Unauthorized {}, -} diff --git a/contracts/cw1-whitelist/src/integration_tests.rs b/contracts/cw1-whitelist/src/integration_tests.rs deleted file mode 100644 index eeb3b5a3b..000000000 --- a/contracts/cw1-whitelist/src/integration_tests.rs +++ /dev/null @@ -1,120 +0,0 @@ -use crate::msg::{AdminListResponse, ExecuteMsg, InstantiateMsg, QueryMsg}; -use anyhow::{anyhow, Result}; -use assert_matches::assert_matches; -use cosmwasm_std::{to_binary, Addr, CosmosMsg, Empty, QueryRequest, StdError, WasmMsg, WasmQuery}; -use cw1::Cw1Contract; -use cw_multi_test::{App, AppResponse, Contract, ContractWrapper, Executor}; -use derivative::Derivative; -use serde::{de::DeserializeOwned, Serialize}; - -fn mock_app() -> App { - App::default() -} - -fn contract_cw1() -> Box> { - let contract = ContractWrapper::new( - crate::contract::execute, - crate::contract::instantiate, - crate::contract::query, - ); - Box::new(contract) -} - -#[derive(Derivative)] -#[derivative(Debug)] -pub struct Suite { - /// Application mock - #[derivative(Debug = "ignore")] - app: App, - /// Special account - pub owner: String, - /// ID of stored code for cw1 contract - cw1_id: u64, -} - -impl Suite { - pub fn init() -> Result { - let mut app = mock_app(); - let owner = "owner".to_owned(); - let cw1_id = app.store_code(contract_cw1()); - - Ok(Suite { app, owner, cw1_id }) - } - - pub fn instantiate_cw1_contract(&mut self, admins: Vec, mutable: bool) -> Cw1Contract { - let contract = self - .app - .instantiate_contract( - self.cw1_id, - Addr::unchecked(self.owner.clone()), - &InstantiateMsg { admins, mutable }, - &[], - "Whitelist", - None, - ) - .unwrap(); - Cw1Contract(contract) - } - - pub fn execute( - &mut self, - sender_contract: Addr, - target_contract: &Addr, - msg: M, - ) -> Result - where - M: Serialize + DeserializeOwned, - { - let execute: ExecuteMsg = ExecuteMsg::Execute { - msgs: vec![CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: target_contract.to_string(), - msg: to_binary(&msg)?, - funds: vec![], - })], - }; - self.app - .execute_contract( - Addr::unchecked(self.owner.clone()), - sender_contract, - &execute, - &[], - ) - .map_err(|err| anyhow!(err)) - } - - pub fn query(&self, target_contract: Addr, msg: M) -> Result - where - M: Serialize + DeserializeOwned, - { - self.app.wrap().query(&QueryRequest::Wasm(WasmQuery::Smart { - contract_addr: target_contract.to_string(), - msg: to_binary(&msg).unwrap(), - })) - } -} - -#[test] -fn proxy_freeze_message() { - let mut suite = Suite::init().unwrap(); - - let first_contract = suite.instantiate_cw1_contract(vec![suite.owner.clone()], true); - let second_contract = - suite.instantiate_cw1_contract(vec![first_contract.addr().to_string()], true); - assert_ne!(second_contract, first_contract); - - let freeze_msg: ExecuteMsg = ExecuteMsg::Freeze {}; - assert_matches!( - suite.execute(first_contract.addr(), &second_contract.addr(), freeze_msg), - Ok(_) - ); - - let query_msg: QueryMsg = QueryMsg::AdminList {}; - assert_matches!( - suite.query(second_contract.addr(), query_msg), - Ok( - AdminListResponse { - mutable, - .. - }) if !mutable - ); -} diff --git a/contracts/cw1-whitelist/src/lib.rs b/contracts/cw1-whitelist/src/lib.rs deleted file mode 100644 index a78fdf9bf..000000000 --- a/contracts/cw1-whitelist/src/lib.rs +++ /dev/null @@ -1,28 +0,0 @@ -/*! -This may be the simplest implementation of [CW1](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw1/README.md), a whitelist of addresses. -It contains a set of admins that are defined upon creation. -Any of those admins may `Execute` any message via the contract, -per the CW1 spec. - -To make this slighly less minimalistic, you can allow the admin set -to be mutable or immutable. If it is mutable, then any admin may -(a) change the admin set and (b) freeze it (making it immutable). - -While largely an example contract for CW1, this has various real-world use-cases, -such as a common account that is shared among multiple trusted devices, -or trading an entire account (used as 1 of 1 mutable). Most of the time, -this can be used as a framework to build your own, -more advanced cw1 implementations. - -For more information on this contract, please check out the -[README](https://github.com/CosmWasm/cw-plus/blob/main/contracts/cw1-whitelist/README.md). -*/ - -pub mod contract; -pub mod error; -#[cfg(test)] -mod integration_tests; -pub mod msg; -pub mod state; - -pub use crate::error::ContractError; diff --git a/contracts/cw1-whitelist/src/msg.rs b/contracts/cw1-whitelist/src/msg.rs deleted file mode 100644 index 8adc0113a..000000000 --- a/contracts/cw1-whitelist/src/msg.rs +++ /dev/null @@ -1,83 +0,0 @@ -use schemars::JsonSchema; - -use std::fmt; - -use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{CosmosMsg, Empty}; - -#[cw_serde] -pub struct InstantiateMsg { - pub admins: Vec, - pub mutable: bool, -} - -#[cw_serde] -pub enum ExecuteMsg -where - T: Clone + fmt::Debug + PartialEq + JsonSchema, -{ - /// Execute requests the contract to re-dispatch all these messages with the - /// contract's address as sender. Every implementation has it's own logic to - /// determine in - Execute { msgs: Vec> }, - /// Freeze will make a mutable contract immutable, must be called by an admin - Freeze {}, - /// UpdateAdmins will change the admin set of the contract, must be called by an existing admin, - /// and only works if the contract is mutable - UpdateAdmins { admins: Vec }, -} - -#[cw_serde] -#[derive(QueryResponses)] -pub enum QueryMsg -where - T: Clone + fmt::Debug + PartialEq + JsonSchema, -{ - /// Shows all admins and whether or not it is mutable - #[returns(AdminListResponse)] - AdminList {}, - /// Checks permissions of the caller on this proxy. - /// If CanExecute returns true then a call to `Execute` with the same message, - /// before any further state changes, should also succeed. - #[returns(cw1::CanExecuteResponse)] - CanExecute { sender: String, msg: CosmosMsg }, -} - -#[cw_serde] -pub struct AdminListResponse { - pub admins: Vec, - pub mutable: bool, -} - -#[cfg(any(test, feature = "test-utils"))] -impl AdminListResponse { - /// 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 quickly, so it seems to be reasonable to keep it as representation-equality, and - /// canonicalize message only when it is needed - /// - /// Example: - /// - /// ``` - /// # use cw1_whitelist::msg::AdminListResponse; - /// - /// let resp1 = AdminListResponse { - /// admins: vec!["admin1".to_owned(), "admin2".to_owned()], - /// mutable: true, - /// }; - /// - /// let resp2 = AdminListResponse { - /// admins: vec!["admin2".to_owned(), "admin1".to_owned(), "admin2".to_owned()], - /// mutable: true, - /// }; - /// - /// assert_eq!(resp1.canonical(), resp2.canonical()); - /// ``` - pub fn canonical(mut self) -> Self { - self.admins.sort(); - self.admins.dedup(); - self - } -} diff --git a/contracts/cw1-whitelist/src/state.rs b/contracts/cw1-whitelist/src/state.rs deleted file mode 100644 index 4b6f31ce5..000000000 --- a/contracts/cw1-whitelist/src/state.rs +++ /dev/null @@ -1,69 +0,0 @@ -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -use cosmwasm_std::Addr; -use cw_storage_plus::Item; - -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug, Default)] -pub struct AdminList { - pub admins: Vec, - pub mutable: bool, -} - -impl AdminList { - /// returns true if the address is a registered admin - pub fn is_admin(&self, addr: impl AsRef) -> bool { - let addr = addr.as_ref(); - self.admins.iter().any(|a| a.as_ref() == addr) - } - - /// returns true if the address is a registered admin and the config is mutable - pub fn can_modify(&self, addr: &str) -> bool { - self.mutable && self.is_admin(addr) - } -} - -pub const ADMIN_LIST: Item = Item::new("admin_list"); - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn is_admin() { - let admins: Vec<_> = vec!["bob", "paul", "john"] - .into_iter() - .map(Addr::unchecked) - .collect(); - let config = AdminList { - admins: admins.clone(), - mutable: false, - }; - - assert!(config.is_admin(admins[0].as_ref())); - assert!(config.is_admin(admins[2].as_ref())); - assert!(!config.is_admin("other")); - } - - #[test] - fn can_modify() { - let alice = Addr::unchecked("alice"); - let bob = Addr::unchecked("bob"); - - // admin can modify mutable contract - let config = AdminList { - admins: vec![bob.clone()], - mutable: true, - }; - assert!(!config.can_modify(alice.as_ref())); - assert!(config.can_modify(bob.as_ref())); - - // no one can modify an immutable contract - let config = AdminList { - admins: vec![alice.clone()], - mutable: false, - }; - assert!(!config.can_modify(alice.as_ref())); - assert!(!config.can_modify(bob.as_ref())); - } -} diff --git a/contracts/cw20-base/.cargo/config b/contracts/cw20-base/.cargo/config deleted file mode 100644 index f5174787c..000000000 --- a/contracts/cw20-base/.cargo/config +++ /dev/null @@ -1,6 +0,0 @@ -[alias] -wasm = "build --release --lib --target wasm32-unknown-unknown" -wasm-debug = "build --lib --target wasm32-unknown-unknown" -unit-test = "test --lib" -integration-test = "test --test integration" -schema = "run --bin schema" diff --git a/contracts/cw20-base/Cargo.toml b/contracts/cw20-base/Cargo.toml deleted file mode 100644 index 2f49e44c4..000000000 --- a/contracts/cw20-base/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "cw20-base" -version = "1.1.2" -authors = ["Ethan Frey "] -edition = "2021" -description = "Basic implementation of a CosmWasm-20 compliant token" -license = "Apache-2.0" -repository = "https://github.com/CosmWasm/cw-plus" -homepage = "https://cosmwasm.com" -documentation = "https://docs.cosmwasm.com" - -[lib] -crate-type = ["cdylib", "rlib"] - -[features] -backtraces = ["cosmwasm-std/backtraces"] -# use library feature to disable all instantiate/execute/query exports -library = [] - -[dependencies] -cosmwasm-schema = { version = "1.4.0" } -cw2 = { path = "../../packages/cw2", version = "1.1.2" } -cw20 = { path = "../../packages/cw20", version = "1.1.2" } -cw-storage-plus = "1.1.0" -cosmwasm-std = { version = "1.4.0" } -schemars = "0.8.15" -semver = "1" -serde = { version = "1.0.188", default-features = false, features = ["derive"] } -thiserror = { version = "1.0.49" } - -[dev-dependencies] -cw-multi-test = "0.16.5" -cw-utils = "1.0.1" diff --git a/contracts/cw20-base/README.md b/contracts/cw20-base/README.md deleted file mode 100644 index 01db9e054..000000000 --- a/contracts/cw20-base/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# CW20 Basic - -This is a basic implementation of a cw20 contract. It implements -the [CW20 spec](../../packages/cw20/README.md) and is designed to -be deployed as is, or imported into other contracts to easily build -cw20-compatible tokens with custom logic. - -Implements: - -- [x] CW20 Base -- [x] Mintable extension -- [x] Allowances extension - -## Running this contract - -You will need Rust 1.44.1+ with `wasm32-unknown-unknown` target installed. - -You can run unit tests on this via: - -`cargo test` - -Once you are happy with the content, you can compile it to wasm via: - -``` -RUSTFLAGS='-C link-arg=-s' cargo wasm -cp ../../target/wasm32-unknown-unknown/release/cw20_base.wasm . -ls -l cw20_base.wasm -sha256sum cw20_base.wasm -``` - -Or for a production-ready (optimized) build, run a build command in the -the repository root: https://github.com/CosmWasm/cw-plus#compiling. - -## Importing this contract - -You can also import much of the logic of this contract to build another -ERC20-contract, such as a bonding curve, overiding or extending what you -need. - -Basically, you just need to write your handle function and import -`cw20_base::contract::handle_transfer`, etc and dispatch to them. -This allows you to use custom `ExecuteMsg` and `QueryMsg` with your additional -calls, but then use the underlying implementation for the standard cw20 -messages you want to support. The same with `QueryMsg`. You *could* reuse `instantiate` -as it, but it is likely you will want to change it. And it is rather simple. - -Look at [`cw20-staking`](https://github.com/CosmWasm/cw-tokens/tree/main/contracts/cw20-staking) for an example of how to "inherit" -all this token functionality and combine it with custom logic. diff --git a/contracts/cw20-base/src/allowances.rs b/contracts/cw20-base/src/allowances.rs deleted file mode 100644 index 38b36da46..000000000 --- a/contracts/cw20-base/src/allowances.rs +++ /dev/null @@ -1,879 +0,0 @@ -use cosmwasm_std::{ - attr, Addr, Binary, BlockInfo, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, - Storage, Uint128, -}; -use cw20::{AllowanceResponse, Cw20ReceiveMsg, Expiration}; - -use crate::error::ContractError; -use crate::state::{ALLOWANCES, ALLOWANCES_SPENDER, BALANCES, TOKEN_INFO}; - -pub fn execute_increase_allowance( - deps: DepsMut, - env: Env, - info: MessageInfo, - spender: String, - amount: Uint128, - expires: Option, -) -> Result { - let spender_addr = deps.api.addr_validate(&spender)?; - if spender_addr == info.sender { - return Err(ContractError::CannotSetOwnAccount {}); - } - - let update_fn = |allow: Option| -> Result<_, _> { - let mut val = allow.unwrap_or_default(); - if let Some(exp) = expires { - if exp.is_expired(&env.block) { - return Err(ContractError::InvalidExpiration {}); - } - val.expires = exp; - } - val.allowance += amount; - Ok(val) - }; - ALLOWANCES.update(deps.storage, (&info.sender, &spender_addr), update_fn)?; - ALLOWANCES_SPENDER.update(deps.storage, (&spender_addr, &info.sender), update_fn)?; - - let res = Response::new().add_attributes(vec![ - attr("action", "increase_allowance"), - attr("owner", info.sender), - attr("spender", spender), - attr("amount", amount), - ]); - Ok(res) -} - -pub fn execute_decrease_allowance( - deps: DepsMut, - env: Env, - info: MessageInfo, - spender: String, - amount: Uint128, - expires: Option, -) -> Result { - let spender_addr = deps.api.addr_validate(&spender)?; - if spender_addr == info.sender { - return Err(ContractError::CannotSetOwnAccount {}); - } - - let key = (&info.sender, &spender_addr); - - fn reverse<'a>(t: (&'a Addr, &'a Addr)) -> (&'a Addr, &'a Addr) { - (t.1, t.0) - } - - // load value and delete if it hits 0, or update otherwise - let mut allowance = 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; - } - ALLOWANCES.save(deps.storage, key, &allowance)?; - ALLOWANCES_SPENDER.save(deps.storage, reverse(key), &allowance)?; - } else { - ALLOWANCES.remove(deps.storage, key); - ALLOWANCES_SPENDER.remove(deps.storage, reverse(key)); - } - - let res = Response::new().add_attributes(vec![ - attr("action", "decrease_allowance"), - attr("owner", info.sender), - attr("spender", spender), - attr("amount", amount), - ]); - Ok(res) -} - -// this can be used to update a lower allowance - call bucket.update with proper keys -pub fn deduct_allowance( - storage: &mut dyn Storage, - owner: &Addr, - spender: &Addr, - block: &BlockInfo, - amount: Uint128, -) -> Result { - let update_fn = |current: Option| -> _ { - match current { - Some(mut a) => { - if a.expires.is_expired(block) { - Err(ContractError::Expired {}) - } else { - // deduct the allowance if enough - a.allowance = a - .allowance - .checked_sub(amount) - .map_err(StdError::overflow)?; - Ok(a) - } - } - None => Err(ContractError::NoAllowance {}), - } - }; - ALLOWANCES.update(storage, (owner, spender), update_fn)?; - ALLOWANCES_SPENDER.update(storage, (spender, owner), update_fn) -} - -pub fn execute_transfer_from( - deps: DepsMut, - env: Env, - info: MessageInfo, - owner: String, - recipient: String, - amount: Uint128, -) -> Result { - let rcpt_addr = deps.api.addr_validate(&recipient)?; - let owner_addr = deps.api.addr_validate(&owner)?; - - // deduct allowance before doing anything else have enough allowance - deduct_allowance(deps.storage, &owner_addr, &info.sender, &env.block, amount)?; - - BALANCES.update( - deps.storage, - &owner_addr, - |balance: Option| -> StdResult<_> { - Ok(balance.unwrap_or_default().checked_sub(amount)?) - }, - )?; - BALANCES.update( - deps.storage, - &rcpt_addr, - |balance: Option| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) }, - )?; - - let res = Response::new().add_attributes(vec![ - attr("action", "transfer_from"), - attr("from", owner), - attr("to", recipient), - attr("by", info.sender), - attr("amount", amount), - ]); - Ok(res) -} - -pub fn execute_burn_from( - deps: DepsMut, - - env: Env, - info: MessageInfo, - owner: String, - amount: Uint128, -) -> Result { - let owner_addr = deps.api.addr_validate(&owner)?; - - // deduct allowance before doing anything else have enough allowance - deduct_allowance(deps.storage, &owner_addr, &info.sender, &env.block, amount)?; - - // lower balance - BALANCES.update( - deps.storage, - &owner_addr, - |balance: Option| -> StdResult<_> { - Ok(balance.unwrap_or_default().checked_sub(amount)?) - }, - )?; - // reduce total_supply - TOKEN_INFO.update(deps.storage, |mut meta| -> StdResult<_> { - meta.total_supply = meta.total_supply.checked_sub(amount)?; - Ok(meta) - })?; - - let res = Response::new().add_attributes(vec![ - attr("action", "burn_from"), - attr("from", owner), - attr("by", info.sender), - attr("amount", amount), - ]); - Ok(res) -} - -pub fn execute_send_from( - deps: DepsMut, - env: Env, - info: MessageInfo, - owner: String, - contract: String, - amount: Uint128, - msg: Binary, -) -> Result { - 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 - deduct_allowance(deps.storage, &owner_addr, &info.sender, &env.block, amount)?; - - // move the tokens to the contract - BALANCES.update( - deps.storage, - &owner_addr, - |balance: Option| -> StdResult<_> { - Ok(balance.unwrap_or_default().checked_sub(amount)?) - }, - )?; - BALANCES.update( - deps.storage, - &rcpt_addr, - |balance: Option| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) }, - )?; - - let attrs = vec![ - attr("action", "send_from"), - attr("from", &owner), - attr("to", &contract), - attr("by", &info.sender), - attr("amount", amount), - ]; - - // create a send message - let msg = Cw20ReceiveMsg { - sender: info.sender.into(), - amount, - msg, - } - .into_cosmos_msg(contract)?; - - let res = Response::new().add_message(msg).add_attributes(attrs); - Ok(res) -} - -pub fn query_allowance(deps: Deps, owner: String, spender: String) -> StdResult { - let owner_addr = deps.api.addr_validate(&owner)?; - let spender_addr = deps.api.addr_validate(&spender)?; - let allowance = ALLOWANCES - .may_load(deps.storage, (&owner_addr, &spender_addr))? - .unwrap_or_default(); - Ok(allowance) -} - -#[cfg(test)] -mod tests { - use super::*; - - use cosmwasm_std::testing::{mock_dependencies_with_balance, mock_env, mock_info}; - use cosmwasm_std::{coins, CosmosMsg, SubMsg, Timestamp, WasmMsg}; - use cw20::{Cw20Coin, TokenInfoResponse}; - - use crate::contract::{execute, instantiate, query_balance, query_token_info}; - use crate::msg::{ExecuteMsg, InstantiateMsg}; - - fn get_balance>(deps: Deps, address: T) -> Uint128 { - query_balance(deps, address.into()).unwrap().balance - } - - // this will set up the instantiation for other tests - fn do_instantiate>( - mut deps: DepsMut, - addr: T, - amount: Uint128, - ) -> TokenInfoResponse { - let instantiate_msg = InstantiateMsg { - name: "Auto Gen".to_string(), - symbol: "AUTO".to_string(), - decimals: 3, - initial_balances: vec![Cw20Coin { - address: addr.into(), - amount, - }], - mint: None, - marketing: None, - }; - let info = mock_info("creator", &[]); - let env = mock_env(); - instantiate(deps.branch(), env, info, instantiate_msg).unwrap(); - query_token_info(deps.as_ref()).unwrap() - } - - #[test] - fn increase_decrease_allowances() { - let mut deps = mock_dependencies_with_balance(&coins(2, "token")); - - let owner = String::from("addr0001"); - let spender = String::from("addr0002"); - let info = mock_info(owner.as_ref(), &[]); - let env = mock_env(); - do_instantiate(deps.as_mut(), owner.clone(), Uint128::new(12340000)); - - // no allowance to start - let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); - assert_eq!(allowance, AllowanceResponse::default()); - - // set allowance with height expiration - let allow1 = Uint128::new(7777); - let expires = Expiration::AtHeight(123_456); - let msg = ExecuteMsg::IncreaseAllowance { - spender: spender.clone(), - amount: allow1, - expires: Some(expires), - }; - execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); - - // ensure it looks good - let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); - assert_eq!( - allowance, - AllowanceResponse { - allowance: allow1, - expires - } - ); - - // decrease it a bit with no expire set - stays the same - let lower = Uint128::new(4444); - let allow2 = allow1.checked_sub(lower).unwrap(); - let msg = ExecuteMsg::DecreaseAllowance { - spender: spender.clone(), - amount: lower, - expires: None, - }; - execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); - let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); - assert_eq!( - allowance, - AllowanceResponse { - allowance: allow2, - expires - } - ); - - // increase it some more and override the expires - let raise = Uint128::new(87654); - let allow3 = allow2 + raise; - let new_expire = Expiration::AtTime(Timestamp::from_seconds(8888888888)); - let msg = ExecuteMsg::IncreaseAllowance { - spender: spender.clone(), - amount: raise, - expires: Some(new_expire), - }; - execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); - let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); - assert_eq!( - allowance, - AllowanceResponse { - allowance: allow3, - expires: new_expire - } - ); - - // decrease it below 0 - let msg = ExecuteMsg::DecreaseAllowance { - spender: spender.clone(), - amount: Uint128::new(99988647623876347), - expires: None, - }; - execute(deps.as_mut(), env, info, msg).unwrap(); - let allowance = query_allowance(deps.as_ref(), owner, spender).unwrap(); - assert_eq!(allowance, AllowanceResponse::default()); - } - - #[test] - fn allowances_independent() { - let mut deps = mock_dependencies_with_balance(&coins(2, "token")); - - let owner = String::from("addr0001"); - let spender = String::from("addr0002"); - let spender2 = String::from("addr0003"); - let info = mock_info(owner.as_ref(), &[]); - let env = mock_env(); - do_instantiate(deps.as_mut(), &owner, Uint128::new(12340000)); - - // no allowance to start - assert_eq!( - query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(), - AllowanceResponse::default() - ); - assert_eq!( - query_allowance(deps.as_ref(), owner.clone(), spender2.clone()).unwrap(), - AllowanceResponse::default() - ); - assert_eq!( - query_allowance(deps.as_ref(), spender.clone(), spender2.clone()).unwrap(), - AllowanceResponse::default() - ); - - // set allowance with height expiration - let allow1 = Uint128::new(7777); - let expires = Expiration::AtHeight(123_456); - let msg = ExecuteMsg::IncreaseAllowance { - spender: spender.clone(), - amount: allow1, - expires: Some(expires), - }; - execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); - - // set other allowance with no expiration - let allow2 = Uint128::new(87654); - let msg = ExecuteMsg::IncreaseAllowance { - spender: spender2.clone(), - amount: allow2, - expires: None, - }; - execute(deps.as_mut(), env, info, msg).unwrap(); - - // check they are proper - let expect_one = AllowanceResponse { - allowance: allow1, - expires, - }; - let expect_two = AllowanceResponse { - allowance: allow2, - expires: Expiration::Never {}, - }; - assert_eq!( - query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(), - expect_one - ); - assert_eq!( - query_allowance(deps.as_ref(), owner.clone(), spender2.clone()).unwrap(), - expect_two - ); - assert_eq!( - query_allowance(deps.as_ref(), spender.clone(), spender2.clone()).unwrap(), - AllowanceResponse::default() - ); - - // also allow spender -> spender2 with no interference - let info = mock_info(spender.as_ref(), &[]); - let env = mock_env(); - let allow3 = Uint128::new(1821); - let expires3 = Expiration::AtTime(Timestamp::from_seconds(3767626296)); - let msg = ExecuteMsg::IncreaseAllowance { - spender: spender2.clone(), - amount: allow3, - expires: Some(expires3), - }; - execute(deps.as_mut(), env, info, msg).unwrap(); - let expect_three = AllowanceResponse { - allowance: allow3, - expires: expires3, - }; - assert_eq!( - query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(), - expect_one - ); - assert_eq!( - query_allowance(deps.as_ref(), owner, spender2.clone()).unwrap(), - expect_two - ); - assert_eq!( - query_allowance(deps.as_ref(), spender, spender2).unwrap(), - expect_three - ); - } - - #[test] - fn no_self_allowance() { - let mut deps = mock_dependencies_with_balance(&coins(2, "token")); - - let owner = String::from("addr0001"); - let info = mock_info(owner.as_ref(), &[]); - let env = mock_env(); - do_instantiate(deps.as_mut(), &owner, Uint128::new(12340000)); - - // self-allowance - let msg = ExecuteMsg::IncreaseAllowance { - spender: owner.clone(), - amount: Uint128::new(7777), - expires: None, - }; - let err = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap_err(); - assert_eq!(err, ContractError::CannotSetOwnAccount {}); - - // decrease self-allowance - let msg = ExecuteMsg::DecreaseAllowance { - spender: owner, - amount: Uint128::new(7777), - expires: None, - }; - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(err, ContractError::CannotSetOwnAccount {}); - } - - #[test] - fn transfer_from_respects_limits() { - let mut deps = mock_dependencies_with_balance(&[]); - let owner = String::from("addr0001"); - let spender = String::from("addr0002"); - let rcpt = String::from("addr0003"); - - let start = Uint128::new(999999); - do_instantiate(deps.as_mut(), &owner, start); - - // provide an allowance - let allow1 = Uint128::new(77777); - let msg = ExecuteMsg::IncreaseAllowance { - spender: spender.clone(), - amount: allow1, - expires: None, - }; - let info = mock_info(owner.as_ref(), &[]); - let env = mock_env(); - execute(deps.as_mut(), env, info, msg).unwrap(); - - // valid transfer of part of the allowance - let transfer = Uint128::new(44444); - let msg = ExecuteMsg::TransferFrom { - owner: owner.clone(), - recipient: rcpt.clone(), - amount: transfer, - }; - let info = mock_info(spender.as_ref(), &[]); - let env = mock_env(); - let res = execute(deps.as_mut(), env, info, msg).unwrap(); - assert_eq!(res.attributes[0], attr("action", "transfer_from")); - - // make sure money arrived - assert_eq!( - get_balance(deps.as_ref(), owner.clone()), - start.checked_sub(transfer).unwrap() - ); - assert_eq!(get_balance(deps.as_ref(), rcpt.clone()), transfer); - - // ensure it looks good - let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); - let expect = AllowanceResponse { - allowance: allow1.checked_sub(transfer).unwrap(), - expires: Expiration::Never {}, - }; - assert_eq!(expect, allowance); - - // cannot send more than the allowance - let msg = ExecuteMsg::TransferFrom { - owner: owner.clone(), - recipient: rcpt.clone(), - amount: Uint128::new(33443), - }; - let info = mock_info(spender.as_ref(), &[]); - let env = mock_env(); - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); - - // let us increase limit, but set the expiration to expire in the next block - let info = mock_info(owner.as_ref(), &[]); - let mut env = mock_env(); - let msg = ExecuteMsg::IncreaseAllowance { - spender: spender.clone(), - amount: Uint128::new(1000), - expires: Some(Expiration::AtHeight(env.block.height + 1)), - }; - execute(deps.as_mut(), env.clone(), info, msg).unwrap(); - - env.block.height += 1; - - // we should now get the expiration error - let msg = ExecuteMsg::TransferFrom { - owner, - recipient: rcpt, - amount: Uint128::new(33443), - }; - let info = mock_info(spender.as_ref(), &[]); - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(err, ContractError::Expired {}); - } - - #[test] - fn burn_from_respects_limits() { - let mut deps = mock_dependencies_with_balance(&[]); - let owner = String::from("addr0001"); - let spender = String::from("addr0002"); - - let start = Uint128::new(999999); - do_instantiate(deps.as_mut(), &owner, start); - - // provide an allowance - let allow1 = Uint128::new(77777); - let msg = ExecuteMsg::IncreaseAllowance { - spender: spender.clone(), - amount: allow1, - expires: None, - }; - let info = mock_info(owner.as_ref(), &[]); - let env = mock_env(); - execute(deps.as_mut(), env, info, msg).unwrap(); - - // valid burn of part of the allowance - let transfer = Uint128::new(44444); - let msg = ExecuteMsg::BurnFrom { - owner: owner.clone(), - amount: transfer, - }; - let info = mock_info(spender.as_ref(), &[]); - let env = mock_env(); - let res = execute(deps.as_mut(), env, info, msg).unwrap(); - assert_eq!(res.attributes[0], attr("action", "burn_from")); - - // make sure money burnt - assert_eq!( - get_balance(deps.as_ref(), owner.clone()), - start.checked_sub(transfer).unwrap() - ); - - // ensure it looks good - let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); - let expect = AllowanceResponse { - allowance: allow1.checked_sub(transfer).unwrap(), - expires: Expiration::Never {}, - }; - assert_eq!(expect, allowance); - - // cannot burn more than the allowance - let msg = ExecuteMsg::BurnFrom { - owner: owner.clone(), - amount: Uint128::new(33443), - }; - let info = mock_info(spender.as_ref(), &[]); - let env = mock_env(); - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); - - // let us increase limit, but set the expiration to expire in the next block - let info = mock_info(owner.as_ref(), &[]); - let mut env = mock_env(); - let msg = ExecuteMsg::IncreaseAllowance { - spender: spender.clone(), - amount: Uint128::new(1000), - expires: Some(Expiration::AtHeight(env.block.height + 1)), - }; - execute(deps.as_mut(), env.clone(), info, msg).unwrap(); - - // increase block height, so the limit is expired now - env.block.height += 1; - - // we should now get the expiration error - let msg = ExecuteMsg::BurnFrom { - owner, - amount: Uint128::new(33443), - }; - let info = mock_info(spender.as_ref(), &[]); - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(err, ContractError::Expired {}); - } - - #[test] - fn send_from_respects_limits() { - let mut deps = mock_dependencies_with_balance(&[]); - let owner = String::from("addr0001"); - let spender = String::from("addr0002"); - let contract = String::from("cool-dex"); - let send_msg = Binary::from(r#"{"some":123}"#.as_bytes()); - - let start = Uint128::new(999999); - do_instantiate(deps.as_mut(), &owner, start); - - // provide an allowance - let allow1 = Uint128::new(77777); - let msg = ExecuteMsg::IncreaseAllowance { - spender: spender.clone(), - amount: allow1, - expires: None, - }; - let info = mock_info(owner.as_ref(), &[]); - let env = mock_env(); - execute(deps.as_mut(), env, info, msg).unwrap(); - - // valid send of part of the allowance - let transfer = Uint128::new(44444); - let msg = ExecuteMsg::SendFrom { - owner: owner.clone(), - amount: transfer, - contract: contract.clone(), - msg: send_msg.clone(), - }; - let info = mock_info(spender.as_ref(), &[]); - let env = mock_env(); - let res = execute(deps.as_mut(), env, info, msg).unwrap(); - assert_eq!(res.attributes[0], attr("action", "send_from")); - assert_eq!(1, res.messages.len()); - - // we record this as sent by the one who requested, not the one who was paying - let binary_msg = Cw20ReceiveMsg { - sender: spender.clone(), - amount: transfer, - msg: send_msg.clone(), - } - .into_binary() - .unwrap(); - assert_eq!( - res.messages[0], - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: contract.clone(), - msg: binary_msg, - funds: vec![], - })) - ); - - // make sure money sent - assert_eq!( - get_balance(deps.as_ref(), owner.clone()), - start.checked_sub(transfer).unwrap() - ); - assert_eq!(get_balance(deps.as_ref(), contract.clone()), transfer); - - // ensure it looks good - let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); - let expect = AllowanceResponse { - allowance: allow1.checked_sub(transfer).unwrap(), - expires: Expiration::Never {}, - }; - assert_eq!(expect, allowance); - - // cannot send more than the allowance - let msg = ExecuteMsg::SendFrom { - owner: owner.clone(), - amount: Uint128::new(33443), - contract: contract.clone(), - msg: send_msg.clone(), - }; - let info = mock_info(spender.as_ref(), &[]); - let env = mock_env(); - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); - - // let us increase limit, but set the expiration to the next block - let info = mock_info(owner.as_ref(), &[]); - let mut env = mock_env(); - let msg = ExecuteMsg::IncreaseAllowance { - spender: spender.clone(), - amount: Uint128::new(1000), - expires: Some(Expiration::AtHeight(env.block.height + 1)), - }; - execute(deps.as_mut(), env.clone(), info, msg).unwrap(); - - // increase block height, so the limit is expired now - env.block.height += 1; - - // we should now get the expiration error - let msg = ExecuteMsg::SendFrom { - owner, - amount: Uint128::new(33443), - contract, - msg: send_msg, - }; - let info = mock_info(spender.as_ref(), &[]); - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(err, ContractError::Expired {}); - } - - #[test] - fn no_past_expiration() { - let mut deps = mock_dependencies_with_balance(&coins(2, "token")); - - let owner = String::from("addr0001"); - let spender = String::from("addr0002"); - let info = mock_info(owner.as_ref(), &[]); - let env = mock_env(); - do_instantiate(deps.as_mut(), owner.clone(), Uint128::new(12340000)); - - // set allowance with height expiration at current block height - let expires = Expiration::AtHeight(env.block.height); - let msg = ExecuteMsg::IncreaseAllowance { - spender: spender.clone(), - amount: Uint128::new(7777), - expires: Some(expires), - }; - - // ensure it is rejected - assert_eq!( - Err(ContractError::InvalidExpiration {}), - execute(deps.as_mut(), env.clone(), info.clone(), msg) - ); - - // set allowance with time expiration in the past - let expires = Expiration::AtTime(env.block.time.minus_seconds(1)); - let msg = ExecuteMsg::IncreaseAllowance { - spender: spender.clone(), - amount: Uint128::new(7777), - expires: Some(expires), - }; - - // ensure it is rejected - assert_eq!( - Err(ContractError::InvalidExpiration {}), - execute(deps.as_mut(), env.clone(), info.clone(), msg) - ); - - // set allowance with height expiration at next block height - let expires = Expiration::AtHeight(env.block.height + 1); - let allow = Uint128::new(7777); - let msg = ExecuteMsg::IncreaseAllowance { - spender: spender.clone(), - amount: allow, - expires: Some(expires), - }; - - execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); - - // ensure it looks good - let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); - assert_eq!( - allowance, - AllowanceResponse { - allowance: allow, - expires - } - ); - - // set allowance with time expiration in the future - let expires = Expiration::AtTime(env.block.time.plus_seconds(10)); - let allow = Uint128::new(7777); - let msg = ExecuteMsg::IncreaseAllowance { - spender: spender.clone(), - amount: allow, - expires: Some(expires), - }; - - execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); - - // ensure it looks good - let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); - assert_eq!( - allowance, - AllowanceResponse { - allowance: allow + allow, // we increased twice - expires - } - ); - - // decrease with height expiration at current block height - let expires = Expiration::AtHeight(env.block.height); - let allow = Uint128::new(7777); - let msg = ExecuteMsg::IncreaseAllowance { - spender: spender.clone(), - amount: allow, - expires: Some(expires), - }; - - // ensure it is rejected - assert_eq!( - Err(ContractError::InvalidExpiration {}), - execute(deps.as_mut(), env.clone(), info.clone(), msg) - ); - - // decrease with height expiration at next block height - let expires = Expiration::AtHeight(env.block.height + 1); - let allow = Uint128::new(7777); - let msg = ExecuteMsg::DecreaseAllowance { - spender: spender.clone(), - amount: allow, - expires: Some(expires), - }; - - execute(deps.as_mut(), env, info, msg).unwrap(); - - // ensure it looks good - let allowance = query_allowance(deps.as_ref(), owner, spender).unwrap(); - assert_eq!( - allowance, - AllowanceResponse { - allowance: allow, - expires - } - ); - } -} diff --git a/contracts/cw20-base/src/bin/schema.rs b/contracts/cw20-base/src/bin/schema.rs deleted file mode 100644 index 40d16fd4f..000000000 --- a/contracts/cw20-base/src/bin/schema.rs +++ /dev/null @@ -1,11 +0,0 @@ -use cosmwasm_schema::write_api; - -use cw20_base::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; - -fn main() { - write_api! { - instantiate: InstantiateMsg, - execute: ExecuteMsg, - query: QueryMsg, - } -} diff --git a/contracts/cw20-base/src/contract.rs b/contracts/cw20-base/src/contract.rs deleted file mode 100644 index 6f92ac227..000000000 --- a/contracts/cw20-base/src/contract.rs +++ /dev/null @@ -1,2219 +0,0 @@ -#[cfg(not(feature = "library"))] -use cosmwasm_std::entry_point; -use cosmwasm_std::Order::Ascending; -use cosmwasm_std::{ - to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Uint128, -}; - -use cw2::{ensure_from_older_version, set_contract_version}; -use cw20::{ - BalanceResponse, Cw20Coin, Cw20ReceiveMsg, DownloadLogoResponse, EmbeddedLogo, Logo, LogoInfo, - MarketingInfoResponse, MinterResponse, TokenInfoResponse, -}; - -use crate::allowances::{ - execute_burn_from, execute_decrease_allowance, execute_increase_allowance, execute_send_from, - execute_transfer_from, query_allowance, -}; -use crate::enumerable::{query_all_accounts, query_owner_allowances, query_spender_allowances}; -use crate::error::ContractError; -use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; -use crate::state::{ - MinterData, TokenInfo, ALLOWANCES, ALLOWANCES_SPENDER, BALANCES, LOGO, MARKETING_INFO, - TOKEN_INFO, -}; - -// version info for migration info -const CONTRACT_NAME: &str = "crates.io:cw20-base"; -const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); - -const LOGO_SIZE_CAP: usize = 5 * 1024; - -/// 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""; - - 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. -} - -/// 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 passed logo is correct, and if not, returns an error -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 - } -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn instantiate( - mut deps: DepsMut, - _env: Env, - _info: MessageInfo, - msg: InstantiateMsg, -) -> Result { - set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - // check valid token info - msg.validate()?; - // create initial accounts - let total_supply = create_accounts(&mut deps, &msg.initial_balances)?; - - if let Some(limit) = msg.get_cap() { - if total_supply > limit { - return Err(StdError::generic_err("Initial supply greater than cap").into()); - } - } - - let mint = match msg.mint { - Some(m) => Some(MinterData { - minter: deps.api.addr_validate(&m.minter)?, - cap: m.cap, - }), - None => None, - }; - - // store token info - let data = TokenInfo { - name: msg.name, - symbol: msg.symbol, - decimals: msg.decimals, - total_supply, - mint, - }; - TOKEN_INFO.save(deps.storage, &data)?; - - if let Some(marketing) = msg.marketing { - let logo = if let Some(logo) = marketing.logo { - verify_logo(&logo)?; - 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, - }; - MARKETING_INFO.save(deps.storage, &data)?; - } - - Ok(Response::default()) -} - -pub fn create_accounts( - deps: &mut DepsMut, - accounts: &[Cw20Coin], -) -> Result { - validate_accounts(accounts)?; - - let mut total_supply = Uint128::zero(); - for row in accounts { - let address = deps.api.addr_validate(&row.address)?; - BALANCES.save(deps.storage, &address, &row.amount)?; - total_supply += row.amount; - } - - Ok(total_supply) -} - -pub fn validate_accounts(accounts: &[Cw20Coin]) -> Result<(), ContractError> { - let mut addresses = accounts.iter().map(|c| &c.address).collect::>(); - addresses.sort(); - addresses.dedup(); - - if addresses.len() != accounts.len() { - Err(ContractError::DuplicateInitialBalanceAddresses {}) - } else { - Ok(()) - } -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn execute( - deps: DepsMut, - env: Env, - info: MessageInfo, - msg: ExecuteMsg, -) -> Result { - match msg { - ExecuteMsg::Transfer { recipient, amount } => { - execute_transfer(deps, env, info, recipient, amount) - } - ExecuteMsg::Burn { amount } => execute_burn(deps, env, info, amount), - ExecuteMsg::Send { - contract, - amount, - msg, - } => execute_send(deps, env, info, contract, amount, msg), - ExecuteMsg::Mint { recipient, amount } => execute_mint(deps, env, info, recipient, amount), - ExecuteMsg::IncreaseAllowance { - spender, - amount, - expires, - } => execute_increase_allowance(deps, env, info, spender, amount, expires), - ExecuteMsg::DecreaseAllowance { - spender, - amount, - expires, - } => execute_decrease_allowance(deps, env, info, spender, amount, expires), - ExecuteMsg::TransferFrom { - owner, - recipient, - amount, - } => execute_transfer_from(deps, env, info, owner, recipient, amount), - ExecuteMsg::BurnFrom { owner, amount } => execute_burn_from(deps, env, info, owner, amount), - ExecuteMsg::SendFrom { - owner, - contract, - amount, - msg, - } => execute_send_from(deps, env, info, owner, contract, amount, msg), - ExecuteMsg::UpdateMarketing { - project, - description, - marketing, - } => execute_update_marketing(deps, env, info, project, description, marketing), - ExecuteMsg::UploadLogo(logo) => execute_upload_logo(deps, env, info, logo), - ExecuteMsg::UpdateMinter { new_minter } => { - execute_update_minter(deps, env, info, new_minter) - } - } -} - -pub fn execute_transfer( - deps: DepsMut, - _env: Env, - info: MessageInfo, - recipient: String, - amount: Uint128, -) -> Result { - let rcpt_addr = deps.api.addr_validate(&recipient)?; - - BALANCES.update( - deps.storage, - &info.sender, - |balance: Option| -> StdResult<_> { - Ok(balance.unwrap_or_default().checked_sub(amount)?) - }, - )?; - BALANCES.update( - deps.storage, - &rcpt_addr, - |balance: Option| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) }, - )?; - - let res = Response::new() - .add_attribute("action", "transfer") - .add_attribute("from", info.sender) - .add_attribute("to", recipient) - .add_attribute("amount", amount); - Ok(res) -} - -pub fn execute_burn( - deps: DepsMut, - _env: Env, - info: MessageInfo, - amount: Uint128, -) -> Result { - // lower balance - BALANCES.update( - deps.storage, - &info.sender, - |balance: Option| -> StdResult<_> { - Ok(balance.unwrap_or_default().checked_sub(amount)?) - }, - )?; - // reduce total_supply - 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) -} - -pub fn execute_mint( - deps: DepsMut, - _env: Env, - info: MessageInfo, - recipient: String, - amount: Uint128, -) -> Result { - let mut config = TOKEN_INFO - .may_load(deps.storage)? - .ok_or(ContractError::Unauthorized {})?; - - if config - .mint - .as_ref() - .ok_or(ContractError::Unauthorized {})? - .minter - != info.sender - { - return Err(ContractError::Unauthorized {}); - } - - // update supply and enforce cap - config.total_supply += amount; - if let Some(limit) = config.get_cap() { - if config.total_supply > limit { - return Err(ContractError::CannotExceedCap {}); - } - } - TOKEN_INFO.save(deps.storage, &config)?; - - // add amount to recipient balance - let rcpt_addr = deps.api.addr_validate(&recipient)?; - BALANCES.update( - deps.storage, - &rcpt_addr, - |balance: Option| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) }, - )?; - - let res = Response::new() - .add_attribute("action", "mint") - .add_attribute("to", recipient) - .add_attribute("amount", amount); - Ok(res) -} - -pub fn execute_send( - deps: DepsMut, - _env: Env, - info: MessageInfo, - contract: String, - amount: Uint128, - msg: Binary, -) -> Result { - let rcpt_addr = deps.api.addr_validate(&contract)?; - - // move the tokens to the contract - BALANCES.update( - deps.storage, - &info.sender, - |balance: Option| -> StdResult<_> { - Ok(balance.unwrap_or_default().checked_sub(amount)?) - }, - )?; - BALANCES.update( - deps.storage, - &rcpt_addr, - |balance: Option| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) }, - )?; - - let res = Response::new() - .add_attribute("action", "send") - .add_attribute("from", &info.sender) - .add_attribute("to", &contract) - .add_attribute("amount", amount) - .add_message( - Cw20ReceiveMsg { - sender: info.sender.into(), - amount, - msg, - } - .into_cosmos_msg(contract)?, - ); - Ok(res) -} - -pub fn execute_update_minter( - deps: DepsMut, - _env: Env, - info: MessageInfo, - new_minter: Option, -) -> Result { - let mut config = TOKEN_INFO - .may_load(deps.storage)? - .ok_or(ContractError::Unauthorized {})?; - - let mint = config.mint.as_ref().ok_or(ContractError::Unauthorized {})?; - if mint.minter != info.sender { - return Err(ContractError::Unauthorized {}); - } - - let minter_data = new_minter - .map(|new_minter| deps.api.addr_validate(&new_minter)) - .transpose()? - .map(|minter| MinterData { - minter, - cap: mint.cap, - }); - - config.mint = minter_data; - - TOKEN_INFO.save(deps.storage, &config)?; - - Ok(Response::default() - .add_attribute("action", "update_minter") - .add_attribute( - "new_minter", - config - .mint - .map(|m| m.minter.into_string()) - .unwrap_or_else(|| "None".to_string()), - )) -} - -pub fn execute_update_marketing( - deps: DepsMut, - _env: Env, - info: MessageInfo, - project: Option, - description: Option, - marketing: Option, -) -> Result { - let mut marketing_info = MARKETING_INFO - .may_load(deps.storage)? - .ok_or(ContractError::Unauthorized {})?; - - if marketing_info - .marketing - .as_ref() - .ok_or(ContractError::Unauthorized {})? - != info.sender - { - return Err(ContractError::Unauthorized {}); - } - - match project { - Some(empty) if empty.trim().is_empty() => marketing_info.project = None, - Some(project) => marketing_info.project = Some(project), - None => (), - } - - match description { - Some(empty) if empty.trim().is_empty() => marketing_info.description = None, - Some(description) => marketing_info.description = Some(description), - None => (), - } - - match marketing { - Some(empty) if empty.trim().is_empty() => marketing_info.marketing = None, - Some(marketing) => marketing_info.marketing = Some(deps.api.addr_validate(&marketing)?), - None => (), - } - - if marketing_info.project.is_none() - && marketing_info.description.is_none() - && marketing_info.marketing.is_none() - && marketing_info.logo.is_none() - { - MARKETING_INFO.remove(deps.storage); - } else { - MARKETING_INFO.save(deps.storage, &marketing_info)?; - } - - let res = Response::new().add_attribute("action", "update_marketing"); - Ok(res) -} - -pub fn execute_upload_logo( - deps: DepsMut, - _env: Env, - info: MessageInfo, - logo: Logo, -) -> Result { - let mut marketing_info = MARKETING_INFO - .may_load(deps.storage)? - .ok_or(ContractError::Unauthorized {})?; - - verify_logo(&logo)?; - - if marketing_info - .marketing - .as_ref() - .ok_or(ContractError::Unauthorized {})? - != info.sender - { - return Err(ContractError::Unauthorized {}); - } - - LOGO.save(deps.storage, &logo)?; - - let logo_info = match logo { - Logo::Url(url) => LogoInfo::Url(url), - Logo::Embedded(_) => LogoInfo::Embedded, - }; - - marketing_info.logo = Some(logo_info); - MARKETING_INFO.save(deps.storage, &marketing_info)?; - - let res = Response::new().add_attribute("action", "upload_logo"); - Ok(res) -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { - match msg { - QueryMsg::Balance { address } => to_binary(&query_balance(deps, address)?), - QueryMsg::TokenInfo {} => to_binary(&query_token_info(deps)?), - QueryMsg::Minter {} => to_binary(&query_minter(deps)?), - QueryMsg::Allowance { owner, spender } => { - to_binary(&query_allowance(deps, owner, spender)?) - } - QueryMsg::AllAllowances { - owner, - start_after, - limit, - } => to_binary(&query_owner_allowances(deps, owner, start_after, limit)?), - QueryMsg::AllSpenderAllowances { - spender, - start_after, - limit, - } => to_binary(&query_spender_allowances( - deps, - spender, - start_after, - limit, - )?), - QueryMsg::AllAccounts { start_after, limit } => { - to_binary(&query_all_accounts(deps, start_after, limit)?) - } - QueryMsg::MarketingInfo {} => to_binary(&query_marketing_info(deps)?), - QueryMsg::DownloadLogo {} => to_binary(&query_download_logo(deps)?), - } -} - -pub fn query_balance(deps: Deps, address: String) -> StdResult { - let address = deps.api.addr_validate(&address)?; - let balance = BALANCES - .may_load(deps.storage, &address)? - .unwrap_or_default(); - Ok(BalanceResponse { balance }) -} - -pub fn query_token_info(deps: Deps) -> StdResult { - let info = TOKEN_INFO.load(deps.storage)?; - let res = TokenInfoResponse { - name: info.name, - symbol: info.symbol, - decimals: info.decimals, - total_supply: info.total_supply, - }; - Ok(res) -} - -pub fn query_minter(deps: Deps) -> StdResult> { - let meta = TOKEN_INFO.load(deps.storage)?; - let minter = match meta.mint { - Some(m) => Some(MinterResponse { - minter: m.minter.into(), - cap: m.cap, - }), - None => None, - }; - Ok(minter) -} - -pub fn query_marketing_info(deps: Deps) -> StdResult { - Ok(MARKETING_INFO.may_load(deps.storage)?.unwrap_or_default()) -} - -pub fn query_download_logo(deps: Deps) -> StdResult { - let logo = LOGO.load(deps.storage)?; - match logo { - Logo::Embedded(EmbeddedLogo::Svg(logo)) => Ok(DownloadLogoResponse { - mime_type: "image/svg+xml".to_owned(), - data: logo, - }), - Logo::Embedded(EmbeddedLogo::Png(logo)) => Ok(DownloadLogoResponse { - mime_type: "image/png".to_owned(), - data: logo, - }), - Logo::Url(_) => Err(StdError::not_found("logo")), - } -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { - let original_version = - ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - - if original_version < "0.14.0".parse::().unwrap() { - // Build reverse map of allowances per spender - let data = ALLOWANCES - .range(deps.storage, None, None, Ascending) - .collect::>>()?; - for ((owner, spender), allowance) in data { - ALLOWANCES_SPENDER.save(deps.storage, (&spender, &owner), &allowance)?; - } - } - Ok(Response::default()) -} - -#[cfg(test)] -mod tests { - use cosmwasm_std::testing::{ - mock_dependencies, mock_dependencies_with_balance, mock_env, mock_info, - }; - use cosmwasm_std::{coins, from_binary, Addr, CosmosMsg, StdError, SubMsg, WasmMsg}; - - use super::*; - use crate::msg::InstantiateMarketingInfo; - - fn get_balance>(deps: Deps, address: T) -> Uint128 { - query_balance(deps, address.into()).unwrap().balance - } - - // this will set up the instantiation for other tests - fn do_instantiate_with_minter( - deps: DepsMut, - addr: &str, - amount: Uint128, - minter: &str, - cap: Option, - ) -> TokenInfoResponse { - _do_instantiate( - deps, - addr, - amount, - Some(MinterResponse { - minter: minter.to_string(), - cap, - }), - ) - } - - // this will set up the instantiation for other tests - fn do_instantiate(deps: DepsMut, addr: &str, amount: Uint128) -> TokenInfoResponse { - _do_instantiate(deps, addr, amount, None) - } - - // this will set up the instantiation for other tests - fn _do_instantiate( - mut deps: DepsMut, - addr: &str, - amount: Uint128, - mint: Option, - ) -> TokenInfoResponse { - let instantiate_msg = InstantiateMsg { - name: "Auto Gen".to_string(), - symbol: "AUTO".to_string(), - decimals: 3, - initial_balances: vec![Cw20Coin { - address: addr.to_string(), - amount, - }], - mint: mint.clone(), - marketing: None, - }; - let info = mock_info("creator", &[]); - let env = mock_env(); - let res = instantiate(deps.branch(), env, info, instantiate_msg).unwrap(); - assert_eq!(0, res.messages.len()); - - let meta = query_token_info(deps.as_ref()).unwrap(); - assert_eq!( - meta, - TokenInfoResponse { - name: "Auto Gen".to_string(), - symbol: "AUTO".to_string(), - decimals: 3, - total_supply: amount, - } - ); - assert_eq!(get_balance(deps.as_ref(), addr), amount); - assert_eq!(query_minter(deps.as_ref()).unwrap(), mint,); - meta - } - - const PNG_HEADER: [u8; 8] = [0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]; - - mod instantiate { - use super::*; - - #[test] - fn basic() { - let mut deps = mock_dependencies(); - let amount = Uint128::from(11223344u128); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![Cw20Coin { - address: String::from("addr0000"), - amount, - }], - mint: None, - marketing: None, - }; - let info = mock_info("creator", &[]); - let env = mock_env(); - let res = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap(); - assert_eq!(0, res.messages.len()); - - assert_eq!( - query_token_info(deps.as_ref()).unwrap(), - TokenInfoResponse { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - total_supply: amount, - } - ); - assert_eq!( - get_balance(deps.as_ref(), "addr0000"), - Uint128::new(11223344) - ); - } - - #[test] - fn mintable() { - let mut deps = mock_dependencies(); - let amount = Uint128::new(11223344); - let minter = String::from("asmodat"); - let limit = Uint128::new(511223344); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![Cw20Coin { - address: "addr0000".into(), - amount, - }], - mint: Some(MinterResponse { - minter: minter.clone(), - cap: Some(limit), - }), - marketing: None, - }; - let info = mock_info("creator", &[]); - let env = mock_env(); - let res = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap(); - assert_eq!(0, res.messages.len()); - - assert_eq!( - query_token_info(deps.as_ref()).unwrap(), - TokenInfoResponse { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - total_supply: amount, - } - ); - assert_eq!( - get_balance(deps.as_ref(), "addr0000"), - Uint128::new(11223344) - ); - assert_eq!( - query_minter(deps.as_ref()).unwrap(), - Some(MinterResponse { - minter, - cap: Some(limit), - }), - ); - } - - #[test] - fn mintable_over_cap() { - let mut deps = mock_dependencies(); - let amount = Uint128::new(11223344); - let minter = String::from("asmodat"); - let limit = Uint128::new(11223300); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![Cw20Coin { - address: String::from("addr0000"), - amount, - }], - mint: Some(MinterResponse { - minter, - cap: Some(limit), - }), - marketing: None, - }; - let info = mock_info("creator", &[]); - let env = mock_env(); - let err = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap_err(); - assert_eq!( - err, - StdError::generic_err("Initial supply greater than cap").into() - ); - } - - mod marketing { - use super::*; - - #[test] - fn basic() { - let mut deps = mock_dependencies(); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![], - mint: None, - marketing: Some(InstantiateMarketingInfo { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some("marketing".to_owned()), - logo: Some(Logo::Url("url".to_owned())), - }), - }; - - let info = mock_info("creator", &[]); - let env = mock_env(); - let res = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap(); - assert_eq!(0, res.messages.len()); - - assert_eq!( - query_marketing_info(deps.as_ref()).unwrap(), - MarketingInfoResponse { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some(Addr::unchecked("marketing")), - logo: Some(LogoInfo::Url("url".to_owned())), - } - ); - - let err = query_download_logo(deps.as_ref()).unwrap_err(); - assert!( - matches!(err, StdError::NotFound { .. }), - "Expected StdError::NotFound, received {err}", - ); - } - - #[test] - fn invalid_marketing() { - let mut deps = mock_dependencies(); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![], - mint: None, - marketing: Some(InstantiateMarketingInfo { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some("m".to_owned()), - logo: Some(Logo::Url("url".to_owned())), - }), - }; - - let info = mock_info("creator", &[]); - let env = mock_env(); - instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap_err(); - - let err = query_download_logo(deps.as_ref()).unwrap_err(); - assert!( - matches!(err, StdError::NotFound { .. }), - "Expected StdError::NotFound, received {err}", - ); - } - } - } - - #[test] - fn can_mint_by_minter() { - let mut deps = mock_dependencies(); - - let genesis = String::from("genesis"); - let amount = Uint128::new(11223344); - let minter = String::from("asmodat"); - let limit = Uint128::new(511223344); - do_instantiate_with_minter(deps.as_mut(), &genesis, amount, &minter, Some(limit)); - - // minter can mint coins to some winner - let winner = String::from("lucky"); - let prize = Uint128::new(222_222_222); - let msg = ExecuteMsg::Mint { - recipient: winner.clone(), - amount: prize, - }; - - let info = mock_info(minter.as_ref(), &[]); - let env = mock_env(); - let res = execute(deps.as_mut(), env, info, msg).unwrap(); - assert_eq!(0, res.messages.len()); - assert_eq!(get_balance(deps.as_ref(), genesis), amount); - assert_eq!(get_balance(deps.as_ref(), winner.clone()), prize); - - // Allows minting 0 - let msg = ExecuteMsg::Mint { - recipient: winner.clone(), - amount: Uint128::zero(), - }; - let info = mock_info(minter.as_ref(), &[]); - let env = mock_env(); - execute(deps.as_mut(), env, info, msg).unwrap(); - - // but if it exceeds cap (even over multiple rounds), it fails - // cap is enforced - let msg = ExecuteMsg::Mint { - recipient: winner, - amount: Uint128::new(333_222_222), - }; - let info = mock_info(minter.as_ref(), &[]); - let env = mock_env(); - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(err, ContractError::CannotExceedCap {}); - } - - #[test] - fn others_cannot_mint() { - let mut deps = mock_dependencies(); - do_instantiate_with_minter( - deps.as_mut(), - &String::from("genesis"), - Uint128::new(1234), - &String::from("minter"), - None, - ); - - let msg = ExecuteMsg::Mint { - recipient: String::from("lucky"), - amount: Uint128::new(222), - }; - let info = mock_info("anyone else", &[]); - let env = mock_env(); - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(err, ContractError::Unauthorized {}); - } - - #[test] - fn minter_can_update_minter_but_not_cap() { - let mut deps = mock_dependencies(); - let minter = String::from("minter"); - let cap = Some(Uint128::from(3000000u128)); - do_instantiate_with_minter( - deps.as_mut(), - &String::from("genesis"), - Uint128::new(1234), - &minter, - cap, - ); - - let new_minter = "new_minter"; - let msg = ExecuteMsg::UpdateMinter { - new_minter: Some(new_minter.to_string()), - }; - - let info = mock_info(&minter, &[]); - let env = mock_env(); - let res = execute(deps.as_mut(), env.clone(), info, msg); - assert!(res.is_ok()); - let query_minter_msg = QueryMsg::Minter {}; - let res = query(deps.as_ref(), env, query_minter_msg); - let mint: MinterResponse = from_binary(&res.unwrap()).unwrap(); - - // Minter cannot update cap. - assert!(mint.cap == cap); - assert!(mint.minter == new_minter) - } - - #[test] - fn others_cannot_update_minter() { - let mut deps = mock_dependencies(); - let minter = String::from("minter"); - do_instantiate_with_minter( - deps.as_mut(), - &String::from("genesis"), - Uint128::new(1234), - &minter, - None, - ); - - let msg = ExecuteMsg::UpdateMinter { - new_minter: Some("new_minter".to_string()), - }; - - let info = mock_info("not the minter", &[]); - let env = mock_env(); - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(err, ContractError::Unauthorized {}); - } - - #[test] - fn unset_minter() { - let mut deps = mock_dependencies(); - let minter = String::from("minter"); - let cap = None; - do_instantiate_with_minter( - deps.as_mut(), - &String::from("genesis"), - Uint128::new(1234), - &minter, - cap, - ); - - let msg = ExecuteMsg::UpdateMinter { new_minter: None }; - - let info = mock_info(&minter, &[]); - let env = mock_env(); - let res = execute(deps.as_mut(), env.clone(), info, msg); - assert!(res.is_ok()); - let query_minter_msg = QueryMsg::Minter {}; - let res = query(deps.as_ref(), env, query_minter_msg); - let mint: Option = from_binary(&res.unwrap()).unwrap(); - - // Check that mint information was removed. - assert_eq!(mint, None); - - // Check that old minter can no longer mint. - let msg = ExecuteMsg::Mint { - recipient: String::from("lucky"), - amount: Uint128::new(222), - }; - let info = mock_info("minter", &[]); - let env = mock_env(); - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(err, ContractError::Unauthorized {}); - } - - #[test] - fn no_one_mints_if_minter_unset() { - let mut deps = mock_dependencies(); - do_instantiate(deps.as_mut(), &String::from("genesis"), Uint128::new(1234)); - - let msg = ExecuteMsg::Mint { - recipient: String::from("lucky"), - amount: Uint128::new(222), - }; - let info = mock_info("genesis", &[]); - let env = mock_env(); - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(err, ContractError::Unauthorized {}); - } - - #[test] - fn instantiate_multiple_accounts() { - let mut deps = mock_dependencies(); - let amount1 = Uint128::from(11223344u128); - let addr1 = String::from("addr0001"); - let amount2 = Uint128::from(7890987u128); - let addr2 = String::from("addr0002"); - let info = mock_info("creator", &[]); - let env = mock_env(); - - // Fails with duplicate addresses - let instantiate_msg = InstantiateMsg { - name: "Bash Shell".to_string(), - symbol: "BASH".to_string(), - decimals: 6, - initial_balances: vec![ - Cw20Coin { - address: addr1.clone(), - amount: amount1, - }, - Cw20Coin { - address: addr1.clone(), - amount: amount2, - }, - ], - mint: None, - marketing: None, - }; - let err = - instantiate(deps.as_mut(), env.clone(), info.clone(), instantiate_msg).unwrap_err(); - assert_eq!(err, ContractError::DuplicateInitialBalanceAddresses {}); - - // Works with unique addresses - let instantiate_msg = InstantiateMsg { - name: "Bash Shell".to_string(), - symbol: "BASH".to_string(), - decimals: 6, - initial_balances: vec![ - Cw20Coin { - address: addr1.clone(), - amount: amount1, - }, - Cw20Coin { - address: addr2.clone(), - amount: amount2, - }, - ], - mint: None, - marketing: None, - }; - let res = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap(); - assert_eq!(0, res.messages.len()); - assert_eq!( - query_token_info(deps.as_ref()).unwrap(), - TokenInfoResponse { - name: "Bash Shell".to_string(), - symbol: "BASH".to_string(), - decimals: 6, - total_supply: amount1 + amount2, - } - ); - assert_eq!(get_balance(deps.as_ref(), addr1), amount1); - assert_eq!(get_balance(deps.as_ref(), addr2), amount2); - } - - #[test] - fn queries_work() { - let mut deps = mock_dependencies_with_balance(&coins(2, "token")); - let addr1 = String::from("addr0001"); - let amount1 = Uint128::from(12340000u128); - - let expected = do_instantiate(deps.as_mut(), &addr1, amount1); - - // check meta query - let loaded = query_token_info(deps.as_ref()).unwrap(); - assert_eq!(expected, loaded); - - let _info = mock_info("test", &[]); - let env = mock_env(); - // check balance query (full) - let data = query( - deps.as_ref(), - env.clone(), - QueryMsg::Balance { address: addr1 }, - ) - .unwrap(); - let loaded: BalanceResponse = from_binary(&data).unwrap(); - assert_eq!(loaded.balance, amount1); - - // check balance query (empty) - let data = query( - deps.as_ref(), - env, - QueryMsg::Balance { - address: String::from("addr0002"), - }, - ) - .unwrap(); - let loaded: BalanceResponse = from_binary(&data).unwrap(); - assert_eq!(loaded.balance, Uint128::zero()); - } - - #[test] - fn transfer() { - let mut deps = mock_dependencies_with_balance(&coins(2, "token")); - let addr1 = String::from("addr0001"); - let addr2 = String::from("addr0002"); - let amount1 = Uint128::from(12340000u128); - let transfer = Uint128::from(76543u128); - let too_much = Uint128::from(12340321u128); - - do_instantiate(deps.as_mut(), &addr1, amount1); - - // Allows transferring 0 - let info = mock_info(addr1.as_ref(), &[]); - let env = mock_env(); - let msg = ExecuteMsg::Transfer { - recipient: addr2.clone(), - amount: Uint128::zero(), - }; - execute(deps.as_mut(), env, info, msg).unwrap(); - - // cannot send more than we have - let info = mock_info(addr1.as_ref(), &[]); - let env = mock_env(); - let msg = ExecuteMsg::Transfer { - recipient: addr2.clone(), - amount: too_much, - }; - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); - - // cannot send from empty account - let info = mock_info(addr2.as_ref(), &[]); - let env = mock_env(); - let msg = ExecuteMsg::Transfer { - recipient: addr1.clone(), - amount: transfer, - }; - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); - - // valid transfer - let info = mock_info(addr1.as_ref(), &[]); - let env = mock_env(); - let msg = ExecuteMsg::Transfer { - recipient: addr2.clone(), - amount: transfer, - }; - let res = execute(deps.as_mut(), env, info, msg).unwrap(); - assert_eq!(res.messages.len(), 0); - - let remainder = amount1.checked_sub(transfer).unwrap(); - assert_eq!(get_balance(deps.as_ref(), addr1), remainder); - assert_eq!(get_balance(deps.as_ref(), addr2), transfer); - assert_eq!( - query_token_info(deps.as_ref()).unwrap().total_supply, - amount1 - ); - } - - #[test] - fn burn() { - let mut deps = mock_dependencies_with_balance(&coins(2, "token")); - let addr1 = String::from("addr0001"); - let amount1 = Uint128::from(12340000u128); - let burn = Uint128::from(76543u128); - let too_much = Uint128::from(12340321u128); - - do_instantiate(deps.as_mut(), &addr1, amount1); - - // Allows burning 0 - let info = mock_info(addr1.as_ref(), &[]); - let env = mock_env(); - let msg = ExecuteMsg::Burn { - amount: Uint128::zero(), - }; - execute(deps.as_mut(), env, info, msg).unwrap(); - assert_eq!( - query_token_info(deps.as_ref()).unwrap().total_supply, - amount1 - ); - - // cannot burn more than we have - let info = mock_info(addr1.as_ref(), &[]); - let env = mock_env(); - let msg = ExecuteMsg::Burn { amount: too_much }; - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); - assert_eq!( - query_token_info(deps.as_ref()).unwrap().total_supply, - amount1 - ); - - // valid burn reduces total supply - let info = mock_info(addr1.as_ref(), &[]); - let env = mock_env(); - let msg = ExecuteMsg::Burn { amount: burn }; - let res = execute(deps.as_mut(), env, info, msg).unwrap(); - assert_eq!(res.messages.len(), 0); - - let remainder = amount1.checked_sub(burn).unwrap(); - assert_eq!(get_balance(deps.as_ref(), addr1), remainder); - assert_eq!( - query_token_info(deps.as_ref()).unwrap().total_supply, - remainder - ); - } - - #[test] - fn send() { - let mut deps = mock_dependencies_with_balance(&coins(2, "token")); - let addr1 = String::from("addr0001"); - let contract = String::from("addr0002"); - let amount1 = Uint128::from(12340000u128); - let transfer = Uint128::from(76543u128); - let too_much = Uint128::from(12340321u128); - let send_msg = Binary::from(r#"{"some":123}"#.as_bytes()); - - do_instantiate(deps.as_mut(), &addr1, amount1); - - // Allows sending 0 - let info = mock_info(addr1.as_ref(), &[]); - let env = mock_env(); - let msg = ExecuteMsg::Send { - contract: contract.clone(), - amount: Uint128::zero(), - msg: send_msg.clone(), - }; - execute(deps.as_mut(), env, info, msg).unwrap(); - - // cannot send more than we have - let info = mock_info(addr1.as_ref(), &[]); - let env = mock_env(); - let msg = ExecuteMsg::Send { - contract: contract.clone(), - amount: too_much, - msg: send_msg.clone(), - }; - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); - - // valid transfer - let info = mock_info(addr1.as_ref(), &[]); - let env = mock_env(); - let msg = ExecuteMsg::Send { - contract: contract.clone(), - amount: transfer, - msg: send_msg.clone(), - }; - let res = execute(deps.as_mut(), env, info, msg).unwrap(); - assert_eq!(res.messages.len(), 1); - - // ensure proper send message sent - // this is the message we want delivered to the other side - let binary_msg = Cw20ReceiveMsg { - sender: addr1.clone(), - amount: transfer, - msg: send_msg, - } - .into_binary() - .unwrap(); - // and this is how it must be wrapped for the vm to process it - assert_eq!( - res.messages[0], - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: contract.clone(), - msg: binary_msg, - funds: vec![], - })) - ); - - // ensure balance is properly transferred - let remainder = amount1.checked_sub(transfer).unwrap(); - assert_eq!(get_balance(deps.as_ref(), addr1), remainder); - assert_eq!(get_balance(deps.as_ref(), contract), transfer); - assert_eq!( - query_token_info(deps.as_ref()).unwrap().total_supply, - amount1 - ); - } - - mod migration { - use super::*; - - use cosmwasm_std::Empty; - use cw20::{AllAllowancesResponse, AllSpenderAllowancesResponse, SpenderAllowanceInfo}; - use cw_multi_test::{App, Contract, ContractWrapper, Executor}; - use cw_utils::Expiration; - - fn cw20_contract() -> Box> { - let contract = ContractWrapper::new( - crate::contract::execute, - crate::contract::instantiate, - crate::contract::query, - ) - .with_migrate(crate::contract::migrate); - Box::new(contract) - } - - #[test] - fn test_migrate() { - let mut app = App::default(); - - let cw20_id = app.store_code(cw20_contract()); - let cw20_addr = app - .instantiate_contract( - cw20_id, - Addr::unchecked("sender"), - &InstantiateMsg { - name: "Token".to_string(), - symbol: "TOKEN".to_string(), - decimals: 6, - initial_balances: vec![Cw20Coin { - address: "sender".to_string(), - amount: Uint128::new(100), - }], - mint: None, - marketing: None, - }, - &[], - "TOKEN", - Some("sender".to_string()), - ) - .unwrap(); - - // no allowance to start - let allowance: AllAllowancesResponse = app - .wrap() - .query_wasm_smart( - cw20_addr.to_string(), - &QueryMsg::AllAllowances { - owner: "sender".to_string(), - start_after: None, - limit: None, - }, - ) - .unwrap(); - assert_eq!(allowance, AllAllowancesResponse::default()); - - // Set allowance - let allow1 = Uint128::new(7777); - let expires = Expiration::AtHeight(123_456); - let msg = CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: cw20_addr.to_string(), - msg: to_binary(&ExecuteMsg::IncreaseAllowance { - spender: "spender".into(), - amount: allow1, - expires: Some(expires), - }) - .unwrap(), - funds: vec![], - }); - app.execute(Addr::unchecked("sender"), msg).unwrap(); - - // Now migrate - app.execute( - Addr::unchecked("sender"), - CosmosMsg::Wasm(WasmMsg::Migrate { - contract_addr: cw20_addr.to_string(), - new_code_id: cw20_id, - msg: to_binary(&MigrateMsg {}).unwrap(), - }), - ) - .unwrap(); - - // Smoke check that the contract still works. - let balance: cw20::BalanceResponse = app - .wrap() - .query_wasm_smart( - cw20_addr.clone(), - &QueryMsg::Balance { - address: "sender".to_string(), - }, - ) - .unwrap(); - - assert_eq!(balance.balance, Uint128::new(100)); - - // Confirm that the allowance per spender is there - let allowance: AllSpenderAllowancesResponse = app - .wrap() - .query_wasm_smart( - cw20_addr, - &QueryMsg::AllSpenderAllowances { - spender: "spender".to_string(), - start_after: None, - limit: None, - }, - ) - .unwrap(); - assert_eq!( - allowance.allowances, - &[SpenderAllowanceInfo { - owner: "sender".to_string(), - allowance: allow1, - expires - }] - ); - } - } - - mod marketing { - use super::*; - - #[test] - fn update_unauthorised() { - let mut deps = mock_dependencies(); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![], - mint: None, - marketing: Some(InstantiateMarketingInfo { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some("marketing".to_owned()), - logo: Some(Logo::Url("url".to_owned())), - }), - }; - - let info = mock_info("creator", &[]); - - instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); - - let err = execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::UpdateMarketing { - project: Some("New project".to_owned()), - description: Some("Better description".to_owned()), - marketing: Some("creator".to_owned()), - }, - ) - .unwrap_err(); - - assert_eq!(err, ContractError::Unauthorized {}); - - // Ensure marketing didn't change - assert_eq!( - query_marketing_info(deps.as_ref()).unwrap(), - MarketingInfoResponse { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some(Addr::unchecked("marketing")), - logo: Some(LogoInfo::Url("url".to_owned())), - } - ); - - let err = query_download_logo(deps.as_ref()).unwrap_err(); - assert!( - matches!(err, StdError::NotFound { .. }), - "Expected StdError::NotFound, received {err}", - ); - } - - #[test] - fn update_project() { - let mut deps = mock_dependencies(); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![], - mint: None, - marketing: Some(InstantiateMarketingInfo { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some("creator".to_owned()), - logo: Some(Logo::Url("url".to_owned())), - }), - }; - - let info = mock_info("creator", &[]); - - instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); - - let res = execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::UpdateMarketing { - project: Some("New project".to_owned()), - description: None, - marketing: None, - }, - ) - .unwrap(); - - assert_eq!(res.messages, vec![]); - - assert_eq!( - query_marketing_info(deps.as_ref()).unwrap(), - MarketingInfoResponse { - project: Some("New project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some(Addr::unchecked("creator")), - logo: Some(LogoInfo::Url("url".to_owned())), - } - ); - - let err = query_download_logo(deps.as_ref()).unwrap_err(); - assert!( - matches!(err, StdError::NotFound { .. }), - "Expected StdError::NotFound, received {err}", - ); - } - - #[test] - fn clear_project() { - let mut deps = mock_dependencies(); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![], - mint: None, - marketing: Some(InstantiateMarketingInfo { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some("creator".to_owned()), - logo: Some(Logo::Url("url".to_owned())), - }), - }; - - let info = mock_info("creator", &[]); - - instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); - - let res = execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::UpdateMarketing { - project: Some("".to_owned()), - description: None, - marketing: None, - }, - ) - .unwrap(); - - assert_eq!(res.messages, vec![]); - - assert_eq!( - query_marketing_info(deps.as_ref()).unwrap(), - MarketingInfoResponse { - project: None, - description: Some("Description".to_owned()), - marketing: Some(Addr::unchecked("creator")), - logo: Some(LogoInfo::Url("url".to_owned())), - } - ); - - let err = query_download_logo(deps.as_ref()).unwrap_err(); - assert!( - matches!(err, StdError::NotFound { .. }), - "Expected StdError::NotFound, received {err}", - ); - } - - #[test] - fn update_description() { - let mut deps = mock_dependencies(); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![], - mint: None, - marketing: Some(InstantiateMarketingInfo { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some("creator".to_owned()), - logo: Some(Logo::Url("url".to_owned())), - }), - }; - - let info = mock_info("creator", &[]); - - instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); - - let res = execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::UpdateMarketing { - project: None, - description: Some("Better description".to_owned()), - marketing: None, - }, - ) - .unwrap(); - - assert_eq!(res.messages, vec![]); - - assert_eq!( - query_marketing_info(deps.as_ref()).unwrap(), - MarketingInfoResponse { - project: Some("Project".to_owned()), - description: Some("Better description".to_owned()), - marketing: Some(Addr::unchecked("creator")), - logo: Some(LogoInfo::Url("url".to_owned())), - } - ); - - let err = query_download_logo(deps.as_ref()).unwrap_err(); - assert!( - matches!(err, StdError::NotFound { .. }), - "Expected StdError::NotFound, received {err}", - ); - } - - #[test] - fn clear_description() { - let mut deps = mock_dependencies(); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![], - mint: None, - marketing: Some(InstantiateMarketingInfo { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some("creator".to_owned()), - logo: Some(Logo::Url("url".to_owned())), - }), - }; - - let info = mock_info("creator", &[]); - - instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); - - let res = execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::UpdateMarketing { - project: None, - description: Some("".to_owned()), - marketing: None, - }, - ) - .unwrap(); - - assert_eq!(res.messages, vec![]); - - assert_eq!( - query_marketing_info(deps.as_ref()).unwrap(), - MarketingInfoResponse { - project: Some("Project".to_owned()), - description: None, - marketing: Some(Addr::unchecked("creator")), - logo: Some(LogoInfo::Url("url".to_owned())), - } - ); - - let err = query_download_logo(deps.as_ref()).unwrap_err(); - assert!( - matches!(err, StdError::NotFound { .. }), - "Expected StdError::NotFound, received {err}", - ); - } - - #[test] - fn update_marketing() { - let mut deps = mock_dependencies(); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![], - mint: None, - marketing: Some(InstantiateMarketingInfo { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some("creator".to_owned()), - logo: Some(Logo::Url("url".to_owned())), - }), - }; - - let info = mock_info("creator", &[]); - - instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); - - let res = execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::UpdateMarketing { - project: None, - description: None, - marketing: Some("marketing".to_owned()), - }, - ) - .unwrap(); - - assert_eq!(res.messages, vec![]); - - assert_eq!( - query_marketing_info(deps.as_ref()).unwrap(), - MarketingInfoResponse { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some(Addr::unchecked("marketing")), - logo: Some(LogoInfo::Url("url".to_owned())), - } - ); - - let err = query_download_logo(deps.as_ref()).unwrap_err(); - assert!( - matches!(err, StdError::NotFound { .. }), - "Expected StdError::NotFound, received {err}", - ); - } - - #[test] - fn update_marketing_invalid() { - let mut deps = mock_dependencies(); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![], - mint: None, - marketing: Some(InstantiateMarketingInfo { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some("creator".to_owned()), - logo: Some(Logo::Url("url".to_owned())), - }), - }; - - let info = mock_info("creator", &[]); - - instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); - - let err = execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::UpdateMarketing { - project: None, - description: None, - marketing: Some("m".to_owned()), - }, - ) - .unwrap_err(); - - assert!( - matches!(err, ContractError::Std(_)), - "Expected Std error, received: {err}", - ); - - assert_eq!( - query_marketing_info(deps.as_ref()).unwrap(), - MarketingInfoResponse { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some(Addr::unchecked("creator")), - logo: Some(LogoInfo::Url("url".to_owned())), - } - ); - - let err = query_download_logo(deps.as_ref()).unwrap_err(); - assert!( - matches!(err, StdError::NotFound { .. }), - "Expected StdError::NotFound, received {err}", - ); - } - - #[test] - fn clear_marketing() { - let mut deps = mock_dependencies(); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![], - mint: None, - marketing: Some(InstantiateMarketingInfo { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some("creator".to_owned()), - logo: Some(Logo::Url("url".to_owned())), - }), - }; - - let info = mock_info("creator", &[]); - - instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); - - let res = execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::UpdateMarketing { - project: None, - description: None, - marketing: Some("".to_owned()), - }, - ) - .unwrap(); - - assert_eq!(res.messages, vec![]); - - assert_eq!( - query_marketing_info(deps.as_ref()).unwrap(), - MarketingInfoResponse { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: None, - logo: Some(LogoInfo::Url("url".to_owned())), - } - ); - - let err = query_download_logo(deps.as_ref()).unwrap_err(); - assert!( - matches!(err, StdError::NotFound { .. }), - "Expected StdError::NotFound, received {err}", - ); - } - - #[test] - fn update_logo_url() { - let mut deps = mock_dependencies(); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![], - mint: None, - marketing: Some(InstantiateMarketingInfo { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some("creator".to_owned()), - logo: Some(Logo::Url("url".to_owned())), - }), - }; - - let info = mock_info("creator", &[]); - - instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); - - let res = execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::UploadLogo(Logo::Url("new_url".to_owned())), - ) - .unwrap(); - - assert_eq!(res.messages, vec![]); - - assert_eq!( - query_marketing_info(deps.as_ref()).unwrap(), - MarketingInfoResponse { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some(Addr::unchecked("creator")), - logo: Some(LogoInfo::Url("new_url".to_owned())), - } - ); - - let err = query_download_logo(deps.as_ref()).unwrap_err(); - assert!( - matches!(err, StdError::NotFound { .. }), - "Expected StdError::NotFound, received {err}", - ); - } - - #[test] - fn update_logo_png() { - let mut deps = mock_dependencies(); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![], - mint: None, - marketing: Some(InstantiateMarketingInfo { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some("creator".to_owned()), - logo: Some(Logo::Url("url".to_owned())), - }), - }; - - let info = mock_info("creator", &[]); - - instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); - - let res = execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::UploadLogo(Logo::Embedded(EmbeddedLogo::Png(PNG_HEADER.into()))), - ) - .unwrap(); - - assert_eq!(res.messages, vec![]); - - assert_eq!( - query_marketing_info(deps.as_ref()).unwrap(), - MarketingInfoResponse { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some(Addr::unchecked("creator")), - logo: Some(LogoInfo::Embedded), - } - ); - - assert_eq!( - query_download_logo(deps.as_ref()).unwrap(), - DownloadLogoResponse { - mime_type: "image/png".to_owned(), - data: PNG_HEADER.into(), - } - ); - } - - #[test] - fn update_logo_svg() { - let mut deps = mock_dependencies(); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![], - mint: None, - marketing: Some(InstantiateMarketingInfo { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some("creator".to_owned()), - logo: Some(Logo::Url("url".to_owned())), - }), - }; - - let info = mock_info("creator", &[]); - - instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); - - let img = "".as_bytes(); - let res = execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::UploadLogo(Logo::Embedded(EmbeddedLogo::Svg(img.into()))), - ) - .unwrap(); - - assert_eq!(res.messages, vec![]); - - assert_eq!( - query_marketing_info(deps.as_ref()).unwrap(), - MarketingInfoResponse { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some(Addr::unchecked("creator")), - logo: Some(LogoInfo::Embedded), - } - ); - - assert_eq!( - query_download_logo(deps.as_ref()).unwrap(), - DownloadLogoResponse { - mime_type: "image/svg+xml".to_owned(), - data: img.into(), - } - ); - } - - #[test] - fn update_logo_png_oversized() { - let mut deps = mock_dependencies(); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![], - mint: None, - marketing: Some(InstantiateMarketingInfo { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some("creator".to_owned()), - logo: Some(Logo::Url("url".to_owned())), - }), - }; - - let info = mock_info("creator", &[]); - - instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); - - let img = [&PNG_HEADER[..], &[1; 6000][..]].concat(); - let err = execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::UploadLogo(Logo::Embedded(EmbeddedLogo::Png(img.into()))), - ) - .unwrap_err(); - - assert_eq!(err, ContractError::LogoTooBig {}); - - assert_eq!( - query_marketing_info(deps.as_ref()).unwrap(), - MarketingInfoResponse { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some(Addr::unchecked("creator")), - logo: Some(LogoInfo::Url("url".to_owned())), - } - ); - - let err = query_download_logo(deps.as_ref()).unwrap_err(); - assert!( - matches!(err, StdError::NotFound { .. }), - "Expected StdError::NotFound, received {err}", - ); - } - - #[test] - fn update_logo_svg_oversized() { - let mut deps = mock_dependencies(); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![], - mint: None, - marketing: Some(InstantiateMarketingInfo { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some("creator".to_owned()), - logo: Some(Logo::Url("url".to_owned())), - }), - }; - - let info = mock_info("creator", &[]); - - instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); - - let img = [ - "", - std::str::from_utf8(&[b'x'; 6000]).unwrap(), - "", - ] - .concat() - .into_bytes(); - - let err = execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::UploadLogo(Logo::Embedded(EmbeddedLogo::Svg(img.into()))), - ) - .unwrap_err(); - - assert_eq!(err, ContractError::LogoTooBig {}); - - assert_eq!( - query_marketing_info(deps.as_ref()).unwrap(), - MarketingInfoResponse { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some(Addr::unchecked("creator")), - logo: Some(LogoInfo::Url("url".to_owned())), - } - ); - - let err = query_download_logo(deps.as_ref()).unwrap_err(); - assert!( - matches!(err, StdError::NotFound { .. }), - "Expected StdError::NotFound, received {err}", - ); - } - - #[test] - fn update_logo_png_invalid() { - let mut deps = mock_dependencies(); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![], - mint: None, - marketing: Some(InstantiateMarketingInfo { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some("creator".to_owned()), - logo: Some(Logo::Url("url".to_owned())), - }), - }; - - let info = mock_info("creator", &[]); - - instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); - - let img = &[1]; - let err = execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::UploadLogo(Logo::Embedded(EmbeddedLogo::Png(img.into()))), - ) - .unwrap_err(); - - assert_eq!(err, ContractError::InvalidPngHeader {}); - - assert_eq!( - query_marketing_info(deps.as_ref()).unwrap(), - MarketingInfoResponse { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some(Addr::unchecked("creator")), - logo: Some(LogoInfo::Url("url".to_owned())), - } - ); - - let err = query_download_logo(deps.as_ref()).unwrap_err(); - assert!( - matches!(err, StdError::NotFound { .. }), - "Expected StdError::NotFound, received {err}", - ); - } - - #[test] - fn update_logo_svg_invalid() { - let mut deps = mock_dependencies(); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![], - mint: None, - marketing: Some(InstantiateMarketingInfo { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some("creator".to_owned()), - logo: Some(Logo::Url("url".to_owned())), - }), - }; - - let info = mock_info("creator", &[]); - - instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); - - let img = &[1]; - - let err = execute( - deps.as_mut(), - mock_env(), - info, - ExecuteMsg::UploadLogo(Logo::Embedded(EmbeddedLogo::Svg(img.into()))), - ) - .unwrap_err(); - - assert_eq!(err, ContractError::InvalidXmlPreamble {}); - - assert_eq!( - query_marketing_info(deps.as_ref()).unwrap(), - MarketingInfoResponse { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some(Addr::unchecked("creator")), - logo: Some(LogoInfo::Url("url".to_owned())), - } - ); - - let err = query_download_logo(deps.as_ref()).unwrap_err(); - assert!( - matches!(err, StdError::NotFound { .. }), - "Expected StdError::NotFound, received {err}", - ); - } - } -} diff --git a/contracts/cw20-base/src/enumerable.rs b/contracts/cw20-base/src/enumerable.rs deleted file mode 100644 index f465134d2..000000000 --- a/contracts/cw20-base/src/enumerable.rs +++ /dev/null @@ -1,319 +0,0 @@ -use cosmwasm_std::{Deps, Order, StdResult}; -use cw20::{ - AllAccountsResponse, AllAllowancesResponse, AllSpenderAllowancesResponse, AllowanceInfo, - SpenderAllowanceInfo, -}; - -use crate::state::{ALLOWANCES, ALLOWANCES_SPENDER, BALANCES}; -use cw_storage_plus::Bound; - -// settings for pagination -const MAX_LIMIT: u32 = 30; -const DEFAULT_LIMIT: u32 = 10; - -pub fn query_owner_allowances( - deps: Deps, - owner: String, - start_after: Option, - limit: Option, -) -> StdResult { - 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 = 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::>()?; - Ok(AllAllowancesResponse { allowances }) -} - -pub fn query_spender_allowances( - deps: Deps, - spender: String, - start_after: Option, - limit: Option, -) -> StdResult { - let spender_addr = deps.api.addr_validate(&spender)?; - 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 = 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::>()?; - Ok(AllSpenderAllowancesResponse { allowances }) -} - -pub fn query_all_accounts( - deps: Deps, - start_after: Option, - limit: Option, -) -> StdResult { - let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - let start = start_after.map(|s| Bound::ExclusiveRaw(s.into())); - - let accounts = BALANCES - .keys(deps.storage, start, None, Order::Ascending) - .take(limit) - .map(|item| item.map(Into::into)) - .collect::>()?; - - Ok(AllAccountsResponse { accounts }) -} - -#[cfg(test)] -mod tests { - use super::*; - - use cosmwasm_std::testing::{mock_dependencies_with_balance, mock_env, mock_info}; - use cosmwasm_std::{coins, from_binary, DepsMut, Uint128}; - use cw20::{Cw20Coin, Expiration, TokenInfoResponse}; - - use crate::contract::{execute, instantiate, query, query_token_info}; - use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; - - // this will set up the instantiation for other tests - fn do_instantiate(mut deps: DepsMut, addr: &str, amount: Uint128) -> TokenInfoResponse { - let instantiate_msg = InstantiateMsg { - name: "Auto Gen".to_string(), - symbol: "AUTO".to_string(), - decimals: 3, - initial_balances: vec![Cw20Coin { - address: addr.into(), - amount, - }], - mint: None, - marketing: None, - }; - let info = mock_info("creator", &[]); - let env = mock_env(); - instantiate(deps.branch(), env, info, instantiate_msg).unwrap(); - query_token_info(deps.as_ref()).unwrap() - } - - #[test] - fn query_all_owner_allowances_works() { - let mut deps = mock_dependencies_with_balance(&coins(2, "token")); - - let owner = String::from("owner"); - // these are in alphabetical order same than insert order - let spender1 = String::from("earlier"); - let spender2 = String::from("later"); - - let info = mock_info(owner.as_ref(), &[]); - let env = mock_env(); - do_instantiate(deps.as_mut(), &owner, Uint128::new(12340000)); - - // no allowance to start - let allowances = query_owner_allowances(deps.as_ref(), owner.clone(), None, None).unwrap(); - assert_eq!(allowances.allowances, vec![]); - - // set allowance with height expiration - let allow1 = Uint128::new(7777); - let expires = Expiration::AtHeight(123_456); - let msg = ExecuteMsg::IncreaseAllowance { - spender: spender1.clone(), - amount: allow1, - expires: Some(expires), - }; - execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); - - // set allowance with no expiration - let allow2 = Uint128::new(54321); - let msg = ExecuteMsg::IncreaseAllowance { - spender: spender2.clone(), - amount: allow2, - expires: None, - }; - execute(deps.as_mut(), env, info, msg).unwrap(); - - // query list gets 2 - let allowances = query_owner_allowances(deps.as_ref(), owner.clone(), None, None).unwrap(); - assert_eq!(allowances.allowances.len(), 2); - - // first one is spender1 (order of CanonicalAddr uncorrelated with String) - let allowances = - query_owner_allowances(deps.as_ref(), owner.clone(), None, Some(1)).unwrap(); - assert_eq!(allowances.allowances.len(), 1); - let allow = &allowances.allowances[0]; - assert_eq!(&allow.spender, &spender1); - assert_eq!(&allow.expires, &expires); - assert_eq!(&allow.allowance, &allow1); - - // next one is spender2 - let allowances = query_owner_allowances( - deps.as_ref(), - owner, - Some(allow.spender.clone()), - Some(10000), - ) - .unwrap(); - assert_eq!(allowances.allowances.len(), 1); - let allow = &allowances.allowances[0]; - assert_eq!(&allow.spender, &spender2); - assert_eq!(&allow.expires, &Expiration::Never {}); - assert_eq!(&allow.allowance, &allow2); - } - - #[test] - fn query_all_spender_allowances_works() { - let mut deps = mock_dependencies_with_balance(&coins(2, "token")); - - // these are in alphabetical order same than insert order - let owner1 = String::from("owner1"); - let owner2 = String::from("owner2"); - let spender = String::from("spender"); - - let info = mock_info(owner1.as_ref(), &[]); - let env = mock_env(); - do_instantiate(deps.as_mut(), &owner1, Uint128::new(12340000)); - - // no allowance to start - let allowances = - query_spender_allowances(deps.as_ref(), spender.clone(), None, None).unwrap(); - assert_eq!(allowances.allowances, vec![]); - - // set allowance with height expiration - let allow1 = Uint128::new(7777); - let expires = Expiration::AtHeight(123_456); - let msg = ExecuteMsg::IncreaseAllowance { - spender: spender.clone(), - amount: allow1, - expires: Some(expires), - }; - execute(deps.as_mut(), env, info, msg).unwrap(); - - // set allowance with no expiration, from the other owner - let info = mock_info(owner2.as_ref(), &[]); - let env = mock_env(); - do_instantiate(deps.as_mut(), &owner2, Uint128::new(12340000)); - - let allow2 = Uint128::new(54321); - let msg = ExecuteMsg::IncreaseAllowance { - spender: spender.clone(), - amount: allow2, - expires: None, - }; - execute(deps.as_mut(), env.clone(), info, msg).unwrap(); - - // query list gets both - let msg = QueryMsg::AllSpenderAllowances { - spender: spender.clone(), - start_after: None, - limit: None, - }; - let allowances: AllSpenderAllowancesResponse = - from_binary(&query(deps.as_ref(), env.clone(), msg).unwrap()).unwrap(); - assert_eq!(allowances.allowances.len(), 2); - - // one is owner1 (order of CanonicalAddr uncorrelated with String) - let msg = QueryMsg::AllSpenderAllowances { - spender: spender.clone(), - start_after: None, - limit: Some(1), - }; - let allowances: AllSpenderAllowancesResponse = - from_binary(&query(deps.as_ref(), env.clone(), msg).unwrap()).unwrap(); - assert_eq!(allowances.allowances.len(), 1); - let allow = &allowances.allowances[0]; - assert_eq!(&allow.owner, &owner1); - assert_eq!(&allow.expires, &expires); - assert_eq!(&allow.allowance, &allow1); - - // other one is owner2 - let msg = QueryMsg::AllSpenderAllowances { - spender, - start_after: Some(owner1), - limit: Some(10000), - }; - let allowances: AllSpenderAllowancesResponse = - from_binary(&query(deps.as_ref(), env, msg).unwrap()).unwrap(); - assert_eq!(allowances.allowances.len(), 1); - let allow = &allowances.allowances[0]; - assert_eq!(&allow.owner, &owner2); - assert_eq!(&allow.expires, &Expiration::Never {}); - assert_eq!(&allow.allowance, &allow2); - } - - #[test] - fn query_all_accounts_works() { - let mut deps = mock_dependencies_with_balance(&coins(2, "token")); - - // insert order and lexicographical order are different - let acct1 = String::from("acct01"); - let acct2 = String::from("zebra"); - let acct3 = String::from("nice"); - let acct4 = String::from("aaaardvark"); - let expected_order = [acct4.clone(), acct1.clone(), acct3.clone(), acct2.clone()]; - - do_instantiate(deps.as_mut(), &acct1, Uint128::new(12340000)); - - // put money everywhere (to create balanaces) - let info = mock_info(acct1.as_ref(), &[]); - let env = mock_env(); - execute( - deps.as_mut(), - env.clone(), - info.clone(), - ExecuteMsg::Transfer { - recipient: acct2, - amount: Uint128::new(222222), - }, - ) - .unwrap(); - execute( - deps.as_mut(), - env.clone(), - info.clone(), - ExecuteMsg::Transfer { - recipient: acct3, - amount: Uint128::new(333333), - }, - ) - .unwrap(); - execute( - deps.as_mut(), - env, - info, - ExecuteMsg::Transfer { - recipient: acct4, - amount: Uint128::new(444444), - }, - ) - .unwrap(); - - // make sure we get the proper results - let accounts = query_all_accounts(deps.as_ref(), None, None).unwrap(); - assert_eq!(accounts.accounts, expected_order); - - // let's do pagination - let accounts = query_all_accounts(deps.as_ref(), None, Some(2)).unwrap(); - assert_eq!(accounts.accounts, expected_order[0..2].to_vec()); - - let accounts = - query_all_accounts(deps.as_ref(), Some(accounts.accounts[1].clone()), Some(1)).unwrap(); - assert_eq!(accounts.accounts, expected_order[2..3].to_vec()); - - let accounts = - query_all_accounts(deps.as_ref(), Some(accounts.accounts[0].clone()), Some(777)) - .unwrap(); - assert_eq!(accounts.accounts, expected_order[3..].to_vec()); - } -} diff --git a/contracts/cw20-base/src/error.rs b/contracts/cw20-base/src/error.rs deleted file mode 100644 index a0b880c97..000000000 --- a/contracts/cw20-base/src/error.rs +++ /dev/null @@ -1,43 +0,0 @@ -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 {}, - - // Unused error case. Zero is now treated like every other value. - #[deprecated(note = "Unused. All zero amount checks have been removed")] - #[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 deleted file mode 100644 index 7b601f25d..000000000 --- a/contracts/cw20-base/src/lib.rs +++ /dev/null @@ -1,24 +0,0 @@ -/*! -This is a basic implementation of a cw20 contract. It implements -the [CW20 spec](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw20/README.md) and is designed to -be deployed as is, or imported into other contracts to easily build -cw20-compatible tokens with custom logic. - -Implements: - -- [x] CW20 Base -- [x] Mintable extension -- [x] Allowances extension - -For more information on this contract, please check out the -[README](https://github.com/CosmWasm/cw-plus/blob/main/contracts/cw20-base/README.md). -*/ - -pub mod allowances; -pub mod contract; -pub mod enumerable; -mod error; -pub mod msg; -pub mod state; - -pub use crate::error::ContractError; diff --git a/contracts/cw20-base/src/msg.rs b/contracts/cw20-base/src/msg.rs deleted file mode 100644 index 208871255..000000000 --- a/contracts/cw20-base/src/msg.rs +++ /dev/null @@ -1,175 +0,0 @@ -use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{StdError, StdResult, Uint128}; -use cw20::{Cw20Coin, Logo, MinterResponse}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -pub use cw20::Cw20ExecuteMsg as ExecuteMsg; - -#[cw_serde] -pub struct InstantiateMarketingInfo { - pub project: Option, - pub description: Option, - pub marketing: Option, - pub logo: Option, -} - -#[cw_serde] -#[cfg_attr(test, derive(Default))] -pub struct InstantiateMsg { - pub name: String, - pub symbol: String, - pub decimals: u8, - pub initial_balances: Vec, - pub mint: Option, - pub marketing: Option, -} - -impl InstantiateMsg { - pub fn get_cap(&self) -> Option { - self.mint.as_ref().and_then(|v| v.cap) - } - - pub fn validate(&self) -> StdResult<()> { - // Check name, symbol, decimals - if !self.has_valid_name() { - return Err(StdError::generic_err( - "Name is not in the expected format (3-50 UTF-8 bytes)", - )); - } - if !self.has_valid_symbol() { - return Err(StdError::generic_err( - "Ticker symbol is not in expected format [a-zA-Z\\-]{3,12}", - )); - } - if self.decimals > 18 { - return Err(StdError::generic_err("Decimals must not exceed 18")); - } - Ok(()) - } - - fn has_valid_name(&self) -> bool { - let bytes = self.name.as_bytes(); - if bytes.len() < 3 || bytes.len() > 50 { - return false; - } - true - } - - fn has_valid_symbol(&self) -> bool { - let bytes = self.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 - } -} - -#[cw_serde] -#[derive(QueryResponses)] -pub enum QueryMsg { - /// Returns the current balance of the given address, 0 if unset. - #[returns(cw20::BalanceResponse)] - Balance { address: String }, - /// Returns metadata on the contract - name, decimals, supply, etc. - #[returns(cw20::TokenInfoResponse)] - TokenInfo {}, - /// Only with "mintable" extension. - /// Returns who can mint and the hard cap on maximum tokens after minting. - #[returns(cw20::MinterResponse)] - Minter {}, - /// Only with "allowance" extension. - /// Returns how much spender can use from owner account, 0 if unset. - #[returns(cw20::AllowanceResponse)] - Allowance { owner: String, spender: String }, - /// Only with "enumerable" extension (and "allowances") - /// Returns all allowances this owner has approved. Supports pagination. - #[returns(cw20::AllAllowancesResponse)] - AllAllowances { - owner: String, - start_after: Option, - limit: Option, - }, - /// Only with "enumerable" extension (and "allowances") - /// Returns all allowances this spender has been granted. Supports pagination. - #[returns(cw20::AllSpenderAllowancesResponse)] - AllSpenderAllowances { - spender: String, - start_after: Option, - limit: Option, - }, - /// Only with "enumerable" extension - /// Returns all accounts that have balances. Supports pagination. - #[returns(cw20::AllAccountsResponse)] - AllAccounts { - start_after: Option, - limit: Option, - }, - /// Only with "marketing" extension - /// Returns more metadata on the contract to display in the client: - /// - description, logo, project url, etc. - #[returns(cw20::MarketingInfoResponse)] - MarketingInfo {}, - /// Only with "marketing" extension - /// Downloads the embedded logo data (if stored on chain). Errors if no logo data is stored for this - /// contract. - #[returns(cw20::DownloadLogoResponse)] - DownloadLogo {}, -} - -#[derive(Serialize, Deserialize, JsonSchema)] -pub struct MigrateMsg {} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn validate_instantiatemsg_name() { - // Too short - let mut msg = InstantiateMsg { - name: str::repeat("a", 2), - ..InstantiateMsg::default() - }; - assert!(!msg.has_valid_name()); - - // In the correct length range - msg.name = str::repeat("a", 3); - assert!(msg.has_valid_name()); - - // Too long - msg.name = str::repeat("a", 51); - assert!(!msg.has_valid_name()); - } - - #[test] - fn validate_instantiatemsg_symbol() { - // Too short - let mut msg = InstantiateMsg { - symbol: str::repeat("a", 2), - ..InstantiateMsg::default() - }; - assert!(!msg.has_valid_symbol()); - - // In the correct length range - msg.symbol = str::repeat("a", 3); - assert!(msg.has_valid_symbol()); - - // Too long - msg.symbol = str::repeat("a", 13); - assert!(!msg.has_valid_symbol()); - - // Has illegal char - let illegal_chars = [[64u8], [91u8], [123u8]]; - illegal_chars.iter().for_each(|c| { - let c = std::str::from_utf8(c).unwrap(); - msg.symbol = str::repeat(c, 3); - assert!(!msg.has_valid_symbol()); - }); - } -} diff --git a/contracts/cw20-base/src/state.rs b/contracts/cw20-base/src/state.rs deleted file mode 100644 index dc02c6f19..000000000 --- a/contracts/cw20-base/src/state.rs +++ /dev/null @@ -1,36 +0,0 @@ -use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Uint128}; -use cw_storage_plus::{Item, Map}; - -use cw20::{AllowanceResponse, Logo, MarketingInfoResponse}; - -#[cw_serde] -pub struct TokenInfo { - pub name: String, - pub symbol: String, - pub decimals: u8, - pub total_supply: Uint128, - pub mint: Option, -} - -#[cw_serde] -pub struct MinterData { - pub minter: Addr, - /// cap is how many more tokens can be issued by the minter - pub cap: Option, -} - -impl TokenInfo { - pub fn get_cap(&self) -> Option { - self.mint.as_ref().and_then(|v| v.cap) - } -} - -pub const TOKEN_INFO: Item = Item::new("token_info"); -pub const MARKETING_INFO: Item = Item::new("marketing_info"); -pub const LOGO: Item = Item::new("logo"); -pub const BALANCES: Map<&Addr, Uint128> = Map::new("balance"); -pub const ALLOWANCES: Map<(&Addr, &Addr), AllowanceResponse> = Map::new("allowance"); -// TODO: After https://github.com/CosmWasm/cw-plus/issues/670 is implemented, replace this with a `MultiIndex` over `ALLOWANCES` -pub const ALLOWANCES_SPENDER: Map<(&Addr, &Addr), AllowanceResponse> = - Map::new("allowance_spender"); diff --git a/contracts/cw20-ics20/.cargo/config b/contracts/cw20-ics20/.cargo/config deleted file mode 100644 index f5174787c..000000000 --- a/contracts/cw20-ics20/.cargo/config +++ /dev/null @@ -1,6 +0,0 @@ -[alias] -wasm = "build --release --lib --target wasm32-unknown-unknown" -wasm-debug = "build --lib --target wasm32-unknown-unknown" -unit-test = "test --lib" -integration-test = "test --test integration" -schema = "run --bin schema" diff --git a/contracts/cw20-ics20/Cargo.toml b/contracts/cw20-ics20/Cargo.toml deleted file mode 100644 index b4c61eca0..000000000 --- a/contracts/cw20-ics20/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "cw20-ics20" -version = "1.1.2" -authors = ["Ethan Frey "] -edition = "2021" -description = "IBC Enabled contracts that receives CW20 tokens and sends them over ICS20 to a remote chain" -license = "Apache-2.0" -repository = "https://github.com/CosmWasm/cw-plus" -homepage = "https://cosmwasm.com" -documentation = "https://docs.cosmwasm.com" - -[lib] -crate-type = ["cdylib", "rlib"] - -[features] -backtraces = ["cosmwasm-std/backtraces"] -# use library feature to disable all init/handle/query exports -library = [] - -[dependencies] -cosmwasm-schema = { version = "1.4.0" } -cw-utils = "1.0.1" -cw2 = { path = "../../packages/cw2", version = "1.1.2" } -cw20 = { path = "../../packages/cw20", version = "1.1.2" } -cosmwasm-std = { version = "1.4.0", features = ["stargate"] } -cw-storage-plus = "1.1.0" -cw-controllers = { path = "../../packages/controllers", version = "1.1.2" } -schemars = "0.8.15" -semver = "1" -serde = { version = "1.0.188", default-features = false, features = ["derive"] } -thiserror = { version = "1.0.49" } diff --git a/contracts/cw20-ics20/README.md b/contracts/cw20-ics20/README.md deleted file mode 100644 index 9be95ff95..000000000 --- a/contracts/cw20-ics20/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# CW20 ICS20 - -This is an *IBC Enabled* contract that allows us to send CW20 tokens from one chain over the standard ICS20 -protocol to the bank module of another chain. In short, it lets us send our custom CW20 tokens with IBC and use -them just like native tokens on other chains. - -It is only designed to send tokens and redeem previously sent tokens. It will not mint tokens belonging -to assets originating on the foreign chain. This is different than the Golang `ibctransfer` module, but -we properly implement ICS20 and respond with an error message... let's hope the Go side handles this correctly. - -## Workflow - -The contract starts with minimal state. It just stores a default timeout in seconds for all packets it sends. -Most importantly it binds a local IBC port to enable channel connections. - -An external party first needs to make one or more channels using this contract as one endpoint. It will use standard ics20 -unordered channels for the version negotiation. Once established, it manages a list of known channels. You can use -[ts-relayer](https://github.com/confio/ts-relayer) `ibc-setup ics20` command to create these. - -After there is at least one channel, you can send any CW20 token to this contract via the -[receiver pattern](https://github.com/CosmWasm/cw-plus/blob/master/packages/cw20/README.md#receiver). -The receive message must contain the channel to send over and the remote address to send to. It may optionally -include a custom timeout. - -## Messages - -It only accepts CW20ReceiveMsg from a cw20 contract. The data sent along with that message must be a JSON-serialized -TransferMsg: - -```rust -pub struct TransferMsg { - /// The local channel to send the packets on - pub channel: String, - /// The remote address to send to - /// Don't use HumanAddress as this will likely have a different Bech32 prefix than we use - /// and cannot be validated locally - pub remote_address: String, - /// How long the packet lives in seconds. If not specified, use default_timeout - pub timeout: Option, -} -``` - -In addition, it supports directly sending native tokens via `ExecuteMsg::Transfer(TransferMsg)`. -You must send *exactly one* coin denom along with the transfer message, and that amount will be transfered -to the remote host. - -## Queries - -Queries only make sense relative to the established channels of this contract. - -* `Port{}` - returns the port ID this contract has bound, so you can create channels. This info can be queried - via wasmd contract info query, but we expose another query here for convenience. -* `ListChannels{}` - returns a (currently unpaginated) list of all channels that have been created on this contract. - Returns their local channelId along with some basic metadata, like the remote port/channel and the connection they - run on top of. -* `Channel{id}` - returns more detailed information on one specific channel. In addition to the information available - in the list view, it returns the current outstanding balance on that channel, as well as the total amount that - has ever been sent on the channel. - -## IBC Responses - -These are defined by the ICS20 spec. - -Notably, each Channel has a balance of tokens sent over that channel. If an incoming transfer request comes in for -a denom it does not know, or for a balance larger than we have sent, we will return an error in the acknowledgement -packet. \ No newline at end of file diff --git a/contracts/cw20-ics20/src/amount.rs b/contracts/cw20-ics20/src/amount.rs deleted file mode 100644 index a0c42d9f8..000000000 --- a/contracts/cw20-ics20/src/amount.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::error::ContractError; -use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Coin, Uint128}; -use cw20::Cw20Coin; -use std::convert::TryInto; - -#[cw_serde] -pub enum Amount { - Native(Coin), - // FIXME? USe Cw20CoinVerified, and validate cw20 addresses - Cw20(Cw20Coin), -} - -impl Amount { - // TODO: write test for this - pub fn from_parts(denom: String, amount: Uint128) -> Self { - if denom.starts_with("cw20:") { - let address = denom.get(5..).unwrap().into(); - Amount::Cw20(Cw20Coin { address, amount }) - } else { - Amount::Native(Coin { denom, amount }) - } - } - - pub fn cw20(amount: u128, addr: &str) -> Self { - Amount::Cw20(Cw20Coin { - address: addr.into(), - amount: Uint128::new(amount), - }) - } - - pub fn native(amount: u128, denom: &str) -> Self { - Amount::Native(Coin { - denom: denom.to_string(), - amount: Uint128::new(amount), - }) - } -} - -impl Amount { - pub fn denom(&self) -> String { - match self { - Amount::Native(c) => c.denom.clone(), - Amount::Cw20(c) => format!("cw20:{}", c.address.as_str()), - } - } - - pub fn amount(&self) -> Uint128 { - match self { - Amount::Native(c) => c.amount, - Amount::Cw20(c) => c.amount, - } - } - - /// convert the amount into u64 - pub fn u64_amount(&self) -> Result { - Ok(self.amount().u128().try_into()?) - } - - pub fn is_empty(&self) -> bool { - match self { - Amount::Native(c) => c.amount.is_zero(), - Amount::Cw20(c) => c.amount.is_zero(), - } - } -} diff --git a/contracts/cw20-ics20/src/bin/schema.rs b/contracts/cw20-ics20/src/bin/schema.rs deleted file mode 100644 index 55c982ad0..000000000 --- a/contracts/cw20-ics20/src/bin/schema.rs +++ /dev/null @@ -1,11 +0,0 @@ -use cosmwasm_schema::write_api; - -use cw20_ics20::msg::{ExecuteMsg, InitMsg, QueryMsg}; - -fn main() { - write_api! { - instantiate: InitMsg, - execute: ExecuteMsg, - query: QueryMsg, - } -} diff --git a/contracts/cw20-ics20/src/contract.rs b/contracts/cw20-ics20/src/contract.rs deleted file mode 100644 index f33aad865..000000000 --- a/contracts/cw20-ics20/src/contract.rs +++ /dev/null @@ -1,710 +0,0 @@ -#[cfg(not(feature = "library"))] -use cosmwasm_std::entry_point; -use cosmwasm_std::{ - from_binary, to_binary, Addr, Binary, Deps, DepsMut, Env, IbcMsg, IbcQuery, MessageInfo, Order, - PortIdResponse, Response, StdError, StdResult, -}; -use semver::Version; - -use cw2::{get_contract_version, set_contract_version}; -use cw20::{Cw20Coin, Cw20ReceiveMsg}; -use cw_storage_plus::Bound; - -use crate::amount::Amount; -use crate::error::ContractError; -use crate::ibc::Ics20Packet; -use crate::migrations::{v1, v2}; -use crate::msg::{ - AllowMsg, AllowedInfo, AllowedResponse, ChannelResponse, ConfigResponse, ExecuteMsg, InitMsg, - ListAllowedResponse, ListChannelsResponse, MigrateMsg, PortResponse, QueryMsg, TransferMsg, -}; -use crate::state::{ - increase_channel_balance, AllowInfo, Config, ADMIN, ALLOW_LIST, CHANNEL_INFO, CHANNEL_STATE, - CONFIG, -}; -use cw_utils::{maybe_addr, nonpayable, one_coin}; - -// version info for migration info -const CONTRACT_NAME: &str = "crates.io:cw20-ics20"; -const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn instantiate( - mut deps: DepsMut, - _env: Env, - _info: MessageInfo, - msg: InitMsg, -) -> Result { - set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - let cfg = Config { - default_timeout: msg.default_timeout, - default_gas_limit: msg.default_gas_limit, - }; - CONFIG.save(deps.storage, &cfg)?; - - let admin = deps.api.addr_validate(&msg.gov_contract)?; - ADMIN.set(deps.branch(), Some(admin))?; - - // add all allows - for allowed in msg.allowlist { - let contract = deps.api.addr_validate(&allowed.contract)?; - let info = AllowInfo { - gas_limit: allowed.gas_limit, - }; - ALLOW_LIST.save(deps.storage, &contract, &info)?; - } - Ok(Response::default()) -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn execute( - deps: DepsMut, - env: Env, - info: MessageInfo, - msg: ExecuteMsg, -) -> Result { - match msg { - ExecuteMsg::Receive(msg) => execute_receive(deps, env, info, msg), - ExecuteMsg::Transfer(msg) => { - let coin = one_coin(&info)?; - execute_transfer(deps, env, msg, Amount::Native(coin), info.sender) - } - ExecuteMsg::Allow(allow) => execute_allow(deps, env, info, allow), - ExecuteMsg::UpdateAdmin { admin } => { - let admin = deps.api.addr_validate(&admin)?; - Ok(ADMIN.execute_update_admin(deps, info, Some(admin))?) - } - } -} - -pub fn execute_receive( - deps: DepsMut, - env: Env, - info: MessageInfo, - wrapper: Cw20ReceiveMsg, -) -> Result { - nonpayable(&info)?; - - let msg: TransferMsg = from_binary(&wrapper.msg)?; - let amount = Amount::Cw20(Cw20Coin { - address: info.sender.to_string(), - amount: wrapper.amount, - }); - let api = deps.api; - execute_transfer(deps, env, msg, amount, api.addr_validate(&wrapper.sender)?) -} - -pub fn execute_transfer( - deps: DepsMut, - env: Env, - msg: TransferMsg, - amount: Amount, - sender: Addr, -) -> Result { - if amount.is_empty() { - return Err(ContractError::NoFunds {}); - } - // ensure the requested channel is registered - if !CHANNEL_INFO.has(deps.storage, &msg.channel) { - return Err(ContractError::NoSuchChannel { id: msg.channel }); - } - let config = CONFIG.load(deps.storage)?; - - // if cw20 token, validate and ensure it is whitelisted, or we set default gas limit - if let Amount::Cw20(coin) = &amount { - let addr = deps.api.addr_validate(&coin.address)?; - // if limit is set, then we always allow cw20 - if config.default_gas_limit.is_none() { - ALLOW_LIST - .may_load(deps.storage, &addr)? - .ok_or(ContractError::NotOnAllowList)?; - } - }; - - // delta from user is in seconds - let timeout_delta = match msg.timeout { - Some(t) => t, - None => config.default_timeout, - }; - // timeout is in nanoseconds - let timeout = env.block.time.plus_seconds(timeout_delta); - - // build ics20 packet - let packet = Ics20Packet::new( - amount.amount(), - amount.denom(), - sender.as_ref(), - &msg.remote_address, - ) - .with_memo(msg.memo); - packet.validate()?; - - // Update the balance now (optimistically) like ibctransfer modules. - // In on_packet_failure (ack with error message or a timeout), we reduce the balance appropriately. - // This means the channel works fine if success acks are not relayed. - increase_channel_balance(deps.storage, &msg.channel, &amount.denom(), amount.amount())?; - - // prepare ibc message - let msg = IbcMsg::SendPacket { - channel_id: msg.channel, - data: to_binary(&packet)?, - timeout: timeout.into(), - }; - - // send response - let res = Response::new() - .add_message(msg) - .add_attribute("action", "transfer") - .add_attribute("sender", &packet.sender) - .add_attribute("receiver", &packet.receiver) - .add_attribute("denom", &packet.denom) - .add_attribute("amount", packet.amount.to_string()); - Ok(res) -} - -/// The gov contract can allow new contracts, or increase the gas limit on existing contracts. -/// It cannot block or reduce the limit to avoid forcible sticking tokens in the channel. -pub fn execute_allow( - deps: DepsMut, - _env: Env, - info: MessageInfo, - allow: AllowMsg, -) -> Result { - ADMIN.assert_admin(deps.as_ref(), &info.sender)?; - - let contract = deps.api.addr_validate(&allow.contract)?; - let set = AllowInfo { - gas_limit: allow.gas_limit, - }; - ALLOW_LIST.update(deps.storage, &contract, |old| { - if let Some(old) = old { - // we must ensure it increases the limit - match (old.gas_limit, set.gas_limit) { - (None, Some(_)) => return Err(ContractError::CannotLowerGas), - (Some(old), Some(new)) if new < old => return Err(ContractError::CannotLowerGas), - _ => {} - }; - } - Ok(AllowInfo { - gas_limit: allow.gas_limit, - }) - })?; - - let gas = if let Some(gas) = allow.gas_limit { - gas.to_string() - } else { - "None".to_string() - }; - - let res = Response::new() - .add_attribute("action", "allow") - .add_attribute("contract", allow.contract) - .add_attribute("gas_limit", gas); - Ok(res) -} - -const MIGRATE_MIN_VERSION: &str = "0.11.1"; -const MIGRATE_VERSION_2: &str = "0.12.0-alpha1"; -// the new functionality starts in 0.13.1, this is the last release that needs to be migrated to v3 -const MIGRATE_VERSION_3: &str = "0.13.0"; - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(mut deps: DepsMut, env: Env, msg: MigrateMsg) -> Result { - let version: Version = CONTRACT_VERSION.parse().map_err(from_semver)?; - let stored = get_contract_version(deps.storage)?; - let storage_version: Version = stored.version.parse().map_err(from_semver)?; - - // First, ensure we are working from an equal or older version of this contract - // wrong type - if CONTRACT_NAME != stored.contract { - return Err(ContractError::CannotMigrate { - previous_contract: stored.contract, - }); - } - // existing one is newer - if storage_version > version { - return Err(ContractError::CannotMigrateVersion { - previous_version: stored.version, - }); - } - - // Then, run the proper migration - if storage_version < MIGRATE_MIN_VERSION.parse().map_err(from_semver)? { - return Err(ContractError::CannotMigrateVersion { - previous_version: stored.version, - }); - } - // run the v1->v2 conversion if we are v1 style - if storage_version <= MIGRATE_VERSION_2.parse().map_err(from_semver)? { - let old_config = v1::CONFIG.load(deps.storage)?; - ADMIN.set(deps.branch(), Some(old_config.gov_contract))?; - let config = Config { - default_timeout: old_config.default_timeout, - default_gas_limit: None, - }; - CONFIG.save(deps.storage, &config)?; - } - // run the v2->v3 conversion if we are v2 style - if storage_version <= MIGRATE_VERSION_3.parse().map_err(from_semver)? { - v2::update_balances(deps.branch(), &env)?; - } - // otherwise no migration (yet) - add them here - - // always allow setting the default gas limit via MigrateMsg, even if same version - // (Note this doesn't allow unsetting it now) - if msg.default_gas_limit.is_some() { - CONFIG.update(deps.storage, |mut old| -> StdResult<_> { - old.default_gas_limit = msg.default_gas_limit; - Ok(old) - })?; - } - - // we don't need to save anything if migrating from the same version - if storage_version < version { - set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - } - - Ok(Response::new()) -} - -fn from_semver(err: semver::Error) -> StdError { - StdError::generic_err(format!("Semver: {err}")) -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { - match msg { - QueryMsg::Port {} => to_binary(&query_port(deps)?), - QueryMsg::ListChannels {} => to_binary(&query_list(deps)?), - QueryMsg::Channel { id } => to_binary(&query_channel(deps, id)?), - QueryMsg::Config {} => to_binary(&query_config(deps)?), - QueryMsg::Allowed { contract } => to_binary(&query_allowed(deps, contract)?), - QueryMsg::ListAllowed { start_after, limit } => { - to_binary(&list_allowed(deps, start_after, limit)?) - } - QueryMsg::Admin {} => to_binary(&ADMIN.query_admin(deps)?), - } -} - -fn query_port(deps: Deps) -> StdResult { - let query = IbcQuery::PortId {}.into(); - let PortIdResponse { port_id } = deps.querier.query(&query)?; - Ok(PortResponse { port_id }) -} - -fn query_list(deps: Deps) -> StdResult { - let channels = CHANNEL_INFO - .range_raw(deps.storage, None, None, Order::Ascending) - .map(|r| r.map(|(_, v)| v)) - .collect::>()?; - Ok(ListChannelsResponse { channels }) -} - -// make public for ibc tests -pub fn query_channel(deps: Deps, id: String) -> StdResult { - let info = CHANNEL_INFO.load(deps.storage, &id)?; - // this returns Vec<(outstanding, total)> - let state = CHANNEL_STATE - .prefix(&id) - .range(deps.storage, None, None, Order::Ascending) - .map(|r| { - r.map(|(denom, v)| { - let outstanding = Amount::from_parts(denom.clone(), v.outstanding); - let total = Amount::from_parts(denom, v.total_sent); - (outstanding, total) - }) - }) - .collect::>>()?; - // we want (Vec, Vec) - let (balances, total_sent) = state.into_iter().unzip(); - - Ok(ChannelResponse { - info, - balances, - total_sent, - }) -} - -fn query_config(deps: Deps) -> StdResult { - let cfg = CONFIG.load(deps.storage)?; - let admin = ADMIN.get(deps)?.unwrap_or_else(|| Addr::unchecked("")); - let res = ConfigResponse { - default_timeout: cfg.default_timeout, - default_gas_limit: cfg.default_gas_limit, - gov_contract: admin.into(), - }; - Ok(res) -} - -fn query_allowed(deps: Deps, contract: String) -> StdResult { - let addr = deps.api.addr_validate(&contract)?; - let info = ALLOW_LIST.may_load(deps.storage, &addr)?; - let res = match info { - None => AllowedResponse { - is_allowed: false, - gas_limit: None, - }, - Some(a) => AllowedResponse { - is_allowed: true, - gas_limit: a.gas_limit, - }, - }; - Ok(res) -} - -// settings for pagination -const MAX_LIMIT: u32 = 30; -const DEFAULT_LIMIT: u32 = 10; - -fn list_allowed( - deps: Deps, - start_after: Option, - limit: Option, -) -> StdResult { - let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - let addr = maybe_addr(deps.api, start_after)?; - let start = addr.as_ref().map(Bound::exclusive); - - let allow = ALLOW_LIST - .range(deps.storage, start, None, Order::Ascending) - .take(limit) - .map(|item| { - item.map(|(addr, allow)| AllowedInfo { - contract: addr.into(), - gas_limit: allow.gas_limit, - }) - }) - .collect::>()?; - Ok(ListAllowedResponse { allow }) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::test_helpers::*; - - use cosmwasm_schema::cw_serde; - use cosmwasm_std::testing::{mock_env, mock_info, MOCK_CONTRACT_ADDR}; - use cosmwasm_std::{coin, coins, CosmosMsg, IbcMsg, StdError, Uint128}; - - use crate::state::ChannelState; - use cw_utils::PaymentError; - - #[test] - fn setup_and_query() { - let deps = setup(&["channel-3", "channel-7"], &[]); - - let raw_list = query(deps.as_ref(), mock_env(), QueryMsg::ListChannels {}).unwrap(); - let list_res: ListChannelsResponse = from_binary(&raw_list).unwrap(); - assert_eq!(2, list_res.channels.len()); - assert_eq!(mock_channel_info("channel-3"), list_res.channels[0]); - assert_eq!(mock_channel_info("channel-7"), list_res.channels[1]); - - let raw_channel = query( - deps.as_ref(), - mock_env(), - QueryMsg::Channel { - id: "channel-3".to_string(), - }, - ) - .unwrap(); - let chan_res: ChannelResponse = from_binary(&raw_channel).unwrap(); - assert_eq!(chan_res.info, mock_channel_info("channel-3")); - assert_eq!(0, chan_res.total_sent.len()); - assert_eq!(0, chan_res.balances.len()); - - let err = query( - deps.as_ref(), - mock_env(), - QueryMsg::Channel { - id: "channel-10".to_string(), - }, - ) - .unwrap_err(); - assert_eq!(err, StdError::not_found("cw20_ics20::state::ChannelInfo")); - } - - #[test] - fn proper_checks_on_execute_native() { - let send_channel = "channel-5"; - let mut deps = setup(&[send_channel, "channel-10"], &[]); - - let mut transfer = TransferMsg { - channel: send_channel.to_string(), - remote_address: "foreign-address".to_string(), - timeout: None, - memo: None, - }; - - // works with proper funds - let msg = ExecuteMsg::Transfer(transfer.clone()); - let info = mock_info("foobar", &coins(1234567, "ucosm")); - let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - assert_eq!(res.messages[0].gas_limit, None); - assert_eq!(1, res.messages.len()); - if let CosmosMsg::Ibc(IbcMsg::SendPacket { - channel_id, - data, - timeout, - }) = &res.messages[0].msg - { - let expected_timeout = mock_env().block.time.plus_seconds(DEFAULT_TIMEOUT); - assert_eq!(timeout, &expected_timeout.into()); - assert_eq!(channel_id.as_str(), send_channel); - let msg: Ics20Packet = from_binary(data).unwrap(); - assert_eq!(msg.amount, Uint128::new(1234567)); - assert_eq!(msg.denom.as_str(), "ucosm"); - assert_eq!(msg.sender.as_str(), "foobar"); - assert_eq!(msg.receiver.as_str(), "foreign-address"); - } else { - panic!("Unexpected return message: {:?}", res.messages[0]); - } - - // reject with no funds - let msg = ExecuteMsg::Transfer(transfer.clone()); - let info = mock_info("foobar", &[]); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!(err, ContractError::Payment(PaymentError::NoFunds {})); - - // reject with multiple tokens funds - let msg = ExecuteMsg::Transfer(transfer.clone()); - let info = mock_info("foobar", &[coin(1234567, "ucosm"), coin(54321, "uatom")]); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!(err, ContractError::Payment(PaymentError::MultipleDenoms {})); - - // reject with bad channel id - transfer.channel = "channel-45".to_string(); - let msg = ExecuteMsg::Transfer(transfer); - let info = mock_info("foobar", &coins(1234567, "ucosm")); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!( - err, - ContractError::NoSuchChannel { - id: "channel-45".to_string() - } - ); - } - - #[test] - fn proper_checks_on_execute_cw20() { - let send_channel = "channel-15"; - let cw20_addr = "my-token"; - let mut deps = setup(&["channel-3", send_channel], &[(cw20_addr, 123456)]); - - let transfer = TransferMsg { - channel: send_channel.to_string(), - remote_address: "foreign-address".to_string(), - timeout: Some(7777), - memo: None, - }; - let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { - sender: "my-account".into(), - amount: Uint128::new(888777666), - msg: to_binary(&transfer).unwrap(), - }); - - // works with proper funds - let info = mock_info(cw20_addr, &[]); - let res = execute(deps.as_mut(), mock_env(), info, msg.clone()).unwrap(); - assert_eq!(1, res.messages.len()); - assert_eq!(res.messages[0].gas_limit, None); - if let CosmosMsg::Ibc(IbcMsg::SendPacket { - channel_id, - data, - timeout, - }) = &res.messages[0].msg - { - let expected_timeout = mock_env().block.time.plus_seconds(7777); - assert_eq!(timeout, &expected_timeout.into()); - assert_eq!(channel_id.as_str(), send_channel); - let msg: Ics20Packet = from_binary(data).unwrap(); - assert_eq!(msg.amount, Uint128::new(888777666)); - assert_eq!(msg.denom, format!("cw20:{cw20_addr}")); - assert_eq!(msg.sender.as_str(), "my-account"); - assert_eq!(msg.receiver.as_str(), "foreign-address"); - } else { - panic!("Unexpected return message: {:?}", res.messages[0]); - } - - // reject with tokens funds - let info = mock_info("foobar", &coins(1234567, "ucosm")); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!(err, ContractError::Payment(PaymentError::NonPayable {})); - } - - #[test] - fn execute_cw20_fails_if_not_whitelisted_unless_default_gas_limit() { - let send_channel = "channel-15"; - let mut deps = setup(&[send_channel], &[]); - - let cw20_addr = "my-token"; - let transfer = TransferMsg { - channel: send_channel.to_string(), - remote_address: "foreign-address".to_string(), - timeout: Some(7777), - memo: None, - }; - let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { - sender: "my-account".into(), - amount: Uint128::new(888777666), - msg: to_binary(&transfer).unwrap(), - }); - - // rejected as not on allow list - let info = mock_info(cw20_addr, &[]); - let err = execute(deps.as_mut(), mock_env(), info.clone(), msg.clone()).unwrap_err(); - assert_eq!(err, ContractError::NotOnAllowList); - - // add a default gas limit - migrate( - deps.as_mut(), - mock_env(), - MigrateMsg { - default_gas_limit: Some(123456), - }, - ) - .unwrap(); - - // try again - execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - } - - #[test] - fn v3_migration_works() { - // basic state with one channel - let send_channel = "channel-15"; - let cw20_addr = "my-token"; - let native = "ucosm"; - let mut deps = setup(&[send_channel], &[(cw20_addr, 123456)]); - - // mock that we sent some tokens in both native and cw20 (TODO: cw20) - // balances set high - deps.querier - .update_balance(MOCK_CONTRACT_ADDR, coins(50000, native)); - // pretend this is an old contract - set version explicitly - set_contract_version(deps.as_mut().storage, CONTRACT_NAME, MIGRATE_VERSION_3).unwrap(); - - // channel state a bit lower (some in-flight acks) - let state = ChannelState { - // 14000 not accounted for (in-flight) - outstanding: Uint128::new(36000), - total_sent: Uint128::new(100000), - }; - CHANNEL_STATE - .save(deps.as_mut().storage, (send_channel, native), &state) - .unwrap(); - - // run migration - migrate( - deps.as_mut(), - mock_env(), - MigrateMsg { - default_gas_limit: Some(123456), - }, - ) - .unwrap(); - - // check new channel state - let chan = query_channel(deps.as_ref(), send_channel.into()).unwrap(); - assert_eq!(chan.balances, vec![Amount::native(50000, native)]); - assert_eq!(chan.total_sent, vec![Amount::native(114000, native)]); - - // check config updates - let config = query_config(deps.as_ref()).unwrap(); - assert_eq!(config.default_gas_limit, Some(123456)); - } - - fn test_with_memo(memo: &str) { - let send_channel = "channel-5"; - let mut deps = setup(&[send_channel, "channel-10"], &[]); - - let transfer = TransferMsg { - channel: send_channel.to_string(), - remote_address: "foreign-address".to_string(), - timeout: None, - memo: Some(memo.to_string()), - }; - - // works with proper funds - let msg = ExecuteMsg::Transfer(transfer); - let info = mock_info("foobar", &coins(1234567, "ucosm")); - let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - assert_eq!(res.messages[0].gas_limit, None); - assert_eq!(1, res.messages.len()); - if let CosmosMsg::Ibc(IbcMsg::SendPacket { - channel_id, - data, - timeout, - }) = &res.messages[0].msg - { - let expected_timeout = mock_env().block.time.plus_seconds(DEFAULT_TIMEOUT); - assert_eq!(timeout, &expected_timeout.into()); - assert_eq!(channel_id.as_str(), send_channel); - let msg: Ics20Packet = from_binary(data).unwrap(); - assert_eq!(msg.amount, Uint128::new(1234567)); - assert_eq!(msg.denom.as_str(), "ucosm"); - assert_eq!(msg.sender.as_str(), "foobar"); - assert_eq!(msg.receiver.as_str(), "foreign-address"); - assert_eq!( - msg.memo - .expect("Memo was None when Some was expected") - .as_str(), - memo - ); - } else { - panic!("Unexpected return message: {:?}", res.messages[0]); - } - } - - #[test] - fn execute_with_memo_works() { - test_with_memo("memo"); - } - - #[test] - fn execute_with_empty_string_memo_works() { - test_with_memo(""); - } - - #[test] - fn memo_is_backwards_compatible() { - let mut deps = setup(&["channel-5", "channel-10"], &[]); - let transfer: TransferMsg = cosmwasm_std::from_slice( - br#"{"channel": "channel-5", "remote_address": "foreign-address"}"#, - ) - .unwrap(); - - let msg = ExecuteMsg::Transfer(transfer); - let info = mock_info("foobar", &coins(1234567, "ucosm")); - let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - assert_eq!(1, res.messages.len()); - if let CosmosMsg::Ibc(IbcMsg::SendPacket { - channel_id: _, - data, - timeout: _, - }) = &res.messages[0].msg - { - let msg: Ics20Packet = from_binary(data).unwrap(); - assert_eq!(msg.memo, None); - - // This is the old version of the Ics20Packet. Deserializing into it - // should still work as the memo isn't included - #[cw_serde] - struct Ics20PacketNoMemo { - pub amount: Uint128, - pub denom: String, - pub sender: String, - pub receiver: String, - } - - let _msg: Ics20PacketNoMemo = from_binary(data).unwrap(); - } else { - panic!("Unexpected return message: {:?}", res.messages[0]); - } - } - - #[test] - fn invalid_contract_version_should_fail() { - assert!("A.1.0".parse::().map_err(from_semver).is_err()); - } -} diff --git a/contracts/cw20-ics20/src/error.rs b/contracts/cw20-ics20/src/error.rs deleted file mode 100644 index 286ba4a62..000000000 --- a/contracts/cw20-ics20/src/error.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::num::TryFromIntError; -use std::string::FromUtf8Error; -use thiserror::Error; - -use cosmwasm_std::StdError; -use cw_controllers::AdminError; -use cw_utils::PaymentError; - -/// Never is a placeholder to ensure we don't return any errors -#[derive(Error, Debug)] -pub enum Never {} - -#[derive(Error, Debug, PartialEq)] -pub enum ContractError { - #[error("{0}")] - Std(#[from] StdError), - - #[error("{0}")] - Payment(#[from] PaymentError), - - #[error("{0}")] - Admin(#[from] AdminError), - - #[error("Channel doesn't exist: {id}")] - NoSuchChannel { id: String }, - - #[error("Didn't send any funds")] - NoFunds {}, - - #[error("Amount larger than 2**64, not supported by ics20 packets")] - AmountOverflow {}, - - #[error("Only supports channel with ibc version ics20-1, got {version}")] - InvalidIbcVersion { version: String }, - - #[error("Only supports unordered channel")] - OnlyOrderedChannel {}, - - #[error("Insufficient funds to redeem voucher on channel")] - InsufficientFunds {}, - - #[error("Only accepts tokens that originate on this chain, not native tokens of remote chain")] - NoForeignTokens {}, - - #[error("Parsed port from denom ({port}) doesn't match packet")] - FromOtherPort { port: String }, - - #[error("Parsed channel from denom ({channel}) doesn't match packet")] - FromOtherChannel { channel: String }, - - #[error("Cannot migrate from different contract type: {previous_contract}")] - CannotMigrate { previous_contract: String }, - - #[error("Cannot migrate from unsupported version: {previous_version}")] - CannotMigrateVersion { previous_version: String }, - - #[error("Got a submessage reply with unknown id: {id}")] - UnknownReplyId { id: u64 }, - - #[error("You cannot lower the gas limit for a contract on the allow list")] - CannotLowerGas, - - #[error("Only the governance contract can do this")] - Unauthorized, - - #[error("You can only send cw20 tokens that have been explicitly allowed by governance")] - NotOnAllowList, -} - -impl From for ContractError { - fn from(_: FromUtf8Error) -> Self { - ContractError::Std(StdError::invalid_utf8("parsing denom key")) - } -} - -impl From for ContractError { - fn from(_: TryFromIntError) -> Self { - ContractError::AmountOverflow {} - } -} diff --git a/contracts/cw20-ics20/src/ibc.rs b/contracts/cw20-ics20/src/ibc.rs deleted file mode 100644 index ff62c90ce..000000000 --- a/contracts/cw20-ics20/src/ibc.rs +++ /dev/null @@ -1,675 +0,0 @@ -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -use cosmwasm_schema::cw_serde; -use cosmwasm_std::{ - attr, entry_point, from_binary, to_binary, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env, - IbcBasicResponse, IbcChannel, IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg, - IbcEndpoint, IbcOrder, IbcPacket, IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg, - IbcReceiveResponse, Reply, Response, SubMsg, SubMsgResult, Uint128, WasmMsg, -}; - -use crate::amount::Amount; -use crate::error::{ContractError, Never}; -use crate::state::{ - reduce_channel_balance, undo_reduce_channel_balance, ChannelInfo, ReplyArgs, ALLOW_LIST, - CHANNEL_INFO, CONFIG, REPLY_ARGS, -}; -use cw20::Cw20ExecuteMsg; - -pub const ICS20_VERSION: &str = "ics20-1"; -pub const ICS20_ORDERING: IbcOrder = IbcOrder::Unordered; - -/// The format for sending an ics20 packet. -/// Proto defined here: https://github.com/cosmos/cosmos-sdk/blob/v0.42.0/proto/ibc/applications/transfer/v1/transfer.proto#L11-L20 -/// This is compatible with the JSON serialization -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug, Default)] -pub struct Ics20Packet { - /// amount of tokens to transfer is encoded as a string, but limited to u64 max - pub amount: Uint128, - /// the token denomination to be transferred - pub denom: String, - /// the recipient address on the destination chain - pub receiver: String, - /// the sender address - pub sender: String, - /// optional memo for the IBC transfer - #[serde(skip_serializing_if = "Option::is_none")] - pub memo: Option, -} - -impl Ics20Packet { - pub fn new>(amount: Uint128, denom: T, sender: &str, receiver: &str) -> Self { - Ics20Packet { - denom: denom.into(), - amount, - sender: sender.to_string(), - receiver: receiver.to_string(), - memo: None, - } - } - - pub fn with_memo(self, memo: Option) -> Self { - Ics20Packet { memo, ..self } - } - - pub fn validate(&self) -> Result<(), ContractError> { - if self.amount.u128() > (u64::MAX as u128) { - Err(ContractError::AmountOverflow {}) - } else { - Ok(()) - } - } -} - -/// This is a generic ICS acknowledgement format. -/// Proto defined here: https://github.com/cosmos/cosmos-sdk/blob/v0.42.0/proto/ibc/core/channel/v1/channel.proto#L141-L147 -/// This is compatible with the JSON serialization -#[cw_serde] -pub enum Ics20Ack { - Result(Binary), - Error(String), -} - -// create a serialized success message -fn ack_success() -> Binary { - let res = Ics20Ack::Result(b"1".into()); - to_binary(&res).unwrap() -} - -// create a serialized error message -fn ack_fail(err: String) -> Binary { - let res = Ics20Ack::Error(err); - to_binary(&res).unwrap() -} - -const RECEIVE_ID: u64 = 1337; -const ACK_FAILURE_ID: u64 = 0xfa17; - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn reply(deps: DepsMut, _env: Env, reply: Reply) -> Result { - match reply.id { - RECEIVE_ID => match reply.result { - SubMsgResult::Ok(_) => Ok(Response::new()), - SubMsgResult::Err(err) => { - // Important design note: with ibcv2 and wasmd 0.22 we can implement this all much easier. - // No reply needed... the receive function and submessage should return error on failure and all - // state gets reverted with a proper app-level message auto-generated - - // Since we need compatibility with Juno (Jan 2022), we need to ensure that optimisitic - // state updates in ibc_packet_receive get reverted in the (unlikely) chance of an - // error while sending the token - - // However, this requires passing some state between the ibc_packet_receive function and - // the reply handler. We do this with a singleton, with is "okay" for IBC as there is no - // reentrancy on these functions (cannot be called by another contract). This pattern - // should not be used for ExecuteMsg handlers - let reply_args = REPLY_ARGS.load(deps.storage)?; - undo_reduce_channel_balance( - deps.storage, - &reply_args.channel, - &reply_args.denom, - reply_args.amount, - )?; - - Ok(Response::new().set_data(ack_fail(err))) - } - }, - ACK_FAILURE_ID => match reply.result { - SubMsgResult::Ok(_) => Ok(Response::new()), - SubMsgResult::Err(err) => Ok(Response::new().set_data(ack_fail(err))), - }, - _ => Err(ContractError::UnknownReplyId { id: reply.id }), - } -} - -#[cfg_attr(not(feature = "library"), entry_point)] -/// enforces ordering and versioning constraints -pub fn ibc_channel_open( - _deps: DepsMut, - _env: Env, - msg: IbcChannelOpenMsg, -) -> Result<(), ContractError> { - enforce_order_and_version(msg.channel(), msg.counterparty_version())?; - Ok(()) -} - -#[cfg_attr(not(feature = "library"), entry_point)] -/// record the channel in CHANNEL_INFO -pub fn ibc_channel_connect( - deps: DepsMut, - _env: Env, - msg: IbcChannelConnectMsg, -) -> Result { - // we need to check the counter party version in try and ack (sometimes here) - enforce_order_and_version(msg.channel(), msg.counterparty_version())?; - - let channel: IbcChannel = msg.into(); - let info = ChannelInfo { - id: channel.endpoint.channel_id, - counterparty_endpoint: channel.counterparty_endpoint, - connection_id: channel.connection_id, - }; - CHANNEL_INFO.save(deps.storage, &info.id, &info)?; - - Ok(IbcBasicResponse::default()) -} - -fn enforce_order_and_version( - channel: &IbcChannel, - counterparty_version: Option<&str>, -) -> Result<(), ContractError> { - if channel.version != ICS20_VERSION { - return Err(ContractError::InvalidIbcVersion { - version: channel.version.clone(), - }); - } - if let Some(version) = counterparty_version { - if version != ICS20_VERSION { - return Err(ContractError::InvalidIbcVersion { - version: version.to_string(), - }); - } - } - if channel.order != ICS20_ORDERING { - return Err(ContractError::OnlyOrderedChannel {}); - } - Ok(()) -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn ibc_channel_close( - _deps: DepsMut, - _env: Env, - _channel: IbcChannelCloseMsg, -) -> Result { - // TODO: what to do here? - // we will have locked funds that need to be returned somehow - unimplemented!(); -} - -#[cfg_attr(not(feature = "library"), entry_point)] -/// Check to see if we have any balance here -/// We should not return an error if possible, but rather an acknowledgement of failure -pub fn ibc_packet_receive( - deps: DepsMut, - _env: Env, - msg: IbcPacketReceiveMsg, -) -> Result { - let packet = msg.packet; - - do_ibc_packet_receive(deps, &packet).or_else(|err| { - Ok(IbcReceiveResponse::new() - .set_ack(ack_fail(err.to_string())) - .add_attributes(vec![ - attr("action", "receive"), - attr("success", "false"), - attr("error", err.to_string()), - ])) - }) -} - -// Returns local denom if the denom is an encoded voucher from the expected endpoint -// Otherwise, error -fn parse_voucher_denom<'a>( - voucher_denom: &'a str, - remote_endpoint: &IbcEndpoint, -) -> Result<&'a str, ContractError> { - let split_denom: Vec<&str> = voucher_denom.splitn(3, '/').collect(); - if split_denom.len() != 3 { - return Err(ContractError::NoForeignTokens {}); - } - // a few more sanity checks - if split_denom[0] != remote_endpoint.port_id { - return Err(ContractError::FromOtherPort { - port: split_denom[0].into(), - }); - } - if split_denom[1] != remote_endpoint.channel_id { - return Err(ContractError::FromOtherChannel { - channel: split_denom[1].into(), - }); - } - - Ok(split_denom[2]) -} - -// this does the work of ibc_packet_receive, we wrap it to turn errors into acknowledgements -fn do_ibc_packet_receive( - deps: DepsMut, - packet: &IbcPacket, -) -> Result { - let msg: Ics20Packet = from_binary(&packet.data)?; - let channel = packet.dest.channel_id.clone(); - - // If the token originated on the remote chain, it looks like "ucosm". - // If it originated on our chain, it looks like "port/channel/ucosm". - let denom = parse_voucher_denom(&msg.denom, &packet.src)?; - - // make sure we have enough balance for this - reduce_channel_balance(deps.storage, &channel, denom, msg.amount)?; - - // we need to save the data to update the balances in reply - let reply_args = ReplyArgs { - channel, - denom: denom.to_string(), - amount: msg.amount, - }; - REPLY_ARGS.save(deps.storage, &reply_args)?; - - let to_send = Amount::from_parts(denom.to_string(), msg.amount); - let gas_limit = check_gas_limit(deps.as_ref(), &to_send)?; - let send = send_amount(to_send, msg.receiver.clone()); - let mut submsg = SubMsg::reply_on_error(send, RECEIVE_ID); - submsg.gas_limit = gas_limit; - - let res = IbcReceiveResponse::new() - .set_ack(ack_success()) - .add_submessage(submsg) - .add_attribute("action", "receive") - .add_attribute("sender", msg.sender) - .add_attribute("receiver", msg.receiver) - .add_attribute("denom", denom) - .add_attribute("amount", msg.amount) - .add_attribute("success", "true"); - - Ok(res) -} - -fn check_gas_limit(deps: Deps, amount: &Amount) -> Result, ContractError> { - match amount { - Amount::Cw20(coin) => { - // if cw20 token, use the registered gas limit, or error if not whitelisted - let addr = deps.api.addr_validate(&coin.address)?; - let allowed = ALLOW_LIST.may_load(deps.storage, &addr)?; - match allowed { - Some(allow) => Ok(allow.gas_limit), - None => match CONFIG.load(deps.storage)?.default_gas_limit { - Some(base) => Ok(Some(base)), - None => Err(ContractError::NotOnAllowList), - }, - } - } - _ => Ok(None), - } -} - -#[cfg_attr(not(feature = "library"), entry_point)] -/// check if success or failure and update balance, or return funds -pub fn ibc_packet_ack( - deps: DepsMut, - _env: Env, - msg: IbcPacketAckMsg, -) -> Result { - // Design decision: should we trap error like in receive? - // TODO: unsure... as it is now a failed ack handling would revert the tx and would be - // retried again and again. is that good? - let ics20msg: Ics20Ack = from_binary(&msg.acknowledgement.data)?; - match ics20msg { - Ics20Ack::Result(_) => on_packet_success(deps, msg.original_packet), - Ics20Ack::Error(err) => on_packet_failure(deps, msg.original_packet, err), - } -} - -#[cfg_attr(not(feature = "library"), entry_point)] -/// return fund to original sender (same as failure in ibc_packet_ack) -pub fn ibc_packet_timeout( - deps: DepsMut, - _env: Env, - msg: IbcPacketTimeoutMsg, -) -> Result { - // TODO: trap error like in receive? (same question as ack above) - let packet = msg.packet; - on_packet_failure(deps, packet, "timeout".to_string()) -} - -// update the balance stored on this (channel, denom) index -fn on_packet_success(_deps: DepsMut, packet: IbcPacket) -> Result { - let msg: Ics20Packet = from_binary(&packet.data)?; - - // similar event messages like ibctransfer module - let attributes = vec![ - attr("action", "acknowledge"), - attr("sender", &msg.sender), - attr("receiver", &msg.receiver), - attr("denom", &msg.denom), - attr("amount", msg.amount), - attr("success", "true"), - ]; - - Ok(IbcBasicResponse::new().add_attributes(attributes)) -} - -// return the tokens to sender -fn on_packet_failure( - deps: DepsMut, - packet: IbcPacket, - err: String, -) -> Result { - let msg: Ics20Packet = from_binary(&packet.data)?; - - // undo the balance update on failure (as we pre-emptively added it on send) - reduce_channel_balance(deps.storage, &packet.src.channel_id, &msg.denom, msg.amount)?; - - let to_send = Amount::from_parts(msg.denom.clone(), msg.amount); - let gas_limit = check_gas_limit(deps.as_ref(), &to_send)?; - let send = send_amount(to_send, msg.sender.clone()); - let mut submsg = SubMsg::reply_on_error(send, ACK_FAILURE_ID); - submsg.gas_limit = gas_limit; - - // similar event messages like ibctransfer module - let res = IbcBasicResponse::new() - .add_submessage(submsg) - .add_attribute("action", "acknowledge") - .add_attribute("sender", msg.sender) - .add_attribute("receiver", msg.receiver) - .add_attribute("denom", msg.denom) - .add_attribute("amount", msg.amount.to_string()) - .add_attribute("success", "false") - .add_attribute("error", err); - - Ok(res) -} - -fn send_amount(amount: Amount, recipient: String) -> CosmosMsg { - match amount { - Amount::Native(coin) => BankMsg::Send { - to_address: recipient, - amount: vec![coin], - } - .into(), - Amount::Cw20(coin) => { - let msg = Cw20ExecuteMsg::Transfer { - recipient, - amount: coin.amount, - }; - WasmMsg::Execute { - contract_addr: coin.address, - msg: to_binary(&msg).unwrap(), - funds: vec![], - } - .into() - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::test_helpers::*; - - use crate::contract::{execute, migrate, query_channel}; - use crate::msg::{ExecuteMsg, MigrateMsg, TransferMsg}; - use cosmwasm_std::testing::{mock_env, mock_info}; - use cosmwasm_std::{coins, to_vec, IbcEndpoint, IbcMsg, IbcTimeout, Timestamp}; - use cw20::Cw20ReceiveMsg; - - #[test] - fn check_ack_json() { - let success = Ics20Ack::Result(b"1".into()); - let fail = Ics20Ack::Error("bad coin".into()); - - let success_json = String::from_utf8(to_vec(&success).unwrap()).unwrap(); - assert_eq!(r#"{"result":"MQ=="}"#, success_json.as_str()); - - let fail_json = String::from_utf8(to_vec(&fail).unwrap()).unwrap(); - assert_eq!(r#"{"error":"bad coin"}"#, fail_json.as_str()); - } - - #[test] - fn check_packet_json() { - let packet = Ics20Packet::new( - Uint128::new(12345), - "ucosm", - "cosmos1zedxv25ah8fksmg2lzrndrpkvsjqgk4zt5ff7n", - "wasm1fucynrfkrt684pm8jrt8la5h2csvs5cnldcgqc", - ); - // Example message generated from the SDK - let expected = r#"{"amount":"12345","denom":"ucosm","receiver":"wasm1fucynrfkrt684pm8jrt8la5h2csvs5cnldcgqc","sender":"cosmos1zedxv25ah8fksmg2lzrndrpkvsjqgk4zt5ff7n"}"#; - - let encdoded = String::from_utf8(to_vec(&packet).unwrap()).unwrap(); - assert_eq!(expected, encdoded.as_str()); - } - - fn cw20_payment( - amount: u128, - address: &str, - recipient: &str, - gas_limit: Option, - ) -> SubMsg { - let msg = Cw20ExecuteMsg::Transfer { - recipient: recipient.into(), - amount: Uint128::new(amount), - }; - let exec = WasmMsg::Execute { - contract_addr: address.into(), - msg: to_binary(&msg).unwrap(), - funds: vec![], - }; - let mut msg = SubMsg::reply_on_error(exec, RECEIVE_ID); - msg.gas_limit = gas_limit; - msg - } - - fn native_payment(amount: u128, denom: &str, recipient: &str) -> SubMsg { - SubMsg::reply_on_error( - BankMsg::Send { - to_address: recipient.into(), - amount: coins(amount, denom), - }, - RECEIVE_ID, - ) - } - - fn mock_receive_packet( - my_channel: &str, - amount: u128, - denom: &str, - receiver: &str, - ) -> IbcPacket { - let data = Ics20Packet { - // this is returning a foreign (our) token, thus denom is // - denom: format!("{}/{}/{}", REMOTE_PORT, "channel-1234", denom), - amount: amount.into(), - sender: "remote-sender".to_string(), - receiver: receiver.to_string(), - memo: None, - }; - print!("Packet denom: {}", &data.denom); - IbcPacket::new( - to_binary(&data).unwrap(), - IbcEndpoint { - port_id: REMOTE_PORT.to_string(), - channel_id: "channel-1234".to_string(), - }, - IbcEndpoint { - port_id: CONTRACT_PORT.to_string(), - channel_id: my_channel.to_string(), - }, - 3, - Timestamp::from_seconds(1665321069).into(), - ) - } - - #[test] - fn send_receive_cw20() { - let send_channel = "channel-9"; - let cw20_addr = "token-addr"; - let cw20_denom = "cw20:token-addr"; - let gas_limit = 1234567; - let mut deps = setup( - &["channel-1", "channel-7", send_channel], - &[(cw20_addr, gas_limit)], - ); - - // prepare some mock packets - let recv_packet = mock_receive_packet(send_channel, 876543210, cw20_denom, "local-rcpt"); - let recv_high_packet = - mock_receive_packet(send_channel, 1876543210, cw20_denom, "local-rcpt"); - - // cannot receive this denom yet - let msg = IbcPacketReceiveMsg::new(recv_packet.clone()); - let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); - assert!(res.messages.is_empty()); - let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); - let no_funds = Ics20Ack::Error(ContractError::InsufficientFunds {}.to_string()); - assert_eq!(ack, no_funds); - - // we send some cw20 tokens over - let transfer = TransferMsg { - channel: send_channel.to_string(), - remote_address: "remote-rcpt".to_string(), - timeout: None, - memo: None, - }; - let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { - sender: "local-sender".to_string(), - amount: Uint128::new(987654321), - msg: to_binary(&transfer).unwrap(), - }); - let info = mock_info(cw20_addr, &[]); - let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - assert_eq!(1, res.messages.len()); - let expected = Ics20Packet { - denom: cw20_denom.into(), - amount: Uint128::new(987654321), - sender: "local-sender".to_string(), - receiver: "remote-rcpt".to_string(), - memo: None, - }; - let timeout = mock_env().block.time.plus_seconds(DEFAULT_TIMEOUT); - assert_eq!( - &res.messages[0], - &SubMsg::new(IbcMsg::SendPacket { - channel_id: send_channel.to_string(), - data: to_binary(&expected).unwrap(), - timeout: IbcTimeout::with_timestamp(timeout), - }) - ); - - // query channel state|_| - let state = query_channel(deps.as_ref(), send_channel.to_string()).unwrap(); - assert_eq!(state.balances, vec![Amount::cw20(987654321, cw20_addr)]); - assert_eq!(state.total_sent, vec![Amount::cw20(987654321, cw20_addr)]); - - // cannot receive more than we sent - let msg = IbcPacketReceiveMsg::new(recv_high_packet); - let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); - assert!(res.messages.is_empty()); - let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); - assert_eq!(ack, no_funds); - - // we can receive less than we sent - let msg = IbcPacketReceiveMsg::new(recv_packet); - let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); - assert_eq!(1, res.messages.len()); - assert_eq!( - cw20_payment(876543210, cw20_addr, "local-rcpt", Some(gas_limit)), - res.messages[0] - ); - let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); - assert!(matches!(ack, Ics20Ack::Result(_))); - - // TODO: we need to call the reply block - - // query channel state - let state = query_channel(deps.as_ref(), send_channel.to_string()).unwrap(); - assert_eq!(state.balances, vec![Amount::cw20(111111111, cw20_addr)]); - assert_eq!(state.total_sent, vec![Amount::cw20(987654321, cw20_addr)]); - } - - #[test] - fn send_receive_native() { - let send_channel = "channel-9"; - let mut deps = setup(&["channel-1", "channel-7", send_channel], &[]); - - let denom = "uatom"; - - // prepare some mock packets - let recv_packet = mock_receive_packet(send_channel, 876543210, denom, "local-rcpt"); - let recv_high_packet = mock_receive_packet(send_channel, 1876543210, denom, "local-rcpt"); - - // cannot receive this denom yet - let msg = IbcPacketReceiveMsg::new(recv_packet.clone()); - let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); - assert!(res.messages.is_empty()); - let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); - let no_funds = Ics20Ack::Error(ContractError::InsufficientFunds {}.to_string()); - assert_eq!(ack, no_funds); - - // we transfer some tokens - let msg = ExecuteMsg::Transfer(TransferMsg { - channel: send_channel.to_string(), - remote_address: "my-remote-address".to_string(), - timeout: None, - memo: None, - }); - let info = mock_info("local-sender", &coins(987654321, denom)); - execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - // query channel state|_| - let state = query_channel(deps.as_ref(), send_channel.to_string()).unwrap(); - assert_eq!(state.balances, vec![Amount::native(987654321, denom)]); - assert_eq!(state.total_sent, vec![Amount::native(987654321, denom)]); - - // cannot receive more than we sent - let msg = IbcPacketReceiveMsg::new(recv_high_packet); - let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); - assert!(res.messages.is_empty()); - let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); - assert_eq!(ack, no_funds); - - // we can receive less than we sent - let msg = IbcPacketReceiveMsg::new(recv_packet); - let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); - assert_eq!(1, res.messages.len()); - assert_eq!( - native_payment(876543210, denom, "local-rcpt"), - res.messages[0] - ); - let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); - assert!(matches!(ack, Ics20Ack::Result(_))); - - // only need to call reply block on error case - - // query channel state - let state = query_channel(deps.as_ref(), send_channel.to_string()).unwrap(); - assert_eq!(state.balances, vec![Amount::native(111111111, denom)]); - assert_eq!(state.total_sent, vec![Amount::native(987654321, denom)]); - } - - #[test] - fn check_gas_limit_handles_all_cases() { - let send_channel = "channel-9"; - let allowed = "foobar"; - let allowed_gas = 777666; - let mut deps = setup(&[send_channel], &[(allowed, allowed_gas)]); - - // allow list will get proper gas - let limit = check_gas_limit(deps.as_ref(), &Amount::cw20(500, allowed)).unwrap(); - assert_eq!(limit, Some(allowed_gas)); - - // non-allow list will error - let random = "tokenz"; - check_gas_limit(deps.as_ref(), &Amount::cw20(500, random)).unwrap_err(); - - // add default_gas_limit - let def_limit = 54321; - migrate( - deps.as_mut(), - mock_env(), - MigrateMsg { - default_gas_limit: Some(def_limit), - }, - ) - .unwrap(); - - // allow list still gets proper gas - let limit = check_gas_limit(deps.as_ref(), &Amount::cw20(500, allowed)).unwrap(); - assert_eq!(limit, Some(allowed_gas)); - - // non-allow list will now get default - let limit = check_gas_limit(deps.as_ref(), &Amount::cw20(500, random)).unwrap(); - assert_eq!(limit, Some(def_limit)); - } -} diff --git a/contracts/cw20-ics20/src/lib.rs b/contracts/cw20-ics20/src/lib.rs deleted file mode 100644 index a9dba517d..000000000 --- a/contracts/cw20-ics20/src/lib.rs +++ /dev/null @@ -1,23 +0,0 @@ -/*! -This is an *IBC Enabled* contract that allows us to send CW20 tokens from one chain over the standard ICS20 -protocol to the bank module of another chain. In short, it lets us send our custom CW20 tokens with IBC and use -them just like native tokens on other chains. - -It is only designed to send tokens and redeem previously sent tokens. It will not mint tokens belonging -to assets originating on the foreign chain. This is different than the Golang `ibctransfer` module, but -we properly implement ICS20 and respond with an error message... let's hope the Go side handles this correctly. - -For more information on this contract, please check out the -[README](https://github.com/CosmWasm/cw-plus/blob/main/contracts/cw20-ics20/README.md). -*/ - -pub mod amount; -pub mod contract; -mod error; -pub mod ibc; -mod migrations; -pub mod msg; -pub mod state; -mod test_helpers; - -pub use crate::error::ContractError; diff --git a/contracts/cw20-ics20/src/migrations.rs b/contracts/cw20-ics20/src/migrations.rs deleted file mode 100644 index 3ff2fe09f..000000000 --- a/contracts/cw20-ics20/src/migrations.rs +++ /dev/null @@ -1,86 +0,0 @@ -// v1 format is anything older than 0.12.0 -pub mod v1 { - use cosmwasm_schema::cw_serde; - - use cosmwasm_std::Addr; - use cw_storage_plus::Item; - - #[cw_serde] - pub struct Config { - pub default_timeout: u64, - pub gov_contract: Addr, - } - - pub const CONFIG: Item = Item::new("ics20_config"); -} - -// v2 format is anything older than 0.13.1 when we only updated the internal balances on success ack -pub mod v2 { - use crate::amount::Amount; - use crate::state::{ChannelState, CHANNEL_INFO, CHANNEL_STATE}; - use crate::ContractError; - use cosmwasm_std::{to_binary, Addr, DepsMut, Env, Order, StdResult, WasmQuery}; - use cw20::{BalanceResponse, Cw20QueryMsg}; - - pub fn update_balances(mut deps: DepsMut, env: &Env) -> Result<(), ContractError> { - let channels = CHANNEL_INFO - .keys(deps.storage, None, None, Order::Ascending) - .collect::>>()?; - match channels.len() { - 0 => Ok(()), - 1 => { - let channel = &channels[0]; - let addr = &env.contract.address; - let states = CHANNEL_STATE - .prefix(channel) - .range(deps.storage, None, None, Order::Ascending) - .collect::>>()?; - for (denom, state) in states.into_iter() { - update_denom(deps.branch(), addr, channel, denom, state)?; - } - Ok(()) - } - _ => Err(ContractError::CannotMigrate { - previous_contract: "multiple channels open".into(), - }), - } - } - - fn update_denom( - deps: DepsMut, - contract: &Addr, - channel: &str, - denom: String, - mut state: ChannelState, - ) -> StdResult<()> { - // handle this for both native and cw20 - let balance = match Amount::from_parts(denom.clone(), state.outstanding) { - Amount::Native(coin) => deps.querier.query_balance(contract, coin.denom)?.amount, - Amount::Cw20(coin) => { - // FIXME: we should be able to do this with the following line, but QuerierWrapper doesn't play - // with the Querier generics - // `Cw20Contract(contract.clone()).balance(&deps.querier, contract)?` - let query = WasmQuery::Smart { - contract_addr: coin.address, - msg: to_binary(&Cw20QueryMsg::Balance { - address: contract.into(), - })?, - }; - let res: BalanceResponse = deps.querier.query(&query.into())?; - res.balance - } - }; - - // this checks if we have received some coins that are "in flight" and not yet accounted in the state - let diff = balance - state.outstanding; - // if they are in flight, we add them to the internal state now, as if we added them when sent (not when acked) - // to match the current logic - if !diff.is_zero() { - state.outstanding += diff; - state.total_sent += diff; - CHANNEL_STATE.save(deps.storage, (channel, &denom), &state)?; - } - - Ok(()) - } -} diff --git a/contracts/cw20-ics20/src/msg.rs b/contracts/cw20-ics20/src/msg.rs deleted file mode 100644 index ea4290ddb..000000000 --- a/contracts/cw20-ics20/src/msg.rs +++ /dev/null @@ -1,129 +0,0 @@ -use cosmwasm_schema::{cw_serde, QueryResponses}; -use cw20::Cw20ReceiveMsg; - -use crate::amount::Amount; -use crate::state::ChannelInfo; - -#[cw_serde] -pub struct InitMsg { - /// Default timeout for ics20 packets, specified in seconds - pub default_timeout: u64, - /// who can allow more contracts - pub gov_contract: String, - /// initial allowlist - all cw20 tokens we will send must be previously allowed by governance - pub allowlist: Vec, - /// If set, contracts off the allowlist will run with this gas limit. - /// If unset, will refuse to accept any contract off the allow list. - pub default_gas_limit: Option, -} - -#[cw_serde] -pub struct AllowMsg { - pub contract: String, - pub gas_limit: Option, -} - -#[cw_serde] -pub struct MigrateMsg { - pub default_gas_limit: Option, -} - -#[cw_serde] -pub enum ExecuteMsg { - /// This accepts a properly-encoded ReceiveMsg from a cw20 contract - Receive(Cw20ReceiveMsg), - /// This allows us to transfer *exactly one* native token - Transfer(TransferMsg), - /// This must be called by gov_contract, will allow a new cw20 token to be sent - Allow(AllowMsg), - /// Change the admin (must be called by current admin) - UpdateAdmin { admin: String }, -} - -/// This is the message we accept via Receive -#[cw_serde] -pub struct TransferMsg { - /// The local channel to send the packets on - pub channel: String, - /// The remote address to send to. - /// Don't use HumanAddress as this will likely have a different Bech32 prefix than we use - /// and cannot be validated locally - pub remote_address: String, - /// How long the packet lives in seconds. If not specified, use default_timeout - pub timeout: Option, - /// An optional memo to add to the IBC transfer - pub memo: Option, -} - -#[cw_serde] -#[derive(QueryResponses)] -pub enum QueryMsg { - /// Return the port ID bound by this contract. - #[returns(PortResponse)] - Port {}, - /// Show all channels we have connected to. - #[returns(ListChannelsResponse)] - ListChannels {}, - /// Returns the details of the name channel, error if not created. - #[returns(ChannelResponse)] - Channel { id: String }, - /// Show the Config. - #[returns(ConfigResponse)] - Config {}, - #[returns(cw_controllers::AdminResponse)] - Admin {}, - /// Query if a given cw20 contract is allowed. - #[returns(AllowedResponse)] - Allowed { contract: String }, - /// List all allowed cw20 contracts. - #[returns(ListAllowedResponse)] - ListAllowed { - start_after: Option, - limit: Option, - }, -} - -#[cw_serde] -pub struct ListChannelsResponse { - pub channels: Vec, -} - -#[cw_serde] -pub struct ChannelResponse { - /// Information on the channel's connection - pub info: ChannelInfo, - /// How many tokens we currently have pending over this channel - pub balances: Vec, - /// The total number of tokens that have been sent over this channel - /// (even if many have been returned, so balance is low) - pub total_sent: Vec, -} - -#[cw_serde] -pub struct PortResponse { - pub port_id: String, -} - -#[cw_serde] -pub struct ConfigResponse { - pub default_timeout: u64, - pub default_gas_limit: Option, - pub gov_contract: String, -} - -#[cw_serde] -pub struct AllowedResponse { - pub is_allowed: bool, - pub gas_limit: Option, -} - -#[cw_serde] -pub struct ListAllowedResponse { - pub allow: Vec, -} - -#[cw_serde] -pub struct AllowedInfo { - pub contract: String, - pub gas_limit: Option, -} diff --git a/contracts/cw20-ics20/src/state.rs b/contracts/cw20-ics20/src/state.rs deleted file mode 100644 index 19e05a482..000000000 --- a/contracts/cw20-ics20/src/state.rs +++ /dev/null @@ -1,110 +0,0 @@ -use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, IbcEndpoint, StdResult, Storage, Uint128}; -use cw_controllers::Admin; -use cw_storage_plus::{Item, Map}; - -use crate::ContractError; - -pub const ADMIN: Admin = Admin::new("admin"); - -pub const CONFIG: Item = Item::new("ics20_config"); - -// Used to pass info from the ibc_packet_receive to the reply handler -pub const REPLY_ARGS: Item = Item::new("reply_args"); - -/// static info on one channel that doesn't change -pub const CHANNEL_INFO: Map<&str, ChannelInfo> = Map::new("channel_info"); - -/// indexed by (channel_id, denom) maintaining the balance of the channel in that currency -pub const CHANNEL_STATE: Map<(&str, &str), ChannelState> = Map::new("channel_state"); - -/// Every cw20 contract we allow to be sent is stored here, possibly with a gas_limit -pub const ALLOW_LIST: Map<&Addr, AllowInfo> = Map::new("allow_list"); - -#[cw_serde] -#[derive(Default)] -pub struct ChannelState { - pub outstanding: Uint128, - pub total_sent: Uint128, -} - -#[cw_serde] -pub struct Config { - pub default_timeout: u64, - pub default_gas_limit: Option, -} - -#[cw_serde] -pub struct ChannelInfo { - /// id of this channel - pub id: String, - /// the remote channel/port we connect to - pub counterparty_endpoint: IbcEndpoint, - /// the connection this exists on (you can use to query client/consensus info) - pub connection_id: String, -} - -#[cw_serde] -pub struct AllowInfo { - pub gas_limit: Option, -} - -#[cw_serde] -pub struct ReplyArgs { - pub channel: String, - pub denom: String, - pub amount: Uint128, -} - -pub fn increase_channel_balance( - storage: &mut dyn Storage, - channel: &str, - denom: &str, - amount: Uint128, -) -> Result<(), ContractError> { - CHANNEL_STATE.update(storage, (channel, denom), |orig| -> StdResult<_> { - let mut state = orig.unwrap_or_default(); - state.outstanding += amount; - state.total_sent += amount; - Ok(state) - })?; - Ok(()) -} - -pub fn reduce_channel_balance( - storage: &mut dyn Storage, - channel: &str, - denom: &str, - amount: Uint128, -) -> Result<(), ContractError> { - CHANNEL_STATE.update( - storage, - (channel, denom), - |orig| -> Result<_, ContractError> { - // this will return error if we don't have the funds there to cover the request (or no denom registered) - let mut cur = orig.ok_or(ContractError::InsufficientFunds {})?; - cur.outstanding = cur - .outstanding - .checked_sub(amount) - .or(Err(ContractError::InsufficientFunds {}))?; - Ok(cur) - }, - )?; - Ok(()) -} - -// this is like increase, but it only "un-subtracts" (= adds) outstanding, not total_sent -// calling `reduce_channel_balance` and then `undo_reduce_channel_balance` should leave state unchanged. -pub fn undo_reduce_channel_balance( - storage: &mut dyn Storage, - channel: &str, - denom: &str, - amount: Uint128, -) -> Result<(), ContractError> { - CHANNEL_STATE.update(storage, (channel, denom), |orig| -> StdResult<_> { - let mut state = orig.unwrap_or_default(); - state.outstanding += amount; - Ok(state) - })?; - Ok(()) -} diff --git a/contracts/cw20-ics20/src/test_helpers.rs b/contracts/cw20-ics20/src/test_helpers.rs deleted file mode 100644 index 27e2b4f0b..000000000 --- a/contracts/cw20-ics20/src/test_helpers.rs +++ /dev/null @@ -1,86 +0,0 @@ -#![cfg(test)] - -use crate::contract::instantiate; -use crate::ibc::{ibc_channel_connect, ibc_channel_open, ICS20_ORDERING, ICS20_VERSION}; -use crate::state::ChannelInfo; - -use cosmwasm_std::testing::{ - mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage, -}; -use cosmwasm_std::{ - DepsMut, IbcChannel, IbcChannelConnectMsg, IbcChannelOpenMsg, IbcEndpoint, OwnedDeps, -}; - -use crate::msg::{AllowMsg, InitMsg}; - -pub const DEFAULT_TIMEOUT: u64 = 3600; // 1 hour, -pub const CONTRACT_PORT: &str = "ibc:wasm1234567890abcdef"; -pub const REMOTE_PORT: &str = "transfer"; -pub const CONNECTION_ID: &str = "connection-2"; - -pub fn mock_channel(channel_id: &str) -> IbcChannel { - IbcChannel::new( - IbcEndpoint { - port_id: CONTRACT_PORT.into(), - channel_id: channel_id.into(), - }, - IbcEndpoint { - port_id: REMOTE_PORT.into(), - channel_id: format!("{channel_id}5"), - }, - ICS20_ORDERING, - ICS20_VERSION, - CONNECTION_ID, - ) -} - -pub fn mock_channel_info(channel_id: &str) -> ChannelInfo { - ChannelInfo { - id: channel_id.to_string(), - counterparty_endpoint: IbcEndpoint { - port_id: REMOTE_PORT.into(), - channel_id: format!("{channel_id}5"), - }, - connection_id: CONNECTION_ID.into(), - } -} - -// we simulate instantiate and ack here -pub fn add_channel(mut deps: DepsMut, channel_id: &str) { - let channel = mock_channel(channel_id); - let open_msg = IbcChannelOpenMsg::new_init(channel.clone()); - ibc_channel_open(deps.branch(), mock_env(), open_msg).unwrap(); - let connect_msg = IbcChannelConnectMsg::new_ack(channel, ICS20_VERSION); - ibc_channel_connect(deps.branch(), mock_env(), connect_msg).unwrap(); -} - -pub fn setup( - channels: &[&str], - allow: &[(&str, u64)], -) -> OwnedDeps { - let mut deps = mock_dependencies(); - - let allowlist = allow - .iter() - .map(|(contract, gas)| AllowMsg { - contract: contract.to_string(), - gas_limit: Some(*gas), - }) - .collect(); - - // instantiate an empty contract - let instantiate_msg = InitMsg { - default_gas_limit: None, - default_timeout: DEFAULT_TIMEOUT, - gov_contract: "gov".to_string(), - allowlist, - }; - let info = mock_info(&String::from("anyone"), &[]); - let res = instantiate(deps.as_mut(), mock_env(), info, instantiate_msg).unwrap(); - assert_eq!(0, res.messages.len()); - - for channel in channels { - add_channel(deps.as_mut(), channel); - } - deps -} diff --git a/contracts/cw4-stake/.cargo/config b/contracts/cw4-stake/.cargo/config deleted file mode 100644 index de2d36ac7..000000000 --- a/contracts/cw4-stake/.cargo/config +++ /dev/null @@ -1,5 +0,0 @@ -[alias] -wasm = "build --release --lib --target wasm32-unknown-unknown" -wasm-debug = "build --lib --target wasm32-unknown-unknown" -unit-test = "test --lib" -schema = "run --bin schema" diff --git a/contracts/cw4-stake/Cargo.toml b/contracts/cw4-stake/Cargo.toml deleted file mode 100644 index fd58c36eb..000000000 --- a/contracts/cw4-stake/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -name = "cw4-stake" -version = "1.1.2" -authors = ["Ethan Frey "] -edition = "2021" -description = "CW4 implementation of group based on staked tokens" -license = "Apache-2.0" -repository = "https://github.com/CosmWasm/cw-plus" -homepage = "https://cosmwasm.com" -documentation = "https://docs.cosmwasm.com" - -exclude = [ - # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. - "artifacts/*", -] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[lib] -crate-type = ["cdylib", "rlib"] - -[features] -# for more explicit tests, cargo test --features=backtraces -backtraces = ["cosmwasm-std/backtraces"] -# use library feature to disable all instantiate/execute/query exports -library = [] - -[dependencies] -cosmwasm-schema = { version = "1.4.0" } -cw-utils = "1.0.1" -cw2 = { path = "../../packages/cw2", version = "1.1.2" } -cw4 = { path = "../../packages/cw4", version = "1.1.2" } -cw20 = { path = "../../packages/cw20", version = "1.1.2" } -cw-controllers = { path = "../../packages/controllers", version = "1.1.2" } -cw-storage-plus = "1.1.0" -cosmwasm-std = { version = "1.4.0" } -schemars = "0.8.15" -serde = { version = "1.0.188", default-features = false, features = ["derive"] } -thiserror = { version = "1.0.49" } diff --git a/contracts/cw4-stake/README.md b/contracts/cw4-stake/README.md deleted file mode 100644 index d0f4488a3..000000000 --- a/contracts/cw4-stake/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# CW4 Stake - -This is a second implementation of the [cw4 spec](../../packages/cw4/README.md). -It fulfills all elements of the spec, including the raw query lookups, -and is designed to be used as a backing storage for -[cw3 compliant contracts](../../packages/cw3/README.md). - -It provides a similar API to [`cw4-group`] (which handles elected membership), -but rather than appointing members (by admin or multisig), their -membership and weight are based on the number of tokens they have staked. -This is similar to many DAOs. - -Only one denom can be bonded with both `min_bond` as the minimum amount -that must be sent by one address to enter, as well as `tokens_per_weight`, -which can be used to normalize the weight (eg. if the token is uatom -and you want 1 weight per ATOM, you can set `tokens_per_weight = 1_000_000`). - -There is also an unbonding period (`Duration`) which sets how long the -tokens are frozen before being released. These frozen tokens can neither -be used for voting, nor claimed by the original owner. Only after the period -can you get your tokens back. This liquidity loss is the "skin in the game" -provided by staking to this contract. - -## Instantiation - -**TODO** - -To create it, you must pass in a list of members, as well as an optional -`admin`, if you wish it to be mutable. - -```rust -pub struct InstantiateMsg { - /// denom of the token to stake - pub stake: String, - pub tokens_per_weight: u64, - pub min_bond: Uint128, - pub unbonding_period: Duration, -} -``` - -Members are defined by an address and a weight. This is transformed -and stored under their `CanonicalAddr`, in a format defined in -[cw4 raw queries](../../packages/cw4/README.md#raw). - -Note that 0 *is an allowed weight*. This doesn't give any voting rights, -but it does define this address is part of the group, which may be -meaningful in some circumstances. - -The weights of the members will be computed as the funds they send -(in tokens) divided by `tokens_per_weight`, rounded down to the nearest -whole number (i.e. using integer division). If the total sent is less than -`min_bond`, the stake will remain, but they will not be counted as a -member. If `min_bond` is higher than `tokens_per_weight`, you cannot -have any member with 0 weight. - -## Messages - -Most messages and queries are defined by the -[cw4 spec](../../packages/cw4/README.md). Please refer to it for more info. - -The following messages have been added to handle un/staking tokens: - -`Bond{}` - bond all staking tokens sent with the message and update membership weight - -`Unbond{tokens}` - starts the unbonding process for the given number - of tokens. The sender immediately loses weight from these tokens, - and can claim them back to his wallet after `unbonding_period` - -`Claim{}` - used to claim your native tokens that you previously "unbonded" - after the contract-defined waiting period (eg. 1 week) - -And the corresponding queries: - -`Claims{address}` - Claims shows the tokens in process of unbonding - for this address - -`Staked{address}` - Show the number of tokens currently staked by this address. diff --git a/contracts/cw4-stake/src/bin/schema.rs b/contracts/cw4-stake/src/bin/schema.rs deleted file mode 100644 index cc9b421db..000000000 --- a/contracts/cw4-stake/src/bin/schema.rs +++ /dev/null @@ -1,11 +0,0 @@ -use cosmwasm_schema::write_api; - -use cw4_stake::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; - -fn main() { - write_api! { - instantiate: InstantiateMsg, - execute: ExecuteMsg, - query: QueryMsg, - } -} diff --git a/contracts/cw4-stake/src/contract.rs b/contracts/cw4-stake/src/contract.rs deleted file mode 100644 index 9aa97ea30..000000000 --- a/contracts/cw4-stake/src/contract.rs +++ /dev/null @@ -1,1015 +0,0 @@ -#[cfg(not(feature = "library"))] -use cosmwasm_std::entry_point; -use cosmwasm_std::{ - coins, from_slice, to_binary, Addr, BankMsg, Binary, Deps, DepsMut, Env, MessageInfo, Order, - Response, StdResult, Storage, SubMsg, Uint128, WasmMsg, -}; - -use cw2::set_contract_version; -use cw20::{Balance, Cw20CoinVerified, Cw20ExecuteMsg, Cw20ReceiveMsg, Denom}; -use cw4::{ - Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, MemberResponse, - TotalWeightResponse, -}; -use cw_storage_plus::Bound; -use cw_utils::{maybe_addr, NativeBalance}; - -use crate::error::ContractError; -use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, ReceiveMsg, StakedResponse}; -use crate::state::{Config, ADMIN, CLAIMS, CONFIG, HOOKS, MEMBERS, STAKE, TOTAL}; - -// version info for migration info -const CONTRACT_NAME: &str = "crates.io:cw4-stake"; -const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); - -// Note, you can use StdResult in some functions where you do not -// make use of the custom errors -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn instantiate( - mut deps: DepsMut, - _env: Env, - _info: MessageInfo, - msg: InstantiateMsg, -) -> Result { - set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - let api = deps.api; - ADMIN.set(deps.branch(), maybe_addr(api, msg.admin)?)?; - - // min_bond is at least 1, so 0 stake -> non-membership - let min_bond = std::cmp::max(msg.min_bond, Uint128::new(1)); - - let config = Config { - denom: msg.denom, - tokens_per_weight: msg.tokens_per_weight, - min_bond, - unbonding_period: msg.unbonding_period, - }; - CONFIG.save(deps.storage, &config)?; - TOTAL.save(deps.storage, &0)?; - - Ok(Response::default()) -} - -// And declare a custom Error variant for the ones where you will want to make use of it -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn execute( - deps: DepsMut, - env: Env, - info: MessageInfo, - msg: ExecuteMsg, -) -> Result { - let api = deps.api; - match msg { - ExecuteMsg::UpdateAdmin { admin } => { - Ok(ADMIN.execute_update_admin(deps, info, maybe_addr(api, admin)?)?) - } - ExecuteMsg::AddHook { addr } => { - Ok(HOOKS.execute_add_hook(&ADMIN, deps, info, api.addr_validate(&addr)?)?) - } - ExecuteMsg::RemoveHook { addr } => { - Ok(HOOKS.execute_remove_hook(&ADMIN, deps, info, api.addr_validate(&addr)?)?) - } - ExecuteMsg::Bond {} => execute_bond(deps, env, Balance::from(info.funds), info.sender), - ExecuteMsg::Unbond { tokens: amount } => execute_unbond(deps, env, info, amount), - ExecuteMsg::Claim {} => execute_claim(deps, env, info), - ExecuteMsg::Receive(msg) => execute_receive(deps, env, info, msg), - } -} - -pub fn execute_bond( - deps: DepsMut, - env: Env, - amount: Balance, - sender: Addr, -) -> Result { - let cfg = CONFIG.load(deps.storage)?; - - // ensure the sent denom was proper - // NOTE: those clones are not needed (if we move denom, we return early), - // but the compiler cannot see that (yet...) - let amount = match (&cfg.denom, &amount) { - (Denom::Native(want), Balance::Native(have)) => must_pay_funds(have, want), - (Denom::Cw20(want), Balance::Cw20(have)) => { - if want == have.address { - Ok(have.amount) - } else { - Err(ContractError::InvalidDenom(want.into())) - } - } - _ => Err(ContractError::MixedNativeAndCw20( - "Invalid address or denom".to_string(), - )), - }?; - - // update the sender's stake - let new_stake = STAKE.update(deps.storage, &sender, |stake| -> StdResult<_> { - Ok(stake.unwrap_or_default() + amount) - })?; - - let messages = update_membership( - deps.storage, - sender.clone(), - new_stake, - &cfg, - env.block.height, - )?; - - Ok(Response::new() - .add_submessages(messages) - .add_attribute("action", "bond") - .add_attribute("amount", amount) - .add_attribute("sender", sender)) -} - -pub fn execute_receive( - deps: DepsMut, - env: Env, - info: MessageInfo, - wrapper: Cw20ReceiveMsg, -) -> Result { - // info.sender is the address of the cw20 contract (that re-sent this message). - // wrapper.sender is the address of the user that requested the cw20 contract to send this. - // This cannot be fully trusted (the cw20 contract can fake it), so only use it for actions - // in the address's favor (like paying/bonding tokens, not withdrawls) - let msg: ReceiveMsg = from_slice(&wrapper.msg)?; - let balance = Balance::Cw20(Cw20CoinVerified { - address: info.sender, - amount: wrapper.amount, - }); - let api = deps.api; - match msg { - ReceiveMsg::Bond {} => { - execute_bond(deps, env, balance, api.addr_validate(&wrapper.sender)?) - } - } -} - -pub fn execute_unbond( - deps: DepsMut, - env: Env, - info: MessageInfo, - amount: Uint128, -) -> Result { - // reduce the sender's stake - aborting if insufficient - let new_stake = STAKE.update(deps.storage, &info.sender, |stake| -> StdResult<_> { - Ok(stake.unwrap_or_default().checked_sub(amount)?) - })?; - - // provide them a claim - let cfg = CONFIG.load(deps.storage)?; - CLAIMS.create_claim( - deps.storage, - &info.sender, - amount, - cfg.unbonding_period.after(&env.block), - )?; - - let messages = update_membership( - deps.storage, - info.sender.clone(), - new_stake, - &cfg, - env.block.height, - )?; - - Ok(Response::new() - .add_submessages(messages) - .add_attribute("action", "unbond") - .add_attribute("amount", amount) - .add_attribute("sender", info.sender)) -} - -pub fn must_pay_funds(balance: &NativeBalance, denom: &str) -> Result { - match balance.0.len() { - 0 => Err(ContractError::NoFunds {}), - 1 => { - let balance = &balance.0; - let payment = balance[0].amount; - if balance[0].denom == denom { - Ok(payment) - } else { - Err(ContractError::MissingDenom(denom.to_string())) - } - } - _ => Err(ContractError::ExtraDenoms(denom.to_string())), - } -} - -fn update_membership( - storage: &mut dyn Storage, - sender: Addr, - new_stake: Uint128, - cfg: &Config, - height: u64, -) -> StdResult> { - // update their membership weight - let new = calc_weight(new_stake, cfg); - let old = MEMBERS.may_load(storage, &sender)?; - - // short-circuit if no change - if new == old { - return Ok(vec![]); - } - // otherwise, record change of weight - match new.as_ref() { - Some(w) => MEMBERS.save(storage, &sender, w, height), - None => MEMBERS.remove(storage, &sender, height), - }?; - - // update total - TOTAL.update(storage, |total| -> StdResult<_> { - Ok(total + new.unwrap_or_default() - old.unwrap_or_default()) - })?; - - // alert the hooks - let diff = MemberDiff::new(sender, old, new); - HOOKS.prepare_hooks(storage, |h| { - MemberChangedHookMsg::one(diff.clone()) - .into_cosmos_msg(h) - .map(SubMsg::new) - }) -} - -fn calc_weight(stake: Uint128, cfg: &Config) -> Option { - if stake < cfg.min_bond { - None - } else { - let w = stake.u128() / (cfg.tokens_per_weight.u128()); - Some(w as u64) - } -} - -pub fn execute_claim( - deps: DepsMut, - env: Env, - info: MessageInfo, -) -> Result { - let release = CLAIMS.claim_tokens(deps.storage, &info.sender, &env.block, None)?; - if release.is_zero() { - return Err(ContractError::NothingToClaim {}); - } - - let config = CONFIG.load(deps.storage)?; - let (amount_str, message) = match &config.denom { - Denom::Native(denom) => { - let amount_str = coin_to_string(release, denom.as_str()); - let amount = coins(release.u128(), denom); - let message = SubMsg::new(BankMsg::Send { - to_address: info.sender.to_string(), - amount, - }); - (amount_str, message) - } - Denom::Cw20(addr) => { - let amount_str = coin_to_string(release, addr.as_str()); - let transfer = Cw20ExecuteMsg::Transfer { - recipient: info.sender.clone().into(), - amount: release, - }; - let message = SubMsg::new(WasmMsg::Execute { - contract_addr: addr.into(), - msg: to_binary(&transfer)?, - funds: vec![], - }); - (amount_str, message) - } - }; - - Ok(Response::new() - .add_submessage(message) - .add_attribute("action", "claim") - .add_attribute("tokens", amount_str) - .add_attribute("sender", info.sender)) -} - -#[inline] -fn coin_to_string(amount: Uint128, denom: &str) -> String { - format!("{amount} {denom}") -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { - match msg { - QueryMsg::Member { - addr, - at_height: height, - } => to_binary(&query_member(deps, addr, height)?), - QueryMsg::ListMembers { start_after, limit } => { - to_binary(&list_members(deps, start_after, limit)?) - } - QueryMsg::TotalWeight {} => to_binary(&query_total_weight(deps)?), - QueryMsg::Claims { address } => { - to_binary(&CLAIMS.query_claims(deps, &deps.api.addr_validate(&address)?)?) - } - QueryMsg::Staked { address } => to_binary(&query_staked(deps, address)?), - QueryMsg::Admin {} => to_binary(&ADMIN.query_admin(deps)?), - QueryMsg::Hooks {} => to_binary(&HOOKS.query_hooks(deps)?), - } -} - -fn query_total_weight(deps: Deps) -> StdResult { - let weight = TOTAL.load(deps.storage)?; - Ok(TotalWeightResponse { weight }) -} - -pub fn query_staked(deps: Deps, addr: String) -> StdResult { - let addr = deps.api.addr_validate(&addr)?; - let stake = STAKE.may_load(deps.storage, &addr)?.unwrap_or_default(); - let denom = CONFIG.load(deps.storage)?.denom; - Ok(StakedResponse { stake, denom }) -} - -fn query_member(deps: Deps, addr: String, height: Option) -> StdResult { - let addr = deps.api.addr_validate(&addr)?; - let weight = match height { - Some(h) => MEMBERS.may_load_at_height(deps.storage, &addr, h), - None => MEMBERS.may_load(deps.storage, &addr), - }?; - Ok(MemberResponse { weight }) -} - -// settings for pagination -const MAX_LIMIT: u32 = 30; -const DEFAULT_LIMIT: u32 = 10; - -fn list_members( - deps: Deps, - start_after: Option, - limit: Option, -) -> StdResult { - let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - let addr = maybe_addr(deps.api, start_after)?; - let start = addr.as_ref().map(Bound::exclusive); - - let members = MEMBERS - .range(deps.storage, start, None, Order::Ascending) - .take(limit) - .map(|item| { - item.map(|(addr, weight)| Member { - addr: addr.into(), - weight, - }) - }) - .collect::>()?; - - Ok(MemberListResponse { members }) -} - -#[cfg(test)] -mod tests { - use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - use cosmwasm_std::{ - coin, from_slice, CosmosMsg, OverflowError, OverflowOperation, StdError, Storage, - }; - use cw20::Denom; - use cw4::{member_key, TOTAL_KEY}; - use cw_controllers::{AdminError, Claim, HookError}; - use cw_utils::Duration; - - use crate::error::ContractError; - - use super::*; - - const INIT_ADMIN: &str = "juan"; - const USER1: &str = "somebody"; - const USER2: &str = "else"; - const USER3: &str = "funny"; - const DENOM: &str = "stake"; - const TOKENS_PER_WEIGHT: Uint128 = Uint128::new(1_000); - const MIN_BOND: Uint128 = Uint128::new(5_000); - const UNBONDING_BLOCKS: u64 = 100; - const CW20_ADDRESS: &str = "wasm1234567890"; - - fn default_instantiate(deps: DepsMut) { - do_instantiate( - deps, - TOKENS_PER_WEIGHT, - MIN_BOND, - Duration::Height(UNBONDING_BLOCKS), - ) - } - - fn do_instantiate( - deps: DepsMut, - tokens_per_weight: Uint128, - min_bond: Uint128, - unbonding_period: Duration, - ) { - let msg = InstantiateMsg { - denom: Denom::Native("stake".to_string()), - tokens_per_weight, - min_bond, - unbonding_period, - admin: Some(INIT_ADMIN.into()), - }; - let info = mock_info("creator", &[]); - instantiate(deps, mock_env(), info, msg).unwrap(); - } - - fn cw20_instantiate(deps: DepsMut, unbonding_period: Duration) { - let msg = InstantiateMsg { - denom: Denom::Cw20(Addr::unchecked(CW20_ADDRESS)), - tokens_per_weight: TOKENS_PER_WEIGHT, - min_bond: MIN_BOND, - unbonding_period, - admin: Some(INIT_ADMIN.into()), - }; - let info = mock_info("creator", &[]); - instantiate(deps, mock_env(), info, msg).unwrap(); - } - - fn bond(mut deps: DepsMut, user1: u128, user2: u128, user3: u128, height_delta: u64) { - let mut env = mock_env(); - env.block.height += height_delta; - - for (addr, stake) in &[(USER1, user1), (USER2, user2), (USER3, user3)] { - if *stake != 0 { - let msg = ExecuteMsg::Bond {}; - let info = mock_info(addr, &coins(*stake, DENOM)); - execute(deps.branch(), env.clone(), info, msg).unwrap(); - } - } - } - - fn bond_cw20(mut deps: DepsMut, user1: u128, user2: u128, user3: u128, height_delta: u64) { - let mut env = mock_env(); - env.block.height += height_delta; - - for (addr, stake) in &[(USER1, user1), (USER2, user2), (USER3, user3)] { - if *stake != 0 { - let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { - sender: addr.to_string(), - amount: Uint128::new(*stake), - msg: to_binary(&ReceiveMsg::Bond {}).unwrap(), - }); - let info = mock_info(CW20_ADDRESS, &[]); - execute(deps.branch(), env.clone(), info, msg).unwrap(); - } - } - } - - fn unbond(mut deps: DepsMut, user1: u128, user2: u128, user3: u128, height_delta: u64) { - let mut env = mock_env(); - env.block.height += height_delta; - - for (addr, stake) in &[(USER1, user1), (USER2, user2), (USER3, user3)] { - if *stake != 0 { - let msg = ExecuteMsg::Unbond { - tokens: Uint128::new(*stake), - }; - let info = mock_info(addr, &[]); - execute(deps.branch(), env.clone(), info, msg).unwrap(); - } - } - } - - #[test] - fn proper_instantiation() { - let mut deps = mock_dependencies(); - default_instantiate(deps.as_mut()); - - // it worked, let's query the state - let res = ADMIN.query_admin(deps.as_ref()).unwrap(); - assert_eq!(Some(INIT_ADMIN.into()), res.admin); - - let res = query_total_weight(deps.as_ref()).unwrap(); - assert_eq!(0, res.weight); - } - - fn get_member(deps: Deps, addr: String, at_height: Option) -> Option { - let raw = query(deps, mock_env(), QueryMsg::Member { addr, at_height }).unwrap(); - let res: MemberResponse = from_slice(&raw).unwrap(); - res.weight - } - - // this tests the member queries - fn assert_users( - deps: Deps, - user1_weight: Option, - user2_weight: Option, - user3_weight: Option, - height: Option, - ) { - let member1 = get_member(deps, USER1.into(), height); - assert_eq!(member1, user1_weight); - - let member2 = get_member(deps, USER2.into(), height); - assert_eq!(member2, user2_weight); - - let member3 = get_member(deps, USER3.into(), height); - assert_eq!(member3, user3_weight); - - // this is only valid if we are not doing a historical query - if height.is_none() { - // compute expected metrics - let weights = [user1_weight, user2_weight, user3_weight]; - let sum: u64 = weights.iter().map(|x| x.unwrap_or_default()).sum(); - let count = weights.iter().filter(|x| x.is_some()).count(); - - // TODO: more detailed compare? - let msg = QueryMsg::ListMembers { - start_after: None, - limit: None, - }; - let raw = query(deps, mock_env(), msg).unwrap(); - let members: MemberListResponse = from_slice(&raw).unwrap(); - assert_eq!(count, members.members.len()); - - let raw = query(deps, mock_env(), QueryMsg::TotalWeight {}).unwrap(); - let total: TotalWeightResponse = from_slice(&raw).unwrap(); - assert_eq!(sum, total.weight); // 17 - 11 + 15 = 21 - } - } - - // this tests the member queries - fn assert_stake(deps: Deps, user1_stake: u128, user2_stake: u128, user3_stake: u128) { - let stake1 = query_staked(deps, USER1.into()).unwrap(); - assert_eq!(stake1.stake, Uint128::from(user1_stake)); - - let stake2 = query_staked(deps, USER2.into()).unwrap(); - assert_eq!(stake2.stake, Uint128::from(user2_stake)); - - let stake3 = query_staked(deps, USER3.into()).unwrap(); - assert_eq!(stake3.stake, Uint128::from(user3_stake)); - } - - #[test] - fn bond_stake_adds_membership() { - let mut deps = mock_dependencies(); - default_instantiate(deps.as_mut()); - let height = mock_env().block.height; - - // Assert original weights - assert_users(deps.as_ref(), None, None, None, None); - - // ensure it rounds down, and respects cut-off - bond(deps.as_mut(), 12_000, 7_500, 4_000, 1); - - // Assert updated weights - assert_stake(deps.as_ref(), 12_000, 7_500, 4_000); - assert_users(deps.as_ref(), Some(12), Some(7), None, None); - - // add some more, ensure the sum is properly respected (7.5 + 7.6 = 15 not 14) - bond(deps.as_mut(), 0, 7_600, 1_200, 2); - - // Assert updated weights - assert_stake(deps.as_ref(), 12_000, 15_100, 5_200); - assert_users(deps.as_ref(), Some(12), Some(15), Some(5), None); - - // check historical queries all work - assert_users(deps.as_ref(), None, None, None, Some(height + 1)); // before first stake - assert_users(deps.as_ref(), Some(12), Some(7), None, Some(height + 2)); // after first stake - assert_users(deps.as_ref(), Some(12), Some(15), Some(5), Some(height + 3)); - // after second stake - } - - #[test] - fn unbond_stake_update_membership() { - let mut deps = mock_dependencies(); - default_instantiate(deps.as_mut()); - let height = mock_env().block.height; - - // ensure it rounds down, and respects cut-off - bond(deps.as_mut(), 12_000, 7_500, 4_000, 1); - unbond(deps.as_mut(), 4_500, 2_600, 1_111, 2); - - // Assert updated weights - assert_stake(deps.as_ref(), 7_500, 4_900, 2_889); - assert_users(deps.as_ref(), Some(7), None, None, None); - - // Adding a little more returns weight - bond(deps.as_mut(), 600, 100, 2_222, 3); - - // Assert updated weights - assert_users(deps.as_ref(), Some(8), Some(5), Some(5), None); - - // check historical queries all work - assert_users(deps.as_ref(), None, None, None, Some(height + 1)); // before first stake - assert_users(deps.as_ref(), Some(12), Some(7), None, Some(height + 2)); // after first bond - assert_users(deps.as_ref(), Some(7), None, None, Some(height + 3)); // after first unbond - assert_users(deps.as_ref(), Some(8), Some(5), Some(5), Some(height + 4)); // after second bond - - // error if try to unbond more than stake (USER2 has 5000 staked) - let msg = ExecuteMsg::Unbond { - tokens: Uint128::new(5100), - }; - let mut env = mock_env(); - env.block.height += 5; - let info = mock_info(USER2, &[]); - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!( - err, - ContractError::Std(StdError::overflow(OverflowError::new( - OverflowOperation::Sub, - 5000, - 5100 - ))) - ); - } - - #[test] - fn cw20_token_bond() { - let mut deps = mock_dependencies(); - cw20_instantiate(deps.as_mut(), Duration::Height(2000)); - - // Assert original weights - assert_users(deps.as_ref(), None, None, None, None); - - // ensure it rounds down, and respects cut-off - bond_cw20(deps.as_mut(), 12_000, 7_500, 4_000, 1); - - // Assert updated weights - assert_stake(deps.as_ref(), 12_000, 7_500, 4_000); - assert_users(deps.as_ref(), Some(12), Some(7), None, None); - } - - #[test] - fn cw20_token_claim() { - let unbonding_period: u64 = 50; - let unbond_height: u64 = 10; - - let mut deps = mock_dependencies(); - let unbonding = Duration::Height(unbonding_period); - cw20_instantiate(deps.as_mut(), unbonding); - - // bond some tokens - bond_cw20(deps.as_mut(), 20_000, 13_500, 500, 1); - - // unbond part - unbond(deps.as_mut(), 7_900, 4_600, 0, unbond_height); - - // Assert updated weights - assert_stake(deps.as_ref(), 12_100, 8_900, 500); - assert_users(deps.as_ref(), Some(12), Some(8), None, None); - - // with proper claims - let mut env = mock_env(); - env.block.height += unbond_height; - let expires = unbonding.after(&env.block); - assert_eq!( - get_claims(deps.as_ref(), &Addr::unchecked(USER1)), - vec![Claim::new(7_900, expires)] - ); - - // wait til they expire and get payout - env.block.height += unbonding_period; - let res = execute( - deps.as_mut(), - env, - mock_info(USER1, &[]), - ExecuteMsg::Claim {}, - ) - .unwrap(); - assert_eq!(res.messages.len(), 1); - match &res.messages[0].msg { - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr, - msg, - funds, - }) => { - assert_eq!(contract_addr.as_str(), CW20_ADDRESS); - assert_eq!(funds.len(), 0); - let parsed: Cw20ExecuteMsg = from_slice(msg).unwrap(); - assert_eq!( - parsed, - Cw20ExecuteMsg::Transfer { - recipient: USER1.into(), - amount: Uint128::new(7_900) - } - ); - } - _ => panic!("Must initiate cw20 transfer"), - } - } - - #[test] - fn raw_queries_work() { - // add will over-write and remove have no effect - let mut deps = mock_dependencies(); - default_instantiate(deps.as_mut()); - // Set values as (11, 6, None) - bond(deps.as_mut(), 11_000, 6_000, 0, 1); - - // get total from raw key - let total_raw = deps.storage.get(TOTAL_KEY.as_bytes()).unwrap(); - let total: u64 = from_slice(&total_raw).unwrap(); - assert_eq!(17, total); - - // get member votes from raw key - let member2_raw = deps.storage.get(&member_key(USER2)).unwrap(); - let member2: u64 = from_slice(&member2_raw).unwrap(); - assert_eq!(6, member2); - - // and execute misses - let member3_raw = deps.storage.get(&member_key(USER3)); - assert_eq!(None, member3_raw); - } - - fn get_claims(deps: Deps, addr: &Addr) -> Vec { - CLAIMS.query_claims(deps, addr).unwrap().claims - } - - #[test] - fn unbond_claim_workflow() { - let mut deps = mock_dependencies(); - default_instantiate(deps.as_mut()); - - // create some data - bond(deps.as_mut(), 12_000, 7_500, 4_000, 1); - unbond(deps.as_mut(), 4_500, 2_600, 0, 2); - let mut env = mock_env(); - env.block.height += 2; - - // check the claims for each user - let expires = Duration::Height(UNBONDING_BLOCKS).after(&env.block); - assert_eq!( - get_claims(deps.as_ref(), &Addr::unchecked(USER1)), - vec![Claim::new(4_500, expires)] - ); - assert_eq!( - get_claims(deps.as_ref(), &Addr::unchecked(USER2)), - vec![Claim::new(2_600, expires)] - ); - assert_eq!(get_claims(deps.as_ref(), &Addr::unchecked(USER3)), vec![]); - - // do another unbond later on - let mut env2 = mock_env(); - env2.block.height += 22; - unbond(deps.as_mut(), 0, 1_345, 1_500, 22); - - // with updated claims - let expires2 = Duration::Height(UNBONDING_BLOCKS).after(&env2.block); - assert_eq!( - get_claims(deps.as_ref(), &Addr::unchecked(USER1)), - vec![Claim::new(4_500, expires)] - ); - assert_eq!( - get_claims(deps.as_ref(), &Addr::unchecked(USER2)), - vec![Claim::new(2_600, expires), Claim::new(1_345, expires2)] - ); - assert_eq!( - get_claims(deps.as_ref(), &Addr::unchecked(USER3)), - vec![Claim::new(1_500, expires2)] - ); - - // nothing can be withdrawn yet - let err = execute( - deps.as_mut(), - env2, - mock_info(USER1, &[]), - ExecuteMsg::Claim {}, - ) - .unwrap_err(); - assert_eq!(err, ContractError::NothingToClaim {}); - - // now mature first section, withdraw that - let mut env3 = mock_env(); - env3.block.height += 2 + UNBONDING_BLOCKS; - // first one can now release - let res = execute( - deps.as_mut(), - env3.clone(), - mock_info(USER1, &[]), - ExecuteMsg::Claim {}, - ) - .unwrap(); - assert_eq!( - res.messages, - vec![SubMsg::new(BankMsg::Send { - to_address: USER1.into(), - amount: coins(4_500, DENOM), - })] - ); - - // second releases partially - let res = execute( - deps.as_mut(), - env3.clone(), - mock_info(USER2, &[]), - ExecuteMsg::Claim {}, - ) - .unwrap(); - assert_eq!( - res.messages, - vec![SubMsg::new(BankMsg::Send { - to_address: USER2.into(), - amount: coins(2_600, DENOM), - })] - ); - - // but the third one cannot release - let err = execute( - deps.as_mut(), - env3, - mock_info(USER3, &[]), - ExecuteMsg::Claim {}, - ) - .unwrap_err(); - assert_eq!(err, ContractError::NothingToClaim {}); - - // claims updated properly - assert_eq!(get_claims(deps.as_ref(), &Addr::unchecked(USER1)), vec![]); - assert_eq!( - get_claims(deps.as_ref(), &Addr::unchecked(USER2)), - vec![Claim::new(1_345, expires2)] - ); - assert_eq!( - get_claims(deps.as_ref(), &Addr::unchecked(USER3)), - vec![Claim::new(1_500, expires2)] - ); - - // add another few claims for 2 - unbond(deps.as_mut(), 0, 600, 0, 30 + UNBONDING_BLOCKS); - unbond(deps.as_mut(), 0, 1_005, 0, 50 + UNBONDING_BLOCKS); - - // ensure second can claim all tokens at once - let mut env4 = mock_env(); - env4.block.height += 55 + UNBONDING_BLOCKS + UNBONDING_BLOCKS; - let res = execute( - deps.as_mut(), - env4, - mock_info(USER2, &[]), - ExecuteMsg::Claim {}, - ) - .unwrap(); - assert_eq!( - res.messages, - vec![SubMsg::new(BankMsg::Send { - to_address: USER2.into(), - // 1_345 + 600 + 1_005 - amount: coins(2_950, DENOM), - })] - ); - assert_eq!(get_claims(deps.as_ref(), &Addr::unchecked(USER2)), vec![]); - } - - #[test] - fn add_remove_hooks() { - // add will over-write and remove have no effect - let mut deps = mock_dependencies(); - default_instantiate(deps.as_mut()); - - let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); - assert!(hooks.hooks.is_empty()); - - let contract1 = String::from("hook1"); - let contract2 = String::from("hook2"); - - let add_msg = ExecuteMsg::AddHook { - addr: contract1.clone(), - }; - - // non-admin cannot add hook - let user_info = mock_info(USER1, &[]); - let err = execute( - deps.as_mut(), - mock_env(), - user_info.clone(), - add_msg.clone(), - ) - .unwrap_err(); - assert_eq!(err, HookError::Admin(AdminError::NotAdmin {}).into()); - - // admin can add it, and it appears in the query - let admin_info = mock_info(INIT_ADMIN, &[]); - let _ = execute( - deps.as_mut(), - mock_env(), - admin_info.clone(), - add_msg.clone(), - ) - .unwrap(); - let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); - assert_eq!(hooks.hooks, vec![contract1.clone()]); - - // cannot remove a non-registered contract - let remove_msg = ExecuteMsg::RemoveHook { - addr: contract2.clone(), - }; - let err = execute(deps.as_mut(), mock_env(), admin_info.clone(), remove_msg).unwrap_err(); - assert_eq!(err, HookError::HookNotRegistered {}.into()); - - // add second contract - let add_msg2 = ExecuteMsg::AddHook { - addr: contract2.clone(), - }; - let _ = execute(deps.as_mut(), mock_env(), admin_info.clone(), add_msg2).unwrap(); - let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); - assert_eq!(hooks.hooks, vec![contract1.clone(), contract2.clone()]); - - // cannot re-add an existing contract - let err = execute(deps.as_mut(), mock_env(), admin_info.clone(), add_msg).unwrap_err(); - assert_eq!(err, HookError::HookAlreadyRegistered {}.into()); - - // non-admin cannot remove - let remove_msg = ExecuteMsg::RemoveHook { addr: contract1 }; - let err = execute(deps.as_mut(), mock_env(), user_info, remove_msg.clone()).unwrap_err(); - assert_eq!(err, HookError::Admin(AdminError::NotAdmin {}).into()); - - // remove the original - let _ = execute(deps.as_mut(), mock_env(), admin_info, remove_msg).unwrap(); - let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); - assert_eq!(hooks.hooks, vec![contract2]); - } - - #[test] - fn hooks_fire() { - let mut deps = mock_dependencies(); - default_instantiate(deps.as_mut()); - - let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); - assert!(hooks.hooks.is_empty()); - - let contract1 = String::from("hook1"); - let contract2 = String::from("hook2"); - - // register 2 hooks - let admin_info = mock_info(INIT_ADMIN, &[]); - let add_msg = ExecuteMsg::AddHook { - addr: contract1.clone(), - }; - let add_msg2 = ExecuteMsg::AddHook { - addr: contract2.clone(), - }; - for msg in [add_msg, add_msg2] { - let _ = execute(deps.as_mut(), mock_env(), admin_info.clone(), msg).unwrap(); - } - - // check firing on bond - assert_users(deps.as_ref(), None, None, None, None); - let info = mock_info(USER1, &coins(13_800, DENOM)); - let res = execute(deps.as_mut(), mock_env(), info, ExecuteMsg::Bond {}).unwrap(); - assert_users(deps.as_ref(), Some(13), None, None, None); - - // ensure messages for each of the 2 hooks - assert_eq!(res.messages.len(), 2); - let diff = MemberDiff::new(USER1, None, Some(13)); - let hook_msg = MemberChangedHookMsg::one(diff); - let msg1 = SubMsg::new(hook_msg.clone().into_cosmos_msg(contract1.clone()).unwrap()); - let msg2 = SubMsg::new(hook_msg.into_cosmos_msg(contract2.clone()).unwrap()); - assert_eq!(res.messages, vec![msg1, msg2]); - - // check firing on unbond - let msg = ExecuteMsg::Unbond { - tokens: Uint128::new(7_300), - }; - let info = mock_info(USER1, &[]); - let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - assert_users(deps.as_ref(), Some(6), None, None, None); - - // ensure messages for each of the 2 hooks - assert_eq!(res.messages.len(), 2); - let diff = MemberDiff::new(USER1, Some(13), Some(6)); - let hook_msg = MemberChangedHookMsg::one(diff); - let msg1 = SubMsg::new(hook_msg.clone().into_cosmos_msg(contract1).unwrap()); - let msg2 = SubMsg::new(hook_msg.into_cosmos_msg(contract2).unwrap()); - assert_eq!(res.messages, vec![msg1, msg2]); - } - - #[test] - fn only_bond_valid_coins() { - let mut deps = mock_dependencies(); - default_instantiate(deps.as_mut()); - - // cannot bond with 0 coins - let info = mock_info(USER1, &[]); - let err = execute(deps.as_mut(), mock_env(), info, ExecuteMsg::Bond {}).unwrap_err(); - assert_eq!(err, ContractError::NoFunds {}); - - // cannot bond with incorrect denom - let info = mock_info(USER1, &[coin(500, "FOO")]); - let err = execute(deps.as_mut(), mock_env(), info, ExecuteMsg::Bond {}).unwrap_err(); - assert_eq!(err, ContractError::MissingDenom(DENOM.to_string())); - - // cannot bond with 2 coins (even if one is correct) - let info = mock_info(USER1, &[coin(1234, DENOM), coin(5000, "BAR")]); - let err = execute(deps.as_mut(), mock_env(), info, ExecuteMsg::Bond {}).unwrap_err(); - assert_eq!(err, ContractError::ExtraDenoms(DENOM.to_string())); - - // can bond with just the proper denom - // cannot bond with incorrect denom - let info = mock_info(USER1, &[coin(500, DENOM)]); - execute(deps.as_mut(), mock_env(), info, ExecuteMsg::Bond {}).unwrap(); - } - - #[test] - fn ensure_bonding_edge_cases() { - // use min_bond 0, tokens_per_weight 500 - let mut deps = mock_dependencies(); - do_instantiate( - deps.as_mut(), - Uint128::new(100), - Uint128::zero(), - Duration::Height(5), - ); - - // setting 50 tokens, gives us Some(0) weight - // even setting to 1 token - bond(deps.as_mut(), 50, 1, 102, 1); - assert_users(deps.as_ref(), Some(0), Some(0), Some(1), None); - - // reducing to 0 token makes us None even with min_bond 0 - unbond(deps.as_mut(), 49, 1, 102, 2); - assert_users(deps.as_ref(), Some(0), None, None, None); - } -} diff --git a/contracts/cw4-stake/src/error.rs b/contracts/cw4-stake/src/error.rs deleted file mode 100644 index d9f95c761..000000000 --- a/contracts/cw4-stake/src/error.rs +++ /dev/null @@ -1,40 +0,0 @@ -use cosmwasm_std::StdError; -use thiserror::Error; - -use cw_controllers::{AdminError, HookError}; - -#[derive(Error, Debug, PartialEq)] -pub enum ContractError { - #[error("{0}")] - Std(#[from] StdError), - - #[error("{0}")] - Admin(#[from] AdminError), - - #[error("{0}")] - Hook(#[from] HookError), - - #[error("Unauthorized")] - Unauthorized {}, - - #[error("No claims that can be released currently")] - NothingToClaim {}, - - #[error("Must send '{0}' to stake")] - MissingDenom(String), - - #[error("Sent unsupported denoms, must send '{0}' to stake")] - ExtraDenoms(String), - - #[error("Must send valid address to stake")] - InvalidDenom(String), - - #[error("Missed address or denom")] - MixedNativeAndCw20(String), - - #[error("No funds sent")] - NoFunds {}, - - #[error("No data in ReceiveMsg")] - NoData {}, -} diff --git a/contracts/cw4-stake/src/lib.rs b/contracts/cw4-stake/src/lib.rs deleted file mode 100644 index 6ff3f6f15..000000000 --- a/contracts/cw4-stake/src/lib.rs +++ /dev/null @@ -1,33 +0,0 @@ -/*! -This is a second implementation of the [cw4 spec](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw4/README.md). -It fulfills all elements of the spec, including the raw query lookups, -and is designed to be used as a backing storage for -[cw3 compliant contracts](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw3/README.md). - -It provides a similar API to [`cw4-group`](https://github.com/CosmWasm/cw-plus/blob/main/contracts/cw4-group/README.md) -(which handles elected membership), -but rather than appointing members (by admin or multisig), their -membership and weight are based on the number of tokens they have staked. -This is similar to many DAOs. - -Only one denom can be bonded with both `min_bond` as the minimum amount -that must be sent by one address to enter, as well as `tokens_per_weight`, -which can be used to normalize the weight (eg. if the token is uatom -and you want 1 weight per ATOM, you can set `tokens_per_weight = 1_000_000`). - -There is also an unbonding period (`Duration`) which sets how long the -tokens are frozen before being released. These frozen tokens can neither -be used for voting, nor claimed by the original owner. Only after the period -can you get your tokens back. This liquidity loss is the "skin in the game" -provided by staking to this contract. - -For more information on this contract, please check out the -[README](https://github.com/CosmWasm/cw-plus/blob/main/contracts/cw4-stake/README.md). -*/ - -pub mod contract; -mod error; -pub mod msg; -pub mod state; - -pub use crate::error::ContractError; diff --git a/contracts/cw4-stake/src/msg.rs b/contracts/cw4-stake/src/msg.rs deleted file mode 100644 index e1bcc7a5a..000000000 --- a/contracts/cw4-stake/src/msg.rs +++ /dev/null @@ -1,82 +0,0 @@ -use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::Uint128; - -use cw20::{Cw20ReceiveMsg, Denom}; -pub use cw_controllers::ClaimsResponse; -use cw_utils::Duration; - -#[cw_serde] -pub struct InstantiateMsg { - /// denom of the token to stake - pub denom: Denom, - pub tokens_per_weight: Uint128, - pub min_bond: Uint128, - pub unbonding_period: Duration, - - // admin can only add/remove hooks, not change other parameters - pub admin: Option, -} - -#[cw_serde] -pub enum ExecuteMsg { - /// Bond will bond all staking tokens sent with the message and update membership weight - Bond {}, - /// Unbond will start the unbonding process for the given number of tokens. - /// The sender immediately loses weight from these tokens, and can claim them - /// back to his wallet after `unbonding_period` - Unbond { tokens: Uint128 }, - /// Claim is used to claim your native tokens that you previously "unbonded" - /// after the contract-defined waiting period (eg. 1 week) - Claim {}, - - /// Change the admin - UpdateAdmin { admin: Option }, - /// Add a new hook to be informed of all membership changes. Must be called by Admin - AddHook { addr: String }, - /// Remove a hook. Must be called by Admin - RemoveHook { addr: String }, - - /// This accepts a properly-encoded ReceiveMsg from a cw20 contract - Receive(Cw20ReceiveMsg), -} - -#[cw_serde] -pub enum ReceiveMsg { - /// Only valid cw20 message is to bond the tokens - Bond {}, -} - -#[cw_serde] -#[derive(QueryResponses)] -pub enum QueryMsg { - /// Claims shows the tokens in process of unbonding for this address - #[returns(cw_controllers::ClaimsResponse)] - Claims { address: String }, - // Show the number of tokens currently staked by this address. - #[returns(StakedResponse)] - Staked { address: String }, - - #[returns(cw_controllers::AdminResponse)] - Admin {}, - #[returns(cw4::TotalWeightResponse)] - TotalWeight {}, - #[returns(cw4::MemberListResponse)] - ListMembers { - start_after: Option, - limit: Option, - }, - #[returns(cw4::MemberResponse)] - Member { - addr: String, - at_height: Option, - }, - /// Shows all registered hooks. - #[returns(cw_controllers::HooksResponse)] - Hooks {}, -} - -#[cw_serde] -pub struct StakedResponse { - pub stake: Uint128, - pub denom: Denom, -} diff --git a/contracts/cw4-stake/src/state.rs b/contracts/cw4-stake/src/state.rs deleted file mode 100644 index b27f969eb..000000000 --- a/contracts/cw4-stake/src/state.rs +++ /dev/null @@ -1,32 +0,0 @@ -use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Uint128}; -use cw20::Denom; -use cw4::TOTAL_KEY; -use cw_controllers::{Admin, Claims, Hooks}; -use cw_storage_plus::{Item, Map, SnapshotMap, Strategy}; -use cw_utils::Duration; - -pub const CLAIMS: Claims = Claims::new("claims"); - -#[cw_serde] -pub struct Config { - /// denom of the token to stake - pub denom: Denom, - pub tokens_per_weight: Uint128, - pub min_bond: Uint128, - pub unbonding_period: Duration, -} - -pub const ADMIN: Admin = Admin::new("admin"); -pub const HOOKS: Hooks = Hooks::new("cw4-hooks"); -pub const CONFIG: Item = Item::new("config"); -pub const TOTAL: Item = Item::new(TOTAL_KEY); - -pub const MEMBERS: SnapshotMap<&Addr, u64> = SnapshotMap::new( - cw4::MEMBERS_KEY, - cw4::MEMBERS_CHECKPOINTS, - cw4::MEMBERS_CHANGELOG, - Strategy::EveryBlock, -); - -pub const STAKE: Map<&Addr, Uint128> = Map::new("stake"); From 9cfb88d44129c41fd7a7dd95b76ac697f919f074 Mon Sep 17 00:00:00 2001 From: Sturdy <91910406+apollo-sturdy@users.noreply.github.com> Date: Wed, 10 Jan 2024 21:11:36 +0100 Subject: [PATCH 5/5] feat: restore cw20-base --- Cargo.lock | 76 - Cargo.toml | 16 - contracts/cw20-base/.cargo/config | 6 + contracts/cw20-base/Cargo.toml | 33 + contracts/cw20-base/README.md | 48 + contracts/cw20-base/src/allowances.rs | 879 ++++++++++ contracts/cw20-base/src/bin/schema.rs | 11 + contracts/cw20-base/src/contract.rs | 2219 +++++++++++++++++++++++++ contracts/cw20-base/src/enumerable.rs | 319 ++++ contracts/cw20-base/src/error.rs | 43 + contracts/cw20-base/src/lib.rs | 24 + contracts/cw20-base/src/msg.rs | 175 ++ contracts/cw20-base/src/state.rs | 36 + 13 files changed, 3793 insertions(+), 92 deletions(-) create mode 100644 contracts/cw20-base/.cargo/config create mode 100644 contracts/cw20-base/Cargo.toml create mode 100644 contracts/cw20-base/README.md create mode 100644 contracts/cw20-base/src/allowances.rs create mode 100644 contracts/cw20-base/src/bin/schema.rs create mode 100644 contracts/cw20-base/src/contract.rs create mode 100644 contracts/cw20-base/src/enumerable.rs create mode 100644 contracts/cw20-base/src/error.rs create mode 100644 contracts/cw20-base/src/lib.rs create mode 100644 contracts/cw20-base/src/msg.rs create mode 100644 contracts/cw20-base/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index 88d85f061..9ed2536d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,12 +19,6 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" -[[package]] -name = "assert_matches" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" - [[package]] name = "base16ct" version = "0.1.1" @@ -287,42 +281,6 @@ dependencies = [ "serde", ] -[[package]] -name = "cw1-subkeys" -version = "1.1.2" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-storage-plus", - "cw-utils", - "cw1", - "cw1-whitelist", - "cw2 1.1.2", - "schemars", - "semver", - "serde", - "thiserror", -] - -[[package]] -name = "cw1-whitelist" -version = "1.1.2" -dependencies = [ - "anyhow", - "assert_matches", - "cosmwasm-schema", - "cosmwasm-std", - "cw-multi-test", - "cw-storage-plus", - "cw-utils", - "cw1", - "cw2 1.1.2", - "derivative", - "schemars", - "serde", - "thiserror", -] - [[package]] name = "cw2" version = "1.1.2" @@ -379,23 +337,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "cw20-ics20" -version = "1.1.2" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-controllers", - "cw-storage-plus", - "cw-utils", - "cw2 1.1.2", - "cw20", - "schemars", - "semver", - "serde", - "thiserror", -] - [[package]] name = "cw3" version = "1.1.2" @@ -475,23 +416,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "cw4-stake" -version = "1.1.2" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-controllers", - "cw-storage-plus", - "cw-utils", - "cw2 1.1.2", - "cw20", - "cw4", - "schemars", - "serde", - "thiserror", -] - [[package]] name = "der" version = "0.6.1" diff --git a/Cargo.toml b/Cargo.toml index df1b7252f..f717598b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,14 +4,6 @@ members = ["packages/*", "contracts/*"] # Resolver has to be set explicitely in workspaces, see https://github.com/rust-lang/cargo/issues/9956 resolver = "2" -[profile.release.package.cw1-subkeys] -codegen-units = 1 -incremental = false - -[profile.release.package.cw1-whitelist] -codegen-units = 1 -incremental = false - [profile.release.package.cw3-fixed-multisig] codegen-units = 1 incremental = false @@ -24,18 +16,10 @@ incremental = false codegen-units = 1 incremental = false -[profile.release.package.cw4-stake] -codegen-units = 1 -incremental = false - [profile.release.package.cw20-base] codegen-units = 1 incremental = false -[profile.release.package.cw20-ics20] -codegen-units = 1 -incremental = false - [profile.release] rpath = false lto = true diff --git a/contracts/cw20-base/.cargo/config b/contracts/cw20-base/.cargo/config new file mode 100644 index 000000000..f5174787c --- /dev/null +++ b/contracts/cw20-base/.cargo/config @@ -0,0 +1,6 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +wasm-debug = "build --lib --target wasm32-unknown-unknown" +unit-test = "test --lib" +integration-test = "test --test integration" +schema = "run --bin schema" diff --git a/contracts/cw20-base/Cargo.toml b/contracts/cw20-base/Cargo.toml new file mode 100644 index 000000000..2f49e44c4 --- /dev/null +++ b/contracts/cw20-base/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "cw20-base" +version = "1.1.2" +authors = ["Ethan Frey "] +edition = "2021" +description = "Basic implementation of a CosmWasm-20 compliant token" +license = "Apache-2.0" +repository = "https://github.com/CosmWasm/cw-plus" +homepage = "https://cosmwasm.com" +documentation = "https://docs.cosmwasm.com" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-schema = { version = "1.4.0" } +cw2 = { path = "../../packages/cw2", version = "1.1.2" } +cw20 = { path = "../../packages/cw20", version = "1.1.2" } +cw-storage-plus = "1.1.0" +cosmwasm-std = { version = "1.4.0" } +schemars = "0.8.15" +semver = "1" +serde = { version = "1.0.188", default-features = false, features = ["derive"] } +thiserror = { version = "1.0.49" } + +[dev-dependencies] +cw-multi-test = "0.16.5" +cw-utils = "1.0.1" diff --git a/contracts/cw20-base/README.md b/contracts/cw20-base/README.md new file mode 100644 index 000000000..01db9e054 --- /dev/null +++ b/contracts/cw20-base/README.md @@ -0,0 +1,48 @@ +# CW20 Basic + +This is a basic implementation of a cw20 contract. It implements +the [CW20 spec](../../packages/cw20/README.md) and is designed to +be deployed as is, or imported into other contracts to easily build +cw20-compatible tokens with custom logic. + +Implements: + +- [x] CW20 Base +- [x] Mintable extension +- [x] Allowances extension + +## Running this contract + +You will need Rust 1.44.1+ with `wasm32-unknown-unknown` target installed. + +You can run unit tests on this via: + +`cargo test` + +Once you are happy with the content, you can compile it to wasm via: + +``` +RUSTFLAGS='-C link-arg=-s' cargo wasm +cp ../../target/wasm32-unknown-unknown/release/cw20_base.wasm . +ls -l cw20_base.wasm +sha256sum cw20_base.wasm +``` + +Or for a production-ready (optimized) build, run a build command in the +the repository root: https://github.com/CosmWasm/cw-plus#compiling. + +## Importing this contract + +You can also import much of the logic of this contract to build another +ERC20-contract, such as a bonding curve, overiding or extending what you +need. + +Basically, you just need to write your handle function and import +`cw20_base::contract::handle_transfer`, etc and dispatch to them. +This allows you to use custom `ExecuteMsg` and `QueryMsg` with your additional +calls, but then use the underlying implementation for the standard cw20 +messages you want to support. The same with `QueryMsg`. You *could* reuse `instantiate` +as it, but it is likely you will want to change it. And it is rather simple. + +Look at [`cw20-staking`](https://github.com/CosmWasm/cw-tokens/tree/main/contracts/cw20-staking) for an example of how to "inherit" +all this token functionality and combine it with custom logic. diff --git a/contracts/cw20-base/src/allowances.rs b/contracts/cw20-base/src/allowances.rs new file mode 100644 index 000000000..38b36da46 --- /dev/null +++ b/contracts/cw20-base/src/allowances.rs @@ -0,0 +1,879 @@ +use cosmwasm_std::{ + attr, Addr, Binary, BlockInfo, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, + Storage, Uint128, +}; +use cw20::{AllowanceResponse, Cw20ReceiveMsg, Expiration}; + +use crate::error::ContractError; +use crate::state::{ALLOWANCES, ALLOWANCES_SPENDER, BALANCES, TOKEN_INFO}; + +pub fn execute_increase_allowance( + deps: DepsMut, + env: Env, + info: MessageInfo, + spender: String, + amount: Uint128, + expires: Option, +) -> Result { + let spender_addr = deps.api.addr_validate(&spender)?; + if spender_addr == info.sender { + return Err(ContractError::CannotSetOwnAccount {}); + } + + let update_fn = |allow: Option| -> Result<_, _> { + let mut val = allow.unwrap_or_default(); + if let Some(exp) = expires { + if exp.is_expired(&env.block) { + return Err(ContractError::InvalidExpiration {}); + } + val.expires = exp; + } + val.allowance += amount; + Ok(val) + }; + ALLOWANCES.update(deps.storage, (&info.sender, &spender_addr), update_fn)?; + ALLOWANCES_SPENDER.update(deps.storage, (&spender_addr, &info.sender), update_fn)?; + + let res = Response::new().add_attributes(vec![ + attr("action", "increase_allowance"), + attr("owner", info.sender), + attr("spender", spender), + attr("amount", amount), + ]); + Ok(res) +} + +pub fn execute_decrease_allowance( + deps: DepsMut, + env: Env, + info: MessageInfo, + spender: String, + amount: Uint128, + expires: Option, +) -> Result { + let spender_addr = deps.api.addr_validate(&spender)?; + if spender_addr == info.sender { + return Err(ContractError::CannotSetOwnAccount {}); + } + + let key = (&info.sender, &spender_addr); + + fn reverse<'a>(t: (&'a Addr, &'a Addr)) -> (&'a Addr, &'a Addr) { + (t.1, t.0) + } + + // load value and delete if it hits 0, or update otherwise + let mut allowance = 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; + } + ALLOWANCES.save(deps.storage, key, &allowance)?; + ALLOWANCES_SPENDER.save(deps.storage, reverse(key), &allowance)?; + } else { + ALLOWANCES.remove(deps.storage, key); + ALLOWANCES_SPENDER.remove(deps.storage, reverse(key)); + } + + let res = Response::new().add_attributes(vec![ + attr("action", "decrease_allowance"), + attr("owner", info.sender), + attr("spender", spender), + attr("amount", amount), + ]); + Ok(res) +} + +// this can be used to update a lower allowance - call bucket.update with proper keys +pub fn deduct_allowance( + storage: &mut dyn Storage, + owner: &Addr, + spender: &Addr, + block: &BlockInfo, + amount: Uint128, +) -> Result { + let update_fn = |current: Option| -> _ { + match current { + Some(mut a) => { + if a.expires.is_expired(block) { + Err(ContractError::Expired {}) + } else { + // deduct the allowance if enough + a.allowance = a + .allowance + .checked_sub(amount) + .map_err(StdError::overflow)?; + Ok(a) + } + } + None => Err(ContractError::NoAllowance {}), + } + }; + ALLOWANCES.update(storage, (owner, spender), update_fn)?; + ALLOWANCES_SPENDER.update(storage, (spender, owner), update_fn) +} + +pub fn execute_transfer_from( + deps: DepsMut, + env: Env, + info: MessageInfo, + owner: String, + recipient: String, + amount: Uint128, +) -> Result { + let rcpt_addr = deps.api.addr_validate(&recipient)?; + let owner_addr = deps.api.addr_validate(&owner)?; + + // deduct allowance before doing anything else have enough allowance + deduct_allowance(deps.storage, &owner_addr, &info.sender, &env.block, amount)?; + + BALANCES.update( + deps.storage, + &owner_addr, + |balance: Option| -> StdResult<_> { + Ok(balance.unwrap_or_default().checked_sub(amount)?) + }, + )?; + BALANCES.update( + deps.storage, + &rcpt_addr, + |balance: Option| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) }, + )?; + + let res = Response::new().add_attributes(vec![ + attr("action", "transfer_from"), + attr("from", owner), + attr("to", recipient), + attr("by", info.sender), + attr("amount", amount), + ]); + Ok(res) +} + +pub fn execute_burn_from( + deps: DepsMut, + + env: Env, + info: MessageInfo, + owner: String, + amount: Uint128, +) -> Result { + let owner_addr = deps.api.addr_validate(&owner)?; + + // deduct allowance before doing anything else have enough allowance + deduct_allowance(deps.storage, &owner_addr, &info.sender, &env.block, amount)?; + + // lower balance + BALANCES.update( + deps.storage, + &owner_addr, + |balance: Option| -> StdResult<_> { + Ok(balance.unwrap_or_default().checked_sub(amount)?) + }, + )?; + // reduce total_supply + TOKEN_INFO.update(deps.storage, |mut meta| -> StdResult<_> { + meta.total_supply = meta.total_supply.checked_sub(amount)?; + Ok(meta) + })?; + + let res = Response::new().add_attributes(vec![ + attr("action", "burn_from"), + attr("from", owner), + attr("by", info.sender), + attr("amount", amount), + ]); + Ok(res) +} + +pub fn execute_send_from( + deps: DepsMut, + env: Env, + info: MessageInfo, + owner: String, + contract: String, + amount: Uint128, + msg: Binary, +) -> Result { + 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 + deduct_allowance(deps.storage, &owner_addr, &info.sender, &env.block, amount)?; + + // move the tokens to the contract + BALANCES.update( + deps.storage, + &owner_addr, + |balance: Option| -> StdResult<_> { + Ok(balance.unwrap_or_default().checked_sub(amount)?) + }, + )?; + BALANCES.update( + deps.storage, + &rcpt_addr, + |balance: Option| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) }, + )?; + + let attrs = vec![ + attr("action", "send_from"), + attr("from", &owner), + attr("to", &contract), + attr("by", &info.sender), + attr("amount", amount), + ]; + + // create a send message + let msg = Cw20ReceiveMsg { + sender: info.sender.into(), + amount, + msg, + } + .into_cosmos_msg(contract)?; + + let res = Response::new().add_message(msg).add_attributes(attrs); + Ok(res) +} + +pub fn query_allowance(deps: Deps, owner: String, spender: String) -> StdResult { + let owner_addr = deps.api.addr_validate(&owner)?; + let spender_addr = deps.api.addr_validate(&spender)?; + let allowance = ALLOWANCES + .may_load(deps.storage, (&owner_addr, &spender_addr))? + .unwrap_or_default(); + Ok(allowance) +} + +#[cfg(test)] +mod tests { + use super::*; + + use cosmwasm_std::testing::{mock_dependencies_with_balance, mock_env, mock_info}; + use cosmwasm_std::{coins, CosmosMsg, SubMsg, Timestamp, WasmMsg}; + use cw20::{Cw20Coin, TokenInfoResponse}; + + use crate::contract::{execute, instantiate, query_balance, query_token_info}; + use crate::msg::{ExecuteMsg, InstantiateMsg}; + + fn get_balance>(deps: Deps, address: T) -> Uint128 { + query_balance(deps, address.into()).unwrap().balance + } + + // this will set up the instantiation for other tests + fn do_instantiate>( + mut deps: DepsMut, + addr: T, + amount: Uint128, + ) -> TokenInfoResponse { + let instantiate_msg = InstantiateMsg { + name: "Auto Gen".to_string(), + symbol: "AUTO".to_string(), + decimals: 3, + initial_balances: vec![Cw20Coin { + address: addr.into(), + amount, + }], + mint: None, + marketing: None, + }; + let info = mock_info("creator", &[]); + let env = mock_env(); + instantiate(deps.branch(), env, info, instantiate_msg).unwrap(); + query_token_info(deps.as_ref()).unwrap() + } + + #[test] + fn increase_decrease_allowances() { + let mut deps = mock_dependencies_with_balance(&coins(2, "token")); + + let owner = String::from("addr0001"); + let spender = String::from("addr0002"); + let info = mock_info(owner.as_ref(), &[]); + let env = mock_env(); + do_instantiate(deps.as_mut(), owner.clone(), Uint128::new(12340000)); + + // no allowance to start + let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); + assert_eq!(allowance, AllowanceResponse::default()); + + // set allowance with height expiration + let allow1 = Uint128::new(7777); + let expires = Expiration::AtHeight(123_456); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: allow1, + expires: Some(expires), + }; + execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + + // ensure it looks good + let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); + assert_eq!( + allowance, + AllowanceResponse { + allowance: allow1, + expires + } + ); + + // decrease it a bit with no expire set - stays the same + let lower = Uint128::new(4444); + let allow2 = allow1.checked_sub(lower).unwrap(); + let msg = ExecuteMsg::DecreaseAllowance { + spender: spender.clone(), + amount: lower, + expires: None, + }; + execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); + assert_eq!( + allowance, + AllowanceResponse { + allowance: allow2, + expires + } + ); + + // increase it some more and override the expires + let raise = Uint128::new(87654); + let allow3 = allow2 + raise; + let new_expire = Expiration::AtTime(Timestamp::from_seconds(8888888888)); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: raise, + expires: Some(new_expire), + }; + execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); + assert_eq!( + allowance, + AllowanceResponse { + allowance: allow3, + expires: new_expire + } + ); + + // decrease it below 0 + let msg = ExecuteMsg::DecreaseAllowance { + spender: spender.clone(), + amount: Uint128::new(99988647623876347), + expires: None, + }; + execute(deps.as_mut(), env, info, msg).unwrap(); + let allowance = query_allowance(deps.as_ref(), owner, spender).unwrap(); + assert_eq!(allowance, AllowanceResponse::default()); + } + + #[test] + fn allowances_independent() { + let mut deps = mock_dependencies_with_balance(&coins(2, "token")); + + let owner = String::from("addr0001"); + let spender = String::from("addr0002"); + let spender2 = String::from("addr0003"); + let info = mock_info(owner.as_ref(), &[]); + let env = mock_env(); + do_instantiate(deps.as_mut(), &owner, Uint128::new(12340000)); + + // no allowance to start + assert_eq!( + query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(), + AllowanceResponse::default() + ); + assert_eq!( + query_allowance(deps.as_ref(), owner.clone(), spender2.clone()).unwrap(), + AllowanceResponse::default() + ); + assert_eq!( + query_allowance(deps.as_ref(), spender.clone(), spender2.clone()).unwrap(), + AllowanceResponse::default() + ); + + // set allowance with height expiration + let allow1 = Uint128::new(7777); + let expires = Expiration::AtHeight(123_456); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: allow1, + expires: Some(expires), + }; + execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + + // set other allowance with no expiration + let allow2 = Uint128::new(87654); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender2.clone(), + amount: allow2, + expires: None, + }; + execute(deps.as_mut(), env, info, msg).unwrap(); + + // check they are proper + let expect_one = AllowanceResponse { + allowance: allow1, + expires, + }; + let expect_two = AllowanceResponse { + allowance: allow2, + expires: Expiration::Never {}, + }; + assert_eq!( + query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(), + expect_one + ); + assert_eq!( + query_allowance(deps.as_ref(), owner.clone(), spender2.clone()).unwrap(), + expect_two + ); + assert_eq!( + query_allowance(deps.as_ref(), spender.clone(), spender2.clone()).unwrap(), + AllowanceResponse::default() + ); + + // also allow spender -> spender2 with no interference + let info = mock_info(spender.as_ref(), &[]); + let env = mock_env(); + let allow3 = Uint128::new(1821); + let expires3 = Expiration::AtTime(Timestamp::from_seconds(3767626296)); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender2.clone(), + amount: allow3, + expires: Some(expires3), + }; + execute(deps.as_mut(), env, info, msg).unwrap(); + let expect_three = AllowanceResponse { + allowance: allow3, + expires: expires3, + }; + assert_eq!( + query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(), + expect_one + ); + assert_eq!( + query_allowance(deps.as_ref(), owner, spender2.clone()).unwrap(), + expect_two + ); + assert_eq!( + query_allowance(deps.as_ref(), spender, spender2).unwrap(), + expect_three + ); + } + + #[test] + fn no_self_allowance() { + let mut deps = mock_dependencies_with_balance(&coins(2, "token")); + + let owner = String::from("addr0001"); + let info = mock_info(owner.as_ref(), &[]); + let env = mock_env(); + do_instantiate(deps.as_mut(), &owner, Uint128::new(12340000)); + + // self-allowance + let msg = ExecuteMsg::IncreaseAllowance { + spender: owner.clone(), + amount: Uint128::new(7777), + expires: None, + }; + let err = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap_err(); + assert_eq!(err, ContractError::CannotSetOwnAccount {}); + + // decrease self-allowance + let msg = ExecuteMsg::DecreaseAllowance { + spender: owner, + amount: Uint128::new(7777), + expires: None, + }; + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::CannotSetOwnAccount {}); + } + + #[test] + fn transfer_from_respects_limits() { + let mut deps = mock_dependencies_with_balance(&[]); + let owner = String::from("addr0001"); + let spender = String::from("addr0002"); + let rcpt = String::from("addr0003"); + + let start = Uint128::new(999999); + do_instantiate(deps.as_mut(), &owner, start); + + // provide an allowance + let allow1 = Uint128::new(77777); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: allow1, + expires: None, + }; + let info = mock_info(owner.as_ref(), &[]); + let env = mock_env(); + execute(deps.as_mut(), env, info, msg).unwrap(); + + // valid transfer of part of the allowance + let transfer = Uint128::new(44444); + let msg = ExecuteMsg::TransferFrom { + owner: owner.clone(), + recipient: rcpt.clone(), + amount: transfer, + }; + let info = mock_info(spender.as_ref(), &[]); + let env = mock_env(); + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!(res.attributes[0], attr("action", "transfer_from")); + + // make sure money arrived + assert_eq!( + get_balance(deps.as_ref(), owner.clone()), + start.checked_sub(transfer).unwrap() + ); + assert_eq!(get_balance(deps.as_ref(), rcpt.clone()), transfer); + + // ensure it looks good + let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); + let expect = AllowanceResponse { + allowance: allow1.checked_sub(transfer).unwrap(), + expires: Expiration::Never {}, + }; + assert_eq!(expect, allowance); + + // cannot send more than the allowance + let msg = ExecuteMsg::TransferFrom { + owner: owner.clone(), + recipient: rcpt.clone(), + amount: Uint128::new(33443), + }; + let info = mock_info(spender.as_ref(), &[]); + let env = mock_env(); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); + + // let us increase limit, but set the expiration to expire in the next block + let info = mock_info(owner.as_ref(), &[]); + let mut env = mock_env(); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: Uint128::new(1000), + expires: Some(Expiration::AtHeight(env.block.height + 1)), + }; + execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + + env.block.height += 1; + + // we should now get the expiration error + let msg = ExecuteMsg::TransferFrom { + owner, + recipient: rcpt, + amount: Uint128::new(33443), + }; + let info = mock_info(spender.as_ref(), &[]); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::Expired {}); + } + + #[test] + fn burn_from_respects_limits() { + let mut deps = mock_dependencies_with_balance(&[]); + let owner = String::from("addr0001"); + let spender = String::from("addr0002"); + + let start = Uint128::new(999999); + do_instantiate(deps.as_mut(), &owner, start); + + // provide an allowance + let allow1 = Uint128::new(77777); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: allow1, + expires: None, + }; + let info = mock_info(owner.as_ref(), &[]); + let env = mock_env(); + execute(deps.as_mut(), env, info, msg).unwrap(); + + // valid burn of part of the allowance + let transfer = Uint128::new(44444); + let msg = ExecuteMsg::BurnFrom { + owner: owner.clone(), + amount: transfer, + }; + let info = mock_info(spender.as_ref(), &[]); + let env = mock_env(); + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!(res.attributes[0], attr("action", "burn_from")); + + // make sure money burnt + assert_eq!( + get_balance(deps.as_ref(), owner.clone()), + start.checked_sub(transfer).unwrap() + ); + + // ensure it looks good + let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); + let expect = AllowanceResponse { + allowance: allow1.checked_sub(transfer).unwrap(), + expires: Expiration::Never {}, + }; + assert_eq!(expect, allowance); + + // cannot burn more than the allowance + let msg = ExecuteMsg::BurnFrom { + owner: owner.clone(), + amount: Uint128::new(33443), + }; + let info = mock_info(spender.as_ref(), &[]); + let env = mock_env(); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); + + // let us increase limit, but set the expiration to expire in the next block + let info = mock_info(owner.as_ref(), &[]); + let mut env = mock_env(); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: Uint128::new(1000), + expires: Some(Expiration::AtHeight(env.block.height + 1)), + }; + execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + + // increase block height, so the limit is expired now + env.block.height += 1; + + // we should now get the expiration error + let msg = ExecuteMsg::BurnFrom { + owner, + amount: Uint128::new(33443), + }; + let info = mock_info(spender.as_ref(), &[]); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::Expired {}); + } + + #[test] + fn send_from_respects_limits() { + let mut deps = mock_dependencies_with_balance(&[]); + let owner = String::from("addr0001"); + let spender = String::from("addr0002"); + let contract = String::from("cool-dex"); + let send_msg = Binary::from(r#"{"some":123}"#.as_bytes()); + + let start = Uint128::new(999999); + do_instantiate(deps.as_mut(), &owner, start); + + // provide an allowance + let allow1 = Uint128::new(77777); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: allow1, + expires: None, + }; + let info = mock_info(owner.as_ref(), &[]); + let env = mock_env(); + execute(deps.as_mut(), env, info, msg).unwrap(); + + // valid send of part of the allowance + let transfer = Uint128::new(44444); + let msg = ExecuteMsg::SendFrom { + owner: owner.clone(), + amount: transfer, + contract: contract.clone(), + msg: send_msg.clone(), + }; + let info = mock_info(spender.as_ref(), &[]); + let env = mock_env(); + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!(res.attributes[0], attr("action", "send_from")); + assert_eq!(1, res.messages.len()); + + // we record this as sent by the one who requested, not the one who was paying + let binary_msg = Cw20ReceiveMsg { + sender: spender.clone(), + amount: transfer, + msg: send_msg.clone(), + } + .into_binary() + .unwrap(); + assert_eq!( + res.messages[0], + SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract.clone(), + msg: binary_msg, + funds: vec![], + })) + ); + + // make sure money sent + assert_eq!( + get_balance(deps.as_ref(), owner.clone()), + start.checked_sub(transfer).unwrap() + ); + assert_eq!(get_balance(deps.as_ref(), contract.clone()), transfer); + + // ensure it looks good + let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); + let expect = AllowanceResponse { + allowance: allow1.checked_sub(transfer).unwrap(), + expires: Expiration::Never {}, + }; + assert_eq!(expect, allowance); + + // cannot send more than the allowance + let msg = ExecuteMsg::SendFrom { + owner: owner.clone(), + amount: Uint128::new(33443), + contract: contract.clone(), + msg: send_msg.clone(), + }; + let info = mock_info(spender.as_ref(), &[]); + let env = mock_env(); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); + + // let us increase limit, but set the expiration to the next block + let info = mock_info(owner.as_ref(), &[]); + let mut env = mock_env(); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: Uint128::new(1000), + expires: Some(Expiration::AtHeight(env.block.height + 1)), + }; + execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + + // increase block height, so the limit is expired now + env.block.height += 1; + + // we should now get the expiration error + let msg = ExecuteMsg::SendFrom { + owner, + amount: Uint128::new(33443), + contract, + msg: send_msg, + }; + let info = mock_info(spender.as_ref(), &[]); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::Expired {}); + } + + #[test] + fn no_past_expiration() { + let mut deps = mock_dependencies_with_balance(&coins(2, "token")); + + let owner = String::from("addr0001"); + let spender = String::from("addr0002"); + let info = mock_info(owner.as_ref(), &[]); + let env = mock_env(); + do_instantiate(deps.as_mut(), owner.clone(), Uint128::new(12340000)); + + // set allowance with height expiration at current block height + let expires = Expiration::AtHeight(env.block.height); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: Uint128::new(7777), + expires: Some(expires), + }; + + // ensure it is rejected + assert_eq!( + Err(ContractError::InvalidExpiration {}), + execute(deps.as_mut(), env.clone(), info.clone(), msg) + ); + + // set allowance with time expiration in the past + let expires = Expiration::AtTime(env.block.time.minus_seconds(1)); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: Uint128::new(7777), + expires: Some(expires), + }; + + // ensure it is rejected + assert_eq!( + Err(ContractError::InvalidExpiration {}), + execute(deps.as_mut(), env.clone(), info.clone(), msg) + ); + + // set allowance with height expiration at next block height + let expires = Expiration::AtHeight(env.block.height + 1); + let allow = Uint128::new(7777); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: allow, + expires: Some(expires), + }; + + execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + + // ensure it looks good + let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); + assert_eq!( + allowance, + AllowanceResponse { + allowance: allow, + expires + } + ); + + // set allowance with time expiration in the future + let expires = Expiration::AtTime(env.block.time.plus_seconds(10)); + let allow = Uint128::new(7777); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: allow, + expires: Some(expires), + }; + + execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + + // ensure it looks good + let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); + assert_eq!( + allowance, + AllowanceResponse { + allowance: allow + allow, // we increased twice + expires + } + ); + + // decrease with height expiration at current block height + let expires = Expiration::AtHeight(env.block.height); + let allow = Uint128::new(7777); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: allow, + expires: Some(expires), + }; + + // ensure it is rejected + assert_eq!( + Err(ContractError::InvalidExpiration {}), + execute(deps.as_mut(), env.clone(), info.clone(), msg) + ); + + // decrease with height expiration at next block height + let expires = Expiration::AtHeight(env.block.height + 1); + let allow = Uint128::new(7777); + let msg = ExecuteMsg::DecreaseAllowance { + spender: spender.clone(), + amount: allow, + expires: Some(expires), + }; + + execute(deps.as_mut(), env, info, msg).unwrap(); + + // ensure it looks good + let allowance = query_allowance(deps.as_ref(), owner, spender).unwrap(); + assert_eq!( + allowance, + AllowanceResponse { + allowance: allow, + expires + } + ); + } +} diff --git a/contracts/cw20-base/src/bin/schema.rs b/contracts/cw20-base/src/bin/schema.rs new file mode 100644 index 000000000..40d16fd4f --- /dev/null +++ b/contracts/cw20-base/src/bin/schema.rs @@ -0,0 +1,11 @@ +use cosmwasm_schema::write_api; + +use cw20_base::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg, + } +} diff --git a/contracts/cw20-base/src/contract.rs b/contracts/cw20-base/src/contract.rs new file mode 100644 index 000000000..6f92ac227 --- /dev/null +++ b/contracts/cw20-base/src/contract.rs @@ -0,0 +1,2219 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::Order::Ascending; +use cosmwasm_std::{ + to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Uint128, +}; + +use cw2::{ensure_from_older_version, set_contract_version}; +use cw20::{ + BalanceResponse, Cw20Coin, Cw20ReceiveMsg, DownloadLogoResponse, EmbeddedLogo, Logo, LogoInfo, + MarketingInfoResponse, MinterResponse, TokenInfoResponse, +}; + +use crate::allowances::{ + execute_burn_from, execute_decrease_allowance, execute_increase_allowance, execute_send_from, + execute_transfer_from, query_allowance, +}; +use crate::enumerable::{query_all_accounts, query_owner_allowances, query_spender_allowances}; +use crate::error::ContractError; +use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use crate::state::{ + MinterData, TokenInfo, ALLOWANCES, ALLOWANCES_SPENDER, BALANCES, LOGO, MARKETING_INFO, + TOKEN_INFO, +}; + +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:cw20-base"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +const LOGO_SIZE_CAP: usize = 5 * 1024; + +/// 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""; + + 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. +} + +/// 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 passed logo is correct, and if not, returns an error +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 + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + mut deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + // check valid token info + msg.validate()?; + // create initial accounts + let total_supply = create_accounts(&mut deps, &msg.initial_balances)?; + + if let Some(limit) = msg.get_cap() { + if total_supply > limit { + return Err(StdError::generic_err("Initial supply greater than cap").into()); + } + } + + let mint = match msg.mint { + Some(m) => Some(MinterData { + minter: deps.api.addr_validate(&m.minter)?, + cap: m.cap, + }), + None => None, + }; + + // store token info + let data = TokenInfo { + name: msg.name, + symbol: msg.symbol, + decimals: msg.decimals, + total_supply, + mint, + }; + TOKEN_INFO.save(deps.storage, &data)?; + + if let Some(marketing) = msg.marketing { + let logo = if let Some(logo) = marketing.logo { + verify_logo(&logo)?; + 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, + }; + MARKETING_INFO.save(deps.storage, &data)?; + } + + Ok(Response::default()) +} + +pub fn create_accounts( + deps: &mut DepsMut, + accounts: &[Cw20Coin], +) -> Result { + validate_accounts(accounts)?; + + let mut total_supply = Uint128::zero(); + for row in accounts { + let address = deps.api.addr_validate(&row.address)?; + BALANCES.save(deps.storage, &address, &row.amount)?; + total_supply += row.amount; + } + + Ok(total_supply) +} + +pub fn validate_accounts(accounts: &[Cw20Coin]) -> Result<(), ContractError> { + let mut addresses = accounts.iter().map(|c| &c.address).collect::>(); + addresses.sort(); + addresses.dedup(); + + if addresses.len() != accounts.len() { + Err(ContractError::DuplicateInitialBalanceAddresses {}) + } else { + Ok(()) + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Transfer { recipient, amount } => { + execute_transfer(deps, env, info, recipient, amount) + } + ExecuteMsg::Burn { amount } => execute_burn(deps, env, info, amount), + ExecuteMsg::Send { + contract, + amount, + msg, + } => execute_send(deps, env, info, contract, amount, msg), + ExecuteMsg::Mint { recipient, amount } => execute_mint(deps, env, info, recipient, amount), + ExecuteMsg::IncreaseAllowance { + spender, + amount, + expires, + } => execute_increase_allowance(deps, env, info, spender, amount, expires), + ExecuteMsg::DecreaseAllowance { + spender, + amount, + expires, + } => execute_decrease_allowance(deps, env, info, spender, amount, expires), + ExecuteMsg::TransferFrom { + owner, + recipient, + amount, + } => execute_transfer_from(deps, env, info, owner, recipient, amount), + ExecuteMsg::BurnFrom { owner, amount } => execute_burn_from(deps, env, info, owner, amount), + ExecuteMsg::SendFrom { + owner, + contract, + amount, + msg, + } => execute_send_from(deps, env, info, owner, contract, amount, msg), + ExecuteMsg::UpdateMarketing { + project, + description, + marketing, + } => execute_update_marketing(deps, env, info, project, description, marketing), + ExecuteMsg::UploadLogo(logo) => execute_upload_logo(deps, env, info, logo), + ExecuteMsg::UpdateMinter { new_minter } => { + execute_update_minter(deps, env, info, new_minter) + } + } +} + +pub fn execute_transfer( + deps: DepsMut, + _env: Env, + info: MessageInfo, + recipient: String, + amount: Uint128, +) -> Result { + let rcpt_addr = deps.api.addr_validate(&recipient)?; + + BALANCES.update( + deps.storage, + &info.sender, + |balance: Option| -> StdResult<_> { + Ok(balance.unwrap_or_default().checked_sub(amount)?) + }, + )?; + BALANCES.update( + deps.storage, + &rcpt_addr, + |balance: Option| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) }, + )?; + + let res = Response::new() + .add_attribute("action", "transfer") + .add_attribute("from", info.sender) + .add_attribute("to", recipient) + .add_attribute("amount", amount); + Ok(res) +} + +pub fn execute_burn( + deps: DepsMut, + _env: Env, + info: MessageInfo, + amount: Uint128, +) -> Result { + // lower balance + BALANCES.update( + deps.storage, + &info.sender, + |balance: Option| -> StdResult<_> { + Ok(balance.unwrap_or_default().checked_sub(amount)?) + }, + )?; + // reduce total_supply + 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) +} + +pub fn execute_mint( + deps: DepsMut, + _env: Env, + info: MessageInfo, + recipient: String, + amount: Uint128, +) -> Result { + let mut config = TOKEN_INFO + .may_load(deps.storage)? + .ok_or(ContractError::Unauthorized {})?; + + if config + .mint + .as_ref() + .ok_or(ContractError::Unauthorized {})? + .minter + != info.sender + { + return Err(ContractError::Unauthorized {}); + } + + // update supply and enforce cap + config.total_supply += amount; + if let Some(limit) = config.get_cap() { + if config.total_supply > limit { + return Err(ContractError::CannotExceedCap {}); + } + } + TOKEN_INFO.save(deps.storage, &config)?; + + // add amount to recipient balance + let rcpt_addr = deps.api.addr_validate(&recipient)?; + BALANCES.update( + deps.storage, + &rcpt_addr, + |balance: Option| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) }, + )?; + + let res = Response::new() + .add_attribute("action", "mint") + .add_attribute("to", recipient) + .add_attribute("amount", amount); + Ok(res) +} + +pub fn execute_send( + deps: DepsMut, + _env: Env, + info: MessageInfo, + contract: String, + amount: Uint128, + msg: Binary, +) -> Result { + let rcpt_addr = deps.api.addr_validate(&contract)?; + + // move the tokens to the contract + BALANCES.update( + deps.storage, + &info.sender, + |balance: Option| -> StdResult<_> { + Ok(balance.unwrap_or_default().checked_sub(amount)?) + }, + )?; + BALANCES.update( + deps.storage, + &rcpt_addr, + |balance: Option| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) }, + )?; + + let res = Response::new() + .add_attribute("action", "send") + .add_attribute("from", &info.sender) + .add_attribute("to", &contract) + .add_attribute("amount", amount) + .add_message( + Cw20ReceiveMsg { + sender: info.sender.into(), + amount, + msg, + } + .into_cosmos_msg(contract)?, + ); + Ok(res) +} + +pub fn execute_update_minter( + deps: DepsMut, + _env: Env, + info: MessageInfo, + new_minter: Option, +) -> Result { + let mut config = TOKEN_INFO + .may_load(deps.storage)? + .ok_or(ContractError::Unauthorized {})?; + + let mint = config.mint.as_ref().ok_or(ContractError::Unauthorized {})?; + if mint.minter != info.sender { + return Err(ContractError::Unauthorized {}); + } + + let minter_data = new_minter + .map(|new_minter| deps.api.addr_validate(&new_minter)) + .transpose()? + .map(|minter| MinterData { + minter, + cap: mint.cap, + }); + + config.mint = minter_data; + + TOKEN_INFO.save(deps.storage, &config)?; + + Ok(Response::default() + .add_attribute("action", "update_minter") + .add_attribute( + "new_minter", + config + .mint + .map(|m| m.minter.into_string()) + .unwrap_or_else(|| "None".to_string()), + )) +} + +pub fn execute_update_marketing( + deps: DepsMut, + _env: Env, + info: MessageInfo, + project: Option, + description: Option, + marketing: Option, +) -> Result { + let mut marketing_info = MARKETING_INFO + .may_load(deps.storage)? + .ok_or(ContractError::Unauthorized {})?; + + if marketing_info + .marketing + .as_ref() + .ok_or(ContractError::Unauthorized {})? + != info.sender + { + return Err(ContractError::Unauthorized {}); + } + + match project { + Some(empty) if empty.trim().is_empty() => marketing_info.project = None, + Some(project) => marketing_info.project = Some(project), + None => (), + } + + match description { + Some(empty) if empty.trim().is_empty() => marketing_info.description = None, + Some(description) => marketing_info.description = Some(description), + None => (), + } + + match marketing { + Some(empty) if empty.trim().is_empty() => marketing_info.marketing = None, + Some(marketing) => marketing_info.marketing = Some(deps.api.addr_validate(&marketing)?), + None => (), + } + + if marketing_info.project.is_none() + && marketing_info.description.is_none() + && marketing_info.marketing.is_none() + && marketing_info.logo.is_none() + { + MARKETING_INFO.remove(deps.storage); + } else { + MARKETING_INFO.save(deps.storage, &marketing_info)?; + } + + let res = Response::new().add_attribute("action", "update_marketing"); + Ok(res) +} + +pub fn execute_upload_logo( + deps: DepsMut, + _env: Env, + info: MessageInfo, + logo: Logo, +) -> Result { + let mut marketing_info = MARKETING_INFO + .may_load(deps.storage)? + .ok_or(ContractError::Unauthorized {})?; + + verify_logo(&logo)?; + + if marketing_info + .marketing + .as_ref() + .ok_or(ContractError::Unauthorized {})? + != info.sender + { + return Err(ContractError::Unauthorized {}); + } + + LOGO.save(deps.storage, &logo)?; + + let logo_info = match logo { + Logo::Url(url) => LogoInfo::Url(url), + Logo::Embedded(_) => LogoInfo::Embedded, + }; + + marketing_info.logo = Some(logo_info); + MARKETING_INFO.save(deps.storage, &marketing_info)?; + + let res = Response::new().add_attribute("action", "upload_logo"); + Ok(res) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Balance { address } => to_binary(&query_balance(deps, address)?), + QueryMsg::TokenInfo {} => to_binary(&query_token_info(deps)?), + QueryMsg::Minter {} => to_binary(&query_minter(deps)?), + QueryMsg::Allowance { owner, spender } => { + to_binary(&query_allowance(deps, owner, spender)?) + } + QueryMsg::AllAllowances { + owner, + start_after, + limit, + } => to_binary(&query_owner_allowances(deps, owner, start_after, limit)?), + QueryMsg::AllSpenderAllowances { + spender, + start_after, + limit, + } => to_binary(&query_spender_allowances( + deps, + spender, + start_after, + limit, + )?), + QueryMsg::AllAccounts { start_after, limit } => { + to_binary(&query_all_accounts(deps, start_after, limit)?) + } + QueryMsg::MarketingInfo {} => to_binary(&query_marketing_info(deps)?), + QueryMsg::DownloadLogo {} => to_binary(&query_download_logo(deps)?), + } +} + +pub fn query_balance(deps: Deps, address: String) -> StdResult { + let address = deps.api.addr_validate(&address)?; + let balance = BALANCES + .may_load(deps.storage, &address)? + .unwrap_or_default(); + Ok(BalanceResponse { balance }) +} + +pub fn query_token_info(deps: Deps) -> StdResult { + let info = TOKEN_INFO.load(deps.storage)?; + let res = TokenInfoResponse { + name: info.name, + symbol: info.symbol, + decimals: info.decimals, + total_supply: info.total_supply, + }; + Ok(res) +} + +pub fn query_minter(deps: Deps) -> StdResult> { + let meta = TOKEN_INFO.load(deps.storage)?; + let minter = match meta.mint { + Some(m) => Some(MinterResponse { + minter: m.minter.into(), + cap: m.cap, + }), + None => None, + }; + Ok(minter) +} + +pub fn query_marketing_info(deps: Deps) -> StdResult { + Ok(MARKETING_INFO.may_load(deps.storage)?.unwrap_or_default()) +} + +pub fn query_download_logo(deps: Deps) -> StdResult { + let logo = LOGO.load(deps.storage)?; + match logo { + Logo::Embedded(EmbeddedLogo::Svg(logo)) => Ok(DownloadLogoResponse { + mime_type: "image/svg+xml".to_owned(), + data: logo, + }), + Logo::Embedded(EmbeddedLogo::Png(logo)) => Ok(DownloadLogoResponse { + mime_type: "image/png".to_owned(), + data: logo, + }), + Logo::Url(_) => Err(StdError::not_found("logo")), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + let original_version = + ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + if original_version < "0.14.0".parse::().unwrap() { + // Build reverse map of allowances per spender + let data = ALLOWANCES + .range(deps.storage, None, None, Ascending) + .collect::>>()?; + for ((owner, spender), allowance) in data { + ALLOWANCES_SPENDER.save(deps.storage, (&spender, &owner), &allowance)?; + } + } + Ok(Response::default()) +} + +#[cfg(test)] +mod tests { + use cosmwasm_std::testing::{ + mock_dependencies, mock_dependencies_with_balance, mock_env, mock_info, + }; + use cosmwasm_std::{coins, from_binary, Addr, CosmosMsg, StdError, SubMsg, WasmMsg}; + + use super::*; + use crate::msg::InstantiateMarketingInfo; + + fn get_balance>(deps: Deps, address: T) -> Uint128 { + query_balance(deps, address.into()).unwrap().balance + } + + // this will set up the instantiation for other tests + fn do_instantiate_with_minter( + deps: DepsMut, + addr: &str, + amount: Uint128, + minter: &str, + cap: Option, + ) -> TokenInfoResponse { + _do_instantiate( + deps, + addr, + amount, + Some(MinterResponse { + minter: minter.to_string(), + cap, + }), + ) + } + + // this will set up the instantiation for other tests + fn do_instantiate(deps: DepsMut, addr: &str, amount: Uint128) -> TokenInfoResponse { + _do_instantiate(deps, addr, amount, None) + } + + // this will set up the instantiation for other tests + fn _do_instantiate( + mut deps: DepsMut, + addr: &str, + amount: Uint128, + mint: Option, + ) -> TokenInfoResponse { + let instantiate_msg = InstantiateMsg { + name: "Auto Gen".to_string(), + symbol: "AUTO".to_string(), + decimals: 3, + initial_balances: vec![Cw20Coin { + address: addr.to_string(), + amount, + }], + mint: mint.clone(), + marketing: None, + }; + let info = mock_info("creator", &[]); + let env = mock_env(); + let res = instantiate(deps.branch(), env, info, instantiate_msg).unwrap(); + assert_eq!(0, res.messages.len()); + + let meta = query_token_info(deps.as_ref()).unwrap(); + assert_eq!( + meta, + TokenInfoResponse { + name: "Auto Gen".to_string(), + symbol: "AUTO".to_string(), + decimals: 3, + total_supply: amount, + } + ); + assert_eq!(get_balance(deps.as_ref(), addr), amount); + assert_eq!(query_minter(deps.as_ref()).unwrap(), mint,); + meta + } + + const PNG_HEADER: [u8; 8] = [0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]; + + mod instantiate { + use super::*; + + #[test] + fn basic() { + let mut deps = mock_dependencies(); + let amount = Uint128::from(11223344u128); + let instantiate_msg = InstantiateMsg { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + initial_balances: vec![Cw20Coin { + address: String::from("addr0000"), + amount, + }], + mint: None, + marketing: None, + }; + let info = mock_info("creator", &[]); + let env = mock_env(); + let res = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap(); + assert_eq!(0, res.messages.len()); + + assert_eq!( + query_token_info(deps.as_ref()).unwrap(), + TokenInfoResponse { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + total_supply: amount, + } + ); + assert_eq!( + get_balance(deps.as_ref(), "addr0000"), + Uint128::new(11223344) + ); + } + + #[test] + fn mintable() { + let mut deps = mock_dependencies(); + let amount = Uint128::new(11223344); + let minter = String::from("asmodat"); + let limit = Uint128::new(511223344); + let instantiate_msg = InstantiateMsg { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + initial_balances: vec![Cw20Coin { + address: "addr0000".into(), + amount, + }], + mint: Some(MinterResponse { + minter: minter.clone(), + cap: Some(limit), + }), + marketing: None, + }; + let info = mock_info("creator", &[]); + let env = mock_env(); + let res = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap(); + assert_eq!(0, res.messages.len()); + + assert_eq!( + query_token_info(deps.as_ref()).unwrap(), + TokenInfoResponse { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + total_supply: amount, + } + ); + assert_eq!( + get_balance(deps.as_ref(), "addr0000"), + Uint128::new(11223344) + ); + assert_eq!( + query_minter(deps.as_ref()).unwrap(), + Some(MinterResponse { + minter, + cap: Some(limit), + }), + ); + } + + #[test] + fn mintable_over_cap() { + let mut deps = mock_dependencies(); + let amount = Uint128::new(11223344); + let minter = String::from("asmodat"); + let limit = Uint128::new(11223300); + let instantiate_msg = InstantiateMsg { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + initial_balances: vec![Cw20Coin { + address: String::from("addr0000"), + amount, + }], + mint: Some(MinterResponse { + minter, + cap: Some(limit), + }), + marketing: None, + }; + let info = mock_info("creator", &[]); + let env = mock_env(); + let err = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap_err(); + assert_eq!( + err, + StdError::generic_err("Initial supply greater than cap").into() + ); + } + + mod marketing { + use super::*; + + #[test] + fn basic() { + let mut deps = mock_dependencies(); + let instantiate_msg = InstantiateMsg { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + initial_balances: vec![], + mint: None, + marketing: Some(InstantiateMarketingInfo { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some("marketing".to_owned()), + logo: Some(Logo::Url("url".to_owned())), + }), + }; + + let info = mock_info("creator", &[]); + let env = mock_env(); + let res = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap(); + assert_eq!(0, res.messages.len()); + + assert_eq!( + query_marketing_info(deps.as_ref()).unwrap(), + MarketingInfoResponse { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some(Addr::unchecked("marketing")), + logo: Some(LogoInfo::Url("url".to_owned())), + } + ); + + let err = query_download_logo(deps.as_ref()).unwrap_err(); + assert!( + matches!(err, StdError::NotFound { .. }), + "Expected StdError::NotFound, received {err}", + ); + } + + #[test] + fn invalid_marketing() { + let mut deps = mock_dependencies(); + let instantiate_msg = InstantiateMsg { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + initial_balances: vec![], + mint: None, + marketing: Some(InstantiateMarketingInfo { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some("m".to_owned()), + logo: Some(Logo::Url("url".to_owned())), + }), + }; + + let info = mock_info("creator", &[]); + let env = mock_env(); + instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap_err(); + + let err = query_download_logo(deps.as_ref()).unwrap_err(); + assert!( + matches!(err, StdError::NotFound { .. }), + "Expected StdError::NotFound, received {err}", + ); + } + } + } + + #[test] + fn can_mint_by_minter() { + let mut deps = mock_dependencies(); + + let genesis = String::from("genesis"); + let amount = Uint128::new(11223344); + let minter = String::from("asmodat"); + let limit = Uint128::new(511223344); + do_instantiate_with_minter(deps.as_mut(), &genesis, amount, &minter, Some(limit)); + + // minter can mint coins to some winner + let winner = String::from("lucky"); + let prize = Uint128::new(222_222_222); + let msg = ExecuteMsg::Mint { + recipient: winner.clone(), + amount: prize, + }; + + let info = mock_info(minter.as_ref(), &[]); + let env = mock_env(); + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!(0, res.messages.len()); + assert_eq!(get_balance(deps.as_ref(), genesis), amount); + assert_eq!(get_balance(deps.as_ref(), winner.clone()), prize); + + // Allows minting 0 + let msg = ExecuteMsg::Mint { + recipient: winner.clone(), + amount: Uint128::zero(), + }; + let info = mock_info(minter.as_ref(), &[]); + let env = mock_env(); + execute(deps.as_mut(), env, info, msg).unwrap(); + + // but if it exceeds cap (even over multiple rounds), it fails + // cap is enforced + let msg = ExecuteMsg::Mint { + recipient: winner, + amount: Uint128::new(333_222_222), + }; + let info = mock_info(minter.as_ref(), &[]); + let env = mock_env(); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::CannotExceedCap {}); + } + + #[test] + fn others_cannot_mint() { + let mut deps = mock_dependencies(); + do_instantiate_with_minter( + deps.as_mut(), + &String::from("genesis"), + Uint128::new(1234), + &String::from("minter"), + None, + ); + + let msg = ExecuteMsg::Mint { + recipient: String::from("lucky"), + amount: Uint128::new(222), + }; + let info = mock_info("anyone else", &[]); + let env = mock_env(); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::Unauthorized {}); + } + + #[test] + fn minter_can_update_minter_but_not_cap() { + let mut deps = mock_dependencies(); + let minter = String::from("minter"); + let cap = Some(Uint128::from(3000000u128)); + do_instantiate_with_minter( + deps.as_mut(), + &String::from("genesis"), + Uint128::new(1234), + &minter, + cap, + ); + + let new_minter = "new_minter"; + let msg = ExecuteMsg::UpdateMinter { + new_minter: Some(new_minter.to_string()), + }; + + let info = mock_info(&minter, &[]); + let env = mock_env(); + let res = execute(deps.as_mut(), env.clone(), info, msg); + assert!(res.is_ok()); + let query_minter_msg = QueryMsg::Minter {}; + let res = query(deps.as_ref(), env, query_minter_msg); + let mint: MinterResponse = from_binary(&res.unwrap()).unwrap(); + + // Minter cannot update cap. + assert!(mint.cap == cap); + assert!(mint.minter == new_minter) + } + + #[test] + fn others_cannot_update_minter() { + let mut deps = mock_dependencies(); + let minter = String::from("minter"); + do_instantiate_with_minter( + deps.as_mut(), + &String::from("genesis"), + Uint128::new(1234), + &minter, + None, + ); + + let msg = ExecuteMsg::UpdateMinter { + new_minter: Some("new_minter".to_string()), + }; + + let info = mock_info("not the minter", &[]); + let env = mock_env(); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::Unauthorized {}); + } + + #[test] + fn unset_minter() { + let mut deps = mock_dependencies(); + let minter = String::from("minter"); + let cap = None; + do_instantiate_with_minter( + deps.as_mut(), + &String::from("genesis"), + Uint128::new(1234), + &minter, + cap, + ); + + let msg = ExecuteMsg::UpdateMinter { new_minter: None }; + + let info = mock_info(&minter, &[]); + let env = mock_env(); + let res = execute(deps.as_mut(), env.clone(), info, msg); + assert!(res.is_ok()); + let query_minter_msg = QueryMsg::Minter {}; + let res = query(deps.as_ref(), env, query_minter_msg); + let mint: Option = from_binary(&res.unwrap()).unwrap(); + + // Check that mint information was removed. + assert_eq!(mint, None); + + // Check that old minter can no longer mint. + let msg = ExecuteMsg::Mint { + recipient: String::from("lucky"), + amount: Uint128::new(222), + }; + let info = mock_info("minter", &[]); + let env = mock_env(); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::Unauthorized {}); + } + + #[test] + fn no_one_mints_if_minter_unset() { + let mut deps = mock_dependencies(); + do_instantiate(deps.as_mut(), &String::from("genesis"), Uint128::new(1234)); + + let msg = ExecuteMsg::Mint { + recipient: String::from("lucky"), + amount: Uint128::new(222), + }; + let info = mock_info("genesis", &[]); + let env = mock_env(); + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(err, ContractError::Unauthorized {}); + } + + #[test] + fn instantiate_multiple_accounts() { + let mut deps = mock_dependencies(); + let amount1 = Uint128::from(11223344u128); + let addr1 = String::from("addr0001"); + let amount2 = Uint128::from(7890987u128); + let addr2 = String::from("addr0002"); + let info = mock_info("creator", &[]); + let env = mock_env(); + + // Fails with duplicate addresses + let instantiate_msg = InstantiateMsg { + name: "Bash Shell".to_string(), + symbol: "BASH".to_string(), + decimals: 6, + initial_balances: vec![ + Cw20Coin { + address: addr1.clone(), + amount: amount1, + }, + Cw20Coin { + address: addr1.clone(), + amount: amount2, + }, + ], + mint: None, + marketing: None, + }; + let err = + instantiate(deps.as_mut(), env.clone(), info.clone(), instantiate_msg).unwrap_err(); + assert_eq!(err, ContractError::DuplicateInitialBalanceAddresses {}); + + // Works with unique addresses + let instantiate_msg = InstantiateMsg { + name: "Bash Shell".to_string(), + symbol: "BASH".to_string(), + decimals: 6, + initial_balances: vec![ + Cw20Coin { + address: addr1.clone(), + amount: amount1, + }, + Cw20Coin { + address: addr2.clone(), + amount: amount2, + }, + ], + mint: None, + marketing: None, + }; + let res = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap(); + assert_eq!(0, res.messages.len()); + assert_eq!( + query_token_info(deps.as_ref()).unwrap(), + TokenInfoResponse { + name: "Bash Shell".to_string(), + symbol: "BASH".to_string(), + decimals: 6, + total_supply: amount1 + amount2, + } + ); + assert_eq!(get_balance(deps.as_ref(), addr1), amount1); + assert_eq!(get_balance(deps.as_ref(), addr2), amount2); + } + + #[test] + fn queries_work() { + let mut deps = mock_dependencies_with_balance(&coins(2, "token")); + let addr1 = String::from("addr0001"); + let amount1 = Uint128::from(12340000u128); + + let expected = do_instantiate(deps.as_mut(), &addr1, amount1); + + // check meta query + let loaded = query_token_info(deps.as_ref()).unwrap(); + assert_eq!(expected, loaded); + + let _info = mock_info("test", &[]); + let env = mock_env(); + // check balance query (full) + let data = query( + deps.as_ref(), + env.clone(), + QueryMsg::Balance { address: addr1 }, + ) + .unwrap(); + let loaded: BalanceResponse = from_binary(&data).unwrap(); + assert_eq!(loaded.balance, amount1); + + // check balance query (empty) + let data = query( + deps.as_ref(), + env, + QueryMsg::Balance { + address: String::from("addr0002"), + }, + ) + .unwrap(); + let loaded: BalanceResponse = from_binary(&data).unwrap(); + assert_eq!(loaded.balance, Uint128::zero()); + } + + #[test] + fn transfer() { + let mut deps = mock_dependencies_with_balance(&coins(2, "token")); + let addr1 = String::from("addr0001"); + let addr2 = String::from("addr0002"); + let amount1 = Uint128::from(12340000u128); + let transfer = Uint128::from(76543u128); + let too_much = Uint128::from(12340321u128); + + do_instantiate(deps.as_mut(), &addr1, amount1); + + // Allows transferring 0 + let info = mock_info(addr1.as_ref(), &[]); + let env = mock_env(); + let msg = ExecuteMsg::Transfer { + recipient: addr2.clone(), + amount: Uint128::zero(), + }; + execute(deps.as_mut(), env, info, msg).unwrap(); + + // cannot send more than we have + let info = mock_info(addr1.as_ref(), &[]); + let env = mock_env(); + let msg = ExecuteMsg::Transfer { + recipient: addr2.clone(), + amount: too_much, + }; + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); + + // cannot send from empty account + let info = mock_info(addr2.as_ref(), &[]); + let env = mock_env(); + let msg = ExecuteMsg::Transfer { + recipient: addr1.clone(), + amount: transfer, + }; + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); + + // valid transfer + let info = mock_info(addr1.as_ref(), &[]); + let env = mock_env(); + let msg = ExecuteMsg::Transfer { + recipient: addr2.clone(), + amount: transfer, + }; + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!(res.messages.len(), 0); + + let remainder = amount1.checked_sub(transfer).unwrap(); + assert_eq!(get_balance(deps.as_ref(), addr1), remainder); + assert_eq!(get_balance(deps.as_ref(), addr2), transfer); + assert_eq!( + query_token_info(deps.as_ref()).unwrap().total_supply, + amount1 + ); + } + + #[test] + fn burn() { + let mut deps = mock_dependencies_with_balance(&coins(2, "token")); + let addr1 = String::from("addr0001"); + let amount1 = Uint128::from(12340000u128); + let burn = Uint128::from(76543u128); + let too_much = Uint128::from(12340321u128); + + do_instantiate(deps.as_mut(), &addr1, amount1); + + // Allows burning 0 + let info = mock_info(addr1.as_ref(), &[]); + let env = mock_env(); + let msg = ExecuteMsg::Burn { + amount: Uint128::zero(), + }; + execute(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!( + query_token_info(deps.as_ref()).unwrap().total_supply, + amount1 + ); + + // cannot burn more than we have + let info = mock_info(addr1.as_ref(), &[]); + let env = mock_env(); + let msg = ExecuteMsg::Burn { amount: too_much }; + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); + assert_eq!( + query_token_info(deps.as_ref()).unwrap().total_supply, + amount1 + ); + + // valid burn reduces total supply + let info = mock_info(addr1.as_ref(), &[]); + let env = mock_env(); + let msg = ExecuteMsg::Burn { amount: burn }; + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!(res.messages.len(), 0); + + let remainder = amount1.checked_sub(burn).unwrap(); + assert_eq!(get_balance(deps.as_ref(), addr1), remainder); + assert_eq!( + query_token_info(deps.as_ref()).unwrap().total_supply, + remainder + ); + } + + #[test] + fn send() { + let mut deps = mock_dependencies_with_balance(&coins(2, "token")); + let addr1 = String::from("addr0001"); + let contract = String::from("addr0002"); + let amount1 = Uint128::from(12340000u128); + let transfer = Uint128::from(76543u128); + let too_much = Uint128::from(12340321u128); + let send_msg = Binary::from(r#"{"some":123}"#.as_bytes()); + + do_instantiate(deps.as_mut(), &addr1, amount1); + + // Allows sending 0 + let info = mock_info(addr1.as_ref(), &[]); + let env = mock_env(); + let msg = ExecuteMsg::Send { + contract: contract.clone(), + amount: Uint128::zero(), + msg: send_msg.clone(), + }; + execute(deps.as_mut(), env, info, msg).unwrap(); + + // cannot send more than we have + let info = mock_info(addr1.as_ref(), &[]); + let env = mock_env(); + let msg = ExecuteMsg::Send { + contract: contract.clone(), + amount: too_much, + msg: send_msg.clone(), + }; + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); + + // valid transfer + let info = mock_info(addr1.as_ref(), &[]); + let env = mock_env(); + let msg = ExecuteMsg::Send { + contract: contract.clone(), + amount: transfer, + msg: send_msg.clone(), + }; + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!(res.messages.len(), 1); + + // ensure proper send message sent + // this is the message we want delivered to the other side + let binary_msg = Cw20ReceiveMsg { + sender: addr1.clone(), + amount: transfer, + msg: send_msg, + } + .into_binary() + .unwrap(); + // and this is how it must be wrapped for the vm to process it + assert_eq!( + res.messages[0], + SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract.clone(), + msg: binary_msg, + funds: vec![], + })) + ); + + // ensure balance is properly transferred + let remainder = amount1.checked_sub(transfer).unwrap(); + assert_eq!(get_balance(deps.as_ref(), addr1), remainder); + assert_eq!(get_balance(deps.as_ref(), contract), transfer); + assert_eq!( + query_token_info(deps.as_ref()).unwrap().total_supply, + amount1 + ); + } + + mod migration { + use super::*; + + use cosmwasm_std::Empty; + use cw20::{AllAllowancesResponse, AllSpenderAllowancesResponse, SpenderAllowanceInfo}; + use cw_multi_test::{App, Contract, ContractWrapper, Executor}; + use cw_utils::Expiration; + + fn cw20_contract() -> Box> { + let contract = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + ) + .with_migrate(crate::contract::migrate); + Box::new(contract) + } + + #[test] + fn test_migrate() { + let mut app = App::default(); + + let cw20_id = app.store_code(cw20_contract()); + let cw20_addr = app + .instantiate_contract( + cw20_id, + Addr::unchecked("sender"), + &InstantiateMsg { + name: "Token".to_string(), + symbol: "TOKEN".to_string(), + decimals: 6, + initial_balances: vec![Cw20Coin { + address: "sender".to_string(), + amount: Uint128::new(100), + }], + mint: None, + marketing: None, + }, + &[], + "TOKEN", + Some("sender".to_string()), + ) + .unwrap(); + + // no allowance to start + let allowance: AllAllowancesResponse = app + .wrap() + .query_wasm_smart( + cw20_addr.to_string(), + &QueryMsg::AllAllowances { + owner: "sender".to_string(), + start_after: None, + limit: None, + }, + ) + .unwrap(); + assert_eq!(allowance, AllAllowancesResponse::default()); + + // Set allowance + let allow1 = Uint128::new(7777); + let expires = Expiration::AtHeight(123_456); + let msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cw20_addr.to_string(), + msg: to_binary(&ExecuteMsg::IncreaseAllowance { + spender: "spender".into(), + amount: allow1, + expires: Some(expires), + }) + .unwrap(), + funds: vec![], + }); + app.execute(Addr::unchecked("sender"), msg).unwrap(); + + // Now migrate + app.execute( + Addr::unchecked("sender"), + CosmosMsg::Wasm(WasmMsg::Migrate { + contract_addr: cw20_addr.to_string(), + new_code_id: cw20_id, + msg: to_binary(&MigrateMsg {}).unwrap(), + }), + ) + .unwrap(); + + // Smoke check that the contract still works. + let balance: cw20::BalanceResponse = app + .wrap() + .query_wasm_smart( + cw20_addr.clone(), + &QueryMsg::Balance { + address: "sender".to_string(), + }, + ) + .unwrap(); + + assert_eq!(balance.balance, Uint128::new(100)); + + // Confirm that the allowance per spender is there + let allowance: AllSpenderAllowancesResponse = app + .wrap() + .query_wasm_smart( + cw20_addr, + &QueryMsg::AllSpenderAllowances { + spender: "spender".to_string(), + start_after: None, + limit: None, + }, + ) + .unwrap(); + assert_eq!( + allowance.allowances, + &[SpenderAllowanceInfo { + owner: "sender".to_string(), + allowance: allow1, + expires + }] + ); + } + } + + mod marketing { + use super::*; + + #[test] + fn update_unauthorised() { + let mut deps = mock_dependencies(); + let instantiate_msg = InstantiateMsg { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + initial_balances: vec![], + mint: None, + marketing: Some(InstantiateMarketingInfo { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some("marketing".to_owned()), + logo: Some(Logo::Url("url".to_owned())), + }), + }; + + let info = mock_info("creator", &[]); + + instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); + + let err = execute( + deps.as_mut(), + mock_env(), + info, + ExecuteMsg::UpdateMarketing { + project: Some("New project".to_owned()), + description: Some("Better description".to_owned()), + marketing: Some("creator".to_owned()), + }, + ) + .unwrap_err(); + + assert_eq!(err, ContractError::Unauthorized {}); + + // Ensure marketing didn't change + assert_eq!( + query_marketing_info(deps.as_ref()).unwrap(), + MarketingInfoResponse { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some(Addr::unchecked("marketing")), + logo: Some(LogoInfo::Url("url".to_owned())), + } + ); + + let err = query_download_logo(deps.as_ref()).unwrap_err(); + assert!( + matches!(err, StdError::NotFound { .. }), + "Expected StdError::NotFound, received {err}", + ); + } + + #[test] + fn update_project() { + let mut deps = mock_dependencies(); + let instantiate_msg = InstantiateMsg { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + initial_balances: vec![], + mint: None, + marketing: Some(InstantiateMarketingInfo { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some("creator".to_owned()), + logo: Some(Logo::Url("url".to_owned())), + }), + }; + + let info = mock_info("creator", &[]); + + instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); + + let res = execute( + deps.as_mut(), + mock_env(), + info, + ExecuteMsg::UpdateMarketing { + project: Some("New project".to_owned()), + description: None, + marketing: None, + }, + ) + .unwrap(); + + assert_eq!(res.messages, vec![]); + + assert_eq!( + query_marketing_info(deps.as_ref()).unwrap(), + MarketingInfoResponse { + project: Some("New project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some(Addr::unchecked("creator")), + logo: Some(LogoInfo::Url("url".to_owned())), + } + ); + + let err = query_download_logo(deps.as_ref()).unwrap_err(); + assert!( + matches!(err, StdError::NotFound { .. }), + "Expected StdError::NotFound, received {err}", + ); + } + + #[test] + fn clear_project() { + let mut deps = mock_dependencies(); + let instantiate_msg = InstantiateMsg { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + initial_balances: vec![], + mint: None, + marketing: Some(InstantiateMarketingInfo { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some("creator".to_owned()), + logo: Some(Logo::Url("url".to_owned())), + }), + }; + + let info = mock_info("creator", &[]); + + instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); + + let res = execute( + deps.as_mut(), + mock_env(), + info, + ExecuteMsg::UpdateMarketing { + project: Some("".to_owned()), + description: None, + marketing: None, + }, + ) + .unwrap(); + + assert_eq!(res.messages, vec![]); + + assert_eq!( + query_marketing_info(deps.as_ref()).unwrap(), + MarketingInfoResponse { + project: None, + description: Some("Description".to_owned()), + marketing: Some(Addr::unchecked("creator")), + logo: Some(LogoInfo::Url("url".to_owned())), + } + ); + + let err = query_download_logo(deps.as_ref()).unwrap_err(); + assert!( + matches!(err, StdError::NotFound { .. }), + "Expected StdError::NotFound, received {err}", + ); + } + + #[test] + fn update_description() { + let mut deps = mock_dependencies(); + let instantiate_msg = InstantiateMsg { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + initial_balances: vec![], + mint: None, + marketing: Some(InstantiateMarketingInfo { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some("creator".to_owned()), + logo: Some(Logo::Url("url".to_owned())), + }), + }; + + let info = mock_info("creator", &[]); + + instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); + + let res = execute( + deps.as_mut(), + mock_env(), + info, + ExecuteMsg::UpdateMarketing { + project: None, + description: Some("Better description".to_owned()), + marketing: None, + }, + ) + .unwrap(); + + assert_eq!(res.messages, vec![]); + + assert_eq!( + query_marketing_info(deps.as_ref()).unwrap(), + MarketingInfoResponse { + project: Some("Project".to_owned()), + description: Some("Better description".to_owned()), + marketing: Some(Addr::unchecked("creator")), + logo: Some(LogoInfo::Url("url".to_owned())), + } + ); + + let err = query_download_logo(deps.as_ref()).unwrap_err(); + assert!( + matches!(err, StdError::NotFound { .. }), + "Expected StdError::NotFound, received {err}", + ); + } + + #[test] + fn clear_description() { + let mut deps = mock_dependencies(); + let instantiate_msg = InstantiateMsg { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + initial_balances: vec![], + mint: None, + marketing: Some(InstantiateMarketingInfo { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some("creator".to_owned()), + logo: Some(Logo::Url("url".to_owned())), + }), + }; + + let info = mock_info("creator", &[]); + + instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); + + let res = execute( + deps.as_mut(), + mock_env(), + info, + ExecuteMsg::UpdateMarketing { + project: None, + description: Some("".to_owned()), + marketing: None, + }, + ) + .unwrap(); + + assert_eq!(res.messages, vec![]); + + assert_eq!( + query_marketing_info(deps.as_ref()).unwrap(), + MarketingInfoResponse { + project: Some("Project".to_owned()), + description: None, + marketing: Some(Addr::unchecked("creator")), + logo: Some(LogoInfo::Url("url".to_owned())), + } + ); + + let err = query_download_logo(deps.as_ref()).unwrap_err(); + assert!( + matches!(err, StdError::NotFound { .. }), + "Expected StdError::NotFound, received {err}", + ); + } + + #[test] + fn update_marketing() { + let mut deps = mock_dependencies(); + let instantiate_msg = InstantiateMsg { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + initial_balances: vec![], + mint: None, + marketing: Some(InstantiateMarketingInfo { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some("creator".to_owned()), + logo: Some(Logo::Url("url".to_owned())), + }), + }; + + let info = mock_info("creator", &[]); + + instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); + + let res = execute( + deps.as_mut(), + mock_env(), + info, + ExecuteMsg::UpdateMarketing { + project: None, + description: None, + marketing: Some("marketing".to_owned()), + }, + ) + .unwrap(); + + assert_eq!(res.messages, vec![]); + + assert_eq!( + query_marketing_info(deps.as_ref()).unwrap(), + MarketingInfoResponse { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some(Addr::unchecked("marketing")), + logo: Some(LogoInfo::Url("url".to_owned())), + } + ); + + let err = query_download_logo(deps.as_ref()).unwrap_err(); + assert!( + matches!(err, StdError::NotFound { .. }), + "Expected StdError::NotFound, received {err}", + ); + } + + #[test] + fn update_marketing_invalid() { + let mut deps = mock_dependencies(); + let instantiate_msg = InstantiateMsg { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + initial_balances: vec![], + mint: None, + marketing: Some(InstantiateMarketingInfo { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some("creator".to_owned()), + logo: Some(Logo::Url("url".to_owned())), + }), + }; + + let info = mock_info("creator", &[]); + + instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); + + let err = execute( + deps.as_mut(), + mock_env(), + info, + ExecuteMsg::UpdateMarketing { + project: None, + description: None, + marketing: Some("m".to_owned()), + }, + ) + .unwrap_err(); + + assert!( + matches!(err, ContractError::Std(_)), + "Expected Std error, received: {err}", + ); + + assert_eq!( + query_marketing_info(deps.as_ref()).unwrap(), + MarketingInfoResponse { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some(Addr::unchecked("creator")), + logo: Some(LogoInfo::Url("url".to_owned())), + } + ); + + let err = query_download_logo(deps.as_ref()).unwrap_err(); + assert!( + matches!(err, StdError::NotFound { .. }), + "Expected StdError::NotFound, received {err}", + ); + } + + #[test] + fn clear_marketing() { + let mut deps = mock_dependencies(); + let instantiate_msg = InstantiateMsg { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + initial_balances: vec![], + mint: None, + marketing: Some(InstantiateMarketingInfo { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some("creator".to_owned()), + logo: Some(Logo::Url("url".to_owned())), + }), + }; + + let info = mock_info("creator", &[]); + + instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); + + let res = execute( + deps.as_mut(), + mock_env(), + info, + ExecuteMsg::UpdateMarketing { + project: None, + description: None, + marketing: Some("".to_owned()), + }, + ) + .unwrap(); + + assert_eq!(res.messages, vec![]); + + assert_eq!( + query_marketing_info(deps.as_ref()).unwrap(), + MarketingInfoResponse { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: None, + logo: Some(LogoInfo::Url("url".to_owned())), + } + ); + + let err = query_download_logo(deps.as_ref()).unwrap_err(); + assert!( + matches!(err, StdError::NotFound { .. }), + "Expected StdError::NotFound, received {err}", + ); + } + + #[test] + fn update_logo_url() { + let mut deps = mock_dependencies(); + let instantiate_msg = InstantiateMsg { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + initial_balances: vec![], + mint: None, + marketing: Some(InstantiateMarketingInfo { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some("creator".to_owned()), + logo: Some(Logo::Url("url".to_owned())), + }), + }; + + let info = mock_info("creator", &[]); + + instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); + + let res = execute( + deps.as_mut(), + mock_env(), + info, + ExecuteMsg::UploadLogo(Logo::Url("new_url".to_owned())), + ) + .unwrap(); + + assert_eq!(res.messages, vec![]); + + assert_eq!( + query_marketing_info(deps.as_ref()).unwrap(), + MarketingInfoResponse { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some(Addr::unchecked("creator")), + logo: Some(LogoInfo::Url("new_url".to_owned())), + } + ); + + let err = query_download_logo(deps.as_ref()).unwrap_err(); + assert!( + matches!(err, StdError::NotFound { .. }), + "Expected StdError::NotFound, received {err}", + ); + } + + #[test] + fn update_logo_png() { + let mut deps = mock_dependencies(); + let instantiate_msg = InstantiateMsg { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + initial_balances: vec![], + mint: None, + marketing: Some(InstantiateMarketingInfo { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some("creator".to_owned()), + logo: Some(Logo::Url("url".to_owned())), + }), + }; + + let info = mock_info("creator", &[]); + + instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); + + let res = execute( + deps.as_mut(), + mock_env(), + info, + ExecuteMsg::UploadLogo(Logo::Embedded(EmbeddedLogo::Png(PNG_HEADER.into()))), + ) + .unwrap(); + + assert_eq!(res.messages, vec![]); + + assert_eq!( + query_marketing_info(deps.as_ref()).unwrap(), + MarketingInfoResponse { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some(Addr::unchecked("creator")), + logo: Some(LogoInfo::Embedded), + } + ); + + assert_eq!( + query_download_logo(deps.as_ref()).unwrap(), + DownloadLogoResponse { + mime_type: "image/png".to_owned(), + data: PNG_HEADER.into(), + } + ); + } + + #[test] + fn update_logo_svg() { + let mut deps = mock_dependencies(); + let instantiate_msg = InstantiateMsg { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + initial_balances: vec![], + mint: None, + marketing: Some(InstantiateMarketingInfo { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some("creator".to_owned()), + logo: Some(Logo::Url("url".to_owned())), + }), + }; + + let info = mock_info("creator", &[]); + + instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); + + let img = "".as_bytes(); + let res = execute( + deps.as_mut(), + mock_env(), + info, + ExecuteMsg::UploadLogo(Logo::Embedded(EmbeddedLogo::Svg(img.into()))), + ) + .unwrap(); + + assert_eq!(res.messages, vec![]); + + assert_eq!( + query_marketing_info(deps.as_ref()).unwrap(), + MarketingInfoResponse { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some(Addr::unchecked("creator")), + logo: Some(LogoInfo::Embedded), + } + ); + + assert_eq!( + query_download_logo(deps.as_ref()).unwrap(), + DownloadLogoResponse { + mime_type: "image/svg+xml".to_owned(), + data: img.into(), + } + ); + } + + #[test] + fn update_logo_png_oversized() { + let mut deps = mock_dependencies(); + let instantiate_msg = InstantiateMsg { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + initial_balances: vec![], + mint: None, + marketing: Some(InstantiateMarketingInfo { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some("creator".to_owned()), + logo: Some(Logo::Url("url".to_owned())), + }), + }; + + let info = mock_info("creator", &[]); + + instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); + + let img = [&PNG_HEADER[..], &[1; 6000][..]].concat(); + let err = execute( + deps.as_mut(), + mock_env(), + info, + ExecuteMsg::UploadLogo(Logo::Embedded(EmbeddedLogo::Png(img.into()))), + ) + .unwrap_err(); + + assert_eq!(err, ContractError::LogoTooBig {}); + + assert_eq!( + query_marketing_info(deps.as_ref()).unwrap(), + MarketingInfoResponse { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some(Addr::unchecked("creator")), + logo: Some(LogoInfo::Url("url".to_owned())), + } + ); + + let err = query_download_logo(deps.as_ref()).unwrap_err(); + assert!( + matches!(err, StdError::NotFound { .. }), + "Expected StdError::NotFound, received {err}", + ); + } + + #[test] + fn update_logo_svg_oversized() { + let mut deps = mock_dependencies(); + let instantiate_msg = InstantiateMsg { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + initial_balances: vec![], + mint: None, + marketing: Some(InstantiateMarketingInfo { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some("creator".to_owned()), + logo: Some(Logo::Url("url".to_owned())), + }), + }; + + let info = mock_info("creator", &[]); + + instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); + + let img = [ + "", + std::str::from_utf8(&[b'x'; 6000]).unwrap(), + "", + ] + .concat() + .into_bytes(); + + let err = execute( + deps.as_mut(), + mock_env(), + info, + ExecuteMsg::UploadLogo(Logo::Embedded(EmbeddedLogo::Svg(img.into()))), + ) + .unwrap_err(); + + assert_eq!(err, ContractError::LogoTooBig {}); + + assert_eq!( + query_marketing_info(deps.as_ref()).unwrap(), + MarketingInfoResponse { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some(Addr::unchecked("creator")), + logo: Some(LogoInfo::Url("url".to_owned())), + } + ); + + let err = query_download_logo(deps.as_ref()).unwrap_err(); + assert!( + matches!(err, StdError::NotFound { .. }), + "Expected StdError::NotFound, received {err}", + ); + } + + #[test] + fn update_logo_png_invalid() { + let mut deps = mock_dependencies(); + let instantiate_msg = InstantiateMsg { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + initial_balances: vec![], + mint: None, + marketing: Some(InstantiateMarketingInfo { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some("creator".to_owned()), + logo: Some(Logo::Url("url".to_owned())), + }), + }; + + let info = mock_info("creator", &[]); + + instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); + + let img = &[1]; + let err = execute( + deps.as_mut(), + mock_env(), + info, + ExecuteMsg::UploadLogo(Logo::Embedded(EmbeddedLogo::Png(img.into()))), + ) + .unwrap_err(); + + assert_eq!(err, ContractError::InvalidPngHeader {}); + + assert_eq!( + query_marketing_info(deps.as_ref()).unwrap(), + MarketingInfoResponse { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some(Addr::unchecked("creator")), + logo: Some(LogoInfo::Url("url".to_owned())), + } + ); + + let err = query_download_logo(deps.as_ref()).unwrap_err(); + assert!( + matches!(err, StdError::NotFound { .. }), + "Expected StdError::NotFound, received {err}", + ); + } + + #[test] + fn update_logo_svg_invalid() { + let mut deps = mock_dependencies(); + let instantiate_msg = InstantiateMsg { + name: "Cash Token".to_string(), + symbol: "CASH".to_string(), + decimals: 9, + initial_balances: vec![], + mint: None, + marketing: Some(InstantiateMarketingInfo { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some("creator".to_owned()), + logo: Some(Logo::Url("url".to_owned())), + }), + }; + + let info = mock_info("creator", &[]); + + instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap(); + + let img = &[1]; + + let err = execute( + deps.as_mut(), + mock_env(), + info, + ExecuteMsg::UploadLogo(Logo::Embedded(EmbeddedLogo::Svg(img.into()))), + ) + .unwrap_err(); + + assert_eq!(err, ContractError::InvalidXmlPreamble {}); + + assert_eq!( + query_marketing_info(deps.as_ref()).unwrap(), + MarketingInfoResponse { + project: Some("Project".to_owned()), + description: Some("Description".to_owned()), + marketing: Some(Addr::unchecked("creator")), + logo: Some(LogoInfo::Url("url".to_owned())), + } + ); + + let err = query_download_logo(deps.as_ref()).unwrap_err(); + assert!( + matches!(err, StdError::NotFound { .. }), + "Expected StdError::NotFound, received {err}", + ); + } + } +} diff --git a/contracts/cw20-base/src/enumerable.rs b/contracts/cw20-base/src/enumerable.rs new file mode 100644 index 000000000..f465134d2 --- /dev/null +++ b/contracts/cw20-base/src/enumerable.rs @@ -0,0 +1,319 @@ +use cosmwasm_std::{Deps, Order, StdResult}; +use cw20::{ + AllAccountsResponse, AllAllowancesResponse, AllSpenderAllowancesResponse, AllowanceInfo, + SpenderAllowanceInfo, +}; + +use crate::state::{ALLOWANCES, ALLOWANCES_SPENDER, BALANCES}; +use cw_storage_plus::Bound; + +// settings for pagination +const MAX_LIMIT: u32 = 30; +const DEFAULT_LIMIT: u32 = 10; + +pub fn query_owner_allowances( + deps: Deps, + owner: String, + start_after: Option, + limit: Option, +) -> StdResult { + 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 = 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::>()?; + Ok(AllAllowancesResponse { allowances }) +} + +pub fn query_spender_allowances( + deps: Deps, + spender: String, + start_after: Option, + limit: Option, +) -> StdResult { + let spender_addr = deps.api.addr_validate(&spender)?; + 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 = 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::>()?; + Ok(AllSpenderAllowancesResponse { allowances }) +} + +pub fn query_all_accounts( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = start_after.map(|s| Bound::ExclusiveRaw(s.into())); + + let accounts = BALANCES + .keys(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|item| item.map(Into::into)) + .collect::>()?; + + Ok(AllAccountsResponse { accounts }) +} + +#[cfg(test)] +mod tests { + use super::*; + + use cosmwasm_std::testing::{mock_dependencies_with_balance, mock_env, mock_info}; + use cosmwasm_std::{coins, from_binary, DepsMut, Uint128}; + use cw20::{Cw20Coin, Expiration, TokenInfoResponse}; + + use crate::contract::{execute, instantiate, query, query_token_info}; + use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + + // this will set up the instantiation for other tests + fn do_instantiate(mut deps: DepsMut, addr: &str, amount: Uint128) -> TokenInfoResponse { + let instantiate_msg = InstantiateMsg { + name: "Auto Gen".to_string(), + symbol: "AUTO".to_string(), + decimals: 3, + initial_balances: vec![Cw20Coin { + address: addr.into(), + amount, + }], + mint: None, + marketing: None, + }; + let info = mock_info("creator", &[]); + let env = mock_env(); + instantiate(deps.branch(), env, info, instantiate_msg).unwrap(); + query_token_info(deps.as_ref()).unwrap() + } + + #[test] + fn query_all_owner_allowances_works() { + let mut deps = mock_dependencies_with_balance(&coins(2, "token")); + + let owner = String::from("owner"); + // these are in alphabetical order same than insert order + let spender1 = String::from("earlier"); + let spender2 = String::from("later"); + + let info = mock_info(owner.as_ref(), &[]); + let env = mock_env(); + do_instantiate(deps.as_mut(), &owner, Uint128::new(12340000)); + + // no allowance to start + let allowances = query_owner_allowances(deps.as_ref(), owner.clone(), None, None).unwrap(); + assert_eq!(allowances.allowances, vec![]); + + // set allowance with height expiration + let allow1 = Uint128::new(7777); + let expires = Expiration::AtHeight(123_456); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender1.clone(), + amount: allow1, + expires: Some(expires), + }; + execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + + // set allowance with no expiration + let allow2 = Uint128::new(54321); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender2.clone(), + amount: allow2, + expires: None, + }; + execute(deps.as_mut(), env, info, msg).unwrap(); + + // query list gets 2 + let allowances = query_owner_allowances(deps.as_ref(), owner.clone(), None, None).unwrap(); + assert_eq!(allowances.allowances.len(), 2); + + // first one is spender1 (order of CanonicalAddr uncorrelated with String) + let allowances = + query_owner_allowances(deps.as_ref(), owner.clone(), None, Some(1)).unwrap(); + assert_eq!(allowances.allowances.len(), 1); + let allow = &allowances.allowances[0]; + assert_eq!(&allow.spender, &spender1); + assert_eq!(&allow.expires, &expires); + assert_eq!(&allow.allowance, &allow1); + + // next one is spender2 + let allowances = query_owner_allowances( + deps.as_ref(), + owner, + Some(allow.spender.clone()), + Some(10000), + ) + .unwrap(); + assert_eq!(allowances.allowances.len(), 1); + let allow = &allowances.allowances[0]; + assert_eq!(&allow.spender, &spender2); + assert_eq!(&allow.expires, &Expiration::Never {}); + assert_eq!(&allow.allowance, &allow2); + } + + #[test] + fn query_all_spender_allowances_works() { + let mut deps = mock_dependencies_with_balance(&coins(2, "token")); + + // these are in alphabetical order same than insert order + let owner1 = String::from("owner1"); + let owner2 = String::from("owner2"); + let spender = String::from("spender"); + + let info = mock_info(owner1.as_ref(), &[]); + let env = mock_env(); + do_instantiate(deps.as_mut(), &owner1, Uint128::new(12340000)); + + // no allowance to start + let allowances = + query_spender_allowances(deps.as_ref(), spender.clone(), None, None).unwrap(); + assert_eq!(allowances.allowances, vec![]); + + // set allowance with height expiration + let allow1 = Uint128::new(7777); + let expires = Expiration::AtHeight(123_456); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: allow1, + expires: Some(expires), + }; + execute(deps.as_mut(), env, info, msg).unwrap(); + + // set allowance with no expiration, from the other owner + let info = mock_info(owner2.as_ref(), &[]); + let env = mock_env(); + do_instantiate(deps.as_mut(), &owner2, Uint128::new(12340000)); + + let allow2 = Uint128::new(54321); + let msg = ExecuteMsg::IncreaseAllowance { + spender: spender.clone(), + amount: allow2, + expires: None, + }; + execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + + // query list gets both + let msg = QueryMsg::AllSpenderAllowances { + spender: spender.clone(), + start_after: None, + limit: None, + }; + let allowances: AllSpenderAllowancesResponse = + from_binary(&query(deps.as_ref(), env.clone(), msg).unwrap()).unwrap(); + assert_eq!(allowances.allowances.len(), 2); + + // one is owner1 (order of CanonicalAddr uncorrelated with String) + let msg = QueryMsg::AllSpenderAllowances { + spender: spender.clone(), + start_after: None, + limit: Some(1), + }; + let allowances: AllSpenderAllowancesResponse = + from_binary(&query(deps.as_ref(), env.clone(), msg).unwrap()).unwrap(); + assert_eq!(allowances.allowances.len(), 1); + let allow = &allowances.allowances[0]; + assert_eq!(&allow.owner, &owner1); + assert_eq!(&allow.expires, &expires); + assert_eq!(&allow.allowance, &allow1); + + // other one is owner2 + let msg = QueryMsg::AllSpenderAllowances { + spender, + start_after: Some(owner1), + limit: Some(10000), + }; + let allowances: AllSpenderAllowancesResponse = + from_binary(&query(deps.as_ref(), env, msg).unwrap()).unwrap(); + assert_eq!(allowances.allowances.len(), 1); + let allow = &allowances.allowances[0]; + assert_eq!(&allow.owner, &owner2); + assert_eq!(&allow.expires, &Expiration::Never {}); + assert_eq!(&allow.allowance, &allow2); + } + + #[test] + fn query_all_accounts_works() { + let mut deps = mock_dependencies_with_balance(&coins(2, "token")); + + // insert order and lexicographical order are different + let acct1 = String::from("acct01"); + let acct2 = String::from("zebra"); + let acct3 = String::from("nice"); + let acct4 = String::from("aaaardvark"); + let expected_order = [acct4.clone(), acct1.clone(), acct3.clone(), acct2.clone()]; + + do_instantiate(deps.as_mut(), &acct1, Uint128::new(12340000)); + + // put money everywhere (to create balanaces) + let info = mock_info(acct1.as_ref(), &[]); + let env = mock_env(); + execute( + deps.as_mut(), + env.clone(), + info.clone(), + ExecuteMsg::Transfer { + recipient: acct2, + amount: Uint128::new(222222), + }, + ) + .unwrap(); + execute( + deps.as_mut(), + env.clone(), + info.clone(), + ExecuteMsg::Transfer { + recipient: acct3, + amount: Uint128::new(333333), + }, + ) + .unwrap(); + execute( + deps.as_mut(), + env, + info, + ExecuteMsg::Transfer { + recipient: acct4, + amount: Uint128::new(444444), + }, + ) + .unwrap(); + + // make sure we get the proper results + let accounts = query_all_accounts(deps.as_ref(), None, None).unwrap(); + assert_eq!(accounts.accounts, expected_order); + + // let's do pagination + let accounts = query_all_accounts(deps.as_ref(), None, Some(2)).unwrap(); + assert_eq!(accounts.accounts, expected_order[0..2].to_vec()); + + let accounts = + query_all_accounts(deps.as_ref(), Some(accounts.accounts[1].clone()), Some(1)).unwrap(); + assert_eq!(accounts.accounts, expected_order[2..3].to_vec()); + + let accounts = + query_all_accounts(deps.as_ref(), Some(accounts.accounts[0].clone()), Some(777)) + .unwrap(); + assert_eq!(accounts.accounts, expected_order[3..].to_vec()); + } +} diff --git a/contracts/cw20-base/src/error.rs b/contracts/cw20-base/src/error.rs new file mode 100644 index 000000000..a0b880c97 --- /dev/null +++ b/contracts/cw20-base/src/error.rs @@ -0,0 +1,43 @@ +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 {}, + + // Unused error case. Zero is now treated like every other value. + #[deprecated(note = "Unused. All zero amount checks have been removed")] + #[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 000000000..7b601f25d --- /dev/null +++ b/contracts/cw20-base/src/lib.rs @@ -0,0 +1,24 @@ +/*! +This is a basic implementation of a cw20 contract. It implements +the [CW20 spec](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw20/README.md) and is designed to +be deployed as is, or imported into other contracts to easily build +cw20-compatible tokens with custom logic. + +Implements: + +- [x] CW20 Base +- [x] Mintable extension +- [x] Allowances extension + +For more information on this contract, please check out the +[README](https://github.com/CosmWasm/cw-plus/blob/main/contracts/cw20-base/README.md). +*/ + +pub mod allowances; +pub mod contract; +pub mod enumerable; +mod error; +pub mod msg; +pub mod state; + +pub use crate::error::ContractError; diff --git a/contracts/cw20-base/src/msg.rs b/contracts/cw20-base/src/msg.rs new file mode 100644 index 000000000..208871255 --- /dev/null +++ b/contracts/cw20-base/src/msg.rs @@ -0,0 +1,175 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{StdError, StdResult, Uint128}; +use cw20::{Cw20Coin, Logo, MinterResponse}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +pub use cw20::Cw20ExecuteMsg as ExecuteMsg; + +#[cw_serde] +pub struct InstantiateMarketingInfo { + pub project: Option, + pub description: Option, + pub marketing: Option, + pub logo: Option, +} + +#[cw_serde] +#[cfg_attr(test, derive(Default))] +pub struct InstantiateMsg { + pub name: String, + pub symbol: String, + pub decimals: u8, + pub initial_balances: Vec, + pub mint: Option, + pub marketing: Option, +} + +impl InstantiateMsg { + pub fn get_cap(&self) -> Option { + self.mint.as_ref().and_then(|v| v.cap) + } + + pub fn validate(&self) -> StdResult<()> { + // Check name, symbol, decimals + if !self.has_valid_name() { + return Err(StdError::generic_err( + "Name is not in the expected format (3-50 UTF-8 bytes)", + )); + } + if !self.has_valid_symbol() { + return Err(StdError::generic_err( + "Ticker symbol is not in expected format [a-zA-Z\\-]{3,12}", + )); + } + if self.decimals > 18 { + return Err(StdError::generic_err("Decimals must not exceed 18")); + } + Ok(()) + } + + fn has_valid_name(&self) -> bool { + let bytes = self.name.as_bytes(); + if bytes.len() < 3 || bytes.len() > 50 { + return false; + } + true + } + + fn has_valid_symbol(&self) -> bool { + let bytes = self.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 + } +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Returns the current balance of the given address, 0 if unset. + #[returns(cw20::BalanceResponse)] + Balance { address: String }, + /// Returns metadata on the contract - name, decimals, supply, etc. + #[returns(cw20::TokenInfoResponse)] + TokenInfo {}, + /// Only with "mintable" extension. + /// Returns who can mint and the hard cap on maximum tokens after minting. + #[returns(cw20::MinterResponse)] + Minter {}, + /// Only with "allowance" extension. + /// Returns how much spender can use from owner account, 0 if unset. + #[returns(cw20::AllowanceResponse)] + Allowance { owner: String, spender: String }, + /// Only with "enumerable" extension (and "allowances") + /// Returns all allowances this owner has approved. Supports pagination. + #[returns(cw20::AllAllowancesResponse)] + AllAllowances { + owner: String, + start_after: Option, + limit: Option, + }, + /// Only with "enumerable" extension (and "allowances") + /// Returns all allowances this spender has been granted. Supports pagination. + #[returns(cw20::AllSpenderAllowancesResponse)] + AllSpenderAllowances { + spender: String, + start_after: Option, + limit: Option, + }, + /// Only with "enumerable" extension + /// Returns all accounts that have balances. Supports pagination. + #[returns(cw20::AllAccountsResponse)] + AllAccounts { + start_after: Option, + limit: Option, + }, + /// Only with "marketing" extension + /// Returns more metadata on the contract to display in the client: + /// - description, logo, project url, etc. + #[returns(cw20::MarketingInfoResponse)] + MarketingInfo {}, + /// Only with "marketing" extension + /// Downloads the embedded logo data (if stored on chain). Errors if no logo data is stored for this + /// contract. + #[returns(cw20::DownloadLogoResponse)] + DownloadLogo {}, +} + +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct MigrateMsg {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn validate_instantiatemsg_name() { + // Too short + let mut msg = InstantiateMsg { + name: str::repeat("a", 2), + ..InstantiateMsg::default() + }; + assert!(!msg.has_valid_name()); + + // In the correct length range + msg.name = str::repeat("a", 3); + assert!(msg.has_valid_name()); + + // Too long + msg.name = str::repeat("a", 51); + assert!(!msg.has_valid_name()); + } + + #[test] + fn validate_instantiatemsg_symbol() { + // Too short + let mut msg = InstantiateMsg { + symbol: str::repeat("a", 2), + ..InstantiateMsg::default() + }; + assert!(!msg.has_valid_symbol()); + + // In the correct length range + msg.symbol = str::repeat("a", 3); + assert!(msg.has_valid_symbol()); + + // Too long + msg.symbol = str::repeat("a", 13); + assert!(!msg.has_valid_symbol()); + + // Has illegal char + let illegal_chars = [[64u8], [91u8], [123u8]]; + illegal_chars.iter().for_each(|c| { + let c = std::str::from_utf8(c).unwrap(); + msg.symbol = str::repeat(c, 3); + assert!(!msg.has_valid_symbol()); + }); + } +} diff --git a/contracts/cw20-base/src/state.rs b/contracts/cw20-base/src/state.rs new file mode 100644 index 000000000..dc02c6f19 --- /dev/null +++ b/contracts/cw20-base/src/state.rs @@ -0,0 +1,36 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Uint128}; +use cw_storage_plus::{Item, Map}; + +use cw20::{AllowanceResponse, Logo, MarketingInfoResponse}; + +#[cw_serde] +pub struct TokenInfo { + pub name: String, + pub symbol: String, + pub decimals: u8, + pub total_supply: Uint128, + pub mint: Option, +} + +#[cw_serde] +pub struct MinterData { + pub minter: Addr, + /// cap is how many more tokens can be issued by the minter + pub cap: Option, +} + +impl TokenInfo { + pub fn get_cap(&self) -> Option { + self.mint.as_ref().and_then(|v| v.cap) + } +} + +pub const TOKEN_INFO: Item = Item::new("token_info"); +pub const MARKETING_INFO: Item = Item::new("marketing_info"); +pub const LOGO: Item = Item::new("logo"); +pub const BALANCES: Map<&Addr, Uint128> = Map::new("balance"); +pub const ALLOWANCES: Map<(&Addr, &Addr), AllowanceResponse> = Map::new("allowance"); +// TODO: After https://github.com/CosmWasm/cw-plus/issues/670 is implemented, replace this with a `MultiIndex` over `ALLOWANCES` +pub const ALLOWANCES_SPENDER: Map<(&Addr, &Addr), AllowanceResponse> = + Map::new("allowance_spender");