From 7756251f6ac7fc0a2a18f32b2551d99d300b8306 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Sun, 20 Aug 2023 03:19:46 -0700 Subject: [PATCH] Support different NFT contracts with `dao-voting-cw721-staked` (#726) * Support a wider variety of NFT contracts Makes the interface for instantiating a new collection a bit more generic to account for most types of cw721 and sg721 contracts. * Improve test coverage * Address PR comments, check errors * Apply @bekauz's suggestion * Add migrate message New features don't require any state migrations. Both `ACTIVE_THRESHOLD` and `HOOKS` are uninitialized if none are set, so no migration logic needed. * More test coverage --- Cargo.lock | 69 ++++ Cargo.toml | 4 + .../voting/dao-voting-cw721-staked/Cargo.toml | 6 +- .../voting/dao-voting-cw721-staked/README.md | 13 +- .../schema/dao-voting-cw721-staked.json | 58 +-- .../dao-voting-cw721-staked/src/contract.rs | 92 +++-- .../dao-voting-cw721-staked/src/error.rs | 4 +- .../voting/dao-voting-cw721-staked/src/msg.rs | 26 +- .../dao-voting-cw721-staked/src/state.rs | 6 +- .../src/testing/tests.rs | 339 ++++++++++++++---- 10 files changed, 437 insertions(+), 180 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2482e5c75..69cd6094e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1976,6 +1976,10 @@ dependencies = [ "dao-interface", "dao-testing", "dao-voting 2.2.0", + "sg-multi-test", + "sg-std", + "sg721", + "sg721-base", "thiserror", ] @@ -3446,6 +3450,71 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "sg-multi-test" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20744734b8049c64747bfb083bbc06a3c7204d1d34881ed3d89698e182aa9f97" +dependencies = [ + "anyhow", + "cosmwasm-std", + "cw-multi-test", + "schemars", + "serde", + "sg-std", +] + +[[package]] +name = "sg-std" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171f97d3032b7d713dd16decaed06479e7ce5585147f387860ad2fb3f7b9ed94" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils 1.0.1", + "cw721 0.18.0", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "sg721" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e7d8f93b519c4c95973a68c7abee2de838497974d666dddb4dabd04d9c7cbf6" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-ownable", + "cw-utils 1.0.1", + "cw721-base 0.18.0", + "serde", + "thiserror", +] + +[[package]] +name = "sg721-base" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af08801d6f50cb13be05a3d2e815fbdb9dbba82086bbab877599ed4d422e9441" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-ownable", + "cw-storage-plus 1.1.0", + "cw-utils 1.0.1", + "cw2 1.1.0", + "cw721 0.18.0", + "cw721-base 0.18.0", + "serde", + "sg-std", + "sg721", + "thiserror", + "url", +] + [[package]] name = "sha1" version = "0.10.5" diff --git a/Cargo.toml b/Cargo.toml index ca2fd47c1..e02e21d67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,10 @@ rand = "0.8" serde = { version = "1.0", default-features = false, features = ["derive"]} serde_json = "1.0" serde_yaml = "0.9" +sg-std = "3.1.0" +sg721 = "3.1.0" +sg721-base = "3.1.0" +sg-multi-test = "3.1.0" syn = { version = "1.0", features = ["derive"] } test-context = "0.1" thiserror = { version = "1.0" } diff --git a/contracts/voting/dao-voting-cw721-staked/Cargo.toml b/contracts/voting/dao-voting-cw721-staked/Cargo.toml index a882a1eb4..e5535c673 100644 --- a/contracts/voting/dao-voting-cw721-staked/Cargo.toml +++ b/contracts/voting/dao-voting-cw721-staked/Cargo.toml @@ -28,9 +28,13 @@ cw-paginate-storage = { workspace = true } cw-utils = { workspace = true } cw2 = { workspace = true } dao-voting = { workspace = true } +sg-std = { workspace = true } +sg721 = { workspace = true } thiserror = { workspace = true } [dev-dependencies] -cw-multi-test = { workspace = true } anyhow = { workspace = true } +cw-multi-test = { workspace = true } dao-testing = { workspace = true } +sg721-base = { workspace = true, features = ["library"] } +sg-multi-test = { workspace = true } diff --git a/contracts/voting/dao-voting-cw721-staked/README.md b/contracts/voting/dao-voting-cw721-staked/README.md index 015c8d32d..eb8c0b3fe 100644 --- a/contracts/voting/dao-voting-cw721-staked/README.md +++ b/contracts/voting/dao-voting-cw721-staked/README.md @@ -1,8 +1,7 @@ -# Stake CW721 +# `dao-voting-cw721-staked` -This is a basic implementation of a cw721 staking contract. Staked -tokens can be unbonded with a configurable unbonding period. Staked -balances can be queried at any arbitrary height by external -contracts. This contract implements the interface needed to be a DAO -DAO [voting -module](https://github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-Contracts-Design#the-voting-module). +This is a basic implementation of an NFT staking contract. + +Staked tokens can be unbonded with a configurable unbonding period. Staked balances can be queried at any arbitrary height by external contracts. This contract implements the interface needed to be a DAO DAO [voting module](https://github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-Contracts-Design#the-voting-module). + +`dao-voting-cw721-staked` can be used with existing NFT collections or create new `cw721` or `sg721` collections upon instantiation (with the DAO as admin and `minter`). diff --git a/contracts/voting/dao-voting-cw721-staked/schema/dao-voting-cw721-staked.json b/contracts/voting/dao-voting-cw721-staked/schema/dao-voting-cw721-staked.json index 026b7803e..776398bd6 100644 --- a/contracts/voting/dao-voting-cw721-staked/schema/dao-voting-cw721-staked.json +++ b/contracts/voting/dao-voting-cw721-staked/schema/dao-voting-cw721-staked.json @@ -144,6 +144,10 @@ } ] }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, "Decimal": { "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" @@ -182,10 +186,6 @@ } ] }, - "Empty": { - "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", - "type": "object" - }, "NftContract": { "oneOf": [ { @@ -222,8 +222,7 @@ "code_id", "initial_nfts", "label", - "name", - "symbol" + "msg" ], "properties": { "code_id": { @@ -233,23 +232,18 @@ "minimum": 0.0 }, "initial_nfts": { - "description": "Initial NFTs to mint when creating the NFT contract. If empty, an error is thrown.", + "description": "Initial NFTs to mint when creating the NFT contract. If empty, an error is thrown. The binary should be a valid mint message for the corresponding cw721 contract.", "type": "array", "items": { - "$ref": "#/definitions/NftMintMsg" + "$ref": "#/definitions/Binary" } }, "label": { "description": "Label to use for instantiated cw721 contract.", "type": "string" }, - "name": { - "description": "NFT collection name", - "type": "string" - }, - "symbol": { - "description": "NFT collection symbol", - "type": "string" + "msg": { + "$ref": "#/definitions/Binary" } }, "additionalProperties": false @@ -259,40 +253,6 @@ } ] }, - "NftMintMsg": { - "type": "object", - "required": [ - "extension", - "owner", - "token_id" - ], - "properties": { - "extension": { - "description": "Any custom extension used by this contract", - "allOf": [ - { - "$ref": "#/definitions/Empty" - } - ] - }, - "owner": { - "description": "The owner of the newly minter NFT", - "type": "string" - }, - "token_id": { - "description": "Unique ID of the NFT", - "type": "string" - }, - "token_uri": { - "description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" diff --git a/contracts/voting/dao-voting-cw721-staked/src/contract.rs b/contracts/voting/dao-voting-cw721-staked/src/contract.rs index 969666328..3d5b1d849 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/contract.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/contract.rs @@ -1,16 +1,17 @@ use crate::hooks::{stake_hook_msgs, unstake_hook_msgs}; -use crate::msg::{ActiveThresholdResponse, NftContract}; +use crate::msg::{ActiveThresholdResponse, MigrateMsg, NftContract}; use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; use crate::state::{ register_staked_nft, register_unstaked_nfts, Config, ACTIVE_THRESHOLD, CONFIG, DAO, HOOKS, INITITIAL_NFTS, MAX_CLAIMS, NFT_BALANCES, NFT_CLAIMS, STAKED_NFTS_PER_OWNER, TOTAL_STAKED_NFTS, }; use crate::ContractError; +use cosmwasm_schema::cw_serde; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Addr, Binary, CosmosMsg, Decimal, Deps, DepsMut, Empty, Env, MessageInfo, Reply, - Response, StdResult, SubMsg, Uint128, Uint256, WasmMsg, + from_binary, to_binary, Addr, Binary, CosmosMsg, Decimal, Deps, DepsMut, Empty, Env, + MessageInfo, Reply, Response, StdError, StdResult, SubMsg, Uint128, Uint256, WasmMsg, }; use cw2::set_contract_version; use cw721::{Cw721ReceiveMsg, NumTokensResponse}; @@ -29,6 +30,42 @@ const INSTANTIATE_NFT_CONTRACT_REPLY_ID: u64 = 0; // when using active threshold with percent const PRECISION_FACTOR: u128 = 10u128.pow(9); +#[cw_serde] +pub enum NftInstantiateMsg { + Cw721(cw721_base::InstantiateMsg), + Sg721(sg721::InstantiateMsg), +} + +impl NftInstantiateMsg { + fn update_minter(&mut self, minter: &str) { + match self { + NftInstantiateMsg::Cw721(msg) => msg.minter = minter.to_string(), + NftInstantiateMsg::Sg721(msg) => msg.minter = minter.to_string(), + } + } + + fn to_binary(&self) -> Result { + match self { + NftInstantiateMsg::Cw721(msg) => to_binary(&msg), + NftInstantiateMsg::Sg721(msg) => to_binary(&msg), + } + } +} + +pub fn try_deserialize_nft_instantiate_msg( + instantiate_msg: Binary, +) -> Result { + if let Ok(cw721_msg) = from_binary::(&instantiate_msg) { + return Ok(NftInstantiateMsg::Cw721(cw721_msg)); + } + + if let Ok(sg721_msg) = from_binary::(&instantiate_msg) { + return Ok(NftInstantiateMsg::Sg721(sg721_msg)); + } + + Err(ContractError::NftInstantiateError {}) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, @@ -89,10 +126,14 @@ pub fn instantiate( NftContract::New { code_id, label, - name, - symbol, + msg: instantiate_msg, initial_nfts, } => { + // Deserialize the binary msg into either cw721 or sg721 + let mut instantiate_msg = try_deserialize_nft_instantiate_msg(instantiate_msg)?; + // Update the minter to be this contract + instantiate_msg.update_minter(env.contract.address.as_str()); + // Check there is at least one NFT to initialize if initial_nfts.is_empty() { return Err(ContractError::NoInitialNfts {}); @@ -109,29 +150,26 @@ pub fn instantiate( // Save initial NFTs for use in reply INITITIAL_NFTS.save(deps.storage, &initial_nfts)?; - // Create instantiate submessage for NFT roles contract - let msg = SubMsg::reply_on_success( + // Create instantiate submessage for NFT contract + let instantiate_msg = SubMsg::reply_on_success( WasmMsg::Instantiate { code_id, funds: vec![], admin: Some(info.sender.to_string()), label, - msg: to_binary(&cw721_base::msg::InstantiateMsg { - name, - symbol, - // Admin must be set to contract to mint initial NFTs - minter: env.contract.address.to_string(), - })?, + msg: instantiate_msg.to_binary()?, }, INSTANTIATE_NFT_CONTRACT_REPLY_ID, ); - Ok(Response::default().add_submessage(msg).add_attribute( - "owner", - owner - .map(|a| a.into_string()) - .unwrap_or_else(|| "None".to_string()), - )) + Ok(Response::default() + .add_submessage(instantiate_msg) + .add_attribute( + "owner", + owner + .map(|a| a.into_string()) + .unwrap_or_else(|| "None".to_string()), + )) } } } @@ -581,6 +619,13 @@ pub fn query_staked_nfts( to_binary(&range?) } +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + // Set contract to version to latest + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + Ok(Response::default()) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { match msg.id { @@ -605,14 +650,7 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result::Mint { - token_id: nft.token_id.clone(), - owner: nft.owner.clone(), - token_uri: nft.token_uri.clone(), - extension: Empty {}, - }, - )?, + msg: nft.clone(), })) }) .collect::>(); diff --git a/contracts/voting/dao-voting-cw721-staked/src/error.rs b/contracts/voting/dao-voting-cw721-staked/src/error.rs index cf2050df3..10fb9a2db 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/error.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/error.rs @@ -18,10 +18,10 @@ pub enum ContractError { #[error("Invalid token. Got ({received}), expected ({expected})")] InvalidToken { received: Addr, expected: Addr }, - #[error("Error instantiating cw721-roles contract")] + #[error("Error instantiating NFT contract")] NftInstantiateError {}, - #[error("New cw721-roles contract must be instantiated with at least one NFT")] + #[error("New NFT contract must be instantiated with at least one NFT")] NoInitialNfts {}, #[error("Nothing to claim")] diff --git a/contracts/voting/dao-voting-cw721-staked/src/msg.rs b/contracts/voting/dao-voting-cw721-staked/src/msg.rs index 4d3b1bbfc..067716bd9 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/msg.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/msg.rs @@ -1,25 +1,11 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::Empty; +use cosmwasm_std::Binary; use cw721::Cw721ReceiveMsg; use cw_utils::Duration; use dao_dao_macros::{active_query, voting_module_query}; use dao_interface::state::Admin; use dao_voting::threshold::ActiveThreshold; -#[cw_serde] -pub struct NftMintMsg { - /// Unique ID of the NFT - pub token_id: String, - /// The owner of the newly minter NFT - pub owner: String, - /// Universal resource identifier for this NFT - /// Should point to a JSON file that conforms to the ERC721 - /// Metadata JSON Schema - pub token_uri: Option, - /// Any custom extension used by this contract - pub extension: Empty, -} - #[cw_serde] #[allow(clippy::large_enum_variant)] pub enum NftContract { @@ -32,13 +18,11 @@ pub enum NftContract { code_id: u64, /// Label to use for instantiated cw721 contract. label: String, - /// NFT collection name - name: String, - /// NFT collection symbol - symbol: String, + msg: Binary, /// Initial NFTs to mint when creating the NFT contract. - /// If empty, an error is thrown. - initial_nfts: Vec, + /// If empty, an error is thrown. The binary should be a + /// valid mint message for the corresponding cw721 contract. + initial_nfts: Vec, }, } diff --git a/contracts/voting/dao-voting-cw721-staked/src/state.rs b/contracts/voting/dao-voting-cw721-staked/src/state.rs index 486285af2..b7a3c00cd 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/state.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/state.rs @@ -1,12 +1,12 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Empty, StdError, StdResult, Storage, Uint128}; +use cosmwasm_std::{Addr, Binary, Empty, StdError, StdResult, Storage, Uint128}; use cw721_controllers::NftClaims; use cw_controllers::Hooks; use cw_storage_plus::{Item, Map, SnapshotItem, SnapshotMap, Strategy}; use cw_utils::Duration; use dao_voting::threshold::ActiveThreshold; -use crate::{msg::NftMintMsg, ContractError}; +use crate::ContractError; #[cw_serde] pub struct Config { @@ -20,7 +20,7 @@ pub const CONFIG: Item = Item::new("config"); pub const DAO: Item = Item::new("dao"); // Holds initial NFTs messages during instantiation. -pub const INITITIAL_NFTS: Item> = Item::new("initial_nfts"); +pub const INITITIAL_NFTS: Item> = Item::new("initial_nfts"); /// The set of NFTs currently staked by each address. The existence of /// an `(address, token_id)` pair implies that `address` has staked diff --git a/contracts/voting/dao-voting-cw721-staked/src/testing/tests.rs b/contracts/voting/dao-voting-cw721-staked/src/testing/tests.rs index 2acf28174..ba8eb5915 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/testing/tests.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/testing/tests.rs @@ -1,13 +1,20 @@ -use cosmwasm_std::{Addr, Decimal, Empty, Uint128}; +use cosmwasm_std::testing::{mock_dependencies, mock_env}; +use cosmwasm_std::{to_binary, Addr, Decimal, Empty, Uint128}; +use cw721::OwnerOfResponse; +use cw721_base::msg::{ExecuteMsg as Cw721ExecuteMsg, InstantiateMsg as Cw721InstantiateMsg}; use cw721_controllers::{NftClaim, NftClaimsResponse}; -use cw_multi_test::{next_block, App, Executor}; +use cw_multi_test::{next_block, App, Contract, ContractWrapper, Executor}; use cw_utils::Duration; use dao_interface::{state::Admin, voting::IsActiveResponse}; use dao_testing::contracts::{cw721_base_contract, voting_cw721_staked_contract}; use dao_voting::threshold::ActiveThreshold; +use sg721::CollectionInfo; +use sg_multi_test::StargazeApp; +use sg_std::StargazeMsgWrapper; use crate::{ - msg::{ActiveThresholdResponse, ExecuteMsg, InstantiateMsg, NftContract, NftMintMsg, QueryMsg}, + contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}, + msg::{ActiveThresholdResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, NftContract, QueryMsg}, state::{Config, MAX_CLAIMS}, testing::{ execute::{ @@ -26,7 +33,7 @@ use super::{ // I can create new NFT collection when creating a dao-voting-cw721-staked contract #[test] -fn test_instantiate_with_new_collection() -> anyhow::Result<()> { +fn test_instantiate_with_new_cw721_collection() -> anyhow::Result<()> { let mut app = App::default(); let module_id = app.store_code(voting_cw721_staked_contract()); let cw721_id = app.store_code(cw721_base_contract()); @@ -42,14 +49,17 @@ fn test_instantiate_with_new_collection() -> anyhow::Result<()> { nft_contract: NftContract::New { code_id: cw721_id, label: "Test NFT".to_string(), - name: "Test NFT".to_string(), - symbol: "TEST".to_string(), - initial_nfts: vec![NftMintMsg { + msg: to_binary(&Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + })?, + initial_nfts: vec![to_binary(&Cw721ExecuteMsg::::Mint { owner: CREATOR_ADDR.to_string(), token_uri: Some("https://example.com".to_string()), token_id: "1".to_string(), extension: Empty {}, - }], + })?], }, unstaking_duration: None, active_threshold: None, @@ -466,14 +476,19 @@ fn test_instantiate_zero_active_threshold_count() { nft_contract: NftContract::New { code_id: cw721_id, label: "Test NFT".to_string(), - name: "Test NFT".to_string(), - symbol: "TEST".to_string(), - initial_nfts: vec![NftMintMsg { + msg: to_binary(&Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }) + .unwrap(), + initial_nfts: vec![to_binary(&Cw721ExecuteMsg::::Mint { owner: CREATOR_ADDR.to_string(), token_uri: Some("https://example.com".to_string()), token_id: "1".to_string(), extension: Empty {}, - }], + }) + .unwrap()], }, unstaking_duration: None, active_threshold: Some(ActiveThreshold::AbsoluteCount { @@ -504,27 +519,34 @@ fn test_active_threshold_absolute_count() { nft_contract: NftContract::New { code_id: cw721_id, label: "Test NFT".to_string(), - name: "Test NFT".to_string(), - symbol: "TEST".to_string(), + msg: to_binary(&Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }) + .unwrap(), initial_nfts: vec![ - NftMintMsg { + to_binary(&Cw721ExecuteMsg::::Mint { owner: CREATOR_ADDR.to_string(), token_uri: Some("https://example.com".to_string()), token_id: "1".to_string(), extension: Empty {}, - }, - NftMintMsg { + }) + .unwrap(), + to_binary(&Cw721ExecuteMsg::::Mint { owner: CREATOR_ADDR.to_string(), token_uri: Some("https://example.com".to_string()), token_id: "2".to_string(), extension: Empty {}, - }, - NftMintMsg { + }) + .unwrap(), + to_binary(&Cw721ExecuteMsg::::Mint { owner: CREATOR_ADDR.to_string(), token_uri: Some("https://example.com".to_string()), token_id: "3".to_string(), extension: Empty {}, - }, + }) + .unwrap(), ], }, unstaking_duration: None, @@ -580,14 +602,19 @@ fn test_active_threshold_percent() { nft_contract: NftContract::New { code_id: cw721_id, label: "Test NFT".to_string(), - name: "Test NFT".to_string(), - symbol: "TEST".to_string(), - initial_nfts: vec![NftMintMsg { + msg: to_binary(&Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }) + .unwrap(), + initial_nfts: vec![to_binary(&Cw721ExecuteMsg::::Mint { owner: CREATOR_ADDR.to_string(), token_uri: Some("https://example.com".to_string()), token_id: "1".to_string(), extension: Empty {}, - }], + }) + .unwrap()], }, unstaking_duration: None, active_threshold: Some(ActiveThreshold::Percentage { @@ -639,39 +666,48 @@ fn test_active_threshold_percent_rounds_up() { nft_contract: NftContract::New { code_id: cw721_id, label: "Test NFT".to_string(), - name: "Test NFT".to_string(), - symbol: "TEST".to_string(), + msg: to_binary(&Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }) + .unwrap(), initial_nfts: vec![ - NftMintMsg { + to_binary(&Cw721ExecuteMsg::::Mint { owner: CREATOR_ADDR.to_string(), token_uri: Some("https://example.com".to_string()), token_id: "1".to_string(), extension: Empty {}, - }, - NftMintMsg { + }) + .unwrap(), + to_binary(&Cw721ExecuteMsg::::Mint { owner: CREATOR_ADDR.to_string(), token_uri: Some("https://example.com".to_string()), token_id: "2".to_string(), extension: Empty {}, - }, - NftMintMsg { + }) + .unwrap(), + to_binary(&Cw721ExecuteMsg::::Mint { owner: CREATOR_ADDR.to_string(), token_uri: Some("https://example.com".to_string()), token_id: "3".to_string(), extension: Empty {}, - }, - NftMintMsg { + }) + .unwrap(), + to_binary(&Cw721ExecuteMsg::::Mint { owner: CREATOR_ADDR.to_string(), token_uri: Some("https://example.com".to_string()), token_id: "4".to_string(), extension: Empty {}, - }, - NftMintMsg { + }) + .unwrap(), + to_binary(&Cw721ExecuteMsg::::Mint { owner: CREATOR_ADDR.to_string(), token_uri: Some("https://example.com".to_string()), token_id: "5".to_string(), extension: Empty {}, - }, + }) + .unwrap(), ], }, unstaking_duration: None, @@ -736,14 +772,19 @@ fn test_update_active_threshold() { nft_contract: NftContract::New { code_id: cw721_id, label: "Test NFT".to_string(), - name: "Test NFT".to_string(), - symbol: "TEST".to_string(), - initial_nfts: vec![NftMintMsg { + msg: to_binary(&Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }) + .unwrap(), + initial_nfts: vec![to_binary(&Cw721ExecuteMsg::::Mint { owner: CREATOR_ADDR.to_string(), token_uri: Some("https://example.com".to_string()), token_id: "1".to_string(), extension: Empty {}, - }], + }) + .unwrap()], }, unstaking_duration: None, active_threshold: None, @@ -808,14 +849,19 @@ fn test_active_threshold_percentage_gt_100() { nft_contract: NftContract::New { code_id: cw721_id, label: "Test NFT".to_string(), - name: "Test NFT".to_string(), - symbol: "TEST".to_string(), - initial_nfts: vec![NftMintMsg { + msg: to_binary(&Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }) + .unwrap(), + initial_nfts: vec![to_binary(&Cw721ExecuteMsg::::Mint { owner: CREATOR_ADDR.to_string(), token_uri: Some("https://example.com".to_string()), token_id: "1".to_string(), extension: Empty {}, - }], + }) + .unwrap()], }, unstaking_duration: None, active_threshold: Some(ActiveThreshold::Percentage { @@ -846,14 +892,19 @@ fn test_active_threshold_percentage_lte_0() { nft_contract: NftContract::New { code_id: cw721_id, label: "Test NFT".to_string(), - name: "Test NFT".to_string(), - symbol: "TEST".to_string(), - initial_nfts: vec![NftMintMsg { + msg: to_binary(&Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }) + .unwrap(), + initial_nfts: vec![to_binary(&Cw721ExecuteMsg::::Mint { owner: CREATOR_ADDR.to_string(), token_uri: Some("https://example.com".to_string()), token_id: "1".to_string(), extension: Empty {}, - }], + }) + .unwrap()], }, unstaking_duration: None, active_threshold: Some(ActiveThreshold::Percentage { @@ -867,34 +918,182 @@ fn test_active_threshold_percentage_lte_0() { .unwrap(); } +#[test] +fn test_invalid_instantiate_msg() { + let mut app = App::default(); + let module_id = app.store_code(voting_cw721_staked_contract()); + let cw721_id = app.store_code(cw721_base_contract()); + + let err = app + .instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + owner: Some(Admin::Address { + addr: CREATOR_ADDR.to_string(), + }), + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + msg: to_binary(&Empty {}).unwrap(), + initial_nfts: vec![to_binary(&Cw721ExecuteMsg::::Mint { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }) + .unwrap()], + }, + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(1), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Error instantiating NFT contract".to_string() + ); +} + #[test] fn test_no_initial_nfts_fails() { let mut app = App::default(); let cw721_id = app.store_code(cw721_base_contract()); let module_id = app.store_code(voting_cw721_staked_contract()); - app.instantiate_contract( - module_id, - Addr::unchecked(CREATOR_ADDR), - &InstantiateMsg { - owner: Some(Admin::Address { - addr: CREATOR_ADDR.to_string(), - }), - nft_contract: NftContract::New { - code_id: cw721_id, - label: "Test NFT".to_string(), - name: "Test NFT".to_string(), - symbol: "TEST".to_string(), - initial_nfts: vec![], + let err = app + .instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + owner: Some(Admin::Address { + addr: CREATOR_ADDR.to_string(), + }), + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + msg: to_binary(&Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }) + .unwrap(), + initial_nfts: vec![], + }, + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(1), + }), }, - unstaking_duration: None, - active_threshold: Some(ActiveThreshold::Percentage { - percent: Decimal::percent(0), - }), + &[], + "cw721_voting", + None, + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "New NFT contract must be instantiated with at least one NFT".to_string() + ); +} + +// I can create new Stargaze NFT collection when creating a dao-voting-cw721-staked contract +#[test] +fn test_instantiate_with_new_sg721_collection() -> anyhow::Result<()> { + // Setup Stargaze contracts for multi-test + fn sg721_base_contract() -> Box> { + let contract = ContractWrapper::new( + sg721_base::entry::execute, + sg721_base::entry::instantiate, + sg721_base::entry::query, + ); + Box::new(contract) + } + + // Stargze contracts need a custom message wrapper + fn voting_sg721_staked_contract() -> Box> { + let contract = ContractWrapper::new_with_empty( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + ) + .with_reply_empty(crate::contract::reply); + Box::new(contract) + } + + let mut app = StargazeApp::default(); + let module_id = app.store_code(voting_sg721_staked_contract()); + let sg721_id = app.store_code(sg721_base_contract()); + + let module_addr = app + .instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + owner: Some(Admin::Address { + addr: CREATOR_ADDR.to_string(), + }), + nft_contract: NftContract::New { + code_id: sg721_id, + label: "Test NFT".to_string(), + msg: to_binary(&sg721::InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + collection_info: CollectionInfo { + creator: CREATOR_ADDR.to_string(), + description: "Test NFT".to_string(), + image: "https://example.com/image.jpg".to_string(), + external_link: None, + explicit_content: None, + start_trading_time: None, + royalty_info: None, + }, + })?, + initial_nfts: vec![to_binary(&sg721::ExecuteMsg::::Mint { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + })?], + }, + unstaking_duration: None, + active_threshold: None, + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); + + let config: Config = app + .wrap() + .query_wasm_smart(module_addr, &QueryMsg::Config {})?; + let sg721_addr = config.nft_address; + + // Check that the NFT contract was created + let owner: OwnerOfResponse = app.wrap().query_wasm_smart( + sg721_addr, + &cw721::Cw721QueryMsg::OwnerOf { + token_id: "1".to_string(), + include_expired: None, }, - &[], - "cw721_voting", - None, - ) - .unwrap_err(); + )?; + assert_eq!(owner.owner, CREATOR_ADDR); + + Ok(()) +} + +#[test] +pub fn test_migrate_update_version() { + let mut deps = mock_dependencies(); + cw2::set_contract_version(&mut deps.storage, "my-contract", "old-version").unwrap(); + migrate(deps.as_mut(), mock_env(), MigrateMsg {}).unwrap(); + let version = cw2::get_contract_version(&deps.storage).unwrap(); + assert_eq!(version.version, CONTRACT_VERSION); + assert_eq!(version.contract, CONTRACT_NAME); }