From db27bf47188e4229092376cd3578d4573c95572e Mon Sep 17 00:00:00 2001 From: Hussein Ait Lahcen Date: Thu, 9 Jan 2025 11:07:53 +0100 Subject: [PATCH 1/7] feat(zkgm): cosmwasm scaffold --- Cargo.lock | 24 +- Cargo.toml | 1 + cosmwasm/cosmwasm.nix | 13 +- cosmwasm/ibc-union/app/ucs03-zkgm/Cargo.toml | 34 + cosmwasm/ibc-union/app/ucs03-zkgm/src/com.rs | 71 ++ .../ibc-union/app/ucs03-zkgm/src/contract.rs | 625 ++++++++++++++++++ cosmwasm/ibc-union/app/ucs03-zkgm/src/lib.rs | 46 ++ cosmwasm/ibc-union/app/ucs03-zkgm/src/msg.rs | 40 ++ .../ibc-union/app/ucs03-zkgm/src/state.rs | 20 + cosmwasm/token-factory-api/Cargo.toml | 4 +- cosmwasm/token-factory-api/src/lib.rs | 3 +- .../src/content/docs/protocol/deployments.mdx | 1 + evm/contracts/apps/ucs/03-zkgm/Zkgm.sol | 10 +- lib/unionlabs/src/uint.rs | 1 + 14 files changed, 881 insertions(+), 12 deletions(-) create mode 100644 cosmwasm/ibc-union/app/ucs03-zkgm/Cargo.toml create mode 100644 cosmwasm/ibc-union/app/ucs03-zkgm/src/com.rs create mode 100644 cosmwasm/ibc-union/app/ucs03-zkgm/src/contract.rs create mode 100644 cosmwasm/ibc-union/app/ucs03-zkgm/src/lib.rs create mode 100644 cosmwasm/ibc-union/app/ucs03-zkgm/src/msg.rs create mode 100644 cosmwasm/ibc-union/app/ucs03-zkgm/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index 1129c9d063..6b398a1cf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6317,6 +6317,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 +12046,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]] diff --git a/Cargo.toml b/Cargo.toml index 2a58d11cd0..7a62630cfc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "cosmwasm/token-factory-api", "cosmwasm/ucs00-pingpong", "cosmwasm/ibc-union/app/ucs00-pingpong", + "cosmwasm/ibc-union/app/ucs03-zkgm", "cosmwasm/ucs01-relay", "cosmwasm/ucs01-relay-api", "cosmwasm/ucs02-nft", diff --git a/cosmwasm/cosmwasm.nix b/cosmwasm/cosmwasm.nix index 7ca7f74788..a98d36b1f4 100644 --- a/cosmwasm/cosmwasm.nix +++ b/cosmwasm/cosmwasm.nix @@ -24,6 +24,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"; }; @@ -37,7 +40,13 @@ // 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 + // ucs01-relay.checks + // ucs01-relay-api.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..936d342ef3 --- /dev/null +++ b/cosmwasm/ibc-union/app/ucs03-zkgm/src/contract.rs @@ -0,0 +1,625 @@ +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::{MetadataResponse, TokenFactoryMsg, TokenFactoryQuery}; +use unionlabs::{ + ethereum::keccak256, + primitives::{Bytes, H256}, +}; + +use crate::{ + com::{ + Ack, FungibleAssetOrder, FungibleAssetOrderAck, Instruction, ZkgmPacket, + ACK_ERR_ONLY_MAKER, 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, 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::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 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 { + return Ok(Response::new().set_data(ACK_ERR_ONLY_MAKER)); + } + 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, wrapped_denom.clone()) { + HASH_TO_FOREIGN_TOKEN.save( + deps.storage, + subdenom.clone(), + &Bytes::from(order.base_token.to_vec()), + )?; + messages.push( + TokenFactoryMsg::CreateDenom { + subdenom: wrapped_denom, + // TODO: not handled by the current tokenfactory version, require an upgrade!!! + // metadata: Some(Metadata { + // description: None, + // denom_units: vec![], + // base: None, + // display: None, + // name: Some(order.base_token_name), + // symbol: Some(order.base_token_symbol), + // }), + } + .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 { + return Ok(Response::new().set_data(ACK_ERR_ONLY_MAKER)); + } + }; + Ok(Response::new().add_messages(messages).set_data( + FungibleAssetOrderAck { + fill_type: FILL_TYPE_PROTOCOL, + market_maker: Default::default(), + } + .abi_encode_params(), + )) +} + +#[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(response) => { + match response.data { + // 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. + Some(ack) if ack.as_ref() == ACK_ERR_ONLY_MAKER => Err(ContractError::OnlyMaker), + Some(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("failure", 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 path properly + 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 => Err(ContractError::InvalidChannelBalance), + } + })?; + } + }; + 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(), + // TODO: remove base_token prefix? + 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)) +} + +// #[cfg(test)] +// mod tests { +// use std::marker::PhantomData; + +// use cosmwasm_std::{ +// testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage}, +// Addr, Api, CanonicalAddr, OwnedDeps, +// }; +// use ibc_solidity::Packet; +// use token_factory_api::TokenFactoryQuery; + +// use super::execute_packet; +// use crate::{ +// com::{FILL_TYPE_PROTOCOL, TAG_ACK_SUCCESS}, +// contract::predict_wrapped_denom, +// }; + +// #[test] +// fn test() { +// let mut deps = OwnedDeps::<_, _, _, TokenFactoryQuery> { +// storage: MockStorage::default(), +// api: MockApi::default(), +// querier: MockQuerier::default(), +// custom_query_type: PhantomData, +// }; + +// let env = mock_env(); +// let info = mock_info("", &[]); +// let zkgm_packet = hex::decode("00000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000002e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000014153919669edc8a5d0c8d1e4507c9ce60435a1177000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c756e696f6e3164383467743663777839333873616e306874687a37793666307234663030676a71776835397700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014685ce6742351ae9b618f383883d6d1e0c5a31b4b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000044c494e4b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f436861696e4c696e6b20546f6b656e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c426763464b6d70457437547872713151576a59386e6e4e4e6e363259754542555a365577483143466f79666a0000000000000000000000000000000000000000").unwrap(); +// let packet = Packet { +// source_channel: 8, +// destination_channel: 7, +// data: zkgm_packet.into(), +// timeout_height: 0, +// timeout_timestamp: 0, +// }; +// println!( +// "{:#?}", +// execute_packet( +// deps.as_mut(), +// env, +// info, +// packet, +// Addr::unchecked(""), +// Default::default(), +// ) +// .unwrap() +// ); +// panic!(); +// // panic!( +// // "{}", +// // predict_wrapped_denom( +// // Default::default(), +// // 7, +// // hex::decode("685ce6742351ae9b618f383883d6d1e0c5a31b4b") +// // .unwrap() +// // .into() +// // ) +// // ); +// } +// } 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..d8687e826e --- /dev/null +++ b/cosmwasm/ibc-union/app/ucs03-zkgm/src/lib.rs @@ -0,0 +1,46 @@ +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("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( + "the receiver can't be validated, make sure the bech prefix matches the current chain" + )] + UnableToValidateReceiver, +} 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..8d84ddb67e --- /dev/null +++ b/cosmwasm/ibc-union/app/ucs03-zkgm/src/state.rs @@ -0,0 +1,20 @@ +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 HASH_TO_FOREIGN_TOKEN: Map = Map::new("hash_to_foreign_token"); 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..485d0f0385 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. 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..f87acb95fd 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(); @@ -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(); } } @@ -825,7 +825,7 @@ contract UCS03Zkgm is ack ); } else { - revert ZkgmLib.ErrUnknownSyscall(); + revert ZkgmLib.ErrUnknownOpcode(); } } @@ -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 {} From b39b2ac18c6a45cef33b4cf4e27047874194bc9e Mon Sep 17 00:00:00 2001 From: Hussein Ait Lahcen Date: Sat, 11 Jan 2025 12:18:01 +0100 Subject: [PATCH 2/7] fix(zkgm): invalid manual encoding order --- evm/contracts/apps/ucs/03-zkgm/Zkgm.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evm/contracts/apps/ucs/03-zkgm/Zkgm.sol b/evm/contracts/apps/ucs/03-zkgm/Zkgm.sol index f87acb95fd..afbbaf9e54 100644 --- a/evm/contracts/apps/ucs/03-zkgm/Zkgm.sol +++ b/evm/contracts/apps/ucs/03-zkgm/Zkgm.sol @@ -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 ); From e7e519c2cce187d23b2c829da1b5a3199b93449c Mon Sep 17 00:00:00 2001 From: Hussein Ait Lahcen Date: Sat, 11 Jan 2025 13:32:43 +0100 Subject: [PATCH 3/7] fix(ibc-union): forbid empty ack in writeAck --- cosmwasm/ibc-union/core/src/contract.rs | 4 ++++ 1 file changed, 4 insertions(+) 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 { From 37ae5ffa1d91eb95b2ce75178475d3c82de44ff1 Mon Sep 17 00:00:00 2001 From: Hussein Ait Lahcen Date: Sat, 11 Jan 2025 14:19:17 +0100 Subject: [PATCH 4/7] feat(zkgm): denom meta after creation and ack threaded through storage --- .../ibc-union/app/ucs03-zkgm/src/contract.rs | 64 ++++++++++++------- .../ibc-union/app/ucs03-zkgm/src/state.rs | 2 + cosmwasm/token-factory-api/src/lib.rs | 11 ++-- 3 files changed, 50 insertions(+), 27 deletions(-) diff --git a/cosmwasm/ibc-union/app/ucs03-zkgm/src/contract.rs b/cosmwasm/ibc-union/app/ucs03-zkgm/src/contract.rs index 936d342ef3..f3d73fe853 100644 --- a/cosmwasm/ibc-union/app/ucs03-zkgm/src/contract.rs +++ b/cosmwasm/ibc-union/app/ucs03-zkgm/src/contract.rs @@ -13,7 +13,7 @@ use ibc_union_msg::{ module::IbcUnionMsg, msg::{MsgSendPacket, MsgWriteAcknowledgement}, }; -use token_factory_api::{MetadataResponse, TokenFactoryMsg, TokenFactoryQuery}; +use token_factory_api::{Metadata, MetadataResponse, TokenFactoryMsg, TokenFactoryQuery}; use unionlabs::{ ethereum::keccak256, primitives::{Bytes, H256}, @@ -26,7 +26,10 @@ use crate::{ TAG_ACK_SUCCESS, ZKGM_VERSION_0, }, msg::{ExecuteMsg, InitMsg, MigrateMsg}, - state::{CHANNEL_BALANCE, CONFIG, EXECUTING_PACKET, HASH_TO_FOREIGN_TOKEN, TOKEN_ORIGIN}, + state::{ + CHANNEL_BALANCE, CONFIG, EXECUTING_PACKET, EXECUTION_ACK, HASH_TO_FOREIGN_TOKEN, + TOKEN_ORIGIN, + }, ContractError, }; @@ -271,7 +274,8 @@ fn execute_fungible_asset_order( order: FungibleAssetOrder, ) -> Result, ContractError> { if order.quote_amount > order.base_amount { - return Ok(Response::new().set_data(ACK_ERR_ONLY_MAKER)); + EXECUTION_ACK.save(deps.storage, &ACK_ERR_ONLY_MAKER.into())?; + return Ok(Response::new()); } let wrapped_denom = predict_wrapped_denom( path, @@ -292,7 +296,7 @@ fn execute_fungible_asset_order( 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, wrapped_denom.clone()) { + if !HASH_TO_FOREIGN_TOKEN.has(deps.storage, subdenom.clone()) { HASH_TO_FOREIGN_TOKEN.save( deps.storage, subdenom.clone(), @@ -301,15 +305,22 @@ fn execute_fungible_asset_order( messages.push( TokenFactoryMsg::CreateDenom { subdenom: wrapped_denom, - // TODO: not handled by the current tokenfactory version, require an upgrade!!! - // metadata: Some(Metadata { - // description: None, - // denom_units: vec![], - // base: None, - // display: None, - // name: Some(order.base_token_name), - // symbol: Some(order.base_token_symbol), - // }), + } + .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(), ); @@ -374,16 +385,20 @@ fn execute_fungible_asset_order( ); } } else { - return Ok(Response::new().set_data(ACK_ERR_ONLY_MAKER)); + EXECUTION_ACK.save(deps.storage, &ACK_ERR_ONLY_MAKER.into())?; + return Ok(Response::new()); } }; - Ok(Response::new().add_messages(messages).set_data( - FungibleAssetOrderAck { + EXECUTION_ACK.save( + deps.storage, + &FungibleAssetOrderAck { fill_type: FILL_TYPE_PROTOCOL, market_maker: Default::default(), } - .abi_encode_params(), - )) + .abi_encode_params() + .into(), + )?; + Ok(Response::new().add_messages(messages)) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -399,13 +414,16 @@ pub fn reply( let packet = EXECUTING_PACKET.load(deps.storage)?; EXECUTING_PACKET.remove(deps.storage); match reply.result { - SubMsgResult::Ok(response) => { - match response.data { + 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. - Some(ack) if ack.as_ref() == ACK_ERR_ONLY_MAKER => Err(ContractError::OnlyMaker), - Some(ack) if !ack.is_empty() => { + 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(), @@ -435,7 +453,7 @@ pub fn reply( } .abi_encode_params(); Ok(Response::new() - .add_attribute("failure", to_json_string(&e)?) + .add_attribute("fatal_error", to_json_string(&e)?) .add_message(wasm_execute( &ibc_host, &ibc_union_msg::msg::ExecuteMsg::WriteAcknowledgement( diff --git a/cosmwasm/ibc-union/app/ucs03-zkgm/src/state.rs b/cosmwasm/ibc-union/app/ucs03-zkgm/src/state.rs index 8d84ddb67e..e974371539 100644 --- a/cosmwasm/ibc-union/app/ucs03-zkgm/src/state.rs +++ b/cosmwasm/ibc-union/app/ucs03-zkgm/src/state.rs @@ -17,4 +17,6 @@ pub const CHANNEL_BALANCE: Map<(u32, String), Uint256> = Map::new("channel_balan 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/token-factory-api/src/lib.rs b/cosmwasm/token-factory-api/src/lib.rs index 485d0f0385..737b4b2839 100644 --- a/cosmwasm/token-factory-api/src/lib.rs +++ b/cosmwasm/token-factory-api/src/lib.rs @@ -41,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 @@ -62,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 From e8fbb9c739885b3a142c448e9b36511120d97593 Mon Sep 17 00:00:00 2001 From: Hussein Ait Lahcen Date: Sat, 11 Jan 2025 15:30:16 +0100 Subject: [PATCH 5/7] feat(ucs01): deprecated and removed from cargo --- Cargo.lock | 145 ------------------------------------------ Cargo.toml | 3 - cosmwasm/cosmwasm.nix | 9 --- 3 files changed, 157 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b398a1cf8..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" @@ -12641,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 7a62630cfc..33cd8823bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,8 +6,6 @@ members = [ "cosmwasm/ucs00-pingpong", "cosmwasm/ibc-union/app/ucs00-pingpong", "cosmwasm/ibc-union/app/ucs03-zkgm", - "cosmwasm/ucs01-relay", - "cosmwasm/ucs01-relay-api", "cosmwasm/ucs02-nft", "cosmwasm/multicall", @@ -263,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 a98d36b1f4..2d21b5cbab 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"; }; @@ -37,15 +31,12 @@ inherit cw721-base; } // ucs02-nft.packages - // ucs01-relay.packages // ucs00-pingpong.packages // ibc-union.packages // multicall.packages // ibc-union-ucs03-zkgm.packages; checks = ucs02-nft.checks - // ucs01-relay.checks - // ucs01-relay-api.checks // ucs00-pingpong.checks // ibc-union-ucs03-zkgm.checks; }; From 0b931f28e42b3fdede6e66dac96ab2c02b76edc8 Mon Sep 17 00:00:00 2001 From: Hussein Ait Lahcen Date: Sat, 11 Jan 2025 15:33:22 +0100 Subject: [PATCH 6/7] feat(zkgm): optimized calldata commitment when possible --- evm/contracts/apps/ucs/03-zkgm/Zkgm.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evm/contracts/apps/ucs/03-zkgm/Zkgm.sol b/evm/contracts/apps/ucs/03-zkgm/Zkgm.sol index afbbaf9e54..b19a08b516 100644 --- a/evm/contracts/apps/ucs/03-zkgm/Zkgm.sol +++ b/evm/contracts/apps/ucs/03-zkgm/Zkgm.sol @@ -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) { @@ -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) { From ae561bb3414502207916a81414b5df4cad44a521 Mon Sep 17 00:00:00 2001 From: Hussein Ait Lahcen Date: Sat, 11 Jan 2025 17:44:09 +0100 Subject: [PATCH 7/7] feat(zgkm): cosmwasm: handle ack/timeout --- cosmwasm/cosmwasm.nix | 5 +- .../ibc-union/app/ucs03-zkgm/src/contract.rs | 295 ++++++++++++++---- cosmwasm/ibc-union/app/ucs03-zkgm/src/lib.rs | 10 + 3 files changed, 239 insertions(+), 71 deletions(-) diff --git a/cosmwasm/cosmwasm.nix b/cosmwasm/cosmwasm.nix index 2d21b5cbab..87cb7990ae 100644 --- a/cosmwasm/cosmwasm.nix +++ b/cosmwasm/cosmwasm.nix @@ -35,9 +35,6 @@ // ibc-union.packages // multicall.packages // ibc-union-ucs03-zkgm.packages; - checks = - ucs02-nft.checks - // ucs00-pingpong.checks - // ibc-union-ucs03-zkgm.checks; + checks = ucs02-nft.checks // ucs00-pingpong.checks // ibc-union-ucs03-zkgm.checks; }; } diff --git a/cosmwasm/ibc-union/app/ucs03-zkgm/src/contract.rs b/cosmwasm/ibc-union/app/ucs03-zkgm/src/contract.rs index f3d73fe853..a6b9c87a69 100644 --- a/cosmwasm/ibc-union/app/ucs03-zkgm/src/contract.rs +++ b/cosmwasm/ibc-union/app/ucs03-zkgm/src/contract.rs @@ -22,8 +22,8 @@ use unionlabs::{ use crate::{ com::{ Ack, FungibleAssetOrder, FungibleAssetOrderAck, Instruction, ZkgmPacket, - ACK_ERR_ONLY_MAKER, FILL_TYPE_PROTOCOL, OP_FUNGIBLE_ASSET_ORDER, TAG_ACK_FAILURE, - TAG_ACK_SUCCESS, ZKGM_VERSION_0, + 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::{ @@ -103,6 +103,18 @@ pub fn execute( ))) } } + 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()) @@ -173,6 +185,218 @@ fn enforce_version(version: &str, counterparty_version: Option<&str>) -> Result< 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, @@ -494,7 +718,7 @@ fn transfer( return Err(ContractError::MissingFunds); } let mut messages = Vec::>::new(); - // TODO: handle path properly + // 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 @@ -513,7 +737,7 @@ fn transfer( Some(value) => value .checked_add(base_amount.into()) .map_err(|_| ContractError::InvalidChannelBalance), - None => Err(ContractError::InvalidChannelBalance), + None => Ok(base_amount.into()), } })?; } @@ -553,7 +777,6 @@ fn transfer( operand: FungibleAssetOrder { sender: info.sender.as_bytes().to_vec().into(), receiver: receiver.into_vec().into(), - // TODO: remove base_token prefix? base_token: base_token.as_bytes().to_vec().into(), base_amount: base_amount.u128().try_into().expect("u256>u128"), base_token_symbol, @@ -579,65 +802,3 @@ fn transfer( ); Ok(Response::new().add_messages(messages)) } - -// #[cfg(test)] -// mod tests { -// use std::marker::PhantomData; - -// use cosmwasm_std::{ -// testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage}, -// Addr, Api, CanonicalAddr, OwnedDeps, -// }; -// use ibc_solidity::Packet; -// use token_factory_api::TokenFactoryQuery; - -// use super::execute_packet; -// use crate::{ -// com::{FILL_TYPE_PROTOCOL, TAG_ACK_SUCCESS}, -// contract::predict_wrapped_denom, -// }; - -// #[test] -// fn test() { -// let mut deps = OwnedDeps::<_, _, _, TokenFactoryQuery> { -// storage: MockStorage::default(), -// api: MockApi::default(), -// querier: MockQuerier::default(), -// custom_query_type: PhantomData, -// }; - -// let env = mock_env(); -// let info = mock_info("", &[]); -// let zkgm_packet = hex::decode("00000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000002e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000014153919669edc8a5d0c8d1e4507c9ce60435a1177000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c756e696f6e3164383467743663777839333873616e306874687a37793666307234663030676a71776835397700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014685ce6742351ae9b618f383883d6d1e0c5a31b4b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000044c494e4b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f436861696e4c696e6b20546f6b656e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c426763464b6d70457437547872713151576a59386e6e4e4e6e363259754542555a365577483143466f79666a0000000000000000000000000000000000000000").unwrap(); -// let packet = Packet { -// source_channel: 8, -// destination_channel: 7, -// data: zkgm_packet.into(), -// timeout_height: 0, -// timeout_timestamp: 0, -// }; -// println!( -// "{:#?}", -// execute_packet( -// deps.as_mut(), -// env, -// info, -// packet, -// Addr::unchecked(""), -// Default::default(), -// ) -// .unwrap() -// ); -// panic!(); -// // panic!( -// // "{}", -// // predict_wrapped_denom( -// // Default::default(), -// // 7, -// // hex::decode("685ce6742351ae9b618f383883d6d1e0c5a31b4b") -// // .unwrap() -// // .into() -// // ) -// // ); -// } -// } diff --git a/cosmwasm/ibc-union/app/ucs03-zkgm/src/lib.rs b/cosmwasm/ibc-union/app/ucs03-zkgm/src/lib.rs index d8687e826e..6f41c52046 100644 --- a/cosmwasm/ibc-union/app/ucs03-zkgm/src/lib.rs +++ b/cosmwasm/ibc-union/app/ucs03-zkgm/src/lib.rs @@ -31,6 +31,8 @@ pub enum ContractError { 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")] @@ -39,8 +41,16 @@ pub enum ContractError { 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, }