Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/whitelist merkletree #2 #620

Merged
merged 7 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,119 changes: 638 additions & 481 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 2 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ sg-whitelist-flex = { version = "3.2.9", path = "contracts/whitelists/whiteli
ethereum-verify = { version = "3.2.9", path = "packages/ethereum-verify" }
sg-eth-airdrop = { version = "3.2.9", path = "contracts/sg-eth-airdrop" }
test-suite = { version = "3.2.9", path = "test-suite" }
whitelist-mtree = { version = "3.2.9", path = "contracts/whitelists/whitelist-merkletree" }

semver = "1"
cw-ownable = "0.5.1"

Expand Down Expand Up @@ -130,10 +132,6 @@ incremental = false
codegen-units = 1
incremental = false

[profile.release.package.test-suite]
codegen-units = 1
incremental = false

[profile.release]
rpath = false
lto = true
Expand Down
47 changes: 24 additions & 23 deletions contracts/minters/vending-minter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,26 +29,27 @@ backtraces = ["cosmwasm-std/backtraces"]
library = []

[dependencies]
cosmwasm-schema = { workspace = true }
cosmwasm-std = { workspace = true }
cw2 = { workspace = true }
cw721 = { workspace = true }
cw721-base = { workspace = true, features = ["library"] }
cw-storage-plus = { workspace = true }
cw-utils = { workspace = true }
rand_core = { version = "0.6.4", default-features = false }
rand_xoshiro = { version = "0.6.0", default-features = false }
schemars = { workspace = true }
serde = { workspace = true }
sha2 = { workspace = true }
shuffle = { git = "https://github.com/webmaster128/shuffle", branch = "rm-getrandom", version = "0.1.7" }
sg1 = { workspace = true }
sg2 = { workspace = true }
sg4 = { workspace = true }
sg721 = { workspace = true }
sg-std = { workspace = true }
sg-whitelist = { workspace = true, features = ["library"] }
thiserror = { workspace = true }
url = { workspace = true }
vending-factory = { workspace = true, features = ["library"] }
semver = {workspace = true }
cosmwasm-schema = { workspace = true }
cosmwasm-std = { workspace = true }
cw2 = { workspace = true }
cw721 = { workspace = true }
cw721-base = { workspace = true, features = ["library"] }
cw-storage-plus = { workspace = true }
cw-utils = { workspace = true }
rand_core = { version = "0.6.4", default-features = false }
rand_xoshiro = { version = "0.6.0", default-features = false }
schemars = { workspace = true }
serde = { workspace = true }
sha2 = { workspace = true }
shuffle = { git = "https://github.com/webmaster128/shuffle", branch = "rm-getrandom", version = "0.1.7" }
sg1 = { workspace = true }
sg2 = { workspace = true }
sg4 = { workspace = true }
sg721 = { workspace = true }
sg-std = { workspace = true }
sg-whitelist = { workspace = true, features = ["library"] }
whitelist-mtree = { workspace = true, features = ["library"] }
thiserror = { workspace = true }
url = { workspace = true }
vending-factory = { workspace = true, features = ["library"] }
semver = { workspace = true }
38 changes: 29 additions & 9 deletions contracts/minters/vending-minter/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use sg_std::{
use sg_whitelist::msg::{
ConfigResponse as WhitelistConfigResponse, HasMemberResponse, QueryMsg as WhitelistQueryMsg,
};
use whitelist_mtree::msg::QueryMsg as WhitelistMtreeQueryMsg;
use sha2::{Digest, Sha256};
use shuffle::{fy::FisherYates, shuffler::Shuffler};
use std::convert::TryInto;
Expand Down Expand Up @@ -218,7 +219,7 @@ pub fn execute(
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::Mint {} => execute_mint_sender(deps, env, info),
ExecuteMsg::Mint { proof_hashes } => execute_mint_sender(deps, env, info, proof_hashes),
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),
Expand Down Expand Up @@ -483,13 +484,14 @@ pub fn execute_mint_sender(
deps: DepsMut,
env: Env,
info: MessageInfo,
proof_hashes: Option<Vec<String>>,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
let action = "mint_sender";

// If there is no active whitelist right now, check public mint
// Check if after start_time
if is_public_mint(deps.as_ref(), &info)? && (env.block.time < config.extension.start_time) {
if is_public_mint(deps.as_ref(), &info, proof_hashes)? && (env.block.time < config.extension.start_time) {
return Err(ContractError::BeforeMintStartTime {});
}

Expand All @@ -504,7 +506,7 @@ pub fn execute_mint_sender(

// Check if a whitelist exists and not ended
// Sender has to be whitelisted to mint
fn is_public_mint(deps: Deps, info: &MessageInfo) -> Result<bool, ContractError> {
fn is_public_mint(deps: Deps, info: &MessageInfo, proof_hashes: Option<Vec<String>>) -> Result<bool, ContractError> {
let config = CONFIG.load(deps.storage)?;

// If there is no whitelist, there's only a public mint
Expand All @@ -522,12 +524,23 @@ fn is_public_mint(deps: Deps, info: &MessageInfo) -> Result<bool, ContractError>
return Ok(true);
}

let res: HasMemberResponse = deps.querier.query_wasm_smart(
whitelist,
&WhitelistQueryMsg::HasMember {
member: info.sender.to_string(),
},
)?;
let res: HasMemberResponse = if is_merkle_tree_wl(&wl_config) && proof_hashes.is_some() {
deps.querier.query_wasm_smart(
whitelist,
&WhitelistMtreeQueryMsg::HasMember {
member: info.sender.to_string(),
proof_hashes: proof_hashes.unwrap(),
},
)?
} else {
deps.querier.query_wasm_smart(
whitelist,
&WhitelistQueryMsg::HasMember {
member: info.sender.to_string(),
},
)?
};

if !res.has_member {
return Err(ContractError::NotWhitelisted {
addr: info.sender.to_string(),
Expand All @@ -543,6 +556,13 @@ fn is_public_mint(deps: Deps, info: &MessageInfo) -> Result<bool, ContractError>
Ok(false)
}


fn is_merkle_tree_wl(wl_config_res: &WhitelistConfigResponse) -> bool {
wl_config_res.member_limit == 0 &&
wl_config_res.num_members == 0
}


pub fn execute_mint_to(
deps: DepsMut,
env: Env,
Expand Down
4 changes: 3 additions & 1 deletion contracts/minters/vending-minter/src/msg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ pub struct InstantiateMsg {

#[cw_serde]
pub enum ExecuteMsg {
Mint {},
Mint {
proof_hashes: Option<Vec<String>>
},
SetWhitelist {
whitelist: String,
},
Expand Down
4 changes: 4 additions & 0 deletions contracts/whitelists/whitelist-merkletree/.cargo/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[alias]
wasm = "build --release --lib --target wasm32-unknown-unknown"
unit-test = "test --lib"
schema = "run --bin schema"
11 changes: 11 additions & 0 deletions contracts/whitelists/whitelist-merkletree/.editorconfig
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions contracts/whitelists/whitelist-merkletree/.gitignore
Original file line number Diff line number Diff line change
@@ -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
44 changes: 44 additions & 0 deletions contracts/whitelists/whitelist-merkletree/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
[package]
name = "whitelist-mtree"
authors = ["Martin Mo Kromsten <[email protected]>"]
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 }

26 changes: 26 additions & 0 deletions contracts/whitelists/whitelist-merkletree/README.md
Original file line number Diff line number Diff line change
@@ -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.
15 changes: 15 additions & 0 deletions contracts/whitelists/whitelist-merkletree/rustfmt.toml
Original file line number Diff line number Diff line change
@@ -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"

81 changes: 81 additions & 0 deletions contracts/whitelists/whitelist-merkletree/schema/raw/execute.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ExecuteMsg",
"oneOf": [
{
"type": "object",
"required": [
"update_start_time"
],
"properties": {
"update_start_time": {
"$ref": "#/definitions/Timestamp"
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"update_end_time"
],
"properties": {
"update_end_time": {
"$ref": "#/definitions/Timestamp"
}
},
"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": {
"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"
}
]
},
"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"
}
}
}
Loading