diff --git a/contracts/cw721-proxy/Cargo.toml b/contracts/cw721-proxy/Cargo.toml new file mode 100644 index 00000000..6dcae52b --- /dev/null +++ b/contracts/cw721-proxy/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "cw721-proxy" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# enable feature if you want to disable entry points +library = [] + +[dependencies] +cosmwasm-schema = "^1.5" +cosmwasm-std = "^1.5" +cw2 = "^1.1" +serde = { workspace = true } +cw721 = {git = "https://github.com/public-awesome/cw-nfts.git", version = "0.19.0"} +cw-ownable = "2.1.0" +cw-storage-plus = "^1.1" +thiserror = "1.0.64" \ No newline at end of file diff --git a/contracts/cw721-proxy/src/contract.rs b/contracts/cw721-proxy/src/contract.rs new file mode 100644 index 00000000..16300bb3 --- /dev/null +++ b/contracts/cw721-proxy/src/contract.rs @@ -0,0 +1,41 @@ +use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use crate::state::DefaultCw721ProxyContract; +use crate::{ContractError, CONTRACT_NAME, CONTRACT_VERSION}; +use cosmwasm_std::entry_point; +use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response}; +pub use cw721::*; +pub use cw_ownable::{Action, Ownership, OwnershipError}; + +#[entry_point] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + let contract = DefaultCw721ProxyContract::default(); + contract.instantiate_with_version(deps, &env, &info, msg, CONTRACT_NAME, CONTRACT_VERSION) +} + +#[entry_point] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + let contract = DefaultCw721ProxyContract::default(); + contract.execute(deps, env, info, msg) +} + +#[entry_point] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { + let contract = DefaultCw721ProxyContract::default(); + contract.query(deps, env, msg) +} + +#[entry_point] +pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Result { + let contract = DefaultCw721ProxyContract::default(); + contract.migrate(deps, env, msg, CONTRACT_NAME, CONTRACT_VERSION) +} diff --git a/contracts/cw721-proxy/src/error.rs b/contracts/cw721-proxy/src/error.rs new file mode 100644 index 00000000..1957be0d --- /dev/null +++ b/contracts/cw721-proxy/src/error.rs @@ -0,0 +1,21 @@ +#[derive(Debug, thiserror::Error)] +pub enum ContractError { + #[error(transparent)] + Std(#[from] cosmwasm_std::StdError), + + #[error(transparent)] + Cw721(#[from] cw721::error::Cw721ContractError), + + // #[error(transparent)] + // Encode(#[from] cosmos_sdk_proto::prost::EncodeError), + // + // #[error(transparent)] + // Decode(#[from] cosmos_sdk_proto::prost::DecodeError), + #[error("unauthorized")] + Unauthorized, + + #[error("invalid_msg_type")] + InvalidMsgType, +} + +pub type ContractResult = Result; diff --git a/contracts/cw721-proxy/src/execute.rs b/contracts/cw721-proxy/src/execute.rs new file mode 100644 index 00000000..7b4eeac2 --- /dev/null +++ b/contracts/cw721-proxy/src/execute.rs @@ -0,0 +1,77 @@ +use crate::msg::{get_inner, ExecuteMsg, InnerExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use crate::state::DefaultCw721ProxyContract; +use crate::ContractError; +use crate::ContractError::Unauthorized; +use cosmwasm_std::{Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response}; +use cw721::traits::{Cw721Execute, Cw721Query}; + +impl DefaultCw721ProxyContract<'static> { + pub fn instantiate_with_version( + &self, + deps: DepsMut, + env: &Env, + info: &MessageInfo, + msg: InstantiateMsg, + contract_name: &str, + contract_version: &str, + ) -> Result, ContractError> { + // set the proxy addr + self.proxy_addr.save(deps.storage, &msg.proxy_addr)?; + + // passthrough the rest + Ok(self.base_contract.instantiate_with_version( + deps, + env, + info, + msg.inner_msg, + contract_name, + contract_version, + )?) + } + + pub fn execute( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, + ) -> Result, ContractError> { + match msg { + ExecuteMsg::UpdateExtension { msg: proxy_msg } => { + let proxy_addr = self.proxy_addr.load(deps.storage)?; + if info.sender.ne(&proxy_addr) { + return Err(Unauthorized); + } + + let new_info = MessageInfo { + sender: proxy_msg.sender, + funds: info.clone().funds, + }; + Ok(self + .base_contract + .execute(deps, &env, &new_info, proxy_msg.msg)?) + } + _ => { + let inner_msg: InnerExecuteMsg = get_inner(msg)?; + Ok(self.base_contract.execute(deps, &env, &info, inner_msg)?) + } + } + } + + pub fn query(&self, deps: Deps, env: Env, msg: QueryMsg) -> Result { + Ok(self.base_contract.query(deps, &env, msg)?) + } + + pub fn migrate( + &self, + deps: DepsMut, + env: Env, + msg: MigrateMsg, + contract_name: &str, + contract_version: &str, + ) -> Result { + Ok(self + .base_contract + .migrate(deps, env, msg, contract_name, contract_version)?) + } +} diff --git a/contracts/cw721-proxy/src/lib.rs b/contracts/cw721-proxy/src/lib.rs new file mode 100644 index 00000000..956488eb --- /dev/null +++ b/contracts/cw721-proxy/src/lib.rs @@ -0,0 +1,11 @@ +#[cfg(not(feature = "library"))] +pub mod contract; +mod error; +mod execute; +pub mod msg; +mod state; + +pub use crate::error::ContractError; + +pub const CONTRACT_NAME: &str = "cw721-proxy"; +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/contracts/cw721-proxy/src/msg.rs b/contracts/cw721-proxy/src/msg.rs new file mode 100644 index 00000000..0c21c88d --- /dev/null +++ b/contracts/cw721-proxy/src/msg.rs @@ -0,0 +1,103 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Empty}; + +use crate::ContractError; +use crate::ContractError::InvalidMsgType; +use cw721::{ + msg::{Cw721ExecuteMsg, Cw721InstantiateMsg, Cw721MigrateMsg, Cw721QueryMsg}, + DefaultOptionalCollectionExtension, DefaultOptionalCollectionExtensionMsg, + EmptyOptionalNftExtension, EmptyOptionalNftExtensionMsg, +}; + +#[cw_serde] +pub struct InstantiateMsg { + pub proxy_addr: Addr, + + pub inner_msg: Cw721InstantiateMsg, +} + +pub type ExecuteMsg = + Cw721ExecuteMsg; +// pub type InstantiateMsg = Cw721InstantiateMsg; +pub type MigrateMsg = Cw721MigrateMsg; +pub type QueryMsg = + Cw721QueryMsg; + +pub type InnerExecuteMsg = + Cw721ExecuteMsg; + +#[cw_serde] +pub struct ProxyMsg { + pub sender: Addr, + pub msg: InnerExecuteMsg, +} + +pub fn get_inner(msg: ExecuteMsg) -> Result { + match msg { + ExecuteMsg::UpdateOwnership(_0) => Ok(InnerExecuteMsg::UpdateCreatorOwnership(_0)), + ExecuteMsg::UpdateMinterOwnership(_0) => Ok(InnerExecuteMsg::UpdateCreatorOwnership(_0)), + ExecuteMsg::UpdateCreatorOwnership(_0) => Ok(InnerExecuteMsg::UpdateCreatorOwnership(_0)), + ExecuteMsg::UpdateCollectionInfo { collection_info } => { + Ok(InnerExecuteMsg::UpdateCollectionInfo { collection_info }) + } + ExecuteMsg::TransferNft { + recipient, + token_id, + } => Ok(InnerExecuteMsg::TransferNft { + recipient, + token_id, + }), + ExecuteMsg::SendNft { + contract, + token_id, + msg, + } => Ok(InnerExecuteMsg::SendNft { + contract, + token_id, + msg, + }), + ExecuteMsg::Approve { + spender, + token_id, + expires, + } => Ok(InnerExecuteMsg::Approve { + spender, + token_id, + expires, + }), + ExecuteMsg::Revoke { spender, token_id } => { + Ok(InnerExecuteMsg::Revoke { spender, token_id }) + } + ExecuteMsg::ApproveAll { operator, expires } => { + Ok(InnerExecuteMsg::ApproveAll { operator, expires }) + } + ExecuteMsg::RevokeAll { operator } => Ok(InnerExecuteMsg::RevokeAll { operator }), + ExecuteMsg::Mint { + token_id, + owner, + token_uri, + extension, + } => Ok(InnerExecuteMsg::Mint { + token_id, + owner, + token_uri, + extension, + }), + ExecuteMsg::Burn { token_id } => Ok(InnerExecuteMsg::Burn { token_id }), + ExecuteMsg::UpdateExtension { .. } => Err(InvalidMsgType), // cannot convert a proxy msg into an inner msg + ExecuteMsg::UpdateNftInfo { + token_id, + token_uri, + extension, + } => Ok(InnerExecuteMsg::UpdateNftInfo { + token_id, + token_uri, + extension, + }), + ExecuteMsg::SetWithdrawAddress { address } => { + Ok(InnerExecuteMsg::SetWithdrawAddress { address }) + } + ExecuteMsg::RemoveWithdrawAddress {} => Ok(InnerExecuteMsg::RemoveWithdrawAddress {}), + ExecuteMsg::WithdrawFunds { amount } => Ok(InnerExecuteMsg::WithdrawFunds { amount }), + } +} diff --git a/contracts/cw721-proxy/src/state.rs b/contracts/cw721-proxy/src/state.rs new file mode 100644 index 00000000..a286ad61 --- /dev/null +++ b/contracts/cw721-proxy/src/state.rs @@ -0,0 +1,17 @@ +use cosmwasm_std::Addr; +use cw721::extension::Cw721BaseExtensions; +use cw_storage_plus::Item; + +pub struct DefaultCw721ProxyContract<'a> { + pub proxy_addr: Item<'a, Addr>, + pub base_contract: Cw721BaseExtensions<'a>, +} + +impl Default for DefaultCw721ProxyContract<'static> { + fn default() -> Self { + Self { + proxy_addr: Item::new("proxy_addr"), + base_contract: Cw721BaseExtensions::default(), + } + } +} diff --git a/contracts/proxy/Cargo.toml b/contracts/proxy/Cargo.toml new file mode 100644 index 00000000..1ce45e97 --- /dev/null +++ b/contracts/proxy/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "proxy" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# enable feature if you want to disable entry points +library = [] + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw2 = { workspace = true } +cw-storage-plus = { workspace = true } +thiserror = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +schemars = { workspace = true } +cosmos-sdk-proto = { workspace = true } + +[dev-dependencies] +cw721-proxy = {path = "../cw721-proxy"} +cw721 = {git = "https://github.com/public-awesome/cw-nfts.git", version = "0.19.0"} diff --git a/contracts/proxy/src/contract.rs b/contracts/proxy/src/contract.rs new file mode 100644 index 00000000..0adb1bbd --- /dev/null +++ b/contracts/proxy/src/contract.rs @@ -0,0 +1,30 @@ +use crate::error::ContractResult; +use crate::msg::{ExecuteMsg, InstantiateMsg}; +use crate::{execute, CONTRACT_NAME, CONTRACT_VERSION}; +use cosmwasm_std::{entry_point, DepsMut, Env, MessageInfo, Response}; + +#[entry_point] +pub fn instantiate( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> ContractResult { + cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + execute::init(deps, info, msg.admin, msg.code_ids) +} + +#[entry_point] +pub fn execute( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> ContractResult { + match msg { + ExecuteMsg::ProxyMsgs { msgs } => execute::proxy_msgs(deps, info, msgs), + ExecuteMsg::AddCodeIDs { code_ids } => execute::add_code_ids(deps, info, code_ids), + ExecuteMsg::RemoveCodeIDs { code_ids } => execute::remove_code_ids(deps, info, code_ids), + ExecuteMsg::UpdateAdmin { new_admin } => execute::update_admin(deps, info, new_admin), + } +} diff --git a/contracts/proxy/src/error.rs b/contracts/proxy/src/error.rs new file mode 100644 index 00000000..6515b7f2 --- /dev/null +++ b/contracts/proxy/src/error.rs @@ -0,0 +1,22 @@ +#[derive(Debug, thiserror::Error)] +pub enum ContractError { + #[error(transparent)] + Std(#[from] cosmwasm_std::StdError), + + #[error(transparent)] + Encode(#[from] cosmos_sdk_proto::prost::EncodeError), + + #[error(transparent)] + Decode(#[from] cosmos_sdk_proto::prost::DecodeError), + + #[error("unauthorized")] + Unauthorized, + + #[error("invalid_code_id")] + InvalidCodeID { contract: String, code_id: u64 }, + + #[error("invalid_msg_type")] + InvalidMsgType, +} + +pub type ContractResult = Result; diff --git a/contracts/proxy/src/execute.rs b/contracts/proxy/src/execute.rs new file mode 100644 index 00000000..031c2089 --- /dev/null +++ b/contracts/proxy/src/execute.rs @@ -0,0 +1,166 @@ +use crate::error::ContractError::{InvalidCodeID, InvalidMsgType, Unauthorized}; +use crate::error::ContractResult; +use crate::msg::ProxyMsg; +use crate::state::{ADMIN, CODE_IDS}; +use cosmwasm_std::{to_json_binary, Addr, Deps, DepsMut, Event, MessageInfo, Response, WasmMsg}; + +pub fn init( + deps: DepsMut, + _: MessageInfo, + admin: Option, + code_ids: Vec, +) -> ContractResult { + ADMIN.save(deps.storage, &admin)?; + + for code_id in code_ids.clone() { + CODE_IDS.save(deps.storage, code_id, &true)?; + } + + let admin_str: String = match admin { + None => String::new(), + Some(a) => a.into_string(), + }; + + let code_ids_strs: Vec = code_ids.iter().map(|n| n.to_string()).collect(); + let code_ids_str = code_ids_strs.join(", "); + + Ok(Response::new().add_event( + Event::new("create_proxy_instance") + .add_attributes(vec![("admin", admin_str), ("code_ids", code_ids_str)]), + )) +} + +// main logic: this contract is meant to allow a single address to represent +// multiple or dynamic other contracts. In this case, it is any contract that +// is backed by a particular code ID. The sender sends wrapped msgs of the +// WasmMsg::Execute type, which includes the target contract, and the msg binary. +// This proxy contract will make any necessary in-flight checks (code ID here) +// and then submit new ProxyMsg msgs to the target contract. The receiving +// contract must understand and authenticate such msgs + +pub fn proxy_msgs( + deps: DepsMut, + info: MessageInfo, + msgs: Vec, +) -> ContractResult { + let mut proxy_msgs: Vec = Vec::with_capacity(msgs.len()); + + for msg in msgs { + let (proxy_msg, contract_addr, funds) = match msg { + WasmMsg::Execute { + contract_addr, + msg, + funds, + } => ( + ProxyMsg { + sender: info.sender.clone(), + msg, + }, + contract_addr, + funds, + ), + _ => return Err(InvalidMsgType), + }; + let contract_info = deps + .querier + .query_wasm_contract_info(contract_addr.clone())?; + if !CODE_IDS.has(deps.storage, contract_info.code_id) { + return Err(InvalidCodeID { + contract: contract_addr, + code_id: contract_info.code_id, + }); + } + + let exec_msg = WasmMsg::Execute { + contract_addr, + msg: to_json_binary(&proxy_msg)?, + funds, + }; + proxy_msgs.push(exec_msg); + } + + Ok(Response::new() + .add_event(Event::new("proxied_msgs")) + .add_messages(proxy_msgs)) +} + +// administration msgs + +pub fn is_admin(deps: Deps, address: Addr) -> ContractResult<()> { + let admin = ADMIN.load(deps.storage)?; + match admin { + None => Err(Unauthorized), + Some(a) => { + if a != address { + Err(Unauthorized) + } else { + Ok(()) + } + } + } +} + +pub fn update_admin( + deps: DepsMut, + info: MessageInfo, + new_admin: Option, +) -> ContractResult { + is_admin(deps.as_ref(), info.sender.clone())?; + + ADMIN.save(deps.storage, &new_admin)?; + + let admin_str: String = match new_admin { + None => String::new(), + Some(a) => a.into_string(), + }; + + Ok( + Response::new().add_event(Event::new("updated_treasury_admin").add_attributes(vec![ + ("old admin", info.sender.into_string()), + ("new admin", admin_str), + ])), + ) +} +pub fn add_code_ids( + deps: DepsMut, + info: MessageInfo, + code_ids: Vec, +) -> ContractResult { + is_admin(deps.as_ref(), info.sender.clone())?; + + for code_id in code_ids.clone() { + CODE_IDS.save(deps.storage, code_id, &true)?; + } + + let code_ids_strs: Vec = code_ids.iter().map(|n| n.to_string()).collect(); + let code_ids_str = code_ids_strs.join(", "); + + Ok( + Response::new().add_event(Event::new("updated_proxy_code_ids").add_attributes(vec![ + ("admin", info.sender.as_str()), + ("new_code_ids", code_ids_str.as_str()), + ])), + ) +} + +pub fn remove_code_ids( + deps: DepsMut, + info: MessageInfo, + code_ids: Vec, +) -> ContractResult { + is_admin(deps.as_ref(), info.sender.clone())?; + + for code_id in code_ids.clone() { + CODE_IDS.remove(deps.storage, code_id); + } + + let code_ids_strs: Vec = code_ids.iter().map(|n| n.to_string()).collect(); + let code_ids_str = code_ids_strs.join(", "); + + Ok( + Response::new().add_event(Event::new("updated_proxy_code_ids").add_attributes(vec![ + ("admin", info.sender.as_str()), + ("new_code_ids", code_ids_str.as_str()), + ])), + ) +} diff --git a/contracts/proxy/src/lib.rs b/contracts/proxy/src/lib.rs new file mode 100644 index 00000000..71e293a7 --- /dev/null +++ b/contracts/proxy/src/lib.rs @@ -0,0 +1,9 @@ +#[cfg(not(feature = "library"))] +pub mod contract; +mod error; +mod execute; +mod msg; +mod state; + +pub const CONTRACT_NAME: &str = "proxy"; +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/contracts/proxy/src/msg.rs b/contracts/proxy/src/msg.rs new file mode 100644 index 00000000..f828f458 --- /dev/null +++ b/contracts/proxy/src/msg.rs @@ -0,0 +1,22 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Binary, WasmMsg}; + +#[cw_serde] +pub struct InstantiateMsg { + pub admin: Option, + pub code_ids: Vec, +} + +#[cw_serde] +pub enum ExecuteMsg { + ProxyMsgs { msgs: Vec }, + UpdateAdmin { new_admin: Option }, + AddCodeIDs { code_ids: Vec }, + RemoveCodeIDs { code_ids: Vec }, +} + +#[cw_serde] +pub struct ProxyMsg { + pub sender: Addr, + pub msg: Binary, +} diff --git a/contracts/proxy/src/state.rs b/contracts/proxy/src/state.rs new file mode 100644 index 00000000..3c36fbfa --- /dev/null +++ b/contracts/proxy/src/state.rs @@ -0,0 +1,5 @@ +use cosmwasm_std::Addr; +use cw_storage_plus::{Item, Map}; + +pub const ADMIN: Item> = Item::new("admin"); +pub const CODE_IDS: Map = Map::new("code_ids");