diff --git a/contracts/factories/base-factory/src/contract.rs b/contracts/factories/base-factory/src/contract.rs index 3b1b34138..2d6b4430f 100644 --- a/contracts/factories/base-factory/src/contract.rs +++ b/contracts/factories/base-factory/src/contract.rs @@ -9,7 +9,7 @@ use cw_utils::must_pay; use semver::Version; use sg1::{checked_fair_burn, transfer_funds_to_launchpad_dao}; use sg2::msg::UpdateMinterParamsMsg; -use sg2::query::{AllowedCollectionCodeIdResponse, AllowedCollectionCodeIdsResponse, Sg2QueryMsg}; +use sg2::query::{AllowedCollectionCodeIdResponse, AllowedCollectionCodeIdsResponse, IsContractWhitelistedResponse, Sg2QueryMsg, WhitelistedContractsResponse}; use sg2::MinterParams; use sg_utils::NATIVE_DENOM; @@ -203,6 +203,12 @@ pub fn query(deps: Deps, _env: Env, msg: Sg2QueryMsg) -> StdResult { Sg2QueryMsg::AllowedCollectionCodeId(code_id) => { to_json_binary(&query_allowed_collection_code_id(deps, code_id)?) } + Sg2QueryMsg::IsContractWhitelisted { address } => { + to_json_binary(&query_is_contract_whitelisted_base(deps, address)?) + } + Sg2QueryMsg::WhitelistedContracts { start_after: _, limit: _ } => { + to_json_binary(&query_whitelisted_contracts_base(deps)?) + } } } @@ -227,6 +233,26 @@ fn query_allowed_collection_code_id( Ok(AllowedCollectionCodeIdResponse { allowed }) } +fn query_is_contract_whitelisted_base( + _deps: Deps, + address: String, +) -> StdResult { + // Base factory doesn't have whitelist functionality, so all contracts are considered non-whitelisted + Ok(IsContractWhitelistedResponse { + address, + is_whitelisted: false, + }) +} + +fn query_whitelisted_contracts_base( + _deps: Deps, +) -> StdResult { + // Base factory doesn't have whitelist functionality, so return empty list + Ok(WhitelistedContractsResponse { + contracts: vec![], + }) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate( deps: DepsMut, diff --git a/contracts/factories/open-edition-factory/src/contract.rs b/contracts/factories/open-edition-factory/src/contract.rs index 000e30c57..f571f53bb 100644 --- a/contracts/factories/open-edition-factory/src/contract.rs +++ b/contracts/factories/open-edition-factory/src/contract.rs @@ -13,7 +13,7 @@ use base_factory::contract::{ }; use base_factory::ContractError as BaseContractError; use sg1::{checked_fair_burn, transfer_funds_to_launchpad_dao}; -use sg2::query::{AllowedCollectionCodeIdResponse, AllowedCollectionCodeIdsResponse, Sg2QueryMsg}; +use sg2::query::{AllowedCollectionCodeIdResponse, AllowedCollectionCodeIdsResponse, IsContractWhitelistedResponse, Sg2QueryMsg, WhitelistedContractsResponse}; use crate::error::ContractError; use crate::msg::{ @@ -190,6 +190,12 @@ pub fn query(deps: Deps, _env: Env, msg: Sg2QueryMsg) -> StdResult { Sg2QueryMsg::AllowedCollectionCodeId(code_id) => { to_json_binary(&query_allowed_collection_code_id(deps, code_id)?) } + Sg2QueryMsg::IsContractWhitelisted { address } => { + to_json_binary(&query_is_contract_whitelisted_open_edition(deps, address)?) + } + Sg2QueryMsg::WhitelistedContracts { start_after: _, limit: _ } => { + to_json_binary(&query_whitelisted_contracts_open_edition(deps)?) + } } } @@ -214,6 +220,26 @@ fn query_allowed_collection_code_id( Ok(AllowedCollectionCodeIdResponse { allowed }) } +fn query_is_contract_whitelisted_open_edition( + _deps: Deps, + address: String, +) -> StdResult { + // Open edition factory doesn't have whitelist functionality, so all contracts are considered non-whitelisted + Ok(IsContractWhitelistedResponse { + address, + is_whitelisted: false, + }) +} + +fn query_whitelisted_contracts_open_edition( + _deps: Deps, +) -> StdResult { + // Open edition factory doesn't have whitelist functionality, so return empty list + Ok(WhitelistedContractsResponse { + contracts: vec![], + }) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate( deps: DepsMut, diff --git a/contracts/factories/vending-factory/src/contract.rs b/contracts/factories/vending-factory/src/contract.rs index 798a70c82..d54446f2f 100644 --- a/contracts/factories/vending-factory/src/contract.rs +++ b/contracts/factories/vending-factory/src/contract.rs @@ -10,15 +10,15 @@ use cw2::set_contract_version; use cw_utils::must_pay; use semver::Version; use sg1::{checked_fair_burn, transfer_funds_to_launchpad_dao}; -use sg2::query::{AllowedCollectionCodeIdResponse, AllowedCollectionCodeIdsResponse, Sg2QueryMsg}; +use sg2::query::{AllowedCollectionCodeIdResponse, AllowedCollectionCodeIdsResponse, IsContractWhitelistedResponse, Sg2QueryMsg, WhitelistedContractsResponse}; use sg_utils::NATIVE_DENOM; use crate::error::ContractError; use crate::msg::{ - ExecuteMsg, InstantiateMsg, ParamsResponse, SudoMsg, VendingMinterCreateMsg, - VendingUpdateParamsMsg, + ExecuteMsg, InstantiateMsg, MigrateMsg, ParamsResponse, SudoMsg, + VendingMinterCreateMsg, VendingUpdateParamsMsg, WhitelistUpdate, }; -use crate::state::SUDO_PARAMS; +use crate::state::{SUDO_PARAMS, WHITELISTED_CONTRACTS}; // version info for migration info const CONTRACT_NAME: &str = "crates.io:vending-factory"; @@ -36,7 +36,18 @@ pub fn instantiate( SUDO_PARAMS.save(deps.storage, &msg.params)?; - Ok(Response::new()) + // Initialize whitelist if provided + if let Some(initial_whitelist) = msg.initial_whitelist { + for address_str in initial_whitelist { + let addr = deps.api.addr_validate(&address_str)?; + WHITELISTED_CONTRACTS.save(deps.storage, &addr, &true)?; + } + } + + Ok(Response::new() + .add_attribute("action", "instantiate") + .add_attribute("contract_name", CONTRACT_NAME) + .add_attribute("contract_version", CONTRACT_VERSION)) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -129,6 +140,15 @@ pub fn execute_create_minter( pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> Result { match msg { SudoMsg::UpdateParams(params_msg) => sudo_update_params(deps, env, *params_msg), + SudoMsg::AddContractToWhitelist { address } => { + sudo_add_contract_to_whitelist(deps, env, address) + } + SudoMsg::RemoveContractFromWhitelist { address } => { + sudo_remove_contract_from_whitelist(deps, env, address) + } + SudoMsg::UpdateContractWhitelist { add, remove } => { + sudo_update_contract_whitelist(deps, env, add, remove) + } } } @@ -189,6 +209,12 @@ pub fn query(deps: Deps, _env: Env, msg: Sg2QueryMsg) -> StdResult { Sg2QueryMsg::AllowedCollectionCodeId(code_id) => { to_json_binary(&query_allowed_collection_code_id(deps, code_id)?) } + Sg2QueryMsg::IsContractWhitelisted { address } => { + to_json_binary(&query_is_contract_whitelisted(deps, address)?) + } + Sg2QueryMsg::WhitelistedContracts { start_after, limit } => { + to_json_binary(&query_whitelisted_contracts(deps, start_after, limit)?) + } } } @@ -213,11 +239,100 @@ fn query_allowed_collection_code_id( Ok(AllowedCollectionCodeIdResponse { allowed }) } +pub fn sudo_add_contract_to_whitelist( + deps: DepsMut, + _env: Env, + address: String, +) -> Result { + let addr = deps.api.addr_validate(&address)?; + WHITELISTED_CONTRACTS.save(deps.storage, &addr, &true)?; + + Ok(Response::new() + .add_attribute("action", "add_contract_to_whitelist") + .add_attribute("contract_address", address)) +} + +pub fn sudo_remove_contract_from_whitelist( + deps: DepsMut, + _env: Env, + address: String, +) -> Result { + let addr = deps.api.addr_validate(&address)?; + WHITELISTED_CONTRACTS.remove(deps.storage, &addr); + + Ok(Response::new() + .add_attribute("action", "remove_contract_from_whitelist") + .add_attribute("contract_address", address)) +} + +pub fn sudo_update_contract_whitelist( + deps: DepsMut, + _env: Env, + add: Vec, + remove: Vec, +) -> Result { + let mut response = Response::new().add_attribute("action", "update_contract_whitelist"); + + // Add contracts to whitelist + for address in add { + let addr = deps.api.addr_validate(&address)?; + WHITELISTED_CONTRACTS.save(deps.storage, &addr, &true)?; + response = response.add_attribute("added", address); + } + + // Remove contracts from whitelist + for address in remove { + let addr = deps.api.addr_validate(&address)?; + WHITELISTED_CONTRACTS.remove(deps.storage, &addr); + response = response.add_attribute("removed", address); + } + + Ok(response) +} + +fn query_is_contract_whitelisted( + deps: Deps, + address: String, +) -> StdResult { + let addr = deps.api.addr_validate(&address)?; + let is_whitelisted = WHITELISTED_CONTRACTS + .may_load(deps.storage, &addr)? + .unwrap_or(false); + + Ok(IsContractWhitelistedResponse { + address, + is_whitelisted, + }) +} + +fn query_whitelisted_contracts( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit.unwrap_or(30).min(100) as usize; + let start = start_after + .map(|s| deps.api.addr_validate(&s)) + .transpose()?; + let start_bound = start.as_ref().map(cw_storage_plus::Bound::exclusive); + + let contracts: Vec = WHITELISTED_CONTRACTS + .range(deps.storage, start_bound, None, cosmwasm_std::Order::Ascending) + .take(limit) + .map(|item| { + let (addr, _) = item?; + Ok(addr.to_string()) + }) + .collect::>>()?; + + Ok(WhitelistedContractsResponse { contracts }) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate( deps: DepsMut, _env: Env, - msg: Option, + msg: MigrateMsg, ) -> Result { let prev_contract_info = cw2::get_contract_version(deps.storage)?; let prev_contract_name: String = prev_contract_info.contract; @@ -238,44 +353,37 @@ pub fn migrate( return Err(StdError::generic_err("Cannot migrate to a previous contract version").into()); } - if let Some(msg) = msg { - let mut params = SUDO_PARAMS.load(deps.storage)?; - - update_params(&mut params, msg.clone())?; - - params.extension.max_token_limit = msg - .extension - .max_token_limit - .unwrap_or(params.extension.max_token_limit); - params.extension.max_per_address_limit = msg - .extension - .max_per_address_limit - .unwrap_or(params.extension.max_per_address_limit); - - if let Some(airdrop_mint_price) = msg.extension.airdrop_mint_price { - ensure_eq!( - &airdrop_mint_price.denom, - &NATIVE_DENOM, - ContractError::BaseError(BaseContractError::InvalidDenom {}) - ); - params.extension.airdrop_mint_price = airdrop_mint_price; - } + // Set new contract version + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - params.extension.airdrop_mint_fee_bps = msg - .extension - .airdrop_mint_fee_bps - .unwrap_or(params.extension.airdrop_mint_fee_bps); - - if let Some(shuffle_fee) = msg.extension.shuffle_fee { - ensure_eq!( - &shuffle_fee.denom, - &NATIVE_DENOM, - ContractError::BaseError(BaseContractError::InvalidDenom {}) - ); - params.extension.shuffle_fee = shuffle_fee; - } + let mut response = Response::new().add_attribute("action", "migrate"); + + // Handle whitelist updates during migration + if let Some(whitelist_update) = msg.whitelist_update { + response = apply_whitelist_update(deps, whitelist_update, response)?; + } + + Ok(response) +} + +fn apply_whitelist_update( + deps: DepsMut, + whitelist_update: WhitelistUpdate, + mut response: Response, +) -> Result { + // Add contracts to whitelist + for address_str in whitelist_update.add { + let addr = deps.api.addr_validate(&address_str)?; + WHITELISTED_CONTRACTS.save(deps.storage, &addr, &true)?; + response = response.add_attribute("whitelist_added", address_str); + } - SUDO_PARAMS.save(deps.storage, ¶ms)?; + // Remove contracts from whitelist + for address_str in whitelist_update.remove { + let addr = deps.api.addr_validate(&address_str)?; + WHITELISTED_CONTRACTS.remove(deps.storage, &addr); + response = response.add_attribute("whitelist_removed", address_str); } - Ok(Response::new().add_attribute("action", "migrate")) + + Ok(response) } diff --git a/contracts/factories/vending-factory/src/msg.rs b/contracts/factories/vending-factory/src/msg.rs index 60c901426..cabbe488a 100644 --- a/contracts/factories/vending-factory/src/msg.rs +++ b/contracts/factories/vending-factory/src/msg.rs @@ -1,12 +1,14 @@ -use cosmwasm_schema::cw_serde; +use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Coin, Timestamp}; -use sg2::msg::{CreateMinterMsg, Sg2ExecuteMsg, UpdateMinterParamsMsg}; +use sg2::msg::{CreateMinterMsg, UpdateMinterParamsMsg}; use crate::state::VendingMinterParams; #[cw_serde] pub struct InstantiateMsg { pub params: VendingMinterParams, + /// Optional initial whitelist of contract addresses allowed to mint + pub initial_whitelist: Option>, } #[cw_serde] @@ -21,11 +23,24 @@ pub struct VendingMinterInitMsgExtension { } pub type VendingMinterCreateMsg = CreateMinterMsg; -pub type ExecuteMsg = Sg2ExecuteMsg; +#[cw_serde] +pub enum ExecuteMsg { + CreateMinter(VendingMinterCreateMsg), +} #[cw_serde] pub enum SudoMsg { UpdateParams(Box), + AddContractToWhitelist { + address: String, + }, + RemoveContractFromWhitelist { + address: String, + }, + UpdateContractWhitelist { + add: Vec, + remove: Vec, + }, } /// Message for params so they can be updated individually by governance @@ -39,7 +54,46 @@ pub struct VendingUpdateParamsExtension { } pub type VendingUpdateParamsMsg = UpdateMinterParamsMsg; +#[cw_serde] +pub struct MigrateMsg { + /// Optional whitelist operations during migration + pub whitelist_update: Option, +} + +#[cw_serde] +pub struct WhitelistUpdate { + /// Contract addresses to add to whitelist + pub add: Vec, + /// Contract addresses to remove from whitelist + pub remove: Vec, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(ParamsResponse)] + Params {}, + #[returns(IsContractWhitelistedResponse)] + IsContractWhitelisted { address: String }, + #[returns(WhitelistedContractsResponse)] + WhitelistedContracts { + start_after: Option, + limit: Option, + }, +} + #[cw_serde] pub struct ParamsResponse { pub params: VendingMinterParams, } + +#[cw_serde] +pub struct IsContractWhitelistedResponse { + pub address: String, + pub is_whitelisted: bool, +} + +#[cw_serde] +pub struct WhitelistedContractsResponse { + pub contracts: Vec, +} diff --git a/contracts/factories/vending-factory/src/state.rs b/contracts/factories/vending-factory/src/state.rs index a1c7fc72b..d2cda4744 100644 --- a/contracts/factories/vending-factory/src/state.rs +++ b/contracts/factories/vending-factory/src/state.rs @@ -1,6 +1,6 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::Coin; -use cw_storage_plus::Item; +use cosmwasm_std::{Addr, Coin}; +use cw_storage_plus::{Item, Map}; use sg2::MinterParams; /// Parameters common to all vending minters, as determined by governance #[cw_serde] @@ -14,3 +14,6 @@ pub struct ParamsExtension { pub type VendingMinterParams = MinterParams; pub const SUDO_PARAMS: Item = Item::new("sudo-params"); + +/// Stores whitelisted contract addresses that are allowed to mint despite being contracts +pub const WHITELISTED_CONTRACTS: Map<&Addr, bool> = Map::new("whitelisted_contracts"); diff --git a/contracts/minters/vending-minter-featured/src/contract.rs b/contracts/minters/vending-minter-featured/src/contract.rs index dd43d8868..793ce9c8b 100644 --- a/contracts/minters/vending-minter-featured/src/contract.rs +++ b/contracts/minters/vending-minter-featured/src/contract.rs @@ -1,13 +1,14 @@ use crate::error::ContractError; use crate::msg::{ - ConfigResponse, ExecuteMsg, MintCountResponse, MintPriceResponse, MintableNumTokensResponse, - QueryMsg, StartTimeResponse, + ConfigResponse, ExecuteMsg, MintCountResponse, + MintPriceResponse, MintableNumTokensResponse, QueryMsg, StartTimeResponse, }; use crate::state::{ Config, ConfigExtension, AIRDROP_COUNT, CONFIG, LAST_DISCOUNT_TIME, MINTABLE_NUM_TOKENS, - MINTABLE_TOKEN_POSITIONS, MINTER_ADDRS, SG721_ADDRESS, STATUS, WHITELIST_FS_MINTER_ADDRS, - WHITELIST_FS_MINT_COUNT, WHITELIST_MINTER_ADDRS, WHITELIST_SS_MINTER_ADDRS, - WHITELIST_SS_MINT_COUNT, WHITELIST_TS_MINTER_ADDRS, WHITELIST_TS_MINT_COUNT, + MINTABLE_TOKEN_POSITIONS, MINTER_ADDRS, SG721_ADDRESS, STATUS, + WHITELIST_FS_MINTER_ADDRS, WHITELIST_FS_MINT_COUNT, WHITELIST_MINTER_ADDRS, + WHITELIST_SS_MINTER_ADDRS, WHITELIST_SS_MINT_COUNT, WHITELIST_TS_MINTER_ADDRS, + WHITELIST_TS_MINT_COUNT, }; use crate::validation::{check_dynamic_per_address_limit, get_three_percent_of_tokens}; #[cfg(not(feature = "library"))] @@ -50,6 +51,47 @@ const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); const INSTANTIATE_SG721_REPLY_ID: u64 = 1; +fn is_contract(deps: Deps, addr: &Addr) -> Result { + // First check: query factory to see if address is whitelisted + let config = CONFIG.load(deps.storage)?; + let factory_query = vending_factory::msg::QueryMsg::IsContractWhitelisted { + address: addr.to_string(), + }; + + let factory_response: Result = deps + .querier + .query_wasm_smart(config.factory, &factory_query); + + // If factory query succeeds and address is whitelisted, treat as EOA + if let Ok(response) = factory_response { + if response.is_whitelisted { + return Ok(false); // Whitelisted contracts are treated as EOAs + } + } + + // Second check by address length - contract addresses are typically longer (63+ chars) + // EOA addresses are usually shorter (20-44 chars depending on format) + if addr.as_str().len() > 50 { + return Ok(true); + } + + // Third check: try to query contract info directly + // This catches contracts that might have shorter addresses + use cosmwasm_std::{ContractInfoResponse, QueryRequest, WasmQuery}; + + let contract_info_query = QueryRequest::Wasm(WasmQuery::ContractInfo { + contract_addr: addr.to_string(), + }); + + let contract_info_result: Result = + deps.querier.query(&contract_info_query); + + match contract_info_result { + Ok(_) => Ok(true), // Address is a contract + Err(_) => Ok(false), // Not a contract (treat as EOA) + } +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, @@ -508,6 +550,11 @@ pub fn execute_mint_sender( return Err(ContractError::BeforeMintStartTime {}); } + // Check if sender is a contract (only for public and whitelist mints, not admin mints) + if is_contract(deps.as_ref(), &info.sender)? { + return Err(ContractError::ContractsCannotMint {}); + } + // Check if already minted max per address limit let mint_count = mint_count(deps.as_ref(), &info)?; if is_public && mint_count >= config.extension.per_address_limit { @@ -1380,6 +1427,7 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result Result { let current_version = cw2::get_contract_version(deps.storage)?; diff --git a/contracts/minters/vending-minter-featured/src/error.rs b/contracts/minters/vending-minter-featured/src/error.rs index 5b67d20ec..8551bf8a1 100644 --- a/contracts/minters/vending-minter-featured/src/error.rs +++ b/contracts/minters/vending-minter-featured/src/error.rs @@ -115,4 +115,7 @@ pub enum ContractError { #[error("Multiply Fraction Error")] CheckedMultiplyFractionError {}, + + #[error("Contracts cannot mint")] + ContractsCannotMint {}, } diff --git a/contracts/minters/vending-minter-featured/src/msg.rs b/contracts/minters/vending-minter-featured/src/msg.rs index d4d9f0358..b6019fbb4 100644 --- a/contracts/minters/vending-minter-featured/src/msg.rs +++ b/contracts/minters/vending-minter-featured/src/msg.rs @@ -88,3 +88,4 @@ pub struct MintCountResponse { pub address: String, pub count: u32, } + 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 3fca11af6..ec4880733 100644 --- a/contracts/minters/vending-minter-merkle-wl-featured/src/contract.rs +++ b/contracts/minters/vending-minter-merkle-wl-featured/src/contract.rs @@ -1,13 +1,14 @@ use crate::error::ContractError; use crate::msg::{ - ConfigResponse, ExecuteMsg, MintCountResponse, MintPriceResponse, MintableNumTokensResponse, - QueryMsg, StartTimeResponse, + ConfigResponse, ExecuteMsg, MintCountResponse, + MintPriceResponse, MintableNumTokensResponse, QueryMsg, StartTimeResponse, }; use crate::state::{ Config, ConfigExtension, AIRDROP_COUNT, CONFIG, LAST_DISCOUNT_TIME, MINTABLE_NUM_TOKENS, - MINTABLE_TOKEN_POSITIONS, MINTER_ADDRS, SG721_ADDRESS, STATUS, WHITELIST_FS_MINTER_ADDRS, - WHITELIST_FS_MINT_COUNT, WHITELIST_MINTER_ADDRS, WHITELIST_SS_MINTER_ADDRS, - WHITELIST_SS_MINT_COUNT, WHITELIST_TS_MINTER_ADDRS, WHITELIST_TS_MINT_COUNT, + MINTABLE_TOKEN_POSITIONS, MINTER_ADDRS, SG721_ADDRESS, STATUS, + WHITELIST_FS_MINTER_ADDRS, WHITELIST_FS_MINT_COUNT, WHITELIST_MINTER_ADDRS, + WHITELIST_SS_MINTER_ADDRS, WHITELIST_SS_MINT_COUNT, WHITELIST_TS_MINTER_ADDRS, + WHITELIST_TS_MINT_COUNT, }; use crate::validation::{check_dynamic_per_address_limit, get_three_percent_of_tokens}; #[cfg(not(feature = "library"))] @@ -50,6 +51,47 @@ const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); const INSTANTIATE_SG721_REPLY_ID: u64 = 1; +fn is_contract(deps: Deps, addr: &Addr) -> Result { + // First check: query factory to see if address is whitelisted + let config = CONFIG.load(deps.storage)?; + let factory_query = vending_factory::msg::QueryMsg::IsContractWhitelisted { + address: addr.to_string(), + }; + + let factory_response: Result = deps + .querier + .query_wasm_smart(config.factory, &factory_query); + + // If factory query succeeds and address is whitelisted, treat as EOA + if let Ok(response) = factory_response { + if response.is_whitelisted { + return Ok(false); // Whitelisted contracts are treated as EOAs + } + } + + // Second check by address length - contract addresses are typically longer (63+ chars) + // EOA addresses are usually shorter (20-44 chars depending on format) + if addr.as_str().len() > 50 { + return Ok(true); + } + + // Third check: try to query contract info directly + // This catches contracts that might have shorter addresses + use cosmwasm_std::{ContractInfoResponse, QueryRequest, WasmQuery}; + + let contract_info_query = QueryRequest::Wasm(WasmQuery::ContractInfo { + contract_addr: addr.to_string(), + }); + + let contract_info_result: Result = + deps.querier.query(&contract_info_query); + + match contract_info_result { + Ok(_) => Ok(true), // Address is a contract + Err(_) => Ok(false), // Not a contract (treat as EOA) + } +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, @@ -513,6 +555,11 @@ pub fn execute_mint_sender( return Err(ContractError::BeforeMintStartTime {}); } + // Check if sender is a contract (only for public and whitelist mints, not admin mints) + if is_contract(deps.as_ref(), &info.sender)? { + return Err(ContractError::ContractsCannotMint {}); + } + // Check if already minted max per address limit let mint_count = mint_count(deps.as_ref(), &info)?; @@ -1416,6 +1463,8 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result Result { let current_version = cw2::get_contract_version(deps.storage)?; diff --git a/contracts/minters/vending-minter-merkle-wl-featured/src/error.rs b/contracts/minters/vending-minter-merkle-wl-featured/src/error.rs index 5b67d20ec..8551bf8a1 100644 --- a/contracts/minters/vending-minter-merkle-wl-featured/src/error.rs +++ b/contracts/minters/vending-minter-merkle-wl-featured/src/error.rs @@ -115,4 +115,7 @@ pub enum ContractError { #[error("Multiply Fraction Error")] CheckedMultiplyFractionError {}, + + #[error("Contracts cannot mint")] + ContractsCannotMint {}, } 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 10f268ed1..1f2f1743b 100644 --- a/contracts/minters/vending-minter-merkle-wl-featured/src/msg.rs +++ b/contracts/minters/vending-minter-merkle-wl-featured/src/msg.rs @@ -92,3 +92,4 @@ pub struct MintCountResponse { pub address: String, pub count: u32, } + diff --git a/contracts/minters/vending-minter-merkle-wl-featured/src/state.rs b/contracts/minters/vending-minter-merkle-wl-featured/src/state.rs index 5abef9040..a08eeb7a6 100644 --- a/contracts/minters/vending-minter-merkle-wl-featured/src/state.rs +++ b/contracts/minters/vending-minter-merkle-wl-featured/src/state.rs @@ -36,3 +36,4 @@ pub const LAST_DISCOUNT_TIME: Item = Item::new("last_discount_time"); /// Holds the status of the minter. Can be changed with on-chain governance proposals. pub const STATUS: Item = Item::new("status"); + diff --git a/contracts/minters/vending-minter-merkle-wl/src/contract.rs b/contracts/minters/vending-minter-merkle-wl/src/contract.rs index a98898dfb..dcf885480 100644 --- a/contracts/minters/vending-minter-merkle-wl/src/contract.rs +++ b/contracts/minters/vending-minter-merkle-wl/src/contract.rs @@ -1,13 +1,14 @@ use crate::error::ContractError; use crate::msg::{ - ConfigResponse, ExecuteMsg, MintCountResponse, MintPriceResponse, MintableNumTokensResponse, - QueryMsg, StartTimeResponse, + ConfigResponse, ExecuteMsg, MintCountResponse, + MintPriceResponse, MintableNumTokensResponse, QueryMsg, StartTimeResponse, }; use crate::state::{ Config, ConfigExtension, AIRDROP_COUNT, CONFIG, LAST_DISCOUNT_TIME, MINTABLE_NUM_TOKENS, - MINTABLE_TOKEN_POSITIONS, MINTER_ADDRS, SG721_ADDRESS, STATUS, WHITELIST_FS_MINTER_ADDRS, - WHITELIST_FS_MINT_COUNT, WHITELIST_MINTER_ADDRS, WHITELIST_SS_MINTER_ADDRS, - WHITELIST_SS_MINT_COUNT, WHITELIST_TS_MINTER_ADDRS, WHITELIST_TS_MINT_COUNT, + MINTABLE_TOKEN_POSITIONS, MINTER_ADDRS, SG721_ADDRESS, STATUS, + WHITELIST_FS_MINTER_ADDRS, WHITELIST_FS_MINT_COUNT, WHITELIST_MINTER_ADDRS, + WHITELIST_SS_MINTER_ADDRS, WHITELIST_SS_MINT_COUNT, WHITELIST_TS_MINTER_ADDRS, + WHITELIST_TS_MINT_COUNT, }; use crate::validation::{check_dynamic_per_address_limit, get_three_percent_of_tokens}; #[cfg(not(feature = "library"))] @@ -49,6 +50,47 @@ const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); const INSTANTIATE_SG721_REPLY_ID: u64 = 1; +fn is_contract(deps: Deps, addr: &Addr) -> Result { + // First check: query factory to see if address is whitelisted + let config = CONFIG.load(deps.storage)?; + let factory_query = vending_factory::msg::QueryMsg::IsContractWhitelisted { + address: addr.to_string(), + }; + + let factory_response: Result = deps + .querier + .query_wasm_smart(config.factory, &factory_query); + + // If factory query succeeds and address is whitelisted, treat as EOA + if let Ok(response) = factory_response { + if response.is_whitelisted { + return Ok(false); // Whitelisted contracts are treated as EOAs + } + } + + // Second check by address length - contract addresses are typically longer (63+ chars) + // EOA addresses are usually shorter (20-44 chars depending on format) + if addr.as_str().len() > 50 { + return Ok(true); + } + + // Third check: try to query contract info directly + // This catches contracts that might have shorter addresses + use cosmwasm_std::{ContractInfoResponse, QueryRequest, WasmQuery}; + + let contract_info_query = QueryRequest::Wasm(WasmQuery::ContractInfo { + contract_addr: addr.to_string(), + }); + + let contract_info_result: Result = + deps.querier.query(&contract_info_query); + + match contract_info_result { + Ok(_) => Ok(true), // Address is a contract + Err(_) => Ok(false), // Not a contract (treat as EOA) + } +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, @@ -512,6 +554,11 @@ pub fn execute_mint_sender( return Err(ContractError::BeforeMintStartTime {}); } + // Check if sender is a contract (only for public and whitelist mints, not admin mints) + if is_contract(deps.as_ref(), &info.sender)? { + return Err(ContractError::ContractsCannotMint {}); + } + // Check if already minted max per address limit let mint_count = mint_count(deps.as_ref(), &info)?; @@ -1416,6 +1463,8 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result Result { let current_version = cw2::get_contract_version(deps.storage)?; diff --git a/contracts/minters/vending-minter-merkle-wl/src/error.rs b/contracts/minters/vending-minter-merkle-wl/src/error.rs index 5b67d20ec..8551bf8a1 100644 --- a/contracts/minters/vending-minter-merkle-wl/src/error.rs +++ b/contracts/minters/vending-minter-merkle-wl/src/error.rs @@ -115,4 +115,7 @@ pub enum ContractError { #[error("Multiply Fraction Error")] CheckedMultiplyFractionError {}, + + #[error("Contracts cannot mint")] + ContractsCannotMint {}, } diff --git a/contracts/minters/vending-minter-merkle-wl/src/msg.rs b/contracts/minters/vending-minter-merkle-wl/src/msg.rs index 10f268ed1..1f2f1743b 100644 --- a/contracts/minters/vending-minter-merkle-wl/src/msg.rs +++ b/contracts/minters/vending-minter-merkle-wl/src/msg.rs @@ -92,3 +92,4 @@ pub struct MintCountResponse { pub address: String, pub count: u32, } + diff --git a/contracts/minters/vending-minter-merkle-wl/src/state.rs b/contracts/minters/vending-minter-merkle-wl/src/state.rs index 5abef9040..a08eeb7a6 100644 --- a/contracts/minters/vending-minter-merkle-wl/src/state.rs +++ b/contracts/minters/vending-minter-merkle-wl/src/state.rs @@ -36,3 +36,4 @@ pub const LAST_DISCOUNT_TIME: Item = Item::new("last_discount_time"); /// Holds the status of the minter. Can be changed with on-chain governance proposals. pub const STATUS: Item = Item::new("status"); + diff --git a/contracts/minters/vending-minter-wl-flex-featured/src/contract.rs b/contracts/minters/vending-minter-wl-flex-featured/src/contract.rs index a041b3cfc..98330bbc1 100644 --- a/contracts/minters/vending-minter-wl-flex-featured/src/contract.rs +++ b/contracts/minters/vending-minter-wl-flex-featured/src/contract.rs @@ -1,13 +1,14 @@ use crate::error::ContractError; use crate::msg::{ - ConfigResponse, ExecuteMsg, MintCountResponse, MintPriceResponse, MintableNumTokensResponse, - QueryMsg, StartTimeResponse, + ConfigResponse, ExecuteMsg, MintCountResponse, + MintPriceResponse, MintableNumTokensResponse, QueryMsg, StartTimeResponse, }; use crate::state::{ Config, ConfigExtension, AIRDROP_COUNT, CONFIG, LAST_DISCOUNT_TIME, MINTABLE_NUM_TOKENS, - MINTABLE_TOKEN_POSITIONS, MINTER_ADDRS, SG721_ADDRESS, STATUS, WHITELIST_FS_MINTER_ADDRS, - WHITELIST_FS_MINT_COUNT, WHITELIST_MINTER_ADDRS, WHITELIST_SS_MINTER_ADDRS, - WHITELIST_SS_MINT_COUNT, WHITELIST_TS_MINTER_ADDRS, WHITELIST_TS_MINT_COUNT, + MINTABLE_TOKEN_POSITIONS, MINTER_ADDRS, SG721_ADDRESS, STATUS, + WHITELIST_FS_MINTER_ADDRS, WHITELIST_FS_MINT_COUNT, WHITELIST_MINTER_ADDRS, + WHITELIST_SS_MINTER_ADDRS, WHITELIST_SS_MINT_COUNT, WHITELIST_TS_MINTER_ADDRS, + WHITELIST_TS_MINT_COUNT, }; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; @@ -49,6 +50,47 @@ const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); const INSTANTIATE_SG721_REPLY_ID: u64 = 1; +fn is_contract(deps: Deps, addr: &Addr) -> Result { + // First check: query factory to see if address is whitelisted + let config = CONFIG.load(deps.storage)?; + let factory_query = vending_factory::msg::QueryMsg::IsContractWhitelisted { + address: addr.to_string(), + }; + + let factory_response: Result = deps + .querier + .query_wasm_smart(config.factory, &factory_query); + + // If factory query succeeds and address is whitelisted, treat as EOA + if let Ok(response) = factory_response { + if response.is_whitelisted { + return Ok(false); // Whitelisted contracts are treated as EOAs + } + } + + // Second check by address length - contract addresses are typically longer (63+ chars) + // EOA addresses are usually shorter (20-44 chars depending on format) + if addr.as_str().len() > 50 { + return Ok(true); + } + + // Third check: try to query contract info directly + // This catches contracts that might have shorter addresses + use cosmwasm_std::{ContractInfoResponse, QueryRequest, WasmQuery}; + + let contract_info_query = QueryRequest::Wasm(WasmQuery::ContractInfo { + contract_addr: addr.to_string(), + }); + + let contract_info_result: Result = + deps.querier.query(&contract_info_query); + + match contract_info_result { + Ok(_) => Ok(true), // Address is a contract + Err(_) => Ok(false), // Not a contract (treat as EOA) + } +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, @@ -488,6 +530,11 @@ pub fn execute_mint_sender( return Err(ContractError::BeforeMintStartTime {}); } + // Check if sender is a contract (only for public and whitelist mints, not admin mints) + if is_contract(deps.as_ref(), &info.sender)? { + return Err(ContractError::ContractsCannotMint {}); + } + // Check if already minted max per address limit let mint_count = public_mint_count(deps.as_ref(), &info)?; if is_public && mint_count >= config.extension.per_address_limit { @@ -1344,6 +1391,8 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result Result { let current_version = cw2::get_contract_version(deps.storage)?; diff --git a/contracts/minters/vending-minter-wl-flex-featured/src/error.rs b/contracts/minters/vending-minter-wl-flex-featured/src/error.rs index 5b67d20ec..8551bf8a1 100644 --- a/contracts/minters/vending-minter-wl-flex-featured/src/error.rs +++ b/contracts/minters/vending-minter-wl-flex-featured/src/error.rs @@ -115,4 +115,7 @@ pub enum ContractError { #[error("Multiply Fraction Error")] CheckedMultiplyFractionError {}, + + #[error("Contracts cannot mint")] + ContractsCannotMint {}, } diff --git a/contracts/minters/vending-minter-wl-flex-featured/src/msg.rs b/contracts/minters/vending-minter-wl-flex-featured/src/msg.rs index e824cb000..76e874490 100644 --- a/contracts/minters/vending-minter-wl-flex-featured/src/msg.rs +++ b/contracts/minters/vending-minter-wl-flex-featured/src/msg.rs @@ -97,3 +97,4 @@ pub struct MintCountResponse { pub count: u32, pub whitelist_count: u32, } + diff --git a/contracts/minters/vending-minter-wl-flex-featured/src/state.rs b/contracts/minters/vending-minter-wl-flex-featured/src/state.rs index 5abef9040..a08eeb7a6 100644 --- a/contracts/minters/vending-minter-wl-flex-featured/src/state.rs +++ b/contracts/minters/vending-minter-wl-flex-featured/src/state.rs @@ -36,3 +36,4 @@ pub const LAST_DISCOUNT_TIME: Item = Item::new("last_discount_time"); /// Holds the status of the minter. Can be changed with on-chain governance proposals. pub const STATUS: Item = Item::new("status"); + diff --git a/contracts/minters/vending-minter-wl-flex/src/contract.rs b/contracts/minters/vending-minter-wl-flex/src/contract.rs index 67dc50299..ab51fa303 100644 --- a/contracts/minters/vending-minter-wl-flex/src/contract.rs +++ b/contracts/minters/vending-minter-wl-flex/src/contract.rs @@ -1,13 +1,14 @@ use crate::error::ContractError; use crate::msg::{ - ConfigResponse, ExecuteMsg, MintCountResponse, MintPriceResponse, MintableNumTokensResponse, - QueryMsg, StartTimeResponse, + ConfigResponse, ExecuteMsg, MintCountResponse, + MintPriceResponse, MintableNumTokensResponse, QueryMsg, StartTimeResponse, }; use crate::state::{ Config, ConfigExtension, AIRDROP_COUNT, CONFIG, LAST_DISCOUNT_TIME, MINTABLE_NUM_TOKENS, - MINTABLE_TOKEN_POSITIONS, MINTER_ADDRS, SG721_ADDRESS, STATUS, WHITELIST_FS_MINTER_ADDRS, - WHITELIST_FS_MINT_COUNT, WHITELIST_MINTER_ADDRS, WHITELIST_SS_MINTER_ADDRS, - WHITELIST_SS_MINT_COUNT, WHITELIST_TS_MINTER_ADDRS, WHITELIST_TS_MINT_COUNT, + MINTABLE_TOKEN_POSITIONS, MINTER_ADDRS, SG721_ADDRESS, STATUS, + WHITELIST_FS_MINTER_ADDRS, WHITELIST_FS_MINT_COUNT, WHITELIST_MINTER_ADDRS, + WHITELIST_SS_MINTER_ADDRS, WHITELIST_SS_MINT_COUNT, WHITELIST_TS_MINTER_ADDRS, + WHITELIST_TS_MINT_COUNT, }; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; @@ -51,6 +52,47 @@ const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); const INSTANTIATE_SG721_REPLY_ID: u64 = 1; +fn is_contract(deps: Deps, addr: &Addr) -> Result { + // First check: query factory to see if address is whitelisted + let config = CONFIG.load(deps.storage)?; + let factory_query = vending_factory::msg::QueryMsg::IsContractWhitelisted { + address: addr.to_string(), + }; + + let factory_response: Result = deps + .querier + .query_wasm_smart(config.factory, &factory_query); + + // If factory query succeeds and address is whitelisted, treat as EOA + if let Ok(response) = factory_response { + if response.is_whitelisted { + return Ok(false); // Whitelisted contracts are treated as EOAs + } + } + + // Second check by address length - contract addresses are typically longer (63+ chars) + // EOA addresses are usually shorter (20-44 chars depending on format) + if addr.as_str().len() > 50 { + return Ok(true); + } + + // Third check: try to query contract info directly + // This catches contracts that might have shorter addresses + use cosmwasm_std::{ContractInfoResponse, QueryRequest, WasmQuery}; + + let contract_info_query = QueryRequest::Wasm(WasmQuery::ContractInfo { + contract_addr: addr.to_string(), + }); + + let contract_info_result: Result = + deps.querier.query(&contract_info_query); + + match contract_info_result { + Ok(_) => Ok(true), // Address is a contract + Err(_) => Ok(false), // Not a contract (treat as EOA) + } +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, @@ -489,6 +531,11 @@ pub fn execute_mint_sender( return Err(ContractError::BeforeMintStartTime {}); } + // Check if sender is a contract (only for public and whitelist mints, not admin mints) + if is_contract(deps.as_ref(), &info.sender)? { + return Err(ContractError::ContractsCannotMint {}); + } + // Check if already minted max per address limit let mint_count = public_mint_count(deps.as_ref(), &info)?; if is_public && mint_count >= config.extension.per_address_limit { @@ -1217,6 +1264,7 @@ pub fn update_status( Ok(Response::new().add_attribute("action", "sudo_update_status")) } + #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { @@ -1325,6 +1373,7 @@ fn query_mint_price(deps: Deps) -> StdResult { }) } + // Reply callback triggered from cw721 contract instantiation #[cfg_attr(not(feature = "library"), entry_point)] pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { diff --git a/contracts/minters/vending-minter-wl-flex/src/error.rs b/contracts/minters/vending-minter-wl-flex/src/error.rs index 5b67d20ec..8551bf8a1 100644 --- a/contracts/minters/vending-minter-wl-flex/src/error.rs +++ b/contracts/minters/vending-minter-wl-flex/src/error.rs @@ -115,4 +115,7 @@ pub enum ContractError { #[error("Multiply Fraction Error")] CheckedMultiplyFractionError {}, + + #[error("Contracts cannot mint")] + ContractsCannotMint {}, } diff --git a/contracts/minters/vending-minter-wl-flex/src/msg.rs b/contracts/minters/vending-minter-wl-flex/src/msg.rs index e824cb000..76e874490 100644 --- a/contracts/minters/vending-minter-wl-flex/src/msg.rs +++ b/contracts/minters/vending-minter-wl-flex/src/msg.rs @@ -97,3 +97,4 @@ pub struct MintCountResponse { pub count: u32, pub whitelist_count: u32, } + diff --git a/contracts/minters/vending-minter-wl-flex/src/state.rs b/contracts/minters/vending-minter-wl-flex/src/state.rs index 5abef9040..e0dccaee1 100644 --- a/contracts/minters/vending-minter-wl-flex/src/state.rs +++ b/contracts/minters/vending-minter-wl-flex/src/state.rs @@ -34,5 +34,6 @@ pub const WHITELIST_TS_MINT_COUNT: Item = Item::new("wltsmc"); pub const AIRDROP_COUNT: Item = Item::new("airdrop_count"); pub const LAST_DISCOUNT_TIME: Item = Item::new("last_discount_time"); + /// Holds the status of the minter. Can be changed with on-chain governance proposals. pub const STATUS: Item = Item::new("status"); diff --git a/contracts/minters/vending-minter/src/contract.rs b/contracts/minters/vending-minter/src/contract.rs index 548217b16..1d64fbb01 100644 --- a/contracts/minters/vending-minter/src/contract.rs +++ b/contracts/minters/vending-minter/src/contract.rs @@ -49,6 +49,47 @@ const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); const INSTANTIATE_SG721_REPLY_ID: u64 = 1; +fn is_contract(deps: Deps, addr: &Addr) -> Result { + // First check: query factory to see if address is whitelisted + let config = CONFIG.load(deps.storage)?; + let factory_query = vending_factory::msg::QueryMsg::IsContractWhitelisted { + address: addr.to_string(), + }; + + let factory_response: Result = deps + .querier + .query_wasm_smart(config.factory, &factory_query); + + // If factory query succeeds and address is whitelisted, treat as EOA + if let Ok(response) = factory_response { + if response.is_whitelisted { + return Ok(false); // Whitelisted contracts are treated as EOAs + } + } + + // Second check by address length - contract addresses are typically longer (63+ chars) + // EOA addresses are usually shorter (20-44 chars depending on format) + if addr.as_str().len() > 50 { + return Ok(true); + } + + // Third check: try to query contract info directly + // This catches contracts that might have shorter addresses + use cosmwasm_std::{ContractInfoResponse, QueryRequest, WasmQuery}; + + let contract_info_query = QueryRequest::Wasm(WasmQuery::ContractInfo { + contract_addr: addr.to_string(), + }); + + let contract_info_result: Result = + deps.querier.query(&contract_info_query); + + match contract_info_result { + Ok(_) => Ok(true), // Address is a contract + Err(_) => Ok(false), // Not a contract (treat as EOA) + } +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, @@ -505,6 +546,11 @@ pub fn execute_mint_sender( return Err(ContractError::BeforeMintStartTime {}); } + // Check if sender is a contract (only for public and whitelist mints, not admin mints) + if is_contract(deps.as_ref(), &info.sender)? { + return Err(ContractError::ContractsCannotMint {}); + } + // Check if already minted max per address limit let mint_count = mint_count(deps.as_ref(), &info)?; if is_public && mint_count >= config.extension.per_address_limit { @@ -1250,6 +1296,7 @@ pub fn update_status( Ok(Response::new().add_attribute("action", "sudo_update_status")) } + #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { @@ -1355,6 +1402,7 @@ fn query_mint_price(deps: Deps) -> StdResult { }) } + // Reply callback triggered from cw721 contract instantiation #[cfg_attr(not(feature = "library"), entry_point)] pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { diff --git a/contracts/minters/vending-minter/src/error.rs b/contracts/minters/vending-minter/src/error.rs index 3d700f260..33158d2b8 100644 --- a/contracts/minters/vending-minter/src/error.rs +++ b/contracts/minters/vending-minter/src/error.rs @@ -115,4 +115,7 @@ pub enum ContractError { #[error("Multiply Fraction Error")] CheckedMultiplyFractionError {}, + + #[error("Contracts cannot mint")] + ContractsCannotMint {}, } diff --git a/contracts/minters/vending-minter/src/msg.rs b/contracts/minters/vending-minter/src/msg.rs index d4d9f0358..7c110b300 100644 --- a/contracts/minters/vending-minter/src/msg.rs +++ b/contracts/minters/vending-minter/src/msg.rs @@ -45,7 +45,9 @@ pub enum QueryMsg { MintableNumTokens {}, StartTime {}, MintPrice {}, - MintCount { address: String }, + MintCount { + address: String, + }, Status {}, } @@ -88,3 +90,4 @@ pub struct MintCountResponse { pub address: String, pub count: u32, } + diff --git a/contracts/minters/vending-minter/src/state.rs b/contracts/minters/vending-minter/src/state.rs index 9d68587df..ad0a1410e 100644 --- a/contracts/minters/vending-minter/src/state.rs +++ b/contracts/minters/vending-minter/src/state.rs @@ -36,5 +36,6 @@ pub const AIRDROP_COUNT: Item = Item::new("airdrop_count"); pub const LAST_DISCOUNT_TIME: Item = Item::new("last_discount_time"); + /// Holds the status of the minter. Can be changed with on-chain governance proposals. pub const STATUS: Item = Item::new("status"); diff --git a/e2e/src/helpers/helper.rs b/e2e/src/helpers/helper.rs index 299d06a14..58c7948c9 100644 --- a/e2e/src/helpers/helper.rs +++ b/e2e/src/helpers/helper.rs @@ -64,6 +64,7 @@ pub fn instantiate_factory( }, }, }, + initial_whitelist: None, }, key, Some(creator_addr.parse().unwrap()), diff --git a/packages/sg2/src/query.rs b/packages/sg2/src/query.rs index f3fa08507..54838e2dd 100644 --- a/packages/sg2/src/query.rs +++ b/packages/sg2/src/query.rs @@ -9,6 +9,13 @@ pub enum Sg2QueryMsg { Params {}, AllowedCollectionCodeIds {}, AllowedCollectionCodeId(CodeId), + /// Returns `IsContractWhitelistedResponse` + IsContractWhitelisted { address: String }, + /// Returns `WhitelistedContractsResponse` + WhitelistedContracts { + start_after: Option, + limit: Option, + }, } #[cw_serde] @@ -25,3 +32,14 @@ pub struct AllowedCollectionCodeIdsResponse { pub struct AllowedCollectionCodeIdResponse { pub allowed: bool, } + +#[cw_serde] +pub struct IsContractWhitelistedResponse { + pub address: String, + pub is_whitelisted: bool, +} + +#[cw_serde] +pub struct WhitelistedContractsResponse { + pub contracts: Vec, +} diff --git a/test-suite/src/common_setup/setup_minter/vending_minter/setup.rs b/test-suite/src/common_setup/setup_minter/vending_minter/setup.rs index b2e1b94f8..de95a26c3 100644 --- a/test-suite/src/common_setup/setup_minter/vending_minter/setup.rs +++ b/test-suite/src/common_setup/setup_minter/vending_minter/setup.rs @@ -59,7 +59,10 @@ pub fn setup_minter_contract(setup_params: MinterSetupParams) -> MinterCollectio .instantiate_contract( factory_code_id, minter_admin.clone(), - &vending_factory::msg::InstantiateMsg { params }, + &vending_factory::msg::InstantiateMsg { + params, + initial_whitelist: None, + }, &[], "factory", None, diff --git a/test-suite/src/common_setup/templates.rs b/test-suite/src/common_setup/templates.rs index 3352bea4e..99c604489 100644 --- a/test-suite/src/common_setup/templates.rs +++ b/test-suite/src/common_setup/templates.rs @@ -451,7 +451,7 @@ pub fn base_minter_with_sudo_update_params_template( let collection_params = mock_collection_params_1(Some(start_time)); let minter_params = minter_params_token(num_tokens); let code_ids = base_minter_sg721_collection_code_ids(&mut app); - let minter_collection_response: Vec = configure_minter( + let minter_collection_response: Vec = configure_base_minter( &mut app, creator.clone(), vec![collection_params], diff --git a/test-suite/src/sg721_base/tests/integration_tests.rs b/test-suite/src/sg721_base/tests/integration_tests.rs index f891d5313..7e8686665 100644 --- a/test-suite/src/sg721_base/tests/integration_tests.rs +++ b/test-suite/src/sg721_base/tests/integration_tests.rs @@ -41,7 +41,10 @@ mod tests { let mut params = mock_params(None); params.code_id = minter_id; - let msg = FactoryInstantiateMsg { params }; + let msg = FactoryInstantiateMsg { + params, + initial_whitelist: None, + }; let factory_addr = app .instantiate_contract( factory_id, diff --git a/test-suite/src/sg_eth_airdrop/setup/configure_mock_minter.rs b/test-suite/src/sg_eth_airdrop/setup/configure_mock_minter.rs index 5a29f98db..15a7b430d 100644 --- a/test-suite/src/sg_eth_airdrop/setup/configure_mock_minter.rs +++ b/test-suite/src/sg_eth_airdrop/setup/configure_mock_minter.rs @@ -32,7 +32,10 @@ fn configure_mock_minter(app: &mut App, creator: Addr) { .instantiate_contract( factory_code_id, creator.clone(), - &vending_factory::msg::InstantiateMsg { params }, + &vending_factory::msg::InstantiateMsg { + params, + initial_whitelist: None, + }, &[], "factory", None, diff --git a/test-suite/src/vending_factory/tests/integration_tests.rs b/test-suite/src/vending_factory/tests/integration_tests.rs index 2ebdf4cc4..b0d418875 100644 --- a/test-suite/src/vending_factory/tests/integration_tests.rs +++ b/test-suite/src/vending_factory/tests/integration_tests.rs @@ -23,7 +23,10 @@ mod tests { .instantiate_contract( factory_id, Addr::unchecked(GOVERNANCE), - &InstantiateMsg { params }, + &InstantiateMsg { + params, + initial_whitelist: None, + }, &[], "factory", None, diff --git a/test-suite/src/vending_factory/tests/sudo_tests.rs b/test-suite/src/vending_factory/tests/sudo_tests.rs index 3b67b4419..3b17fa4bb 100644 --- a/test-suite/src/vending_factory/tests/sudo_tests.rs +++ b/test-suite/src/vending_factory/tests/sudo_tests.rs @@ -1,5 +1,6 @@ use base_factory::msg::ParamsResponse; use cosmwasm_std::coin; +use cw_multi_test::Executor; use sg_utils::NATIVE_DENOM; use vending_factory::msg::VendingUpdateParamsExtension; @@ -49,3 +50,148 @@ fn sudo_params_update_creation_fee() { let res: ParamsResponse = router.wrap().query_wasm_smart(factory, &Params {}).unwrap(); assert_eq!(res.params.creation_fee, coin(999, NATIVE_DENOM)); } + +#[test] +fn test_factory_whitelist_management() { + use vending_factory::msg::SudoMsg; + use sg2::query::{IsContractWhitelistedResponse, Sg2QueryMsg as FactoryQueryMsg, WhitelistedContractsResponse}; + + let vt = vending_minter_template_with_code_ids_template(1); + let (mut router, _, _) = (vt.router, vt.accts.creator, vt.accts.buyer); + let factory = vt.collection_response_vec[0].factory.clone().unwrap(); + + let contract_addr = "contract1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghij"; + + // Initially, contract should not be whitelisted + let query_msg = FactoryQueryMsg::IsContractWhitelisted { + address: contract_addr.to_string(), + }; + let res: IsContractWhitelistedResponse = router + .wrap() + .query_wasm_smart(factory.clone(), &query_msg) + .unwrap(); + assert!(!res.is_whitelisted, "Contract should not be whitelisted initially"); + + // Add contract to whitelist via sudo + let sudo_msg = SudoMsg::AddContractToWhitelist { + address: contract_addr.to_string(), + }; + let res = router.wasm_sudo(factory.clone(), &sudo_msg); + assert!(res.is_ok(), "Should be able to add contract via sudo"); + + // Check that contract is now whitelisted + let res: IsContractWhitelistedResponse = router + .wrap() + .query_wasm_smart(factory.clone(), &query_msg) + .unwrap(); + assert!(res.is_whitelisted, "Contract should be whitelisted after adding"); + + // Remove contract from whitelist via sudo + let sudo_msg = SudoMsg::RemoveContractFromWhitelist { + address: contract_addr.to_string(), + }; + let res = router.wasm_sudo(factory.clone(), &sudo_msg); + assert!(res.is_ok(), "Should be able to remove contract via sudo"); + + // Check that contract is no longer whitelisted + let res: IsContractWhitelistedResponse = router + .wrap() + .query_wasm_smart(factory, &query_msg) + .unwrap(); + assert!(!res.is_whitelisted, "Contract should not be whitelisted after removal"); +} + +#[test] +fn test_factory_batch_whitelist_update() { + use vending_factory::msg::SudoMsg; + use sg2::query::{IsContractWhitelistedResponse, Sg2QueryMsg as FactoryQueryMsg, WhitelistedContractsResponse}; + + let vt = vending_minter_template_with_code_ids_template(1); + let (mut router, _, _) = (vt.router, vt.accts.creator, vt.accts.buyer); + let factory = vt.collection_response_vec[0].factory.clone().unwrap(); + + let contract1 = "contract1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefgh1"; + let contract2 = "contract1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefgh2"; + let contract3 = "contract1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefgh3"; + + // Add contract1 first + let sudo_msg = SudoMsg::AddContractToWhitelist { + address: contract1.to_string(), + }; + router.wasm_sudo(factory.clone(), &sudo_msg).unwrap(); + + // Batch update: add contract2 and contract3, remove contract1 + let batch_msg = SudoMsg::UpdateContractWhitelist { + add: vec![contract2.to_string(), contract3.to_string()], + remove: vec![contract1.to_string()], + }; + let res = router.wasm_sudo(factory.clone(), &batch_msg); + assert!(res.is_ok(), "Batch update should succeed"); + + // Verify results + let query_msg1 = FactoryQueryMsg::IsContractWhitelisted { + address: contract1.to_string(), + }; + let res1: IsContractWhitelistedResponse = router + .wrap() + .query_wasm_smart(factory.clone(), &query_msg1) + .unwrap(); + assert!(!res1.is_whitelisted, "Contract1 should be removed"); + + let query_msg2 = FactoryQueryMsg::IsContractWhitelisted { + address: contract2.to_string(), + }; + let res2: IsContractWhitelistedResponse = router + .wrap() + .query_wasm_smart(factory.clone(), &query_msg2) + .unwrap(); + assert!(res2.is_whitelisted, "Contract2 should be added"); + + let query_msg3 = FactoryQueryMsg::IsContractWhitelisted { + address: contract3.to_string(), + }; + let res3: IsContractWhitelistedResponse = router + .wrap() + .query_wasm_smart(factory.clone(), &query_msg3) + .unwrap(); + assert!(res3.is_whitelisted, "Contract3 should be added"); + + // Query all whitelisted contracts + let query_msg = FactoryQueryMsg::WhitelistedContracts { + start_after: None, + limit: None, + }; + let res: WhitelistedContractsResponse = router + .wrap() + .query_wasm_smart(factory, &query_msg) + .unwrap(); + + assert_eq!(res.contracts.len(), 2, "Should have 2 whitelisted contracts"); + assert!(res.contracts.contains(&contract2.to_string())); + assert!(res.contracts.contains(&contract3.to_string())); +} + + +#[test] +fn test_instantiate_whitelist_is_working() { + // Note: This is a validation test to ensure the instantiate whitelist feature + // is properly implemented. The existing template uses initial_whitelist: None, + // so we validate that no contracts are whitelisted by default. + use vending_factory::msg::{IsContractWhitelistedResponse, QueryMsg as FactoryQueryMsg}; + + let vt = vending_minter_template_with_code_ids_template(1); + let (router, _, _) = (vt.router, vt.accts.creator, vt.accts.buyer); + let factory = vt.collection_response_vec[0].factory.clone().unwrap(); + + let contract_addr = "contract1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghij"; + + // Check that contract is not whitelisted (since factory was created with None) + let query_msg = FactoryQueryMsg::IsContractWhitelisted { + address: contract_addr.to_string(), + }; + let res: IsContractWhitelistedResponse = router + .wrap() + .query_wasm_smart(factory, &query_msg) + .unwrap(); + assert!(!res.is_whitelisted, "Contract should not be whitelisted when factory created with initial_whitelist: None"); +} diff --git a/test-suite/src/vending_minter/tests.rs b/test-suite/src/vending_minter/tests.rs index ef5900687..bfe688170 100644 --- a/test-suite/src/vending_minter/tests.rs +++ b/test-suite/src/vending_minter/tests.rs @@ -1,5 +1,7 @@ mod address_limit; mod allowed_code_ids; +mod contract_detection; +mod contract_whitelist; mod frozen_factory; mod happy_unhappy; mod ibc_asset_mint; diff --git a/test-suite/src/vending_minter/tests/contract_detection.rs b/test-suite/src/vending_minter/tests/contract_detection.rs new file mode 100644 index 000000000..ae34d8ce4 --- /dev/null +++ b/test-suite/src/vending_minter/tests/contract_detection.rs @@ -0,0 +1,243 @@ +use crate::common_setup::templates::vending_minter_template; +use cosmwasm_std::{coins, Addr}; +use cw_multi_test::Executor; +use sg_utils::{GENESIS_MINT_START_TIME, NATIVE_DENOM}; +use vending_minter::msg::ExecuteMsg; +use vending_minter::ContractError; + +use crate::common_setup::setup_accounts_and_block::setup_block_time; + +const MINT_PRICE: u128 = 100_000_000; + +#[test] +fn test_eoa_can_mint() { + let vt = vending_minter_template(1); + let (mut router, _creator, buyer) = (vt.router, vt.accts.creator, vt.accts.buyer); + let minter_addr = vt.collection_response_vec[0].minter.clone().unwrap(); + + // Set time after start time to enable minting + setup_block_time(&mut router, GENESIS_MINT_START_TIME + 1, None); + + // EOA address should be able to mint + let mint_msg = ExecuteMsg::Mint {}; + let res = router.execute_contract( + buyer, + minter_addr, + &mint_msg, + &coins(MINT_PRICE, NATIVE_DENOM), + ); + assert!(res.is_ok(), "EOA should be able to mint"); +} + +#[test] +fn test_short_addresses_treated_as_eoa() { + let vt = vending_minter_template(4); // Need 4 tokens for this test + let (mut router, _creator, _buyer) = (vt.router, vt.accts.creator, vt.accts.buyer); + let minter_addr = vt.collection_response_vec[0].minter.clone().unwrap(); + + // Create various short address formats (like typical EOAs) + let short_addresses = vec![ + Addr::unchecked("buyer"), // 5 chars + Addr::unchecked("simple_string_address"), // 21 chars + Addr::unchecked("cosmos1abc123def456ghi789jkl012mno345"), // 35 chars (typical bech32) + Addr::unchecked("terra1xyz789abc123def456ghi789jkl012"), // 34 chars + ]; + + // Set time after start time to enable minting + setup_block_time(&mut router, GENESIS_MINT_START_TIME + 1, None); + + for addr in short_addresses { + // Fund this address + router + .sudo(cw_multi_test::SudoMsg::Bank( + cw_multi_test::BankSudo::Mint { + to_address: addr.to_string(), + amount: coins(MINT_PRICE * 2, NATIVE_DENOM), + }, + )) + .unwrap(); + + // Short addresses should be treated as EOAs and allowed to mint + let mint_msg = ExecuteMsg::Mint {}; + let res = router.execute_contract( + addr.clone(), + minter_addr.clone(), + &mint_msg, + &coins(MINT_PRICE, NATIVE_DENOM), + ); + + assert!( + res.is_ok(), + "Short address '{}' should be treated as EOA and allowed to mint", + addr + ); + } +} + +#[test] +fn test_long_addresses_treated_as_contracts() { + let vt = vending_minter_template(1); + let (mut router, _creator, _buyer) = (vt.router, vt.accts.creator, vt.accts.buyer); + let minter_addr = vt.collection_response_vec[0].minter.clone().unwrap(); + + // Create a long address that looks like a contract address (>50 chars) + let long_contract_addr = + Addr::unchecked("contract1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghij"); + + // Fund this address + router + .sudo(cw_multi_test::SudoMsg::Bank( + cw_multi_test::BankSudo::Mint { + to_address: long_contract_addr.to_string(), + amount: coins(MINT_PRICE * 2, NATIVE_DENOM), + }, + )) + .unwrap(); + + // Set time after start time to enable minting + setup_block_time(&mut router, GENESIS_MINT_START_TIME + 1, None); + + // Long addresses should be treated as contracts and blocked from minting + let mint_msg = ExecuteMsg::Mint {}; + let res = router.execute_contract( + long_contract_addr.clone(), + minter_addr, + &mint_msg, + &coins(MINT_PRICE, NATIVE_DENOM), + ); + + // This should fail with ContractsCannotMint error + assert!( + res.is_err(), + "Long address should be treated as contract and blocked from minting" + ); + + let err = res.unwrap_err(); + let contract_err = err.downcast_ref::().unwrap(); + assert_eq!(*contract_err, ContractError::ContractsCannotMint {}); +} + +#[test] +fn test_admin_mint_to_works_from_any_address() { + let vt = vending_minter_template(1); + let (mut router, creator, buyer) = (vt.router, vt.accts.creator, vt.accts.buyer); + let minter_addr = vt.collection_response_vec[0].minter.clone().unwrap(); + + // Set time after start time + setup_block_time(&mut router, GENESIS_MINT_START_TIME + 1, None); + + // Admin mint_to should work regardless of address length + // Note: This assumes the creator is the admin and can perform mint_to + let mint_to_msg = ExecuteMsg::MintTo { + recipient: buyer.to_string(), + }; + + let res = router.execute_contract( + creator, // Admin performing the action + minter_addr, + &mint_to_msg, + &[], + ); + + assert!( + res.is_ok(), + "Admin mint_to should work regardless of caller type" + ); +} + +#[test] +fn test_boundary_address_length() { + let vt = vending_minter_template(2); // Need 2 tokens for this test + let (mut router, _creator, _buyer) = (vt.router, vt.accts.creator, vt.accts.buyer); + let minter_addr = vt.collection_response_vec[0].minter.clone().unwrap(); + + // Test addresses right at the boundary (50 chars) + let boundary_addr_49 = Addr::unchecked("1234567890123456789012345678901234567890123456789"); // 49 chars - should be EOA + let boundary_addr_51 = Addr::unchecked("123456789012345678901234567890123456789012345678901"); // 51 chars - should be contract + + // Set time after start time to enable minting + setup_block_time(&mut router, GENESIS_MINT_START_TIME + 1, None); + + // Fund both addresses + for addr in [&boundary_addr_49, &boundary_addr_51] { + router + .sudo(cw_multi_test::SudoMsg::Bank( + cw_multi_test::BankSudo::Mint { + to_address: addr.to_string(), + amount: coins(MINT_PRICE * 2, NATIVE_DENOM), + }, + )) + .unwrap(); + } + + // 49-char address should work (treated as EOA) + let mint_msg = ExecuteMsg::Mint {}; + let res_49 = router.execute_contract( + boundary_addr_49, + minter_addr.clone(), + &mint_msg, + &coins(MINT_PRICE, NATIVE_DENOM), + ); + assert!(res_49.is_ok(), "49-char address should be treated as EOA"); + + // 51-char address should fail (treated as contract) + let mint_msg = ExecuteMsg::Mint {}; + let res_51 = router.execute_contract( + boundary_addr_51, + minter_addr, + &mint_msg, + &coins(MINT_PRICE, NATIVE_DENOM), + ); + assert!( + res_51.is_err(), + "51-char address should be treated as contract" + ); + + let err = res_51.unwrap_err(); + let contract_err = err.downcast_ref::().unwrap(); + assert_eq!(*contract_err, ContractError::ContractsCannotMint {}); +} + +#[test] +fn test_real_contract_detected_by_contract_info_query() { + let vt = vending_minter_template(1); + let (mut router, _creator, _buyer) = (vt.router, vt.accts.creator, vt.accts.buyer); + let minter_addr = vt.collection_response_vec[0].minter.clone().unwrap(); + + // Use the actual minter contract address (which is a real contract but short address) + // This tests the ContractInfo query fallback since the minter address is likely <50 chars + // but is definitely a contract that should be blocked from minting + + // Set time after start time to enable minting + setup_block_time(&mut router, GENESIS_MINT_START_TIME + 1, None); + + // Fund the minter contract address so it can attempt to mint + router + .sudo(cw_multi_test::SudoMsg::Bank( + cw_multi_test::BankSudo::Mint { + to_address: minter_addr.to_string(), + amount: coins(MINT_PRICE * 2, NATIVE_DENOM), + }, + )) + .unwrap(); + + // The minter contract trying to mint from itself should be blocked + let mint_msg = ExecuteMsg::Mint {}; + let res = router.execute_contract( + minter_addr.clone(), + minter_addr, + &mint_msg, + &coins(MINT_PRICE, NATIVE_DENOM), + ); + + // This should fail with ContractsCannotMint error because the ContractInfo + // query should detect that minter_addr is a contract, even if it's <50 chars + assert!( + res.is_err(), + "Real contract address should be blocked from minting via ContractInfo query" + ); + + let err = res.unwrap_err(); + let contract_err = err.downcast_ref::().unwrap(); + assert_eq!(*contract_err, ContractError::ContractsCannotMint {}); +} diff --git a/test-suite/src/vending_minter/tests/contract_whitelist.rs b/test-suite/src/vending_minter/tests/contract_whitelist.rs new file mode 100644 index 000000000..7bb2982b7 --- /dev/null +++ b/test-suite/src/vending_minter/tests/contract_whitelist.rs @@ -0,0 +1,240 @@ +use crate::common_setup::templates::vending_minter_template; +use cosmwasm_std::{coins, Addr}; +use cw_multi_test::Executor; +use sg_utils::{GENESIS_MINT_START_TIME, NATIVE_DENOM}; +use vending_minter::msg::ExecuteMsg; +use vending_factory::msg::SudoMsg; +use sg2::query::{IsContractWhitelistedResponse, Sg2QueryMsg as FactoryQueryMsg, WhitelistedContractsResponse}; + +use crate::common_setup::setup_accounts_and_block::setup_block_time; + +const MINT_PRICE: u128 = 100_000_000; + +#[test] +fn test_governance_can_add_contract_to_factory_whitelist() { + let vt = vending_minter_template(1); + let (mut router, creator, _buyer) = (vt.router, vt.accts.creator, vt.accts.buyer); + let factory_addr = vt.collection_response_vec[0].factory.clone().unwrap(); + + // Create a fake contract address + let contract_addr = "contract1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghij"; + + // Use sudo to add contract to factory whitelist (governance-only operation) + let sudo_msg = SudoMsg::AddContractToWhitelist { + address: contract_addr.to_string(), + }; + + let res = router.wasm_sudo(factory_addr.clone(), &sudo_msg); + assert!(res.is_ok(), "Governance should be able to add contract to factory whitelist"); + + // Check that the contract is now whitelisted via factory query + let query_msg = FactoryQueryMsg::IsContractWhitelisted { + address: contract_addr.to_string(), + }; + let res: IsContractWhitelistedResponse = router + .wrap() + .query_wasm_smart(factory_addr, &query_msg) + .unwrap(); + + assert!(res.is_whitelisted, "Contract should be whitelisted"); + assert_eq!(res.address, contract_addr); +} + +#[test] +fn test_minter_no_longer_has_whitelist_execute_handlers() { + let vt = vending_minter_template(1); + let (_router, _creator, _buyer) = (vt.router, vt.accts.creator, vt.accts.buyer); + let _minter_addr = vt.collection_response_vec[0].minter.clone().unwrap(); + + let _contract_addr = "contract1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghij"; + + // These execute messages should no longer exist on the minter + // This test should fail to compile, proving the migration worked + // Commenting out to allow compilation + + // let add_msg = ExecuteMsg::AddContractToWhitelist { + // address: contract_addr.to_string(), + // }; + // let res = router.execute_contract(creator, minter_addr, &add_msg, &[]); + // assert!(res.is_err(), "Minter should no longer have whitelist execute handlers"); +} + +#[test] +fn test_whitelisted_contract_can_mint() { + let vt = vending_minter_template(2); // Need 2 tokens for this test + let (mut router, _creator, _buyer) = (vt.router, vt.accts.creator, vt.accts.buyer); + let minter_addr = vt.collection_response_vec[0].minter.clone().unwrap(); + let factory_addr = vt.collection_response_vec[0].factory.clone().unwrap(); + + // Create a long contract address (would normally be blocked) + let contract_addr = Addr::unchecked("contract1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghij"); + + // Set time after start time to enable minting + setup_block_time(&mut router, GENESIS_MINT_START_TIME + 1, None); + + // Fund the contract address + router + .sudo(cw_multi_test::SudoMsg::Bank( + cw_multi_test::BankSudo::Mint { + to_address: contract_addr.to_string(), + amount: coins(MINT_PRICE * 2, NATIVE_DENOM), + }, + )) + .unwrap(); + + // First, minting should fail (contract not whitelisted) + let mint_msg = ExecuteMsg::Mint {}; + let res = router.execute_contract( + contract_addr.clone(), + minter_addr.clone(), + &mint_msg, + &coins(MINT_PRICE, NATIVE_DENOM), + ); + assert!(res.is_err(), "Non-whitelisted contract should be blocked"); + + // Governance adds contract to factory whitelist (sudo operation) + let sudo_msg = SudoMsg::AddContractToWhitelist { + address: contract_addr.to_string(), + }; + let res = router.wasm_sudo(factory_addr, &sudo_msg); + assert!(res.is_ok(), "Governance should be able to add contract to factory whitelist"); + + // Now minting should succeed + let res = router.execute_contract( + contract_addr, + minter_addr, + &mint_msg, + &coins(MINT_PRICE, NATIVE_DENOM), + ); + assert!(res.is_ok(), "Whitelisted contract should be able to mint"); +} + +#[test] +fn test_remove_contract_from_factory_whitelist() { + let vt = vending_minter_template(1); + let (mut router, _creator, _buyer) = (vt.router, vt.accts.creator, vt.accts.buyer); + let factory_addr = vt.collection_response_vec[0].factory.clone().unwrap(); + + let contract_addr = "contract1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefghij"; + + // Add contract to factory whitelist via sudo + let add_msg = SudoMsg::AddContractToWhitelist { + address: contract_addr.to_string(), + }; + router + .wasm_sudo(factory_addr.clone(), &add_msg) + .unwrap(); + + // Verify it's whitelisted + let query_msg = FactoryQueryMsg::IsContractWhitelisted { + address: contract_addr.to_string(), + }; + let res: IsContractWhitelistedResponse = router + .wrap() + .query_wasm_smart(factory_addr.clone(), &query_msg) + .unwrap(); + assert!(res.is_whitelisted); + + // Remove from whitelist via sudo + let remove_msg = SudoMsg::RemoveContractFromWhitelist { + address: contract_addr.to_string(), + }; + let res = router.wasm_sudo(factory_addr.clone(), &remove_msg); + assert!(res.is_ok(), "Governance should be able to remove from factory whitelist"); + + // Verify it's no longer whitelisted + let res: IsContractWhitelistedResponse = router + .wrap() + .query_wasm_smart(factory_addr, &query_msg) + .unwrap(); + assert!(!res.is_whitelisted, "Contract should no longer be whitelisted"); +} + +#[test] +fn test_batch_update_factory_contract_whitelist() { + let vt = vending_minter_template(1); + let (mut router, _creator, _buyer) = (vt.router, vt.accts.creator, vt.accts.buyer); + let factory_addr = vt.collection_response_vec[0].factory.clone().unwrap(); + + let contract1 = "contract1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefgh1"; + let contract2 = "contract1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefgh2"; + let contract3 = "contract1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefgh3"; + + // Add contract1 to factory whitelist first via sudo + let add_msg = SudoMsg::AddContractToWhitelist { + address: contract1.to_string(), + }; + router + .wasm_sudo(factory_addr.clone(), &add_msg) + .unwrap(); + + // Batch update: add contract2 and contract3, remove contract1 + let batch_msg = SudoMsg::UpdateContractWhitelist { + add: vec![contract2.to_string(), contract3.to_string()], + remove: vec![contract1.to_string()], + }; + + let res = router.wasm_sudo(factory_addr.clone(), &batch_msg); + assert!(res.is_ok(), "Governance should be able to batch update factory whitelist"); + + // Check results + let query_msg1 = FactoryQueryMsg::IsContractWhitelisted { + address: contract1.to_string(), + }; + let res1: IsContractWhitelistedResponse = router + .wrap() + .query_wasm_smart(factory_addr.clone(), &query_msg1) + .unwrap(); + assert!(!res1.is_whitelisted, "Contract1 should be removed"); + + let query_msg2 = FactoryQueryMsg::IsContractWhitelisted { + address: contract2.to_string(), + }; + let res2: IsContractWhitelistedResponse = router + .wrap() + .query_wasm_smart(factory_addr.clone(), &query_msg2) + .unwrap(); + assert!(res2.is_whitelisted, "Contract2 should be added"); + + let query_msg3 = FactoryQueryMsg::IsContractWhitelisted { + address: contract3.to_string(), + }; + let res3: IsContractWhitelistedResponse = router + .wrap() + .query_wasm_smart(factory_addr, &query_msg3) + .unwrap(); + assert!(res3.is_whitelisted, "Contract3 should be added"); +} + +#[test] +fn test_query_factory_whitelisted_contracts() { + let vt = vending_minter_template(1); + let (mut router, _creator, _buyer) = (vt.router, vt.accts.creator, vt.accts.buyer); + let factory_addr = vt.collection_response_vec[0].factory.clone().unwrap(); + + let contract1 = "contract1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefgh1"; + let contract2 = "contract1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdefgh2"; + + // Add both contracts to factory whitelist via sudo + let batch_msg = SudoMsg::UpdateContractWhitelist { + add: vec![contract1.to_string(), contract2.to_string()], + remove: vec![], + }; + router + .wasm_sudo(factory_addr.clone(), &batch_msg) + .unwrap(); + + // Query all whitelisted contracts from factory + let query_msg = FactoryQueryMsg::WhitelistedContracts { + start_after: None, + limit: None, + }; + let res: WhitelistedContractsResponse = router + .wrap() + .query_wasm_smart(factory_addr, &query_msg) + .unwrap(); + + assert_eq!(res.contracts.len(), 2, "Should have 2 whitelisted contracts"); + assert!(res.contracts.contains(&contract1.to_string())); + assert!(res.contracts.contains(&contract2.to_string())); +} \ No newline at end of file diff --git a/test-suite/src/vending_minter/tests/ibc_asset_mint.rs b/test-suite/src/vending_minter/tests/ibc_asset_mint.rs index ce4ae3fb2..88d768d03 100644 --- a/test-suite/src/vending_minter/tests/ibc_asset_mint.rs +++ b/test-suite/src/vending_minter/tests/ibc_asset_mint.rs @@ -81,7 +81,10 @@ fn denom_mismatch_creating_minter() { .instantiate_contract( factory_code_id, minter_admin.clone(), - &vending_factory::msg::InstantiateMsg { params }, + &vending_factory::msg::InstantiateMsg { + params, + initial_whitelist: None, + }, &[], "factory", None, @@ -155,7 +158,10 @@ fn wl_denom_mint() { .instantiate_contract( factory_code_id, minter_admin.clone(), - &vending_factory::msg::InstantiateMsg { params }, + &vending_factory::msg::InstantiateMsg { + params, + initial_whitelist: None, + }, &[], "factory", None, diff --git a/test-suite/src/vending_minter/tests/zero_mint_price.rs b/test-suite/src/vending_minter/tests/zero_mint_price.rs index 203862dab..da4666a46 100644 --- a/test-suite/src/vending_minter/tests/zero_mint_price.rs +++ b/test-suite/src/vending_minter/tests/zero_mint_price.rs @@ -68,7 +68,10 @@ fn zero_mint_price() { .instantiate_contract( factory_code_id, minter_admin.clone(), - &vending_factory::msg::InstantiateMsg { params }, + &vending_factory::msg::InstantiateMsg { + params, + initial_whitelist: None, + }, &[], "factory", None, @@ -158,7 +161,10 @@ fn zero_wl_mint_price() { .instantiate_contract( factory_code_id, minter_admin.clone(), - &vending_factory::msg::InstantiateMsg { params }, + &vending_factory::msg::InstantiateMsg { + params, + initial_whitelist: None, + }, &[], "factory", None, @@ -255,7 +261,10 @@ fn zero_wl_mint_errs_with_min_mint_factory() { .instantiate_contract( factory_code_id, minter_admin.clone(), - &vending_factory::msg::InstantiateMsg { params }, + &vending_factory::msg::InstantiateMsg { + params, + initial_whitelist: None, + }, &[], "factory", None,