Repository: public-awesome/launchpad
Pull request #314 (2022-06-23 19:47:57)

randomized token map { position: id }
**DO NOT MERGE YET** need to remove the mintable tokens query before merging

saving the shuffled map doesn't make a difference since it'll be hashed the same way in the merkle tree so querying order will not change. requires using an index. data needs both `token_key` and `token_id`. to both query the presence of `token_id` and know which `token_key` to remove.

jhernandez created multiple small shuffles on the front and back of the list, skipping a pseudo-random number of entries.

this is the v0.1 PR resulting from a series of tests
- switch tokens to multi map
- switch tokens to array
- benchmark against existing token instantiation
- move validation to the front of function blocks

we settled on this version because it adds cost efficient randomization and a few additional enhancements.

still need to test a few things on testnet:
- try minter instantiate look at tx for gas
- try random minting and look at tx's for gas
- try shuffle and look at tx's for gas
- try random minting and look at tx's for gas
- try mint_for multiple times and inspect token ids
- try mint_to multiple times and inspect token ids

```
TESTNET CODE_ID: 91
minter: stars15gdrt02y3x6juldarcu2rt6y6l8gklaneh446uj2yrcsa7myamtqj5zlgf
sg721:  stars175evjsxpxf5347an078zmk3gzjy7df6kjml0mfwxvzpuu5jps8gsx3caxe
batch mint 20 tokens takes 37M gas https://stargaze-testnet-explorer.pages.dev/stargaze/tx/A72BAD4520B2E23DEE2844B4A735FC33931588A86C414EF200A978DEC05A6F8F
```



Patch:
diff --git a/contracts/minter/schema/execute_msg.json b/contracts/minter/schema/execute_msg.json
index e0763c56..98763d66 100644
--- a/contracts/minter/schema/execute_msg.json
+++ b/contracts/minter/schema/execute_msg.json
@@ -114,6 +114,18 @@
       },
       "additionalProperties": false
     },
+    {
+      "type": "object",
+      "required": [
+        "shuffle"
+      ],
+      "properties": {
+        "shuffle": {
+          "type": "object"
+        }
+      },
+      "additionalProperties": false
+    },
     {
       "type": "object",
       "required": [
diff --git a/contracts/minter/schema/query_msg.json b/contracts/minter/schema/query_msg.json
index c97025f7..6f83bb55 100644
--- a/contracts/minter/schema/query_msg.json
+++ b/contracts/minter/schema/query_msg.json
@@ -69,6 +69,18 @@
         }
       },
       "additionalProperties": false
+    },
+    {
+      "type": "object",
+      "required": [
+        "mintable_tokens"
+      ],
+      "properties": {
+        "mintable_tokens": {
+          "type": "object"
+        }
+      },
+      "additionalProperties": false
     }
   ]
 }
diff --git a/contracts/minter/src/contract.rs b/contracts/minter/src/contract.rs
index 656f7603..33c28920 100644
--- a/contracts/minter/src/contract.rs
+++ b/contracts/minter/src/contract.rs
@@ -3,10 +3,10 @@ use std::convert::TryInto;
 use crate::error::ContractError;
 use crate::msg::{
     ConfigResponse, ExecuteMsg, InstantiateMsg, MintCountResponse, MintPriceResponse,
-    MintableNumTokensResponse, QueryMsg, StartTimeResponse,
+    MintableNumTokensResponse, MintableTokensResponse, QueryMsg, StartTimeResponse,
 };
 use crate::state::{
-    Config, CONFIG, MINTABLE_NUM_TOKENS, MINTABLE_TOKEN_IDS, MINTER_ADDRS, SG721_ADDRESS,
+    Config, CONFIG, MINTABLE_NUM_TOKENS, MINTABLE_TOKEN_POSITIONS, MINTER_ADDRS, SG721_ADDRESS,
 };
 #[cfg(not(feature = "library"))]
 use cosmwasm_std::entry_point;
