From e36089303a9504a6d9d19f17d0fdc905933cb601 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Mon, 16 Sep 2024 10:34:45 +0300 Subject: [PATCH] Add tiered-whitelist-merkletree --- Cargo.lock | 22 + .../tiered-whitelist-merkletree/.cargo/config | 4 + .../tiered-whitelist-merkletree/.editorconfig | 11 + .../tiered-whitelist-merkletree/.gitignore | 15 + .../tiered-whitelist-merkletree/Cargo.toml | 45 ++ .../tiered-whitelist-merkletree/README.md | 26 + .../tiered-whitelist-merkletree/rustfmt.toml | 15 + .../schema/raw/execute.json | 146 ++++++ .../schema/raw/instantiate.json | 107 +++++ .../schema/raw/query.json | 451 ++++++++++++++++++ .../schema/raw/response_to_admin_list.json | 21 + .../schema/raw/response_to_can_execute.json | 14 + .../schema/raw/response_to_config.json | 77 +++ .../schema/raw/response_to_has_ended.json | 14 + .../schema/raw/response_to_has_member.json | 14 + .../schema/raw/response_to_has_started.json | 14 + .../schema/raw/response_to_is_active.json | 14 + .../schema/raw/response_to_merkle_root.json | 17 + .../raw/response_to_merkle_tree_u_r_i.json | 17 + .../tiered-whitelist-merkletree/src/admin.rs | 67 +++ .../src/bin/schema.rs | 11 + .../src/contract.rs | 369 ++++++++++++++ .../tiered-whitelist-merkletree/src/error.rs | 58 +++ .../src/helpers.rs | 4 + .../src/helpers/crypto.rs | 34 ++ .../src/helpers/interface.rs | 26 + .../src/helpers/utils.rs | 99 ++++ .../src/helpers/validators.rs | 5 + .../tiered-whitelist-merkletree/src/lib.rs | 7 + .../tiered-whitelist-merkletree/src/msg.rs | 129 +++++ .../tiered-whitelist-merkletree/src/state.rs | 38 ++ 31 files changed, 1891 insertions(+) create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/.cargo/config create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/.editorconfig create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/.gitignore create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/Cargo.toml create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/README.md create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/rustfmt.toml create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/schema/raw/execute.json create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/schema/raw/instantiate.json create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/schema/raw/query.json create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_admin_list.json create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_can_execute.json create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_config.json create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_has_ended.json create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_has_member.json create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_has_started.json create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_is_active.json create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_merkle_root.json create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_merkle_tree_u_r_i.json create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/src/admin.rs create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/src/bin/schema.rs create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/src/contract.rs create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/src/error.rs create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/src/helpers.rs create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/src/helpers/crypto.rs create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/src/helpers/interface.rs create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/src/helpers/utils.rs create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/src/helpers/validators.rs create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/src/lib.rs create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/src/msg.rs create mode 100644 contracts/whitelists/tiered-whitelist-merkletree/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index f3474c11b..e19929170 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4392,6 +4392,28 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "tiered-whitelist-merkletree" +version = "3.14.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "hex", + "rs_merkle", + "rust_decimal", + "schemars", + "semver", + "serde", + "serde_json", + "sg-std", + "sg1", + "thiserror", + "url", +] + [[package]] name = "time" version = "0.3.34" diff --git a/contracts/whitelists/tiered-whitelist-merkletree/.cargo/config b/contracts/whitelists/tiered-whitelist-merkletree/.cargo/config new file mode 100644 index 000000000..af5698e58 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --bin schema" diff --git a/contracts/whitelists/tiered-whitelist-merkletree/.editorconfig b/contracts/whitelists/tiered-whitelist-merkletree/.editorconfig new file mode 100644 index 000000000..3d36f20b1 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.rs] +indent_size = 4 diff --git a/contracts/whitelists/tiered-whitelist-merkletree/.gitignore b/contracts/whitelists/tiered-whitelist-merkletree/.gitignore new file mode 100644 index 000000000..dfdaaa6bc --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/.gitignore @@ -0,0 +1,15 @@ +# Build results +/target + +# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) +.cargo-ok + +# Text file backups +**/*.rs.bk + +# macOS +.DS_Store + +# IDEs +*.iml +.idea diff --git a/contracts/whitelists/tiered-whitelist-merkletree/Cargo.toml b/contracts/whitelists/tiered-whitelist-merkletree/Cargo.toml new file mode 100644 index 000000000..d26b7a654 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "tiered-whitelist-merkletree" +authors = ["Martin Mo Kromsten "] +description = "Stargaze Merkle Tree Whitelist Contract" +version = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +license = { workspace = true } + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw2 = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +rust_decimal = { version = "1.14.3" } +schemars = { workspace = true } +serde = { workspace = true } +sg1 = { workspace = true } +sg-std = { workspace = true } +thiserror = { workspace = true } +url = { workspace = true } +hex = "0.4.3" +serde_json = "1.0.105" +rs_merkle = { version = "1.4.1", default-features = false } +semver = { workspace = true } + diff --git a/contracts/whitelists/tiered-whitelist-merkletree/README.md b/contracts/whitelists/tiered-whitelist-merkletree/README.md new file mode 100644 index 000000000..da2ee3822 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/README.md @@ -0,0 +1,26 @@ +# Whitelist MerkleTree contract + +A whitelist contract that relies on MerkleTree data structure for verifying inclusion of an address in a whitelist. + +Only merkle root (and optionaly URI of a tree) are stored within the state. Inclusion can be verified by submitting a user address and hex-encoded list of merklee proofs. This approach allows significant reduction of gas usage during storage phase with a downside of having actual data off-chain and reliance on 3rd parties for providing inclusiong proofs. + +Inclusion operation is a slighly more complex and costly when compared to the standard map-based whitelist. The contract uses **Sha256** for hashing concatenated proofs. Hashes are sorted on byte level prior to concatenation, which significantly simplifies the verification process by not requiring submission of leaf positions. + +**Important:** Make sure that your algorithm for merkle tree construction also sort the hashes. See example of extending `rs-merkle` library in `tests/hasher.rs` + +## Gas Usage + +The contracts for the merkletree based whitelist and the updated minter that supports it were both deployed to the testnet to measure actual gas usage in production. The contracts were instantiated and tested with two different whitelist sizes: **703** and **91,750,400** entries + +#### Instantiating +Naturally due to only needing to store a merkle tree root in the state of the contract there is no difference between instantiating a whitelist with the [smaller](https://testnet-explorer.publicawesome.dev/stargaze/tx/07BB768915A24C17C12982D3FE34ADF0453AA9231961197A8B4E5E228D5C6B54) and the [bigger](https://testnet-explorer.publicawesome.dev/stargaze/tx/14E2DFB03AFB2A711A6AF601FA43FAEADFC8D0BA8581DD9E02EEFFB582E8AFB7) list sizes and they both consume 190,350 units of gas. + +#### Minting + +Number of hashing operations required to check for inclusion of an address in a merkle tree is at most `Math.ceil[ logâ‚‚N ]` and in some cases even smaller depending on the depth of a leaf within a tree. + +In case of the smaller tree with 704 records we had to submit 8 hash proofs and an example mint [transaction](https://testnet-explorer.publicawesome.dev/stargaze/tx/8692581537939E09BF5D81594B078436D4224F0944B515A421F096CEE480ECA9) took 635,345 units of gas + +The bigger tree with ~90 million records [used](https://testnet-explorer.publicawesome.dev/stargaze/tx/670A76A64F0A64FB1A5077DADDB6C326A9A64B66999215345C47BA3F03265811) 647,448 units of gas and required 24 proofs only (up to 27 with deeper leaves). + +The jump from computing 8 to computing 24 proofs (+16) only took additional 8 thousands units of gas. Keep in mind that another increase in 16 proofs allow us to check for inclusion in a tree with 1 trillion addresses. diff --git a/contracts/whitelists/tiered-whitelist-merkletree/rustfmt.toml b/contracts/whitelists/tiered-whitelist-merkletree/rustfmt.toml new file mode 100644 index 000000000..11a85e6a9 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/rustfmt.toml @@ -0,0 +1,15 @@ +# stable +newline_style = "unix" +hard_tabs = false +tab_spaces = 4 + +# unstable... should we require `rustup run nightly cargo fmt` ? +# or just update the style guide when they are stable? +#fn_single_line = true +#format_code_in_doc_comments = true +#overflow_delimited_expr = true +#reorder_impl_items = true +#struct_field_align_threshold = 20 +#struct_lit_single_line = true +#report_todo = "Always" + diff --git a/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/execute.json b/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/execute.json new file mode 100644 index 000000000..90ea3c02e --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/execute.json @@ -0,0 +1,146 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "update_stage_config" + ], + "properties": { + "update_stage_config": { + "$ref": "#/definitions/UpdateStageConfigMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_admins" + ], + "properties": { + "update_admins": { + "type": "object", + "required": [ + "admins" + ], + "properties": { + "admins": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "freeze" + ], + "properties": { + "freeze": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + }, + "UpdateStageConfigMsg": { + "type": "object", + "required": [ + "stage_id" + ], + "properties": { + "end_time": { + "anyOf": [ + { + "$ref": "#/definitions/Timestamp" + }, + { + "type": "null" + } + ] + }, + "mint_price": { + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "per_address_limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "stage_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "start_time": { + "anyOf": [ + { + "$ref": "#/definitions/Timestamp" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/instantiate.json b/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/instantiate.json new file mode 100644 index 000000000..da448518e --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/instantiate.json @@ -0,0 +1,107 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "admins", + "admins_mutable", + "merkle_roots", + "stages" + ], + "properties": { + "admins": { + "type": "array", + "items": { + "type": "string" + } + }, + "admins_mutable": { + "type": "boolean" + }, + "merkle_roots": { + "type": "array", + "items": { + "type": "string" + } + }, + "merkle_tree_uris": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "stages": { + "type": "array", + "items": { + "$ref": "#/definitions/Stage" + } + } + }, + "additionalProperties": false, + "definitions": { + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Stage": { + "type": "object", + "required": [ + "end_time", + "mint_price", + "name", + "per_address_limit", + "start_time" + ], + "properties": { + "end_time": { + "$ref": "#/definitions/Timestamp" + }, + "mint_price": { + "$ref": "#/definitions/Coin" + }, + "name": { + "type": "string" + }, + "per_address_limit": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "start_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/query.json b/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/query.json new file mode 100644 index 000000000..e3ed04297 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/query.json @@ -0,0 +1,451 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "has_started" + ], + "properties": { + "has_started": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "has_ended" + ], + "properties": { + "has_ended": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "is_active" + ], + "properties": { + "is_active": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "has_member" + ], + "properties": { + "has_member": { + "type": "object", + "required": [ + "member", + "proof_hashes" + ], + "properties": { + "member": { + "type": "string" + }, + "proof_hashes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "admin_list" + ], + "properties": { + "admin_list": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "can_execute" + ], + "properties": { + "can_execute": { + "type": "object", + "required": [ + "msg", + "sender" + ], + "properties": { + "msg": { + "$ref": "#/definitions/CosmosMsg_for_Empty" + }, + "sender": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "merkle_root" + ], + "properties": { + "merkle_root": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "merkle_tree_u_r_i" + ], + "properties": { + "merkle_tree_u_r_i": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "BankMsg": { + "description": "The message types of the bank module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto", + "oneOf": [ + { + "description": "Sends native tokens from the contract to the given address.\n\nThis is translated to a [MsgSend](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto#L19-L28). `from_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "send" + ], + "properties": { + "send": { + "type": "object", + "required": [ + "amount", + "to_address" + ], + "properties": { + "amount": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "to_address": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "This will burn the given coins from the contract's account. There is no Cosmos SDK message that performs this, but it can be done by calling the bank keeper. Important if a contract controls significant token supply that must be retired.", + "type": "object", + "required": [ + "burn" + ], + "properties": { + "burn": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + } + } + }, + "additionalProperties": false + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "CosmosMsg_for_Empty": { + "oneOf": [ + { + "type": "object", + "required": [ + "bank" + ], + "properties": { + "bank": { + "$ref": "#/definitions/BankMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "$ref": "#/definitions/Empty" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "wasm" + ], + "properties": { + "wasm": { + "$ref": "#/definitions/WasmMsg" + } + }, + "additionalProperties": false + } + ] + }, + "Empty": { + "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", + "type": "object" + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "WasmMsg": { + "description": "The message types of the wasm module.\n\nSee https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto", + "oneOf": [ + { + "description": "Dispatches a call to another contract at a known address (with known ABI).\n\nThis is translated to a [MsgExecuteContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L68-L78). `sender` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "execute" + ], + "properties": { + "execute": { + "type": "object", + "required": [ + "contract_addr", + "funds", + "msg" + ], + "properties": { + "contract_addr": { + "type": "string" + }, + "funds": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "msg": { + "description": "msg is the json-encoded ExecuteMsg struct (as raw Binary)", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Instantiates a new contracts from previously uploaded Wasm code.\n\nThe contract address is non-predictable. But it is guaranteed that when emitting the same Instantiate message multiple times, multiple instances on different addresses will be generated. See also Instantiate2.\n\nThis is translated to a [MsgInstantiateContract](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L53-L71). `sender` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "instantiate" + ], + "properties": { + "instantiate": { + "type": "object", + "required": [ + "code_id", + "funds", + "label", + "msg" + ], + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + }, + "code_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "funds": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "label": { + "description": "A human-readable label for the contract.\n\nValid values should: - not be empty - not be bigger than 128 bytes (or some chain-specific limit) - not start / end with whitespace", + "type": "string" + }, + "msg": { + "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Migrates a given contracts to use new wasm code. Passes a MigrateMsg to allow us to customize behavior.\n\nOnly the contract admin (as defined in wasmd), if any, is able to make this call.\n\nThis is translated to a [MsgMigrateContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L86-L96). `sender` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "migrate" + ], + "properties": { + "migrate": { + "type": "object", + "required": [ + "contract_addr", + "msg", + "new_code_id" + ], + "properties": { + "contract_addr": { + "type": "string" + }, + "msg": { + "description": "msg is the json-encoded MigrateMsg struct that will be passed to the new code", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "new_code_id": { + "description": "the code_id of the new logic to place in the given contract", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Sets a new admin (for migrate) on the given contract. Fails if this contract is not currently admin of the target contract.", + "type": "object", + "required": [ + "update_admin" + ], + "properties": { + "update_admin": { + "type": "object", + "required": [ + "admin", + "contract_addr" + ], + "properties": { + "admin": { + "type": "string" + }, + "contract_addr": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Clears the admin on the given contract, so no more migration possible. Fails if this contract is not currently admin of the target contract.", + "type": "object", + "required": [ + "clear_admin" + ], + "properties": { + "clear_admin": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_admin_list.json b/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_admin_list.json new file mode 100644 index 000000000..3b04e955c --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_admin_list.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AdminListResponse", + "type": "object", + "required": [ + "admins", + "mutable" + ], + "properties": { + "admins": { + "type": "array", + "items": { + "type": "string" + } + }, + "mutable": { + "type": "boolean" + } + }, + "additionalProperties": false +} diff --git a/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_can_execute.json b/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_can_execute.json new file mode 100644 index 000000000..e2ed10214 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_can_execute.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CanExecuteResponse", + "type": "object", + "required": [ + "can_execute" + ], + "properties": { + "can_execute": { + "type": "boolean" + } + }, + "additionalProperties": false +} diff --git a/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_config.json b/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_config.json new file mode 100644 index 000000000..e96c30e53 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_config.json @@ -0,0 +1,77 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ConfigResponse", + "type": "object", + "required": [ + "end_time", + "is_active", + "member_limit", + "mint_price", + "num_members", + "per_address_limit", + "start_time" + ], + "properties": { + "end_time": { + "$ref": "#/definitions/Timestamp" + }, + "is_active": { + "type": "boolean" + }, + "member_limit": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mint_price": { + "$ref": "#/definitions/Coin" + }, + "num_members": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "per_address_limit": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "start_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false, + "definitions": { + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_has_ended.json b/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_has_ended.json new file mode 100644 index 000000000..6e207ab48 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_has_ended.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "HasEndedResponse", + "type": "object", + "required": [ + "has_ended" + ], + "properties": { + "has_ended": { + "type": "boolean" + } + }, + "additionalProperties": false +} diff --git a/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_has_member.json b/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_has_member.json new file mode 100644 index 000000000..8e203003b --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_has_member.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "HasMemberResponse", + "type": "object", + "required": [ + "has_member" + ], + "properties": { + "has_member": { + "type": "boolean" + } + }, + "additionalProperties": false +} diff --git a/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_has_started.json b/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_has_started.json new file mode 100644 index 000000000..25614f8d8 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_has_started.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "HasStartedResponse", + "type": "object", + "required": [ + "has_started" + ], + "properties": { + "has_started": { + "type": "boolean" + } + }, + "additionalProperties": false +} diff --git a/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_is_active.json b/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_is_active.json new file mode 100644 index 000000000..2dc928c41 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_is_active.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "IsActiveResponse", + "type": "object", + "required": [ + "is_active" + ], + "properties": { + "is_active": { + "type": "boolean" + } + }, + "additionalProperties": false +} diff --git a/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_merkle_root.json b/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_merkle_root.json new file mode 100644 index 000000000..9d48a9d0a --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_merkle_root.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MerkleRootsResponse", + "type": "object", + "required": [ + "merkle_roots" + ], + "properties": { + "merkle_roots": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false +} diff --git a/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_merkle_tree_u_r_i.json b/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_merkle_tree_u_r_i.json new file mode 100644 index 000000000..59c61db66 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/schema/raw/response_to_merkle_tree_u_r_i.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MerkleTreeURIsResponse", + "type": "object", + "properties": { + "merkle_tree_uris": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false +} diff --git a/contracts/whitelists/tiered-whitelist-merkletree/src/admin.rs b/contracts/whitelists/tiered-whitelist-merkletree/src/admin.rs new file mode 100644 index 000000000..f4c065b9f --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/src/admin.rs @@ -0,0 +1,67 @@ +use cosmwasm_std::{Addr, Deps, DepsMut, Env, MessageInfo, StdResult}; +use sg_std::Response; + +use crate::{ + helpers::validators::map_validate, + msg::{AdminListResponse, CanExecuteResponse}, + state::ADMIN_LIST, + ContractError, +}; + +pub fn execute_update_admins( + deps: DepsMut, + _env: Env, + info: MessageInfo, + admins: Vec, +) -> Result { + let mut cfg = ADMIN_LIST.load(deps.storage)?; + if !cfg.can_modify(info.sender.as_ref()) { + Err(ContractError::Unauthorized {}) + } else { + cfg.admins = map_validate(deps.api, &admins)?; + ADMIN_LIST.save(deps.storage, &cfg)?; + + let res = Response::new().add_attribute("action", "update_admins"); + Ok(res) + } +} + +pub fn can_execute(deps: &DepsMut, sender: Addr) -> Result { + let cfg = ADMIN_LIST.load(deps.storage)?; + let can = cfg.is_admin(&sender); + if !can { + return Err(ContractError::Unauthorized {}); + } + Ok(sender) +} + +pub fn execute_freeze( + deps: DepsMut, + _env: Env, + info: MessageInfo, +) -> Result { + let mut cfg = ADMIN_LIST.load(deps.storage)?; + if !cfg.can_modify(info.sender.as_ref()) { + Err(ContractError::Unauthorized {}) + } else { + cfg.mutable = false; + ADMIN_LIST.save(deps.storage, &cfg)?; + + let res = Response::new().add_attribute("action", "freeze"); + Ok(res) + } +} + +pub fn query_admin_list(deps: Deps) -> StdResult { + let cfg = ADMIN_LIST.load(deps.storage)?; + Ok(AdminListResponse { + admins: cfg.admins.into_iter().map(|a| a.into()).collect(), + mutable: cfg.mutable, + }) +} + +pub fn query_can_execute(deps: Deps, sender: &str) -> StdResult { + let cfg = ADMIN_LIST.load(deps.storage)?; + let can = cfg.is_admin(deps.api.addr_validate(sender)?); + Ok(CanExecuteResponse { can_execute: can }) +} diff --git a/contracts/whitelists/tiered-whitelist-merkletree/src/bin/schema.rs b/contracts/whitelists/tiered-whitelist-merkletree/src/bin/schema.rs new file mode 100644 index 000000000..99225a8bf --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/src/bin/schema.rs @@ -0,0 +1,11 @@ +use cosmwasm_schema::write_api; + +use tiered_whitelist_merkletree::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg, + } +} diff --git a/contracts/whitelists/tiered-whitelist-merkletree/src/contract.rs b/contracts/whitelists/tiered-whitelist-merkletree/src/contract.rs new file mode 100644 index 000000000..d3b4d52c5 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/src/contract.rs @@ -0,0 +1,369 @@ +use crate::admin::{ + can_execute, execute_freeze, execute_update_admins, query_admin_list, query_can_execute, +}; +use crate::error::ContractError; +use crate::helpers::crypto::{string_to_byte_slice, valid_hash_string, verify_merkle_root}; +use crate::helpers::utils::{ + fetch_active_stage, fetch_active_stage_index, validate_stages, verify_tree_uri, +}; +use crate::helpers::validators::map_validate; +use crate::msg::{ + ConfigResponse, ExecuteMsg, HasEndedResponse, HasMemberResponse, HasStartedResponse, + InstantiateMsg, IsActiveResponse, MerkleRootsResponse, MerkleTreeURIsResponse, QueryMsg, + UpdateStageConfigMsg, +}; +use crate::state::{AdminList, Config, Stage, ADMIN_LIST, CONFIG, MERKLE_ROOTS, MERKLE_TREE_URIS}; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + ensure, to_json_binary, Binary, Coin, Deps, DepsMut, Empty, Env, Event, MessageInfo, StdError, + StdResult, Timestamp, Uint128, +}; +use cw2::set_contract_version; +use cw_utils::must_pay; +use sg_std::{Response, NATIVE_DENOM}; + +use rs_merkle::{algorithms::Sha256, Hasher}; +use semver::Version; +use sg1::checked_fair_burn; + +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:tiered-whitelist-merkletree"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +// contract governance params +pub const CREATION_FEE: u128 = 1_000_000_000; +pub const MIN_MINT_PRICE: u128 = 0; +pub const MAX_PER_ADDRESS_LIMIT: u32 = 50; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + for merkle_root in msg.merkle_roots.iter() { + verify_merkle_root(merkle_root)?; + } + if let Some(tree_uris) = msg.merkle_tree_uris.as_ref() { + for uri in tree_uris.iter() { + verify_tree_uri(uri)?; + } + } + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + let payment = must_pay(&info, NATIVE_DENOM)?; + if payment.u128() != CREATION_FEE { + return Err(ContractError::IncorrectCreationFee( + payment.u128(), + CREATION_FEE, + )); + } + + validate_stages(&env, &msg.stages)?; + + let mut res = Response::new(); + checked_fair_burn(&info, CREATION_FEE, None, &mut res)?; + + let config = Config { stages: msg.stages }; + + let admin_config = AdminList { + admins: map_validate(deps.api, &msg.admins)?, + mutable: msg.admins_mutable, + }; + + MERKLE_ROOTS.save(deps.storage, &msg.merkle_roots)?; + ADMIN_LIST.save(deps.storage, &admin_config)?; + CONFIG.save(deps.storage, &config)?; + + let tree_uris = msg.merkle_tree_uris.unwrap_or_default(); + if !tree_uris.is_empty() { + MERKLE_TREE_URIS.save(deps.storage, &tree_uris.clone())?; + } + + let mut attrs = Vec::with_capacity(6); + + attrs.push(("action", "update_merkle_tree")); + let merkle_roots_joined = msg.merkle_roots.join(","); + attrs.push(("merkle_roots", &merkle_roots_joined)); + attrs.push(("contract_name", CONTRACT_NAME)); + attrs.push(("contract_version", CONTRACT_VERSION)); + let tree_uris_joined = tree_uris.join(","); + if !tree_uris.is_empty() { + attrs.push(("merkle_tree_uris", &tree_uris_joined)); + } + attrs.push(("sender", info.sender.as_str())); + + Ok(res.add_attributes(attrs)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::UpdateStageConfig(msg) => execute_update_stage_config(deps, env, info, msg), + ExecuteMsg::UpdateAdmins { admins } => execute_update_admins(deps, env, info, admins), + ExecuteMsg::Freeze {} => execute_freeze(deps, env, info), + } +} + +pub fn execute_update_merkle_tree( + deps: DepsMut, + env: Env, + info: MessageInfo, + merkle_roots: Vec, + merkle_tree_uris: Option>, +) -> Result { + can_execute(&deps, info.sender.clone())?; + let config = CONFIG.load(deps.storage)?; + + for merkle_root in merkle_roots.iter() { + verify_merkle_root(merkle_root)?; + } + + if let Some(tree_uris) = merkle_tree_uris.as_ref() { + for uri in tree_uris.iter() { + verify_tree_uri(uri)?; + } + } + + ensure!( + config + .stages + .iter() + .all(|stage| stage.end_time <= env.block.time), + ContractError::AlreadyEnded {} + ); + + MERKLE_ROOTS.save(deps.storage, &merkle_roots)?; + + let tree_uris = merkle_tree_uris.clone().unwrap_or_default(); + if !tree_uris.is_empty() { + MERKLE_TREE_URIS.save(deps.storage, &tree_uris)?; + } + + let mut attrs = Vec::with_capacity(4); + + attrs.push(("action", String::from("update_merkle_tree"))); + attrs.push(("merkle_roots", merkle_roots.join(","))); + if let Some(uris) = merkle_tree_uris.clone() { + attrs.push(("merkle_tree_uris", uris.join(","))); + } + attrs.push(("sender", info.sender.to_string())); + + Ok(Response::new().add_attributes(attrs)) +} + +pub fn execute_update_stage_config( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: UpdateStageConfigMsg, +) -> Result { + can_execute(&deps, info.sender.clone())?; + let mut config = CONFIG.load(deps.storage)?; + let stage_id = msg.stage_id as usize; + let updated_stage = Stage { + name: msg.name.unwrap_or(config.stages[stage_id].clone().name), + start_time: msg + .start_time + .unwrap_or(config.stages[stage_id].clone().start_time), + end_time: msg + .end_time + .unwrap_or(config.stages[stage_id].clone().end_time), + mint_price: msg + .mint_price + .unwrap_or(config.stages[stage_id].clone().mint_price), + per_address_limit: msg + .per_address_limit + .unwrap_or(config.stages[stage_id].clone().per_address_limit), + }; + config.stages[stage_id] = updated_stage.clone(); + validate_stages(&env, &config.stages)?; + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new() + .add_attribute("action", "update_stage_config") + .add_attribute("stage_id", stage_id.to_string()) + .add_attribute("name", updated_stage.clone().name) + .add_attribute("start_time", updated_stage.clone().start_time.to_string()) + .add_attribute("end_time", updated_stage.clone().end_time.to_string()) + .add_attribute("mint_price", updated_stage.clone().mint_price.to_string()) + .add_attribute( + "per_address_limit", + updated_stage.clone().per_address_limit.to_string(), + ) + .add_attribute("sender", info.sender)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::HasStarted {} => to_json_binary(&query_has_started(deps, env)?), + QueryMsg::HasEnded {} => to_json_binary(&query_has_ended(deps, env)?), + QueryMsg::IsActive {} => to_json_binary(&query_is_active(deps, env)?), + QueryMsg::HasMember { + member, + proof_hashes, + } => to_json_binary(&query_has_member(deps, member, env, proof_hashes)?), + QueryMsg::Config {} => to_json_binary(&query_config(deps, env)?), + QueryMsg::AdminList {} => to_json_binary(&query_admin_list(deps)?), + QueryMsg::CanExecute { sender, .. } => to_json_binary(&query_can_execute(deps, &sender)?), + QueryMsg::MerkleRoot {} => to_json_binary(&query_merkle_root(deps)?), + QueryMsg::MerkleTreeURI {} => to_json_binary(&query_merkle_tree_uri(deps)?), + } +} + +fn query_has_started(deps: Deps, env: Env) -> StdResult { + let config = CONFIG.load(deps.storage)?; + Ok(HasStartedResponse { + has_started: (config.stages.len() > 0) && (env.block.time >= config.stages[0].start_time), + }) +} + +fn query_has_ended(deps: Deps, env: Env) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let stage_count = config.stages.len(); + Ok(HasEndedResponse { + has_ended: (stage_count > 0) && (env.block.time >= config.stages[stage_count - 1].end_time), + }) +} + +fn query_is_active(deps: Deps, env: Env) -> StdResult { + Ok(IsActiveResponse { + is_active: fetch_active_stage(deps.storage, &env).is_some(), + }) +} + +pub fn query_has_member( + deps: Deps, + member: String, + env: Env, + proof_hashes: Vec, +) -> StdResult { + deps.api.addr_validate(&member)?; + + let active_stage = fetch_active_stage_index(deps.storage, &env) + .ok_or_else(|| StdError::generic_err("No active stage found"))?; + + let merkle_root = MERKLE_ROOTS.load(deps.storage)?[active_stage as usize].clone(); + + let member_init_hash_slice = Sha256::hash(member.as_bytes()); + + let final_hash = proof_hashes.into_iter().try_fold( + member_init_hash_slice, + |accum_hash_slice, new_proof_hashstring| { + valid_hash_string(&new_proof_hashstring)?; + + let mut hashe_slices = [ + accum_hash_slice, + string_to_byte_slice(&new_proof_hashstring)?, + ]; + hashe_slices.sort_unstable(); + Result::<[u8; 32], StdError>::Ok(Sha256::hash(&hashe_slices.concat())) + }, + ); + + if final_hash.is_err() { + return Err(cosmwasm_std::StdError::GenericErr { + msg: "Invalid Merkle Proof".to_string(), + }); + } + + Ok(HasMemberResponse { + has_member: merkle_root == hex::encode(final_hash.unwrap()), + }) +} + +pub fn query_config(deps: Deps, env: Env) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let active_stage = fetch_active_stage(deps.storage, &env); + if let Some(stage) = active_stage { + Ok(ConfigResponse { + num_members: 0, + per_address_limit: stage.per_address_limit, + member_limit: 0, + start_time: stage.start_time, + end_time: stage.end_time, + mint_price: stage.mint_price, + is_active: true, + }) + } else if config.stages.len() > 0 { + let stage = if env.block.time < config.stages[0].start_time { + config.stages[0].clone() + } else { + config.stages[config.stages.len() - 1].clone() + }; + Ok(ConfigResponse { + num_members: 0, + per_address_limit: stage.per_address_limit, + member_limit: 0, + start_time: stage.start_time, + end_time: stage.end_time, + mint_price: stage.mint_price, + is_active: false, + }) + } else { + Ok(ConfigResponse { + num_members: 0, + per_address_limit: 0, + member_limit: 0, + start_time: Timestamp::from_seconds(0), + end_time: Timestamp::from_seconds(0), + mint_price: Coin { + denom: NATIVE_DENOM.to_string(), + amount: Uint128::zero(), + }, + is_active: false, + }) + } +} + +pub fn query_merkle_root(deps: Deps) -> StdResult { + Ok(MerkleRootsResponse { + merkle_roots: MERKLE_ROOTS.load(deps.storage)?, + }) +} + +pub fn query_merkle_tree_uri(deps: Deps) -> StdResult { + Ok(MerkleTreeURIsResponse { + merkle_tree_uris: MERKLE_TREE_URIS.may_load(deps.storage)?, + }) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: Empty) -> Result { + let current_version = cw2::get_contract_version(deps.storage)?; + if current_version.contract != CONTRACT_NAME { + return Err(StdError::generic_err("Cannot upgrade to a different contract").into()); + } + let version: Version = current_version + .version + .parse() + .map_err(|_| StdError::generic_err("Invalid contract version"))?; + let new_version: Version = CONTRACT_VERSION + .parse() + .map_err(|_| StdError::generic_err("Invalid contract version"))?; + + if version > new_version { + return Err(StdError::generic_err("Cannot upgrade to a previous contract version").into()); + } + // if same version return + if version == new_version { + return Ok(Response::new()); + } + + // set new contract version + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + let event = Event::new("migrate") + .add_attribute("from_name", current_version.contract) + .add_attribute("from_version", current_version.version) + .add_attribute("to_name", CONTRACT_NAME) + .add_attribute("to_version", CONTRACT_VERSION); + Ok(Response::new().add_event(event)) +} diff --git a/contracts/whitelists/tiered-whitelist-merkletree/src/error.rs b/contracts/whitelists/tiered-whitelist-merkletree/src/error.rs new file mode 100644 index 000000000..bcf7e4f5d --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/src/error.rs @@ -0,0 +1,58 @@ +use cosmwasm_std::{StdError, Timestamp}; +use cw_utils::PaymentError; +use sg1::FeeError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("AlreadyStarted")] + AlreadyStarted {}, + + #[error("AlreadyEnded")] + AlreadyEnded {}, + + #[error("InvalidDenom: {0}")] + InvalidDenom(String), + + #[error("NoMemberFound: {0}")] + NoMemberFound(String), + + #[error("InvalidStartTime {0} > {1}")] + InvalidStartTime(Timestamp, Timestamp), + + #[error("InvalidEndTime {0} > {1}")] + InvalidEndTime(Timestamp, Timestamp), + + #[error("Invalid merkle tree URI (must be an IPFS URI)")] + InvalidMerkleTreeURI {}, + + #[error("Max minting limit per address exceeded")] + MaxPerAddressLimitExceeded {}, + + #[error("Invalid minting limit per address. max: {max}, got: {got}")] + InvalidPerAddressLimit { max: String, got: String }, + + #[error("{0}")] + Fee(#[from] FeeError), + + #[error("InvalidUnitPrice {0} < {1}")] + InvalidUnitPrice(u128, u128), + + #[error("IncorrectCreationFee {0} < {1}")] + IncorrectCreationFee(u128, u128), + + #[error("{0}")] + PaymentError(#[from] PaymentError), + + #[error("UnauthorizedAdmin")] + UnauthorizedAdmin {}, + + #[error("InvalidHashString: {0}")] + InvalidHashString(String), +} diff --git a/contracts/whitelists/tiered-whitelist-merkletree/src/helpers.rs b/contracts/whitelists/tiered-whitelist-merkletree/src/helpers.rs new file mode 100644 index 000000000..8009d09df --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/src/helpers.rs @@ -0,0 +1,4 @@ +pub mod crypto; +pub mod interface; +pub mod utils; +pub mod validators; diff --git a/contracts/whitelists/tiered-whitelist-merkletree/src/helpers/crypto.rs b/contracts/whitelists/tiered-whitelist-merkletree/src/helpers/crypto.rs new file mode 100644 index 000000000..b76747b28 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/src/helpers/crypto.rs @@ -0,0 +1,34 @@ +use cosmwasm_std::{HexBinary, StdError, StdResult}; + +pub fn valid_hash_string(hash_string: &String) -> StdResult<()> { + let hex_res = HexBinary::from_hex(hash_string.as_str()); + if hex_res.is_err() { + return Err(cosmwasm_std::StdError::InvalidHex { + msg: hash_string.to_string(), + }); + } + + let hex_binary = hex_res.unwrap(); + + let decoded = hex_binary.to_array::<32>(); + + if decoded.is_err() { + return Err(cosmwasm_std::StdError::InvalidDataSize { + expected: 32, + actual: hex_binary.len() as u64, + }); + } + Ok(()) +} + +pub fn verify_merkle_root(merkle_root: &String) -> StdResult<()> { + valid_hash_string(merkle_root) +} + +pub fn string_to_byte_slice(string: &String) -> StdResult<[u8; 32]> { + let mut byte_slice = [0; 32]; + hex::decode_to_slice(string, &mut byte_slice).map_err(|_| StdError::GenericErr { + msg: "Couldn't decode hash string".to_string(), + })?; + Ok(byte_slice) +} diff --git a/contracts/whitelists/tiered-whitelist-merkletree/src/helpers/interface.rs b/contracts/whitelists/tiered-whitelist-merkletree/src/helpers/interface.rs new file mode 100644 index 000000000..6f09e831a --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/src/helpers/interface.rs @@ -0,0 +1,26 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{to_json_binary, Addr, StdResult, WasmMsg}; +use sg_std::CosmosMsg; + +use crate::msg::ExecuteMsg; + +/// CwTemplateContract is a wrapper around Addr that provides a lot of helpers +/// for working with this. +#[cw_serde] +pub struct CollectionWhitelistContract(pub Addr); + +impl CollectionWhitelistContract { + pub fn addr(&self) -> Addr { + self.0.clone() + } + + pub fn call>(&self, msg: T) -> StdResult { + let msg = to_json_binary(&msg.into())?; + Ok(WasmMsg::Execute { + contract_addr: self.addr().into(), + msg, + funds: vec![], + } + .into()) + } +} diff --git a/contracts/whitelists/tiered-whitelist-merkletree/src/helpers/utils.rs b/contracts/whitelists/tiered-whitelist-merkletree/src/helpers/utils.rs new file mode 100644 index 000000000..1e37c4a44 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/src/helpers/utils.rs @@ -0,0 +1,99 @@ +use crate::contract::{MAX_PER_ADDRESS_LIMIT, MIN_MINT_PRICE}; +use crate::state::{Config, Stage, CONFIG}; +use crate::ContractError; +use cosmwasm_std::{ensure, Env, StdError, StdResult, Storage}; +use url::Url; + +pub fn verify_tree_uri(tree_uri: &String) -> StdResult<()> { + let res = Url::parse(tree_uri); + if res.is_err() { + return Err(cosmwasm_std::StdError::GenericErr { + msg: "Invalid tree uri".to_string(), + }); + } + Ok(()) +} + +pub fn fetch_active_stage(deps: &dyn Storage, env: &Env) -> Option { + let config: Config = CONFIG.load(deps).ok()?; + let current_time = env.block.time; + config + .stages + .iter() + .find(|stage| stage.start_time <= current_time && current_time <= stage.end_time) + .cloned() +} + +pub fn fetch_active_stage_index(deps: &dyn Storage, env: &Env) -> Option { + let config: Config = CONFIG.load(deps).ok()?; + let current_time = env.block.time; + config + .stages + .iter() + .position(|stage| stage.start_time <= current_time && current_time <= stage.end_time) + .map(|i| i as u32) +} + +pub fn validate_stages(env: &Env, stages: &[Stage]) -> Result<(), ContractError> { + ensure!( + stages.len() > 0, + StdError::generic_err("Must have at least one stage") + ); + ensure!( + stages.len() < 4, + StdError::generic_err("Cannot have more than 3 stages") + ); + + // Check per address limit is valid + if stages.iter().any(|stage| { + stage.per_address_limit == 0 || stage.per_address_limit > MAX_PER_ADDRESS_LIMIT + }) { + return Err(ContractError::InvalidPerAddressLimit { + max: MAX_PER_ADDRESS_LIMIT.to_string(), + got: stages + .iter() + .map(|s| s.per_address_limit) + .max() + .unwrap() + .to_string(), + }) + .into(); + } + + // Check mint price is valid + if stages + .iter() + .any(|stage| stage.mint_price.amount.u128() < MIN_MINT_PRICE) + { + return Err(ContractError::InvalidUnitPrice { + 0: MIN_MINT_PRICE, + 1: stages + .iter() + .map(|s| s.mint_price.amount.u128()) + .min() + .unwrap(), + }) + .into(); + } + + ensure!( + stages[0].start_time > env.block.time, + StdError::generic_err("Stages must have a start time in the future") + ); + for i in 0..stages.len() { + let stage = &stages[i]; + ensure!( + stage.start_time < stage.end_time, + StdError::generic_err("Stage start time must be before the end time") + ); + + for j in i + 1..stages.len() { + let other_stage = &stages[j]; + ensure!( + other_stage.start_time >= stage.end_time, + StdError::generic_err("Stages must have non-overlapping times") + ); + } + } + Ok(()) +} diff --git a/contracts/whitelists/tiered-whitelist-merkletree/src/helpers/validators.rs b/contracts/whitelists/tiered-whitelist-merkletree/src/helpers/validators.rs new file mode 100644 index 000000000..b5ffe2984 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/src/helpers/validators.rs @@ -0,0 +1,5 @@ +use cosmwasm_std::{Addr, Api, StdResult}; + +pub fn map_validate(api: &dyn Api, admins: &[String]) -> StdResult> { + admins.iter().map(|addr| api.addr_validate(addr)).collect() +} diff --git a/contracts/whitelists/tiered-whitelist-merkletree/src/lib.rs b/contracts/whitelists/tiered-whitelist-merkletree/src/lib.rs new file mode 100644 index 000000000..b0867fb09 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/src/lib.rs @@ -0,0 +1,7 @@ +pub mod admin; +pub mod contract; +pub mod error; +pub mod helpers; +pub mod msg; +pub mod state; +pub use crate::error::ContractError; diff --git a/contracts/whitelists/tiered-whitelist-merkletree/src/msg.rs b/contracts/whitelists/tiered-whitelist-merkletree/src/msg.rs new file mode 100644 index 000000000..1a0988fdb --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/src/msg.rs @@ -0,0 +1,129 @@ +use crate::state::Stage; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Coin, CosmosMsg, Empty, Timestamp}; + +#[cw_serde] +pub struct Member { + pub address: String, + pub mint_count: u32, +} + +#[cw_serde] +pub struct InstantiateMsg { + pub stages: Vec, + pub merkle_roots: Vec, + pub merkle_tree_uris: Option>, + pub admins: Vec, + pub admins_mutable: bool, +} + +#[cw_serde] +pub enum ExecuteMsg { + UpdateStageConfig(UpdateStageConfigMsg), + UpdateAdmins { admins: Vec }, + Freeze {}, +} + +#[cw_serde] +pub struct AdminListResponse { + pub admins: Vec, + pub mutable: bool, +} + +#[cw_serde] +pub struct UpdateStageConfigMsg { + pub stage_id: u32, + pub name: Option, + pub start_time: Option, + pub end_time: Option, + pub mint_price: Option, + pub per_address_limit: Option, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(HasStartedResponse)] + HasStarted {}, + #[returns(HasEndedResponse)] + HasEnded {}, + #[returns(IsActiveResponse)] + IsActive {}, + #[returns(HasMemberResponse)] + HasMember { + member: String, + proof_hashes: Vec, + }, + #[returns(ConfigResponse)] + Config {}, + #[returns(AdminListResponse)] + AdminList {}, + #[returns(CanExecuteResponse)] + CanExecute { + sender: String, + msg: CosmosMsg, + }, + #[returns(MerkleRootsResponse)] + MerkleRoot {}, + #[returns(MerkleTreeURIsResponse)] + MerkleTreeURI {}, +} + +#[cw_serde] +pub struct HasMemberResponse { + pub has_member: bool, +} + +#[cw_serde] +pub struct HasEndedResponse { + pub has_ended: bool, +} + +#[cw_serde] +pub struct HasStartedResponse { + pub has_started: bool, +} + +#[cw_serde] +pub struct IsActiveResponse { + pub is_active: bool, +} + +#[cw_serde] +pub struct MintPriceResponse { + pub mint_price: Coin, +} + +#[cw_serde] +pub struct ConfigResponse { + pub num_members: u32, + pub per_address_limit: u32, + pub member_limit: u32, + pub start_time: Timestamp, + pub end_time: Timestamp, + pub mint_price: Coin, + pub is_active: bool, +} + +#[cw_serde] +pub struct MerkleRootsResponse { + pub merkle_roots: Vec, +} + +#[cw_serde] +pub struct MerkleTreeURIsResponse { + pub merkle_tree_uris: Option>, +} + +#[cw_serde] +pub enum SudoMsg { + /// Add a new operator + AddOperator { operator: String }, + /// Remove operator + RemoveOperator { operator: String }, +} + +#[cw_serde] +pub struct CanExecuteResponse { + pub can_execute: bool, +} diff --git a/contracts/whitelists/tiered-whitelist-merkletree/src/state.rs b/contracts/whitelists/tiered-whitelist-merkletree/src/state.rs new file mode 100644 index 000000000..716e59c23 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/src/state.rs @@ -0,0 +1,38 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Coin, Timestamp}; +use cw_storage_plus::Item; + +#[cw_serde] +pub struct Stage { + pub name: String, + pub start_time: Timestamp, + pub end_time: Timestamp, + pub mint_price: Coin, + pub per_address_limit: u32, +} +#[cw_serde] +pub struct Config { + pub stages: Vec, +} + +#[cw_serde] +pub struct AdminList { + pub admins: Vec, + pub mutable: bool, +} + +impl AdminList { + pub fn is_admin(&self, addr: impl AsRef) -> bool { + let addr = addr.as_ref(); + self.admins.iter().any(|a| a.as_ref() == addr) + } + + pub fn can_modify(&self, addr: &str) -> bool { + self.mutable && self.is_admin(addr) + } +} + +pub const ADMIN_LIST: Item = Item::new("admin_list"); +pub const CONFIG: Item = Item::new("config"); +pub const MERKLE_ROOTS: Item> = Item::new("merkle_roots"); +pub const MERKLE_TREE_URIS: Item> = Item::new("merkle_tree_uris");