diff --git a/Cargo.lock b/Cargo.lock index 46d177b..703c280 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -401,6 +401,21 @@ dependencies = [ "cosmwasm-std", ] +[[package]] +name = "cw-controllers" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57de8d3761e46be863e3ac1eba8c8a976362a48c6abf240df1e26c3e421ee9e8" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "cw-ibc-lite-derive" version = "0.1.0" @@ -445,6 +460,25 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cw-ibc-lite-ics20-transfer" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-ibc-lite-ics02-client", + "cw-ibc-lite-shared", + "cw-storage-plus", + "cw2", + "cw20", + "cw20-ics20", + "ibc-client-cw", + "ibc-core-host", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "cw-ibc-lite-ics26-router" version = "0.1.0" @@ -547,6 +581,38 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cw20" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "526e39bb20534e25a1cd0386727f0038f4da294e5e535729ba3ef54055246abd" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils", + "schemars", + "serde", +] + +[[package]] +name = "cw20-ics20" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76221201da08fed611c857ea3aa21c031a4a7dc771a8b1750559ca987335dc02" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-storage-plus", + "cw-utils", + "cw2", + "cw20", + "schemars", + "semver", + "serde", + "thiserror", +] + [[package]] name = "der" version = "0.7.9" diff --git a/Cargo.toml b/Cargo.toml index c6841c1..ec45b25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,8 @@ cosmwasm-std = { version = "1.5.5", features = [ ] } cw-storage-plus = "1.2.0" cw2 = "1.1.2" +cw20 = "1.1.2" +cw20-ics20 = "1.1.2" schemars = "0.8.16" serde = { version = "1.0.197", default-features = false, features = ["derive"] } thiserror = { version = "1.0.58" } diff --git a/contracts/ics07-tendermint/src/contract.rs b/contracts/ics07-tendermint/src/contract.rs index 987daf5..125a620 100644 --- a/contracts/ics07-tendermint/src/contract.rs +++ b/contracts/ics07-tendermint/src/contract.rs @@ -75,8 +75,9 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result<Binary, ContractErro query::check_for_misbehaviour(deps, env, msg.try_into()?) } QueryMsg::VerifyMembership(_) | QueryMsg::VerifyNonMembership(_) => { - query::execute_query(deps, env, msg.try_into()?) + query::sudo_query(deps, env, msg.try_into()?) } + QueryMsg::Ownership {} => query::ownership(deps), } } @@ -145,7 +146,7 @@ mod query { } #[allow(clippy::needless_pass_by_value, clippy::module_name_repetitions)] - pub fn execute_query( + pub fn sudo_query( deps: Deps, env: Env, msg: ibc_client_cw::types::SudoMsg, @@ -156,4 +157,10 @@ mod query { ctx.sudo(msg).map_err(ContractError::from) } + + pub fn ownership(deps: Deps) -> Result<Binary, ContractError> { + Ok(cosmwasm_std::to_json_binary(&cw_ownable::get_ownership( + deps.storage, + )?)?) + } } diff --git a/contracts/ics20-transfer/.gitignore b/contracts/ics20-transfer/.gitignore new file mode 100644 index 0000000..9095dea --- /dev/null +++ b/contracts/ics20-transfer/.gitignore @@ -0,0 +1,16 @@ +# Build results +/target +/schema + +# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) +.cargo-ok + +# Text file backups +**/*.rs.bk + +# macOS +.DS_Store + +# IDEs +*.iml +.idea diff --git a/contracts/ics20-transfer/Cargo.toml b/contracts/ics20-transfer/Cargo.toml new file mode 100644 index 0000000..b2a5fbe --- /dev/null +++ b/contracts/ics20-transfer/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "cw-ibc-lite-ics20-transfer" +description = "ICS-20 Transfer application for `cw-ibc-lite`" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +repository = { workspace = true } +license = { workspace = true } + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# exclude export feature to disable all instantiate/execute/query exports +default = ["export"] +export = [] + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +cw20-ics20 = { workspace = true } +cw20 = { workspace = true } +schemars = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } +ibc-core-host = { workspace = true } +cw-ibc-lite-shared = { workspace = true } +cw-ibc-lite-ics02-client = { workspace = true } +ibc-client-cw = { workspace = true } diff --git a/contracts/ics20-transfer/README.md b/contracts/ics20-transfer/README.md new file mode 100644 index 0000000..52f7529 --- /dev/null +++ b/contracts/ics20-transfer/README.md @@ -0,0 +1,3 @@ +# CosmWasm IBC Lite Transfer App + +This is a transfer application for `cw-ibc-lite`. It is based on [cw20-ics20](https://github.com/CosmWasm/cw-plus/tree/main/contracts/cw20-ics20) contract. It currently only works with cw20 tokens. diff --git a/contracts/ics20-transfer/src/bin/schema.rs b/contracts/ics20-transfer/src/bin/schema.rs new file mode 100644 index 0000000..d80ae70 --- /dev/null +++ b/contracts/ics20-transfer/src/bin/schema.rs @@ -0,0 +1,11 @@ +use cosmwasm_schema::write_api; + +use cw_ibc_lite_ics20_transfer::types::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg, + } +} diff --git a/contracts/ics20-transfer/src/contract.rs b/contracts/ics20-transfer/src/contract.rs new file mode 100644 index 0000000..1e8692b --- /dev/null +++ b/contracts/ics20-transfer/src/contract.rs @@ -0,0 +1,100 @@ +//! This module handles the execution logic of the contract. + +use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response}; + +use cw_ibc_lite_shared::types::error::ContractError; + +use crate::types::{ + keys, + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, +}; + +/// Instantiates a new contract. +/// +/// # Errors +/// Will return an error if the instantiation fails. +#[allow(clippy::needless_pass_by_value)] +#[cosmwasm_std::entry_point] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: InstantiateMsg, +) -> Result<Response, ContractError> { + // NOTE: Contract admin is assumed to be the ics26-router contract. + cw2::set_contract_version(deps.storage, keys::CONTRACT_NAME, keys::CONTRACT_VERSION)?; + + todo!() +} + +/// Handles the execution of the contract by routing the messages to the respective handlers. +/// +/// # Errors +/// Will return an error if the handler returns an error. +#[allow(clippy::needless_pass_by_value)] +#[cosmwasm_std::entry_point] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result<Response, ContractError> { + match msg { + ExecuteMsg::Receive(receive_msg) => execute::receive(deps, env, info, receive_msg), + ExecuteMsg::ReceiveIbcAppCallback(callback_msg) => { + execute::receive_ibc_callback(deps, env, info, callback_msg) + } + } +} + +/// Handles the query messages by routing them to the respective handlers. +/// +/// # Errors +/// Will return an error if the handler returns an error. +#[allow(clippy::needless_pass_by_value)] +#[cosmwasm_std::entry_point] +pub fn query(_deps: Deps, _env: Env, _msg: QueryMsg) -> Result<Binary, ContractError> { + todo!() +} + +mod execute { + use cw_ibc_lite_shared::types::{apps::callbacks::IbcAppCallbackMsg, error::TransferError}; + + use crate::types::{msg::TransferMsg, state}; + + use super::{ContractError, DepsMut, Env, MessageInfo, Response}; + + #[allow(clippy::needless_pass_by_value)] + pub fn receive( + _deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: cw20::Cw20ReceiveMsg, + ) -> Result<Response, ContractError> { + if !info.funds.is_empty() { + return Err(TransferError::UnexpectedNativeToken.into()); + } + + // NOTE: We use the sender contract address as the denom. + let _denom = info.sender.as_str(); + let _transfer_msg: TransferMsg = cosmwasm_std::from_json(msg.msg)?; + todo!() + } + + #[allow(clippy::needless_pass_by_value)] + pub fn receive_ibc_callback( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: IbcAppCallbackMsg, + ) -> Result<Response, ContractError> { + state::admin::assert_admin(&env, &deps.querier, &info.sender)?; + + match msg { + IbcAppCallbackMsg::OnSendPacket { .. } => todo!(), + IbcAppCallbackMsg::OnRecvPacket { .. } => todo!(), + IbcAppCallbackMsg::OnAcknowledgementPacket { .. } => todo!(), + IbcAppCallbackMsg::OnTimeoutPacket { .. } => todo!(), + } + } +} diff --git a/contracts/ics20-transfer/src/lib.rs b/contracts/ics20-transfer/src/lib.rs new file mode 100644 index 0000000..f260b20 --- /dev/null +++ b/contracts/ics20-transfer/src/lib.rs @@ -0,0 +1,7 @@ +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] +#![deny(clippy::nursery, clippy::pedantic, warnings)] + +#[cfg(feature = "export")] +pub mod contract; +pub mod types; diff --git a/contracts/ics20-transfer/src/types/events.rs b/contracts/ics20-transfer/src/types/events.rs new file mode 100644 index 0000000..870c4d2 --- /dev/null +++ b/contracts/ics20-transfer/src/types/events.rs @@ -0,0 +1 @@ +//! `cw-ibc-lite-ics20-transfer` Event Keys diff --git a/contracts/ics20-transfer/src/types/keys.rs b/contracts/ics20-transfer/src/types/keys.rs new file mode 100644 index 0000000..4d6da32 --- /dev/null +++ b/contracts/ics20-transfer/src/types/keys.rs @@ -0,0 +1,12 @@ +//! # Keys +//! +//! Contains key constants definitions for the contract such as version info for migrations. + +/// `CONTRACT_NAME` is the name of the contract recorded with [`cw2`] +pub const CONTRACT_NAME: &str = "crates.io:cw-ibc-lite-ics20-transfer"; +/// `CONTRACT_VERSION` is the version of the cargo package. +/// This is also the version of the contract recorded in [`cw2`] +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// `ICS20_VERSION` is the version of the ICS20 module used in the contract. +pub const ICS20_VERSION: &str = "ics20-1"; diff --git a/contracts/ics20-transfer/src/types/mod.rs b/contracts/ics20-transfer/src/types/mod.rs new file mode 100644 index 0000000..9116a4a --- /dev/null +++ b/contracts/ics20-transfer/src/types/mod.rs @@ -0,0 +1,7 @@ +//! This module contains the types used by the contract's execution and state logic. + +pub mod events; +pub mod keys; +#[allow(clippy::module_name_repetitions)] +pub mod msg; +pub mod state; diff --git a/contracts/ics20-transfer/src/types/msg.rs b/contracts/ics20-transfer/src/types/msg.rs new file mode 100644 index 0000000..eee50e5 --- /dev/null +++ b/contracts/ics20-transfer/src/types/msg.rs @@ -0,0 +1,42 @@ +//! # Messages +//! +//! This module defines the messages that this contract receives. + +use cosmwasm_schema::{cw_serde, QueryResponses}; + +use cw_ibc_lite_shared::types::apps::helpers::ibc_lite_app_callback; + +/// The message to instantiate the contract. +#[cw_serde] +pub struct InstantiateMsg {} + +/// The execute messages supported by the contract. +#[ibc_lite_app_callback] +#[cw_serde] +pub enum ExecuteMsg { + /// This accepts a properly-encoded ReceiveMsg from a cw20 contract + /// The wrapped message is expected to be [`TransferMsg`]. + Receive(cw20::Cw20ReceiveMsg), +} + +/// This is the message we accept via [`ExecuteMsg::Receive`]. +#[cw_serde] +pub struct TransferMsg { + /// The local channel to send the packets on + pub source_channel: String, + /// The remote address to send to. + /// Don't use HumanAddress as this will likely have a different Bech32 prefix than we use + /// and cannot be validated locally + pub receiver: String, + /// How long the packet lives in seconds. If not specified, use default_timeout + #[serde(skip_serializing_if = "Option::is_none")] + pub timeout: Option<u64>, + /// An optional memo to add to the IBC transfer + #[serde(skip_serializing_if = "Option::is_none")] + pub memo: Option<String>, +} + +/// The query messages supported by the contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg {} diff --git a/contracts/ics20-transfer/src/types/state.rs b/contracts/ics20-transfer/src/types/state.rs new file mode 100644 index 0000000..c1a5e46 --- /dev/null +++ b/contracts/ics20-transfer/src/types/state.rs @@ -0,0 +1,30 @@ +//! This module defines the state storage of the Contract. + +/// A collection of methods to access the admin of the contract. +pub mod admin { + use cosmwasm_std::{Addr, Env, QuerierWrapper}; + use cw_ibc_lite_shared::types::error::ContractError; + + /// Asserts that the given address is the admin of the contract. + /// + /// # Errors + /// Returns an error if the given address is not the admin of the contract or the contract + /// doesn't have an admin. + #[allow(clippy::module_name_repetitions)] + pub fn assert_admin( + env: &Env, + querier: &QuerierWrapper, + addr: &Addr, + ) -> Result<(), ContractError> { + let admin = querier + .query_wasm_contract_info(&env.contract.address)? + .admin + .ok_or(ContractError::Unauthorized)?; + + if admin != addr.as_str() { + return Err(ContractError::Unauthorized); + } + + Ok(()) + } +} diff --git a/packages/derive/README.md b/packages/derive/README.md index 8da03dc..6de59c3 100644 --- a/packages/derive/README.md +++ b/packages/derive/README.md @@ -11,7 +11,7 @@ message enum variant into their `ExecuteMsg` enum. ```rust use cosmwasm_schema::{cw_serde, QueryResponses}; -use cw_ibc_lite_shared::types::apps::callbacks::ibc_lite_callback; +use cw_ibc_lite_shared::types::apps::helpers::ibc_lite_callback; #[cw_serde] pub struct InstantiateMsg {} diff --git a/packages/derive/src/lib.rs b/packages/derive/src/lib.rs index 42948bf..fa48f87 100644 --- a/packages/derive/src/lib.rs +++ b/packages/derive/src/lib.rs @@ -13,7 +13,7 @@ use syn::{parse_macro_input, AttributeArgs, DataEnum, DeriveInput}; /// For example: /// /// ``` -/// use cw_ibc_lite_shared::types::apps::callbacks::ibc_lite_app_callback; +/// use cw_ibc_lite_shared::types::apps::helpers::ibc_lite_app_callback; /// use cosmwasm_schema::cw_serde; /// /// #[ibc_lite_app_callback] @@ -35,7 +35,7 @@ use syn::{parse_macro_input, AttributeArgs, DataEnum, DeriveInput}; /// occurs before the addition of the field. /// /// ```compile_fail -/// use cw_ibc_lite_shared::types::apps::callbacks::ibc_lite_app_callback; +/// use cw_ibc_lite_shared::types::apps::helpers::ibc_lite_app_callback; /// use cosmwasm_schema::cw_serde; /// /// #[derive(Clone)] diff --git a/packages/shared/src/types/apps/callbacks.rs b/packages/shared/src/types/apps/callbacks.rs index 5d30981..5a0aeec 100644 --- a/packages/shared/src/types/apps/callbacks.rs +++ b/packages/shared/src/types/apps/callbacks.rs @@ -6,9 +6,6 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Binary, StdResult}; -// Export the derive macro -pub use cw_ibc_lite_derive::ibc_lite_app_callback; - /// All IBC applications built with `cw-ibc-lite` must handle these callback messages. #[cw_serde] pub enum IbcAppCallbackMsg { diff --git a/packages/shared/src/types/apps/helpers.rs b/packages/shared/src/types/apps/helpers.rs index 47212d1..049f0ae 100644 --- a/packages/shared/src/types/apps/helpers.rs +++ b/packages/shared/src/types/apps/helpers.rs @@ -6,6 +6,9 @@ use super::callbacks::{self}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +// Export the derive macro +pub use cw_ibc_lite_derive::ibc_lite_app_callback; + use cosmwasm_std::{Addr, CosmosMsg, StdResult, WasmMsg}; /// `IbcApplicationContract` is a wrapper around Addr that provides helpers diff --git a/packages/shared/src/types/clients/msg.rs b/packages/shared/src/types/clients/msg.rs index b9f130d..32509a8 100644 --- a/packages/shared/src/types/clients/msg.rs +++ b/packages/shared/src/types/clients/msg.rs @@ -31,6 +31,7 @@ pub enum ExecuteMsg { } /// Query messages supported by all light client contracts in ibc-lite +#[cw_ownable::cw_ownable_query] #[derive(QueryResponses)] #[cw_serde] pub enum QueryMsg { diff --git a/packages/shared/src/types/error.rs b/packages/shared/src/types/error.rs index 8e3c23b..e2643fa 100644 --- a/packages/shared/src/types/error.rs +++ b/packages/shared/src/types/error.rs @@ -18,6 +18,8 @@ pub enum ContractError { UTF8Error(#[from] std::str::Utf8Error), #[error("{0}")] IdentifierError(#[from] ibc_core_host::types::error::IdentifierError), + #[error("{0}")] + TransferError(#[from] TransferError), #[error("unauthorized")] Unauthorized, @@ -64,6 +66,15 @@ pub enum ContractError { RecvPacketCallbackNoResponse, } +/// `TransferError` is the error type returned by the ics20 transfer contract. +#[allow(missing_docs, clippy::module_name_repetitions)] +#[non_exhaustive] +#[derive(Error, Debug)] +pub enum TransferError { + #[error("unexpected native token")] + UnexpectedNativeToken, +} + impl ContractError { /// Returns a new [`ContractError::NotFound`] with the given type name and key. #[must_use]