diff --git a/Cargo.lock b/Cargo.lock index 1129c9d063..21a256c44d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3863,45 +3863,6 @@ dependencies = [ "cosmwasm-std 1.5.8", ] -[[package]] -name = "cw-controllers" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c1804013d21060b994dea28a080f9eab78a3bcb6b617f05e7634b0600bf7b1" -dependencies = [ - "cosmwasm-schema 2.1.4", - "cosmwasm-std 2.1.4", - "cw-storage-plus 2.0.0", - "cw-utils 2.0.0", - "schemars", - "serde", - "thiserror", -] - -[[package]] -name = "cw-multi-test" -version = "2.1.0-rc.1" -source = "git+https://github.com/CosmWasm/cw-multi-test.git?rev=e1a2f587c7f9d723444ec93ad8fa48f1d88b65bc#e1a2f587c7f9d723444ec93ad8fa48f1d88b65bc" -dependencies = [ - "anyhow", - "bech32 0.11.0", - "cosmwasm-schema 2.1.4", - "cosmwasm-std 2.1.4", - "cw-storage-plus 2.0.0", - "cw-utils 2.0.0", - "cw20-ics20", - "derivative", - "hex", - "itertools 0.13.0", - "log", - "prost 0.12.6", - "schemars", - "serde", - "serde_json", - "sha2 0.10.8", - "thiserror", -] - [[package]] name = "cw-ownable" version = "0.5.1" @@ -4013,19 +3974,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "cw-utils" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dfee7f12f802431a856984a32bce1cb7da1e6c006b5409e3981035ce562dec" -dependencies = [ - "cosmwasm-schema 2.1.4", - "cosmwasm-std 2.1.4", - "schemars", - "serde", - "thiserror", -] - [[package]] name = "cw2" version = "0.16.0" @@ -4054,53 +4002,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "cw2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b04852cd38f044c0751259d5f78255d07590d136b8a86d4e09efdd7666bd6d27" -dependencies = [ - "cosmwasm-schema 2.1.4", - "cosmwasm-std 2.1.4", - "cw-storage-plus 2.0.0", - "schemars", - "semver 1.0.22", - "serde", - "thiserror", -] - -[[package]] -name = "cw20" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a42212b6bf29bbdda693743697c621894723f35d3db0d5df930be22903d0e27c" -dependencies = [ - "cosmwasm-schema 2.1.4", - "cosmwasm-std 2.1.4", - "cw-utils 2.0.0", - "schemars", - "serde", -] - -[[package]] -name = "cw20-ics20" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80a9e377dbbd1ffb3b6a8a2dbf9128609a6458a3292f88f99e0b6840a7e9762e" -dependencies = [ - "cosmwasm-schema 2.1.4", - "cosmwasm-std 2.1.4", - "cw-controllers", - "cw-storage-plus 2.0.0", - "cw-utils 2.0.0", - "cw2 2.0.0", - "cw20", - "schemars", - "semver 1.0.22", - "serde", - "thiserror", -] - [[package]] name = "cw721" version = "0.16.0" @@ -5591,12 +5492,6 @@ dependencies = [ "unionlabs", ] -[[package]] -name = "go-parse-duration" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "558b88954871f5e5b2af0e62e2e176c8bde7a6c2c4ed41b13d138d96da2e2cbd" - [[package]] name = "group" version = "0.13.0" @@ -6317,6 +6212,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ibc-union-ucs03-zkgm" +version = "1.0.0" +dependencies = [ + "alloy", + "base58 0.2.0", + "cosmwasm-schema 1.5.8", + "cosmwasm-std 1.5.8", + "cw-storage-plus 1.2.0", + "ethabi", + "hex", + "ibc-solidity", + "ibc-union-msg", + "serde", + "serde_json", + "thiserror", + "token-factory-api", + "unionlabs", +] + [[package]] name = "ics008-wasm-client" version = "0.1.0" @@ -12026,8 +11941,8 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" name = "token-factory-api" version = "0.1.0" dependencies = [ - "cosmwasm-schema 2.1.4", - "cosmwasm-std 2.1.4", + "cosmwasm-schema 1.5.8", + "cosmwasm-std 1.5.8", ] [[package]] @@ -12621,46 +12536,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "ucs01-relay" -version = "1.0.1" -dependencies = [ - "base58 0.2.0", - "cosmwasm-schema 2.1.4", - "cosmwasm-std 2.1.4", - "cw-controllers", - "cw-multi-test", - "cw-storage-plus 2.0.0", - "cw2 2.0.0", - "hex", - "ibc-solidity", - "ibc-union-msg", - "prost 0.12.6", - "protos", - "serde", - "serde-json-wasm 1.0.1", - "sha2 0.10.8", - "thiserror", - "token-factory-api", - "ucs01-relay-api", - "unionlabs", -] - -[[package]] -name = "ucs01-relay-api" -version = "0.1.0" -dependencies = [ - "cosmwasm-schema 2.1.4", - "cosmwasm-std 2.1.4", - "ethabi", - "go-parse-duration", - "serde", - "serde-json-wasm 1.0.1", - "serde-utils", - "thiserror", - "unionlabs", -] - [[package]] name = "ucs02-nft" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 2a58d11cd0..33cd8823bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,8 +5,7 @@ members = [ "cosmwasm/token-factory-api", "cosmwasm/ucs00-pingpong", "cosmwasm/ibc-union/app/ucs00-pingpong", - "cosmwasm/ucs01-relay", - "cosmwasm/ucs01-relay-api", + "cosmwasm/ibc-union/app/ucs03-zkgm", "cosmwasm/ucs02-nft", "cosmwasm/multicall", @@ -262,7 +261,6 @@ subset-of = { path = "lib/subset-of", default-features = false } subset-of-derive = { path = "lib/subset-of-derive", default-features = false } token-factory-api = { path = "cosmwasm/token-factory-api", default-features = false } -ucs01-relay-api = { path = "cosmwasm/ucs01-relay-api", default-features = false } unionlabs = { path = "lib/unionlabs", default-features = false } unionlabs-primitives = { path = "lib/unionlabs-primitives", default-features = false } zktrie = { path = "lib/zktrie-rs", default-features = false } diff --git a/cosmwasm/cosmwasm.nix b/cosmwasm/cosmwasm.nix index 7ca7f74788..87cb7990ae 100644 --- a/cosmwasm/cosmwasm.nix +++ b/cosmwasm/cosmwasm.nix @@ -6,12 +6,6 @@ ucs02-nft = crane.buildWasmContract { crateDirFromRoot = "cosmwasm/ucs02-nft"; }; - ucs01-relay = crane.buildWasmContract { - crateDirFromRoot = "cosmwasm/ucs01-relay"; - }; - ucs01-relay-api = crane.buildWorkspaceMember { - crateDirFromRoot = "cosmwasm/ucs01-relay-api"; - }; ucs00-pingpong = crane.buildWasmContract { crateDirFromRoot = "cosmwasm/ucs00-pingpong"; }; @@ -24,6 +18,9 @@ ibc-union = crane.buildWasmContract { crateDirFromRoot = "cosmwasm/ibc-union/core"; }; + ibc-union-ucs03-zkgm = crane.buildWasmContract { + crateDirFromRoot = "cosmwasm/ibc-union/app/ucs03-zkgm"; + }; multicall = crane.buildWasmContract { crateDirFromRoot = "cosmwasm/multicall"; }; @@ -34,10 +31,10 @@ inherit cw721-base; } // ucs02-nft.packages - // ucs01-relay.packages // ucs00-pingpong.packages // ibc-union.packages - // multicall.packages; - checks = ucs02-nft.checks // ucs01-relay.checks // ucs01-relay-api.checks // ucs00-pingpong.checks; + // multicall.packages + // ibc-union-ucs03-zkgm.packages; + checks = ucs02-nft.checks // ucs00-pingpong.checks // ibc-union-ucs03-zkgm.checks; }; } diff --git a/cosmwasm/ibc-union/app/ucs03-zkgm/Cargo.toml b/cosmwasm/ibc-union/app/ucs03-zkgm/Cargo.toml new file mode 100644 index 0000000000..5f244d7380 --- /dev/null +++ b/cosmwasm/ibc-union/app/ucs03-zkgm/Cargo.toml @@ -0,0 +1,34 @@ +[package] +authors = ["Union.fi Labs"] +edition = { workspace = true } +license-file = { workspace = true } +name = "ibc-union-ucs03-zkgm" +repository = "https://github.com/unionlabs/union" +version = "1.0.0" + +[lints] +workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +library = [] + +[dependencies] +alloy = { workspace = true, features = ["sol-types"] } +base58 = { version = "0.2" } +cosmwasm-schema = { version = "1.5" } +cosmwasm-std = { version = "1.5" } +cw-storage-plus = { version = "1.2" } +ethabi = { workspace = true } +ibc-solidity = { workspace = true, features = ["serde"] } +ibc-union-msg = { workspace = true } +serde = { workspace = true, features = ["derive"] } +thiserror = { workspace = true } +token-factory-api = { workspace = true } +unionlabs = { workspace = true, features = ["ethabi"] } + +[dev-dependencies] +hex = { workspace = true } +serde_json = { workspace = true } diff --git a/cosmwasm/ibc-union/app/ucs03-zkgm/src/com.rs b/cosmwasm/ibc-union/app/ucs03-zkgm/src/com.rs new file mode 100644 index 0000000000..0dfa225a15 --- /dev/null +++ b/cosmwasm/ibc-union/app/ucs03-zkgm/src/com.rs @@ -0,0 +1,71 @@ +use alloy::primitives::U256; + +pub const ZKGM_VERSION_0: u8 = 0x00; + +pub const OP_FUNGIBLE_ASSET_ORDER: u8 = 0x03; + +pub const ACK_ERR_ONLY_MAKER: &[u8] = &[0xDE, 0xAD, 0xC0, 0xDE]; + +pub const TAG_ACK_FAILURE: U256 = U256::ZERO; +pub const TAG_ACK_SUCCESS: U256 = U256::from_be_slice(&[1]); + +pub const FILL_TYPE_PROTOCOL: U256 = U256::from_be_slice(&[0xB0, 0xCA, 0xD0]); +pub const FILL_TYPE_MARKETMAKER: U256 = U256::from_be_slice(&[0xD1, 0xCE, 0xC4, 0x5E]); + +alloy::sol! { + struct ZkgmPacket { + bytes32 salt; + uint256 path; + Instruction instruction; + } + + struct Instruction { + uint8 version; + uint8 opcode; + bytes operand; + } + + struct Forward { + uint32 channel_id; + uint64 timeout_height; + uint64 timeout_timestamp; + Instruction instruction; + } + + struct Multiplex { + bytes sender; + bool eureka; + bytes contract_address; + bytes contract_calldata; + } + + struct Batch { + Instruction[] instructions; + } + + struct FungibleAssetOrder { + bytes sender; + bytes receiver; + bytes base_token; + uint256 base_amount; + string base_token_symbol; + string base_token_name; + uint256 base_token_path; + bytes quote_token; + uint256 quote_amount; + } + + struct Ack { + uint256 tag; + bytes inner_ack; + } + + struct BatchAck { + bytes[] acknowledgements; + } + + struct FungibleAssetOrderAck { + uint256 fill_type; + bytes market_maker; + } +} diff --git a/cosmwasm/ibc-union/app/ucs03-zkgm/src/contract.rs b/cosmwasm/ibc-union/app/ucs03-zkgm/src/contract.rs new file mode 100644 index 0000000000..a6b9c87a69 --- /dev/null +++ b/cosmwasm/ibc-union/app/ucs03-zkgm/src/contract.rs @@ -0,0 +1,804 @@ +use core::str; + +use alloy::sol_types::SolValue; +use base58::ToBase58; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_json_string, wasm_execute, Addr, BankMsg, Coin, CosmosMsg, DepsMut, Env, MessageInfo, + QueryRequest, Reply, Response, StdError, SubMsg, SubMsgResult, Uint128, Uint256, +}; +use ibc_solidity::Packet; +use ibc_union_msg::{ + module::IbcUnionMsg, + msg::{MsgSendPacket, MsgWriteAcknowledgement}, +}; +use token_factory_api::{Metadata, MetadataResponse, TokenFactoryMsg, TokenFactoryQuery}; +use unionlabs::{ + ethereum::keccak256, + primitives::{Bytes, H256}, +}; + +use crate::{ + com::{ + Ack, FungibleAssetOrder, FungibleAssetOrderAck, Instruction, ZkgmPacket, + ACK_ERR_ONLY_MAKER, FILL_TYPE_MARKETMAKER, FILL_TYPE_PROTOCOL, OP_FUNGIBLE_ASSET_ORDER, + TAG_ACK_FAILURE, TAG_ACK_SUCCESS, ZKGM_VERSION_0, + }, + msg::{ExecuteMsg, InitMsg, MigrateMsg}, + state::{ + CHANNEL_BALANCE, CONFIG, EXECUTING_PACKET, EXECUTION_ACK, HASH_TO_FOREIGN_TOKEN, + TOKEN_ORIGIN, + }, + ContractError, +}; + +pub const PROTOCOL_VERSION: &str = "ucs03-zkgm-0"; + +pub const REPLY_ID: u64 = 0x1337; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InitMsg, +) -> Result { + CONFIG.save(deps.storage, &msg.config)?; + Ok(Response::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(_: DepsMut, _: Env, _: MigrateMsg) -> Result { + Ok(Response::new()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result, ContractError> { + match msg { + ExecuteMsg::IbcUnionMsg(ibc_msg) => { + let ibc_host = CONFIG.load(deps.storage)?.ibc_host; + if info.sender != ibc_host { + return Err(ContractError::OnlyIBCHost); + } + match ibc_msg { + IbcUnionMsg::OnChannelOpenInit { version, .. } => { + enforce_version(&version, None)?; + Ok(Response::default()) + } + IbcUnionMsg::OnChannelOpenTry { + version, + counterparty_version, + .. + } => { + enforce_version(&version, Some(&counterparty_version))?; + Ok(Response::default()) + } + IbcUnionMsg::OnRecvPacket { + packet, + relayer, + relayer_msg, + } => { + let relayer = deps.api.addr_validate(&relayer)?; + if EXECUTING_PACKET.exists(deps.storage) { + Err(ContractError::AlreadyExecuting) + } else { + EXECUTING_PACKET.save(deps.storage, &packet)?; + Ok(Response::default().add_submessage(SubMsg::reply_always( + wasm_execute( + env.contract.address, + &ExecuteMsg::ExecutePacket { + packet, + relayer, + relayer_msg, + }, + vec![], + )?, + REPLY_ID, + ))) + } + } + IbcUnionMsg::OnAcknowledgementPacket { + packet, + acknowledgement, + relayer, + } => { + let relayer = deps.api.addr_validate(&relayer)?; + acknowledge_packet(deps, env, info, packet, relayer, acknowledgement) + } + IbcUnionMsg::OnTimeoutPacket { packet, relayer } => { + let relayer = deps.api.addr_validate(&relayer)?; + timeout_packet(deps, env, info, packet, relayer) + } + IbcUnionMsg::OnChannelCloseInit { .. } + | IbcUnionMsg::OnChannelCloseConfirm { .. } => { + Err(StdError::generic_err("the show must go on").into()) + } + x => Err( + StdError::generic_err(format!("not handled: {}", to_json_string(&x)?)).into(), + ), + } + } + ExecuteMsg::BatchExecute { msgs } => { + if info.sender != env.contract.address { + Err(ContractError::OnlySelf) + } else { + Ok(Response::default().add_messages(msgs)) + } + } + ExecuteMsg::ExecutePacket { + packet, + relayer, + relayer_msg, + } => { + if info.sender != env.contract.address { + Err(ContractError::OnlySelf) + } else { + execute_packet(deps, env, info, packet, relayer, relayer_msg) + } + } + ExecuteMsg::Transfer { + channel_id, + receiver, + base_token, + base_amount, + quote_token, + quote_amount, + timeout_height, + timeout_timestamp, + salt, + } => transfer( + deps, + env, + info, + channel_id, + receiver, + base_token, + base_amount, + quote_token, + quote_amount, + timeout_height, + timeout_timestamp, + salt, + ), + } +} + +fn enforce_version(version: &str, counterparty_version: Option<&str>) -> Result<(), ContractError> { + if version != PROTOCOL_VERSION { + return Err(ContractError::InvalidIbcVersion { + version: version.to_string(), + }); + } + if let Some(version) = counterparty_version { + if version != PROTOCOL_VERSION { + return Err(ContractError::InvalidIbcVersion { + version: version.to_string(), + }); + } + } + Ok(()) +} + +fn timeout_packet( + deps: DepsMut, + env: Env, + info: MessageInfo, + packet: Packet, + relayer: Addr, +) -> Result, ContractError> { + let zkgm_packet = ZkgmPacket::abi_decode_params(&packet.data, true)?; + timeout_internal( + deps, + env, + info, + packet, + relayer, + zkgm_packet.salt.into(), + zkgm_packet.path, + zkgm_packet.instruction, + ) +} + +fn timeout_internal( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + packet: Packet, + _relayer: Addr, + _salt: H256, + _path: alloy::primitives::U256, + instruction: Instruction, +) -> Result, ContractError> { + if instruction.version != ZKGM_VERSION_0 { + return Err(ContractError::UnsupportedVersion { + version: instruction.version, + }); + } + match instruction.opcode { + OP_FUNGIBLE_ASSET_ORDER => { + let order = FungibleAssetOrder::abi_decode_params(&instruction.operand, true)?; + refund(deps, packet.source_channel, order) + } + _ => { + return Err(ContractError::UnknownOpcode { + opcode: instruction.opcode, + }) + } + } +} + +fn acknowledge_packet( + deps: DepsMut, + env: Env, + info: MessageInfo, + packet: Packet, + relayer: Addr, + ack: Bytes, +) -> Result, ContractError> { + let zkgm_packet = ZkgmPacket::abi_decode_params(&packet.data, true)?; + acknowledge_internal( + deps, + env, + info, + packet, + relayer, + zkgm_packet.salt.into(), + zkgm_packet.path, + zkgm_packet.instruction, + ack, + ) +} + +fn acknowledge_internal( + deps: DepsMut, + env: Env, + info: MessageInfo, + packet: Packet, + relayer: Addr, + salt: H256, + path: alloy::primitives::U256, + instruction: Instruction, + ack: Bytes, +) -> Result, ContractError> { + if instruction.version != ZKGM_VERSION_0 { + return Err(ContractError::UnsupportedVersion { + version: instruction.version, + }); + } + let ack = Ack::abi_decode_params(&ack, true)?; + match instruction.opcode { + OP_FUNGIBLE_ASSET_ORDER => { + let order = FungibleAssetOrder::abi_decode_params(&instruction.operand, true)?; + let order_ack = if ack.tag == TAG_ACK_SUCCESS { + Some(FungibleAssetOrderAck::abi_decode_params( + &ack.inner_ack, + true, + )?) + } else { + None + }; + acknowledge_fungible_asset_order( + deps, env, info, packet, relayer, salt, path, order, order_ack, + ) + } + _ => { + return Err(ContractError::UnknownOpcode { + opcode: instruction.opcode, + }) + } + } +} + +fn refund( + deps: DepsMut, + source_channel: u32, + order: FungibleAssetOrder, +) -> Result, ContractError> { + let sender = deps + .api + .addr_validate(str::from_utf8(&order.sender).map_err(|_| ContractError::InvalidSender)?) + .map_err(|_| ContractError::UnableToValidateSender)?; + let base_amount = + u128::try_from(order.base_amount).map_err(|_| ContractError::AmountOverflow)?; + let base_denom = String::from_utf8(order.base_token.to_vec()) + .map_err(|_| ContractError::InvalidBaseToken)?; + let mut messages = Vec::>::new(); + // TODO: handle forward path + if order.base_token_path == source_channel.try_into().unwrap() { + messages.push( + TokenFactoryMsg::MintTokens { + denom: base_denom, + amount: base_amount.into(), + mint_to_address: sender.into_string(), + } + .into(), + ); + } else { + messages.push( + BankMsg::Send { + to_address: sender.into_string(), + amount: vec![Coin { + denom: base_denom, + amount: base_amount.into(), + }], + } + .into(), + ); + } + Ok(Response::new().add_messages(messages)) +} + +fn acknowledge_fungible_asset_order( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + packet: Packet, + _relayer: Addr, + _salt: H256, + _path: alloy::primitives::U256, + order: FungibleAssetOrder, + order_ack: Option, +) -> Result, ContractError> { + match order_ack { + Some(successful_ack) => { + let mut messages = Vec::>::new(); + match successful_ack.fill_type { + FILL_TYPE_PROTOCOL => { + // Protocol filled, fee was paid on destination to the relayer. + } + FILL_TYPE_MARKETMAKER => { + // A market maker filled, we pay (unescrow|mint) with the base asset. + let base_amount = u128::try_from(order.base_amount) + .map_err(|_| ContractError::AmountOverflow)?; + let market_maker = deps + .api + .addr_validate( + str::from_utf8(successful_ack.market_maker.as_ref()) + .map_err(|_| ContractError::InvalidReceiver)?, + ) + .map_err(|_| ContractError::UnableToValidateMarketMaker)?; + let base_denom = String::from_utf8(order.base_token.to_vec()) + .map_err(|_| ContractError::InvalidBaseToken)?; + // TODO: handle forward path + if order.base_token_path == packet.source_channel.try_into().unwrap() { + messages.push( + TokenFactoryMsg::MintTokens { + denom: base_denom, + amount: base_amount.into(), + mint_to_address: market_maker.into_string(), + } + .into(), + ); + } else { + messages.push( + BankMsg::Send { + to_address: market_maker.into_string(), + amount: vec![Coin { + denom: base_denom, + amount: base_amount.into(), + }], + } + .into(), + ); + } + } + _ => return Err(StdError::generic_err("unknown fill_type, impossible?").into()), + } + Ok(Response::new().add_messages(messages)) + } + // Transfer failed, refund + None => refund(deps, packet.source_channel, order), + } +} + +fn execute_packet( + deps: DepsMut, + env: Env, + info: MessageInfo, + packet: Packet, + relayer: Addr, + relayer_msg: Bytes, +) -> Result, ContractError> { + let zkgm_packet = ZkgmPacket::abi_decode_params(&packet.data, true)?; + execute_internal( + deps, + env, + info, + packet, + relayer, + relayer_msg, + zkgm_packet.salt.into(), + zkgm_packet.path, + zkgm_packet.instruction, + ) +} + +fn execute_internal( + deps: DepsMut, + env: Env, + info: MessageInfo, + packet: Packet, + relayer: Addr, + relayer_msg: Bytes, + salt: H256, + path: alloy::primitives::U256, + instruction: Instruction, +) -> Result, ContractError> { + if instruction.version != ZKGM_VERSION_0 { + return Err(ContractError::UnsupportedVersion { + version: instruction.version, + }); + } + match instruction.opcode { + OP_FUNGIBLE_ASSET_ORDER => { + let order = FungibleAssetOrder::abi_decode_params(&instruction.operand, true)?; + execute_fungible_asset_order( + deps, + env, + info, + packet, + relayer, + relayer_msg, + salt, + path, + order, + ) + } + _ => { + return Err(ContractError::UnknownOpcode { + opcode: instruction.opcode, + }) + } + } +} + +fn factory_denom(token: &str, contract: &str) -> String { + format!("factory/{}/{}", contract, token) +} + +fn predict_wrapped_denom(path: alloy::primitives::U256, channel: u32, token: Bytes) -> String { + // TokenFactory denom name limit + const MAX_DENOM_LENGTH: usize = 44; + + let token_hash = keccak256( + [ + path.to_be_bytes_vec().as_ref(), + channel.to_be_bytes().as_ref(), + &token, + ] + .concat(), + ) + .get() + .to_base58(); + + // https://en.wikipedia.org/wiki/Binary-to-text_encoding + // Luckily, base58 encoding has ~0.73 efficiency: + // (1 / 0.73) * 32 = 43.8356164384 + // TokenFactory denom name limit + assert!(token_hash.len() <= MAX_DENOM_LENGTH); + + token_hash.to_string() +} + +fn execute_fungible_asset_order( + deps: DepsMut, + env: Env, + _info: MessageInfo, + packet: Packet, + relayer: Addr, + _relayer_msg: Bytes, + _salt: H256, + path: alloy::primitives::U256, + order: FungibleAssetOrder, +) -> Result, ContractError> { + if order.quote_amount > order.base_amount { + EXECUTION_ACK.save(deps.storage, &ACK_ERR_ONLY_MAKER.into())?; + return Ok(Response::new()); + } + let wrapped_denom = predict_wrapped_denom( + path, + packet.destination_channel, + Bytes::from(order.base_token.to_vec()), + ); + let quote_amount = + u128::try_from(order.quote_amount).map_err(|_| ContractError::AmountOverflow)?; + let fee_amount = order.base_amount - order.quote_amount; + let fee_amount = u128::try_from(fee_amount).map_err(|_| ContractError::AmountOverflow)?; + let receiver = deps + .api + .addr_validate( + str::from_utf8(order.receiver.as_ref()).map_err(|_| ContractError::InvalidReceiver)?, + ) + .map_err(|_| ContractError::UnableToValidateReceiver)?; + let mut messages = Vec::>::new(); + if order.quote_token.as_ref() == wrapped_denom.as_bytes() { + // TODO: handle forwarding path + let subdenom = factory_denom(&wrapped_denom, env.contract.address.as_str()); + if !HASH_TO_FOREIGN_TOKEN.has(deps.storage, subdenom.clone()) { + HASH_TO_FOREIGN_TOKEN.save( + deps.storage, + subdenom.clone(), + &Bytes::from(order.base_token.to_vec()), + )?; + messages.push( + TokenFactoryMsg::CreateDenom { + subdenom: wrapped_denom, + } + .into(), + ); + messages.push( + TokenFactoryMsg::SetDenomMetadata { + denom: subdenom.clone(), + metadata: Metadata { + description: None, + denom_units: vec![], + base: None, + display: None, + name: Some(order.base_token_name), + symbol: Some(order.base_token_symbol), + uri: None, + uri_hash: None, + }, + } + .into(), + ); + TOKEN_ORIGIN.save( + deps.storage, + subdenom.clone(), + &Uint256::from_u128(packet.destination_channel as _), + )?; + }; + messages.push( + TokenFactoryMsg::MintTokens { + denom: subdenom.clone(), + amount: quote_amount.into(), + mint_to_address: receiver.into_string(), + } + .into(), + ); + if fee_amount > 0 { + messages.push( + TokenFactoryMsg::MintTokens { + denom: subdenom, + amount: fee_amount.into(), + mint_to_address: relayer.into_string(), + } + .into(), + ); + } + } else { + if order.base_token_path == packet.source_channel.try_into().unwrap() { + let quote_token = String::from_utf8(order.quote_token.to_vec()) + .map_err(|_| ContractError::InvalidQuoteToken)?; + CHANNEL_BALANCE.update( + deps.storage, + (packet.destination_channel, quote_token.clone()), + |balance| match balance { + Some(value) => value + .checked_sub(quote_amount.into()) + .map_err(|_| ContractError::InvalidChannelBalance), + None => Err(ContractError::InvalidChannelBalance), + }, + )?; + messages.push( + BankMsg::Send { + to_address: receiver.into_string(), + amount: vec![Coin { + denom: quote_token.clone(), + amount: quote_amount.into(), + }], + } + .into(), + ); + if fee_amount > 0 { + messages.push( + BankMsg::Send { + to_address: relayer.into_string(), + amount: vec![Coin { + denom: quote_token, + amount: fee_amount.into(), + }], + } + .into(), + ); + } + } else { + EXECUTION_ACK.save(deps.storage, &ACK_ERR_ONLY_MAKER.into())?; + return Ok(Response::new()); + } + }; + EXECUTION_ACK.save( + deps.storage, + &FungibleAssetOrderAck { + fill_type: FILL_TYPE_PROTOCOL, + market_maker: Default::default(), + } + .abi_encode_params() + .into(), + )?; + Ok(Response::new().add_messages(messages)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply( + deps: DepsMut, + _env: Env, + reply: Reply, +) -> Result, ContractError> { + if reply.id != REPLY_ID { + return Err(ContractError::UnknownReply { id: reply.id }); + } + let ibc_host = CONFIG.load(deps.storage)?.ibc_host; + let packet = EXECUTING_PACKET.load(deps.storage)?; + EXECUTING_PACKET.remove(deps.storage); + match reply.result { + SubMsgResult::Ok(_) => { + // If the execution succedeed ack is guaranteed to exist. + let execution_ack = EXECUTION_ACK.load(deps.storage)?; + EXECUTION_ACK.remove(deps.storage); + match execution_ack { + // Specific value when the execution must be replayed by a MM. No + // side effects were executed. We break the TX for MMs to be able to + // replay the packet. + ack if ack.as_ref() == ACK_ERR_ONLY_MAKER => Err(ContractError::OnlyMaker), + ack if !ack.is_empty() => { + let zkgm_ack = Ack { + tag: TAG_ACK_SUCCESS, + inner_ack: Vec::from(ack).into(), + } + .abi_encode_params(); + Ok(Response::new().add_message(wasm_execute( + &ibc_host, + &ibc_union_msg::msg::ExecuteMsg::WriteAcknowledgement( + MsgWriteAcknowledgement { + channel_id: packet.destination_channel, + packet, + acknowledgement: zkgm_ack.into(), + }, + ), + vec![], + )?)) + } + // Async acknowledgement, we don't write anything + _ => Ok(Response::new()), + } + } + // Something went horribly wrong. + SubMsgResult::Err(e) => { + let zkgm_ack = Ack { + tag: TAG_ACK_FAILURE, + inner_ack: Default::default(), + } + .abi_encode_params(); + Ok(Response::new() + .add_attribute("fatal_error", to_json_string(&e)?) + .add_message(wasm_execute( + &ibc_host, + &ibc_union_msg::msg::ExecuteMsg::WriteAcknowledgement( + MsgWriteAcknowledgement { + channel_id: packet.destination_channel, + packet, + acknowledgement: zkgm_ack.into(), + }, + ), + vec![], + )?)) + } + } +} + +fn transfer( + deps: DepsMut, + env: Env, + info: MessageInfo, + channel_id: u32, + receiver: Bytes, + base_token: String, + base_amount: Uint128, + quote_token: Bytes, + quote_amount: Uint256, + timeout_height: u64, + timeout_timestamp: u64, + salt: H256, +) -> Result, ContractError> { + if base_amount.is_zero() { + return Err(ContractError::InvalidAmount); + } + let contains_base_token = info + .funds + .iter() + .any(|coin| coin.denom == base_token && coin.amount == base_amount); + if !contains_base_token { + return Err(ContractError::MissingFunds); + } + let mut messages = Vec::>::new(); + // TODO: handle forward path + let origin = TOKEN_ORIGIN.may_load(deps.storage, base_token.clone())?; + match origin { + // Burn as we are going to unescrow on the counterparty + Some(path) if path == Uint256::from(channel_id) => messages.push( + TokenFactoryMsg::BurnTokens { + denom: base_token.clone(), + amount: base_amount, + burn_from_address: env.contract.address.into_string(), + } + .into(), + ), + // Escrow and update the balance, the counterparty will mint the token + _ => { + CHANNEL_BALANCE.update(deps.storage, (channel_id, base_token.clone()), |balance| { + match balance { + Some(value) => value + .checked_add(base_amount.into()) + .map_err(|_| ContractError::InvalidChannelBalance), + None => Ok(base_amount.into()), + } + })?; + } + }; + let denom_metadata = + deps.querier + .query::(&QueryRequest::::Custom( + TokenFactoryQuery::Metadata { + denom: base_token.clone(), + }, + )); + let default_name = "".into(); + let default_symbol = base_token.clone(); + let (base_token_name, base_token_symbol) = match denom_metadata { + Ok(MetadataResponse { + metadata: Some(metadata), + }) => ( + metadata.name.unwrap_or(default_name), + metadata.symbol.unwrap_or(default_symbol), + ), + _ => (default_name, default_symbol), + }; + let config = CONFIG.load(deps.storage)?; + messages.push( + wasm_execute( + &config.ibc_host, + &ibc_union_msg::msg::ExecuteMsg::PacketSend(MsgSendPacket { + source_channel: channel_id, + timeout_height, + timeout_timestamp, + data: ZkgmPacket { + salt: salt.into(), + path: alloy::primitives::U256::ZERO, + instruction: Instruction { + version: ZKGM_VERSION_0, + opcode: OP_FUNGIBLE_ASSET_ORDER, + operand: FungibleAssetOrder { + sender: info.sender.as_bytes().to_vec().into(), + receiver: receiver.into_vec().into(), + base_token: base_token.as_bytes().to_vec().into(), + base_amount: base_amount.u128().try_into().expect("u256>u128"), + base_token_symbol, + base_token_name, + base_token_path: origin + .map(|x| alloy::primitives::U256::from_be_bytes(x.to_be_bytes())) + .unwrap_or(alloy::primitives::U256::ZERO), + quote_token: quote_token.into_vec().into(), + quote_amount: alloy::primitives::U256::from_be_bytes( + quote_amount.to_be_bytes(), + ), + } + .abi_encode_params() + .into(), + }, + } + .abi_encode_params() + .into(), + }), + vec![], + )? + .into(), + ); + Ok(Response::new().add_messages(messages)) +} diff --git a/cosmwasm/ibc-union/app/ucs03-zkgm/src/lib.rs b/cosmwasm/ibc-union/app/ucs03-zkgm/src/lib.rs new file mode 100644 index 0000000000..6f41c52046 --- /dev/null +++ b/cosmwasm/ibc-union/app/ucs03-zkgm/src/lib.rs @@ -0,0 +1,56 @@ +pub mod com; +pub mod contract; +pub mod msg; +mod state; +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + #[error("invalid ibc version, got {version}")] + InvalidIbcVersion { version: String }, + #[error("invalid operation, sender must be ibc host")] + OnlyIBCHost, + #[error("invalid operation, sender must be self")] + OnlySelf, + #[error(transparent)] + Alloy(#[from] alloy::sol_types::Error), + #[error("invalid zkgm instruction version: {version}")] + UnsupportedVersion { version: u8 }, + #[error("unknown zkgm instruction opcode: {opcode}")] + UnknownOpcode { opcode: u8 }, + #[error("unknown reply id: {id}")] + UnknownReply { id: u64 }, + #[error("invalid operation, can only be executed by a market maker")] + OnlyMaker, + #[error("packet execution reentrancy not allowed")] + AlreadyExecuting, + #[error("order amount must be u128")] + AmountOverflow, + #[error("the quote token must be a valid utf8 denom")] + InvalidQuoteToken, + #[error("the base token must be a valid utf8 denom")] + InvalidBaseToken, + #[error("invalid channel balance, counterparty has been taken over?")] + InvalidChannelBalance, + #[error("amount must be non zero")] + InvalidAmount, + #[error("transfer require funds to be submitted along the transaction")] + MissingFunds, + #[error("receiver must be a valid address")] + InvalidReceiver, + #[error("receiver must be a valid address")] + InvalidSender, + #[error( + "the receiver can't be validated, make sure the bech prefix matches the current chain" + )] + UnableToValidateReceiver, + #[error( + "the receiver can't be validated, make sure the bech prefix matches the current chain" + )] + UnableToValidateMarketMaker, + #[error("the sender can't be validated, make sure the bech prefix matches the current chain")] + UnableToValidateSender, +} diff --git a/cosmwasm/ibc-union/app/ucs03-zkgm/src/msg.rs b/cosmwasm/ibc-union/app/ucs03-zkgm/src/msg.rs new file mode 100644 index 0000000000..1a9363a653 --- /dev/null +++ b/cosmwasm/ibc-union/app/ucs03-zkgm/src/msg.rs @@ -0,0 +1,40 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, CosmosMsg, Uint128, Uint256}; +use ibc_solidity::Packet; +use token_factory_api::TokenFactoryMsg; +use unionlabs::primitives::{Bytes, H256}; + +use crate::state::Config; + +#[cw_serde] +pub struct InitMsg { + pub config: Config, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ExecuteMsg { + Transfer { + channel_id: u32, + receiver: Bytes, + base_token: String, + base_amount: Uint128, + quote_token: Bytes, + quote_amount: Uint256, + timeout_height: u64, + timeout_timestamp: u64, + salt: H256, + }, + BatchExecute { + msgs: Vec>, + }, + ExecutePacket { + packet: Packet, + relayer: Addr, + relayer_msg: Bytes, + }, + IbcUnionMsg(ibc_union_msg::module::IbcUnionMsg), +} + +#[cw_serde] +pub struct MigrateMsg {} diff --git a/cosmwasm/ibc-union/app/ucs03-zkgm/src/state.rs b/cosmwasm/ibc-union/app/ucs03-zkgm/src/state.rs new file mode 100644 index 0000000000..e974371539 --- /dev/null +++ b/cosmwasm/ibc-union/app/ucs03-zkgm/src/state.rs @@ -0,0 +1,22 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Uint256}; +use cw_storage_plus::{Item, Map}; +use ibc_solidity::Packet; +use unionlabs::primitives::Bytes; + +#[cw_serde] +pub struct Config { + pub ibc_host: Addr, +} + +pub const CONFIG: Item = Item::new("config"); + +pub const TOKEN_ORIGIN: Map = Map::new("token_origin"); + +pub const CHANNEL_BALANCE: Map<(u32, String), Uint256> = Map::new("channel_balance"); + +pub const EXECUTING_PACKET: Item = Item::new("executing_packet"); + +pub const EXECUTION_ACK: Item = Item::new("execution_ack"); + +pub const HASH_TO_FOREIGN_TOKEN: Map = Map::new("hash_to_foreign_token"); diff --git a/cosmwasm/ibc-union/core/src/contract.rs b/cosmwasm/ibc-union/core/src/contract.rs index 6546cdc08f..e60e15d23b 100644 --- a/cosmwasm/ibc-union/core/src/contract.rs +++ b/cosmwasm/ibc-union/core/src/contract.rs @@ -1509,6 +1509,10 @@ fn write_acknowledgement( packet: Packet, acknowledgement: Vec, ) -> ContractResult { + if acknowledgement.is_empty() { + return Err(ContractError::AcknowledgementIsEmpty); + } + // make sure the caller owns the channel let port_id = CHANNEL_OWNER.load(deps.storage, channel_id)?; if port_id != sender { diff --git a/cosmwasm/token-factory-api/Cargo.toml b/cosmwasm/token-factory-api/Cargo.toml index 1ef88c48cb..80ffaea52a 100644 --- a/cosmwasm/token-factory-api/Cargo.toml +++ b/cosmwasm/token-factory-api/Cargo.toml @@ -7,5 +7,5 @@ version = "0.1.0" workspace = true [dependencies] -cosmwasm-schema = { workspace = true } -cosmwasm-std = { workspace = true } +cosmwasm-schema = { version = "1.5" } +cosmwasm-std = { version = "1.5" } diff --git a/cosmwasm/token-factory-api/src/lib.rs b/cosmwasm/token-factory-api/src/lib.rs index 0d5ec0ab2a..737b4b2839 100644 --- a/cosmwasm/token-factory-api/src/lib.rs +++ b/cosmwasm/token-factory-api/src/lib.rs @@ -16,7 +16,8 @@ pub enum TokenFactoryMsg { /// to calling SetMetadata directly on the returned denom. CreateDenom { subdenom: String, - metadata: Option, + // TODO: upgrade tokenfactory to handle this + // metadata: Option, }, /// ChangeAdmin changes the admin for a factory denom. /// Can only be called by the current contract admin. @@ -40,10 +41,9 @@ pub enum TokenFactoryMsg { amount: Uint128, burn_from_address: String, }, - SetMetadata { - denom: String, - metadata: Metadata, - }, + /// Contracts can set metadata for an existing factory denom that they are + /// admin of. + SetDenomMetadata { denom: String, metadata: Metadata }, } /// This maps to cosmos.bank.v1beta1.Metadata protobuf struct @@ -61,6 +61,10 @@ pub struct Metadata { /// symbol is the token symbol usually shown on exchanges (eg: ATOM). This can /// be the same as the display. pub symbol: Option, + /// URI to a document (on or off-chain) that contains additional information. Optional. + pub uri: Option, + /// URIHash is a sha256 hash of a document pointed by URI. It's used to verify that the document didn't change. Optional. + pub uri_hash: Option, } /// This maps to cosmos.bank.v1beta1.DenomUnit protobuf struct diff --git a/docs/src/content/docs/protocol/deployments.mdx b/docs/src/content/docs/protocol/deployments.mdx index 6ab37c2855..0171148669 100644 --- a/docs/src/content/docs/protocol/deployments.mdx +++ b/docs/src/content/docs/protocol/deployments.mdx @@ -55,6 +55,7 @@ Deployments of `ibc-union` on CosmWasm (cosmos) chains. CosmWasm contract source | `tendermint-light-client` | [`union17ymdtz48qey0lpha8erch8hghj37ag4dn0qqyyrtseymvgw6lfnqa962sy`](https://explorer.testnet-9.union.build/union/cosmwasm/0/transactions?contract=union17ymdtz48qey0lpha8erch8hghj37ag4dn0qqyyrtseymvgw6lfnqa962sy) | | `berachain-light-client` | [`union1au6fkkfcgqc6vn8dz9tq2a6ma0vzwn2zfwwgpm7awpaeekw346uqjedtky`](https://explorer.testnet-9.union.build/union/cosmwasm/0/transactions?contract=union1au6fkkfcgqc6vn8dz9tq2a6ma0vzwn2zfwwgpm7awpaeekw346uqjedtky) | | `ucs00` | [`union194e3rchcaqyynwcj6qr6647ge7lheymrgkhq9tdknw35050ufhuqzqz2he`](https://explorer.testnet-9.union.build/union/cosmwasm/0/transactions?contract=union194e3rchcaqyynwcj6qr6647ge7lheymrgkhq9tdknw35050ufhuqzqz2he) | +| `ucs03` | [`union19hspxmypfxsdsnxttma8rxvp7dtcmzhl9my0ee64avg358vlpawsdvucqa`](https://explorer.testnet-9.union.build/union/cosmwasm/0/transactions?contract=union19hspxmypfxsdsnxttma8rxvp7dtcmzhl9my0ee64avg358vlpawsdvucqa) | | ? | `union1au6fkkfcgqc6vn8dz9tq2a6ma0vzwn2zfwwgpm7awpaeekw346uqjedtky` | diff --git a/evm/contracts/apps/ucs/03-zkgm/Zkgm.sol b/evm/contracts/apps/ucs/03-zkgm/Zkgm.sol index f5fdba5276..b19a08b516 100644 --- a/evm/contracts/apps/ucs/03-zkgm/Zkgm.sol +++ b/evm/contracts/apps/ucs/03-zkgm/Zkgm.sol @@ -100,7 +100,7 @@ library ZkgmLib { error ErrUnsupportedVersion(); error ErrUnimplemented(); error ErrBatchMustBeSync(); - error ErrUnknownSyscall(); + error ErrUnknownOpcode(); error ErrInfiniteGame(); error ErrUnauthorized(); error ErrInvalidAmount(); @@ -213,10 +213,10 @@ library ZkgmLib { transfer.sender, transfer.receiver, transfer.baseToken, - transfer.baseTokenPath, + transfer.baseAmount, transfer.baseTokenSymbol, transfer.baseTokenName, - transfer.baseAmount, + transfer.baseTokenPath, transfer.quoteToken, transfer.quoteAmount ); @@ -403,7 +403,7 @@ contract UCS03Zkgm is channelId, path, ZkgmLib.decodeMultiplex(instruction.operand) ); } else { - revert ZkgmLib.ErrUnknownSyscall(); + revert ZkgmLib.ErrUnknownOpcode(); } } @@ -569,7 +569,7 @@ contract UCS03Zkgm is ZkgmLib.decodeMultiplex(instruction.operand) ); } else { - revert ZkgmLib.ErrUnknownSyscall(); + revert ZkgmLib.ErrUnknownOpcode(); } } @@ -757,7 +757,7 @@ contract UCS03Zkgm is bytes calldata ack, address relayer ) external virtual override onlyIBC { - bytes32 packetHash = IBCPacketLib.commitPacketMemory(ibcPacket); + bytes32 packetHash = IBCPacketLib.commitPacket(ibcPacket); IBCPacket memory parent = inFlightPacket[packetHash]; // Specific case of forwarding where the ack is threaded back directly. if (parent.timeoutTimestamp != 0 || parent.timeoutHeight != 0) { @@ -825,7 +825,7 @@ contract UCS03Zkgm is ack ); } else { - revert ZkgmLib.ErrUnknownSyscall(); + revert ZkgmLib.ErrUnknownOpcode(); } } @@ -934,7 +934,7 @@ contract UCS03Zkgm is IBCPacket calldata ibcPacket, address relayer ) external virtual override onlyIBC { - bytes32 packetHash = IBCPacketLib.commitPacketMemory(ibcPacket); + bytes32 packetHash = IBCPacketLib.commitPacket(ibcPacket); IBCPacket memory parent = inFlightPacket[packetHash]; // Specific case of forwarding where the failure is threaded back directly. if (parent.timeoutTimestamp != 0 || parent.timeoutHeight != 0) { @@ -991,7 +991,7 @@ contract UCS03Zkgm is ZkgmLib.decodeMultiplex(instruction.operand) ); } else { - revert ZkgmLib.ErrUnknownSyscall(); + revert ZkgmLib.ErrUnknownOpcode(); } } diff --git a/lib/unionlabs/src/uint.rs b/lib/unionlabs/src/uint.rs index 975c0b56d3..b8598153fa 100644 --- a/lib/unionlabs/src/uint.rs +++ b/lib/unionlabs/src/uint.rs @@ -106,6 +106,7 @@ impl fmt::LowerHex for U256 { impl U256 { pub const MAX: Self = Self::from_limbs([u64::MAX; 4]); pub const ZERO: Self = Self::from_limbs([0; 4]); + pub const ONE: Self = Self::from_limbs([0, 0, 0, 1]); // one day... // pub const fn from_const_str() -> Self {}