Skip to content

Commit

Permalink
Support different NFT contracts with dao-voting-cw721-staked (#726)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
JakeHartnell authored Aug 20, 2023
1 parent 61dcd63 commit 7756251
Show file tree
Hide file tree
Showing 10 changed files with 437 additions and 180 deletions.
69 changes: 69 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
6 changes: 5 additions & 1 deletion contracts/voting/dao-voting-cw721-staked/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
13 changes: 6 additions & 7 deletions contracts/voting/dao-voting-cw721-staked/README.md
Original file line number Diff line number Diff line change
@@ -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`).
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@
}
]
},
"Binary": {
"description": "Binary is a wrapper around Vec<u8> 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<u8>. See also <https://github.com/CosmWasm/cosmwasm/blob/main/docs/MESSAGE_TYPES.md>.",
"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"
Expand Down Expand Up @@ -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": [
{
Expand Down Expand Up @@ -222,8 +222,7 @@
"code_id",
"initial_nfts",
"label",
"name",
"symbol"
"msg"
],
"properties": {
"code_id": {
Expand All @@ -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
Expand All @@ -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"
Expand Down
92 changes: 65 additions & 27 deletions contracts/voting/dao-voting-cw721-staked/src/contract.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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<Binary, StdError> {
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<NftInstantiateMsg, ContractError> {
if let Ok(cw721_msg) = from_binary::<cw721_base::msg::InstantiateMsg>(&instantiate_msg) {
return Ok(NftInstantiateMsg::Cw721(cw721_msg));
}

if let Ok(sg721_msg) = from_binary::<sg721::InstantiateMsg>(&instantiate_msg) {
return Ok(NftInstantiateMsg::Sg721(sg721_msg));
}

Err(ContractError::NftInstantiateError {})
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
Expand Down Expand Up @@ -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 {});
Expand All @@ -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()),
))
}
}
}
Expand Down Expand Up @@ -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<Response, ContractError> {
// 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<Response, ContractError> {
match msg.id {
Expand All @@ -605,14 +650,7 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result<Response, ContractE
Ok(SubMsg::new(WasmMsg::Execute {
contract_addr: nft_contract.clone(),
funds: vec![],
msg: to_binary(
&cw721_base::msg::ExecuteMsg::<Empty, Empty>::Mint {
token_id: nft.token_id.clone(),
owner: nft.owner.clone(),
token_uri: nft.token_uri.clone(),
extension: Empty {},
},
)?,
msg: nft.clone(),
}))
})
.collect::<Vec<SubMsg>>();
Expand Down
4 changes: 2 additions & 2 deletions contracts/voting/dao-voting-cw721-staked/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
Loading

0 comments on commit 7756251

Please sign in to comment.