diff --git a/Cargo.lock b/Cargo.lock index 0466c0804..348d0f093 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3640,6 +3640,40 @@ dependencies = [ "thiserror", ] +[[package]] +name = "sg-tiered-whitelist" +version = "3.15.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "rust_decimal", + "schemars", + "serde", + "sg-std", + "sg1", + "thiserror", +] + +[[package]] +name = "sg-tiered-whitelist-flex" +version = "3.15.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "rust_decimal", + "schemars", + "serde", + "sg-std", + "sg1", + "thiserror", +] + [[package]] name = "sg-whitelist" version = "3.15.0" @@ -4234,6 +4268,28 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "tiered-whitelist-merkletree" +version = "3.15.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.36" diff --git a/contracts/minters/open-edition-minter-merkle-wl/src/contract.rs b/contracts/minters/open-edition-minter-merkle-wl/src/contract.rs index 94a02e82c..de3b12c0c 100644 --- a/contracts/minters/open-edition-minter-merkle-wl/src/contract.rs +++ b/contracts/minters/open-edition-minter-merkle-wl/src/contract.rs @@ -196,9 +196,10 @@ pub fn execute( ) -> Result { match msg { ExecuteMsg::Mint { + stage, proof_hashes, allocation, - } => execute_mint_sender(deps, env, info, proof_hashes, allocation), + } => execute_mint_sender(deps, env, info, stage, proof_hashes, allocation), ExecuteMsg::Purge {} => execute_purge(deps, env, info), ExecuteMsg::UpdateMintPrice { price } => execute_update_mint_price(deps, env, info, price), ExecuteMsg::UpdateStartTime(time) => execute_update_start_time(deps, env, info, time), @@ -352,6 +353,7 @@ pub fn execute_mint_sender( deps: DepsMut, env: Env, info: MessageInfo, + stage: Option, proof_hashes: Option>, allocation: Option, ) -> Result { @@ -359,7 +361,7 @@ pub fn execute_mint_sender( let action = "mint_sender"; // If there is no active whitelist right now, check public mint - let is_public_mint = is_public_mint(deps.as_ref(), &info, proof_hashes, allocation)?; + let is_public_mint = is_public_mint(deps.as_ref(), &info, stage, proof_hashes, allocation)?; // Check start and end time (if not optional) if is_public_mint && (env.block.time < config.extension.start_time) { return Err(ContractError::BeforeMintStartTime {}); @@ -384,6 +386,7 @@ pub fn execute_mint_sender( fn is_public_mint( deps: Deps, info: &MessageInfo, + stage: Option, proof_hashes: Option>, allocation: Option, ) -> Result { @@ -408,9 +411,13 @@ fn is_public_mint( deps.querier.query_wasm_smart( whitelist, &WhitelistQueryMsg::HasMember { - member: match allocation { - Some(allocation) => format!("{}{}", info.sender, allocation), - None => info.sender.to_string(), + member: match (stage, allocation) { + (None, Some(allocation)) => format!("{}{}", info.sender, allocation), + (Some(stage), None) => format!("{}{}", stage, info.sender), + (Some(stage), Some(allocation)) => { + format!("{}{}{}", stage, info.sender, allocation) + } + (None, None) => info.sender.to_string(), }, proof_hashes: proof_hashes.unwrap(), }, diff --git a/contracts/minters/open-edition-minter-merkle-wl/src/msg.rs b/contracts/minters/open-edition-minter-merkle-wl/src/msg.rs index 91a978f1c..7d4e0f3ec 100644 --- a/contracts/minters/open-edition-minter-merkle-wl/src/msg.rs +++ b/contracts/minters/open-edition-minter-merkle-wl/src/msg.rs @@ -13,6 +13,7 @@ pub struct InstantiateMsg { #[cw_serde] pub enum ExecuteMsg { Mint { + stage: Option, proof_hashes: Option>, allocation: Option, }, diff --git a/contracts/minters/vending-minter-merkle-wl-featured/src/contract.rs b/contracts/minters/vending-minter-merkle-wl-featured/src/contract.rs index 008ff1cb5..6a5f4b752 100644 --- a/contracts/minters/vending-minter-merkle-wl-featured/src/contract.rs +++ b/contracts/minters/vending-minter-merkle-wl-featured/src/contract.rs @@ -219,9 +219,10 @@ pub fn execute( ) -> Result { match msg { ExecuteMsg::Mint { + stage, proof_hashes, allocation, - } => execute_mint_sender(deps, env, info, proof_hashes, allocation), + } => execute_mint_sender(deps, env, info, stage, proof_hashes, allocation), ExecuteMsg::Purge {} => execute_purge(deps, env, info), ExecuteMsg::UpdateMintPrice { price } => execute_update_mint_price(deps, env, info, price), ExecuteMsg::UpdateStartTime(time) => execute_update_start_time(deps, env, info, time), @@ -498,6 +499,7 @@ pub fn execute_mint_sender( deps: DepsMut, env: Env, info: MessageInfo, + stage: Option, proof_hashes: Option>, allocation: Option, ) -> Result { @@ -505,7 +507,7 @@ pub fn execute_mint_sender( let action = "mint_sender"; // If there is no active whitelist right now, check public mint - let is_public_mint = is_public_mint(deps.as_ref(), &info, proof_hashes, allocation)?; + let is_public_mint = is_public_mint(deps.as_ref(), &info, stage, proof_hashes, allocation)?; // Check if after start_time if is_public_mint && (env.block.time < config.extension.start_time) { return Err(ContractError::BeforeMintStartTime {}); @@ -530,6 +532,7 @@ pub fn execute_mint_sender( fn is_public_mint( deps: Deps, info: &MessageInfo, + stage: Option, proof_hashes: Option>, allocation: Option, ) -> Result { @@ -554,9 +557,13 @@ fn is_public_mint( deps.querier.query_wasm_smart( whitelist, &WhitelistMtreeQueryMsg::HasMember { - member: match allocation { - Some(allocation) => format!("{}{}", info.sender, allocation), - None => info.sender.to_string(), + member: match (stage, allocation) { + (None, Some(allocation)) => format!("{}{}", info.sender, allocation), + (Some(stage), None) => format!("{}{}", stage, info.sender), + (Some(stage), Some(allocation)) => { + format!("{}{}{}", stage, info.sender, allocation) + } + (None, None) => info.sender.to_string(), }, proof_hashes: proof_hashes.unwrap(), }, diff --git a/contracts/minters/vending-minter-merkle-wl-featured/src/msg.rs b/contracts/minters/vending-minter-merkle-wl-featured/src/msg.rs index 44bc33ebf..10f268ed1 100644 --- a/contracts/minters/vending-minter-merkle-wl-featured/src/msg.rs +++ b/contracts/minters/vending-minter-merkle-wl-featured/src/msg.rs @@ -11,6 +11,7 @@ pub struct InstantiateMsg { #[cw_serde] pub enum ExecuteMsg { Mint { + stage: Option, proof_hashes: Option>, allocation: Option, }, diff --git a/contracts/minters/vending-minter-merkle-wl/src/contract.rs b/contracts/minters/vending-minter-merkle-wl/src/contract.rs index 4e7ea5770..23e42755b 100644 --- a/contracts/minters/vending-minter-merkle-wl/src/contract.rs +++ b/contracts/minters/vending-minter-merkle-wl/src/contract.rs @@ -219,9 +219,10 @@ pub fn execute( ) -> Result { match msg { ExecuteMsg::Mint { + stage, proof_hashes, allocation, - } => execute_mint_sender(deps, env, info, proof_hashes, allocation), + } => execute_mint_sender(deps, env, info, stage, proof_hashes, allocation), ExecuteMsg::Purge {} => execute_purge(deps, env, info), ExecuteMsg::UpdateMintPrice { price } => execute_update_mint_price(deps, env, info, price), ExecuteMsg::UpdateStartTime(time) => execute_update_start_time(deps, env, info, time), @@ -498,6 +499,7 @@ pub fn execute_mint_sender( deps: DepsMut, env: Env, info: MessageInfo, + stage: Option, proof_hashes: Option>, allocation: Option, ) -> Result { @@ -505,7 +507,7 @@ pub fn execute_mint_sender( let action = "mint_sender"; // If there is no active whitelist right now, check public mint - let is_public_mint = is_public_mint(deps.as_ref(), &info, proof_hashes, allocation)?; + let is_public_mint = is_public_mint(deps.as_ref(), &info, stage, proof_hashes, allocation)?; // Check if after start_time if is_public_mint && (env.block.time < config.extension.start_time) { return Err(ContractError::BeforeMintStartTime {}); @@ -530,6 +532,7 @@ pub fn execute_mint_sender( fn is_public_mint( deps: Deps, info: &MessageInfo, + stage: Option, proof_hashes: Option>, allocation: Option, ) -> Result { @@ -554,9 +557,13 @@ fn is_public_mint( deps.querier.query_wasm_smart( whitelist, &WhitelistMtreeQueryMsg::HasMember { - member: match allocation { - Some(allocation) => format!("{}{}", info.sender, allocation), - None => info.sender.to_string(), + member: match (stage, allocation) { + (None, Some(allocation)) => format!("{}{}", info.sender, allocation), + (Some(stage), None) => format!("{}{}", stage, info.sender), + (Some(stage), Some(allocation)) => { + format!("{}{}{}", stage, info.sender, allocation) + } + (None, None) => info.sender.to_string(), }, proof_hashes: proof_hashes.unwrap(), }, diff --git a/contracts/minters/vending-minter-merkle-wl/src/msg.rs b/contracts/minters/vending-minter-merkle-wl/src/msg.rs index 44bc33ebf..10f268ed1 100644 --- a/contracts/minters/vending-minter-merkle-wl/src/msg.rs +++ b/contracts/minters/vending-minter-merkle-wl/src/msg.rs @@ -11,6 +11,7 @@ pub struct InstantiateMsg { #[cw_serde] pub enum ExecuteMsg { Mint { + stage: Option, proof_hashes: Option>, allocation: Option, }, diff --git a/contracts/whitelists/tiered-whitelist-flex/.cargo/config b/contracts/whitelists/tiered-whitelist-flex/.cargo/config new file mode 100644 index 000000000..ab407a024 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" diff --git a/contracts/whitelists/tiered-whitelist-flex/.editorconfig b/contracts/whitelists/tiered-whitelist-flex/.editorconfig new file mode 100644 index 000000000..3d36f20b1 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/.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-flex/.gitignore b/contracts/whitelists/tiered-whitelist-flex/.gitignore new file mode 100644 index 000000000..dfdaaa6bc --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/.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-flex/Cargo.toml b/contracts/whitelists/tiered-whitelist-flex/Cargo.toml new file mode 100644 index 000000000..ee0099a1d --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "sg-tiered-whitelist-flex" +authors = ["Shane Vitarana "] +description = "Stargaze NFT tiered 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 } diff --git a/contracts/whitelists/tiered-whitelist-flex/README.md b/contracts/whitelists/tiered-whitelist-flex/README.md new file mode 100644 index 000000000..93425fc4c --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/README.md @@ -0,0 +1 @@ +# Tiered Whitelist contract diff --git a/contracts/whitelists/tiered-whitelist-flex/examples/schema.rs b/contracts/whitelists/tiered-whitelist-flex/examples/schema.rs new file mode 100644 index 000000000..e1d6a630a --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/examples/schema.rs @@ -0,0 +1,31 @@ +use std::env::current_dir; +use std::fs::create_dir_all; + +use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; + +use sg_tiered_whitelist_flex::msg::{ + AdminListResponse, CanExecuteResponse, ConfigResponse, ExecuteMsg, HasEndedResponse, + HasMemberResponse, HasStartedResponse, InstantiateMsg, IsActiveResponse, MembersResponse, + QueryMsg, +}; +use sg_tiered_whitelist_flex::state::Config; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + export_schema(&schema_for!(Config), &out_dir); + export_schema(&schema_for!(ConfigResponse), &out_dir); + export_schema(&schema_for!(ExecuteMsg), &out_dir); + export_schema(&schema_for!(HasEndedResponse), &out_dir); + export_schema(&schema_for!(HasMemberResponse), &out_dir); + export_schema(&schema_for!(HasStartedResponse), &out_dir); + export_schema(&schema_for!(InstantiateMsg), &out_dir); + export_schema(&schema_for!(IsActiveResponse), &out_dir); + export_schema(&schema_for!(MembersResponse), &out_dir); + export_schema(&schema_for!(QueryMsg), &out_dir); + export_schema(&schema_for!(AdminListResponse), &out_dir); + export_schema(&schema_for!(CanExecuteResponse), &out_dir); +} diff --git a/contracts/whitelists/tiered-whitelist-flex/rustfmt.toml b/contracts/whitelists/tiered-whitelist-flex/rustfmt.toml new file mode 100644 index 000000000..11a85e6a9 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/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-flex/schema/admin_list_response.json b/contracts/whitelists/tiered-whitelist-flex/schema/admin_list_response.json new file mode 100644 index 000000000..3b04e955c --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/schema/admin_list_response.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-flex/schema/can_execute_response.json b/contracts/whitelists/tiered-whitelist-flex/schema/can_execute_response.json new file mode 100644 index 000000000..e2ed10214 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/schema/can_execute_response.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-flex/schema/config.json b/contracts/whitelists/tiered-whitelist-flex/schema/config.json new file mode 100644 index 000000000..e715f9d08 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/schema/config.json @@ -0,0 +1,94 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Config", + "type": "object", + "required": [ + "member_limit", + "num_members", + "stages" + ], + "properties": { + "member_limit": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "num_members": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "stages": { + "type": "array", + "items": { + "$ref": "#/definitions/Stage" + } + }, + "whale_cap": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "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", + "start_time" + ], + "properties": { + "end_time": { + "$ref": "#/definitions/Timestamp" + }, + "mint_price": { + "$ref": "#/definitions/Coin" + }, + "name": { + "type": "string" + }, + "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-flex/schema/config_response.json b/contracts/whitelists/tiered-whitelist-flex/schema/config_response.json new file mode 100644 index 000000000..91cb21ded --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/schema/config_response.json @@ -0,0 +1,79 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ConfigResponse", + "type": "object", + "required": [ + "end_time", + "is_active", + "member_limit", + "mint_price", + "num_members", + "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 + }, + "start_time": { + "$ref": "#/definitions/Timestamp" + }, + "whale_cap": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "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-flex/schema/execute_msg.json b/contracts/whitelists/tiered-whitelist-flex/schema/execute_msg.json new file mode 100644 index 000000000..68508f410 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/schema/execute_msg.json @@ -0,0 +1,317 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "add_stage" + ], + "properties": { + "add_stage": { + "$ref": "#/definitions/AddStageMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "remove_stage" + ], + "properties": { + "remove_stage": { + "$ref": "#/definitions/RemoveStageMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "add_members" + ], + "properties": { + "add_members": { + "$ref": "#/definitions/AddMembersMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "remove_members" + ], + "properties": { + "remove_members": { + "$ref": "#/definitions/RemoveMembersMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_stage_config" + ], + "properties": { + "update_stage_config": { + "$ref": "#/definitions/UpdateStageConfigMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "increase_member_limit" + ], + "properties": { + "increase_member_limit": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "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": { + "AddMembersMsg": { + "type": "object", + "required": [ + "stage_id", + "to_add" + ], + "properties": { + "stage_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "to_add": { + "type": "array", + "items": { + "$ref": "#/definitions/Member" + } + } + }, + "additionalProperties": false + }, + "AddStageMsg": { + "type": "object", + "required": [ + "members", + "stage" + ], + "properties": { + "members": { + "type": "array", + "items": { + "$ref": "#/definitions/Member" + } + }, + "stage": { + "$ref": "#/definitions/Stage" + } + }, + "additionalProperties": false + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Member": { + "type": "object", + "required": [ + "address", + "mint_count" + ], + "properties": { + "address": { + "type": "string" + }, + "mint_count": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "RemoveMembersMsg": { + "type": "object", + "required": [ + "stage_id", + "to_remove" + ], + "properties": { + "stage_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "to_remove": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "RemoveStageMsg": { + "type": "object", + "required": [ + "stage_id" + ], + "properties": { + "stage_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Stage": { + "type": "object", + "required": [ + "end_time", + "mint_price", + "name", + "start_time" + ], + "properties": { + "end_time": { + "$ref": "#/definitions/Timestamp" + }, + "mint_price": { + "$ref": "#/definitions/Coin" + }, + "name": { + "type": "string" + }, + "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" + }, + "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" + ] + }, + "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-flex/schema/has_ended_response.json b/contracts/whitelists/tiered-whitelist-flex/schema/has_ended_response.json new file mode 100644 index 000000000..6e207ab48 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/schema/has_ended_response.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-flex/schema/has_member_response.json b/contracts/whitelists/tiered-whitelist-flex/schema/has_member_response.json new file mode 100644 index 000000000..8e203003b --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/schema/has_member_response.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-flex/schema/has_started_response.json b/contracts/whitelists/tiered-whitelist-flex/schema/has_started_response.json new file mode 100644 index 000000000..25614f8d8 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/schema/has_started_response.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-flex/schema/instantiate_msg.json b/contracts/whitelists/tiered-whitelist-flex/schema/instantiate_msg.json new file mode 100644 index 000000000..10101c860 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/schema/instantiate_msg.json @@ -0,0 +1,127 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "admins", + "admins_mutable", + "member_limit", + "members", + "stages" + ], + "properties": { + "admins": { + "type": "array", + "items": { + "type": "string" + } + }, + "admins_mutable": { + "type": "boolean" + }, + "member_limit": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "members": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/Member" + } + } + }, + "stages": { + "type": "array", + "items": { + "$ref": "#/definitions/Stage" + } + }, + "whale_cap": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Member": { + "type": "object", + "required": [ + "address", + "mint_count" + ], + "properties": { + "address": { + "type": "string" + }, + "mint_count": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Stage": { + "type": "object", + "required": [ + "end_time", + "mint_price", + "name", + "start_time" + ], + "properties": { + "end_time": { + "$ref": "#/definitions/Timestamp" + }, + "mint_price": { + "$ref": "#/definitions/Coin" + }, + "name": { + "type": "string" + }, + "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-flex/schema/is_active_response.json b/contracts/whitelists/tiered-whitelist-flex/schema/is_active_response.json new file mode 100644 index 000000000..2dc928c41 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/schema/is_active_response.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-flex/schema/members_response.json b/contracts/whitelists/tiered-whitelist-flex/schema/members_response.json new file mode 100644 index 000000000..3a34364fa --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/schema/members_response.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MembersResponse", + "type": "object", + "required": [ + "members" + ], + "properties": { + "members": { + "type": "array", + "items": { + "$ref": "#/definitions/Member" + } + } + }, + "additionalProperties": false, + "definitions": { + "Member": { + "type": "object", + "required": [ + "address", + "mint_count" + ], + "properties": { + "address": { + "type": "string" + }, + "mint_count": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/whitelists/tiered-whitelist-flex/schema/query_msg.json b/contracts/whitelists/tiered-whitelist-flex/schema/query_msg.json new file mode 100644 index 000000000..41634cf19 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/schema/query_msg.json @@ -0,0 +1,525 @@ +{ + "$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": [ + "active_stage" + ], + "properties": { + "active_stage": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "members" + ], + "properties": { + "members": { + "type": "object", + "required": [ + "stage_id" + ], + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "stage_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "has_member" + ], + "properties": { + "has_member": { + "type": "object", + "required": [ + "member" + ], + "properties": { + "member": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "member" + ], + "properties": { + "member": { + "type": "object", + "required": [ + "member" + ], + "properties": { + "member": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "stage" + ], + "properties": { + "stage": { + "type": "object", + "required": [ + "stage_id" + ], + "properties": { + "stage_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "stages" + ], + "properties": { + "stages": { + "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 + } + ], + "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-flex/src/admin.rs b/contracts/whitelists/tiered-whitelist-flex/src/admin.rs new file mode 100644 index 000000000..f4c065b9f --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/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-flex/src/contract.rs b/contracts/whitelists/tiered-whitelist-flex/src/contract.rs new file mode 100644 index 000000000..a278c1f9f --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/src/contract.rs @@ -0,0 +1,541 @@ +use crate::admin::{ + can_execute, execute_freeze, execute_update_admins, query_admin_list, query_can_execute, +}; +use crate::error::ContractError; +use crate::helpers::validators::map_validate; +use crate::helpers::{fetch_active_stage, fetch_active_stage_index, validate_stages}; +use crate::msg::{ + AddMembersMsg, ConfigResponse, ExecuteMsg, HasEndedResponse, HasMemberResponse, + HasStartedResponse, InstantiateMsg, IsActiveResponse, Member, MembersResponse, QueryMsg, + RemoveMembersMsg, StageResponse, StagesResponse, UpdateStageConfigMsg, +}; +use crate::state::{AdminList, Config, Stage, ADMIN_LIST, CONFIG, WHITELIST_STAGES}; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + ensure, to_json_binary, Binary, Coin, Deps, DepsMut, Env, MessageInfo, StdResult, Timestamp, + Uint128, +}; +use cosmwasm_std::{Order, StdError}; +use cw2::set_contract_version; +use cw_storage_plus::Bound; +use cw_utils::{may_pay, maybe_addr, must_pay}; +use rust_decimal::prelude::ToPrimitive; +use rust_decimal::Decimal; +use sg1::checked_fair_burn; +use sg_std::{Response, NATIVE_DENOM}; + +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:sg-tiered-whitelist-flex"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +// contract governance params +pub const MAX_MEMBERS: u32 = 30000; +pub const PRICE_PER_1000_MEMBERS: u128 = 100_000_000; +pub const MIN_MINT_PRICE: u128 = 0; +pub const MAX_PER_ADDRESS_LIMIT: u32 = 30; + +// queries +const PAGINATION_DEFAULT_LIMIT: u32 = 25; +const PAGINATION_MAX_LIMIT: u32 = 100; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + if msg.member_limit == 0 || msg.member_limit > MAX_MEMBERS { + return Err(ContractError::InvalidMemberLimit { + min: 1, + max: MAX_MEMBERS, + got: msg.member_limit, + }); + } + + validate_stages(&env, &msg.stages)?; + + let creation_fee = Decimal::new(msg.member_limit.into(), 3) + .ceil() + .to_u128() + .unwrap() + * PRICE_PER_1000_MEMBERS; + let payment = must_pay(&info, NATIVE_DENOM)?; + if payment.u128() != creation_fee { + return Err(ContractError::IncorrectCreationFee( + payment.u128(), + creation_fee, + )); + } + + if let Some(whale_cap) = msg.whale_cap { + ensure!( + whale_cap > msg.member_limit, + ContractError::InvalidWhaleCap(whale_cap, msg.member_limit) + ); + } + + let config = Config { + stages: msg.stages.clone(), + num_members: msg.members.iter().map(|m| m.len() as u32).sum(), + member_limit: msg.member_limit, + whale_cap: msg.whale_cap, + }; + CONFIG.save(deps.storage, &config)?; + + let admin_config = AdminList { + admins: map_validate(deps.api, &msg.admins)?, + mutable: msg.admins_mutable, + }; + ADMIN_LIST.save(deps.storage, &admin_config)?; + + let mut res = Response::new(); + checked_fair_burn(&info, creation_fee, None, &mut res)?; + + if config.member_limit < config.num_members { + return Err(ContractError::MembersExceeded { + expected: config.member_limit, + actual: config.num_members, + }); + } + + for stage in 0..msg.stages.clone().len() { + for member in msg.members[stage].iter() { + let addr = deps.api.addr_validate(&member.address)?; + if let Some(whale_cap) = config.whale_cap { + if member.mint_count > whale_cap { + return Err(ContractError::ExceededWhaleCap {}); + } + } + WHITELIST_STAGES.save(deps.storage, (stage as u32, addr), &member.mint_count)?; + } + } + + Ok(res + .add_attribute("action", "instantiate") + .add_attribute("contract_name", CONTRACT_NAME) + .add_attribute("contract_version", CONTRACT_VERSION) + .add_attribute("sender", info.sender)) +} + +#[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::AddStage(msg) => execute_add_stage(deps, env, info, msg.stage, msg.members), + ExecuteMsg::RemoveStage(msg) => execute_remove_stage(deps, env, info, msg.stage_id), + ExecuteMsg::AddMembers(msg) => execute_add_members(deps, env, info, msg), + ExecuteMsg::RemoveMembers(msg) => execute_remove_members(deps, env, info, msg), + ExecuteMsg::IncreaseMemberLimit(member_limit) => { + execute_increase_member_limit(deps, info, member_limit) + } + ExecuteMsg::UpdateAdmins { admins } => execute_update_admins(deps, env, info, admins), + ExecuteMsg::Freeze {} => execute_freeze(deps, env, info), + } +} + +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), + }; + 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("sender", info.sender)) +} + +pub fn execute_add_members( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: AddMembersMsg, +) -> Result { + can_execute(&deps, info.sender.clone())?; + let mut config = CONFIG.load(deps.storage)?; + ensure!( + msg.stage_id < config.stages.len() as u32, + ContractError::StageNotFound {} + ); + + let mut members_added = 0; + for add in msg.to_add.into_iter() { + if config.num_members >= config.member_limit { + return Err(ContractError::MembersExceeded { + expected: config.member_limit, + actual: config.num_members, + }); + } + let addr = deps.api.addr_validate(&add.address)?; + if WHITELIST_STAGES.has(deps.storage, (msg.stage_id, addr.clone())) { + continue; + } + members_added += 1; + WHITELIST_STAGES.save(deps.storage, (msg.stage_id, addr.clone()), &add.mint_count)?; + config.num_members += 1; + } + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new() + .add_attribute("action", "add_members") + .add_attribute("stage_id", msg.stage_id.to_string()) + .add_attribute("num_members", config.num_members.to_string()) + .add_attribute("members_added", members_added.to_string()) + .add_attribute("sender", info.sender)) +} + +pub fn execute_remove_members( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: RemoveMembersMsg, +) -> Result { + can_execute(&deps, info.sender.clone())?; + let mut config = CONFIG.load(deps.storage)?; + ensure!( + msg.stage_id < config.stages.len() as u32, + ContractError::StageNotFound {} + ); + + ensure!( + env.block.time < config.stages[msg.stage_id as usize].start_time, + ContractError::AlreadyStarted {} + ); + + for remove in msg.to_remove.into_iter() { + let addr = deps.api.addr_validate(&remove)?; + if !WHITELIST_STAGES.has(deps.storage, (msg.stage_id, addr.clone())) { + return Err(ContractError::NoMemberFound(addr.to_string())); + } + WHITELIST_STAGES.remove(deps.storage, (msg.stage_id, addr.clone())); + config.num_members -= 1; + } + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new() + .add_attribute("action", "remove_members") + .add_attribute("stage_id", msg.stage_id.to_string()) + .add_attribute("sender", info.sender)) +} + +pub fn execute_add_stage( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: Stage, + members: Vec, +) -> Result { + can_execute(&deps, info.sender.clone())?; + let mut config = CONFIG.load(deps.storage)?; + ensure!( + config.stages.len().lt(&3), + ContractError::MaxStageCountExceeded {} + ); + config.stages.push(msg.clone()); + validate_stages(&env, &config.stages)?; + let stage_id = config.stages.len().saturating_sub(1) as u32; + + for add in members.into_iter() { + if config.num_members >= config.member_limit { + return Err(ContractError::MembersExceeded { + expected: config.member_limit, + actual: config.num_members, + }); + } + let addr = deps.api.addr_validate(&add.address)?; + if let Some(whale_cap) = config.whale_cap { + if add.mint_count > whale_cap { + return Err(ContractError::ExceededWhaleCap {}); + } + } + if WHITELIST_STAGES.has(deps.storage, (stage_id, addr.clone())) { + continue; + } + WHITELIST_STAGES.save(deps.storage, (stage_id, addr.clone()), &add.mint_count)?; + config.num_members += 1; + } + + CONFIG.save(deps.storage, &config)?; + Ok(Response::new() + .add_attribute("action", "add_stage") + .add_attribute("stage_id", config.stages.len().to_string()) + .add_attribute("sender", info.sender)) +} + +pub fn execute_remove_stage( + deps: DepsMut, + env: Env, + info: MessageInfo, + stage_id: u32, +) -> Result { + can_execute(&deps, info.sender.clone())?; + let mut config = CONFIG.load(deps.storage)?; + ensure!( + config.stages.len().gt(&(stage_id as usize)), + ContractError::StageNotFound {} + ); + + ensure!( + env.block.time < config.stages[stage_id as usize].start_time, + ContractError::AlreadyStarted {} + ); + // remove the stage and following stages permanently + config.stages = config.stages.into_iter().take(stage_id as usize).collect(); + + // remove members from the WHITELIST_STAGES for stage_id and following stages. Reduce the num_members count + for stage in stage_id..config.stages.len() as u32 { + let members = WHITELIST_STAGES + .prefix(stage) + .keys(deps.storage, None, None, Order::Ascending) + .map(|key| key.unwrap()) + .collect::>(); + for member in members.into_iter() { + WHITELIST_STAGES.remove(deps.storage, (stage, member)); + config.num_members -= 1; + } + } + + CONFIG.save(deps.storage, &config)?; + Ok(Response::new() + .add_attribute("action", "add_stage") + .add_attribute("stage_id", config.stages.len().to_string()) + .add_attribute("sender", info.sender)) +} + +/// Increase member limit. Must include a fee if crossing 1000, 2000, etc member limit. +pub fn execute_increase_member_limit( + deps: DepsMut, + info: MessageInfo, + member_limit: u32, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + if config.member_limit >= member_limit || member_limit > MAX_MEMBERS { + return Err(ContractError::InvalidMemberLimit { + min: config.member_limit, + max: MAX_MEMBERS, + got: member_limit, + }); + } + + // if new limit crosses 1,000 members, requires upgrade fee. Otherwise, upgrade. + let old_limit = Decimal::new(config.member_limit.into(), 3).ceil(); + let new_limit = Decimal::new(member_limit.into(), 3).ceil(); + let upgrade_fee: u128 = if new_limit > old_limit { + (new_limit - old_limit).to_u128().unwrap() * PRICE_PER_1000_MEMBERS + } else { + 0 + }; + let payment = may_pay(&info, NATIVE_DENOM)?; + if payment.u128() != upgrade_fee { + return Err(ContractError::IncorrectCreationFee( + payment.u128(), + upgrade_fee, + )); + } + + let mut res = Response::new(); + if upgrade_fee > 0 { + checked_fair_burn(&info, upgrade_fee, None, &mut res)? + } + + config.member_limit = member_limit; + CONFIG.save(deps.storage, &config)?; + Ok(res + .add_attribute("action", "increase_member_limit") + .add_attribute("member_limit", member_limit.to_string())) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Members { + start_after, + limit, + stage_id, + } => to_json_binary(&query_members(deps, start_after, limit, stage_id)?), + + 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::ActiveStage {} => to_json_binary(&fetch_active_stage(deps.storage, &env)), + QueryMsg::HasMember { member } => to_json_binary(&query_has_member(deps, env, member)?), + QueryMsg::Config {} => to_json_binary(&query_config(deps, env)?), + QueryMsg::Stage { stage_id } => to_json_binary(&query_stage(deps, stage_id)?), + QueryMsg::Stages {} => to_json_binary(&query_stage_list(deps)?), + QueryMsg::AdminList {} => to_json_binary(&query_admin_list(deps)?), + QueryMsg::CanExecute { sender, .. } => to_json_binary(&query_can_execute(deps, &sender)?), + QueryMsg::Member { member } => to_json_binary(&query_member(deps, env, member)?), + } +} + +fn query_has_started(deps: Deps, env: Env) -> StdResult { + let config = CONFIG.load(deps.storage)?; + Ok(HasStartedResponse { + has_started: !config.stages.is_empty() && (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_members( + deps: Deps, + start_after: Option, + limit: Option, + stage_id: u32, +) -> StdResult { + let limit = limit + .unwrap_or(PAGINATION_DEFAULT_LIMIT) + .min(PAGINATION_MAX_LIMIT) as usize; + let start_addr = maybe_addr(deps.api, start_after)?; + let start = start_addr.map(Bound::exclusive); + let members = WHITELIST_STAGES + .prefix(stage_id) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|addr| { + let (k, v) = addr?; + Ok(Member { + address: k.to_string(), + mint_count: v, + }) + }) + .collect::>>()?; + + Ok(MembersResponse { members }) +} + +pub fn query_has_member(deps: Deps, env: Env, member: String) -> StdResult { + let addr = deps.api.addr_validate(&member)?; + let active_stage_id = fetch_active_stage_index(deps.storage, &env); + let has_member = match active_stage_id { + Some(stage_id) => WHITELIST_STAGES.has(deps.storage, (stage_id, addr)), + None => false, + }; + Ok(HasMemberResponse { has_member }) +} + +pub fn query_member(deps: Deps, env: Env, member: String) -> StdResult { + let addr = deps.api.addr_validate(&member)?; + let active_stage_id = fetch_active_stage_index(deps.storage, &env); + if active_stage_id.is_none() { + return Err(StdError::generic_err("No active stage found")); + } + let mint_count = + WHITELIST_STAGES.load(deps.storage, (active_stage_id.unwrap(), addr.clone()))?; + Ok(Member { + address: addr.into_string(), + mint_count, + }) +} + +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: config.num_members, + member_limit: config.member_limit, + start_time: stage.start_time, + end_time: stage.end_time, + mint_price: stage.mint_price, + whale_cap: config.whale_cap, + is_active: true, + }) + } else if !config.stages.is_empty() { + 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: config.num_members, + member_limit: config.member_limit, + start_time: stage.start_time, + end_time: stage.end_time, + mint_price: stage.mint_price, + whale_cap: config.whale_cap, + is_active: false, + }) + } else { + Ok(ConfigResponse { + num_members: config.num_members, + member_limit: config.member_limit, + start_time: Timestamp::from_seconds(0), + end_time: Timestamp::from_seconds(0), + mint_price: Coin { + denom: NATIVE_DENOM.to_string(), + amount: Uint128::zero(), + }, + whale_cap: config.whale_cap, + is_active: false, + }) + } +} + +pub fn query_stage(deps: Deps, stage_id: u32) -> StdResult { + let config = CONFIG.load(deps.storage)?; + ensure!( + stage_id < config.stages.len() as u32, + StdError::generic_err("Stage not found") + ); + Ok(StageResponse { + stage: config.stages[stage_id as usize].clone(), + }) +} + +pub fn query_stage_list(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + ensure!( + !config.stages.is_empty(), + StdError::generic_err("No stages found") + ); + Ok(StagesResponse { + stages: config.stages.clone(), + }) +} diff --git a/contracts/whitelists/tiered-whitelist-flex/src/error.rs b/contracts/whitelists/tiered-whitelist-flex/src/error.rs new file mode 100644 index 000000000..be0046b07 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/src/error.rs @@ -0,0 +1,67 @@ +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("DuplicateMember: {0}")] + DuplicateMember(String), + + #[error("NoMemberFound: {0}")] + NoMemberFound(String), + + #[error("InvalidStartTime {0} > {1}")] + InvalidStartTime(Timestamp, Timestamp), + + #[error("InvalidEndTime {0} > {1}")] + InvalidEndTime(Timestamp, Timestamp), + + #[error("MembersExceeded: {expected} got {actual}")] + MembersExceeded { expected: u32, actual: u32 }, + + #[error("Exceeded whale cap")] + ExceededWhaleCap {}, + + #[error("InvalidWhaleCap {0} > {1}")] + InvalidWhaleCap(u32, u32), + + #[error("Invalid minting limit per address. max: {max}, got: {got}")] + InvalidPerAddressLimit { max: String, got: String }, + + #[error("Invalid member limit. min: {min}, max: {max}, got: {got}")] + InvalidMemberLimit { min: u32, max: u32, got: u32 }, + + #[error("Max minting limit per address exceeded")] + MaxPerAddressLimitExceeded {}, + + #[error("The number of stages cannot exceed 3")] + MaxStageCountExceeded {}, + + #[error("Stage not found")] + StageNotFound {}, + + #[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 {}, +} diff --git a/contracts/whitelists/tiered-whitelist-flex/src/helpers.rs b/contracts/whitelists/tiered-whitelist-flex/src/helpers.rs new file mode 100644 index 000000000..29cf1119e --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/src/helpers.rs @@ -0,0 +1,73 @@ +pub mod interface; +pub mod validators; + +use crate::contract::MIN_MINT_PRICE; +use crate::state::{Config, Stage, CONFIG}; +use crate::ContractError; +use cosmwasm_std::{ensure, Env, StdError, Storage}; + +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.is_empty(), + StdError::generic_err("Must have at least one stage") + ); + ensure!( + stages.len() < 4, + StdError::generic_err("Cannot have more than 3 stages") + ); + + // Check mint price is valid + if stages + .iter() + .any(|stage| stage.mint_price.amount.u128() < MIN_MINT_PRICE) + { + return Err(ContractError::InvalidUnitPrice( + MIN_MINT_PRICE, + stages + .iter() + .map(|s| s.mint_price.amount.u128()) + .min() + .unwrap(), + )); + } + + 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 other_stage in stages.iter().skip(i + 1) { + 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-flex/src/helpers/interface.rs b/contracts/whitelists/tiered-whitelist-flex/src/helpers/interface.rs new file mode 100644 index 000000000..c63ddcbd6 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/src/helpers/interface.rs @@ -0,0 +1,28 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +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. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +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-flex/src/helpers/validators.rs b/contracts/whitelists/tiered-whitelist-flex/src/helpers/validators.rs new file mode 100644 index 000000000..b5ffe2984 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/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-flex/src/lib.rs b/contracts/whitelists/tiered-whitelist-flex/src/lib.rs new file mode 100644 index 000000000..b0867fb09 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/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-flex/src/msg.rs b/contracts/whitelists/tiered-whitelist-flex/src/msg.rs new file mode 100644 index 000000000..4879d5670 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/src/msg.rs @@ -0,0 +1,171 @@ +use crate::state::Stage; +use cosmwasm_schema::cw_serde; +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 members: Vec>, + pub stages: Vec, + pub member_limit: u32, + pub admins: Vec, + pub admins_mutable: bool, + pub whale_cap: Option, +} + +#[cw_serde] +pub enum ExecuteMsg { + AddStage(AddStageMsg), + RemoveStage(RemoveStageMsg), + AddMembers(AddMembersMsg), + RemoveMembers(RemoveMembersMsg), + UpdateStageConfig(UpdateStageConfigMsg), + IncreaseMemberLimit(u32), + UpdateAdmins { admins: Vec }, + Freeze {}, +} + +#[cw_serde] +pub struct AdminListResponse { + pub admins: Vec, + pub mutable: bool, +} + +#[cw_serde] +pub struct AddMembersMsg { + pub to_add: Vec, + pub stage_id: u32, +} + +#[cw_serde] +pub struct RemoveMembersMsg { + pub to_remove: Vec, + pub stage_id: u32, +} +#[cw_serde] +pub struct AddStageMsg { + pub stage: Stage, + pub members: Vec, +} + +#[cw_serde] +pub struct RemoveStageMsg { + pub stage_id: u32, +} + +#[cw_serde] +pub struct UpdateStageConfigMsg { + pub stage_id: u32, + pub name: Option, + pub start_time: Option, + pub end_time: Option, + pub mint_price: Option, +} + +#[cw_serde] +pub enum QueryMsg { + HasStarted {}, + HasEnded {}, + IsActive {}, + ActiveStage {}, + Members { + start_after: Option, + limit: Option, + stage_id: u32, + }, + HasMember { + member: String, + }, + + Member { + member: String, + }, + + Config {}, + + Stage { + stage_id: u32, + }, + + Stages {}, + + AdminList {}, + + CanExecute { + sender: String, + msg: CosmosMsg, + }, +} + +#[cw_serde] +pub struct MembersResponse { + pub members: Vec, +} + +#[cw_serde] +pub struct HasMemberResponse { + pub has_member: bool, +} + +#[cw_serde] +pub struct MemberResponse { + pub member: Member, +} + +#[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 member_limit: u32, + pub start_time: Timestamp, + pub end_time: Timestamp, + pub mint_price: Coin, + pub is_active: bool, + pub whale_cap: 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, +} + +#[cw_serde] +pub struct StageResponse { + pub stage: Stage, +} + +#[cw_serde] +pub struct StagesResponse { + pub stages: Vec, +} diff --git a/contracts/whitelists/tiered-whitelist-flex/src/state.rs b/contracts/whitelists/tiered-whitelist-flex/src/state.rs new file mode 100644 index 000000000..5ad0e29ed --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-flex/src/state.rs @@ -0,0 +1,42 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Coin, Timestamp}; +use cw_storage_plus::{Item, Map}; + +#[cw_serde] +pub struct Stage { + pub name: String, + pub start_time: Timestamp, + pub end_time: Timestamp, + pub mint_price: Coin, +} + +#[cw_serde] +pub struct Config { + pub stages: Vec, + pub num_members: u32, + pub member_limit: u32, + pub whale_cap: Option, +} + +#[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 WHITELIST_STAGES: Map<(u32, Addr), u32> = Map::new("wl_stages"); 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..ee12e175c --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/src/contract.rs @@ -0,0 +1,394 @@ +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, MerkleRootResponse, MerkleTreeURIResponse, QueryMsg, + StageResponse, StagesResponse, 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::ActiveStage {} => to_json_binary(&fetch_active_stage(deps.storage, &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::Stage { stage_id } => to_json_binary(&query_stage(deps, stage_id)?), + QueryMsg::Stages {} => to_json_binary(&query_stage_list(deps)?), + 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.is_empty() && (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.is_empty() { + 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(MerkleRootResponse { + merkle_root: MERKLE_ROOTS.load(deps.storage)?, + }) +} + +pub fn query_merkle_tree_uri(deps: Deps) -> StdResult { + Ok(MerkleTreeURIResponse { + merkle_tree_uri: MERKLE_TREE_URIS.may_load(deps.storage)?, + }) +} + +pub fn query_stage(deps: Deps, stage_id: u32) -> StdResult { + let config = CONFIG.load(deps.storage)?; + ensure!( + stage_id < config.stages.len() as u32, + StdError::generic_err("Stage not found") + ); + Ok(StageResponse { + stage: config.stages[stage_id as usize].clone(), + }) +} + +pub fn query_stage_list(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + ensure!( + !config.stages.is_empty(), + StdError::generic_err("No stages found") + ); + Ok(StagesResponse { + stages: config.stages.clone(), + }) +} + +#[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..9c99542f5 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/src/helpers/utils.rs @@ -0,0 +1,96 @@ +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: &str) -> 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.is_empty(), + 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(), + }); + } + + // Check mint price is valid + if stages + .iter() + .any(|stage| stage.mint_price.amount.u128() < MIN_MINT_PRICE) + { + return Err(ContractError::InvalidUnitPrice( + MIN_MINT_PRICE, + stages + .iter() + .map(|s| s.mint_price.amount.u128()) + .min() + .unwrap(), + )); + } + + 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 other_stage in stages.iter().skip(i + 1) { + 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..1828e9dc7 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist-merkletree/src/msg.rs @@ -0,0 +1,145 @@ +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(StageResponse)] + ActiveStage {}, + #[returns(HasMemberResponse)] + HasMember { + member: String, + proof_hashes: Vec, + }, + #[returns(ConfigResponse)] + Config {}, + #[returns(StageResponse)] + Stage { stage_id: u32 }, + #[returns(StagesResponse)] + Stages {}, + #[returns(AdminListResponse)] + AdminList {}, + #[returns(CanExecuteResponse)] + CanExecute { + sender: String, + msg: CosmosMsg, + }, + #[returns(MerkleRootResponse)] + MerkleRoot {}, + #[returns(MerkleTreeURIResponse)] + 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 MerkleRootResponse { + pub merkle_root: Vec, +} + +#[cw_serde] +pub struct MerkleTreeURIResponse { + pub merkle_tree_uri: 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, +} + +#[cw_serde] +pub struct StageResponse { + pub stage: Stage, +} + +#[cw_serde] +pub struct StagesResponse { + pub stages: Vec, +} 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"); diff --git a/contracts/whitelists/tiered-whitelist/.cargo/config b/contracts/whitelists/tiered-whitelist/.cargo/config new file mode 100644 index 000000000..ab407a024 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" diff --git a/contracts/whitelists/tiered-whitelist/.editorconfig b/contracts/whitelists/tiered-whitelist/.editorconfig new file mode 100644 index 000000000..3d36f20b1 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/.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/.gitignore b/contracts/whitelists/tiered-whitelist/.gitignore new file mode 100644 index 000000000..dfdaaa6bc --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/.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/Cargo.toml b/contracts/whitelists/tiered-whitelist/Cargo.toml new file mode 100644 index 000000000..1400014b5 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "sg-tiered-whitelist" +authors = ["Shane Vitarana "] +description = "Stargaze NFT tiered 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 } diff --git a/contracts/whitelists/tiered-whitelist/README.md b/contracts/whitelists/tiered-whitelist/README.md new file mode 100644 index 000000000..93425fc4c --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/README.md @@ -0,0 +1 @@ +# Tiered Whitelist contract diff --git a/contracts/whitelists/tiered-whitelist/examples/schema.rs b/contracts/whitelists/tiered-whitelist/examples/schema.rs new file mode 100644 index 000000000..bd82ca480 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/examples/schema.rs @@ -0,0 +1,31 @@ +use std::env::current_dir; +use std::fs::create_dir_all; + +use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; + +use sg_tiered_whitelist::msg::{ + AdminListResponse, CanExecuteResponse, ConfigResponse, ExecuteMsg, HasEndedResponse, + HasMemberResponse, HasStartedResponse, InstantiateMsg, IsActiveResponse, MembersResponse, + QueryMsg, +}; +use sg_tiered_whitelist::state::Config; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + export_schema(&schema_for!(Config), &out_dir); + export_schema(&schema_for!(ConfigResponse), &out_dir); + export_schema(&schema_for!(ExecuteMsg), &out_dir); + export_schema(&schema_for!(HasEndedResponse), &out_dir); + export_schema(&schema_for!(HasMemberResponse), &out_dir); + export_schema(&schema_for!(HasStartedResponse), &out_dir); + export_schema(&schema_for!(InstantiateMsg), &out_dir); + export_schema(&schema_for!(IsActiveResponse), &out_dir); + export_schema(&schema_for!(MembersResponse), &out_dir); + export_schema(&schema_for!(QueryMsg), &out_dir); + export_schema(&schema_for!(AdminListResponse), &out_dir); + export_schema(&schema_for!(CanExecuteResponse), &out_dir); +} diff --git a/contracts/whitelists/tiered-whitelist/rustfmt.toml b/contracts/whitelists/tiered-whitelist/rustfmt.toml new file mode 100644 index 000000000..11a85e6a9 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/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/schema/admin_list_response.json b/contracts/whitelists/tiered-whitelist/schema/admin_list_response.json new file mode 100644 index 000000000..3b04e955c --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/schema/admin_list_response.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/schema/can_execute_response.json b/contracts/whitelists/tiered-whitelist/schema/can_execute_response.json new file mode 100644 index 000000000..e2ed10214 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/schema/can_execute_response.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/schema/config.json b/contracts/whitelists/tiered-whitelist/schema/config.json new file mode 100644 index 000000000..c5e383ae9 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/schema/config.json @@ -0,0 +1,92 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Config", + "type": "object", + "required": [ + "member_limit", + "num_members", + "stages" + ], + "properties": { + "member_limit": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "num_members": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "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/schema/config_response.json b/contracts/whitelists/tiered-whitelist/schema/config_response.json new file mode 100644 index 000000000..e96c30e53 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/schema/config_response.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/schema/execute_msg.json b/contracts/whitelists/tiered-whitelist/schema/execute_msg.json new file mode 100644 index 000000000..44539dc71 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/schema/execute_msg.json @@ -0,0 +1,313 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "add_stage" + ], + "properties": { + "add_stage": { + "$ref": "#/definitions/AddStageMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "remove_stage" + ], + "properties": { + "remove_stage": { + "$ref": "#/definitions/RemoveStageMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "add_members" + ], + "properties": { + "add_members": { + "$ref": "#/definitions/AddMembersMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "remove_members" + ], + "properties": { + "remove_members": { + "$ref": "#/definitions/RemoveMembersMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_stage_config" + ], + "properties": { + "update_stage_config": { + "$ref": "#/definitions/UpdateStageConfigMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "increase_member_limit" + ], + "properties": { + "increase_member_limit": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "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": { + "AddMembersMsg": { + "type": "object", + "required": [ + "stage_id", + "to_add" + ], + "properties": { + "stage_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "to_add": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "AddStageMsg": { + "type": "object", + "required": [ + "members", + "stage" + ], + "properties": { + "members": { + "type": "array", + "items": { + "type": "string" + } + }, + "stage": { + "$ref": "#/definitions/Stage" + } + }, + "additionalProperties": false + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "RemoveMembersMsg": { + "type": "object", + "required": [ + "stage_id", + "to_remove" + ], + "properties": { + "stage_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "to_remove": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "RemoveStageMsg": { + "type": "object", + "required": [ + "stage_id" + ], + "properties": { + "stage_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "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" + }, + "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/schema/has_ended_response.json b/contracts/whitelists/tiered-whitelist/schema/has_ended_response.json new file mode 100644 index 000000000..6e207ab48 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/schema/has_ended_response.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/schema/has_member_response.json b/contracts/whitelists/tiered-whitelist/schema/has_member_response.json new file mode 100644 index 000000000..8e203003b --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/schema/has_member_response.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/schema/has_started_response.json b/contracts/whitelists/tiered-whitelist/schema/has_started_response.json new file mode 100644 index 000000000..25614f8d8 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/schema/has_started_response.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/schema/instantiate_msg.json b/contracts/whitelists/tiered-whitelist/schema/instantiate_msg.json new file mode 100644 index 000000000..75250c5c1 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/schema/instantiate_msg.json @@ -0,0 +1,107 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "admins", + "admins_mutable", + "member_limit", + "members", + "stages" + ], + "properties": { + "admins": { + "type": "array", + "items": { + "type": "string" + } + }, + "admins_mutable": { + "type": "boolean" + }, + "member_limit": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "members": { + "type": "array", + "items": { + "type": "array", + "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/schema/is_active_response.json b/contracts/whitelists/tiered-whitelist/schema/is_active_response.json new file mode 100644 index 000000000..2dc928c41 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/schema/is_active_response.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/schema/members_response.json b/contracts/whitelists/tiered-whitelist/schema/members_response.json new file mode 100644 index 000000000..0236b953f --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/schema/members_response.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MembersResponse", + "type": "object", + "required": [ + "members" + ], + "properties": { + "members": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false +} diff --git a/contracts/whitelists/tiered-whitelist/schema/query_msg.json b/contracts/whitelists/tiered-whitelist/schema/query_msg.json new file mode 100644 index 000000000..937ab19a6 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/schema/query_msg.json @@ -0,0 +1,504 @@ +{ + "$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": [ + "active_stage" + ], + "properties": { + "active_stage": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "members" + ], + "properties": { + "members": { + "type": "object", + "required": [ + "stage_id" + ], + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "stage_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "has_member" + ], + "properties": { + "has_member": { + "type": "object", + "required": [ + "member" + ], + "properties": { + "member": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "stage" + ], + "properties": { + "stage": { + "type": "object", + "required": [ + "stage_id" + ], + "properties": { + "stage_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "stages" + ], + "properties": { + "stages": { + "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 + } + ], + "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/src/admin.rs b/contracts/whitelists/tiered-whitelist/src/admin.rs new file mode 100644 index 000000000..f4c065b9f --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/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/src/contract.rs b/contracts/whitelists/tiered-whitelist/src/contract.rs new file mode 100644 index 000000000..bceabcb92 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/src/contract.rs @@ -0,0 +1,520 @@ +use crate::admin::{ + can_execute, execute_freeze, execute_update_admins, query_admin_list, query_can_execute, +}; +use crate::error::ContractError; +use crate::helpers::validators::map_validate; +use crate::helpers::{fetch_active_stage, fetch_active_stage_index, validate_stages}; +use crate::msg::{ + AddMembersMsg, ConfigResponse, ExecuteMsg, HasEndedResponse, HasMemberResponse, + HasStartedResponse, InstantiateMsg, IsActiveResponse, MembersResponse, QueryMsg, + RemoveMembersMsg, StageResponse, StagesResponse, UpdateStageConfigMsg, +}; +use crate::state::{AdminList, Config, Stage, ADMIN_LIST, CONFIG, WHITELIST_STAGES}; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + ensure, to_json_binary, Binary, Coin, Deps, DepsMut, Env, MessageInfo, StdResult, Timestamp, + Uint128, +}; +use cosmwasm_std::{Order, StdError}; +use cw2::set_contract_version; +use cw_storage_plus::Bound; +use cw_utils::{may_pay, maybe_addr, must_pay}; +use rust_decimal::prelude::ToPrimitive; +use rust_decimal::Decimal; +use sg1::checked_fair_burn; +use sg_std::{Response, NATIVE_DENOM}; + +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:sg-tiered-whitelist"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +// contract governance params +pub const MAX_MEMBERS: u32 = 30000; +pub const PRICE_PER_1000_MEMBERS: u128 = 100_000_000; +pub const MIN_MINT_PRICE: u128 = 0; +pub const MAX_PER_ADDRESS_LIMIT: u32 = 30; + +// queries +const PAGINATION_DEFAULT_LIMIT: u32 = 25; +const PAGINATION_MAX_LIMIT: u32 = 100; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + mut msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + if msg.member_limit == 0 || msg.member_limit > MAX_MEMBERS { + return Err(ContractError::InvalidMemberLimit { + min: 1, + max: MAX_MEMBERS, + got: msg.member_limit, + }); + } + + validate_stages(&env, &msg.stages)?; + + let creation_fee = Decimal::new(msg.member_limit.into(), 3) + .ceil() + .to_u128() + .unwrap() + * PRICE_PER_1000_MEMBERS; + let payment = must_pay(&info, NATIVE_DENOM)?; + if payment.u128() != creation_fee { + return Err(ContractError::IncorrectCreationFee( + payment.u128(), + creation_fee, + )); + } + + // remove duplicate from the vector of member lists for each stage + msg.members + .iter_mut() + .for_each(|members| members.sort_unstable()); + msg.members.iter_mut().for_each(|members| members.dedup()); + + let config = Config { + stages: msg.stages.clone(), + num_members: msg.members.iter().map(|m| m.len() as u32).sum(), + member_limit: msg.member_limit, + }; + CONFIG.save(deps.storage, &config)?; + + let admin_config = AdminList { + admins: map_validate(deps.api, &msg.admins)?, + mutable: msg.admins_mutable, + }; + ADMIN_LIST.save(deps.storage, &admin_config)?; + + let mut res = Response::new(); + checked_fair_burn(&info, creation_fee, None, &mut res)?; + + if config.member_limit < config.num_members { + return Err(ContractError::MembersExceeded { + expected: config.member_limit, + actual: config.num_members, + }); + } + + for stage in 0..msg.stages.clone().len() { + for member in msg.members[stage].iter() { + let addr = deps.api.addr_validate(member)?; + WHITELIST_STAGES.save(deps.storage, (stage as u32, addr), &true)?; + } + } + + Ok(res + .add_attribute("action", "instantiate") + .add_attribute("contract_name", CONTRACT_NAME) + .add_attribute("contract_version", CONTRACT_VERSION) + .add_attribute("sender", info.sender)) +} + +#[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::AddStage(msg) => execute_add_stage(deps, env, info, msg.stage, msg.members), + ExecuteMsg::RemoveStage(msg) => execute_remove_stage(deps, env, info, msg.stage_id), + ExecuteMsg::AddMembers(msg) => execute_add_members(deps, env, info, msg), + ExecuteMsg::RemoveMembers(msg) => execute_remove_members(deps, env, info, msg), + ExecuteMsg::IncreaseMemberLimit(member_limit) => { + execute_increase_member_limit(deps, info, member_limit) + } + ExecuteMsg::UpdateAdmins { admins } => execute_update_admins(deps, env, info, admins), + ExecuteMsg::Freeze {} => execute_freeze(deps, env, info), + } +} + +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)) +} + +pub fn execute_add_members( + deps: DepsMut, + _env: Env, + info: MessageInfo, + mut msg: AddMembersMsg, +) -> Result { + can_execute(&deps, info.sender.clone())?; + let mut config = CONFIG.load(deps.storage)?; + ensure!( + msg.stage_id < config.stages.len() as u32, + ContractError::StageNotFound {} + ); + // remove duplicate members + msg.to_add.sort_unstable(); + msg.to_add.dedup(); + let mut members_added = 0; + for add in msg.to_add.into_iter() { + if config.num_members >= config.member_limit { + return Err(ContractError::MembersExceeded { + expected: config.member_limit, + actual: config.num_members, + }); + } + let addr = deps.api.addr_validate(&add)?; + if WHITELIST_STAGES.has(deps.storage, (msg.stage_id, addr.clone())) { + continue; + } + members_added += 1; + WHITELIST_STAGES.save(deps.storage, (msg.stage_id, addr.clone()), &true)?; + config.num_members += 1; + } + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new() + .add_attribute("action", "add_members") + .add_attribute("stage_id", msg.stage_id.to_string()) + .add_attribute("num_members", config.num_members.to_string()) + .add_attribute("members_added", members_added.to_string()) + .add_attribute("sender", info.sender)) +} + +pub fn execute_remove_members( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: RemoveMembersMsg, +) -> Result { + can_execute(&deps, info.sender.clone())?; + let mut config = CONFIG.load(deps.storage)?; + ensure!( + msg.stage_id < config.stages.len() as u32, + ContractError::StageNotFound {} + ); + + ensure!( + env.block.time < config.stages[msg.stage_id as usize].start_time, + ContractError::AlreadyStarted {} + ); + + for remove in msg.to_remove.into_iter() { + let addr = deps.api.addr_validate(&remove)?; + if !WHITELIST_STAGES.has(deps.storage, (msg.stage_id, addr.clone())) { + return Err(ContractError::NoMemberFound(addr.to_string())); + } + WHITELIST_STAGES.remove(deps.storage, (msg.stage_id, addr.clone())); + config.num_members -= 1; + } + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new() + .add_attribute("action", "remove_members") + .add_attribute("stage_id", msg.stage_id.to_string()) + .add_attribute("sender", info.sender)) +} + +pub fn execute_add_stage( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: Stage, + mut members: Vec, +) -> Result { + can_execute(&deps, info.sender.clone())?; + let mut config = CONFIG.load(deps.storage)?; + ensure!( + config.stages.len().lt(&3), + ContractError::MaxStageCountExceeded {} + ); + config.stages.push(msg.clone()); + validate_stages(&env, &config.stages)?; + let stage_id = config.stages.len().saturating_sub(1) as u32; + + // remove duplicate members + members.sort_unstable(); + members.dedup(); + for add in members.into_iter() { + if config.num_members >= config.member_limit { + return Err(ContractError::MembersExceeded { + expected: config.member_limit, + actual: config.num_members, + }); + } + let addr = deps.api.addr_validate(&add)?; + if WHITELIST_STAGES.has(deps.storage, (stage_id, addr.clone())) { + continue; + } + WHITELIST_STAGES.save(deps.storage, (stage_id, addr.clone()), &true)?; + config.num_members += 1; + } + + CONFIG.save(deps.storage, &config)?; + Ok(Response::new() + .add_attribute("action", "add_stage") + .add_attribute("stage_id", config.stages.len().to_string()) + .add_attribute("sender", info.sender)) +} + +pub fn execute_remove_stage( + deps: DepsMut, + env: Env, + info: MessageInfo, + stage_id: u32, +) -> Result { + can_execute(&deps, info.sender.clone())?; + let mut config = CONFIG.load(deps.storage)?; + ensure!( + config.stages.len().gt(&(stage_id as usize)), + ContractError::StageNotFound {} + ); + + ensure!( + env.block.time < config.stages[stage_id as usize].start_time, + ContractError::AlreadyStarted {} + ); + // remove the stage and following stages permanently + config.stages = config.stages.into_iter().take(stage_id as usize).collect(); + + // remove members from the WHITELIST_STAGES for stage_id and following stages. Reduce the num_members count + for stage in stage_id..config.stages.len() as u32 { + let members = WHITELIST_STAGES + .prefix(stage) + .keys(deps.storage, None, None, Order::Ascending) + .map(|key| key.unwrap()) + .collect::>(); + for member in members.into_iter() { + WHITELIST_STAGES.remove(deps.storage, (stage, member)); + config.num_members -= 1; + } + } + + CONFIG.save(deps.storage, &config)?; + Ok(Response::new() + .add_attribute("action", "add_stage") + .add_attribute("stage_id", config.stages.len().to_string()) + .add_attribute("sender", info.sender)) +} + +/// Increase member limit. Must include a fee if crossing 1000, 2000, etc member limit. +pub fn execute_increase_member_limit( + deps: DepsMut, + info: MessageInfo, + member_limit: u32, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + if config.member_limit >= member_limit || member_limit > MAX_MEMBERS { + return Err(ContractError::InvalidMemberLimit { + min: config.member_limit, + max: MAX_MEMBERS, + got: member_limit, + }); + } + + // if new limit crosses 1,000 members, requires upgrade fee. Otherwise, upgrade. + let old_limit = Decimal::new(config.member_limit.into(), 3).ceil(); + let new_limit = Decimal::new(member_limit.into(), 3).ceil(); + let upgrade_fee: u128 = if new_limit > old_limit { + (new_limit - old_limit).to_u128().unwrap() * PRICE_PER_1000_MEMBERS + } else { + 0 + }; + let payment = may_pay(&info, NATIVE_DENOM)?; + if payment.u128() != upgrade_fee { + return Err(ContractError::IncorrectCreationFee( + payment.u128(), + upgrade_fee, + )); + } + + let mut res = Response::new(); + if upgrade_fee > 0 { + checked_fair_burn(&info, upgrade_fee, None, &mut res)? + } + + config.member_limit = member_limit; + CONFIG.save(deps.storage, &config)?; + Ok(res + .add_attribute("action", "increase_member_limit") + .add_attribute("member_limit", member_limit.to_string())) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Members { + start_after, + limit, + stage_id, + } => to_json_binary(&query_members(deps, start_after, limit, stage_id)?), + + 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::ActiveStage {} => to_json_binary(&fetch_active_stage(deps.storage, &env)), + QueryMsg::HasMember { member } => to_json_binary(&query_has_member(deps, env, member)?), + QueryMsg::Config {} => to_json_binary(&query_config(deps, env)?), + QueryMsg::Stage { stage_id } => to_json_binary(&query_stage(deps, stage_id)?), + QueryMsg::Stages {} => to_json_binary(&query_stage_list(deps)?), + QueryMsg::AdminList {} => to_json_binary(&query_admin_list(deps)?), + QueryMsg::CanExecute { sender, .. } => to_json_binary(&query_can_execute(deps, &sender)?), + } +} + +fn query_has_started(deps: Deps, env: Env) -> StdResult { + let config = CONFIG.load(deps.storage)?; + Ok(HasStartedResponse { + has_started: !config.stages.is_empty() && (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_members( + deps: Deps, + start_after: Option, + limit: Option, + stage_id: u32, +) -> StdResult { + let limit = limit + .unwrap_or(PAGINATION_DEFAULT_LIMIT) + .min(PAGINATION_MAX_LIMIT) as usize; + let start_addr = maybe_addr(deps.api, start_after)?; + let start = start_addr.map(Bound::exclusive); + let members = WHITELIST_STAGES + .prefix(stage_id) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|addr| addr.unwrap().0.to_string()) + .collect::>(); + + Ok(MembersResponse { members }) +} + +pub fn query_has_member(deps: Deps, env: Env, member: String) -> StdResult { + let addr = deps.api.addr_validate(&member)?; + let active_stage_id = fetch_active_stage_index(deps.storage, &env); + let has_member = match active_stage_id { + Some(stage_id) => WHITELIST_STAGES.has(deps.storage, (stage_id, addr)), + None => false, + }; + Ok(HasMemberResponse { has_member }) +} + +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: config.num_members, + per_address_limit: stage.per_address_limit, + member_limit: config.member_limit, + start_time: stage.start_time, + end_time: stage.end_time, + mint_price: stage.mint_price, + is_active: true, + }) + } else if !config.stages.is_empty() { + 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: config.num_members, + per_address_limit: stage.per_address_limit, + member_limit: config.member_limit, + start_time: stage.start_time, + end_time: stage.end_time, + mint_price: stage.mint_price, + is_active: false, + }) + } else { + Ok(ConfigResponse { + num_members: config.num_members, + per_address_limit: 0, + member_limit: config.member_limit, + 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_stage(deps: Deps, stage_id: u32) -> StdResult { + let config = CONFIG.load(deps.storage)?; + ensure!( + stage_id < config.stages.len() as u32, + StdError::generic_err("Stage not found") + ); + Ok(StageResponse { + stage: config.stages[stage_id as usize].clone(), + }) +} + +pub fn query_stage_list(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + ensure!( + !config.stages.is_empty(), + StdError::generic_err("No stages found") + ); + Ok(StagesResponse { + stages: config.stages.clone(), + }) +} diff --git a/contracts/whitelists/tiered-whitelist/src/error.rs b/contracts/whitelists/tiered-whitelist/src/error.rs new file mode 100644 index 000000000..6ed3d3bfc --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/src/error.rs @@ -0,0 +1,61 @@ +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("DuplicateMember: {0}")] + DuplicateMember(String), + + #[error("NoMemberFound: {0}")] + NoMemberFound(String), + + #[error("InvalidStartTime {0} > {1}")] + InvalidStartTime(Timestamp, Timestamp), + + #[error("InvalidEndTime {0} > {1}")] + InvalidEndTime(Timestamp, Timestamp), + + #[error("MembersExceeded: {expected} got {actual}")] + MembersExceeded { expected: u32, actual: u32 }, + + #[error("Invalid minting limit per address. max: {max}, got: {got}")] + InvalidPerAddressLimit { max: String, got: String }, + + #[error("Invalid member limit. min: {min}, max: {max}, got: {got}")] + InvalidMemberLimit { min: u32, max: u32, got: u32 }, + + #[error("Max minting limit per address exceeded")] + MaxPerAddressLimitExceeded {}, + + #[error("The number of stages cannot exceed 3")] + MaxStageCountExceeded {}, + + #[error("Stage not found")] + StageNotFound {}, + + #[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 {}, +} diff --git a/contracts/whitelists/tiered-whitelist/src/helpers.rs b/contracts/whitelists/tiered-whitelist/src/helpers.rs new file mode 100644 index 000000000..a5cafb13a --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/src/helpers.rs @@ -0,0 +1,88 @@ +pub mod interface; +pub mod validators; + +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, Storage}; + +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.is_empty(), + 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(), + }); + } + + // Check mint price is valid + if stages + .iter() + .any(|stage| stage.mint_price.amount.u128() < MIN_MINT_PRICE) + { + return Err(ContractError::InvalidUnitPrice( + MIN_MINT_PRICE, + stages + .iter() + .map(|s| s.mint_price.amount.u128()) + .min() + .unwrap(), + )); + } + + 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 other_stage in stages.iter().skip(i + 1) { + 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/src/helpers/interface.rs b/contracts/whitelists/tiered-whitelist/src/helpers/interface.rs new file mode 100644 index 000000000..c63ddcbd6 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/src/helpers/interface.rs @@ -0,0 +1,28 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +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. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +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/src/helpers/validators.rs b/contracts/whitelists/tiered-whitelist/src/helpers/validators.rs new file mode 100644 index 000000000..b5ffe2984 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/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/src/lib.rs b/contracts/whitelists/tiered-whitelist/src/lib.rs new file mode 100644 index 000000000..b0867fb09 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/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/src/msg.rs b/contracts/whitelists/tiered-whitelist/src/msg.rs new file mode 100644 index 000000000..e15c06df7 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/src/msg.rs @@ -0,0 +1,156 @@ +use crate::state::Stage; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Coin, CosmosMsg, Empty, Timestamp}; + +#[cw_serde] +pub struct InstantiateMsg { + pub members: Vec>, + pub stages: Vec, + pub member_limit: u32, + pub admins: Vec, + pub admins_mutable: bool, +} + +#[cw_serde] +pub enum ExecuteMsg { + AddStage(AddStageMsg), + RemoveStage(RemoveStageMsg), + AddMembers(AddMembersMsg), + RemoveMembers(RemoveMembersMsg), + UpdateStageConfig(UpdateStageConfigMsg), + IncreaseMemberLimit(u32), + UpdateAdmins { admins: Vec }, + Freeze {}, +} + +#[cw_serde] +pub struct AdminListResponse { + pub admins: Vec, + pub mutable: bool, +} + +#[cw_serde] +pub struct AddMembersMsg { + pub to_add: Vec, + pub stage_id: u32, +} + +#[cw_serde] +pub struct RemoveMembersMsg { + pub to_remove: Vec, + pub stage_id: u32, +} +#[cw_serde] +pub struct AddStageMsg { + pub stage: Stage, + pub members: Vec, +} + +#[cw_serde] +pub struct RemoveStageMsg { + pub stage_id: u32, +} + +#[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] +pub enum QueryMsg { + HasStarted {}, + HasEnded {}, + IsActive {}, + ActiveStage {}, + Members { + start_after: Option, + limit: Option, + stage_id: u32, + }, + HasMember { + member: String, + }, + Config {}, + + Stage { + stage_id: u32, + }, + + Stages {}, + + AdminList {}, + + CanExecute { + sender: String, + msg: CosmosMsg, + }, +} + +#[cw_serde] +pub struct MembersResponse { + pub members: Vec, +} + +#[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 enum SudoMsg { + /// Add a new operator + AddOperator { operator: String }, + /// Remove operator + RemoveOperator { operator: String }, +} + +#[cw_serde] +pub struct CanExecuteResponse { + pub can_execute: bool, +} + +#[cw_serde] +pub struct StageResponse { + pub stage: Stage, +} + +#[cw_serde] +pub struct StagesResponse { + pub stages: Vec, +} diff --git a/contracts/whitelists/tiered-whitelist/src/state.rs b/contracts/whitelists/tiered-whitelist/src/state.rs new file mode 100644 index 000000000..57abd3443 --- /dev/null +++ b/contracts/whitelists/tiered-whitelist/src/state.rs @@ -0,0 +1,42 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Coin, Timestamp}; +use cw_storage_plus::{Item, Map}; + +#[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, + pub num_members: u32, + pub member_limit: u32, +} + +#[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 WHITELIST_STAGES: Map<(u32, Addr), bool> = Map::new("wl_stages");