@@ -31,6 +31,11 @@ use whitelist::msg::{
 pub type Response = cosmwasm_std::Response<StargazeMsgWrapper>;
 pub type SubMsg = cosmwasm_std::SubMsg<StargazeMsgWrapper>;
 
+pub struct TokenPositionMapping {
+    pub position: u32,
+    pub token_id: u32,
+}
+
 // version info for migration info
 const CONTRACT_NAME: &str = "crates.io:sg-minter";
 const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -42,6 +47,7 @@ const MAX_TOKEN_LIMIT: u32 = 10000;
 const MAX_PER_ADDRESS_LIMIT: u32 = 50;
 const MIN_MINT_PRICE: u128 = 50_000_000;
 const MINT_FEE_PERCENT: u32 = 10;
+const SHUFFLE_FEE: u128 = 500_000_000;
 
 #[cfg_attr(not(feature = "library"), entry_point)]
 pub fn instantiate(
@@ -122,10 +128,16 @@ pub fn instantiate(
     CONFIG.save(deps.storage, &config)?;
     MINTABLE_NUM_TOKENS.save(deps.storage, &msg.num_tokens)?;
 
-    let token_list = random_token_list(&env, msg.num_tokens)?;
+    let token_ids = random_token_list(
+        &env,
+        info.sender.clone(),
+        (1..=msg.num_tokens).collect::<Vec<u32>>(),
+    )?;
     // Save mintable token ids map
-    for token_id in token_list {
-        MINTABLE_TOKEN_IDS.save(deps.storage, token_id, &true)?;
+    let mut token_position = 1;
+    for token_id in token_ids {
+        MINTABLE_TOKEN_POSITIONS.save(deps.storage, token_position, &token_id)?;
+        token_position += 1;
     }
 
     // Submessage to instantiate sg721 contract
@@ -177,10 +189,46 @@ pub fn execute(
         ExecuteMsg::SetWhitelist { whitelist } => {
             execute_set_whitelist(deps, env, info, &whitelist)
         }
+        ExecuteMsg::Shuffle {} => execute_shuffle(deps, env, info),
         ExecuteMsg::Withdraw {} => execute_withdraw(deps, env, info),
     }
 }
 
+// Anyone can pay to shuffle at any time
+// Introduces another source of randomness to minting
+// There's a fee because this action is expensive.
+pub fn execute_shuffle(
+    deps: DepsMut,
+    env: Env,
+    info: MessageInfo,
+) -> Result<Response, ContractError> {
+    // Check exact shuffle fee payment included in message
+    let fee_msgs = checked_fair_burn(&info, SHUFFLE_FEE)?;
+    // Check not sold out
+    let mintable_num_tokens = MINTABLE_NUM_TOKENS.load(deps.storage)?;
+    if mintable_num_tokens == 0 {
+        return Err(ContractError::SoldOut {});
+    }
+
+    // get positions and token_ids, then randomize token_ids and reassign positions
+    let mut positions = vec![];
+    let mut token_ids = vec![];
+    for mapping in MINTABLE_TOKEN_POSITIONS.range(deps.storage, None, None, Order::Ascending) {
+        let (position, token_id) = mapping?;
+        positions.push(position);
+        token_ids.push(token_id);
+    }
+    let randomized_token_ids = random_token_list(&env, info.sender.clone(), token_ids.clone())?;
+    for (i, position) in positions.iter().enumerate() {
+        MINTABLE_TOKEN_POSITIONS.save(deps.storage, *position, &randomized_token_ids[i])?;
+    }
+
+    Ok(Response::default()
+        .add_attribute("action", "shuffle")
+        .add_attribute("sender", info.sender)
+        .add_messages(fee_msgs))
+}
+
 pub fn execute_withdraw(
     deps: DepsMut,
     env: Env,
@@ -373,15 +421,19 @@ fn _execute_mint(
     recipient: Option<Addr>,
     token_id: Option<u32>,
 ) -> Result<Response, ContractError> {
-    let mintable = MINTABLE_TOKEN_IDS
-        .keys(deps.storage, None, None, Order::Ascending)
-        .next()
-        .is_some();
-    if !mintable {
+    let mintable_num_tokens = MINTABLE_NUM_TOKENS.load(deps.storage)?;
+    if mintable_num_tokens == 0 {
         return Err(ContractError::SoldOut {});
     }
 
     let config = CONFIG.load(deps.storage)?;
+
+    if let Some(token_id) = token_id {
+        if token_id == 0 || token_id > config.num_tokens {
+            return Err(ContractError::InvalidTokenId {});
+        }
+    }
+
     let sg721_address = SG721_ADDRESS.load(deps.storage)?;
 
     let recipient_addr = match recipient {
@@ -411,28 +463,35 @@ fn _execute_mint(
         network_fee
     };
 
-    let mintable_token_id = match token_id {
+    let mintable_token_mapping = match token_id {
         Some(token_id) => {
-            if token_id == 0 || token_id > config.num_tokens {
-                return Err(ContractError::InvalidTokenId {});
+            // set position to invalid value, iterate to find matching token_id
+            // if token_id not found, token_id is already sold, position is unchanged and throw err
+            // otherwise return position and token_id
+            let mut position = 0;
+            for res in MINTABLE_TOKEN_POSITIONS.range(deps.storage, None, None, Order::Ascending) {
+                let (pos, id) = res?;
+                if id == token_id {
+                    position = pos;
+                    break;
+                }
             }
-            // If token_id not on mintable map, throw err
-            if !MINTABLE_TOKEN_IDS.has(deps.storage, token_id) {
+            if position == 0 {
                 return Err(ContractError::TokenIdAlreadySold { token_id });
             }
-            token_id
-        }
-        None => {
-            let token_id: u32 = random_mintable_token_id(deps.as_ref(), env, info.sender.clone())?;
-            token_id
+            TokenPositionMapping { position, token_id }
         }
+        None => random_mintable_token_mapping(deps.as_ref(), env, info.sender.clone())?,
     };
 
     // Create mint msgs
     let mint_msg = Cw721ExecuteMsg::Mint(MintMsg::<Empty> {
-        token_id: mintable_token_id.to_string(),
+        token_id: mintable_token_mapping.token_id.to_string(),
         owner: recipient_addr.to_string(),
-        token_uri: Some(format!("{}/{}", config.base_token_uri, mintable_token_id)),
+        token_uri: Some(format!(
+            "{}/{}",
+            config.base_token_uri, mintable_token_mapping.token_id
+        )),
         extension: Empty {},
     });
     let msg = CosmosMsg::Wasm(WasmMsg::Execute {
@@ -442,8 +501,8 @@ fn _execute_mint(
     });
     msgs.append(&mut vec![msg]);
 
-    // Remove mintable token id from map
-    MINTABLE_TOKEN_IDS.remove(deps.storage, mintable_token_id);
+    // Remove mintable token position from map
+    MINTABLE_TOKEN_POSITIONS.remove(deps.storage, mintable_token_mapping.position);
     let mintable_num_tokens = MINTABLE_NUM_TOKENS.load(deps.storage)?;
     // Decrement mintable num tokens
     MINTABLE_NUM_TOKENS.save(deps.storage, &(mintable_num_tokens - 1))?;
@@ -455,15 +514,19 @@ fn _execute_mint(
         .add_attribute("action", action)
         .add_attribute("sender", info.sender)
         .add_attribute("recipient", recipient_addr)
-        .add_attribute("token_id", mintable_token_id.to_string())
+        .add_attribute("token_id", mintable_token_mapping.token_id.to_string())
         .add_attribute("network_fee", network_fee)
         .add_attribute("mint_price", mint_price.amount)
         .add_messages(msgs))
 }
 
-fn random_token_list(env: &Env, num_tokens: u32) -> Result<Vec<u32>, ContractError> {
-    let mut tokens: Vec<u32> = (1..=num_tokens).collect::<Vec<u32>>();
-    let sha256 = Sha256::digest(format!("{}{}", env.block.height, num_tokens).into_bytes());
+fn random_token_list(
+    env: &Env,
+    sender: Addr,
+    mut tokens: Vec<u32>,
+) -> Result<Vec<u32>, ContractError> {
+    let sha256 =
+        Sha256::digest(format!("{}{}{}", sender, env.block.height, tokens.len()).into_bytes());
     // Cut first 16 bytes from 32 byte value
     let randomness: [u8; 16] = sha256.to_vec()[0..16].try_into().unwrap();
     let mut rng = Xoshiro128PlusPlus::from_seed(randomness);
@@ -474,7 +537,12 @@ fn random_token_list(env: &Env, num_tokens: u32) -> Result<Vec<u32>, ContractErr
     Ok(tokens)
 }
 
-fn random_mintable_token_id(deps: Deps, env: Env, sender: Addr) -> Result<u32, ContractError> {
+// Does a baby shuffle, picking a token_id from the first or last 50 mintable positions.
+fn random_mintable_token_mapping(
+    deps: Deps,
+    env: Env,
+    sender: Addr,
+) -> Result<TokenPositionMapping, ContractError> {
     let num_tokens = MINTABLE_NUM_TOKENS.load(deps.storage)?;
     let sha256 =
         Sha256::digest(format!("{}{}{}", sender, num_tokens, env.block.height).into_bytes());
@@ -494,12 +562,14 @@ fn random_mintable_token_id(deps: Deps, env: Env, sender: Addr) -> Result<u32, C
         rem = num_tokens;
     }
     let n = r % rem;
-    let mintable_tokens = MINTABLE_TOKEN_IDS
+    let position = MINTABLE_TOKEN_POSITIONS
         .keys(deps.storage, None, None, order)
         .skip(n as usize)
         .take(1)
-        .collect::<StdResult<Vec<_>>>()?;
-    Ok(mintable_tokens[0])
+        .collect::<StdResult<Vec<_>>>()?[0];
+
+    let token_id = MINTABLE_TOKEN_POSITIONS.load(deps.storage, position)?;
+    Ok(TokenPositionMapping { position, token_id })
 }
 
 pub fn execute_update_start_time(
@@ -608,6 +678,8 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
         QueryMsg::MintableNumTokens {} => to_binary(&query_mintable_num_tokens(deps)?),
         QueryMsg::MintPrice {} => to_binary(&query_mint_price(deps)?),
         QueryMsg::MintCount { address } => to_binary(&query_mint_count(deps, address)?),
+        // TODO for debug to test shuffle. remove before prod
+        QueryMsg::MintableTokens {} => to_binary(&query_mintable_tokens(deps)?),
     }
 }
 
@@ -649,6 +721,18 @@ fn query_mintable_num_tokens(deps: Deps) -> StdResult<MintableNumTokensResponse>
     Ok(MintableNumTokensResponse { count })
 }
 
+//TODO for debug to test shuffle. remove before prod
+fn query_mintable_tokens(deps: Deps) -> StdResult<MintableTokensResponse> {
+    let tokens = MINTABLE_TOKEN_POSITIONS
+        .range(deps.storage, None, None, Order::Ascending)
+        .map(|t| t.unwrap())
+        .collect::<Vec<_>>();
+
+    Ok(MintableTokensResponse {
+        mintable_tokens: tokens,
+    })
+}
+
 fn query_mint_price(deps: Deps) -> StdResult<MintPriceResponse> {
     let config = CONFIG.load(deps.storage)?;
     let current_price = mint_price(deps, false)?;
diff --git a/contracts/minter/src/msg.rs b/contracts/minter/src/msg.rs
index b85c4ef4..513c9da7 100644
--- a/contracts/minter/src/msg.rs
+++ b/contracts/minter/src/msg.rs
@@ -25,6 +25,7 @@ pub enum ExecuteMsg {
     UpdatePerAddressLimit { per_address_limit: u32 },
     MintTo { recipient: String },
     MintFor { token_id: u32, recipient: String },
+    Shuffle {},
     Withdraw {},
 }
 
@@ -36,6 +37,7 @@ pub enum QueryMsg {
     StartTime {},
     MintPrice {},
     MintCount { address: String },
+    MintableTokens {},
 }
 
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
@@ -73,3 +75,9 @@ pub struct MintCountResponse {
     pub address: String,
     pub count: u32,
 }
+
+//TODO for debug to test shuffle. remove before prod
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct MintableTokensResponse {
+    pub mintable_tokens: Vec<(u32, u32)>,
+}
diff --git a/contracts/minter/src/state.rs b/contracts/minter/src/state.rs
index d957d083..0e64f4c8 100644
--- a/contracts/minter/src/state.rs
+++ b/contracts/minter/src/state.rs
@@ -18,6 +18,7 @@ pub struct Config {
 
 pub const CONFIG: Item<Config> = Item::new("config");
 pub const SG721_ADDRESS: Item<Addr> = Item::new("sg721_address");
-pub const MINTABLE_TOKEN_IDS: Map<u32, bool> = Map::new("mt");
+// map of index position and token id
+pub const MINTABLE_TOKEN_POSITIONS: Map<u32, u32> = Map::new("mt");
 pub const MINTABLE_NUM_TOKENS: Item<u32> = Item::new("mintable_num_tokens");
 pub const MINTER_ADDRS: Map<Addr, u32> = Map::new("ma");
diff --git a/packages/sg-utils/src/bech32_convert.rs b/packages/sg-utils/src/bech32_convert.rs
index 1f729efe..039e3a74 100644
--- a/packages/sg-utils/src/bech32_convert.rs
+++ b/packages/sg-utils/src/bech32_convert.rs
@@ -23,7 +23,7 @@ impl ToStars for str {
 
 impl ToStars for Addr {
     fn to_stars(&self) -> Result<String, Error> {
-        let decoded = decode_and_convert(&self.to_string())?;
+        let decoded = decode_and_convert(self.as_ref())?;
         convert_and_encode(PREFIX.to_string(), decoded.1)
     }
 }


Test patch:
diff --git a/contracts/minter/src/contract_tests.rs b/contracts/minter/src/contract_tests.rs
index 449bd45c..e60a865e 100644
--- a/contracts/minter/src/contract_tests.rs
+++ b/contracts/minter/src/contract_tests.rs
@@ -14,7 +14,7 @@ use whitelist::msg::{AddMembersMsg, ExecuteMsg as WhitelistExecuteMsg};
 use crate::contract::instantiate;
 use crate::msg::{
     ConfigResponse, ExecuteMsg, InstantiateMsg, MintCountResponse, MintPriceResponse,
-    MintableNumTokensResponse, QueryMsg, StartTimeResponse,
+    MintableNumTokensResponse, MintableTokensResponse, QueryMsg, StartTimeResponse,
 };
 use crate::ContractError;
 
@@ -649,11 +649,24 @@ fn mint_count_query() {
     assert_eq!(res.count, 2);
     assert_eq!(res.address, buyer.to_string());
 
+    // get random mint token_id
+    let tokens_msg = Cw721QueryMsg::Tokens {
+        owner: buyer.to_string(),
+        start_after: None,
+        limit: None,
+    };
+    let res: TokensResponse = router
+        .wrap()
+        .query_wasm_smart(sg721_addr.clone(), &tokens_msg)
+        .unwrap();
+    let sold_token_id: u32 = res.tokens[1].parse::<u32>().unwrap();
+    println!("sold token id: {}", sold_token_id);
+
     // Buyer transfers NFT to creator
-    // random mint token id: 3
+    // random mint token id: 8
     let transfer_msg: Cw721ExecuteMsg<Empty> = Cw721ExecuteMsg::TransferNft {
         recipient: creator.to_string(),
-        token_id: "3".to_string(),
+        token_id: "8".to_string(),
     };
     let res = router.execute_contract(
         buyer.clone(),
@@ -1117,7 +1130,7 @@ fn mint_for_token_id_addr() {
     };
     let res: TokensResponse = router
         .wrap()
-        .query_wasm_smart(config.sg721_address, &tokens_msg)
+        .query_wasm_smart(config.sg721_address.clone(), &tokens_msg)
         .unwrap();
     let sold_token_id: u32 = res.tokens[0].parse::<u32>().unwrap();
 
@@ -1198,6 +1211,18 @@ fn mint_for_token_id_addr() {
     );
     assert!(res.is_ok());
 
+    let res: OwnerOfResponse = router
+        .wrap()
+        .query_wasm_smart(
+            config.sg721_address,
+            &Cw721QueryMsg::OwnerOf {
+                token_id: 2.to_string(),
+                include_expired: None,
+            },
+        )
+        .unwrap();
+    assert_eq!(res.owner, buyer.to_string());
+
     let mintable_num_tokens_response: MintableNumTokensResponse = router
         .wrap()
         .query_wasm_smart(minter_addr, &QueryMsg::MintableNumTokens {})
@@ -1504,3 +1529,94 @@ fn can_withdraw() {
         coins(INITIAL_BALANCE + UNIT_PRICE - MINT_FEE, NATIVE_DENOM)
     );
 }
+
+//TODO for debug to test shuffle. remove before prod
+#[test]
+fn shuffle() {
+    const SHUFFLE_FEE: u128 = 500_000_000;
+    // setup accounts
+    let mut router = custom_mock_app();
+    let (creator, buyer) = setup_accounts(&mut router);
+    // setup contracts
+    let num_tokens = 20;
+    let (minter_addr, _config) = setup_minter_contract(&mut router, &creator, num_tokens);
+    // query mintable order for mints
+    let query_mintable_tokens_msg = QueryMsg::MintableTokens {};
+    let res: MintableTokensResponse = router
+        .wrap()
+        .query_wasm_smart(minter_addr.clone(), &query_mintable_tokens_msg)
+        .unwrap();
+    dbg!("after initialize {:?}", res);
+    // perform shuffle
+    let shuffle_msg = ExecuteMsg::Shuffle {};
+    let funds = coins(SHUFFLE_FEE, NATIVE_DENOM);
+    let res = router.execute_contract(creator.clone(), minter_addr.clone(), &shuffle_msg, &funds);
+    assert!(res.is_ok());
+    // query and compare mintable order for mints
+    let query_mintable_tokens_msg = QueryMsg::MintableTokens {};
+    let res: MintableTokensResponse = router
+        .wrap()
+        .query_wasm_smart(minter_addr.clone(), &query_mintable_tokens_msg)
+        .unwrap();
+    dbg!("after shuffle {:?}", res);
+    // mint a few tokens
+    let mut i = 0;
+    while i < 3 {
+        let mint_to_msg = ExecuteMsg::MintTo {
+            recipient: buyer.to_string(),
+        };
+        let res = router.execute_contract(
+            creator.clone(),
+            minter_addr.clone(),
+            &mint_to_msg,
+            &coins_for_msg(Coin {
+                amount: Uint128::from(ADMIN_MINT_PRICE),
+                denom: NATIVE_DENOM.to_string(),
+            }),
+        );
+        assert!(res.is_ok());
+        i += 1;
+    }
+    println!("---after mint_to---");
+    // query mintable order for mints
+    let query_mintable_tokens_msg = QueryMsg::MintableTokens {};
+    let res: MintableTokensResponse = router
+        .wrap()
+        .query_wasm_smart(minter_addr.clone(), &query_mintable_tokens_msg)
+        .unwrap();
+    dbg!("{:?}", res);
+    // perform shuffle
+    let shuffle_msg = ExecuteMsg::Shuffle {};
+    let funds = coins(SHUFFLE_FEE, NATIVE_DENOM);
+    let res = router.execute_contract(creator.clone(), minter_addr.clone(), &shuffle_msg, &funds);
+    assert!(res.is_ok());
+    // query and compare mintable order for mints
+    let query_mintable_tokens_msg = QueryMsg::MintableTokens {};
+    let res: MintableTokensResponse = router
+        .wrap()
+        .query_wasm_smart(minter_addr.clone(), &query_mintable_tokens_msg)
+        .unwrap();
+    dbg!("after another shuffle {:?}", res);
+    // mint until sold out
+    while i < num_tokens {
+        let mint_to_msg = ExecuteMsg::MintTo {
+            recipient: buyer.to_string(),
+        };
+        let res = router.execute_contract(
+            creator.clone(),
+            minter_addr.clone(),
+            &mint_to_msg,
+            &coins_for_msg(Coin {
+                amount: Uint128::from(ADMIN_MINT_PRICE),
+                denom: NATIVE_DENOM.to_string(),
+            }),
+        );
+        assert!(res.is_ok());
+        i += 1;
+    }
+    // try shuffle
+    let shuffle_msg = ExecuteMsg::Shuffle {};
+    let funds = coins(SHUFFLE_FEE, NATIVE_DENOM);
+    let res = router.execute_contract(creator.clone(), minter_addr, &shuffle_msg, &funds);
+    assert!(res.is_err());
+}