├── .circleci └── config.yml ├── .editorconfig ├── .gitignore ├── .prettierrc.json ├── CHANGELOG.md ├── CONTRACTS.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── MIGRATING.md ├── NOTICE ├── PATTERNS.md ├── README.md ├── SECURITY.md ├── codecov.yml ├── contracts ├── cw1-subkeys │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── bin │ │ └── schema.rs │ │ ├── contract.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── msg.rs │ │ └── state.rs ├── cw1-whitelist │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── bin │ │ └── schema.rs │ │ ├── contract.rs │ │ ├── error.rs │ │ ├── integration_tests.rs │ │ ├── lib.rs │ │ ├── msg.rs │ │ └── state.rs ├── cw20-base │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── allowances.rs │ │ ├── bin │ │ └── schema.rs │ │ ├── contract.rs │ │ ├── enumerable.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── msg.rs │ │ └── state.rs ├── cw20-ics20 │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── amount.rs │ │ ├── bin │ │ └── schema.rs │ │ ├── contract.rs │ │ ├── error.rs │ │ ├── ibc.rs │ │ ├── lib.rs │ │ ├── migrations.rs │ │ ├── msg.rs │ │ ├── state.rs │ │ └── test_helpers.rs ├── cw3-fixed-multisig │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── bin │ │ └── schema.rs │ │ ├── contract.rs │ │ ├── error.rs │ │ ├── integration_tests.rs │ │ ├── lib.rs │ │ ├── msg.rs │ │ └── state.rs ├── cw3-flex-multisig │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── bin │ │ └── schema.rs │ │ ├── contract.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── msg.rs │ │ └── state.rs ├── cw4-group │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── bin │ │ └── schema.rs │ │ ├── contract.rs │ │ ├── error.rs │ │ ├── helpers.rs │ │ ├── lib.rs │ │ ├── msg.rs │ │ ├── state.rs │ │ └── tests.rs └── cw4-stake │ ├── .cargo │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ └── src │ ├── bin │ └── schema.rs │ ├── contract.rs │ ├── error.rs │ ├── lib.rs │ ├── msg.rs │ └── state.rs ├── packages ├── cw1 │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── bin │ │ └── schema.rs │ │ ├── helpers.rs │ │ ├── lib.rs │ │ ├── msg.rs │ │ └── query.rs ├── cw20 │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── balance.rs │ │ ├── bin │ │ └── schema.rs │ │ ├── coin.rs │ │ ├── denom.rs │ │ ├── helpers.rs │ │ ├── lib.rs │ │ ├── logo.rs │ │ ├── msg.rs │ │ ├── query.rs │ │ └── receiver.rs ├── cw3 │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── bin │ │ └── schema.rs │ │ ├── deposit.rs │ │ ├── helpers.rs │ │ ├── lib.rs │ │ ├── msg.rs │ │ ├── proposal.rs │ │ └── query.rs ├── cw4 │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── bin │ │ └── schema.rs │ │ ├── helpers.rs │ │ ├── hook.rs │ │ ├── lib.rs │ │ ├── msg.rs │ │ └── query.rs └── easy-addr │ ├── Cargo.toml │ └── src │ └── lib.rs └── scripts ├── optimizer.sh └── update_changelog.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | max_line_length = 120 11 | 12 | [*.rs] 13 | indent_size = 4 14 | max_line_length = 100 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Text file backups 5 | **/*.rs.bk 6 | 7 | # Build results 8 | target/ 9 | 10 | # IDEs 11 | .vscode/ 12 | .idea/ 13 | *.iml 14 | 15 | # Auto-gen 16 | .cargo-ok 17 | 18 | # Build artifacts 19 | *.wasm 20 | hash.txt 21 | contracts.txt 22 | artifacts/ 23 | 24 | # code coverage 25 | tarpaulin-report.* 26 | 27 | packages/*/schema 28 | contracts/*/schema 29 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": "*.md", 5 | "options": { 6 | "proseWrap": "always" 7 | } 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /CONTRACTS.md: -------------------------------------------------------------------------------- 1 | # Contracts 2 | 3 | Here are a number of useful contracts that either implement or consume 4 | the interfaces defined in `packages/cw*`. 5 | 6 | ## Creating a new contract 7 | 8 | Use [`cosmwasm-template`](https://github.com/CosmWasm/cosmwasm-template) as a 9 | basis, in particular the `cw-plus` branch. 10 | 11 | ```bash 12 | cd contracts 13 | cargo generate --git https://github.com/CosmWasm/cosmwasm-template.git --branch cw-plus --name PROJECT_NAME 14 | cd PROJECT_NAME 15 | rm -rf .git 16 | rm .gitignore 17 | rm .cargo-ok 18 | git add . 19 | ``` 20 | 21 | Now, integrate it into the CI and build system 22 | 23 | 1. Edit `.circleci/config.yml`, copy an existing contracts job and replace the name. 24 | Then add your new job to the jobs list on top. (eg. copy `contracts_cw1_whitelist` 25 | to `contracts_cw721_base` and then replace the 3 instances of `cw1-whitelist` in 26 | that job description with `cw721-base`. And don't forget to add `contracts_cw721_base` 27 | to `workflows.test.jobs`) 28 | 29 | 1. Add to the `ALL_CONTRACTS` variable in `scripts/publish.sh` 30 | 31 | 1. Set the `version` variable in `Cargo.toml` to the same version as `packages/cw20`. 32 | For example, "0.5.0" rather than the default "0.1.0" 33 | 34 | 1. Edit the root `Cargo.toml` file and add a `profile.release.package.CONTRACT_NAME` 35 | section, just like `profile.release.package.cw1-subkeys`, but with your 36 | package name. 37 | 38 | 1. Run `cargo build && cargo test` in the new contract dir 39 | 40 | 1. Commit all changes and push the branch. Open a PR and ensure the CI runs this. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | We're happily accepting bugfixes, style improvements, good refactors and enhancements to documentation. 4 | 5 | For the specs, their APIs are considered final and immutable. We do not accept API-breaking changes. 6 | 7 | It is possible to propose a new protocol instead. In that case, please open an issue for discussion first, including 8 | some motivating use cases. 9 | 10 | The contracts are not expected to be packed with features. They're expected to be minimal, reference implementations of 11 | the specifications. We do not therefore accept enhancements. 12 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["packages/*", "contracts/*"] 3 | 4 | # Resolver has to be set explicitely in workspaces, see https://github.com/rust-lang/cargo/issues/9956 5 | resolver = "2" 6 | 7 | [workspace.package] 8 | version = "2.0.0" 9 | 10 | [workspace.dependencies] 11 | cosmwasm-schema = "2.0.2" 12 | cosmwasm-std = "2.0.2" 13 | cw2 = "2.0.0" 14 | cw-controllers = "2.0.0" 15 | cw-multi-test = "2.0.0" 16 | cw-storage-plus = "2.0.0" 17 | cw-utils = "2.0.0" 18 | schemars = "0.8.15" 19 | semver = "1" 20 | serde = { version = "1.0.188", default-features = false, features = ["derive"] } 21 | thiserror = "1.0.4" 22 | 23 | cw1 = { path = "packages/cw1", version = "2.0.0-rc.0" } 24 | cw1-whitelist = { path = "contracts/cw1-whitelist", version = "2.0.0-rc.0", features = [ 25 | "library", 26 | ] } 27 | cw20 = { path = "packages/cw20", version = "2.0.0-rc.0" } 28 | cw20-base = { path = "contracts/cw20-base", version = "2.0.0-rc.0", features = ["library"] } 29 | cw3 = { path = "packages/cw3", version = "2.0.0-rc.0" } 30 | cw3-fixed-multisig = { path = "contracts/cw3-fixed-multisig", version = "2.0.0-rc.0", features = [ 31 | "library", 32 | ] } 33 | cw4 = { path = "packages/cw4", version = "2.0.0-rc.0" } 34 | cw4-group = { path = "contracts/cw4-group", version = "2.0.0-rc.0" } 35 | easy-addr = { path = "packages/easy-addr" } 36 | 37 | [profile.release.package.cw1-subkeys] 38 | codegen-units = 1 39 | incremental = false 40 | 41 | [profile.release.package.cw1-whitelist] 42 | codegen-units = 1 43 | incremental = false 44 | 45 | [profile.release.package.cw3-fixed-multisig] 46 | codegen-units = 1 47 | incremental = false 48 | 49 | [profile.release.package.cw3-flex-multisig] 50 | codegen-units = 1 51 | incremental = false 52 | 53 | [profile.release.package.cw4-group] 54 | codegen-units = 1 55 | incremental = false 56 | 57 | [profile.release.package.cw4-stake] 58 | codegen-units = 1 59 | incremental = false 60 | 61 | [profile.release.package.cw20-base] 62 | codegen-units = 1 63 | incremental = false 64 | 65 | [profile.release.package.cw20-ics20] 66 | codegen-units = 1 67 | incremental = false 68 | 69 | [profile.release] 70 | rpath = false 71 | lto = true 72 | overflow-checks = true 73 | opt-level = 3 74 | debug = false 75 | debug-assertions = false 76 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | CosmWasm-Plus: A collection of production-quality CosmWasm contracts 2 | 3 | Copyright 2021,2022 Confio GmbH 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | This repository is maintained by Confio as part of the CosmWasm stack. 4 | Please see https://github.com/CosmWasm/advisories/blob/main/SECURITY.md 5 | for our security policy. 6 | 7 | ## Supported Versions 8 | 9 | cw-plus is still pre v1.0. A best effort has been made that the contracts here are secure, and we have moved the more 10 | experimental contracts into community repositories like [cw-nfts](https://github.com/CosmWasm/cw-nfts) and 11 | [cw-tokens](https://github.com/CosmWasm/cw-tokens). That said, we have not done an audit on them (formal or informal) 12 | and you can use them at your own risk. We highly suggest doing your own audit on any contract you plan to deploy 13 | with significant token value, and please inform us if it detects any issues so we can upstream them. 14 | 15 | Until v1.0 APIs are subject to change. The contracts APIs are pretty much stable, most work is currently 16 | in `storage-plus` and `multi-test`. 17 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | threshold: 0.05% 8 | patch: 9 | default: 10 | threshold: 0.05% 11 | 12 | ignore: 13 | -------------------------------------------------------------------------------- /contracts/cw1-subkeys/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --lib --target wasm32-unknown-unknown" 3 | wasm-debug = "build --lib --target wasm32-unknown-unknown" 4 | unit-test = "test --lib" 5 | integration-test = "test --test integration" 6 | schema = "run --bin schema" 7 | -------------------------------------------------------------------------------- /contracts/cw1-subkeys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw1-subkeys" 3 | version.workspace = true 4 | authors = ["Ethan Frey "] 5 | edition = "2021" 6 | description = "Implement subkeys for authorizing native tokens as a cw1 proxy contract" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cw-plus" 9 | homepage = "https://cosmwasm.com" 10 | documentation = "https://docs.cosmwasm.com" 11 | 12 | [lib] 13 | crate-type = ["cdylib", "rlib"] 14 | 15 | [features] 16 | # use library feature to disable all instantiate/execute/query exports 17 | library = [] 18 | test-utils = [] 19 | 20 | [dependencies] 21 | cosmwasm-schema = { workspace = true } 22 | cw-utils = { workspace = true } 23 | cw1 = { workspace = true } 24 | cw2 = { workspace = true } 25 | cw1-whitelist = { workspace = true } 26 | cosmwasm-std = { workspace = true, features = ["staking"] } 27 | cw-storage-plus = { workspace = true } 28 | schemars = { workspace = true } 29 | serde = { workspace = true } 30 | thiserror = { workspace = true } 31 | semver = { workspace = true } 32 | 33 | [dev-dependencies] 34 | cw1-whitelist = { workspace = true, features = [ 35 | "library", 36 | "test-utils", 37 | ] } 38 | easy-addr = { workspace = true } 39 | -------------------------------------------------------------------------------- /contracts/cw1-subkeys/README.md: -------------------------------------------------------------------------------- 1 | # CW1 Subkeys 2 | 3 | This builds on `cw1-whitelist` to provide the first non-trivial solution. 4 | It still works like `cw1-whitelist` with a set of admins (typically 1) 5 | which have full control of the account. However, you can then grant 6 | a number of accounts allowances to send native tokens from this account. 7 | 8 | This was proposed in Summer 2019 for the Cosmos Hub and resembles the 9 | functionality of ERC20 (allowances and transfer from). 10 | 11 | ## Details 12 | 13 | Basically, any admin can add an allowance for a `(spender, denom)` pair 14 | (similar to cw20 `IncreaseAllowance` / `DecreaseAllowance`). Any non-admin 15 | account can try to execute a `CosmosMsg::Bank(BankMsg::Send{})` from this 16 | contract and if they have the required allowances, their allowance will be 17 | reduced and the send message relayed. If they don't have sufficient authorization, 18 | or if they try to proxy any other message type, then the attempt will be rejected. 19 | Admin can give permissions to subkeys to relay specific types of messages 20 | (covers _Delegate, Undelegate, Redelegate, Withdraw_ for now). Subkeys have no permission 21 | on creation, it can be setup with `SetupPermission` message. 22 | 23 | ### Messages 24 | 25 | This adds 2 messages beyond the `cw1` spec: 26 | 27 | ```rust 28 | enum ExecuteMsg { 29 | IncreaseAllowance { 30 | spender: HumanAddr, 31 | denom: String, 32 | amount: Uint128, 33 | expires: Option, 34 | }, 35 | DecreaseAllowance { 36 | spender: HumanAddr, 37 | denom: String, 38 | amount: Uint128, 39 | expires: Option, 40 | }, 41 | SetupPermissions { 42 | spender: HumanAddr, 43 | permissions: Permissions, 44 | } 45 | } 46 | ``` 47 | 48 | ### Queries 49 | 50 | It also adds one more query type: 51 | 52 | ```rust 53 | enum QueryMsg { 54 | Allowance { 55 | spender: HumanAddr, 56 | }, 57 | AllAllowances { 58 | start_after: Option, 59 | limit: Option, 60 | }, 61 | } 62 | 63 | pub struct AllowanceInfo { 64 | pub spender: HumanAddr, 65 | pub balance: Balance, 66 | pub expires: Expiration, 67 | pub permissions: Permissions, 68 | } 69 | 70 | pub struct AllAllowancesResponse { 71 | pub allowances: Vec, 72 | } 73 | ``` 74 | 75 | ## Running this contract 76 | 77 | You will need Rust 1.44.1+ with `wasm32-unknown-unknown` target installed. 78 | 79 | You can run unit tests on this via: 80 | 81 | `cargo test` 82 | 83 | Once you are happy with the content, you can compile it to wasm via: 84 | 85 | ``` 86 | RUSTFLAGS='-C link-arg=-s' cargo wasm 87 | cp ../../target/wasm32-unknown-unknown/release/cw1_subkeys.wasm . 88 | ls -l cw1_subkeys.wasm 89 | sha256sum cw1_subkeys.wasm 90 | ``` 91 | 92 | Or for a production-ready (optimized) build, run a build command in the 93 | the repository root: https://github.com/CosmWasm/cw-plus#compiling. 94 | -------------------------------------------------------------------------------- /contracts/cw1-subkeys/src/bin/schema.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::write_api; 2 | 3 | use cw1_subkeys::msg::{ExecuteMsg, QueryMsg}; 4 | 5 | use cw1_whitelist::msg::InstantiateMsg; 6 | 7 | fn main() { 8 | write_api! { 9 | instantiate: InstantiateMsg, 10 | execute: ExecuteMsg, 11 | query: QueryMsg, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /contracts/cw1-subkeys/src/error.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::StdError; 2 | use cw_utils::Expiration; 3 | use thiserror::Error; 4 | 5 | #[derive(Error, Debug, PartialEq)] 6 | pub enum ContractError { 7 | #[error("{0}")] 8 | Std(#[from] StdError), 9 | 10 | #[error("Unauthorized")] 11 | Unauthorized {}, 12 | 13 | #[error("Cannot set to own account")] 14 | CannotSetOwnAccount {}, 15 | 16 | #[error("No permissions for this account")] 17 | NotAllowed {}, 18 | 19 | #[error("No allowance for this account")] 20 | NoAllowance {}, 21 | 22 | #[error("Message type rejected")] 23 | MessageTypeRejected {}, 24 | 25 | #[error("Delegate is not allowed")] 26 | DelegatePerm {}, 27 | 28 | #[error("Re-delegate is not allowed")] 29 | ReDelegatePerm {}, 30 | 31 | #[error("Un-delegate is not allowed")] 32 | UnDelegatePerm {}, 33 | 34 | #[error("Withdraw is not allowed")] 35 | WithdrawPerm {}, 36 | 37 | #[error("Set withdraw address is not allowed")] 38 | WithdrawAddrPerm {}, 39 | 40 | #[error("Unsupported message")] 41 | UnsupportedMessage {}, 42 | 43 | #[error("Allowance already expired while setting: {0}")] 44 | SettingExpiredAllowance(Expiration), 45 | 46 | #[error("Semver parsing error: {0}")] 47 | SemVer(String), 48 | } 49 | 50 | impl From for ContractError { 51 | fn from(err: cw1_whitelist::ContractError) -> Self { 52 | match err { 53 | cw1_whitelist::ContractError::Std(error) => ContractError::Std(error), 54 | cw1_whitelist::ContractError::Unauthorized {} => ContractError::Unauthorized {}, 55 | } 56 | } 57 | } 58 | 59 | impl From for ContractError { 60 | fn from(err: semver::Error) -> Self { 61 | Self::SemVer(err.to_string()) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /contracts/cw1-subkeys/src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | This builds on [`cw1_whitelist`] to provide the first non-trivial solution. 3 | It still works like [`cw1_whitelist`] with a set of admins (typically 1) 4 | which have full control of the account. However, you can then grant 5 | a number of accounts allowances to send native tokens from this account. 6 | 7 | This was proposed in Summer 2019 for the Cosmos Hub and resembles the 8 | functionality of ERC20 (allowances and transfer from). 9 | 10 | For more information on this contract, please check out the 11 | [README](https://github.com/CosmWasm/cw-plus/blob/main/contracts/cw1-subkeys/README.md). 12 | */ 13 | 14 | pub mod contract; 15 | mod error; 16 | pub mod msg; 17 | pub mod state; 18 | 19 | pub use crate::error::ContractError; 20 | -------------------------------------------------------------------------------- /contracts/cw1-subkeys/src/msg.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | 3 | use std::fmt; 4 | 5 | use cosmwasm_schema::{cw_serde, QueryResponses}; 6 | use cosmwasm_std::{Coin, CosmosMsg, Empty}; 7 | use cw_utils::{Expiration, NativeBalance}; 8 | 9 | use crate::state::Permissions; 10 | 11 | #[cw_serde] 12 | pub enum ExecuteMsg 13 | where 14 | T: Clone + fmt::Debug + PartialEq + JsonSchema, 15 | { 16 | /// Execute requests the contract to re-dispatch all these messages with the 17 | /// contract's address as sender. Every implementation has it's own logic to 18 | /// determine in 19 | Execute { msgs: Vec> }, 20 | /// Freeze will make a mutable contract immutable, must be called by an admin 21 | Freeze {}, 22 | /// UpdateAdmins will change the admin set of the contract, must be called by an existing admin, 23 | /// and only works if the contract is mutable 24 | UpdateAdmins { admins: Vec }, 25 | 26 | /// Add an allowance to a given subkey (subkey must not be admin) 27 | IncreaseAllowance { 28 | spender: String, 29 | amount: Coin, 30 | expires: Option, 31 | }, 32 | /// Decreases an allowance for a given subkey (subkey must not be admin) 33 | DecreaseAllowance { 34 | spender: String, 35 | amount: Coin, 36 | expires: Option, 37 | }, 38 | 39 | // Setups up permissions for a given subkey. 40 | SetPermissions { 41 | spender: String, 42 | permissions: Permissions, 43 | }, 44 | } 45 | 46 | #[cw_serde] 47 | #[derive(QueryResponses)] 48 | pub enum QueryMsg 49 | where 50 | T: Clone + fmt::Debug + PartialEq + JsonSchema, 51 | { 52 | /// Shows all admins and whether or not it is mutable 53 | #[returns(cw1_whitelist::msg::AdminListResponse)] 54 | AdminList {}, 55 | /// Get the current allowance for the given subkey (how much it can spend) 56 | #[returns(crate::state::Allowance)] 57 | Allowance { spender: String }, 58 | /// Get the current permissions for the given subkey (how much it can spend) 59 | #[returns(PermissionsInfo)] 60 | Permissions { spender: String }, 61 | /// Checks permissions of the caller on this proxy. 62 | /// If CanExecute returns true then a call to `Execute` with the same message, 63 | /// before any further state changes, should also succeed. 64 | #[returns(cw1::CanExecuteResponse)] 65 | CanExecute { sender: String, msg: CosmosMsg }, 66 | /// Gets all Allowances for this contract 67 | #[returns(AllAllowancesResponse)] 68 | AllAllowances { 69 | start_after: Option, 70 | limit: Option, 71 | }, 72 | /// Gets all Permissions for this contract 73 | #[returns(AllPermissionsResponse)] 74 | AllPermissions { 75 | start_after: Option, 76 | limit: Option, 77 | }, 78 | } 79 | 80 | #[cw_serde] 81 | pub struct AllAllowancesResponse { 82 | pub allowances: Vec, 83 | } 84 | 85 | #[cfg(test)] 86 | impl AllAllowancesResponse { 87 | pub fn canonical(mut self) -> Self { 88 | self.allowances = self 89 | .allowances 90 | .into_iter() 91 | .map(AllowanceInfo::canonical) 92 | .collect(); 93 | self.allowances.sort_by(AllowanceInfo::cmp_by_spender); 94 | self 95 | } 96 | } 97 | 98 | #[cw_serde] 99 | pub struct AllowanceInfo { 100 | pub spender: String, 101 | pub balance: NativeBalance, 102 | pub expires: Expiration, 103 | } 104 | 105 | #[cfg(test)] 106 | impl AllowanceInfo { 107 | /// Utility function providing some ordering to be used with `slice::sort_by`. 108 | /// 109 | /// Note, that this doesn't implement full ordering - items with same spender but differing on 110 | /// permissions, would be considered equal, however as spender is a unique key in any valid 111 | /// state this is enough for testing purposes. 112 | /// 113 | /// Example: 114 | /// 115 | /// ``` 116 | /// # use cw_utils::{Expiration, NativeBalance}; 117 | /// # use cw1_subkeys::msg::AllowanceInfo; 118 | /// # use cosmwasm_schema::{cw_serde, QueryResponses};use cosmwasm_std::coin; 119 | /// 120 | /// let mut allows = vec![AllowanceInfo { 121 | /// spender: "spender2".to_owned(), 122 | /// balance: NativeBalance(vec![coin(1, "token1")]), 123 | /// expires: Expiration::Never {}, 124 | /// }, AllowanceInfo { 125 | /// spender: "spender1".to_owned(), 126 | /// balance: NativeBalance(vec![coin(2, "token2")]), 127 | /// expires: Expiration::Never {}, 128 | /// }]; 129 | /// 130 | /// allows.sort_by(AllowanceInfo::cmp_by_spender); 131 | /// 132 | /// assert_eq!( 133 | /// allows.into_iter().map(|allow| allow.spender).collect::>(), 134 | /// vec!["spender1".to_owned(), "spender2".to_owned()] 135 | /// ); 136 | /// ``` 137 | pub fn cmp_by_spender(left: &Self, right: &Self) -> std::cmp::Ordering { 138 | left.spender.cmp(&right.spender) 139 | } 140 | 141 | pub fn canonical(mut self) -> Self { 142 | self.balance.normalize(); 143 | self 144 | } 145 | } 146 | 147 | #[cw_serde] 148 | pub struct PermissionsInfo { 149 | pub spender: String, 150 | pub permissions: Permissions, 151 | } 152 | 153 | #[cfg(any(test, feature = "test-utils"))] 154 | impl PermissionsInfo { 155 | /// Utility function providing some ordering to be used with `slice::sort_by`. 156 | /// 157 | /// Note, that this doesn't implement full ordering - items with same spender but differing on 158 | /// permissions, would be considered equal, however as spender is a unique key in any valid 159 | /// state this is enough for testing purposes. 160 | /// 161 | /// Example: 162 | /// 163 | /// ``` 164 | /// # use cw1_subkeys::msg::PermissionsInfo; 165 | /// # use cw1_subkeys::state::Permissions; 166 | /// 167 | /// let mut perms = vec![PermissionsInfo { 168 | /// spender: "spender2".to_owned(), 169 | /// permissions: Permissions::default(), 170 | /// }, PermissionsInfo { 171 | /// spender: "spender1".to_owned(), 172 | /// permissions: Permissions::default(), 173 | /// }]; 174 | /// 175 | /// perms.sort_by(PermissionsInfo::cmp_by_spender); 176 | /// 177 | /// assert_eq!( 178 | /// perms.into_iter().map(|perm| perm.spender).collect::>(), 179 | /// vec!["spender1".to_owned(), "spender2".to_owned()] 180 | /// ); 181 | /// ``` 182 | pub fn cmp_by_spender(left: &Self, right: &Self) -> std::cmp::Ordering { 183 | left.spender.cmp(&right.spender) 184 | } 185 | } 186 | 187 | #[cw_serde] 188 | pub struct AllPermissionsResponse { 189 | pub permissions: Vec, 190 | } 191 | -------------------------------------------------------------------------------- /contracts/cw1-subkeys/src/state.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | use std::fmt; 4 | 5 | use cosmwasm_std::Addr; 6 | use cw_storage_plus::Map; 7 | use cw_utils::{Expiration, NativeBalance}; 8 | 9 | // Permissions struct defines users message execution permissions. 10 | // Could have implemented permissions for each cosmos module(StakingPermissions, GovPermissions etc...) 11 | // But that meant a lot of code for each module. Keeping the permissions inside one struct is more 12 | // optimal. Define other modules permissions here. 13 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, Default, Copy)] 14 | pub struct Permissions { 15 | pub delegate: bool, 16 | pub redelegate: bool, 17 | pub undelegate: bool, 18 | pub withdraw: bool, 19 | } 20 | 21 | impl fmt::Display for Permissions { 22 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 23 | write!( 24 | f, 25 | "staking: {{ delegate: {}, redelegate: {}, undelegate: {}, withdraw: {} }}", 26 | self.delegate, self.redelegate, self.undelegate, self.withdraw 27 | ) 28 | } 29 | } 30 | 31 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, Default)] 32 | pub struct Allowance { 33 | pub balance: NativeBalance, 34 | pub expires: Expiration, 35 | } 36 | 37 | #[cfg(test)] 38 | impl Allowance { 39 | /// Utility function for converting message to its canonical form, so two messages with 40 | /// different representation but same semantic meaning can be easily compared. 41 | /// 42 | /// It could be encapsulated in custom `PartialEq` implementation, but `PartialEq` is expected 43 | /// to be fast, so it seems to be reasonable to keep it as representation-equality, and 44 | /// canonicalize message only when it is needed 45 | /// 46 | /// Example: 47 | /// 48 | /// ``` 49 | /// # use cw_utils::{Expiration, NativeBalance}; 50 | /// # use cw1_subkeys::state::Allowance; 51 | /// # use cosmwasm_std::coin; 52 | /// 53 | /// let allow1 = Allowance { 54 | /// balance: NativeBalance(vec![coin(1, "token1"), coin(0, "token2"), coin(2, "token1"), coin(3, "token3")]), 55 | /// expires: Expiration::Never {}, 56 | /// }; 57 | /// 58 | /// let allow2 = Allowance { 59 | /// balance: NativeBalance(vec![coin(3, "token3"), coin(3, "token1")]), 60 | /// expires: Expiration::Never {}, 61 | /// }; 62 | /// 63 | /// assert_eq!(allow1.canonical(), allow2.canonical()); 64 | /// ``` 65 | pub fn canonical(mut self) -> Self { 66 | self.balance.normalize(); 67 | self 68 | } 69 | } 70 | 71 | pub const PERMISSIONS: Map<&Addr, Permissions> = Map::new("permissions"); 72 | pub const ALLOWANCES: Map<&Addr, Allowance> = Map::new("allowances"); 73 | -------------------------------------------------------------------------------- /contracts/cw1-whitelist/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --lib --target wasm32-unknown-unknown" 3 | wasm-debug = "build --lib --target wasm32-unknown-unknown" 4 | unit-test = "test --lib" 5 | integration-test = "test --test integration" 6 | schema = "run --bin schema" 7 | -------------------------------------------------------------------------------- /contracts/cw1-whitelist/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw1-whitelist" 3 | version.workspace = true 4 | authors = ["Ethan Frey "] 5 | edition = "2021" 6 | description = "Implementation of an proxy contract using a whitelist" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cw-plus" 9 | homepage = "https://cosmwasm.com" 10 | documentation = "https://docs.cosmwasm.com" 11 | 12 | [lib] 13 | crate-type = ["cdylib", "rlib"] 14 | 15 | [features] 16 | # use library feature to disable all instantiate/execute/query exports 17 | library = [] 18 | test-utils = [] 19 | 20 | [dependencies] 21 | cosmwasm-schema = { workspace = true } 22 | cw-utils = { workspace = true } 23 | cw1 = { workspace = true } 24 | cw2 = { workspace = true } 25 | cosmwasm-std = { workspace = true, features = ["staking"] } 26 | cw-storage-plus = { workspace = true } 27 | schemars = { workspace = true } 28 | serde = { workspace = true } 29 | thiserror = { workspace = true } 30 | 31 | [dev-dependencies] 32 | anyhow = "1" 33 | assert_matches = "1" 34 | cw-multi-test = { workspace = true } 35 | derivative = "2" 36 | -------------------------------------------------------------------------------- /contracts/cw1-whitelist/README.md: -------------------------------------------------------------------------------- 1 | # CW1 Whitelist 2 | 3 | This may be the simplest implementation of CW1, a whitelist of addresses. 4 | It contains a set of admins that are defined upon creation. 5 | Any of those admins may `Execute` any message via the contract, 6 | per the CW1 spec. 7 | 8 | To make this slightly less minimalistic, you can allow the admin set 9 | to be mutable or immutable. If it is mutable, then any admin may 10 | (a) change the admin set and (b) freeze it (making it immutable). 11 | 12 | While largely an example contract for CW1, this has various real-world use-cases, 13 | such as a common account that is shared among multiple trusted devices, 14 | or trading an entire account (used as 1 of 1 mutable). Most of the time, 15 | this can be used as a framework to build your own, 16 | more advanced cw1 implementations. 17 | 18 | ## Allowing Custom Messages 19 | 20 | By default, this doesn't support `CustomMsg` in order to be fully generic 21 | among blockchains. However, all types are Generic over `T`, and this is only 22 | fixed in `handle`. You can import this contract and just redefine your `handle` 23 | function, setting a different parameter to `ExecuteMsg`, and you can produce 24 | a chain-specific message. 25 | 26 | ## Running this contract 27 | 28 | You will need Rust 1.44.1+ with `wasm32-unknown-unknown` target installed. 29 | 30 | You can run unit tests on this via: 31 | 32 | `cargo test` 33 | 34 | Once you are happy with the content, you can compile it to wasm via: 35 | 36 | ``` 37 | RUSTFLAGS='-C link-arg=-s' cargo wasm 38 | cp ../../target/wasm32-unknown-unknown/release/cw1_whitelist.wasm . 39 | ls -l cw1_whitelist.wasm 40 | sha256sum cw1_whitelist.wasm 41 | ``` 42 | 43 | Or for a production-ready (optimized) build, run a build command in 44 | the repository root: https://github.com/CosmWasm/cw-plus#compiling. 45 | -------------------------------------------------------------------------------- /contracts/cw1-whitelist/src/bin/schema.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::write_api; 2 | 3 | use cw1_whitelist::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; 4 | 5 | fn main() { 6 | write_api! { 7 | instantiate: InstantiateMsg, 8 | execute: ExecuteMsg, 9 | query: QueryMsg, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /contracts/cw1-whitelist/src/error.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::StdError; 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug, PartialEq)] 5 | pub enum ContractError { 6 | #[error("{0}")] 7 | Std(#[from] StdError), 8 | 9 | #[error("Unauthorized")] 10 | Unauthorized {}, 11 | } 12 | -------------------------------------------------------------------------------- /contracts/cw1-whitelist/src/integration_tests.rs: -------------------------------------------------------------------------------- 1 | use crate::msg::{AdminListResponse, ExecuteMsg, InstantiateMsg, QueryMsg}; 2 | use anyhow::{anyhow, Result}; 3 | use assert_matches::assert_matches; 4 | use cosmwasm_std::{ 5 | testing::mock_dependencies, to_json_binary, Addr, CosmosMsg, Empty, QueryRequest, StdError, 6 | WasmMsg, WasmQuery, 7 | }; 8 | use cw1::Cw1Contract; 9 | use cw_multi_test::{App, AppResponse, Contract, ContractWrapper, Executor}; 10 | use derivative::Derivative; 11 | use serde::{de::DeserializeOwned, Serialize}; 12 | 13 | fn mock_app() -> App { 14 | App::default() 15 | } 16 | 17 | fn contract_cw1() -> Box> { 18 | let contract = ContractWrapper::new( 19 | crate::contract::execute, 20 | crate::contract::instantiate, 21 | crate::contract::query, 22 | ); 23 | Box::new(contract) 24 | } 25 | 26 | #[derive(Derivative)] 27 | #[derivative(Debug)] 28 | pub struct Suite { 29 | /// Application mock 30 | #[derivative(Debug = "ignore")] 31 | app: App, 32 | /// Special account 33 | pub owner: String, 34 | /// ID of stored code for cw1 contract 35 | cw1_id: u64, 36 | } 37 | 38 | impl Suite { 39 | pub fn init() -> Result { 40 | let mut app = mock_app(); 41 | let owner = mock_dependencies().api.addr_make("owner").to_string(); 42 | let cw1_id = app.store_code(contract_cw1()); 43 | 44 | Ok(Suite { app, owner, cw1_id }) 45 | } 46 | 47 | pub fn instantiate_cw1_contract(&mut self, admins: Vec, mutable: bool) -> Cw1Contract { 48 | let contract = self 49 | .app 50 | .instantiate_contract( 51 | self.cw1_id, 52 | Addr::unchecked(self.owner.clone()), 53 | &InstantiateMsg { admins, mutable }, 54 | &[], 55 | "Whitelist", 56 | None, 57 | ) 58 | .unwrap(); 59 | Cw1Contract(contract) 60 | } 61 | 62 | pub fn execute( 63 | &mut self, 64 | sender_contract: Addr, 65 | target_contract: &Addr, 66 | msg: M, 67 | ) -> Result 68 | where 69 | M: Serialize + DeserializeOwned, 70 | { 71 | let execute: ExecuteMsg = ExecuteMsg::Execute { 72 | msgs: vec![CosmosMsg::Wasm(WasmMsg::Execute { 73 | contract_addr: target_contract.to_string(), 74 | msg: to_json_binary(&msg)?, 75 | funds: vec![], 76 | })], 77 | }; 78 | self.app 79 | .execute_contract( 80 | Addr::unchecked(self.owner.clone()), 81 | sender_contract, 82 | &execute, 83 | &[], 84 | ) 85 | .map_err(|err| anyhow!(err)) 86 | } 87 | 88 | pub fn query(&self, target_contract: Addr, msg: M) -> Result 89 | where 90 | M: Serialize + DeserializeOwned, 91 | { 92 | self.app.wrap().query(&QueryRequest::Wasm(WasmQuery::Smart { 93 | contract_addr: target_contract.to_string(), 94 | msg: to_json_binary(&msg).unwrap(), 95 | })) 96 | } 97 | } 98 | 99 | #[test] 100 | fn proxy_freeze_message() { 101 | let mut suite = Suite::init().unwrap(); 102 | 103 | let first_contract = suite.instantiate_cw1_contract(vec![suite.owner.clone()], true); 104 | let second_contract = 105 | suite.instantiate_cw1_contract(vec![first_contract.addr().to_string()], true); 106 | assert_ne!(second_contract, first_contract); 107 | 108 | let freeze_msg: ExecuteMsg = ExecuteMsg::Freeze {}; 109 | assert_matches!( 110 | suite.execute(first_contract.addr(), &second_contract.addr(), freeze_msg), 111 | Ok(_) 112 | ); 113 | 114 | let query_msg: QueryMsg = QueryMsg::AdminList {}; 115 | assert_matches!( 116 | suite.query(second_contract.addr(), query_msg), 117 | Ok( 118 | AdminListResponse { 119 | mutable, 120 | .. 121 | }) if !mutable 122 | ); 123 | } 124 | -------------------------------------------------------------------------------- /contracts/cw1-whitelist/src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | This may be the simplest implementation of [CW1](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw1/README.md), a whitelist of addresses. 3 | It contains a set of admins that are defined upon creation. 4 | Any of those admins may `Execute` any message via the contract, 5 | per the CW1 spec. 6 | 7 | To make this slighly less minimalistic, you can allow the admin set 8 | to be mutable or immutable. If it is mutable, then any admin may 9 | (a) change the admin set and (b) freeze it (making it immutable). 10 | 11 | While largely an example contract for CW1, this has various real-world use-cases, 12 | such as a common account that is shared among multiple trusted devices, 13 | or trading an entire account (used as 1 of 1 mutable). Most of the time, 14 | this can be used as a framework to build your own, 15 | more advanced cw1 implementations. 16 | 17 | For more information on this contract, please check out the 18 | [README](https://github.com/CosmWasm/cw-plus/blob/main/contracts/cw1-whitelist/README.md). 19 | */ 20 | 21 | pub mod contract; 22 | pub mod error; 23 | #[cfg(test)] 24 | mod integration_tests; 25 | pub mod msg; 26 | pub mod state; 27 | 28 | pub use crate::error::ContractError; 29 | -------------------------------------------------------------------------------- /contracts/cw1-whitelist/src/msg.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | 3 | use std::fmt; 4 | 5 | use cosmwasm_schema::{cw_serde, QueryResponses}; 6 | use cosmwasm_std::{CosmosMsg, Empty}; 7 | 8 | #[cw_serde] 9 | pub struct InstantiateMsg { 10 | pub admins: Vec, 11 | pub mutable: bool, 12 | } 13 | 14 | #[cw_serde] 15 | pub enum ExecuteMsg 16 | where 17 | T: Clone + fmt::Debug + PartialEq + JsonSchema, 18 | { 19 | /// Execute requests the contract to re-dispatch all these messages with the 20 | /// contract's address as sender. Every implementation has it's own logic to 21 | /// determine in 22 | Execute { msgs: Vec> }, 23 | /// Freeze will make a mutable contract immutable, must be called by an admin 24 | Freeze {}, 25 | /// UpdateAdmins will change the admin set of the contract, must be called by an existing admin, 26 | /// and only works if the contract is mutable 27 | UpdateAdmins { admins: Vec }, 28 | } 29 | 30 | #[cw_serde] 31 | #[derive(QueryResponses)] 32 | pub enum QueryMsg 33 | where 34 | T: Clone + fmt::Debug + PartialEq + JsonSchema, 35 | { 36 | /// Shows all admins and whether or not it is mutable 37 | #[returns(AdminListResponse)] 38 | AdminList {}, 39 | /// Checks permissions of the caller on this proxy. 40 | /// If CanExecute returns true then a call to `Execute` with the same message, 41 | /// before any further state changes, should also succeed. 42 | #[returns(cw1::CanExecuteResponse)] 43 | CanExecute { sender: String, msg: CosmosMsg }, 44 | } 45 | 46 | #[cw_serde] 47 | pub struct AdminListResponse { 48 | pub admins: Vec, 49 | pub mutable: bool, 50 | } 51 | 52 | #[cfg(any(test, feature = "test-utils"))] 53 | impl AdminListResponse { 54 | /// Utility function for converting message to its canonical form, so two messages with 55 | /// different representation but same semantic meaning can be easily compared. 56 | /// 57 | /// It could be encapsulated in custom `PartialEq` implementation, but `PartialEq` is expected 58 | /// to be quickly, so it seems to be reasonable to keep it as representation-equality, and 59 | /// canonicalize message only when it is needed 60 | /// 61 | /// Example: 62 | /// 63 | /// ``` 64 | /// # use cw1_whitelist::msg::AdminListResponse; 65 | /// 66 | /// let resp1 = AdminListResponse { 67 | /// admins: vec!["admin1".to_owned(), "admin2".to_owned()], 68 | /// mutable: true, 69 | /// }; 70 | /// 71 | /// let resp2 = AdminListResponse { 72 | /// admins: vec!["admin2".to_owned(), "admin1".to_owned(), "admin2".to_owned()], 73 | /// mutable: true, 74 | /// }; 75 | /// 76 | /// assert_eq!(resp1.canonical(), resp2.canonical()); 77 | /// ``` 78 | pub fn canonical(mut self) -> Self { 79 | self.admins.sort(); 80 | self.admins.dedup(); 81 | self 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /contracts/cw1-whitelist/src/state.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use cosmwasm_std::Addr; 5 | use cw_storage_plus::Item; 6 | 7 | #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug, Default)] 8 | pub struct AdminList { 9 | pub admins: Vec, 10 | pub mutable: bool, 11 | } 12 | 13 | impl AdminList { 14 | /// returns true if the address is a registered admin 15 | pub fn is_admin(&self, addr: impl AsRef) -> bool { 16 | let addr = addr.as_ref(); 17 | self.admins.iter().any(|a| a.as_ref() == addr) 18 | } 19 | 20 | /// returns true if the address is a registered admin and the config is mutable 21 | pub fn can_modify(&self, addr: &str) -> bool { 22 | self.mutable && self.is_admin(addr) 23 | } 24 | } 25 | 26 | pub const ADMIN_LIST: Item = Item::new("admin_list"); 27 | 28 | #[cfg(test)] 29 | mod tests { 30 | use super::*; 31 | 32 | #[test] 33 | fn is_admin() { 34 | let admins: Vec<_> = vec!["bob", "paul", "john"] 35 | .into_iter() 36 | .map(Addr::unchecked) 37 | .collect(); 38 | let config = AdminList { 39 | admins: admins.clone(), 40 | mutable: false, 41 | }; 42 | 43 | assert!(config.is_admin(admins[0].as_ref())); 44 | assert!(config.is_admin(admins[2].as_ref())); 45 | assert!(!config.is_admin("other")); 46 | } 47 | 48 | #[test] 49 | fn can_modify() { 50 | let alice = Addr::unchecked("alice"); 51 | let bob = Addr::unchecked("bob"); 52 | 53 | // admin can modify mutable contract 54 | let config = AdminList { 55 | admins: vec![bob.clone()], 56 | mutable: true, 57 | }; 58 | assert!(!config.can_modify(alice.as_ref())); 59 | assert!(config.can_modify(bob.as_ref())); 60 | 61 | // no one can modify an immutable contract 62 | let config = AdminList { 63 | admins: vec![alice.clone()], 64 | mutable: false, 65 | }; 66 | assert!(!config.can_modify(alice.as_ref())); 67 | assert!(!config.can_modify(bob.as_ref())); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /contracts/cw20-base/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --lib --target wasm32-unknown-unknown" 3 | wasm-debug = "build --lib --target wasm32-unknown-unknown" 4 | unit-test = "test --lib" 5 | integration-test = "test --test integration" 6 | schema = "run --bin schema" 7 | -------------------------------------------------------------------------------- /contracts/cw20-base/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw20-base" 3 | version.workspace = true 4 | authors = ["Ethan Frey "] 5 | edition = "2021" 6 | description = "Basic implementation of a CosmWasm-20 compliant token" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cw-plus" 9 | homepage = "https://cosmwasm.com" 10 | documentation = "https://docs.cosmwasm.com" 11 | 12 | [lib] 13 | crate-type = ["cdylib", "rlib"] 14 | 15 | [features] 16 | # use library feature to disable all instantiate/execute/query exports 17 | library = [] 18 | 19 | [dependencies] 20 | cosmwasm-schema = { workspace = true } 21 | cw2 = { workspace = true } 22 | cw20 = { workspace = true } 23 | cw-storage-plus = { workspace = true } 24 | cosmwasm-std = { workspace = true } 25 | schemars = { workspace = true } 26 | semver = { workspace = true } 27 | serde = { workspace = true } 28 | thiserror = { workspace = true } 29 | 30 | [dev-dependencies] 31 | cw-multi-test = { workspace = true } 32 | cw-utils = { workspace = true } 33 | -------------------------------------------------------------------------------- /contracts/cw20-base/README.md: -------------------------------------------------------------------------------- 1 | # CW20 Basic 2 | 3 | This is a basic implementation of a cw20 contract. It implements 4 | the [CW20 spec](../../packages/cw20/README.md) and is designed to 5 | be deployed as is, or imported into other contracts to easily build 6 | cw20-compatible tokens with custom logic. 7 | 8 | Implements: 9 | 10 | - [x] CW20 Base 11 | - [x] Mintable extension 12 | - [x] Allowances extension 13 | 14 | ## Running this contract 15 | 16 | You will need Rust 1.44.1+ with `wasm32-unknown-unknown` target installed. 17 | 18 | You can run unit tests on this via: 19 | 20 | `cargo test` 21 | 22 | Once you are happy with the content, you can compile it to wasm via: 23 | 24 | ``` 25 | RUSTFLAGS='-C link-arg=-s' cargo wasm 26 | cp ../../target/wasm32-unknown-unknown/release/cw20_base.wasm . 27 | ls -l cw20_base.wasm 28 | sha256sum cw20_base.wasm 29 | ``` 30 | 31 | Or for a production-ready (optimized) build, run a build command in the 32 | the repository root: https://github.com/CosmWasm/cw-plus#compiling. 33 | 34 | ## Importing this contract 35 | 36 | You can also import much of the logic of this contract to build another 37 | ERC20-contract, such as a bonding curve, overiding or extending what you 38 | need. 39 | 40 | Basically, you just need to write your handle function and import 41 | `cw20_base::contract::handle_transfer`, etc and dispatch to them. 42 | This allows you to use custom `ExecuteMsg` and `QueryMsg` with your additional 43 | calls, but then use the underlying implementation for the standard cw20 44 | messages you want to support. The same with `QueryMsg`. You *could* reuse `instantiate` 45 | as it, but it is likely you will want to change it. And it is rather simple. 46 | 47 | Look at [`cw20-staking`](https://github.com/CosmWasm/cw-tokens/tree/main/contracts/cw20-staking) for an example of how to "inherit" 48 | all this token functionality and combine it with custom logic. 49 | -------------------------------------------------------------------------------- /contracts/cw20-base/src/bin/schema.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::write_api; 2 | 3 | use cw20_base::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; 4 | 5 | fn main() { 6 | write_api! { 7 | instantiate: InstantiateMsg, 8 | execute: ExecuteMsg, 9 | query: QueryMsg, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /contracts/cw20-base/src/error.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::StdError; 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug, PartialEq)] 5 | pub enum ContractError { 6 | #[error("{0}")] 7 | Std(#[from] StdError), 8 | 9 | #[error("Unauthorized")] 10 | Unauthorized {}, 11 | 12 | #[error("Cannot set to own account")] 13 | CannotSetOwnAccount {}, 14 | 15 | // Unused error case. Zero is now treated like every other value. 16 | #[deprecated(note = "Unused. All zero amount checks have been removed")] 17 | #[error("Invalid zero amount")] 18 | InvalidZeroAmount {}, 19 | 20 | #[error("Allowance is expired")] 21 | Expired {}, 22 | 23 | #[error("No allowance for this account")] 24 | NoAllowance {}, 25 | 26 | #[error("Minting cannot exceed the cap")] 27 | CannotExceedCap {}, 28 | 29 | #[error("Logo binary data exceeds 5KB limit")] 30 | LogoTooBig {}, 31 | 32 | #[error("Invalid xml preamble for SVG")] 33 | InvalidXmlPreamble {}, 34 | 35 | #[error("Invalid png header")] 36 | InvalidPngHeader {}, 37 | 38 | #[error("Invalid expiration value")] 39 | InvalidExpiration {}, 40 | 41 | #[error("Duplicate initial balance addresses")] 42 | DuplicateInitialBalanceAddresses {}, 43 | } 44 | -------------------------------------------------------------------------------- /contracts/cw20-base/src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | This is a basic implementation of a cw20 contract. It implements 3 | the [CW20 spec](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw20/README.md) and is designed to 4 | be deployed as is, or imported into other contracts to easily build 5 | cw20-compatible tokens with custom logic. 6 | 7 | Implements: 8 | 9 | - [x] CW20 Base 10 | - [x] Mintable extension 11 | - [x] Allowances extension 12 | 13 | For more information on this contract, please check out the 14 | [README](https://github.com/CosmWasm/cw-plus/blob/main/contracts/cw20-base/README.md). 15 | */ 16 | 17 | pub mod allowances; 18 | pub mod contract; 19 | pub mod enumerable; 20 | mod error; 21 | pub mod msg; 22 | pub mod state; 23 | 24 | pub use crate::error::ContractError; 25 | -------------------------------------------------------------------------------- /contracts/cw20-base/src/msg.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::{cw_serde, QueryResponses}; 2 | use cosmwasm_std::{StdError, StdResult, Uint128}; 3 | use cw20::{Cw20Coin, Logo, MinterResponse}; 4 | use schemars::JsonSchema; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | pub use cw20::Cw20ExecuteMsg as ExecuteMsg; 8 | 9 | #[cw_serde] 10 | pub struct InstantiateMarketingInfo { 11 | pub project: Option, 12 | pub description: Option, 13 | pub marketing: Option, 14 | pub logo: Option, 15 | } 16 | 17 | #[cw_serde] 18 | #[cfg_attr(test, derive(Default))] 19 | pub struct InstantiateMsg { 20 | pub name: String, 21 | pub symbol: String, 22 | pub decimals: u8, 23 | pub initial_balances: Vec, 24 | pub mint: Option, 25 | pub marketing: Option, 26 | } 27 | 28 | impl InstantiateMsg { 29 | pub fn get_cap(&self) -> Option { 30 | self.mint.as_ref().and_then(|v| v.cap) 31 | } 32 | 33 | pub fn validate(&self) -> StdResult<()> { 34 | // Check name, symbol, decimals 35 | if !self.has_valid_name() { 36 | return Err(StdError::generic_err( 37 | "Name is not in the expected format (3-50 UTF-8 bytes)", 38 | )); 39 | } 40 | if !self.has_valid_symbol() { 41 | return Err(StdError::generic_err( 42 | "Ticker symbol is not in expected format [a-zA-Z\\-]{3,12}", 43 | )); 44 | } 45 | if self.decimals > 18 { 46 | return Err(StdError::generic_err("Decimals must not exceed 18")); 47 | } 48 | Ok(()) 49 | } 50 | 51 | fn has_valid_name(&self) -> bool { 52 | let bytes = self.name.as_bytes(); 53 | if bytes.len() < 3 || bytes.len() > 50 { 54 | return false; 55 | } 56 | true 57 | } 58 | 59 | fn has_valid_symbol(&self) -> bool { 60 | let bytes = self.symbol.as_bytes(); 61 | if bytes.len() < 3 || bytes.len() > 12 { 62 | return false; 63 | } 64 | for byte in bytes.iter() { 65 | if (*byte != 45) && (*byte < 65 || *byte > 90) && (*byte < 97 || *byte > 122) { 66 | return false; 67 | } 68 | } 69 | true 70 | } 71 | } 72 | 73 | #[cw_serde] 74 | #[derive(QueryResponses)] 75 | pub enum QueryMsg { 76 | /// Returns the current balance of the given address, 0 if unset. 77 | #[returns(cw20::BalanceResponse)] 78 | Balance { address: String }, 79 | /// Returns metadata on the contract - name, decimals, supply, etc. 80 | #[returns(cw20::TokenInfoResponse)] 81 | TokenInfo {}, 82 | /// Only with "mintable" extension. 83 | /// Returns who can mint and the hard cap on maximum tokens after minting. 84 | #[returns(cw20::MinterResponse)] 85 | Minter {}, 86 | /// Only with "allowance" extension. 87 | /// Returns how much spender can use from owner account, 0 if unset. 88 | #[returns(cw20::AllowanceResponse)] 89 | Allowance { owner: String, spender: String }, 90 | /// Only with "enumerable" extension (and "allowances") 91 | /// Returns all allowances this owner has approved. Supports pagination. 92 | #[returns(cw20::AllAllowancesResponse)] 93 | AllAllowances { 94 | owner: String, 95 | start_after: Option, 96 | limit: Option, 97 | }, 98 | /// Only with "enumerable" extension (and "allowances") 99 | /// Returns all allowances this spender has been granted. Supports pagination. 100 | #[returns(cw20::AllSpenderAllowancesResponse)] 101 | AllSpenderAllowances { 102 | spender: String, 103 | start_after: Option, 104 | limit: Option, 105 | }, 106 | /// Only with "enumerable" extension 107 | /// Returns all accounts that have balances. Supports pagination. 108 | #[returns(cw20::AllAccountsResponse)] 109 | AllAccounts { 110 | start_after: Option, 111 | limit: Option, 112 | }, 113 | /// Only with "marketing" extension 114 | /// Returns more metadata on the contract to display in the client: 115 | /// - description, logo, project url, etc. 116 | #[returns(cw20::MarketingInfoResponse)] 117 | MarketingInfo {}, 118 | /// Only with "marketing" extension 119 | /// Downloads the embedded logo data (if stored on chain). Errors if no logo data is stored for this 120 | /// contract. 121 | #[returns(cw20::DownloadLogoResponse)] 122 | DownloadLogo {}, 123 | } 124 | 125 | #[derive(Serialize, Deserialize, JsonSchema)] 126 | pub struct MigrateMsg {} 127 | 128 | #[cfg(test)] 129 | mod tests { 130 | use super::*; 131 | 132 | #[test] 133 | fn validate_instantiatemsg_name() { 134 | // Too short 135 | let mut msg = InstantiateMsg { 136 | name: str::repeat("a", 2), 137 | ..InstantiateMsg::default() 138 | }; 139 | assert!(!msg.has_valid_name()); 140 | 141 | // In the correct length range 142 | msg.name = str::repeat("a", 3); 143 | assert!(msg.has_valid_name()); 144 | 145 | // Too long 146 | msg.name = str::repeat("a", 51); 147 | assert!(!msg.has_valid_name()); 148 | } 149 | 150 | #[test] 151 | fn validate_instantiatemsg_symbol() { 152 | // Too short 153 | let mut msg = InstantiateMsg { 154 | symbol: str::repeat("a", 2), 155 | ..InstantiateMsg::default() 156 | }; 157 | assert!(!msg.has_valid_symbol()); 158 | 159 | // In the correct length range 160 | msg.symbol = str::repeat("a", 3); 161 | assert!(msg.has_valid_symbol()); 162 | 163 | // Too long 164 | msg.symbol = str::repeat("a", 13); 165 | assert!(!msg.has_valid_symbol()); 166 | 167 | // Has illegal char 168 | let illegal_chars = [[64u8], [91u8], [123u8]]; 169 | illegal_chars.iter().for_each(|c| { 170 | let c = std::str::from_utf8(c).unwrap(); 171 | msg.symbol = str::repeat(c, 3); 172 | assert!(!msg.has_valid_symbol()); 173 | }); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /contracts/cw20-base/src/state.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | use cosmwasm_std::{Addr, Uint128}; 3 | use cw_storage_plus::{Item, Map}; 4 | 5 | use cw20::{AllowanceResponse, Logo, MarketingInfoResponse}; 6 | 7 | #[cw_serde] 8 | pub struct TokenInfo { 9 | pub name: String, 10 | pub symbol: String, 11 | pub decimals: u8, 12 | pub total_supply: Uint128, 13 | pub mint: Option, 14 | } 15 | 16 | #[cw_serde] 17 | pub struct MinterData { 18 | pub minter: Addr, 19 | /// cap is how many more tokens can be issued by the minter 20 | pub cap: Option, 21 | } 22 | 23 | impl TokenInfo { 24 | pub fn get_cap(&self) -> Option { 25 | self.mint.as_ref().and_then(|v| v.cap) 26 | } 27 | } 28 | 29 | pub const TOKEN_INFO: Item = Item::new("token_info"); 30 | pub const MARKETING_INFO: Item = Item::new("marketing_info"); 31 | pub const LOGO: Item = Item::new("logo"); 32 | pub const BALANCES: Map<&Addr, Uint128> = Map::new("balance"); 33 | pub const ALLOWANCES: Map<(&Addr, &Addr), AllowanceResponse> = Map::new("allowance"); 34 | // TODO: After https://github.com/CosmWasm/cw-plus/issues/670 is implemented, replace this with a `MultiIndex` over `ALLOWANCES` 35 | pub const ALLOWANCES_SPENDER: Map<(&Addr, &Addr), AllowanceResponse> = 36 | Map::new("allowance_spender"); 37 | -------------------------------------------------------------------------------- /contracts/cw20-ics20/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --lib --target wasm32-unknown-unknown" 3 | wasm-debug = "build --lib --target wasm32-unknown-unknown" 4 | unit-test = "test --lib" 5 | integration-test = "test --test integration" 6 | schema = "run --bin schema" 7 | -------------------------------------------------------------------------------- /contracts/cw20-ics20/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw20-ics20" 3 | version.workspace = true 4 | authors = ["Ethan Frey "] 5 | edition = "2021" 6 | description = "IBC Enabled contracts that receives CW20 tokens and sends them over ICS20 to a remote chain" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cw-plus" 9 | homepage = "https://cosmwasm.com" 10 | documentation = "https://docs.cosmwasm.com" 11 | 12 | [lib] 13 | crate-type = ["cdylib", "rlib"] 14 | 15 | [features] 16 | # use library feature to disable all init/handle/query exports 17 | library = [] 18 | 19 | [dependencies] 20 | cosmwasm-schema = { workspace = true } 21 | cw-utils = { workspace = true } 22 | cw2 = { workspace = true } 23 | cw20 = { workspace = true } 24 | cosmwasm-std = { workspace = true, features = ["stargate"] } 25 | cw-storage-plus = { workspace = true } 26 | cw-controllers = { workspace = true } 27 | schemars = { workspace = true } 28 | semver = { workspace = true } 29 | serde = { workspace = true } 30 | thiserror = { workspace = true } 31 | 32 | [dev-dependencies] 33 | easy-addr = { workspace = true } 34 | -------------------------------------------------------------------------------- /contracts/cw20-ics20/README.md: -------------------------------------------------------------------------------- 1 | # CW20 ICS20 2 | 3 | This is an *IBC Enabled* contract that allows us to send CW20 tokens from one chain over the standard ICS20 4 | protocol to the bank module of another chain. In short, it lets us send our custom CW20 tokens with IBC and use 5 | them just like native tokens on other chains. 6 | 7 | It is only designed to send tokens and redeem previously sent tokens. It will not mint tokens belonging 8 | to assets originating on the foreign chain. This is different than the Golang `ibctransfer` module, but 9 | we properly implement ICS20 and respond with an error message... let's hope the Go side handles this correctly. 10 | 11 | ## Workflow 12 | 13 | The contract starts with minimal state. It just stores a default timeout in seconds for all packets it sends. 14 | Most importantly it binds a local IBC port to enable channel connections. 15 | 16 | An external party first needs to make one or more channels using this contract as one endpoint. It will use standard ics20 17 | unordered channels for the version negotiation. Once established, it manages a list of known channels. You can use 18 | [ts-relayer](https://github.com/confio/ts-relayer) `ibc-setup ics20` command to create these. 19 | 20 | After there is at least one channel, you can send any CW20 token to this contract via the 21 | [receiver pattern](https://github.com/CosmWasm/cw-plus/blob/master/packages/cw20/README.md#receiver). 22 | The receive message must contain the channel to send over and the remote address to send to. It may optionally 23 | include a custom timeout. 24 | 25 | ## Messages 26 | 27 | It only accepts CW20ReceiveMsg from a cw20 contract. The data sent along with that message must be a JSON-serialized 28 | TransferMsg: 29 | 30 | ```rust 31 | pub struct TransferMsg { 32 | /// The local channel to send the packets on 33 | pub channel: String, 34 | /// The remote address to send to 35 | /// Don't use HumanAddress as this will likely have a different Bech32 prefix than we use 36 | /// and cannot be validated locally 37 | pub remote_address: String, 38 | /// How long the packet lives in seconds. If not specified, use default_timeout 39 | pub timeout: Option, 40 | } 41 | ``` 42 | 43 | In addition, it supports directly sending native tokens via `ExecuteMsg::Transfer(TransferMsg)`. 44 | You must send *exactly one* coin denom along with the transfer message, and that amount will be transfered 45 | to the remote host. 46 | 47 | ## Queries 48 | 49 | Queries only make sense relative to the established channels of this contract. 50 | 51 | * `Port{}` - returns the port ID this contract has bound, so you can create channels. This info can be queried 52 | via wasmd contract info query, but we expose another query here for convenience. 53 | * `ListChannels{}` - returns a (currently unpaginated) list of all channels that have been created on this contract. 54 | Returns their local channelId along with some basic metadata, like the remote port/channel and the connection they 55 | run on top of. 56 | * `Channel{id}` - returns more detailed information on one specific channel. In addition to the information available 57 | in the list view, it returns the current outstanding balance on that channel, as well as the total amount that 58 | has ever been sent on the channel. 59 | 60 | ## IBC Responses 61 | 62 | These are defined by the ICS20 spec. 63 | 64 | Notably, each Channel has a balance of tokens sent over that channel. If an incoming transfer request comes in for 65 | a denom it does not know, or for a balance larger than we have sent, we will return an error in the acknowledgement 66 | packet. -------------------------------------------------------------------------------- /contracts/cw20-ics20/src/amount.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ContractError; 2 | use cosmwasm_schema::cw_serde; 3 | use cosmwasm_std::{Coin, Uint128}; 4 | use cw20::Cw20Coin; 5 | use std::convert::TryInto; 6 | 7 | #[cw_serde] 8 | pub enum Amount { 9 | Native(Coin), 10 | // FIXME? USe Cw20CoinVerified, and validate cw20 addresses 11 | Cw20(Cw20Coin), 12 | } 13 | 14 | impl Amount { 15 | // TODO: write test for this 16 | pub fn from_parts(denom: String, amount: Uint128) -> Self { 17 | if denom.starts_with("cw20:") { 18 | let address = denom.get(5..).unwrap().into(); 19 | Amount::Cw20(Cw20Coin { address, amount }) 20 | } else { 21 | Amount::Native(Coin { denom, amount }) 22 | } 23 | } 24 | 25 | pub fn cw20(amount: u128, addr: &str) -> Self { 26 | Amount::Cw20(Cw20Coin { 27 | address: addr.into(), 28 | amount: Uint128::new(amount), 29 | }) 30 | } 31 | 32 | pub fn native(amount: u128, denom: &str) -> Self { 33 | Amount::Native(Coin { 34 | denom: denom.to_string(), 35 | amount: Uint128::new(amount), 36 | }) 37 | } 38 | } 39 | 40 | impl Amount { 41 | pub fn denom(&self) -> String { 42 | match self { 43 | Amount::Native(c) => c.denom.clone(), 44 | Amount::Cw20(c) => format!("cw20:{}", c.address.as_str()), 45 | } 46 | } 47 | 48 | pub fn amount(&self) -> Uint128 { 49 | match self { 50 | Amount::Native(c) => c.amount, 51 | Amount::Cw20(c) => c.amount, 52 | } 53 | } 54 | 55 | /// convert the amount into u64 56 | pub fn u64_amount(&self) -> Result { 57 | Ok(self.amount().u128().try_into()?) 58 | } 59 | 60 | pub fn is_empty(&self) -> bool { 61 | match self { 62 | Amount::Native(c) => c.amount.is_zero(), 63 | Amount::Cw20(c) => c.amount.is_zero(), 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /contracts/cw20-ics20/src/bin/schema.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::write_api; 2 | 3 | use cw20_ics20::msg::{ExecuteMsg, InitMsg, QueryMsg}; 4 | 5 | fn main() { 6 | write_api! { 7 | instantiate: InitMsg, 8 | execute: ExecuteMsg, 9 | query: QueryMsg, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /contracts/cw20-ics20/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::num::TryFromIntError; 2 | use std::string::FromUtf8Error; 3 | use thiserror::Error; 4 | 5 | use cosmwasm_std::StdError; 6 | use cw_controllers::AdminError; 7 | use cw_utils::PaymentError; 8 | 9 | /// Never is a placeholder to ensure we don't return any errors 10 | #[derive(Error, Debug)] 11 | pub enum Never {} 12 | 13 | #[derive(Error, Debug, PartialEq)] 14 | pub enum ContractError { 15 | #[error("{0}")] 16 | Std(#[from] StdError), 17 | 18 | #[error("{0}")] 19 | Payment(#[from] PaymentError), 20 | 21 | #[error("{0}")] 22 | Admin(#[from] AdminError), 23 | 24 | #[error("Channel doesn't exist: {id}")] 25 | NoSuchChannel { id: String }, 26 | 27 | #[error("Didn't send any funds")] 28 | NoFunds {}, 29 | 30 | #[error("Amount larger than 2**64, not supported by ics20 packets")] 31 | AmountOverflow {}, 32 | 33 | #[error("Only supports channel with ibc version ics20-1, got {version}")] 34 | InvalidIbcVersion { version: String }, 35 | 36 | #[error("Only supports unordered channel")] 37 | OnlyOrderedChannel {}, 38 | 39 | #[error("Insufficient funds to redeem voucher on channel")] 40 | InsufficientFunds {}, 41 | 42 | #[error("Only accepts tokens that originate on this chain, not native tokens of remote chain")] 43 | NoForeignTokens {}, 44 | 45 | #[error("Parsed port from denom ({port}) doesn't match packet")] 46 | FromOtherPort { port: String }, 47 | 48 | #[error("Parsed channel from denom ({channel}) doesn't match packet")] 49 | FromOtherChannel { channel: String }, 50 | 51 | #[error("Cannot migrate from different contract type: {previous_contract}")] 52 | CannotMigrate { previous_contract: String }, 53 | 54 | #[error("Cannot migrate from unsupported version: {previous_version}")] 55 | CannotMigrateVersion { previous_version: String }, 56 | 57 | #[error("Got a submessage reply with unknown id: {id}")] 58 | UnknownReplyId { id: u64 }, 59 | 60 | #[error("You cannot lower the gas limit for a contract on the allow list")] 61 | CannotLowerGas, 62 | 63 | #[error("Only the governance contract can do this")] 64 | Unauthorized, 65 | 66 | #[error("You can only send cw20 tokens that have been explicitly allowed by governance")] 67 | NotOnAllowList, 68 | } 69 | 70 | impl From for ContractError { 71 | fn from(_: FromUtf8Error) -> Self { 72 | ContractError::Std(StdError::invalid_utf8("parsing denom key")) 73 | } 74 | } 75 | 76 | impl From for ContractError { 77 | fn from(_: TryFromIntError) -> Self { 78 | ContractError::AmountOverflow {} 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /contracts/cw20-ics20/src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | This is an *IBC Enabled* contract that allows us to send CW20 tokens from one chain over the standard ICS20 3 | protocol to the bank module of another chain. In short, it lets us send our custom CW20 tokens with IBC and use 4 | them just like native tokens on other chains. 5 | 6 | It is only designed to send tokens and redeem previously sent tokens. It will not mint tokens belonging 7 | to assets originating on the foreign chain. This is different than the Golang `ibctransfer` module, but 8 | we properly implement ICS20 and respond with an error message... let's hope the Go side handles this correctly. 9 | 10 | For more information on this contract, please check out the 11 | [README](https://github.com/CosmWasm/cw-plus/blob/main/contracts/cw20-ics20/README.md). 12 | */ 13 | 14 | pub mod amount; 15 | pub mod contract; 16 | mod error; 17 | pub mod ibc; 18 | mod migrations; 19 | pub mod msg; 20 | pub mod state; 21 | mod test_helpers; 22 | 23 | pub use crate::error::ContractError; 24 | -------------------------------------------------------------------------------- /contracts/cw20-ics20/src/migrations.rs: -------------------------------------------------------------------------------- 1 | // v1 format is anything older than 0.12.0 2 | pub mod v1 { 3 | use cosmwasm_schema::cw_serde; 4 | 5 | use cosmwasm_std::Addr; 6 | use cw_storage_plus::Item; 7 | 8 | #[cw_serde] 9 | pub struct Config { 10 | pub default_timeout: u64, 11 | pub gov_contract: Addr, 12 | } 13 | 14 | pub const CONFIG: Item = Item::new("ics20_config"); 15 | } 16 | 17 | // v2 format is anything older than 0.13.1 when we only updated the internal balances on success ack 18 | pub mod v2 { 19 | use crate::amount::Amount; 20 | use crate::state::{ChannelState, CHANNEL_INFO, CHANNEL_STATE}; 21 | use crate::ContractError; 22 | use cosmwasm_std::{to_json_binary, Addr, DepsMut, Env, Order, StdResult, WasmQuery}; 23 | use cw20::{BalanceResponse, Cw20QueryMsg}; 24 | 25 | pub fn update_balances(mut deps: DepsMut, env: &Env) -> Result<(), ContractError> { 26 | let channels = CHANNEL_INFO 27 | .keys(deps.storage, None, None, Order::Ascending) 28 | .collect::>>()?; 29 | match channels.len() { 30 | 0 => Ok(()), 31 | 1 => { 32 | let channel = &channels[0]; 33 | let addr = &env.contract.address; 34 | let states = CHANNEL_STATE 35 | .prefix(channel) 36 | .range(deps.storage, None, None, Order::Ascending) 37 | .collect::>>()?; 38 | for (denom, state) in states.into_iter() { 39 | update_denom(deps.branch(), addr, channel, denom, state)?; 40 | } 41 | Ok(()) 42 | } 43 | _ => Err(ContractError::CannotMigrate { 44 | previous_contract: "multiple channels open".into(), 45 | }), 46 | } 47 | } 48 | 49 | fn update_denom( 50 | deps: DepsMut, 51 | contract: &Addr, 52 | channel: &str, 53 | denom: String, 54 | mut state: ChannelState, 55 | ) -> StdResult<()> { 56 | // handle this for both native and cw20 57 | let balance = match Amount::from_parts(denom.clone(), state.outstanding) { 58 | Amount::Native(coin) => deps.querier.query_balance(contract, coin.denom)?.amount, 59 | Amount::Cw20(coin) => { 60 | // FIXME: we should be able to do this with the following line, but QuerierWrapper doesn't play 61 | // with the Querier generics 62 | // `Cw20Contract(contract.clone()).balance(&deps.querier, contract)?` 63 | let query = WasmQuery::Smart { 64 | contract_addr: coin.address, 65 | msg: to_json_binary(&Cw20QueryMsg::Balance { 66 | address: contract.into(), 67 | })?, 68 | }; 69 | let res: BalanceResponse = deps.querier.query(&query.into())?; 70 | res.balance 71 | } 72 | }; 73 | 74 | // this checks if we have received some coins that are "in flight" and not yet accounted in the state 75 | let diff = balance - state.outstanding; 76 | // if they are in flight, we add them to the internal state now, as if we added them when sent (not when acked) 77 | // to match the current logic 78 | if !diff.is_zero() { 79 | state.outstanding += diff; 80 | state.total_sent += diff; 81 | CHANNEL_STATE.save(deps.storage, (channel, &denom), &state)?; 82 | } 83 | 84 | Ok(()) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /contracts/cw20-ics20/src/msg.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::{cw_serde, QueryResponses}; 2 | use cw20::Cw20ReceiveMsg; 3 | 4 | use crate::amount::Amount; 5 | use crate::state::ChannelInfo; 6 | 7 | #[cw_serde] 8 | pub struct InitMsg { 9 | /// Default timeout for ics20 packets, specified in seconds 10 | pub default_timeout: u64, 11 | /// who can allow more contracts 12 | pub gov_contract: String, 13 | /// initial allowlist - all cw20 tokens we will send must be previously allowed by governance 14 | pub allowlist: Vec, 15 | /// If set, contracts off the allowlist will run with this gas limit. 16 | /// If unset, will refuse to accept any contract off the allow list. 17 | pub default_gas_limit: Option, 18 | } 19 | 20 | #[cw_serde] 21 | pub struct AllowMsg { 22 | pub contract: String, 23 | pub gas_limit: Option, 24 | } 25 | 26 | #[cw_serde] 27 | pub struct MigrateMsg { 28 | pub default_gas_limit: Option, 29 | } 30 | 31 | #[cw_serde] 32 | pub enum ExecuteMsg { 33 | /// This accepts a properly-encoded ReceiveMsg from a cw20 contract 34 | Receive(Cw20ReceiveMsg), 35 | /// This allows us to transfer *exactly one* native token 36 | Transfer(TransferMsg), 37 | /// This must be called by gov_contract, will allow a new cw20 token to be sent 38 | Allow(AllowMsg), 39 | /// Change the admin (must be called by current admin) 40 | UpdateAdmin { admin: String }, 41 | } 42 | 43 | /// This is the message we accept via Receive 44 | #[cw_serde] 45 | pub struct TransferMsg { 46 | /// The local channel to send the packets on 47 | pub channel: String, 48 | /// The remote address to send to. 49 | /// Don't use HumanAddress as this will likely have a different Bech32 prefix than we use 50 | /// and cannot be validated locally 51 | pub remote_address: String, 52 | /// How long the packet lives in seconds. If not specified, use default_timeout 53 | pub timeout: Option, 54 | /// An optional memo to add to the IBC transfer 55 | pub memo: Option, 56 | } 57 | 58 | #[cw_serde] 59 | #[derive(QueryResponses)] 60 | pub enum QueryMsg { 61 | /// Return the port ID bound by this contract. 62 | #[returns(PortResponse)] 63 | Port {}, 64 | /// Show all channels we have connected to. 65 | #[returns(ListChannelsResponse)] 66 | ListChannels {}, 67 | /// Returns the details of the name channel, error if not created. 68 | #[returns(ChannelResponse)] 69 | Channel { id: String }, 70 | /// Show the Config. 71 | #[returns(ConfigResponse)] 72 | Config {}, 73 | #[returns(cw_controllers::AdminResponse)] 74 | Admin {}, 75 | /// Query if a given cw20 contract is allowed. 76 | #[returns(AllowedResponse)] 77 | Allowed { contract: String }, 78 | /// List all allowed cw20 contracts. 79 | #[returns(ListAllowedResponse)] 80 | ListAllowed { 81 | start_after: Option, 82 | limit: Option, 83 | }, 84 | } 85 | 86 | #[cw_serde] 87 | pub struct ListChannelsResponse { 88 | pub channels: Vec, 89 | } 90 | 91 | #[cw_serde] 92 | pub struct ChannelResponse { 93 | /// Information on the channel's connection 94 | pub info: ChannelInfo, 95 | /// How many tokens we currently have pending over this channel 96 | pub balances: Vec, 97 | /// The total number of tokens that have been sent over this channel 98 | /// (even if many have been returned, so balance is low) 99 | pub total_sent: Vec, 100 | } 101 | 102 | #[cw_serde] 103 | pub struct PortResponse { 104 | pub port_id: String, 105 | } 106 | 107 | #[cw_serde] 108 | pub struct ConfigResponse { 109 | pub default_timeout: u64, 110 | pub default_gas_limit: Option, 111 | pub gov_contract: String, 112 | } 113 | 114 | #[cw_serde] 115 | pub struct AllowedResponse { 116 | pub is_allowed: bool, 117 | pub gas_limit: Option, 118 | } 119 | 120 | #[cw_serde] 121 | pub struct ListAllowedResponse { 122 | pub allow: Vec, 123 | } 124 | 125 | #[cw_serde] 126 | pub struct AllowedInfo { 127 | pub contract: String, 128 | pub gas_limit: Option, 129 | } 130 | -------------------------------------------------------------------------------- /contracts/cw20-ics20/src/state.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | use cosmwasm_std::{Addr, IbcEndpoint, StdResult, Storage, Uint128}; 3 | use cw_controllers::Admin; 4 | use cw_storage_plus::{Item, Map}; 5 | 6 | use crate::ContractError; 7 | 8 | pub const ADMIN: Admin = Admin::new("admin"); 9 | 10 | pub const CONFIG: Item = Item::new("ics20_config"); 11 | 12 | // Used to pass info from the ibc_packet_receive to the reply handler 13 | pub const REPLY_ARGS: Item = Item::new("reply_args"); 14 | 15 | /// static info on one channel that doesn't change 16 | pub const CHANNEL_INFO: Map<&str, ChannelInfo> = Map::new("channel_info"); 17 | 18 | /// indexed by (channel_id, denom) maintaining the balance of the channel in that currency 19 | pub const CHANNEL_STATE: Map<(&str, &str), ChannelState> = Map::new("channel_state"); 20 | 21 | /// Every cw20 contract we allow to be sent is stored here, possibly with a gas_limit 22 | pub const ALLOW_LIST: Map<&Addr, AllowInfo> = Map::new("allow_list"); 23 | 24 | #[cw_serde] 25 | #[derive(Default)] 26 | pub struct ChannelState { 27 | pub outstanding: Uint128, 28 | pub total_sent: Uint128, 29 | } 30 | 31 | #[cw_serde] 32 | pub struct Config { 33 | pub default_timeout: u64, 34 | pub default_gas_limit: Option, 35 | } 36 | 37 | #[cw_serde] 38 | pub struct ChannelInfo { 39 | /// id of this channel 40 | pub id: String, 41 | /// the remote channel/port we connect to 42 | pub counterparty_endpoint: IbcEndpoint, 43 | /// the connection this exists on (you can use to query client/consensus info) 44 | pub connection_id: String, 45 | } 46 | 47 | #[cw_serde] 48 | pub struct AllowInfo { 49 | pub gas_limit: Option, 50 | } 51 | 52 | #[cw_serde] 53 | pub struct ReplyArgs { 54 | pub channel: String, 55 | pub denom: String, 56 | pub amount: Uint128, 57 | } 58 | 59 | pub fn increase_channel_balance( 60 | storage: &mut dyn Storage, 61 | channel: &str, 62 | denom: &str, 63 | amount: Uint128, 64 | ) -> Result<(), ContractError> { 65 | CHANNEL_STATE.update(storage, (channel, denom), |orig| -> StdResult<_> { 66 | let mut state = orig.unwrap_or_default(); 67 | state.outstanding += amount; 68 | state.total_sent += amount; 69 | Ok(state) 70 | })?; 71 | Ok(()) 72 | } 73 | 74 | pub fn reduce_channel_balance( 75 | storage: &mut dyn Storage, 76 | channel: &str, 77 | denom: &str, 78 | amount: Uint128, 79 | ) -> Result<(), ContractError> { 80 | CHANNEL_STATE.update( 81 | storage, 82 | (channel, denom), 83 | |orig| -> Result<_, ContractError> { 84 | // this will return error if we don't have the funds there to cover the request (or no denom registered) 85 | let mut cur = orig.ok_or(ContractError::InsufficientFunds {})?; 86 | cur.outstanding = cur 87 | .outstanding 88 | .checked_sub(amount) 89 | .or(Err(ContractError::InsufficientFunds {}))?; 90 | Ok(cur) 91 | }, 92 | )?; 93 | Ok(()) 94 | } 95 | 96 | // this is like increase, but it only "un-subtracts" (= adds) outstanding, not total_sent 97 | // calling `reduce_channel_balance` and then `undo_reduce_channel_balance` should leave state unchanged. 98 | pub fn undo_reduce_channel_balance( 99 | storage: &mut dyn Storage, 100 | channel: &str, 101 | denom: &str, 102 | amount: Uint128, 103 | ) -> Result<(), ContractError> { 104 | CHANNEL_STATE.update(storage, (channel, denom), |orig| -> StdResult<_> { 105 | let mut state = orig.unwrap_or_default(); 106 | state.outstanding += amount; 107 | Ok(state) 108 | })?; 109 | Ok(()) 110 | } 111 | -------------------------------------------------------------------------------- /contracts/cw20-ics20/src/test_helpers.rs: -------------------------------------------------------------------------------- 1 | #![cfg(test)] 2 | 3 | use crate::contract::instantiate; 4 | use crate::ibc::{ibc_channel_connect, ibc_channel_open, ICS20_ORDERING, ICS20_VERSION}; 5 | use crate::state::ChannelInfo; 6 | 7 | use cosmwasm_std::testing::{ 8 | mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage, 9 | }; 10 | use cosmwasm_std::{ 11 | DepsMut, IbcChannel, IbcChannelConnectMsg, IbcChannelOpenMsg, IbcEndpoint, OwnedDeps, 12 | }; 13 | 14 | use crate::msg::{AllowMsg, InitMsg}; 15 | 16 | pub const DEFAULT_TIMEOUT: u64 = 3600; // 1 hour, 17 | pub const CONTRACT_PORT: &str = "ibc:wasm1234567890abcdef"; 18 | pub const REMOTE_PORT: &str = "transfer"; 19 | pub const CONNECTION_ID: &str = "connection-2"; 20 | 21 | pub fn mock_channel(channel_id: &str) -> IbcChannel { 22 | IbcChannel::new( 23 | IbcEndpoint { 24 | port_id: CONTRACT_PORT.into(), 25 | channel_id: channel_id.into(), 26 | }, 27 | IbcEndpoint { 28 | port_id: REMOTE_PORT.into(), 29 | channel_id: format!("{channel_id}5"), 30 | }, 31 | ICS20_ORDERING, 32 | ICS20_VERSION, 33 | CONNECTION_ID, 34 | ) 35 | } 36 | 37 | pub fn mock_channel_info(channel_id: &str) -> ChannelInfo { 38 | ChannelInfo { 39 | id: channel_id.to_string(), 40 | counterparty_endpoint: IbcEndpoint { 41 | port_id: REMOTE_PORT.into(), 42 | channel_id: format!("{channel_id}5"), 43 | }, 44 | connection_id: CONNECTION_ID.into(), 45 | } 46 | } 47 | 48 | // we simulate instantiate and ack here 49 | pub fn add_channel(mut deps: DepsMut, channel_id: &str) { 50 | let channel = mock_channel(channel_id); 51 | let open_msg = IbcChannelOpenMsg::new_init(channel.clone()); 52 | ibc_channel_open(deps.branch(), mock_env(), open_msg).unwrap(); 53 | let connect_msg = IbcChannelConnectMsg::new_ack(channel, ICS20_VERSION); 54 | ibc_channel_connect(deps.branch(), mock_env(), connect_msg).unwrap(); 55 | } 56 | 57 | pub fn setup( 58 | channels: &[&str], 59 | allow: &[(&str, u64)], 60 | ) -> OwnedDeps { 61 | let mut deps = mock_dependencies(); 62 | 63 | let allowlist = allow 64 | .iter() 65 | .map(|(contract, gas)| AllowMsg { 66 | contract: contract.to_string(), 67 | gas_limit: Some(*gas), 68 | }) 69 | .collect(); 70 | 71 | // instantiate an empty contract 72 | let instantiate_msg = InitMsg { 73 | default_gas_limit: None, 74 | default_timeout: DEFAULT_TIMEOUT, 75 | gov_contract: deps.api.addr_make("gov").to_string(), 76 | allowlist, 77 | }; 78 | let info = mock_info("anyone", &[]); 79 | let res = instantiate(deps.as_mut(), mock_env(), info, instantiate_msg).unwrap(); 80 | assert_eq!(0, res.messages.len()); 81 | 82 | for channel in channels { 83 | add_channel(deps.as_mut(), channel); 84 | } 85 | deps 86 | } 87 | -------------------------------------------------------------------------------- /contracts/cw3-fixed-multisig/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --lib --target wasm32-unknown-unknown" 3 | wasm-debug = "build --lib --target wasm32-unknown-unknown" 4 | unit-test = "test --lib" 5 | integration-test = "test --test integration" 6 | schema = "run --bin schema" 7 | -------------------------------------------------------------------------------- /contracts/cw3-fixed-multisig/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw3-fixed-multisig" 3 | version.workspace = true 4 | authors = ["Ethan Frey "] 5 | edition = "2021" 6 | description = "Implementing cw3 with an fixed group multisig" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cw-plus" 9 | homepage = "https://cosmwasm.com" 10 | documentation = "https://docs.cosmwasm.com" 11 | 12 | [lib] 13 | crate-type = ["cdylib", "rlib"] 14 | 15 | [features] 16 | # use library feature to disable all instantiate/execute/query exports 17 | library = [] 18 | 19 | [dependencies] 20 | cosmwasm-schema = { workspace = true } 21 | cw-utils = { workspace = true } 22 | cw2 = { workspace = true } 23 | cw3 = { workspace = true } 24 | cw-storage-plus = { workspace = true } 25 | cosmwasm-std = { workspace = true } 26 | schemars = { workspace = true } 27 | serde = { workspace = true } 28 | thiserror = { workspace = true } 29 | 30 | [dev-dependencies] 31 | cw20 = { workspace = true } 32 | cw20-base = { workspace = true } 33 | cw-multi-test = { workspace = true } 34 | easy-addr = { workspace = true } 35 | -------------------------------------------------------------------------------- /contracts/cw3-fixed-multisig/README.md: -------------------------------------------------------------------------------- 1 | # CW3 Fixed Multisig 2 | 3 | This is a simple implementation of the [cw3 spec](../../packages/cw3/README.md). 4 | It is a multisig with a fixed set of addresses created upon instatiation. 5 | Each address may have the same weight (K of N), or some may have extra voting 6 | power. This works much like the native Cosmos SDK multisig, except that rather 7 | than aggregating the signatures off chain and submitting the final result, 8 | we aggregate the approvals on-chain. 9 | 10 | This is usable as is, and probably the most secure implementation of cw3 11 | (as it is the simplest), but we will be adding more complex cases, such 12 | as updating the multisig set, different voting rules for the same group 13 | with different permissions, and even allow token-weighted voting. All through 14 | the same client interface. 15 | 16 | ## Instantiation 17 | 18 | To create the multisig, you must pass in a set of `HumanAddr` with a weight 19 | for each one, as well as a required weight to pass a proposal. To create 20 | a 2 of 3 multisig, pass 3 voters with weight 1 and a `required_weight` of 2. 21 | 22 | Note that 0 *is an allowed weight*. This doesn't give any voting rights, but 23 | it does allow that key to submit proposals that can later be approved by the 24 | voters. Any address not in the voter set cannot submit a proposal. 25 | 26 | ## Execution Process 27 | 28 | First, a registered voter must submit a proposal. This also includes the 29 | first "Yes" vote on the proposal by the proposer. The proposer can set 30 | an expiration time for the voting process, or it defaults to the limit 31 | provided when creating the contract (so proposals can be closed after several 32 | days). 33 | 34 | Before the proposal has expired, any voter with non-zero weight can add their 35 | vote. Only "Yes" votes are tallied. If enough "Yes" votes were submitted before 36 | the proposal expiration date, the status is set to "Passed". 37 | 38 | Once a proposal is "Passed", anyone may submit an "Execute" message. This will 39 | trigger the proposal to send all stored messages from the proposal and update 40 | it's state to "Executed", so it cannot run again. (Note if the execution fails 41 | for any reason - out of gas, insufficient funds, etc - the state update will 42 | be reverted, and it will remain "Passed", so you can try again). 43 | 44 | Once a proposal has expired without passing, anyone can submit a "Close" 45 | message to mark it closed. This has no effect beyond cleaning up the UI/database. 46 | 47 | ## Running this contract 48 | 49 | You will need Rust 1.44.1+ with `wasm32-unknown-unknown` target installed. 50 | 51 | You can run unit tests on this via: 52 | 53 | `cargo test` 54 | 55 | Once you are happy with the content, you can compile it to wasm via: 56 | 57 | ``` 58 | RUSTFLAGS='-C link-arg=-s' cargo wasm 59 | cp ../../target/wasm32-unknown-unknown/release/cw3_fixed_multisig.wasm . 60 | ls -l cw3_fixed_multisig.wasm 61 | sha256sum cw3_fixed_multisig.wasm 62 | ``` 63 | 64 | Or for a production-ready (optimized) build, run a build command in the 65 | repository root: https://github.com/CosmWasm/cw-plus#compiling. 66 | -------------------------------------------------------------------------------- /contracts/cw3-fixed-multisig/src/bin/schema.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::write_api; 2 | 3 | use cw3_fixed_multisig::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; 4 | 5 | fn main() { 6 | write_api! { 7 | instantiate: InstantiateMsg, 8 | execute: ExecuteMsg, 9 | query: QueryMsg, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /contracts/cw3-fixed-multisig/src/error.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::StdError; 2 | use cw_utils::ThresholdError; 3 | 4 | use thiserror::Error; 5 | 6 | #[derive(Error, Debug, PartialEq)] 7 | pub enum ContractError { 8 | #[error("{0}")] 9 | Std(#[from] StdError), 10 | 11 | #[error("{0}")] 12 | Threshold(#[from] ThresholdError), 13 | 14 | #[error("Required weight cannot be zero")] 15 | ZeroWeight {}, 16 | 17 | #[error("Not possible to reach required (passing) weight")] 18 | UnreachableWeight {}, 19 | 20 | #[error("No voters")] 21 | NoVoters {}, 22 | 23 | #[error("Unauthorized")] 24 | Unauthorized {}, 25 | 26 | #[error("Proposal is not open")] 27 | NotOpen {}, 28 | 29 | #[error("Proposal voting period has expired")] 30 | Expired {}, 31 | 32 | #[error("Proposal must expire before you can close it")] 33 | NotExpired {}, 34 | 35 | #[error("Wrong expiration option")] 36 | WrongExpiration {}, 37 | 38 | #[error("Already voted on this proposal")] 39 | AlreadyVoted {}, 40 | 41 | #[error("Proposal must have passed and not yet been executed")] 42 | WrongExecuteStatus {}, 43 | 44 | #[error("Cannot close completed or passed proposals")] 45 | WrongCloseStatus {}, 46 | } 47 | -------------------------------------------------------------------------------- /contracts/cw3-fixed-multisig/src/integration_tests.rs: -------------------------------------------------------------------------------- 1 | #![cfg(test)] 2 | 3 | use cosmwasm_std::{to_json_binary, Empty, Uint128, WasmMsg}; 4 | use cw20::{BalanceResponse, MinterResponse}; 5 | use cw20_base::msg::QueryMsg; 6 | use cw3::Vote; 7 | use cw_multi_test::{App, Contract, ContractWrapper, Executor}; 8 | use cw_utils::{Duration, Threshold}; 9 | 10 | use crate::contract::{execute, instantiate, query}; 11 | use crate::msg::{ExecuteMsg, InstantiateMsg, Voter}; 12 | 13 | fn mock_app() -> App { 14 | App::default() 15 | } 16 | 17 | pub fn contract_cw3_fixed_multisig() -> Box> { 18 | let contract = ContractWrapper::new(execute, instantiate, query); 19 | Box::new(contract) 20 | } 21 | 22 | pub fn contract_cw20() -> Box> { 23 | let contract = ContractWrapper::new( 24 | cw20_base::contract::execute, 25 | cw20_base::contract::instantiate, 26 | cw20_base::contract::query, 27 | ); 28 | Box::new(contract) 29 | } 30 | 31 | #[test] 32 | // cw3 multisig account can control cw20 admin actions 33 | fn cw3_controls_cw20() { 34 | let mut router = mock_app(); 35 | 36 | // setup cw3 multisig with 3 accounts 37 | let cw3_id = router.store_code(contract_cw3_fixed_multisig()); 38 | 39 | let addr1 = router.api().addr_make("addr1"); 40 | let addr2 = router.api().addr_make("addr2"); 41 | let addr3 = router.api().addr_make("addr3"); 42 | let cw3_instantiate_msg = InstantiateMsg { 43 | voters: vec![ 44 | Voter { 45 | addr: addr1.to_string(), 46 | weight: 1, 47 | }, 48 | Voter { 49 | addr: addr2.to_string(), 50 | weight: 1, 51 | }, 52 | Voter { 53 | addr: addr3.to_string(), 54 | weight: 1, 55 | }, 56 | ], 57 | threshold: Threshold::AbsoluteCount { weight: 2 }, 58 | max_voting_period: Duration::Height(3), 59 | }; 60 | 61 | let multisig_addr = router 62 | .instantiate_contract( 63 | cw3_id, 64 | addr1.clone(), 65 | &cw3_instantiate_msg, 66 | &[], 67 | "Consortium", 68 | None, 69 | ) 70 | .unwrap(); 71 | 72 | // setup cw20 as cw3 multisig admin 73 | let cw20_id = router.store_code(contract_cw20()); 74 | 75 | let cw20_instantiate_msg = cw20_base::msg::InstantiateMsg { 76 | name: "Consortium Token".parse().unwrap(), 77 | symbol: "CST".parse().unwrap(), 78 | decimals: 6, 79 | initial_balances: vec![], 80 | mint: Some(MinterResponse { 81 | minter: multisig_addr.to_string(), 82 | cap: None, 83 | }), 84 | marketing: None, 85 | }; 86 | let cw20_addr = router 87 | .instantiate_contract( 88 | cw20_id, 89 | multisig_addr.clone(), 90 | &cw20_instantiate_msg, 91 | &[], 92 | "Consortium", 93 | None, 94 | ) 95 | .unwrap(); 96 | 97 | // mint some cw20 tokens according to proposal result 98 | let mint_recipient = router.api().addr_make("mint_recipient"); 99 | let mint_amount = Uint128::new(1000); 100 | let cw20_mint_msg = cw20_base::msg::ExecuteMsg::Mint { 101 | recipient: mint_recipient.to_string(), 102 | amount: mint_amount, 103 | }; 104 | 105 | let execute_mint_msg = WasmMsg::Execute { 106 | contract_addr: cw20_addr.to_string(), 107 | msg: to_json_binary(&cw20_mint_msg).unwrap(), 108 | funds: vec![], 109 | }; 110 | let propose_msg = ExecuteMsg::Propose { 111 | title: "Mint tokens".to_string(), 112 | description: "Need to mint tokens".to_string(), 113 | msgs: vec![execute_mint_msg.into()], 114 | latest: None, 115 | }; 116 | // propose mint 117 | router 118 | .execute_contract(addr1.clone(), multisig_addr.clone(), &propose_msg, &[]) 119 | .unwrap(); 120 | 121 | // second votes 122 | let vote2_msg = ExecuteMsg::Vote { 123 | proposal_id: 1, 124 | vote: Vote::Yes, 125 | }; 126 | router 127 | .execute_contract(addr2, multisig_addr.clone(), &vote2_msg, &[]) 128 | .unwrap(); 129 | 130 | // only 1 vote and msg mint fails 131 | let execute_proposal_msg = ExecuteMsg::Execute { proposal_id: 1 }; 132 | // execute mint 133 | router 134 | .execute_contract(addr1, multisig_addr, &execute_proposal_msg, &[]) 135 | .unwrap(); 136 | 137 | // check the mint is successful 138 | let cw20_balance_query = QueryMsg::Balance { 139 | address: mint_recipient.to_string(), 140 | }; 141 | let balance: BalanceResponse = router 142 | .wrap() 143 | .query_wasm_smart(&cw20_addr, &cw20_balance_query) 144 | .unwrap(); 145 | 146 | // compare minted amount 147 | assert_eq!(balance.balance, mint_amount); 148 | } 149 | -------------------------------------------------------------------------------- /contracts/cw3-fixed-multisig/src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | This is a simple implementation of the [cw3 spec](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw3/README.md). 3 | It is a multisig with a fixed set of addresses created upon instatiation. 4 | Each address may have the same weight (K of N), or some may have extra voting 5 | power. This works much like the native Cosmos SDK multisig, except that rather 6 | than aggregating the signatures off chain and submitting the final result, 7 | we aggregate the approvals on-chain. 8 | 9 | This is usable as is, and probably the most secure implementation of cw3 10 | (as it is the simplest), but we will be adding more complex cases, such 11 | as updating the multisig set, different voting rules for the same group 12 | with different permissions, and even allow token-weighted voting. All through 13 | the same client interface. 14 | 15 | For more information on this contract, please check out the 16 | [README](https://github.com/CosmWasm/cw-plus/blob/main/contracts/cw3-fixed-multisig/README.md). 17 | */ 18 | 19 | pub mod contract; 20 | mod error; 21 | mod integration_tests; 22 | pub mod msg; 23 | pub mod state; 24 | 25 | pub use crate::error::ContractError; 26 | -------------------------------------------------------------------------------- /contracts/cw3-fixed-multisig/src/msg.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::{cw_serde, QueryResponses}; 2 | use cosmwasm_std::{CosmosMsg, Empty}; 3 | use cw3::Vote; 4 | use cw_utils::{Duration, Expiration, Threshold}; 5 | 6 | #[cw_serde] 7 | pub struct InstantiateMsg { 8 | pub voters: Vec, 9 | pub threshold: Threshold, 10 | pub max_voting_period: Duration, 11 | } 12 | 13 | #[cw_serde] 14 | pub struct Voter { 15 | pub addr: String, 16 | pub weight: u64, 17 | } 18 | 19 | // TODO: add some T variants? Maybe good enough as fixed Empty for now 20 | #[cw_serde] 21 | pub enum ExecuteMsg { 22 | Propose { 23 | title: String, 24 | description: String, 25 | msgs: Vec>, 26 | // note: we ignore API-spec'd earliest if passed, always opens immediately 27 | latest: Option, 28 | }, 29 | Vote { 30 | proposal_id: u64, 31 | vote: Vote, 32 | }, 33 | Execute { 34 | proposal_id: u64, 35 | }, 36 | Close { 37 | proposal_id: u64, 38 | }, 39 | } 40 | 41 | // We can also add this as a cw3 extension 42 | #[cw_serde] 43 | #[derive(QueryResponses)] 44 | pub enum QueryMsg { 45 | #[returns(cw_utils::ThresholdResponse)] 46 | Threshold {}, 47 | #[returns(cw3::ProposalResponse)] 48 | Proposal { proposal_id: u64 }, 49 | #[returns(cw3::ProposalListResponse)] 50 | ListProposals { 51 | start_after: Option, 52 | limit: Option, 53 | }, 54 | #[returns(cw3::ProposalListResponse)] 55 | ReverseProposals { 56 | start_before: Option, 57 | limit: Option, 58 | }, 59 | #[returns(cw3::VoteResponse)] 60 | Vote { proposal_id: u64, voter: String }, 61 | #[returns(cw3::VoteListResponse)] 62 | ListVotes { 63 | proposal_id: u64, 64 | start_after: Option, 65 | limit: Option, 66 | }, 67 | #[returns(cw3::VoterResponse)] 68 | Voter { address: String }, 69 | #[returns(cw3::VoterListResponse)] 70 | ListVoters { 71 | start_after: Option, 72 | limit: Option, 73 | }, 74 | } 75 | -------------------------------------------------------------------------------- /contracts/cw3-fixed-multisig/src/state.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | use cosmwasm_std::{Addr, StdResult, Storage}; 3 | 4 | use cw3::{Ballot, Proposal}; 5 | use cw_storage_plus::{Item, Map}; 6 | use cw_utils::{Duration, Threshold}; 7 | 8 | #[cw_serde] 9 | pub struct Config { 10 | pub threshold: Threshold, 11 | pub total_weight: u64, 12 | pub max_voting_period: Duration, 13 | } 14 | 15 | // unique items 16 | pub const CONFIG: Item = Item::new("config"); 17 | pub const PROPOSAL_COUNT: Item = Item::new("proposal_count"); 18 | 19 | // multiple-item map 20 | pub const BALLOTS: Map<(u64, &Addr), Ballot> = Map::new("votes"); 21 | pub const PROPOSALS: Map = Map::new("proposals"); 22 | 23 | // multiple-item maps 24 | pub const VOTERS: Map<&Addr, u64> = Map::new("voters"); 25 | 26 | pub fn next_id(store: &mut dyn Storage) -> StdResult { 27 | let id: u64 = PROPOSAL_COUNT.may_load(store)?.unwrap_or_default() + 1; 28 | PROPOSAL_COUNT.save(store, &id)?; 29 | Ok(id) 30 | } 31 | -------------------------------------------------------------------------------- /contracts/cw3-flex-multisig/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --lib --target wasm32-unknown-unknown" 3 | wasm-debug = "build --lib --target wasm32-unknown-unknown" 4 | unit-test = "test --lib" 5 | integration-test = "test --test integration" 6 | schema = "run --bin schema" 7 | -------------------------------------------------------------------------------- /contracts/cw3-flex-multisig/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw3-flex-multisig" 3 | version.workspace = true 4 | authors = ["Ethan Frey "] 5 | edition = "2021" 6 | description = "Implementing cw3 with multiple voting patterns and dynamic groups" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cw-plus" 9 | homepage = "https://cosmwasm.com" 10 | documentation = "https://docs.cosmwasm.com" 11 | 12 | [lib] 13 | crate-type = ["cdylib", "rlib"] 14 | 15 | [features] 16 | # use library feature to disable all instantiate/execute/query exports 17 | library = [] 18 | 19 | [dependencies] 20 | cosmwasm-schema = { workspace = true } 21 | cw-utils = { workspace = true } 22 | cw2 = { workspace = true } 23 | cw3 = { workspace = true } 24 | cw3-fixed-multisig = { workspace = true } 25 | cw4 = { workspace = true } 26 | cw20 = { workspace = true } 27 | cw-storage-plus = { workspace = true } 28 | cosmwasm-std = { workspace = true } 29 | schemars = { workspace = true } 30 | serde = { workspace = true } 31 | thiserror = { workspace = true } 32 | 33 | [dev-dependencies] 34 | cw4-group = { workspace = true } 35 | cw-multi-test = { workspace = true } 36 | cw20-base = { workspace = true } 37 | easy-addr = { workspace = true } 38 | -------------------------------------------------------------------------------- /contracts/cw3-flex-multisig/README.md: -------------------------------------------------------------------------------- 1 | # CW3 Flexible Multisig 2 | 3 | This builds on [cw3-fixed-multisig](../cw3-fixed-multisig) with a more 4 | powerful implementation of the [cw3 spec](../../packages/cw3/README.md). 5 | It is a multisig contract that is backed by a 6 | [cw4 (group)](../../packages/cw4/README.md) contract, which independently 7 | maintains the voter set. 8 | 9 | This provides 2 main advantages: 10 | 11 | * You can create two different multisigs with different voting thresholds 12 | backed by the same group. Thus, you can have a 50% vote, and a 67% vote 13 | that always use the same voter set, but can take other actions. 14 | * TODO: It allows dynamic multisig groups. Since the group can change, 15 | we can set one of the multisigs as the admin of the group contract, 16 | and the 17 | 18 | 19 | In addition to the dynamic voting set, the main difference with the native 20 | Cosmos SDK multisig, is that it aggregates the signatures on chain, with 21 | visible proposals (like `x/gov` in the Cosmos SDK), rather than requiring 22 | signers to share signatures off chain. 23 | 24 | ## Instantiation 25 | 26 | The first step to create such a multisig is to instantiate a cw4 contract 27 | with the desired member set. For now, this only is supported by 28 | [cw4-group](../cw4-group), but we will add a token-weighted group contract 29 | (TODO). 30 | 31 | If you create a `cw4-group` contract and want a multisig to be able 32 | to modify its own group, do the following in multiple transactions: 33 | 34 | * instantiate cw4-group, with your personal key as admin 35 | * instantiate a multisig pointing to the group 36 | * `AddHook{multisig}` on the group contract 37 | * `UpdateAdmin{multisig}` on the group contract 38 | 39 | This is the current practice to create such circular dependencies, 40 | and depends on an external driver (hard to impossible to script such a 41 | self-deploying contract on-chain). (TODO: document better). 42 | 43 | When creating the multisig, you must set the required weight to pass a vote 44 | as well as the max/default voting period. (TODO: allow more threshold types) 45 | 46 | ## Execution Process 47 | 48 | First, a registered voter must submit a proposal. This also includes the 49 | first "Yes" vote on the proposal by the proposer. The proposer can set 50 | an expiration time for the voting process, or it defaults to the limit 51 | provided when creating the contract (so proposals can be closed after several 52 | days). 53 | 54 | Before the proposal has expired, any voter with non-zero weight can add their 55 | vote. Only "Yes" votes are tallied. If enough "Yes" votes were submitted before 56 | the proposal expiration date, the status is set to "Passed". 57 | 58 | Once a proposal is "Passed", anyone may submit an "Execute" message. This will 59 | trigger the proposal to send all stored messages from the proposal and update 60 | it's state to "Executed", so it cannot run again. (Note if the execution fails 61 | for any reason - out of gas, insufficient funds, etc - the state update will 62 | be reverted, and it will remain "Passed", so you can try again). 63 | 64 | Once a proposal has expired without passing, anyone can submit a "Close" 65 | message to mark it closed. This has no effect beyond cleaning up the UI/database. 66 | 67 | ## Running this contract 68 | 69 | You will need Rust 1.44.1+ with `wasm32-unknown-unknown` target installed. 70 | 71 | You can run unit tests on this via: 72 | 73 | `cargo test` 74 | 75 | Once you are happy with the content, you can compile it to wasm via: 76 | 77 | ``` 78 | RUSTFLAGS='-C link-arg=-s' cargo wasm 79 | cp ../../target/wasm32-unknown-unknown/release/cw3_flex_multisig.wasm . 80 | ls -l cw3_flex_multisig.wasm 81 | sha256sum cw3_flex_multisig.wasm 82 | ``` 83 | 84 | Or for a production-ready (optimized) build, run a build command in 85 | the repository root: https://github.com/CosmWasm/cw-plus#compiling. 86 | -------------------------------------------------------------------------------- /contracts/cw3-flex-multisig/src/bin/schema.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::write_api; 2 | 3 | use cw3_flex_multisig::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; 4 | 5 | fn main() { 6 | write_api! { 7 | instantiate: InstantiateMsg, 8 | execute: ExecuteMsg, 9 | query: QueryMsg, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /contracts/cw3-flex-multisig/src/error.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::StdError; 2 | use cw3::DepositError; 3 | use cw_utils::{PaymentError, ThresholdError}; 4 | 5 | use thiserror::Error; 6 | 7 | #[derive(Error, Debug, PartialEq)] 8 | pub enum ContractError { 9 | #[error("{0}")] 10 | Std(#[from] StdError), 11 | 12 | #[error("{0}")] 13 | Threshold(#[from] ThresholdError), 14 | 15 | #[error("Group contract invalid address '{addr}'")] 16 | InvalidGroup { addr: String }, 17 | 18 | #[error("Unauthorized")] 19 | Unauthorized {}, 20 | 21 | #[error("Proposal is not open")] 22 | NotOpen {}, 23 | 24 | #[error("Proposal voting period has expired")] 25 | Expired {}, 26 | 27 | #[error("Proposal must expire before you can close it")] 28 | NotExpired {}, 29 | 30 | #[error("Wrong expiration option")] 31 | WrongExpiration {}, 32 | 33 | #[error("Already voted on this proposal")] 34 | AlreadyVoted {}, 35 | 36 | #[error("Proposal must have passed and not yet been executed")] 37 | WrongExecuteStatus {}, 38 | 39 | #[error("Cannot close completed or passed proposals")] 40 | WrongCloseStatus {}, 41 | 42 | #[error("{0}")] 43 | Payment(#[from] PaymentError), 44 | 45 | #[error("{0}")] 46 | Deposit(#[from] DepositError), 47 | } 48 | -------------------------------------------------------------------------------- /contracts/cw3-flex-multisig/src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | This builds on [`cw3_fixed_multisig`] with a more 3 | powerful implementation of the [cw3 spec](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw3/README.md). 4 | It is a multisig contract that is backed by a 5 | [cw4 (group)](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw4/README.md) contract, which independently 6 | maintains the voter set. 7 | 8 | This provides 2 main advantages: 9 | 10 | * You can create two different multisigs with different voting thresholds 11 | backed by the same group. Thus, you can have a 50% vote, and a 67% vote 12 | that always use the same voter set, but can take other actions. 13 | * TODO: It allows dynamic multisig groups. 14 | 15 | 16 | In addition to the dynamic voting set, the main difference with the native 17 | Cosmos SDK multisig, is that it aggregates the signatures on chain, with 18 | visible proposals (like `x/gov` in the Cosmos SDK), rather than requiring 19 | signers to share signatures off chain. 20 | 21 | For more information on this contract, please check out the 22 | [README](https://github.com/CosmWasm/cw-plus/blob/main/contracts/cw3-flex-multisig/README.md). 23 | */ 24 | 25 | pub mod contract; 26 | pub mod error; 27 | pub mod msg; 28 | pub mod state; 29 | 30 | pub use crate::error::ContractError; 31 | -------------------------------------------------------------------------------- /contracts/cw3-flex-multisig/src/msg.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::{cw_serde, QueryResponses}; 2 | use cosmwasm_std::{CosmosMsg, Empty}; 3 | use cw3::{UncheckedDepositInfo, Vote}; 4 | use cw4::MemberChangedHookMsg; 5 | use cw_utils::{Duration, Expiration, Threshold}; 6 | 7 | use crate::state::Executor; 8 | 9 | #[cw_serde] 10 | pub struct InstantiateMsg { 11 | // this is the group contract that contains the member list 12 | pub group_addr: String, 13 | pub threshold: Threshold, 14 | pub max_voting_period: Duration, 15 | // who is able to execute passed proposals 16 | // None means that anyone can execute 17 | pub executor: Option, 18 | /// The cost of creating a proposal (if any). 19 | pub proposal_deposit: Option, 20 | } 21 | 22 | // TODO: add some T variants? Maybe good enough as fixed Empty for now 23 | #[cw_serde] 24 | pub enum ExecuteMsg { 25 | Propose { 26 | title: String, 27 | description: String, 28 | msgs: Vec>, 29 | // note: we ignore API-spec'd earliest if passed, always opens immediately 30 | latest: Option, 31 | }, 32 | Vote { 33 | proposal_id: u64, 34 | vote: Vote, 35 | }, 36 | Execute { 37 | proposal_id: u64, 38 | }, 39 | Close { 40 | proposal_id: u64, 41 | }, 42 | /// Handles update hook messages from the group contract 43 | MemberChangedHook(MemberChangedHookMsg), 44 | } 45 | 46 | // We can also add this as a cw3 extension 47 | #[cw_serde] 48 | #[derive(QueryResponses)] 49 | pub enum QueryMsg { 50 | #[returns(cw_utils::ThresholdResponse)] 51 | Threshold {}, 52 | #[returns(cw3::ProposalResponse)] 53 | Proposal { proposal_id: u64 }, 54 | #[returns(cw3::ProposalListResponse)] 55 | ListProposals { 56 | start_after: Option, 57 | limit: Option, 58 | }, 59 | #[returns(cw3::ProposalListResponse)] 60 | ReverseProposals { 61 | start_before: Option, 62 | limit: Option, 63 | }, 64 | #[returns(cw3::VoteResponse)] 65 | Vote { proposal_id: u64, voter: String }, 66 | #[returns(cw3::VoteListResponse)] 67 | ListVotes { 68 | proposal_id: u64, 69 | start_after: Option, 70 | limit: Option, 71 | }, 72 | #[returns(cw3::VoterResponse)] 73 | Voter { address: String }, 74 | #[returns(cw3::VoterListResponse)] 75 | ListVoters { 76 | start_after: Option, 77 | limit: Option, 78 | }, 79 | /// Gets the current configuration. 80 | #[returns(crate::state::Config)] 81 | Config {}, 82 | } 83 | -------------------------------------------------------------------------------- /contracts/cw3-flex-multisig/src/state.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | use cosmwasm_std::{Addr, QuerierWrapper}; 3 | use cw3::DepositInfo; 4 | use cw4::Cw4Contract; 5 | use cw_storage_plus::Item; 6 | use cw_utils::{Duration, Threshold}; 7 | 8 | use crate::error::ContractError; 9 | 10 | /// Defines who is able to execute proposals once passed 11 | #[cw_serde] 12 | pub enum Executor { 13 | /// Any member of the voting group, even with 0 points 14 | Member, 15 | /// Only the given address 16 | Only(Addr), 17 | } 18 | 19 | #[cw_serde] 20 | pub struct Config { 21 | pub threshold: Threshold, 22 | pub max_voting_period: Duration, 23 | // Total weight and voters are queried from this contract 24 | pub group_addr: Cw4Contract, 25 | // who is able to execute passed proposals 26 | // None means that anyone can execute 27 | pub executor: Option, 28 | /// The price, if any, of creating a new proposal. 29 | pub proposal_deposit: Option, 30 | } 31 | 32 | impl Config { 33 | // Executor can be set in 3 ways: 34 | // - Member: any member of the voting group is authorized 35 | // - Only: only passed address is authorized 36 | // - None: Everyone are authorized 37 | pub fn authorize(&self, querier: &QuerierWrapper, sender: &Addr) -> Result<(), ContractError> { 38 | if let Some(executor) = &self.executor { 39 | match executor { 40 | Executor::Member => { 41 | self.group_addr 42 | .is_member(querier, sender, None)? 43 | .ok_or(ContractError::Unauthorized {})?; 44 | } 45 | Executor::Only(addr) => { 46 | if addr != sender { 47 | return Err(ContractError::Unauthorized {}); 48 | } 49 | } 50 | } 51 | } 52 | Ok(()) 53 | } 54 | } 55 | 56 | // unique items 57 | pub const CONFIG: Item = Item::new("config"); 58 | -------------------------------------------------------------------------------- /contracts/cw4-group/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --lib --target wasm32-unknown-unknown" 3 | wasm-debug = "build --lib --target wasm32-unknown-unknown" 4 | unit-test = "test --lib" 5 | schema = "run --bin schema" 6 | -------------------------------------------------------------------------------- /contracts/cw4-group/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw4-group" 3 | version.workspace = true 4 | authors = ["Ethan Frey "] 5 | edition = "2021" 6 | description = "Simple cw4 implementation of group membership controlled by admin " 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cw-plus" 9 | homepage = "https://cosmwasm.com" 10 | documentation = "https://docs.cosmwasm.com" 11 | 12 | exclude = [ 13 | # 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. 14 | "artifacts/*", 15 | ] 16 | 17 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 18 | 19 | [lib] 20 | crate-type = ["cdylib", "rlib"] 21 | 22 | [features] 23 | # use library feature to disable all instantiate/execute/query exports 24 | library = [] 25 | 26 | [dependencies] 27 | cosmwasm-schema = { workspace = true } 28 | cw-utils = { workspace = true } 29 | cw2 = { workspace = true } 30 | cw4 = { workspace = true } 31 | cw-controllers = { workspace = true } 32 | cw-storage-plus = { workspace = true } 33 | cosmwasm-std = { workspace = true } 34 | schemars = { workspace = true } 35 | serde = { workspace = true } 36 | thiserror = { workspace = true } 37 | 38 | [dev-dependencies] 39 | easy-addr = { workspace = true } 40 | -------------------------------------------------------------------------------- /contracts/cw4-group/README.md: -------------------------------------------------------------------------------- 1 | # CW4 Group 2 | 3 | This is a basic implementation of the [cw4 spec](../../packages/cw4/README.md). 4 | It fulfills all elements of the spec, including the raw query lookups, 5 | and it designed to be used as a backing storage for 6 | [cw3 compliant contracts](../../packages/cw3/README.md). 7 | 8 | It stores a set of members along with an admin, and allows the admin to 9 | update the state. Raw queries (intended for cross-contract queries) 10 | can check a given member address and the total weight. Smart queries (designed 11 | for client API) can do the same, and also query the admin address as well as 12 | paginate over all members. 13 | 14 | ## Init 15 | 16 | To create it, you must pass in a list of members, as well as an optional 17 | `admin`, if you wish it to be mutable. 18 | 19 | ```rust 20 | pub struct InitMsg { 21 | pub admin: Option, 22 | pub members: Vec, 23 | } 24 | 25 | pub struct Member { 26 | pub addr: HumanAddr, 27 | pub weight: u64, 28 | } 29 | ``` 30 | 31 | Members are defined by an address and a weight. This is transformed 32 | and stored under their `CanonicalAddr`, in a format defined in 33 | [cw4 raw queries](../../packages/cw4/README.md#raw). 34 | 35 | Note that 0 *is an allowed weight*. This doesn't give any voting rights, but 36 | it does define this address is part of the group. This could be used in 37 | e.g. a KYC whitelist to say they are allowed, but cannot participate in 38 | decision-making. 39 | 40 | ## Messages 41 | 42 | Basic update messages, queries, and hooks are defined by the 43 | [cw4 spec](../../packages/cw4/README.md). Please refer to it for more info. 44 | 45 | `cw4-group` adds one message to control the group membership: 46 | 47 | `UpdateMembers{add, remove}` - takes a membership diff and adds/updates the 48 | members, as well as removing any provided addresses. If an address is on both 49 | lists, it will be removed. If it appears multiple times in `add`, only the 50 | last occurrence will be used. 51 | 52 | -------------------------------------------------------------------------------- /contracts/cw4-group/src/bin/schema.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::write_api; 2 | 3 | use cw4_group::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; 4 | 5 | fn main() { 6 | write_api! { 7 | instantiate: InstantiateMsg, 8 | execute: ExecuteMsg, 9 | query: QueryMsg, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /contracts/cw4-group/src/contract.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(feature = "library"))] 2 | use cosmwasm_std::entry_point; 3 | use cosmwasm_std::{ 4 | attr, to_json_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, 5 | StdResult, SubMsg, Uint64, 6 | }; 7 | use cw2::set_contract_version; 8 | use cw4::{ 9 | Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, MemberResponse, 10 | TotalWeightResponse, 11 | }; 12 | use cw_storage_plus::Bound; 13 | use cw_utils::maybe_addr; 14 | 15 | use crate::error::ContractError; 16 | use crate::helpers::validate_unique_members; 17 | use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; 18 | use crate::state::{ADMIN, HOOKS, MEMBERS, TOTAL}; 19 | 20 | // version info for migration info 21 | const CONTRACT_NAME: &str = "crates.io:cw4-group"; 22 | const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); 23 | 24 | // Note, you can use StdResult in some functions where you do not 25 | // make use of the custom errors 26 | #[cfg_attr(not(feature = "library"), entry_point)] 27 | pub fn instantiate( 28 | deps: DepsMut, 29 | env: Env, 30 | _info: MessageInfo, 31 | msg: InstantiateMsg, 32 | ) -> Result { 33 | set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; 34 | create(deps, msg.admin, msg.members, env.block.height)?; 35 | Ok(Response::default()) 36 | } 37 | 38 | // create is the instantiation logic with set_contract_version removed so it can more 39 | // easily be imported in other contracts 40 | pub fn create( 41 | mut deps: DepsMut, 42 | admin: Option, 43 | mut members: Vec, 44 | height: u64, 45 | ) -> Result<(), ContractError> { 46 | validate_unique_members(&mut members)?; 47 | let members = members; // let go of mutability 48 | 49 | let admin_addr = admin 50 | .map(|admin| deps.api.addr_validate(&admin)) 51 | .transpose()?; 52 | ADMIN.set(deps.branch(), admin_addr)?; 53 | 54 | let mut total = Uint64::zero(); 55 | for member in members.into_iter() { 56 | let member_weight = Uint64::from(member.weight); 57 | total = total.checked_add(member_weight)?; 58 | let member_addr = deps.api.addr_validate(&member.addr)?; 59 | MEMBERS.save(deps.storage, &member_addr, &member_weight.u64(), height)?; 60 | } 61 | TOTAL.save(deps.storage, &total.u64(), height)?; 62 | 63 | Ok(()) 64 | } 65 | 66 | // And declare a custom Error variant for the ones where you will want to make use of it 67 | #[cfg_attr(not(feature = "library"), entry_point)] 68 | pub fn execute( 69 | deps: DepsMut, 70 | env: Env, 71 | info: MessageInfo, 72 | msg: ExecuteMsg, 73 | ) -> Result { 74 | let api = deps.api; 75 | match msg { 76 | ExecuteMsg::UpdateAdmin { admin } => Ok(ADMIN.execute_update_admin( 77 | deps, 78 | info, 79 | admin.map(|admin| api.addr_validate(&admin)).transpose()?, 80 | )?), 81 | ExecuteMsg::UpdateMembers { add, remove } => { 82 | execute_update_members(deps, env, info, add, remove) 83 | } 84 | ExecuteMsg::AddHook { addr } => { 85 | Ok(HOOKS.execute_add_hook(&ADMIN, deps, info, api.addr_validate(&addr)?)?) 86 | } 87 | ExecuteMsg::RemoveHook { addr } => { 88 | Ok(HOOKS.execute_remove_hook(&ADMIN, deps, info, api.addr_validate(&addr)?)?) 89 | } 90 | } 91 | } 92 | 93 | pub fn execute_update_members( 94 | mut deps: DepsMut, 95 | env: Env, 96 | info: MessageInfo, 97 | add: Vec, 98 | remove: Vec, 99 | ) -> Result { 100 | let attributes = vec![ 101 | attr("action", "update_members"), 102 | attr("added", add.len().to_string()), 103 | attr("removed", remove.len().to_string()), 104 | attr("sender", &info.sender), 105 | ]; 106 | 107 | // make the local update 108 | let diff = update_members(deps.branch(), env.block.height, info.sender, add, remove)?; 109 | // call all registered hooks 110 | let messages = HOOKS.prepare_hooks(deps.storage, |h| { 111 | diff.clone().into_cosmos_msg(h).map(SubMsg::new) 112 | })?; 113 | Ok(Response::new() 114 | .add_submessages(messages) 115 | .add_attributes(attributes)) 116 | } 117 | 118 | // the logic from execute_update_members extracted for easier import 119 | pub fn update_members( 120 | deps: DepsMut, 121 | height: u64, 122 | sender: Addr, 123 | mut to_add: Vec, 124 | to_remove: Vec, 125 | ) -> Result { 126 | validate_unique_members(&mut to_add)?; 127 | let to_add = to_add; // let go of mutability 128 | 129 | ADMIN.assert_admin(deps.as_ref(), &sender)?; 130 | 131 | let mut total = Uint64::from(TOTAL.load(deps.storage)?); 132 | let mut diffs: Vec = vec![]; 133 | 134 | // add all new members and update total 135 | for add in to_add.into_iter() { 136 | let add_addr = deps.api.addr_validate(&add.addr)?; 137 | MEMBERS.update(deps.storage, &add_addr, height, |old| -> StdResult<_> { 138 | total = total.checked_sub(Uint64::from(old.unwrap_or_default()))?; 139 | total = total.checked_add(Uint64::from(add.weight))?; 140 | diffs.push(MemberDiff::new(add.addr, old, Some(add.weight))); 141 | Ok(add.weight) 142 | })?; 143 | } 144 | 145 | for remove in to_remove.into_iter() { 146 | let remove_addr = deps.api.addr_validate(&remove)?; 147 | let old = MEMBERS.may_load(deps.storage, &remove_addr)?; 148 | // Only process this if they were actually in the list before 149 | if let Some(weight) = old { 150 | diffs.push(MemberDiff::new(remove, Some(weight), None)); 151 | total = total.checked_sub(Uint64::from(weight))?; 152 | MEMBERS.remove(deps.storage, &remove_addr, height)?; 153 | } 154 | } 155 | 156 | TOTAL.save(deps.storage, &total.u64(), height)?; 157 | Ok(MemberChangedHookMsg { diffs }) 158 | } 159 | 160 | #[cfg_attr(not(feature = "library"), entry_point)] 161 | pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { 162 | match msg { 163 | QueryMsg::Member { 164 | addr, 165 | at_height: height, 166 | } => to_json_binary(&query_member(deps, addr, height)?), 167 | QueryMsg::ListMembers { start_after, limit } => { 168 | to_json_binary(&query_list_members(deps, start_after, limit)?) 169 | } 170 | QueryMsg::TotalWeight { at_height: height } => { 171 | to_json_binary(&query_total_weight(deps, height)?) 172 | } 173 | QueryMsg::Admin {} => to_json_binary(&ADMIN.query_admin(deps)?), 174 | QueryMsg::Hooks {} => to_json_binary(&HOOKS.query_hooks(deps)?), 175 | } 176 | } 177 | 178 | pub fn query_total_weight(deps: Deps, height: Option) -> StdResult { 179 | let weight = match height { 180 | Some(h) => TOTAL.may_load_at_height(deps.storage, h), 181 | None => TOTAL.may_load(deps.storage), 182 | }? 183 | .unwrap_or_default(); 184 | Ok(TotalWeightResponse { weight }) 185 | } 186 | 187 | pub fn query_member(deps: Deps, addr: String, height: Option) -> StdResult { 188 | let addr = deps.api.addr_validate(&addr)?; 189 | let weight = match height { 190 | Some(h) => MEMBERS.may_load_at_height(deps.storage, &addr, h), 191 | None => MEMBERS.may_load(deps.storage, &addr), 192 | }?; 193 | Ok(MemberResponse { weight }) 194 | } 195 | 196 | // settings for pagination 197 | const MAX_LIMIT: u32 = 30; 198 | const DEFAULT_LIMIT: u32 = 10; 199 | 200 | pub fn query_list_members( 201 | deps: Deps, 202 | start_after: Option, 203 | limit: Option, 204 | ) -> StdResult { 205 | let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; 206 | let addr = maybe_addr(deps.api, start_after)?; 207 | let start = addr.as_ref().map(Bound::exclusive); 208 | 209 | let members = MEMBERS 210 | .range(deps.storage, start, None, Order::Ascending) 211 | .take(limit) 212 | .map(|item| { 213 | item.map(|(addr, weight)| Member { 214 | addr: addr.into(), 215 | weight, 216 | }) 217 | }) 218 | .collect::>()?; 219 | 220 | Ok(MemberListResponse { members }) 221 | } 222 | -------------------------------------------------------------------------------- /contracts/cw4-group/src/error.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::{OverflowError, StdError}; 2 | use thiserror::Error; 3 | 4 | use cw_controllers::{AdminError, HookError}; 5 | 6 | #[derive(Error, Debug, PartialEq)] 7 | pub enum ContractError { 8 | #[error("{0}")] 9 | Std(#[from] StdError), 10 | 11 | #[error("{0}")] 12 | Hook(#[from] HookError), 13 | 14 | #[error("{0}")] 15 | Admin(#[from] AdminError), 16 | 17 | #[error("{0}")] 18 | Overflow(#[from] OverflowError), 19 | 20 | #[error("Unauthorized")] 21 | Unauthorized {}, 22 | 23 | #[error("Message contained duplicate member: {member}")] 24 | DuplicateMember { member: String }, 25 | } 26 | -------------------------------------------------------------------------------- /contracts/cw4-group/src/helpers.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use cosmwasm_schema::cw_serde; 4 | use cosmwasm_std::{to_json_binary, Addr, CosmosMsg, StdResult, WasmMsg}; 5 | use cw4::{Cw4Contract, Member}; 6 | 7 | use crate::{msg::ExecuteMsg, ContractError}; 8 | 9 | /// Cw4GroupContract is a wrapper around Cw4Contract that provides a lot of helpers 10 | /// for working with cw4-group contracts. 11 | /// 12 | /// It extends Cw4Contract to add the extra calls from cw4-group. 13 | #[cw_serde] 14 | pub struct Cw4GroupContract(pub Cw4Contract); 15 | 16 | impl Deref for Cw4GroupContract { 17 | type Target = Cw4Contract; 18 | 19 | fn deref(&self) -> &Self::Target { 20 | &self.0 21 | } 22 | } 23 | 24 | impl Cw4GroupContract { 25 | pub fn new(addr: Addr) -> Self { 26 | Cw4GroupContract(Cw4Contract(addr)) 27 | } 28 | 29 | fn encode_msg(&self, msg: ExecuteMsg) -> StdResult { 30 | Ok(WasmMsg::Execute { 31 | contract_addr: self.addr().into(), 32 | msg: to_json_binary(&msg)?, 33 | funds: vec![], 34 | } 35 | .into()) 36 | } 37 | 38 | pub fn update_members(&self, remove: Vec, add: Vec) -> StdResult { 39 | let msg = ExecuteMsg::UpdateMembers { remove, add }; 40 | self.encode_msg(msg) 41 | } 42 | } 43 | 44 | /// Sorts the slice and verifies all member addresses are unique. 45 | pub fn validate_unique_members(members: &mut [Member]) -> Result<(), ContractError> { 46 | members.sort_by(|a, b| a.addr.cmp(&b.addr)); 47 | for (a, b) in members.iter().zip(members.iter().skip(1)) { 48 | if a.addr == b.addr { 49 | return Err(ContractError::DuplicateMember { 50 | member: a.addr.clone(), 51 | }); 52 | } 53 | } 54 | 55 | Ok(()) 56 | } 57 | -------------------------------------------------------------------------------- /contracts/cw4-group/src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | This is a basic implementation of the [cw4 spec](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw4/README.md). 3 | It fulfills all elements of the spec, including the raw query lookups, 4 | and it designed to be used as a backing storage for 5 | [cw3 compliant contracts](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw3/README.md). 6 | 7 | It stores a set of members along with an admin, and allows the admin to 8 | update the state. Raw queries (intended for cross-contract queries) 9 | can check a given member address and the total weight. Smart queries (designed 10 | for client API) can do the same, and also query the admin address as well as 11 | paginate over all members. 12 | 13 | For more information on this contract, please check out the 14 | [README](https://github.com/CosmWasm/cw-plus/blob/main/contracts/cw4-group/README.md). 15 | */ 16 | 17 | pub mod contract; 18 | pub mod error; 19 | pub mod helpers; 20 | pub mod msg; 21 | pub mod state; 22 | 23 | pub use crate::error::ContractError; 24 | 25 | #[cfg(test)] 26 | mod tests; 27 | -------------------------------------------------------------------------------- /contracts/cw4-group/src/msg.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::{cw_serde, QueryResponses}; 2 | use cw4::Member; 3 | 4 | #[cw_serde] 5 | pub struct InstantiateMsg { 6 | /// The admin is the only account that can update the group state. 7 | /// Omit it to make the group immutable. 8 | pub admin: Option, 9 | pub members: Vec, 10 | } 11 | 12 | #[cw_serde] 13 | pub enum ExecuteMsg { 14 | /// Change the admin 15 | UpdateAdmin { admin: Option }, 16 | /// apply a diff to the existing members. 17 | /// remove is applied after add, so if an address is in both, it is removed 18 | UpdateMembers { 19 | remove: Vec, 20 | add: Vec, 21 | }, 22 | /// Add a new hook to be informed of all membership changes. Must be called by Admin 23 | AddHook { addr: String }, 24 | /// Remove a hook. Must be called by Admin 25 | RemoveHook { addr: String }, 26 | } 27 | 28 | #[cw_serde] 29 | #[derive(QueryResponses)] 30 | pub enum QueryMsg { 31 | #[returns(cw_controllers::AdminResponse)] 32 | Admin {}, 33 | #[returns(cw4::TotalWeightResponse)] 34 | TotalWeight { at_height: Option }, 35 | #[returns(cw4::MemberListResponse)] 36 | ListMembers { 37 | start_after: Option, 38 | limit: Option, 39 | }, 40 | #[returns(cw4::MemberResponse)] 41 | Member { 42 | addr: String, 43 | at_height: Option, 44 | }, 45 | /// Shows all registered hooks. 46 | #[returns(cw_controllers::HooksResponse)] 47 | Hooks {}, 48 | } 49 | -------------------------------------------------------------------------------- /contracts/cw4-group/src/state.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::Addr; 2 | use cw4::{ 3 | MEMBERS_CHANGELOG, MEMBERS_CHECKPOINTS, MEMBERS_KEY, TOTAL_KEY, TOTAL_KEY_CHANGELOG, 4 | TOTAL_KEY_CHECKPOINTS, 5 | }; 6 | use cw_controllers::{Admin, Hooks}; 7 | use cw_storage_plus::{SnapshotItem, SnapshotMap, Strategy}; 8 | 9 | pub const ADMIN: Admin = Admin::new("admin"); 10 | pub const HOOKS: Hooks = Hooks::new("cw4-hooks"); 11 | 12 | pub const TOTAL: SnapshotItem = SnapshotItem::new( 13 | TOTAL_KEY, 14 | TOTAL_KEY_CHECKPOINTS, 15 | TOTAL_KEY_CHANGELOG, 16 | Strategy::EveryBlock, 17 | ); 18 | 19 | pub const MEMBERS: SnapshotMap<&Addr, u64> = SnapshotMap::new( 20 | MEMBERS_KEY, 21 | MEMBERS_CHECKPOINTS, 22 | MEMBERS_CHANGELOG, 23 | Strategy::EveryBlock, 24 | ); 25 | -------------------------------------------------------------------------------- /contracts/cw4-stake/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --lib --target wasm32-unknown-unknown" 3 | wasm-debug = "build --lib --target wasm32-unknown-unknown" 4 | unit-test = "test --lib" 5 | schema = "run --bin schema" 6 | -------------------------------------------------------------------------------- /contracts/cw4-stake/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw4-stake" 3 | version.workspace = true 4 | authors = ["Ethan Frey "] 5 | edition = "2021" 6 | description = "CW4 implementation of group based on staked tokens" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cw-plus" 9 | homepage = "https://cosmwasm.com" 10 | documentation = "https://docs.cosmwasm.com" 11 | 12 | exclude = [ 13 | # 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. 14 | "artifacts/*", 15 | ] 16 | 17 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 18 | 19 | [lib] 20 | crate-type = ["cdylib", "rlib"] 21 | 22 | [features] 23 | # use library feature to disable all instantiate/execute/query exports 24 | library = [] 25 | 26 | [dependencies] 27 | cosmwasm-schema = { workspace = true } 28 | cw-utils = { workspace = true } 29 | cw2 = { workspace = true } 30 | cw4 = { workspace = true } 31 | cw20 = { workspace = true } 32 | cw-controllers = { workspace = true } 33 | cw-storage-plus = { workspace = true } 34 | cosmwasm-std = { workspace = true } 35 | schemars = { workspace = true } 36 | serde = { workspace = true } 37 | thiserror = { workspace = true } 38 | 39 | [dev-dependencies] 40 | easy-addr = { workspace = true } 41 | -------------------------------------------------------------------------------- /contracts/cw4-stake/README.md: -------------------------------------------------------------------------------- 1 | # CW4 Stake 2 | 3 | This is a second implementation of the [cw4 spec](../../packages/cw4/README.md). 4 | It fulfills all elements of the spec, including the raw query lookups, 5 | and is designed to be used as a backing storage for 6 | [cw3 compliant contracts](../../packages/cw3/README.md). 7 | 8 | It provides a similar API to [`cw4-group`] (which handles elected membership), 9 | but rather than appointing members (by admin or multisig), their 10 | membership and weight are based on the number of tokens they have staked. 11 | This is similar to many DAOs. 12 | 13 | Only one denom can be bonded with both `min_bond` as the minimum amount 14 | that must be sent by one address to enter, as well as `tokens_per_weight`, 15 | which can be used to normalize the weight (eg. if the token is uatom 16 | and you want 1 weight per ATOM, you can set `tokens_per_weight = 1_000_000`). 17 | 18 | There is also an unbonding period (`Duration`) which sets how long the 19 | tokens are frozen before being released. These frozen tokens can neither 20 | be used for voting, nor claimed by the original owner. Only after the period 21 | can you get your tokens back. This liquidity loss is the "skin in the game" 22 | provided by staking to this contract. 23 | 24 | ## Instantiation 25 | 26 | **TODO** 27 | 28 | To create it, you must pass in a list of members, as well as an optional 29 | `admin`, if you wish it to be mutable. 30 | 31 | ```rust 32 | pub struct InstantiateMsg { 33 | /// denom of the token to stake 34 | pub stake: String, 35 | pub tokens_per_weight: u64, 36 | pub min_bond: Uint128, 37 | pub unbonding_period: Duration, 38 | } 39 | ``` 40 | 41 | Members are defined by an address and a weight. This is transformed 42 | and stored under their `CanonicalAddr`, in a format defined in 43 | [cw4 raw queries](../../packages/cw4/README.md#raw). 44 | 45 | Note that 0 *is an allowed weight*. This doesn't give any voting rights, 46 | but it does define this address is part of the group, which may be 47 | meaningful in some circumstances. 48 | 49 | The weights of the members will be computed as the funds they send 50 | (in tokens) divided by `tokens_per_weight`, rounded down to the nearest 51 | whole number (i.e. using integer division). If the total sent is less than 52 | `min_bond`, the stake will remain, but they will not be counted as a 53 | member. If `min_bond` is higher than `tokens_per_weight`, you cannot 54 | have any member with 0 weight. 55 | 56 | ## Messages 57 | 58 | Most messages and queries are defined by the 59 | [cw4 spec](../../packages/cw4/README.md). Please refer to it for more info. 60 | 61 | The following messages have been added to handle un/staking tokens: 62 | 63 | `Bond{}` - bond all staking tokens sent with the message and update membership weight 64 | 65 | `Unbond{tokens}` - starts the unbonding process for the given number 66 | of tokens. The sender immediately loses weight from these tokens, 67 | and can claim them back to his wallet after `unbonding_period` 68 | 69 | `Claim{}` - used to claim your native tokens that you previously "unbonded" 70 | after the contract-defined waiting period (eg. 1 week) 71 | 72 | And the corresponding queries: 73 | 74 | `Claims{address}` - Claims shows the tokens in process of unbonding 75 | for this address 76 | 77 | `Staked{address}` - Show the number of tokens currently staked by this address. 78 | -------------------------------------------------------------------------------- /contracts/cw4-stake/src/bin/schema.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::write_api; 2 | 3 | use cw4_stake::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; 4 | 5 | fn main() { 6 | write_api! { 7 | instantiate: InstantiateMsg, 8 | execute: ExecuteMsg, 9 | query: QueryMsg, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /contracts/cw4-stake/src/error.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::StdError; 2 | use thiserror::Error; 3 | 4 | use cw_controllers::{AdminError, HookError}; 5 | 6 | #[derive(Error, Debug, PartialEq)] 7 | pub enum ContractError { 8 | #[error("{0}")] 9 | Std(#[from] StdError), 10 | 11 | #[error("{0}")] 12 | Admin(#[from] AdminError), 13 | 14 | #[error("{0}")] 15 | Hook(#[from] HookError), 16 | 17 | #[error("Unauthorized")] 18 | Unauthorized {}, 19 | 20 | #[error("No claims that can be released currently")] 21 | NothingToClaim {}, 22 | 23 | #[error("Must send '{0}' to stake")] 24 | MissingDenom(String), 25 | 26 | #[error("Sent unsupported denoms, must send '{0}' to stake")] 27 | ExtraDenoms(String), 28 | 29 | #[error("Must send valid address to stake")] 30 | InvalidDenom(String), 31 | 32 | #[error("Missed address or denom")] 33 | MixedNativeAndCw20(String), 34 | 35 | #[error("No funds sent")] 36 | NoFunds {}, 37 | 38 | #[error("No data in ReceiveMsg")] 39 | NoData {}, 40 | } 41 | -------------------------------------------------------------------------------- /contracts/cw4-stake/src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | This is a second implementation of the [cw4 spec](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw4/README.md). 3 | It fulfills all elements of the spec, including the raw query lookups, 4 | and is designed to be used as a backing storage for 5 | [cw3 compliant contracts](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw3/README.md). 6 | 7 | It provides a similar API to [`cw4-group`](https://github.com/CosmWasm/cw-plus/blob/main/contracts/cw4-group/README.md) 8 | (which handles elected membership), 9 | but rather than appointing members (by admin or multisig), their 10 | membership and weight are based on the number of tokens they have staked. 11 | This is similar to many DAOs. 12 | 13 | Only one denom can be bonded with both `min_bond` as the minimum amount 14 | that must be sent by one address to enter, as well as `tokens_per_weight`, 15 | which can be used to normalize the weight (eg. if the token is uatom 16 | and you want 1 weight per ATOM, you can set `tokens_per_weight = 1_000_000`). 17 | 18 | There is also an unbonding period (`Duration`) which sets how long the 19 | tokens are frozen before being released. These frozen tokens can neither 20 | be used for voting, nor claimed by the original owner. Only after the period 21 | can you get your tokens back. This liquidity loss is the "skin in the game" 22 | provided by staking to this contract. 23 | 24 | For more information on this contract, please check out the 25 | [README](https://github.com/CosmWasm/cw-plus/blob/main/contracts/cw4-stake/README.md). 26 | */ 27 | 28 | pub mod contract; 29 | mod error; 30 | pub mod msg; 31 | pub mod state; 32 | 33 | pub use crate::error::ContractError; 34 | -------------------------------------------------------------------------------- /contracts/cw4-stake/src/msg.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::{cw_serde, QueryResponses}; 2 | use cosmwasm_std::Uint128; 3 | 4 | use cw20::{Cw20ReceiveMsg, Denom}; 5 | pub use cw_controllers::ClaimsResponse; 6 | use cw_utils::Duration; 7 | 8 | #[cw_serde] 9 | pub struct InstantiateMsg { 10 | /// denom of the token to stake 11 | pub denom: Denom, 12 | pub tokens_per_weight: Uint128, 13 | pub min_bond: Uint128, 14 | pub unbonding_period: Duration, 15 | 16 | // admin can only add/remove hooks, not change other parameters 17 | pub admin: Option, 18 | } 19 | 20 | #[cw_serde] 21 | pub enum ExecuteMsg { 22 | /// Bond will bond all staking tokens sent with the message and update membership weight 23 | Bond {}, 24 | /// Unbond will start the unbonding process for the given number of tokens. 25 | /// The sender immediately loses weight from these tokens, and can claim them 26 | /// back to his wallet after `unbonding_period` 27 | Unbond { tokens: Uint128 }, 28 | /// Claim is used to claim your native tokens that you previously "unbonded" 29 | /// after the contract-defined waiting period (eg. 1 week) 30 | Claim {}, 31 | 32 | /// Change the admin 33 | UpdateAdmin { admin: Option }, 34 | /// Add a new hook to be informed of all membership changes. Must be called by Admin 35 | AddHook { addr: String }, 36 | /// Remove a hook. Must be called by Admin 37 | RemoveHook { addr: String }, 38 | 39 | /// This accepts a properly-encoded ReceiveMsg from a cw20 contract 40 | Receive(Cw20ReceiveMsg), 41 | } 42 | 43 | #[cw_serde] 44 | pub enum ReceiveMsg { 45 | /// Only valid cw20 message is to bond the tokens 46 | Bond {}, 47 | } 48 | 49 | #[cw_serde] 50 | #[derive(QueryResponses)] 51 | pub enum QueryMsg { 52 | /// Claims shows the tokens in process of unbonding for this address 53 | #[returns(cw_controllers::ClaimsResponse)] 54 | Claims { address: String }, 55 | // Show the number of tokens currently staked by this address. 56 | #[returns(StakedResponse)] 57 | Staked { address: String }, 58 | 59 | #[returns(cw_controllers::AdminResponse)] 60 | Admin {}, 61 | #[returns(cw4::TotalWeightResponse)] 62 | TotalWeight {}, 63 | #[returns(cw4::MemberListResponse)] 64 | ListMembers { 65 | start_after: Option, 66 | limit: Option, 67 | }, 68 | #[returns(cw4::MemberResponse)] 69 | Member { 70 | addr: String, 71 | at_height: Option, 72 | }, 73 | /// Shows all registered hooks. 74 | #[returns(cw_controllers::HooksResponse)] 75 | Hooks {}, 76 | } 77 | 78 | #[cw_serde] 79 | pub struct StakedResponse { 80 | pub stake: Uint128, 81 | pub denom: Denom, 82 | } 83 | -------------------------------------------------------------------------------- /contracts/cw4-stake/src/state.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | use cosmwasm_std::{Addr, Uint128}; 3 | use cw20::Denom; 4 | use cw4::TOTAL_KEY; 5 | use cw_controllers::{Admin, Claims, Hooks}; 6 | use cw_storage_plus::{Item, Map, SnapshotMap, Strategy}; 7 | use cw_utils::Duration; 8 | 9 | pub const CLAIMS: Claims = Claims::new("claims"); 10 | 11 | #[cw_serde] 12 | pub struct Config { 13 | /// denom of the token to stake 14 | pub denom: Denom, 15 | pub tokens_per_weight: Uint128, 16 | pub min_bond: Uint128, 17 | pub unbonding_period: Duration, 18 | } 19 | 20 | pub const ADMIN: Admin = Admin::new("admin"); 21 | pub const HOOKS: Hooks = Hooks::new("cw4-hooks"); 22 | pub const CONFIG: Item = Item::new("config"); 23 | pub const TOTAL: Item = Item::new(TOTAL_KEY); 24 | 25 | pub const MEMBERS: SnapshotMap<&Addr, u64> = SnapshotMap::new( 26 | cw4::MEMBERS_KEY, 27 | cw4::MEMBERS_CHECKPOINTS, 28 | cw4::MEMBERS_CHANGELOG, 29 | Strategy::EveryBlock, 30 | ); 31 | 32 | pub const STAKE: Map<&Addr, Uint128> = Map::new("stake"); 33 | -------------------------------------------------------------------------------- /packages/cw1/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --lib --target wasm32-unknown-unknown" 3 | wasm-debug = "build --lib --target wasm32-unknown-unknown" 4 | schema = "run --bin schema" 5 | -------------------------------------------------------------------------------- /packages/cw1/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw1" 3 | version.workspace = true 4 | authors = ["Ethan Frey "] 5 | edition = "2021" 6 | description = "Definition and types for the CosmWasm-1 interface" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cw-plus" 9 | homepage = "https://cosmwasm.com" 10 | 11 | [dependencies] 12 | cosmwasm-schema = { workspace = true } 13 | cosmwasm-std = { workspace = true } 14 | schemars = { workspace = true } 15 | serde = { workspace = true } 16 | -------------------------------------------------------------------------------- /packages/cw1/README.md: -------------------------------------------------------------------------------- 1 | # CW1 Spec: Proxy Contracts 2 | 3 | CW1 is a specification for proxy contracts based on CosmWasm. It is a very simple, but flexible interface designed for 4 | the case where one contract is meant to hold assets (or rights) on behalf of other contracts. 5 | 6 | The simplest example is a contract that will accept messages from the creator and resend them from its address. Simply 7 | by making this transferable, you can then begin to transfer non-transferable assets (eg. staked tokens, voting power, 8 | etc). 9 | 10 | You can imagine more complex examples, such as a "1 of N" multisig, or conditional approval, where "sub-accounts" have 11 | the right to spend a limited amount of funds from this account, with a "admin account" retaining full control. 12 | 13 | The common denominator is that they allow you to immediately execute arbitrary `CosmosMsg` in the same transaction. 14 | 15 | ### Messages 16 | 17 | `Execute{msgs}` - This accepts `Vec` and checks permissions before re-dispatching all those messages from the 18 | contract address. It emits the following attributes: 19 | 20 | | Key | Value | 21 | | -------- | ------------ | 22 | | "action" | "execute" | 23 | | "owner" | [msg sender] | 24 | 25 | ### Queries 26 | 27 | `CanExecute{sender, msg}` - This accepts one `CosmosMsg` and checks permissions, returning true or false based on the 28 | permissions. If `CanExecute` returns true then a call to `Execute` from that sender, with the same message, before any 29 | further state changes, should also succeed. This can be used to dynamically provide some client info on a generic cw1 30 | contract without knowing the extension details. (eg. detect if they can send coins or stake) 31 | -------------------------------------------------------------------------------- /packages/cw1/src/bin/schema.rs: -------------------------------------------------------------------------------- 1 | use std::env::current_dir; 2 | use std::fs::create_dir_all; 3 | 4 | use cosmwasm_schema::{export_schema, export_schema_with_title, remove_schemas, schema_for}; 5 | 6 | use cw1::{CanExecuteResponse, Cw1ExecuteMsg, Cw1QueryMsg}; 7 | 8 | fn main() { 9 | let mut out_dir = current_dir().unwrap(); 10 | out_dir.push("schema"); 11 | create_dir_all(&out_dir).unwrap(); 12 | remove_schemas(&out_dir).unwrap(); 13 | 14 | export_schema_with_title(&schema_for!(Cw1ExecuteMsg), &out_dir, "ExecuteMsg"); 15 | export_schema_with_title(&schema_for!(Cw1QueryMsg), &out_dir, "QueryMsg"); 16 | export_schema(&schema_for!(CanExecuteResponse), &out_dir); 17 | } 18 | -------------------------------------------------------------------------------- /packages/cw1/src/helpers.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | use cosmwasm_std::{to_json_binary, Addr, CosmosMsg, StdResult, WasmMsg}; 3 | 4 | use crate::msg::Cw1ExecuteMsg; 5 | 6 | /// Cw1Contract is a wrapper around Addr that provides a lot of helpers 7 | /// for working with this. 8 | /// 9 | /// If you wish to persist this, convert to Cw1CanonicalContract via .canonical() 10 | #[cw_serde] 11 | pub struct Cw1Contract(pub Addr); 12 | 13 | impl Cw1Contract { 14 | pub fn addr(&self) -> Addr { 15 | self.0.clone() 16 | } 17 | 18 | pub fn execute>>(&self, msgs: T) -> StdResult { 19 | let msg = Cw1ExecuteMsg::Execute { msgs: msgs.into() }; 20 | Ok(WasmMsg::Execute { 21 | contract_addr: self.addr().into(), 22 | msg: to_json_binary(&msg)?, 23 | funds: vec![], 24 | } 25 | .into()) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/cw1/src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | CW1 is a specification for proxy contracts based on CosmWasm. 3 | It is a very simple, but flexible interface designed for the case 4 | where one contract is meant to hold assets (or rights) on behalf of 5 | other contracts. 6 | 7 | The simplest example is a contract that will accept messages from 8 | the creator and resend them from its address. Simply by making this 9 | transferable, you can then begin to transfer non-transferable assets 10 | (eg. staked tokens, voting power, etc). 11 | 12 | You can imagine more complex examples, such as a "1 of N" multisig, 13 | or conditional approval, where "sub-accounts" have the right to spend 14 | a limited amount of funds from this account, with a "admin account" 15 | retaining full control. 16 | 17 | The common denominator is that they allow you to immediately 18 | execute arbitrary `CosmosMsg` in the same transaction. 19 | 20 | For more information on this specification, please check out the 21 | [README](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw1/README.md). 22 | */ 23 | 24 | pub mod helpers; 25 | pub mod msg; 26 | pub mod query; 27 | 28 | pub use crate::helpers::Cw1Contract; 29 | pub use crate::msg::Cw1ExecuteMsg; 30 | pub use crate::query::{CanExecuteResponse, Cw1QueryMsg}; 31 | -------------------------------------------------------------------------------- /packages/cw1/src/msg.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use std::fmt; 3 | 4 | use cosmwasm_schema::cw_serde; 5 | use cosmwasm_std::{CosmosMsg, Empty}; 6 | 7 | #[cw_serde] 8 | pub enum Cw1ExecuteMsg 9 | where 10 | T: Clone + fmt::Debug + PartialEq + JsonSchema, 11 | { 12 | /// Execute requests the contract to re-dispatch all these messages with the 13 | /// contract's address as sender. Every implementation has it's own logic to 14 | /// determine in 15 | Execute { msgs: Vec> }, 16 | } 17 | -------------------------------------------------------------------------------- /packages/cw1/src/query.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use std::fmt; 3 | 4 | use cosmwasm_schema::cw_serde; 5 | use cosmwasm_std::{CosmosMsg, Empty}; 6 | 7 | #[cw_serde] 8 | pub enum Cw1QueryMsg 9 | where 10 | T: Clone + fmt::Debug + PartialEq + JsonSchema, 11 | { 12 | /// Checks permissions of the caller on this proxy. 13 | /// If CanExecute returns true then a call to `Execute` with the same message, 14 | /// from the given sender, before any further state changes, should also succeed. 15 | CanExecute { sender: String, msg: CosmosMsg }, 16 | } 17 | 18 | #[cw_serde] 19 | pub struct CanExecuteResponse { 20 | pub can_execute: bool, 21 | } 22 | -------------------------------------------------------------------------------- /packages/cw20/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --lib --target wasm32-unknown-unknown" 3 | wasm-debug = "build --lib --target wasm32-unknown-unknown" 4 | schema = "run --bin schema" 5 | -------------------------------------------------------------------------------- /packages/cw20/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw20" 3 | version.workspace = true 4 | authors = ["Ethan Frey "] 5 | edition = "2021" 6 | description = "Definition and types for the CosmWasm-20 interface" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cw-plus" 9 | homepage = "https://cosmwasm.com" 10 | 11 | [dependencies] 12 | cw-utils = { workspace = true } 13 | cosmwasm-schema = { workspace = true } 14 | cosmwasm-std = { workspace = true } 15 | schemars = { workspace = true } 16 | serde = { workspace = true } 17 | 18 | [dev-dependencies] 19 | cosmwasm-schema = { workspace = true } 20 | -------------------------------------------------------------------------------- /packages/cw20/src/balance.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | use cosmwasm_std::Coin; 3 | 4 | use std::{fmt, fmt::Display}; 5 | 6 | use cw_utils::NativeBalance; 7 | 8 | use crate::Cw20CoinVerified; 9 | 10 | #[cw_serde] 11 | 12 | pub enum Balance { 13 | Native(NativeBalance), 14 | Cw20(Cw20CoinVerified), 15 | } 16 | 17 | impl Default for Balance { 18 | fn default() -> Balance { 19 | Balance::Native(NativeBalance(vec![])) 20 | } 21 | } 22 | 23 | impl Display for Balance { 24 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 25 | match self { 26 | Balance::Native(native) => write!(f, "{native}"), 27 | Balance::Cw20(cw20) => write!(f, "{cw20}"), 28 | }?; 29 | Ok(()) 30 | } 31 | } 32 | 33 | impl Balance { 34 | pub fn is_empty(&self) -> bool { 35 | match self { 36 | Balance::Native(balance) => balance.is_empty(), 37 | Balance::Cw20(coin) => coin.is_empty(), 38 | } 39 | } 40 | 41 | /// normalize Wallet 42 | pub fn normalize(&mut self) { 43 | match self { 44 | Balance::Native(balance) => balance.normalize(), 45 | Balance::Cw20(_) => {} 46 | } 47 | } 48 | } 49 | 50 | impl From> for Balance { 51 | fn from(coins: Vec) -> Balance { 52 | Balance::Native(NativeBalance(coins)) 53 | } 54 | } 55 | 56 | impl From for Balance { 57 | fn from(cw20_coin: Cw20CoinVerified) -> Balance { 58 | Balance::Cw20(cw20_coin) 59 | } 60 | } 61 | 62 | #[cfg(test)] 63 | mod tests { 64 | use super::*; 65 | use cosmwasm_std::{Addr, Uint128}; 66 | 67 | #[test] 68 | fn default_balance_is_native() { 69 | let balance: Balance = Default::default(); 70 | assert!(matches!(balance, Balance::Native(_))); 71 | } 72 | 73 | #[test] 74 | fn displaying_native_balance_works() { 75 | let balance: Balance = Default::default(); 76 | assert_eq!("", format!("{balance}",)); 77 | } 78 | 79 | #[test] 80 | fn displaying_cw20_balance_works() { 81 | let balance = Balance::Cw20(Cw20CoinVerified { 82 | address: Addr::unchecked("sender"), 83 | amount: Uint128::zero(), 84 | }); 85 | assert_eq!("address: sender, amount: 0", format!("{balance}",)); 86 | } 87 | 88 | #[test] 89 | fn default_native_balance_is_empty() { 90 | assert!(Balance::default().is_empty()); 91 | } 92 | 93 | #[test] 94 | fn cw20_balance_with_zero_amount_is_empty() { 95 | assert!(Balance::Cw20(Cw20CoinVerified { 96 | address: Addr::unchecked("sender"), 97 | amount: Uint128::zero(), 98 | }) 99 | .is_empty()); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /packages/cw20/src/bin/schema.rs: -------------------------------------------------------------------------------- 1 | use std::env::current_dir; 2 | use std::fs::create_dir_all; 3 | 4 | use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; 5 | 6 | use cw20::{ 7 | AllAccountsResponse, AllAllowancesResponse, AllowanceResponse, BalanceResponse, Cw20ExecuteMsg, 8 | Cw20QueryMsg, Cw20ReceiveMsg, DownloadLogoResponse, MarketingInfoResponse, MinterResponse, 9 | TokenInfoResponse, 10 | }; 11 | 12 | fn main() { 13 | let mut out_dir = current_dir().unwrap(); 14 | out_dir.push("schema"); 15 | create_dir_all(&out_dir).unwrap(); 16 | remove_schemas(&out_dir).unwrap(); 17 | 18 | export_schema(&schema_for!(Cw20ExecuteMsg), &out_dir); 19 | export_schema(&schema_for!(Cw20QueryMsg), &out_dir); 20 | export_schema(&schema_for!(Cw20ReceiveMsg), &out_dir); 21 | export_schema(&schema_for!(AllowanceResponse), &out_dir); 22 | export_schema(&schema_for!(BalanceResponse), &out_dir); 23 | export_schema(&schema_for!(TokenInfoResponse), &out_dir); 24 | export_schema(&schema_for!(MinterResponse), &out_dir); 25 | export_schema(&schema_for!(DownloadLogoResponse), &out_dir); 26 | export_schema(&schema_for!(MarketingInfoResponse), &out_dir); 27 | export_schema(&schema_for!(AllAllowancesResponse), &out_dir); 28 | export_schema(&schema_for!(AllAccountsResponse), &out_dir); 29 | } 30 | -------------------------------------------------------------------------------- /packages/cw20/src/coin.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | use cosmwasm_std::{Addr, Uint128}; 3 | use std::fmt; 4 | 5 | #[cw_serde] 6 | pub struct Cw20Coin { 7 | pub address: String, 8 | pub amount: Uint128, 9 | } 10 | 11 | impl Cw20Coin { 12 | pub fn is_empty(&self) -> bool { 13 | self.amount == Uint128::zero() 14 | } 15 | } 16 | 17 | impl fmt::Display for Cw20Coin { 18 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 19 | write!(f, "address: {}, amount: {}", self.address, self.amount) 20 | } 21 | } 22 | 23 | #[cw_serde] 24 | pub struct Cw20CoinVerified { 25 | pub address: Addr, 26 | pub amount: Uint128, 27 | } 28 | 29 | impl Cw20CoinVerified { 30 | pub fn is_empty(&self) -> bool { 31 | self.amount == Uint128::zero() 32 | } 33 | } 34 | 35 | impl fmt::Display for Cw20CoinVerified { 36 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 37 | write!(f, "address: {}, amount: {}", self.address, self.amount) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/cw20/src/denom.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | use cosmwasm_std::{Addr, Deps, StdResult, Uint128}; 3 | 4 | use crate::{Cw20QueryMsg, TokenInfoResponse}; 5 | 6 | #[cw_serde] 7 | pub enum Denom { 8 | Native(String), 9 | Cw20(Addr), 10 | } 11 | 12 | #[cw_serde] 13 | pub enum UncheckedDenom { 14 | Native(String), 15 | Cw20(String), 16 | } 17 | 18 | #[cw_serde] 19 | pub struct DepositInfo { 20 | amount: Uint128, 21 | denom: UncheckedDenom, 22 | } 23 | 24 | impl UncheckedDenom { 25 | pub fn into_checked(self, deps: Deps) -> StdResult { 26 | match self { 27 | Self::Native(denom) => Ok(Denom::Native(denom)), 28 | Self::Cw20(addr) => { 29 | let addr = deps.api.addr_validate(&addr)?; 30 | let _info: TokenInfoResponse = deps 31 | .querier 32 | .query_wasm_smart(addr.clone(), &Cw20QueryMsg::TokenInfo {})?; 33 | Ok(Denom::Cw20(addr)) 34 | } 35 | } 36 | } 37 | } 38 | 39 | // TODO: remove or figure out where needed 40 | impl Default for Denom { 41 | fn default() -> Denom { 42 | Denom::Native(String::default()) 43 | } 44 | } 45 | 46 | impl Denom { 47 | pub fn is_empty(&self) -> bool { 48 | match self { 49 | Denom::Native(string) => string.is_empty(), 50 | Denom::Cw20(addr) => addr.as_ref().is_empty(), 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/cw20/src/helpers.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | use cosmwasm_std::{ 3 | to_json_binary, Addr, CosmosMsg, CustomQuery, QuerierWrapper, QueryRequest, StdResult, Uint128, 4 | WasmMsg, WasmQuery, 5 | }; 6 | 7 | use crate::{ 8 | AllowanceResponse, BalanceResponse, Cw20ExecuteMsg, Cw20QueryMsg, MinterResponse, 9 | TokenInfoResponse, 10 | }; 11 | 12 | /// Cw20Contract is a wrapper around Addr that provides a lot of helpers 13 | /// for working with this. 14 | /// 15 | /// If you wish to persist this, convert to Cw20CanonicalContract via .canonical() 16 | #[cw_serde] 17 | pub struct Cw20Contract(pub Addr); 18 | 19 | impl Cw20Contract { 20 | pub fn addr(&self) -> Addr { 21 | self.0.clone() 22 | } 23 | 24 | pub fn call>(&self, msg: T) -> StdResult { 25 | let msg = to_json_binary(&msg.into())?; 26 | Ok(WasmMsg::Execute { 27 | contract_addr: self.addr().into(), 28 | msg, 29 | funds: vec![], 30 | } 31 | .into()) 32 | } 33 | 34 | fn encode_smart_query( 35 | &self, 36 | msg: Cw20QueryMsg, 37 | ) -> StdResult> { 38 | Ok(WasmQuery::Smart { 39 | contract_addr: self.addr().into(), 40 | msg: to_json_binary(&msg)?, 41 | } 42 | .into()) 43 | } 44 | 45 | /// Get token balance for the given address 46 | pub fn balance(&self, querier: &QuerierWrapper, address: T) -> StdResult 47 | where 48 | T: Into, 49 | CQ: CustomQuery, 50 | { 51 | let query = self.encode_smart_query(Cw20QueryMsg::Balance { 52 | address: address.into(), 53 | })?; 54 | let res: BalanceResponse = querier.query(&query)?; 55 | Ok(res.balance) 56 | } 57 | 58 | /// Get metadata from the contract. This is a good check that the address 59 | /// is a valid Cw20 contract. 60 | pub fn meta( 61 | &self, 62 | querier: &QuerierWrapper, 63 | ) -> StdResult { 64 | let query = self.encode_smart_query(Cw20QueryMsg::TokenInfo {})?; 65 | querier.query(&query) 66 | } 67 | 68 | /// Get allowance of spender to use owner's account 69 | pub fn allowance( 70 | &self, 71 | querier: &QuerierWrapper, 72 | owner: T, 73 | spender: U, 74 | ) -> StdResult 75 | where 76 | T: Into, 77 | U: Into, 78 | CQ: CustomQuery, 79 | { 80 | let query = self.encode_smart_query(Cw20QueryMsg::Allowance { 81 | owner: owner.into(), 82 | spender: spender.into(), 83 | })?; 84 | querier.query(&query) 85 | } 86 | 87 | /// Find info on who can mint, and how much 88 | pub fn minter( 89 | &self, 90 | querier: &QuerierWrapper, 91 | ) -> StdResult> { 92 | let query = self.encode_smart_query(Cw20QueryMsg::Minter {})?; 93 | querier.query(&query) 94 | } 95 | 96 | /// returns true if the contract supports the allowance extension 97 | pub fn has_allowance(&self, querier: &QuerierWrapper) -> bool { 98 | self.allowance(querier, self.addr(), self.addr()).is_ok() 99 | } 100 | 101 | /// returns true if the contract supports the mintable extension 102 | pub fn is_mintable(&self, querier: &QuerierWrapper) -> bool { 103 | self.minter(querier).is_ok() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /packages/cw20/src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | CW20 is a specification for fungible tokens based on CosmWasm. 3 | The name and design is loosely based on Ethereum's ERC20 standard, 4 | but many changes have been made. The types in here can be imported by 5 | contracts that wish to implement this spec, or by contracts that call 6 | to any standard cw20 contract. 7 | 8 | For more information on this specification, please check out the 9 | [README](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw20/README.md). 10 | */ 11 | 12 | pub use cw_utils::Expiration; 13 | 14 | pub use crate::balance::Balance; 15 | pub use crate::coin::{Cw20Coin, Cw20CoinVerified}; 16 | pub use crate::denom::{Denom, UncheckedDenom}; 17 | pub use crate::helpers::Cw20Contract; 18 | pub use crate::logo::{EmbeddedLogo, Logo, LogoInfo}; 19 | pub use crate::msg::Cw20ExecuteMsg; 20 | pub use crate::query::{ 21 | AllAccountsResponse, AllAllowancesResponse, AllSpenderAllowancesResponse, AllowanceInfo, 22 | AllowanceResponse, BalanceResponse, Cw20QueryMsg, DownloadLogoResponse, MarketingInfoResponse, 23 | MinterResponse, SpenderAllowanceInfo, TokenInfoResponse, 24 | }; 25 | pub use crate::receiver::Cw20ReceiveMsg; 26 | 27 | mod balance; 28 | mod coin; 29 | mod denom; 30 | mod helpers; 31 | mod logo; 32 | mod msg; 33 | mod query; 34 | mod receiver; 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | #[test] 39 | fn it_works() { 40 | // test me 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/cw20/src/logo.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | use cosmwasm_std::Binary; 3 | 4 | /// This is used for uploading logo data, or setting it in InstantiateData 5 | #[cw_serde] 6 | 7 | pub enum Logo { 8 | /// A reference to an externally hosted logo. Must be a valid HTTP or HTTPS URL. 9 | Url(String), 10 | /// Logo content stored on the blockchain. Enforce maximum size of 5KB on all variants 11 | Embedded(EmbeddedLogo), 12 | } 13 | 14 | /// This is used to store the logo on the blockchain in an accepted format. 15 | /// Enforce maximum size of 5KB on all variants. 16 | #[cw_serde] 17 | 18 | pub enum EmbeddedLogo { 19 | /// Store the Logo as an SVG file. The content must conform to the spec 20 | /// at https://en.wikipedia.org/wiki/Scalable_Vector_Graphics 21 | /// (The contract should do some light-weight sanity-check validation) 22 | Svg(Binary), 23 | /// Store the Logo as a PNG file. This will likely only support up to 64x64 or so 24 | /// within the 5KB limit. 25 | Png(Binary), 26 | } 27 | 28 | /// This is used to display logo info, provide a link or inform there is one 29 | /// that can be downloaded from the blockchain itself 30 | #[cw_serde] 31 | 32 | pub enum LogoInfo { 33 | /// A reference to an externally hosted logo. Must be a valid HTTP or HTTPS URL. 34 | Url(String), 35 | /// There is an embedded logo on the chain, make another call to download it. 36 | Embedded, 37 | } 38 | -------------------------------------------------------------------------------- /packages/cw20/src/msg.rs: -------------------------------------------------------------------------------- 1 | use crate::logo::Logo; 2 | use cosmwasm_schema::cw_serde; 3 | use cosmwasm_std::{Binary, Uint128}; 4 | use cw_utils::Expiration; 5 | 6 | #[cw_serde] 7 | 8 | pub enum Cw20ExecuteMsg { 9 | /// Transfer is a base message to move tokens to another account without triggering actions 10 | Transfer { recipient: String, amount: Uint128 }, 11 | /// Burn is a base message to destroy tokens forever 12 | Burn { amount: Uint128 }, 13 | /// Send is a base message to transfer tokens to a contract and trigger an action 14 | /// on the receiving contract. 15 | Send { 16 | contract: String, 17 | amount: Uint128, 18 | msg: Binary, 19 | }, 20 | /// Only with "approval" extension. Allows spender to access an additional amount tokens 21 | /// from the owner's (env.sender) account. If expires is Some(), overwrites current allowance 22 | /// expiration with this one. 23 | IncreaseAllowance { 24 | spender: String, 25 | amount: Uint128, 26 | expires: Option, 27 | }, 28 | /// Only with "approval" extension. Lowers the spender's access of tokens 29 | /// from the owner's (env.sender) account by amount. If expires is Some(), overwrites current 30 | /// allowance expiration with this one. 31 | DecreaseAllowance { 32 | spender: String, 33 | amount: Uint128, 34 | expires: Option, 35 | }, 36 | /// Only with "approval" extension. Transfers amount tokens from owner -> recipient 37 | /// if `env.sender` has sufficient pre-approval. 38 | TransferFrom { 39 | owner: String, 40 | recipient: String, 41 | amount: Uint128, 42 | }, 43 | /// Only with "approval" extension. Sends amount tokens from owner -> contract 44 | /// if `env.sender` has sufficient pre-approval. 45 | SendFrom { 46 | owner: String, 47 | contract: String, 48 | amount: Uint128, 49 | msg: Binary, 50 | }, 51 | /// Only with "approval" extension. Destroys tokens forever 52 | BurnFrom { owner: String, amount: Uint128 }, 53 | /// Only with the "mintable" extension. If authorized, creates amount new tokens 54 | /// and adds to the recipient balance. 55 | Mint { recipient: String, amount: Uint128 }, 56 | /// Only with the "mintable" extension. The current minter may set 57 | /// a new minter. Setting the minter to None will remove the 58 | /// token's minter forever. 59 | UpdateMinter { new_minter: Option }, 60 | /// Only with the "marketing" extension. If authorized, updates marketing metadata. 61 | /// Setting None/null for any of these will leave it unchanged. 62 | /// Setting Some("") will clear this field on the contract storage 63 | UpdateMarketing { 64 | /// A URL pointing to the project behind this token. 65 | project: Option, 66 | /// A longer description of the token and it's utility. Designed for tooltips or such 67 | description: Option, 68 | /// The address (if any) who can update this data structure 69 | marketing: Option, 70 | }, 71 | /// If set as the "marketing" role on the contract, upload a new URL, SVG, or PNG for the token 72 | UploadLogo(Logo), 73 | } 74 | -------------------------------------------------------------------------------- /packages/cw20/src/query.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use cosmwasm_schema::cw_serde; 5 | use cosmwasm_std::{Addr, Binary, Uint128}; 6 | 7 | use crate::logo::LogoInfo; 8 | use cw_utils::Expiration; 9 | 10 | #[cw_serde] 11 | 12 | pub enum Cw20QueryMsg { 13 | /// Returns the current balance of the given address, 0 if unset. 14 | /// Return type: BalanceResponse. 15 | Balance { address: String }, 16 | /// Returns metadata on the contract - name, decimals, supply, etc. 17 | /// Return type: TokenInfoResponse. 18 | TokenInfo {}, 19 | /// Only with "allowance" extension. 20 | /// Returns how much spender can use from owner account, 0 if unset. 21 | /// Return type: AllowanceResponse. 22 | Allowance { owner: String, spender: String }, 23 | /// Only with "mintable" extension. 24 | /// Returns who can mint and the hard cap on maximum tokens after minting. 25 | /// Return type: MinterResponse. 26 | Minter {}, 27 | /// Only with "marketing" extension 28 | /// Returns more metadata on the contract to display in the client: 29 | /// - description, logo, project url, etc. 30 | /// Return type: MarketingInfoResponse. 31 | MarketingInfo {}, 32 | /// Only with "marketing" extension 33 | /// Downloads the embedded logo data (if stored on chain). Errors if no logo data stored for 34 | /// this contract. 35 | /// Return type: DownloadLogoResponse. 36 | DownloadLogo {}, 37 | /// Only with "enumerable" extension (and "allowances") 38 | /// Returns all allowances this owner has approved. Supports pagination. 39 | /// Return type: AllAllowancesResponse. 40 | AllAllowances { 41 | owner: String, 42 | start_after: Option, 43 | limit: Option, 44 | }, 45 | /// Only with "enumerable" extension 46 | /// Returns all accounts that have balances. Supports pagination. 47 | /// Return type: AllAccountsResponse. 48 | AllAccounts { 49 | start_after: Option, 50 | limit: Option, 51 | }, 52 | } 53 | 54 | #[cw_serde] 55 | pub struct BalanceResponse { 56 | pub balance: Uint128, 57 | } 58 | 59 | #[cw_serde] 60 | pub struct TokenInfoResponse { 61 | pub name: String, 62 | pub symbol: String, 63 | pub decimals: u8, 64 | pub total_supply: Uint128, 65 | } 66 | 67 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug, Default)] 68 | pub struct AllowanceResponse { 69 | pub allowance: Uint128, 70 | pub expires: Expiration, 71 | } 72 | 73 | #[cw_serde] 74 | pub struct MinterResponse { 75 | pub minter: String, 76 | /// cap is a hard cap on total supply that can be achieved by minting. 77 | /// Note that this refers to total_supply. 78 | /// If None, there is unlimited cap. 79 | pub cap: Option, 80 | } 81 | 82 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug, Default)] 83 | pub struct MarketingInfoResponse { 84 | /// A URL pointing to the project behind this token. 85 | pub project: Option, 86 | /// A longer description of the token and it's utility. Designed for tooltips or such 87 | pub description: Option, 88 | /// A link to the logo, or a comment there is an on-chain logo stored 89 | pub logo: Option, 90 | /// The address (if any) who can update this data structure 91 | pub marketing: Option, 92 | } 93 | 94 | /// When we download an embedded logo, we get this response type. 95 | /// We expect a SPA to be able to accept this info and display it. 96 | #[cw_serde] 97 | pub struct DownloadLogoResponse { 98 | pub mime_type: String, 99 | pub data: Binary, 100 | } 101 | 102 | #[cw_serde] 103 | pub struct AllowanceInfo { 104 | pub spender: String, 105 | pub allowance: Uint128, 106 | pub expires: Expiration, 107 | } 108 | 109 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug, Default)] 110 | pub struct AllAllowancesResponse { 111 | pub allowances: Vec, 112 | } 113 | 114 | #[cw_serde] 115 | pub struct SpenderAllowanceInfo { 116 | pub owner: String, 117 | pub allowance: Uint128, 118 | pub expires: Expiration, 119 | } 120 | 121 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug, Default)] 122 | pub struct AllSpenderAllowancesResponse { 123 | pub allowances: Vec, 124 | } 125 | 126 | #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug, Default)] 127 | pub struct AllAccountsResponse { 128 | pub accounts: Vec, 129 | } 130 | -------------------------------------------------------------------------------- /packages/cw20/src/receiver.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | use cosmwasm_std::{to_json_binary, Binary, CosmosMsg, StdResult, Uint128, WasmMsg}; 3 | 4 | /// Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg 5 | #[cw_serde] 6 | 7 | pub struct Cw20ReceiveMsg { 8 | pub sender: String, 9 | pub amount: Uint128, 10 | pub msg: Binary, 11 | } 12 | 13 | impl Cw20ReceiveMsg { 14 | /// serializes the message 15 | pub fn into_json_binary(self) -> StdResult { 16 | let msg = ReceiverExecuteMsg::Receive(self); 17 | to_json_binary(&msg) 18 | } 19 | 20 | /// creates a cosmos_msg sending this struct to the named contract 21 | pub fn into_cosmos_msg>(self, contract_addr: T) -> StdResult { 22 | let msg = self.into_json_binary()?; 23 | let execute = WasmMsg::Execute { 24 | contract_addr: contract_addr.into(), 25 | msg, 26 | funds: vec![], 27 | }; 28 | Ok(execute.into()) 29 | } 30 | } 31 | 32 | // This is just a helper to properly serialize the above message 33 | #[cw_serde] 34 | 35 | enum ReceiverExecuteMsg { 36 | Receive(Cw20ReceiveMsg), 37 | } 38 | -------------------------------------------------------------------------------- /packages/cw3/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --lib --target wasm32-unknown-unknown" 3 | wasm-debug = "build --lib --target wasm32-unknown-unknown" 4 | schema = "run --bin schema" 5 | -------------------------------------------------------------------------------- /packages/cw3/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw3" 3 | version.workspace = true 4 | authors = ["Ethan Frey "] 5 | edition = "2021" 6 | description = "CosmWasm-3 Interface: On-Chain MultiSig/Voting contracts" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cw-plus" 9 | homepage = "https://cosmwasm.com" 10 | 11 | [dependencies] 12 | cw-utils = { workspace = true } 13 | cw20 = { workspace = true } 14 | cosmwasm-schema = { workspace = true } 15 | cosmwasm-std = { workspace = true } 16 | schemars = { workspace = true } 17 | serde = { workspace = true } 18 | thiserror = { workspace = true } 19 | -------------------------------------------------------------------------------- /packages/cw3/README.md: -------------------------------------------------------------------------------- 1 | # CW3 Spec: MultiSig/Voting Contracts 2 | 3 | CW3 is a specification for voting contracts based on CosmWasm. It is an extension of CW1 (which served as an immediate 1 4 | of N multisig). In this case, no key can immediately execute, but only propose a set of messages for execution. The 5 | proposal, subsequent approvals, and signature aggregation all happen on chain. 6 | 7 | There are at least 3 different cases we want to cover in this spec: 8 | 9 | - K of N immutible multisig. One key proposes a set of messages, after K-1 others approve it, it can be executed with 10 | the multisig address. 11 | - K of N flexible, mutable multisig. Like above, but with multiple contracts. One contract stores the group, which is 12 | referenced from multiple multisig contracts (which in turn implement cw3). One cw3 contracts is able to update the 13 | group content (maybe needing 67% vote). Other cw3 contracts may hold tokens, staking rights, etc with various 14 | execution thresholds, all controlled by one group. (Group interface and updating them will be defined in a future 15 | spec, likely cw4). 16 | 17 | This should fit in this interface (possibly with some extensions for pieces, but the usage should look the same 18 | externally): 19 | 20 | - Token weighted voting. People lock tokens in a contract for voting rights. There is a vote threshold to execute 21 | messages. The voting set is dynamic. This has a similar "propose, approve, execute" flow, but we will need to support 22 | clear YES/NO votes and quora not just absolute thresholds. 23 | 24 | The common denominator is that they allow you to propose arbitrary `CosmosMsg` to a contract, and allow a series of 25 | votes/approvals to determine if it can be executed, as well as a final step to execute any approved proposal once. 26 | 27 | ## Base 28 | 29 | The following interfaces must be implemented for all cw3 contracts. Note that updating the members of the voting 30 | contract is not contained here (one approach is defined in cw4). Also, how to change the threshold rules (if at all) is 31 | not standardized. Those are considered admin tasks, and the common API is designed for standard usage, as that is where 32 | we can standardize the most tooling without limiting more complex governance controls. 33 | 34 | ### Messages 35 | 36 | `Propose{title, description, msgs, earliest, latest}` - This accepts `Vec` and creates a new proposal. This 37 | will return an auto-generated ID in the `Data` field (and the logs) that can be used to reference the proposal later. 38 | 39 | If the Proposer is a valid voter on the proposal, this will imply a Yes vote by the Proposer for a faster workflow, 40 | especially useful in eg. 2 of 3 or 3 of 5 multisig, we don't need to propose in one block, get result, and vote in 41 | another block. 42 | 43 | Earliest and latest are optional and can request the first and last height/time that we can try `Execute`. For a vote, 44 | we may require at least 2 days to pass, but no more than 7. This is optional and even if set, may be modified by the 45 | contract (overriding or just enforcing min/max/default values). 46 | 47 | Many implementations will want to restrict who can propose. Maybe only people in the voting set. Maybe there is some 48 | deposit to be made along with the proposal. This is not in the spec but left open to the implementation. 49 | 50 | Attributes emitted: 51 | 52 | | Key | Value | 53 | | ------------- | ---------------------- | 54 | | "action" | "propose" | 55 | | "sender" | msg sender | 56 | | "proposal_id" | a UID for the proposal | 57 | | "status" | new proposal status | 58 | 59 | `Vote{proposal_id, vote}` - Given a proposal_id, you can vote yes, no, abstain or veto. Each signed may have a different 60 | "weight" in the voting and they apply their entire weight on the vote. 61 | 62 | Many contracts (like typical multisig with absolute threshold) may consider veto and abstain as no and just count yes 63 | votes. Contracts with quora may count abstain towards quora but not yes or no for threshold. Some contracts may give 64 | extra power to veto rather than a simple no, but this may just act like a normal no vote. 65 | 66 | Attributes emitted: 67 | 68 | | Key | Value | 69 | | ------------- | ---------------------- | 70 | | "action" | "vote" | 71 | | "sender" | msg sender | 72 | | "proposal_id" | a UID for the proposal | 73 | | "status" | new proposal status | 74 | 75 | `Execute{proposal_id}` - This will check if the voting conditions have passed for the given proposal. If it has 76 | succeeded, the proposal is marked as `Executed` and the messages are dispatched. If the messages fail (eg out of gas), 77 | this is all reverted and can be tried again later with more gas. 78 | 79 | Attributes emitted: 80 | 81 | | Key | Value | 82 | | ------------- | ---------------------- | 83 | | "action" | "execute" | 84 | | "sender" | msg sender | 85 | | "proposal_id" | a UID for the proposal | 86 | 87 | `Close{proposal_id}` - This will check if the voting conditions have failed for the given proposal. If so (eg. time 88 | expired and insufficient votes), then the proposal is marked `Failed`. This is not strictly necessary, as it will only 89 | act when it is impossible the contract would ever be executed, but can be triggered to provide some better UI. 90 | 91 | Attributes emitted: 92 | 93 | | Key | Value | 94 | | ------------- | ---------------------- | 95 | | "action" | "close" | 96 | | "sender" | msg sender | 97 | | "proposal_id" | a UID for the proposal | 98 | 99 | ### Queries 100 | 101 | `Threshold{}` - This returns information on the rules needed to declare a contract a success. What percentage of the 102 | votes and how they are tallied. 103 | 104 | `Proposal{proposal_id}` - Returns the information set when creating the proposal, along with the current status. 105 | 106 | `ListProposals{start_after, limit}` - Returns the same info as `Proposal`, but for all proposals along with pagination. 107 | Starts at proposal_id 1 and accending. 108 | 109 | `ReverseProposals{start_before, limit}` - Returns the same info as `Proposal`, but for all proposals along with 110 | pagination. Starts at latest proposal_id and descending. (Often this is what you will want for a UI) 111 | 112 | `Vote{proposal_id, voter}` - Returns how the given voter (HumanAddr) voted on the proposal. (May be null) 113 | 114 | `ListVotes{proposal_id, start_after, limit}` - Returns the same info as `Vote`, but for all votes along with pagination. 115 | Returns the voters sorted by the voters' address in lexographically ascending order. 116 | 117 | ## Voter Info 118 | 119 | Information on who can vote is contract dependent. But we will work on a common API to display some of this. 120 | 121 | `Voter { address }` - returns voting power (weight) of this address, if any 122 | 123 | `ListVoters { start_after, limit }` - list all eligable voters 124 | -------------------------------------------------------------------------------- /packages/cw3/src/bin/schema.rs: -------------------------------------------------------------------------------- 1 | use std::env::current_dir; 2 | use std::fs::create_dir_all; 3 | 4 | use cosmwasm_schema::{export_schema, export_schema_with_title, remove_schemas, schema_for}; 5 | 6 | use cw3::{ 7 | Cw3ExecuteMsg, Cw3QueryMsg, ProposalListResponse, ProposalResponse, VoteListResponse, 8 | VoteResponse, VoterDetail, VoterListResponse, VoterResponse, 9 | }; 10 | use cw_utils::ThresholdResponse; 11 | 12 | fn main() { 13 | let mut out_dir = current_dir().unwrap(); 14 | out_dir.push("schema"); 15 | create_dir_all(&out_dir).unwrap(); 16 | remove_schemas(&out_dir).unwrap(); 17 | 18 | export_schema_with_title(&schema_for!(Cw3ExecuteMsg), &out_dir, "ExecuteMsg"); 19 | export_schema_with_title(&schema_for!(Cw3QueryMsg), &out_dir, "QueryMsg"); 20 | export_schema_with_title(&schema_for!(ProposalResponse), &out_dir, "ProposalResponse"); 21 | export_schema(&schema_for!(ProposalListResponse), &out_dir); 22 | export_schema(&schema_for!(VoteResponse), &out_dir); 23 | export_schema(&schema_for!(VoteListResponse), &out_dir); 24 | export_schema(&schema_for!(VoterResponse), &out_dir); 25 | export_schema(&schema_for!(VoterDetail), &out_dir); 26 | export_schema(&schema_for!(VoterListResponse), &out_dir); 27 | export_schema(&schema_for!(ThresholdResponse), &out_dir); 28 | } 29 | -------------------------------------------------------------------------------- /packages/cw3/src/deposit.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | use cw_utils::{must_pay, PaymentError}; 3 | use thiserror::Error; 4 | 5 | use cosmwasm_std::{ 6 | to_json_binary, Addr, BankMsg, Coin, CosmosMsg, Deps, MessageInfo, StdResult, Uint128, WasmMsg, 7 | }; 8 | use cw20::{Denom, UncheckedDenom}; 9 | 10 | /// Information about the deposit required to create a proposal. 11 | #[cw_serde] 12 | pub struct DepositInfo { 13 | /// The number tokens required for payment. 14 | pub amount: Uint128, 15 | /// The denom of the deposit payment. 16 | pub denom: Denom, 17 | /// Should failed proposals have their deposits refunded? 18 | pub refund_failed_proposals: bool, 19 | } 20 | 21 | /// Information about the deposit required to create a proposal. For 22 | /// use in messages. To validate, transform into `DepositInfo` via 23 | /// `into_checked()`. 24 | #[cw_serde] 25 | pub struct UncheckedDepositInfo { 26 | /// The number tokens required for payment. 27 | pub amount: Uint128, 28 | /// The denom of the deposit payment. 29 | pub denom: UncheckedDenom, 30 | /// Should failed proposals have their deposits refunded? 31 | pub refund_failed_proposals: bool, 32 | } 33 | 34 | #[derive(Error, Debug, PartialEq, Eq)] 35 | pub enum DepositError { 36 | #[error("Invalid zero deposit. Set the deposit to None to have no deposit.")] 37 | ZeroDeposit {}, 38 | 39 | #[error("Invalid cw20")] 40 | InvalidCw20 {}, 41 | 42 | #[error("{0}")] 43 | Payment(#[from] PaymentError), 44 | 45 | #[error("Invalid native deposit amount")] 46 | InvalidDeposit {}, 47 | } 48 | 49 | impl UncheckedDepositInfo { 50 | /// Checks deposit info. 51 | pub fn into_checked(self, deps: Deps) -> Result { 52 | if self.amount.is_zero() { 53 | Err(DepositError::ZeroDeposit {}) 54 | } else { 55 | Ok(DepositInfo { 56 | amount: self.amount, 57 | denom: self 58 | .denom 59 | .into_checked(deps) 60 | .map_err(|_| DepositError::InvalidCw20 {})?, 61 | refund_failed_proposals: self.refund_failed_proposals, 62 | }) 63 | } 64 | } 65 | } 66 | 67 | impl DepositInfo { 68 | pub fn check_native_deposit_paid(&self, info: &MessageInfo) -> Result<(), DepositError> { 69 | if let Self { 70 | amount, 71 | denom: Denom::Native(denom), 72 | .. 73 | } = self 74 | { 75 | let paid = must_pay(info, denom)?; 76 | if paid != *amount { 77 | Err(DepositError::InvalidDeposit {}) 78 | } else { 79 | Ok(()) 80 | } 81 | } else { 82 | Ok(()) 83 | } 84 | } 85 | 86 | pub fn get_take_deposit_messages( 87 | &self, 88 | depositor: &Addr, 89 | contract: &Addr, 90 | ) -> StdResult> { 91 | let take_deposit_msg: Vec = if let DepositInfo { 92 | amount, 93 | denom: Denom::Cw20(address), 94 | .. 95 | } = self 96 | { 97 | // into_checked() makes sure this isn't the case, but just for 98 | // posterity. 99 | if amount.is_zero() { 100 | vec![] 101 | } else { 102 | vec![WasmMsg::Execute { 103 | contract_addr: address.to_string(), 104 | funds: vec![], 105 | msg: to_json_binary(&cw20::Cw20ExecuteMsg::TransferFrom { 106 | owner: depositor.to_string(), 107 | recipient: contract.to_string(), 108 | amount: *amount, 109 | })?, 110 | } 111 | .into()] 112 | } 113 | } else { 114 | vec![] 115 | }; 116 | Ok(take_deposit_msg) 117 | } 118 | 119 | pub fn get_return_deposit_message(&self, depositor: &Addr) -> StdResult { 120 | let message = match &self.denom { 121 | Denom::Native(denom) => BankMsg::Send { 122 | to_address: depositor.to_string(), 123 | amount: vec![Coin { 124 | amount: self.amount, 125 | denom: denom.to_string(), 126 | }], 127 | } 128 | .into(), 129 | Denom::Cw20(address) => WasmMsg::Execute { 130 | contract_addr: address.to_string(), 131 | msg: to_json_binary(&cw20::Cw20ExecuteMsg::Transfer { 132 | recipient: depositor.to_string(), 133 | amount: self.amount, 134 | })?, 135 | funds: vec![], 136 | } 137 | .into(), 138 | }; 139 | Ok(message) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /packages/cw3/src/helpers.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | use cosmwasm_std::{to_json_binary, Addr, CosmosMsg, StdResult, WasmMsg}; 3 | 4 | use crate::msg::{Cw3ExecuteMsg, Vote}; 5 | use cw_utils::Expiration; 6 | 7 | /// Cw3Contract is a wrapper around Addr that provides a lot of helpers 8 | /// for working with this. 9 | /// 10 | /// If you wish to persist this, convert to Cw3CanonicalContract via .canonical() 11 | /// 12 | /// FIXME: Cw3Contract currently only supports CosmosMsg. When we actually 13 | /// use this in some consuming code, we should make it generic over CosmosMsg. 14 | #[cw_serde] 15 | pub struct Cw3Contract(pub Addr); 16 | 17 | impl Cw3Contract { 18 | pub fn addr(&self) -> Addr { 19 | self.0.clone() 20 | } 21 | 22 | pub fn encode_msg(&self, msg: Cw3ExecuteMsg) -> StdResult { 23 | Ok(WasmMsg::Execute { 24 | contract_addr: self.addr().into(), 25 | msg: to_json_binary(&msg)?, 26 | funds: vec![], 27 | } 28 | .into()) 29 | } 30 | 31 | /// helper doesn't support custom messages now 32 | pub fn proposal, U: Into>( 33 | &self, 34 | title: T, 35 | description: U, 36 | msgs: Vec, 37 | earliest: Option, 38 | latest: Option, 39 | ) -> StdResult { 40 | let msg = Cw3ExecuteMsg::Propose { 41 | title: title.into(), 42 | description: description.into(), 43 | msgs, 44 | earliest, 45 | latest, 46 | }; 47 | self.encode_msg(msg) 48 | } 49 | 50 | pub fn vote(&self, proposal_id: u64, vote: Vote) -> StdResult { 51 | let msg = Cw3ExecuteMsg::Vote { proposal_id, vote }; 52 | self.encode_msg(msg) 53 | } 54 | 55 | pub fn execute(&self, proposal_id: u64) -> StdResult { 56 | let msg = Cw3ExecuteMsg::Execute { proposal_id }; 57 | self.encode_msg(msg) 58 | } 59 | 60 | pub fn close(&self, proposal_id: u64) -> StdResult { 61 | let msg = Cw3ExecuteMsg::Close { proposal_id }; 62 | self.encode_msg(msg) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/cw3/src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | CW3 is a specification for voting contracts based on CosmWasm. 3 | It is an extension of CW1 (which served as an immediate 1 of N multisig). 4 | In this case, no key can immediately execute, but only propose 5 | a set of messages for execution. The proposal, subsequent 6 | approvals, and signature aggregation all happen on chain. 7 | 8 | For more information on this specification, please check out the 9 | [README](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw3/README.md). 10 | */ 11 | 12 | // mod helpers; 13 | mod deposit; 14 | mod helpers; 15 | mod msg; 16 | mod proposal; 17 | mod query; 18 | 19 | pub use crate::deposit::{DepositError, DepositInfo, UncheckedDepositInfo}; 20 | pub use crate::helpers::Cw3Contract; 21 | pub use crate::msg::{Cw3ExecuteMsg, Vote}; 22 | pub use crate::proposal::{Ballot, Proposal, Votes}; 23 | pub use crate::query::{ 24 | Cw3QueryMsg, ProposalListResponse, ProposalResponse, Status, VoteInfo, VoteListResponse, 25 | VoteResponse, VoterDetail, VoterListResponse, VoterResponse, 26 | }; 27 | -------------------------------------------------------------------------------- /packages/cw3/src/msg.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | use std::fmt; 4 | 5 | use cosmwasm_schema::cw_serde; 6 | use cosmwasm_std::{CosmosMsg, Empty}; 7 | use cw_utils::Expiration; 8 | 9 | #[cw_serde] 10 | 11 | pub enum Cw3ExecuteMsg 12 | where 13 | T: Clone + fmt::Debug + PartialEq + JsonSchema, 14 | { 15 | Propose { 16 | title: String, 17 | description: String, 18 | msgs: Vec>, 19 | earliest: Option, 20 | latest: Option, 21 | }, 22 | Vote { 23 | proposal_id: u64, 24 | vote: Vote, 25 | }, 26 | Execute { 27 | proposal_id: u64, 28 | }, 29 | Close { 30 | proposal_id: u64, 31 | }, 32 | } 33 | 34 | #[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, JsonSchema, Debug)] 35 | #[serde(rename_all = "lowercase")] 36 | pub enum Vote { 37 | /// Marks support for the proposal. 38 | Yes, 39 | /// Marks opposition to the proposal. 40 | No, 41 | /// Marks participation but does not count towards the ratio of support / opposed 42 | Abstain, 43 | /// Veto is generally to be treated as a No vote. Some implementations may allow certain 44 | /// voters to be able to Veto, or them to be counted stronger than No in some way. 45 | Veto, 46 | } 47 | 48 | #[cfg(test)] 49 | mod test { 50 | use super::*; 51 | use cosmwasm_std::to_json_vec; 52 | 53 | #[test] 54 | fn vote_encoding() { 55 | let a = Vote::Yes; 56 | let encoded = to_json_vec(&a).unwrap(); 57 | let json = String::from_utf8_lossy(&encoded).to_string(); 58 | assert_eq!(r#""yes""#, json.as_str()); 59 | } 60 | 61 | #[test] 62 | fn vote_encoding_embedded() { 63 | let msg = Cw3ExecuteMsg::Vote:: { 64 | proposal_id: 17, 65 | vote: Vote::No, 66 | }; 67 | let encoded = to_json_vec(&msg).unwrap(); 68 | let json = String::from_utf8_lossy(&encoded).to_string(); 69 | assert_eq!(r#"{"vote":{"proposal_id":17,"vote":"no"}}"#, json.as_str()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/cw3/src/query.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | use cosmwasm_std::{Addr, CosmosMsg, Empty}; 3 | use cw_utils::{Expiration, ThresholdResponse}; 4 | 5 | use crate::{msg::Vote, DepositInfo}; 6 | 7 | #[cw_serde] 8 | pub enum Cw3QueryMsg { 9 | /// Returns the threshold rules that would be used for a new proposal that was 10 | /// opened right now. The threshold rules do not change often, but the `total_weight` 11 | /// in the response may easily differ from that used in previously opened proposals. 12 | /// Returns ThresholdResponse. 13 | Threshold {}, 14 | /// Returns details of the proposal state. Returns ProposalResponse. 15 | Proposal { proposal_id: u64 }, 16 | /// Iterate over details of all proposals from oldest to newest. Returns ProposalListResponse 17 | ListProposals { 18 | start_after: Option, 19 | limit: Option, 20 | }, 21 | /// Iterate reverse over details of all proposals, this is useful to easily query 22 | /// only the most recent proposals (to get updates). Returns ProposalListResponse 23 | ReverseProposals { 24 | start_before: Option, 25 | limit: Option, 26 | }, 27 | /// Query the vote made by the given voter on `proposal_id`. This should 28 | /// return an error if there is no such proposal. It will return a None value 29 | /// if the proposal exists but the voter did not vote. Returns VoteResponse 30 | Vote { proposal_id: u64, voter: String }, 31 | /// Iterate (with pagination) over all votes for this proposal. The ordering is arbitrary, 32 | /// unlikely to be sorted by address. But ordering is consistent and pagination from the end 33 | /// of each page will cover all votes for the proposal. Returns VoteListResponse 34 | ListVotes { 35 | proposal_id: u64, 36 | start_after: Option, 37 | limit: Option, 38 | }, 39 | /// Voter extension: Returns VoterResponse 40 | Voter { address: String }, 41 | /// ListVoters extension: Returns VoterListResponse 42 | ListVoters { 43 | start_after: Option, 44 | limit: Option, 45 | }, 46 | } 47 | 48 | /// Note, if you are storing custom messages in the proposal, 49 | /// the querier needs to know what possible custom message types 50 | /// those are in order to parse the response 51 | #[cw_serde] 52 | pub struct ProposalResponse { 53 | pub id: u64, 54 | pub title: String, 55 | pub description: String, 56 | pub msgs: Vec>, 57 | pub status: Status, 58 | pub expires: Expiration, 59 | /// This is the threshold that is applied to this proposal. Both 60 | /// the rules of the voting contract, as well as the total_weight 61 | /// of the voting group may have changed since this time. That 62 | /// means that the generic `Threshold{}` query does not provide 63 | /// valid information for existing proposals. 64 | pub threshold: ThresholdResponse, 65 | pub proposer: Addr, 66 | pub deposit: Option, 67 | } 68 | 69 | #[cw_serde] 70 | #[derive(Copy)] 71 | #[repr(u8)] 72 | pub enum Status { 73 | /// proposal was created, but voting has not yet begun for whatever reason 74 | Pending = 1, 75 | /// you can vote on this 76 | Open = 2, 77 | /// voting is over and it did not pass 78 | Rejected = 3, 79 | /// voting is over and it did pass, but has not yet executed 80 | Passed = 4, 81 | /// voting is over it passed, and the proposal was executed 82 | Executed = 5, 83 | } 84 | 85 | #[cw_serde] 86 | pub struct ProposalListResponse { 87 | pub proposals: Vec>, 88 | } 89 | 90 | #[cw_serde] 91 | pub struct VoteListResponse { 92 | pub votes: Vec, 93 | } 94 | 95 | /// Returns the vote (opinion as well as weight counted) as well as 96 | /// the address of the voter who submitted it 97 | #[cw_serde] 98 | pub struct VoteInfo { 99 | pub proposal_id: u64, 100 | pub voter: String, 101 | pub vote: Vote, 102 | pub weight: u64, 103 | } 104 | 105 | #[cw_serde] 106 | pub struct VoteResponse { 107 | pub vote: Option, 108 | } 109 | 110 | #[cw_serde] 111 | pub struct VoterResponse { 112 | pub weight: Option, 113 | } 114 | 115 | #[cw_serde] 116 | pub struct VoterListResponse { 117 | pub voters: Vec, 118 | } 119 | 120 | #[cw_serde] 121 | pub struct VoterDetail { 122 | pub addr: String, 123 | pub weight: u64, 124 | } 125 | -------------------------------------------------------------------------------- /packages/cw4/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --lib --target wasm32-unknown-unknown" 3 | wasm-debug = "build --lib --target wasm32-unknown-unknown" 4 | schema = "run --bin schema" 5 | -------------------------------------------------------------------------------- /packages/cw4/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw4" 3 | version.workspace = true 4 | authors = ["Ethan Frey "] 5 | edition = "2021" 6 | description = "CosmWasm-4 Interface: Groups Members" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cw-plus" 9 | homepage = "https://cosmwasm.com" 10 | 11 | [dependencies] 12 | cw-storage-plus = { workspace = true } 13 | cosmwasm-schema = { workspace = true } 14 | cosmwasm-std = { workspace = true } 15 | schemars = { workspace = true } 16 | serde = { workspace = true } 17 | -------------------------------------------------------------------------------- /packages/cw4/README.md: -------------------------------------------------------------------------------- 1 | # CW4 Spec: Group Members 2 | 3 | CW4 is a spec for storing group membership, which can be combined with CW3 multisigs. The purpose is to store a set of 4 | members/voters that can be accessed to determine permissions in another section. 5 | 6 | Since this is often deployed as a contract pair, we expect this contract to often be queried with `QueryRaw` and the 7 | internal layout of some of the data structures becomes part of the public API. Implementations may add more data 8 | structures, but at least the ones laid out here should be under the specified keys and in the same format. 9 | 10 | In this case, a cw3 contract could _read_ an external group contract with no significant cost besides reading local 11 | storage. However, updating that group contract (if allowed), would be an external message and will be charged as part of 12 | the overhead for each contract. 13 | 14 | ## Messages 15 | 16 | We define an `InstantiateMsg{admin, members}` to make it easy to set up a group as part of another flow. Implementations 17 | should work with this setup, but may add extra `Option` fields for non-essential extensions to configure in the 18 | `instantiate` phase. 19 | 20 | There are three messages supported by a group contract: 21 | 22 | `UpdateAdmin{admin}` - changes (or clears) the admin for the contract 23 | 24 | Attributes emitted: 25 | 26 | | Key | Value | 27 | | --------- | ------------------------ | 28 | | "action" | "update_members" | 29 | | "sender" | msg sender | 30 | | "added" | count of added members | 31 | | "removed" | count of removed members | 32 | 33 | `AddHook{addr}` - adds a contract address to be called upon every `UpdateMembers` call. This can only be called by the 34 | admin, and care must be taken. A contract returning an error or running out of gas will revert the membership change 35 | (see more in Hooks section below). 36 | 37 | Attributes emitted: 38 | 39 | | Key | Value | 40 | | -------- | ------------ | 41 | | "action" | "add_hook" | 42 | | "sender" | msg sender | 43 | | "hook" | hook address | 44 | 45 | `RemoveHook{addr}` - unregister a contract address that was previously set by `AddHook`. 46 | 47 | Attributes emitted: 48 | 49 | | Key | Value | 50 | | -------- | ------------- | 51 | | "action" | "remove_hook" | 52 | | "sender" | msg sender | 53 | | "hook" | hook address | 54 | 55 | Only the `admin` may execute any of these function. Thus, by omitting an `admin`, we end up with a similar functionality 56 | ad `cw3-fixed-multisig`. If we include one, it may often be desired to be a `cw3` contract that uses this group contract 57 | as a group. This leads to a bit of chicken-and-egg problem, but we cover how to instantiate that in 58 | [`cw3-flex-multisig`](../../contracts/cw3-flex-multisig/README.md#instantiation). 59 | 60 | ## Queries 61 | 62 | ### Smart 63 | 64 | `TotalWeight{}` - Returns the total weight of all current members, this is very useful if some conditions are defined on 65 | a "percentage of members". 66 | 67 | `Member{addr, height}` - Returns the weight of this voter if they are a member of the group (may be 0), or `None` if 68 | they are not a member of the group. If height is set and the cw4 implementation supports snapshots, this will return the 69 | weight of that member at the beginning of the block with the given height. 70 | 71 | `MemberList{start_after, limit}` - Allows us to paginate over the list of all members. 0-weight members will be 72 | included. Removed members will not. 73 | 74 | `Admin{}` - Returns the `admin` address, or `None` if unset. 75 | 76 | ### Raw 77 | 78 | In addition to the above "SmartQueries", which make up the public API, we define two raw queries that are designed for 79 | more efficiency in contract-contract calls. These use keys exported by `cw4` 80 | 81 | `TOTAL_KEY` - making a raw query with this key (`b"total"`) will return a JSON-encoded `u64` 82 | 83 | `members_key()` - takes a `CanonicalAddr` and returns a key that can be used for raw query 84 | (`"\x00\x07members" || addr`). This will return empty bytes if the member is not inside the group, otherwise a 85 | JSON-encoded `u64` 86 | 87 | ## Hooks 88 | 89 | One special feature of the `cw4` contracts is they allow the admin to register multiple hooks. These are special 90 | contracts that need to react to changes in the group membership, and this allows them stay in sync. Again, note this is 91 | a powerful ability and you should only set hooks to contracts you fully trust, generally some contracts you deployed 92 | alongside the group. 93 | 94 | If a contract is registered as a hook on a cw4 contract, then anytime `UpdateMembers` is successfully executed, the hook 95 | will receive a `handle` call with the following format: 96 | 97 | ```json 98 | { 99 | "member_changed_hook": { 100 | "diffs": [ 101 | { 102 | "addr": "cosmos1y3x7q772u8s25c5zve949fhanrhvmtnu484l8z", 103 | "old_weight": 20, 104 | "new_weight": 24 105 | } 106 | ] 107 | } 108 | } 109 | ``` 110 | 111 | See [hook.rs](./src/hook.rs) for full details. Note that this example shows an update or an existing member. 112 | `old_weight` will be missing if the address was added for the first time. And `new_weight` will be missing if the 113 | address was removed. 114 | 115 | The receiving contract must be able to handle the `MemberChangedHookMsg` and should only return an error if it wants to 116 | change the functionality of the group contract (eg. a multisig that wants to prevent membership changes while there is 117 | an open proposal). However, such cases are quite rare and often point to fragile code. 118 | 119 | Note that the message sender will be the group contract that was updated. Make sure you check this when handling, so 120 | external actors cannot call this hook, only the trusted group. 121 | -------------------------------------------------------------------------------- /packages/cw4/src/bin/schema.rs: -------------------------------------------------------------------------------- 1 | use std::env::current_dir; 2 | use std::fs::create_dir_all; 3 | 4 | use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; 5 | 6 | pub use cw4::{ 7 | AdminResponse, Cw4ExecuteMsg, Cw4QueryMsg, Member, MemberChangedHookMsg, MemberListResponse, 8 | MemberResponse, TotalWeightResponse, 9 | }; 10 | 11 | fn main() { 12 | let mut out_dir = current_dir().unwrap(); 13 | out_dir.push("schema"); 14 | create_dir_all(&out_dir).unwrap(); 15 | remove_schemas(&out_dir).unwrap(); 16 | 17 | export_schema(&schema_for!(Cw4ExecuteMsg), &out_dir); 18 | export_schema(&schema_for!(Cw4QueryMsg), &out_dir); 19 | export_schema(&schema_for!(AdminResponse), &out_dir); 20 | export_schema(&schema_for!(MemberListResponse), &out_dir); 21 | export_schema(&schema_for!(MemberResponse), &out_dir); 22 | export_schema(&schema_for!(TotalWeightResponse), &out_dir); 23 | export_schema(&schema_for!(MemberChangedHookMsg), &out_dir); 24 | } 25 | -------------------------------------------------------------------------------- /packages/cw4/src/helpers.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | use cosmwasm_std::{ 3 | to_json_binary, Addr, CosmosMsg, CustomQuery, QuerierWrapper, QueryRequest, StdResult, WasmMsg, 4 | WasmQuery, 5 | }; 6 | 7 | use crate::msg::Cw4ExecuteMsg; 8 | use crate::query::HooksResponse; 9 | use crate::{ 10 | AdminResponse, Cw4QueryMsg, Member, MemberListResponse, MemberResponse, MEMBERS_KEY, TOTAL_KEY, 11 | }; 12 | use cw_storage_plus::{Item, Map}; 13 | 14 | /// Cw4Contract is a wrapper around Addr that provides a lot of helpers 15 | /// for working with cw4 contracts 16 | /// 17 | /// If you wish to persist this, convert to Cw4CanonicalContract via .canonical() 18 | #[cw_serde] 19 | pub struct Cw4Contract(pub Addr); 20 | 21 | impl Cw4Contract { 22 | pub fn new(addr: Addr) -> Self { 23 | Cw4Contract(addr) 24 | } 25 | 26 | pub fn addr(&self) -> Addr { 27 | self.0.clone() 28 | } 29 | 30 | fn encode_msg(&self, msg: Cw4ExecuteMsg) -> StdResult { 31 | Ok(WasmMsg::Execute { 32 | contract_addr: self.addr().into(), 33 | msg: to_json_binary(&msg)?, 34 | funds: vec![], 35 | } 36 | .into()) 37 | } 38 | 39 | pub fn add_hook>(&self, addr: T) -> StdResult { 40 | let msg = Cw4ExecuteMsg::AddHook { addr: addr.into() }; 41 | self.encode_msg(msg) 42 | } 43 | 44 | pub fn remove_hook>(&self, addr: T) -> StdResult { 45 | let msg = Cw4ExecuteMsg::RemoveHook { addr: addr.into() }; 46 | self.encode_msg(msg) 47 | } 48 | 49 | pub fn update_admin>(&self, admin: Option) -> StdResult { 50 | let msg = Cw4ExecuteMsg::UpdateAdmin { 51 | admin: admin.map(|x| x.into()), 52 | }; 53 | self.encode_msg(msg) 54 | } 55 | 56 | fn encode_smart_query(&self, msg: Cw4QueryMsg) -> StdResult> { 57 | Ok(WasmQuery::Smart { 58 | contract_addr: self.addr().into(), 59 | msg: to_json_binary(&msg)?, 60 | } 61 | .into()) 62 | } 63 | 64 | /// Show the hooks 65 | pub fn hooks(&self, querier: &QuerierWrapper) -> StdResult> { 66 | let query = self.encode_smart_query(Cw4QueryMsg::Hooks {})?; 67 | let res: HooksResponse = querier.query(&query)?; 68 | Ok(res.hooks) 69 | } 70 | 71 | /// Read the total weight 72 | pub fn total_weight(&self, querier: &QuerierWrapper) -> StdResult { 73 | Item::new(TOTAL_KEY).query(querier, self.addr()) 74 | } 75 | 76 | /// Check if this address is a member and returns its weight 77 | pub fn is_member( 78 | &self, 79 | querier: &QuerierWrapper, 80 | member: &Addr, 81 | height: Option, 82 | ) -> StdResult> { 83 | match height { 84 | Some(height) => self.member_at_height(querier, member.to_string(), height.into()), 85 | None => Map::new(MEMBERS_KEY).query(querier, self.addr(), member), 86 | } 87 | } 88 | 89 | /// Check if this address is a member, and if its weight is >= 1 90 | /// Returns member's weight in positive case 91 | pub fn is_voting_member( 92 | &self, 93 | querier: &QuerierWrapper, 94 | member: &Addr, 95 | height: impl Into>, 96 | ) -> StdResult> { 97 | if let Some(weight) = self.member_at_height(querier, member.to_string(), height.into())? { 98 | if weight >= 1 { 99 | return Ok(Some(weight)); 100 | } 101 | } 102 | Ok(None) 103 | } 104 | 105 | /// Return the member's weight at the given snapshot - requires a smart query 106 | pub fn member_at_height( 107 | &self, 108 | querier: &QuerierWrapper, 109 | member: impl Into, 110 | at_height: Option, 111 | ) -> StdResult> { 112 | let query = self.encode_smart_query(Cw4QueryMsg::Member { 113 | addr: member.into(), 114 | at_height, 115 | })?; 116 | let res: MemberResponse = querier.query(&query)?; 117 | Ok(res.weight) 118 | } 119 | 120 | pub fn list_members( 121 | &self, 122 | querier: &QuerierWrapper, 123 | start_after: Option, 124 | limit: Option, 125 | ) -> StdResult> { 126 | let query = self.encode_smart_query(Cw4QueryMsg::ListMembers { start_after, limit })?; 127 | let res: MemberListResponse = querier.query(&query)?; 128 | Ok(res.members) 129 | } 130 | 131 | /// Read the admin 132 | pub fn admin(&self, querier: &QuerierWrapper) -> StdResult> { 133 | let query = self.encode_smart_query(Cw4QueryMsg::Admin {})?; 134 | let res: AdminResponse = querier.query(&query)?; 135 | Ok(res.admin) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /packages/cw4/src/hook.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | use cosmwasm_std::{to_json_binary, Binary, CosmosMsg, StdResult, WasmMsg}; 3 | 4 | /// MemberDiff shows the old and new states for a given cw4 member 5 | /// They cannot both be None. 6 | /// old = None, new = Some -> Insert 7 | /// old = Some, new = Some -> Update 8 | /// old = Some, new = None -> Delete 9 | #[cw_serde] 10 | pub struct MemberDiff { 11 | pub key: String, 12 | pub old: Option, 13 | pub new: Option, 14 | } 15 | 16 | impl MemberDiff { 17 | pub fn new>(addr: T, old_weight: Option, new_weight: Option) -> Self { 18 | MemberDiff { 19 | key: addr.into(), 20 | old: old_weight, 21 | new: new_weight, 22 | } 23 | } 24 | } 25 | 26 | /// MemberChangedHookMsg should be de/serialized under `MemberChangedHook()` variant in a ExecuteMsg. 27 | /// This contains a list of all diffs on the given transaction. 28 | #[cw_serde] 29 | pub struct MemberChangedHookMsg { 30 | pub diffs: Vec, 31 | } 32 | 33 | impl MemberChangedHookMsg { 34 | pub fn one(diff: MemberDiff) -> Self { 35 | MemberChangedHookMsg { diffs: vec![diff] } 36 | } 37 | 38 | pub fn new(diffs: Vec) -> Self { 39 | MemberChangedHookMsg { diffs } 40 | } 41 | 42 | /// serializes the message 43 | pub fn into_json_binary(self) -> StdResult { 44 | let msg = MemberChangedExecuteMsg::MemberChangedHook(self); 45 | to_json_binary(&msg) 46 | } 47 | 48 | /// creates a cosmos_msg sending this struct to the named contract 49 | pub fn into_cosmos_msg>(self, contract_addr: T) -> StdResult { 50 | let msg = self.into_json_binary()?; 51 | let execute = WasmMsg::Execute { 52 | contract_addr: contract_addr.into(), 53 | msg, 54 | funds: vec![], 55 | }; 56 | Ok(execute.into()) 57 | } 58 | } 59 | 60 | // This is just a helper to properly serialize the above message 61 | #[cw_serde] 62 | 63 | enum MemberChangedExecuteMsg { 64 | MemberChangedHook(MemberChangedHookMsg), 65 | } 66 | -------------------------------------------------------------------------------- /packages/cw4/src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | CW4 is a spec for storing group membership, which can be combined 3 | with CW3 multisigs. The purpose is to store a set of members/voters 4 | that can be accessed to determine permissions in another section. 5 | 6 | Since this is often deployed as a contract pair, we expect this 7 | contract to often be queried with `QueryRaw` and the internal 8 | layout of some of the data structures becomes part of the public API. 9 | Implementations may add more data structures, but at least 10 | the ones laid out here should be under the specified keys and in the 11 | same format. 12 | 13 | In this case, a cw3 contract could *read* an external group contract with 14 | no significant cost besides reading local storage. However, updating 15 | that group contract (if allowed), would be an external message and 16 | will be charged as part of the overhead for each contract. 17 | 18 | For more information on this specification, please check out the 19 | [README](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw4/README.md). 20 | */ 21 | 22 | mod helpers; 23 | mod hook; 24 | mod msg; 25 | mod query; 26 | 27 | pub use crate::helpers::Cw4Contract; 28 | pub use crate::hook::{MemberChangedHookMsg, MemberDiff}; 29 | pub use crate::msg::Cw4ExecuteMsg; 30 | pub use crate::query::{ 31 | member_key, AdminResponse, Cw4QueryMsg, HooksResponse, Member, MemberListResponse, 32 | MemberResponse, TotalWeightResponse, MEMBERS_CHANGELOG, MEMBERS_CHECKPOINTS, MEMBERS_KEY, 33 | TOTAL_KEY, TOTAL_KEY_CHANGELOG, TOTAL_KEY_CHECKPOINTS, 34 | }; 35 | -------------------------------------------------------------------------------- /packages/cw4/src/msg.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | 3 | #[cw_serde] 4 | pub enum Cw4ExecuteMsg { 5 | /// Change the admin 6 | UpdateAdmin { admin: Option }, 7 | /// Add a new hook to be informed of all membership changes. Must be called by Admin 8 | AddHook { addr: String }, 9 | /// Remove a hook. Must be called by Admin 10 | RemoveHook { addr: String }, 11 | } 12 | -------------------------------------------------------------------------------- /packages/cw4/src/query.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_schema::cw_serde; 2 | 3 | #[cw_serde] 4 | 5 | pub enum Cw4QueryMsg { 6 | /// Return AdminResponse 7 | Admin {}, 8 | /// Return TotalWeightResponse 9 | TotalWeight { at_height: Option }, 10 | /// Returns MembersListResponse 11 | ListMembers { 12 | start_after: Option, 13 | limit: Option, 14 | }, 15 | /// Returns MemberResponse 16 | Member { 17 | addr: String, 18 | at_height: Option, 19 | }, 20 | /// Shows all registered hooks. Returns HooksResponse. 21 | Hooks {}, 22 | } 23 | 24 | #[cw_serde] 25 | pub struct AdminResponse { 26 | pub admin: Option, 27 | } 28 | 29 | /// A group member has a weight associated with them. 30 | /// This may all be equal, or may have meaning in the app that 31 | /// makes use of the group (eg. voting power) 32 | #[cw_serde] 33 | pub struct Member { 34 | pub addr: String, 35 | pub weight: u64, 36 | } 37 | 38 | #[cw_serde] 39 | pub struct MemberListResponse { 40 | pub members: Vec, 41 | } 42 | 43 | #[cw_serde] 44 | pub struct MemberResponse { 45 | pub weight: Option, 46 | } 47 | 48 | #[cw_serde] 49 | pub struct TotalWeightResponse { 50 | pub weight: u64, 51 | } 52 | 53 | #[cw_serde] 54 | pub struct HooksResponse { 55 | pub hooks: Vec, 56 | } 57 | 58 | /// TOTAL_KEY is meant for raw queries 59 | pub const TOTAL_KEY: &str = "total"; 60 | pub const TOTAL_KEY_CHECKPOINTS: &str = "total__checkpoints"; 61 | pub const TOTAL_KEY_CHANGELOG: &str = "total__changelog"; 62 | pub const MEMBERS_KEY: &str = "members"; 63 | pub const MEMBERS_CHECKPOINTS: &str = "members__checkpoints"; 64 | pub const MEMBERS_CHANGELOG: &str = "members__changelog"; 65 | 66 | /// member_key is meant for raw queries for one member, given address 67 | pub fn member_key(address: &str) -> Vec { 68 | // FIXME: Inlined here to avoid storage-plus import 69 | let mut key = [b"\x00", &[MEMBERS_KEY.len() as u8], MEMBERS_KEY.as_bytes()].concat(); 70 | key.extend_from_slice(address.as_bytes()); 71 | key 72 | } 73 | -------------------------------------------------------------------------------- /packages/easy-addr/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "easy-addr" 3 | version.workspace = true 4 | edition = "2021" 5 | publish = false 6 | 7 | [lib] 8 | proc-macro = true 9 | 10 | [dependencies] 11 | cosmwasm-std = { workspace = true } 12 | proc-macro2 = "1" 13 | quote = "1" 14 | syn = { version = "1.0.6", features = ["full", "printing", "extra-traits"] } 15 | -------------------------------------------------------------------------------- /packages/easy-addr/src/lib.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::testing::mock_dependencies; 2 | 3 | use proc_macro::TokenStream; 4 | use quote::quote; 5 | use syn::parse_macro_input; 6 | 7 | #[proc_macro] 8 | pub fn addr(input: TokenStream) -> TokenStream { 9 | let input = parse_macro_input!(input as syn::LitStr).value(); 10 | let addr = mock_dependencies() 11 | .api 12 | .addr_make(input.as_str()) 13 | .to_string(); 14 | TokenStream::from(quote! {#addr}) 15 | } 16 | -------------------------------------------------------------------------------- /scripts/optimizer.sh: -------------------------------------------------------------------------------- 1 | : 2 | 3 | U="cosmwasm" 4 | V="0.16.0" 5 | 6 | M=$(uname -m) 7 | #M="x86_64" # Force Intel arch 8 | 9 | A="linux/${M/x86_64/amd64}" 10 | S=${M#x86_64} 11 | S=${S:+-$S} 12 | 13 | docker run --platform $A --rm -v "$(pwd)":/code \ 14 | --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \ 15 | --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ 16 | $U/optimizer$S:$V 17 | -------------------------------------------------------------------------------- /scripts/update_changelog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit -o pipefail 3 | 4 | ORIGINAL_OPTS=$* 5 | OPTS=$(getopt -l "help,since-tag:,upcoming-tag:,full,token:" -o "hu:ft" -- "$@") || exit 1 6 | 7 | function print_usage() { 8 | echo -e "Usage: $0 [-h|--help] [-f|--full] [--since-tag ] [-u|--upcoming-tag] [-t|--token ] 9 | -h, --help Display help 10 | -f, --full Process changes since the beginning (by default: since latest git version tag) 11 | --since-tag Process changes since git version tag (by default: since latest git version tag) 12 | -u, --upcoming-tag Add a title in CHANGELOG for the new changes 13 | --token Pass changelog github token " 14 | } 15 | 16 | function remove_opt() { 17 | ORIGINAL_OPTS=$(echo "$ORIGINAL_OPTS" | sed "s/\\B$1\\b//") 18 | } 19 | 20 | eval set -- "$OPTS" 21 | while true 22 | do 23 | case $1 in 24 | -h|--help) 25 | print_usage 26 | exit 0 27 | ;; 28 | --since-tag) 29 | shift 30 | TAG="$1" 31 | ;; 32 | -f|--full) 33 | TAG="" 34 | remove_opt $1 35 | ;; 36 | -u|--upcoming-tag) 37 | remove_opt $1 38 | shift 39 | UPCOMING_TAG="$1" 40 | remove_opt $1 41 | ;; 42 | --) 43 | shift 44 | break 45 | ;; 46 | esac 47 | shift 48 | done 49 | 50 | 51 | if [ -z "$TAG" ] 52 | then 53 | # Use latest git version tag 54 | TAG=$(git tag --sort=creatordate | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+' | tail -1) 55 | ORIGINAL_OPTS="$ORIGINAL_OPTS --since-tag $TAG" 56 | fi 57 | 58 | echo "Git version tag: $TAG" 59 | 60 | cp CHANGELOG.md /tmp/CHANGELOG.md.$$ 61 | # Consolidate tag for matching changelog entries 62 | TAG=$(echo "$TAG" | sed -e 's/-\([A-Za-z]*\)[^A-Za-z]*/-\1/' -e 's/-$//') 63 | echo "Consolidated tag: $TAG" 64 | sed -i -n "/^## \\[${TAG}[^]]*\\]/,\$p" CHANGELOG.md 65 | 66 | github_changelog_generator -u CosmWasm -p cw-plus --base CHANGELOG.md $ORIGINAL_OPTS || cp /tmp/CHANGELOG.md.$$ CHANGELOG.md 67 | 68 | if [ -n "$UPCOMING_TAG" ] 69 | then 70 | # Add "upcoming" version tag 71 | TODAY=$(date "+%Y-%m-%d") 72 | sed -i "s+\[Full Changelog\](https://github.com/CosmWasm/cw-plus/compare/\(.*\)\.\.\.HEAD)+[Full Changelog](https://github.com/CosmWasm/cw-plus/compare/$UPCOMING_TAG...HEAD)\n\n## [$UPCOMING_TAG](https://github.com/CosmWasm/cw-plus/tree/$UPCOMING_TAG) ($TODAY)\n\n[Full Changelog](https://github.com/CosmWasm/cw-plus/compare/\1...$UPCOMING_TAG)+" CHANGELOG.md 73 | fi 74 | 75 | rm -f /tmp/CHANGELOG.md.$$ 76 | --------------------------------------------------------------------------------