From 9271c23bbd9093d7a4117b35c894a18f69f34afd Mon Sep 17 00:00:00 2001 From: Hussein Ait Lahcen Date: Wed, 26 Jun 2024 01:51:46 +0200 Subject: [PATCH] feat(ucs01): denom metadata update packet --- Cargo.lock | 15 +- cosmwasm/ucs01-relay-api/Cargo.toml | 2 + cosmwasm/ucs01-relay-api/src/lib.rs | 3 + cosmwasm/ucs01-relay-api/src/protocol.rs | 55 +- cosmwasm/ucs01-relay-api/src/types.rs | 133 +++- cosmwasm/ucs01-relay/src/protocol.rs | 22 +- .../apps/ucs/01-relay/ERC20Denom.sol | 47 +- .../apps/ucs/01-relay/IERC20Denom.sol | 9 +- evm/contracts/apps/ucs/01-relay/Relay.sol | 255 +++++-- evm/evm.nix | 4 +- .../src/25-handler/IBCPacketHandler.t.sol | 14 +- evm/tests/src/apps/ucs/01-relay/Relay.t.sol | 679 +++++++++++------- 12 files changed, 888 insertions(+), 350 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f85950d757..4df35ef536 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6251,6 +6251,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + [[package]] name = "num-format" version = "0.4.4" @@ -9446,7 +9457,7 @@ source = "git+https://github.com/unionlabs/tendermint-rs?branch=v0.34.0-bn254#2a dependencies = [ "bytes", "flex-error", - "num-derive", + "num-derive 0.3.3", "num-traits", "prost 0.12.6", "prost-types 0.12.3", @@ -10379,6 +10390,8 @@ dependencies = [ "cosmwasm-std 2.0.3", "ethabi", "go-parse-duration", + "num-derive 0.4.2", + "num-traits", "prost 0.12.6", "protos", "serde", diff --git a/cosmwasm/ucs01-relay-api/Cargo.toml b/cosmwasm/ucs01-relay-api/Cargo.toml index ba942bef78..5ff7bf07cd 100644 --- a/cosmwasm/ucs01-relay-api/Cargo.toml +++ b/cosmwasm/ucs01-relay-api/Cargo.toml @@ -11,6 +11,8 @@ cosmwasm-schema = { version = "2.0.0" } cosmwasm-std = { version = "2.0.0", features = ["stargate"] } ethabi = { workspace = true } go-parse-duration = { workspace = true } +num-derive = { version = "0.4" } +num-traits = { version = "0.2" } prost = { workspace = true } protos = { workspace = true } serde = { workspace = true } diff --git a/cosmwasm/ucs01-relay-api/src/lib.rs b/cosmwasm/ucs01-relay-api/src/lib.rs index e559e25373..d6dd3dcbab 100644 --- a/cosmwasm/ucs01-relay-api/src/lib.rs +++ b/cosmwasm/ucs01-relay-api/src/lib.rs @@ -1,3 +1,6 @@ +#[macro_use] +extern crate num_derive; + pub mod middleware; pub mod protocol; pub mod types; diff --git a/cosmwasm/ucs01-relay-api/src/protocol.rs b/cosmwasm/ucs01-relay-api/src/protocol.rs index 9b271c7bd5..fc001833cb 100644 --- a/cosmwasm/ucs01-relay-api/src/protocol.rs +++ b/cosmwasm/ucs01-relay-api/src/protocol.rs @@ -9,7 +9,9 @@ use unionlabs::encoding::{self, Decode, DecodeErrorOf, Encode}; use crate::{ middleware::{InFlightPfmPacket, Memo, PacketForward}, - types::{EncodingError, GenericAck, TransferPacket, TransferPacketCommon, TransferToken}, + types::{ + EncodingError, GenericAck, PacketTag, TransferPacket, TransferPacketCommon, TransferToken, + }, }; // https://github.com/cosmos/ibc-go/blob/8218aeeef79d556852ec62a773f2bc1a013529d4/modules/apps/transfer/types/keys.go#L12 @@ -49,7 +51,7 @@ pub enum ProtocolError { Unauthorized, } -pub type PacketExtensionOf = <::Packet as TransferPacket>::Extension; +pub type PacketExtensionOf = <::TokenPacket as TransferPacket>::Extension; pub struct TransferInput { pub current_time: Timestamp, @@ -88,7 +90,7 @@ pub trait TransferProtocol { const ORDERING: IbcOrder; const RECEIVE_REPLY_ID: u64; - type Packet: Decode + Encode + TransferPacket; + type TokenPacket: Decode + Encode + TransferPacket; type Ack: Decode + Encode + Into; @@ -99,7 +101,7 @@ pub trait TransferProtocol { type Error: Debug + From + From - + From> + + From> + From>; fn channel_endpoint(&self) -> &IbcEndpoint; @@ -112,7 +114,7 @@ pub trait TransferProtocol { fn common_to_protocol_packet( &self, packet: TransferPacketCommon>, - ) -> Result; + ) -> Result; fn ack_success() -> Self::Ack; @@ -125,22 +127,22 @@ pub trait TransferProtocol { fn send_tokens( &mut self, - sender: &AddrOf, - receiver: &AddrOf, + sender: &AddrOf, + receiver: &AddrOf, tokens: Vec, ) -> Result>, Self::Error>; fn send_tokens_success( &mut self, - sender: &AddrOf, - receiver: &AddrOf, + sender: &AddrOf, + receiver: &AddrOf, tokens: Vec, ) -> Result>, Self::Error>; fn send_tokens_failure( &mut self, - sender: &AddrOf, - receiver: &AddrOf, + sender: &AddrOf, + receiver: &AddrOf, tokens: Vec, ) -> Result>, Self::Error>; @@ -194,15 +196,14 @@ pub trait TransferProtocol { ])) } - fn send_ack( + fn send_ack_relay( &mut self, ibc_packet: IbcPacketAckMsg, ) -> Result, Self::Error> { // NOTE: `ibc_packet.original_packet` here refers to the packet that is being acknowledged, not the // original packet in the pfm chain. At this point in the ack handling process, we don't even know // if this is a pfm message anyways. - - let packet = Self::Packet::decode(ibc_packet.original_packet.data.as_slice())?; + let packet = Self::TokenPacket::decode(ibc_packet.original_packet.data.as_slice())?; // https://github.com/cosmos/ibc-go/blob/5ca37ef6e56a98683cf2b3b1570619dc9b322977/modules/apps/transfer/ibc_module.go#L261 let ack: GenericAck = Self::Ack::decode(ibc_packet.acknowledgement.data.as_slice())?.into(); @@ -274,11 +275,27 @@ pub trait TransferProtocol { .add_messages(ack_msgs)) } + fn send_ack_metadata( + &mut self, + ibc_packet: IbcPacketAckMsg, + ) -> Result, Self::Error> { + } + + fn send_ack( + &mut self, + ibc_packet: IbcPacketAckMsg, + ) -> Result, Self::Error> { + match PacketTag::decode(ibc_packet.original_packet.data.as_slice())? { + PacketTag::Relay => self.send_ack_relay(ibc_packet), + PacketTag::Metadata => todo!(), + } + } + fn send_timeout( &mut self, ibc_packet: IbcPacket, ) -> Result, Self::Error> { - let packet = Self::Packet::decode(ibc_packet.clone().data.as_slice())?; + let packet = Self::TokenPacket::decode(ibc_packet.clone().data.as_slice())?; // same branch as failure ack let memo = packet.extension().to_string(); let ack = GenericAck::Err(ACK_ERR_TIMEOUT_MSG.to_vec()); @@ -317,13 +334,13 @@ pub trait TransferProtocol { #[allow(clippy::type_complexity)] fn receive_transfer( &mut self, - receiver: &AddrOf, + receiver: &AddrOf, tokens: Vec, ) -> Result<(Vec, Vec>), Self::Error>; fn receive(&mut self, original_packet: IbcPacket) -> IbcReceiveResponse { let handle = || -> Result, Self::Error> { - let packet = Self::Packet::decode(original_packet.data.as_slice())?; + let packet = Self::TokenPacket::decode(original_packet.data.as_slice())?; let memo = packet.extension().to_string(); @@ -389,7 +406,7 @@ pub trait TransferProtocol { /// Extracts and processes the forward information from a messages memo. Initiates the forward transfer process. fn packet_forward( &mut self, - packet: Self::Packet, + packet: Self::TokenPacket, original_packet: IbcPacket, forward: PacketForward, processed: bool, @@ -419,7 +436,7 @@ pub trait TransferProtocol { ack: GenericAck, ibc_packet: IbcPacket, refund_info: InFlightPfmPacket, - sender: &AddrOf, + sender: &AddrOf, tokens: Vec, ) -> Result<(Vec>, Vec), Self::Error>; diff --git a/cosmwasm/ucs01-relay-api/src/types.rs b/cosmwasm/ucs01-relay-api/src/types.rs index cc1d5d01a6..e51e28a0d0 100644 --- a/cosmwasm/ucs01-relay-api/src/types.rs +++ b/cosmwasm/ucs01-relay-api/src/types.rs @@ -21,6 +21,8 @@ pub enum EncodingError { InvalidSender { value: String, err: StdError }, #[error("Invalid receiver address: receiver: `{value}`, err: {err}")] InvalidReceiver { value: String, err: StdError }, + #[error("Invalid packet tag: {value}")] + InvalidPacketTag { value: u8 }, } /// A json encoding specific to [`serde_json_wasm`] as it does not use the same error types as `serde_json`. @@ -122,6 +124,11 @@ impl Ucs01TransferPacket { impl Encode for Ucs01TransferPacket { fn encode(self) -> Vec { ethabi::encode(&[ + Token::Uint( + num_traits::ToPrimitive::to_u8(&PacketTag::Relay) + .expect("impossible") + .into(), + ), Token::Bytes(self.sender.into()), Token::Bytes(self.receiver.into()), Token::Array( @@ -146,6 +153,8 @@ impl Decode for Ucs01TransferPacket { fn decode(bytes: &[u8]) -> Result { let encoded_packet = ethabi::decode( &[ + // The packet tag + ParamType::Uint(8), ParamType::Bytes, ParamType::Bytes, ParamType::Array(Box::new(ParamType::Tuple(vec![ @@ -163,7 +172,8 @@ impl Decode for Ucs01TransferPacket { // NOTE: at this point, it is technically impossible to have any other branch than the one we // match unless there is a bug in the underlying `ethabi` crate match &encoded_packet[..] { - [Token::Bytes(sender), Token::Bytes(receiver), Token::Array(tokens), Token::String(memo)] => { + // Discard the tag + [Token::Uint(_), Token::Bytes(sender), Token::Bytes(receiver), Token::Array(tokens), Token::String(memo)] => { Ok(Ucs01TransferPacket { sender: sender.clone().into(), receiver: receiver.clone().into(), @@ -372,6 +382,127 @@ impl<'a> From<(&'a str, &IbcEndpoint)> for DenomOrigin<'a> { } } +pub struct Ucs01Metadata { + pub denom: String, + pub name: String, + pub symbol: String, + pub decimals: u8, +} + +pub struct Ucs01MetadataPacket { + pub tokens_metadata: Vec, +} + +impl Encode for Ucs01MetadataPacket { + fn encode(self) -> Vec { + ethabi::encode(&[ + Token::Uint( + num_traits::ToPrimitive::to_u8(&PacketTag::Metadata) + .expect("impossible") + .into(), + ), + Token::Array( + self.tokens_metadata + .into_iter() + .map( + |Ucs01Metadata { + denom, + name, + symbol, + decimals, + }| { + Token::Tuple(vec![ + Token::String(denom), + Token::String(name), + Token::String(symbol), + Token::Uint(decimals.into()), + ]) + }, + ) + .collect(), + ), + ]) + } +} + +impl Decode for Ucs01MetadataPacket { + type Error = EncodingError; + + fn decode(bytes: &[u8]) -> Result { + let encoded_packet = ethabi::decode( + &[ + ParamType::Uint(8), + ParamType::Array(Box::new(ParamType::Tuple(vec![ + ParamType::String, + ParamType::String, + ParamType::String, + ParamType::String, + ParamType::Bytes, + ParamType::Uint(8), + ]))), + ], + bytes, + ) + .map_err(|err| EncodingError::InvalidUCS01PacketEncoding { + value: bytes.to_vec(), + err, + })?; + match &encoded_packet[..] { + [Token::Uint(_), Token::Array(tokens_metadata)] => { + Ok(Ucs01MetadataPacket { + tokens_metadata: tokens_metadata + .iter() + .map(|token_metadata| { + if let Token::Tuple(encoded_token_metadata_inner) = token_metadata { + match &encoded_token_metadata_inner[..] { + [Token::String(denom), Token::String(name), Token::String(symbol), Token::Uint(decimals)] => { + Ucs01Metadata { + denom: denom.clone(), + name: name.clone(), + symbol: symbol.clone(), + decimals: decimals.as_u128() as u8, + } + } + _ => unreachable!(), + } + } else { + unreachable!() + } + }) + .collect(), + }) + } + _ => unreachable!(), + } + } +} + +#[derive(FromPrimitive, ToPrimitive)] +pub enum PacketTag { + Relay = 0, + Metadata = 1, +} + +impl Decode for PacketTag { + type Error = EncodingError; + + fn decode(bytes: &[u8]) -> Result { + match ethabi::decode(&[ParamType::Uint(8)], bytes) { + Ok(tokens) => match &tokens[..] { + &[Token::Uint(tag)] => num_traits::FromPrimitive::from_u8(tag.as_u128() as u8) + .ok_or(EncodingError::InvalidPacketTag { + value: tag.as_u128() as u8, + }), + _ => unreachable!(), + }, + Err(err) => Err(EncodingError::InvalidUCS01PacketEncoding { + value: bytes.to_vec(), + err, + }), + } + } +} + #[cfg(test)] mod tests { use cosmwasm_std::{IbcEndpoint, Uint128}; diff --git a/cosmwasm/ucs01-relay/src/protocol.rs b/cosmwasm/ucs01-relay/src/protocol.rs index 6493f951cd..7cf31a466d 100644 --- a/cosmwasm/ucs01-relay/src/protocol.rs +++ b/cosmwasm/ucs01-relay/src/protocol.rs @@ -60,7 +60,7 @@ pub trait TransferProtocolExt<'a>: ibc_packet: IbcPacket, refund_info: InFlightPfmPacket, ack: Result, Vec>, - sender: &AddrOf, + sender: &AddrOf, tokens: Vec, ) -> Result<(Vec>, Vec), Self::Error> { let ack = @@ -607,7 +607,7 @@ impl<'a> TransferProtocol for Ics20Protocol<'a> { const ORDERING: IbcOrder = IbcOrder::Unordered; const RECEIVE_REPLY_ID: u64 = 0; - type Packet = Ics20Packet; + type TokenPacket = Ics20Packet; type Ack = Ics20Ack; type CustomMsg = TokenFactoryMsg; type Error = ContractError; @@ -679,7 +679,7 @@ impl<'a> TransferProtocol for Ics20Protocol<'a> { #[allow(clippy::type_complexity)] fn receive_transfer( &mut self, - receiver: &AddrOf, + receiver: &AddrOf, tokens: Vec, ) -> Result<(Vec, Vec>), ContractError> { let (tokens, msgs) = StatefulOnReceive { @@ -719,13 +719,13 @@ impl<'a> TransferProtocol for Ics20Protocol<'a> { packet: ucs01_relay_api::types::TransferPacketCommon< ucs01_relay_api::protocol::PacketExtensionOf, >, - ) -> Result { + ) -> Result { Ics20Packet::try_from(packet) } fn packet_forward( &mut self, - packet: Self::Packet, + packet: Self::TokenPacket, original_packet: IbcPacket, forward: PacketForward, processed: bool, @@ -792,7 +792,7 @@ impl<'a> TransferProtocol for Ics20Protocol<'a> { ack: GenericAck, ibc_packet: IbcPacket, refund_info: InFlightPfmPacket, - sender: &AddrOf, + sender: &AddrOf, tokens: Vec, ) -> Result<(Vec>, Vec), Self::Error> { self.do_pfm_ack(ibc_packet, refund_info, ack, sender, tokens) @@ -832,7 +832,7 @@ impl<'a> TransferProtocol for Ucs01Protocol<'a> { const ORDERING: IbcOrder = IbcOrder::Unordered; const RECEIVE_REPLY_ID: u64 = 1; - type Packet = Ucs01TransferPacket; + type TokenPacket = Ucs01TransferPacket; type Ack = Ucs01Ack; type CustomMsg = TokenFactoryMsg; type Error = ContractError; @@ -906,7 +906,7 @@ impl<'a> TransferProtocol for Ucs01Protocol<'a> { #[allow(clippy::type_complexity)] fn receive_transfer( &mut self, - receiver: &AddrOf, + receiver: &AddrOf, tokens: Vec, ) -> Result<(Vec, Vec>), ContractError> { let receiver = self @@ -953,7 +953,7 @@ impl<'a> TransferProtocol for Ucs01Protocol<'a> { packet: ucs01_relay_api::types::TransferPacketCommon< ucs01_relay_api::protocol::PacketExtensionOf, >, - ) -> Result { + ) -> Result { Ok(Ucs01TransferPacket::new( self.common .deps @@ -977,7 +977,7 @@ impl<'a> TransferProtocol for Ucs01Protocol<'a> { fn packet_forward( &mut self, - packet: Self::Packet, + packet: Self::TokenPacket, original_packet: IbcPacket, forward: PacketForward, processed: bool, @@ -1054,7 +1054,7 @@ impl<'a> TransferProtocol for Ucs01Protocol<'a> { ack: GenericAck, ibc_packet: IbcPacket, refund_info: InFlightPfmPacket, - sender: &AddrOf, + sender: &AddrOf, tokens: Vec, ) -> Result<(Vec>, Vec), Self::Error> { self.do_pfm_ack(ibc_packet, refund_info, ack, sender, tokens) diff --git a/evm/contracts/apps/ucs/01-relay/ERC20Denom.sol b/evm/contracts/apps/ucs/01-relay/ERC20Denom.sol index b1830a31a3..4359a06738 100644 --- a/evm/contracts/apps/ucs/01-relay/ERC20Denom.sol +++ b/evm/contracts/apps/ucs/01-relay/ERC20Denom.sol @@ -8,7 +8,12 @@ contract ERC20Denom is ERC20, IERC20Denom { address public admin; - constructor(string memory name) ERC20(name, name) { + // Metadata updated via UCS01 governance + string private _name; + string private _symbol; + uint8 private _decimals; + + constructor(string memory denomName) ERC20(denomName, denomName) { admin = msg.sender; } @@ -25,4 +30,44 @@ contract ERC20Denom is ERC20, IERC20Denom { } _burn(from, amount); } + + function update( + string calldata newName, + string calldata newSymbol, + uint8 newDecimals + ) external { + if (msg.sender != admin) { + revert ERC20Unauthorized(); + } + _name = newName; + _symbol = newSymbol; + _decimals = newDecimals; + } + + function name() + public + view + override(ERC20, IERC20Metadata) + returns (string memory) + { + return _name; + } + + function symbol() + public + view + override(ERC20, IERC20Metadata) + returns (string memory) + { + return _symbol; + } + + function decimals() + public + view + override(ERC20, IERC20Metadata) + returns (uint8) + { + return _decimals; + } } diff --git a/evm/contracts/apps/ucs/01-relay/IERC20Denom.sol b/evm/contracts/apps/ucs/01-relay/IERC20Denom.sol index fcbf50b572..950cb18c61 100644 --- a/evm/contracts/apps/ucs/01-relay/IERC20Denom.sol +++ b/evm/contracts/apps/ucs/01-relay/IERC20Denom.sol @@ -1,9 +1,16 @@ pragma solidity ^0.8.23; import "@openzeppelin/token/ERC20/IERC20.sol"; +import "@openzeppelin/token/ERC20/extensions/IERC20Metadata.sol"; -interface IERC20Denom is IERC20 { +interface IERC20Denom is IERC20, IERC20Metadata { function mint(address to, uint256 amount) external; function burn(address from, uint256 amount) external; + + function update( + string calldata name, + string calldata symbol, + uint8 decimals + ) external; } diff --git a/evm/contracts/apps/ucs/01-relay/Relay.sol b/evm/contracts/apps/ucs/01-relay/Relay.sol index 2267af3556..7d7d4ccf29 100644 --- a/evm/contracts/apps/ucs/01-relay/Relay.sol +++ b/evm/contracts/apps/ucs/01-relay/Relay.sol @@ -31,12 +31,27 @@ struct Token { } struct RelayPacket { + // This is a convenient addition to avoid having to shift the body by 1 byte to eliminate the tag. + uint8 tag; bytes sender; bytes receiver; Token[] tokens; string extension; } +struct TokenMetadata { + string denom; + string name; + string symbol; + uint8 decimals; +} + +struct UpdateMetadataPacket { + // This is a convenient addition to avoid having to shift the body by 1 byte to eliminate the tag. + uint8 tag; + TokenMetadata[] tokensMetadata; +} + interface IRelay is IIBCModule { function getDenomAddress( string memory sourceChannel, @@ -67,6 +82,9 @@ library RelayLib { error ErrInvalidProtocolVersion(); error ErrInvalidProtocolOrdering(); error ErrInvalidCounterpartyProtocolVersion(); + error ErrDenomNotFound(); + error ErrUnknownPacketTag(); + error ErrInvalidMetadataTimeout(); error ErrUnstoppable(); IbcCoreChannelV1GlobalEnums.Order public constant ORDER = @@ -76,6 +94,9 @@ library RelayLib { bytes1 public constant ACK_FAILURE = 0x00; uint256 public constant ACK_LENGTH = 1; + uint8 public constant PACKET_TAG_RELAY = 0; + uint8 public constant PACKET_TAG_UPDATE_METADATA = 1; + event DenomCreated( uint64 indexed packetSequence, string channelId, @@ -110,6 +131,26 @@ library RelayLib { uint256 amount ); + event TokenMetadataUpdate( + uint64 packetSequence, + string channelId, + address indexed token, + string indexed denom, + string name, + string symbol, + uint8 decimals + ); + + event TokenMetadataUpdated( + uint64 packetSequence, + string channelId, + address indexed token, + string indexed denom, + string name, + string symbol, + uint8 decimals + ); + function isValidVersion(string memory version) internal pure @@ -158,7 +199,11 @@ library RelayPacketLib { returns (bytes memory) { return abi.encode( - packet.sender, packet.receiver, packet.tokens, packet.extension + packet.tag, + packet.sender, + packet.receiver, + packet.tokens, + packet.extension ); } @@ -175,6 +220,28 @@ library RelayPacketLib { } } +library UpdateMetadataPacketLib { + function encode(UpdateMetadataPacket memory packet) + internal + pure + returns (bytes memory) + { + return abi.encode(packet.tag, packet.tokensMetadata); + } + + function decode(bytes calldata stream) + internal + pure + returns (UpdateMetadataPacket calldata) + { + UpdateMetadataPacket calldata packet; + assembly { + packet := stream.offset + } + return packet; + } +} + contract UCS01Relay is IBCAppBase, IRelay, @@ -184,17 +251,19 @@ contract UCS01Relay is PausableUpgradeable { using RelayPacketLib for RelayPacket; + using UpdateMetadataPacketLib for UpdateMetadataPacket; using LibString for *; using strings for *; IIBCPacket private ibcHandler; - // A mapping from remote denom to local ERC20 wrapper. mapping(string => mapping(string => address)) private denomToAddress; // A mapping from a local ERC20 wrapper to the remote denom. // Required to determine whether an ERC20 token is originating from a remote chain. mapping(string => mapping(address => string)) private addressToDenom; mapping(string => mapping(address => uint256)) private outstanding; + // Max number of a seconds for a metadata update packet to timeout. + uint64 updateMetadataMaxTimeout; constructor() { _disableInitializers(); @@ -206,6 +275,14 @@ contract UCS01Relay is ) public initializer { __Ownable_init(admin); ibcHandler = _ibcHandler; + updateMetadataMaxTimeout = 3600; + } + + function setUpdateMetadataMaxTimeout(uint64 _updateMetadataMaxTimeout) + public + onlyOwner + { + updateMetadataMaxTimeout = _updateMetadataMaxTimeout; } function ibcAddress() public view virtual override returns (address) { @@ -248,6 +325,50 @@ contract UCS01Relay is outstanding[sourceChannel][token] -= amount; } + // Dispatch a metadata update for the given tokens. + // The message will be handled by the counterparty protocol to update wrapped assets metadata. + function sendMetadataUpdate( + string calldata sourceChannel, + address[] calldata tokens, + uint64 timeoutTimestamp + ) public { + uint64 currentTimestamp = uint64(block.timestamp) * 1e9; + if (timeoutTimestamp - currentTimestamp > updateMetadataMaxTimeout) { + revert RelayLib.ErrInvalidMetadataTimeout(); + } + uint256 tokensLength = tokens.length; + TokenMetadata[] memory tokensMetadata = + new TokenMetadata[](tokens.length); + for (uint256 i; i < tokensLength; i++) { + IERC20Denom token = IERC20Denom(tokens[i]); + tokensMetadata[i].denom = address(token).toHexString(); + tokensMetadata[i].name = token.name(); + tokensMetadata[i].symbol = token.symbol(); + tokensMetadata[i].decimals = token.decimals(); + } + UpdateMetadataPacket memory packet = UpdateMetadataPacket({ + tag: RelayLib.PACKET_TAG_UPDATE_METADATA, + tokensMetadata: tokensMetadata + }); + uint64 packetSequence = ibcHandler.sendPacket( + sourceChannel, + IbcCoreClientV1Height.Data({revision_number: 0, revision_height: 0}), + timeoutTimestamp, + packet.encode() + ); + for (uint256 i; i < tokensLength; i++) { + emit RelayLib.TokenMetadataUpdate( + packetSequence, + sourceChannel, + tokens[i], + tokensMetadata[i].denom, + tokensMetadata[i].name, + tokensMetadata[i].symbol, + tokensMetadata[i].decimals + ); + } + } + // Internal function // Send the given token over the specified channel. // If token is native, we increase the oustanding amount and escrow it. Otherwise, we burn the amount. @@ -295,6 +416,7 @@ contract UCS01Relay is normalizedTokens[i].amount = localToken.amount; } RelayPacket memory packet = RelayPacket({ + tag: RelayLib.PACKET_TAG_RELAY, sender: abi.encodePacked(msg.sender), receiver: receiver, tokens: normalizedTokens, @@ -355,13 +477,59 @@ contract UCS01Relay is } } - function onRecvPacketProcessing( - IbcCoreChannelV1Packet.Data calldata ibcPacket, - address - ) public { - if (msg.sender != address(this)) { - revert RelayLib.ErrUnauthorized(); + function onRecvUpdateMetadataPacket( + IbcCoreChannelV1Packet.Data calldata ibcPacket + ) internal { + UpdateMetadataPacket calldata packet = + UpdateMetadataPacketLib.decode(ibcPacket.data); + uint256 packetTokensMetadataLength = packet.tokensMetadata.length; + for (uint256 i; i < packetTokensMetadataLength; i++) { + TokenMetadata calldata tokenMetadata = packet.tokensMetadata[i]; + string memory denom = RelayLib.makeForeignDenom( + ibcPacket.destination_port, + ibcPacket.destination_channel, + tokenMetadata.denom + ); + address denomAddress = getOrCreateWrapper( + ibcPacket.sequence, + ibcPacket.destination_channel, + denom + ); + IERC20Denom(denomAddress).update( + tokenMetadata.name, tokenMetadata.symbol, tokenMetadata.decimals + ); + emit RelayLib.TokenMetadataUpdated( + ibcPacket.sequence, + ibcPacket.destination_channel, + denomAddress, + tokenMetadata.denom, + tokenMetadata.name, + tokenMetadata.symbol, + tokenMetadata.decimals + ); } + } + + function getOrCreateWrapper( + uint64 packetSequence, + string memory sourceChannel, + string memory denom + ) internal returns (address) { + address denomAddress = denomToAddress[sourceChannel][denom]; + if (denomAddress == address(0)) { + denomAddress = address(new ERC20Denom{salt: keccak256(bytes(denom))}(denom)); + denomToAddress[sourceChannel][denom] = denomAddress; + addressToDenom[sourceChannel][denomAddress] = denom; + emit RelayLib.DenomCreated( + packetSequence, sourceChannel, denom, denomAddress + ); + } + return denomAddress; + } + + function onRecvRelayPacket(IbcCoreChannelV1Packet.Data calldata ibcPacket) + internal + { RelayPacket calldata packet = RelayPacketLib.decode(ibcPacket.data); string memory prefix = RelayLib.makeDenomPrefix( ibcPacket.source_port, ibcPacket.source_channel @@ -369,18 +537,14 @@ contract UCS01Relay is uint256 packetTokensLength = packet.tokens.length; for (uint256 i; i < packetTokensLength; i++) { Token memory token = packet.tokens[i]; - strings.slice memory denomSlice = token.denom.toSlice(); - // This will trim the denom in-place IFF it is prefixed - strings.slice memory trimedDenom = - denomSlice.beyond(prefix.toSlice()); address receiver = RelayLib.bytesToAddress(packet.receiver); address denomAddress; string memory denom; - if (!denomSlice.equals(token.denom.toSlice())) { + if (token.denom.startsWith(prefix)) { // In this branch the token was originating from // this chain as it was prefixed by the local channel/port. // We need to unescrow the amount. - denom = trimedDenom.toString(); + denom = token.denom.toSlice().beyond(prefix.toSlice()).toString(); // It's an ERC20 string 0x prefixed hex address denomAddress = Hex.hexToAddress(denom); // The token must be outstanding. @@ -396,21 +560,9 @@ contract UCS01Relay is ibcPacket.destination_channel, token.denom ); - denomAddress = - denomToAddress[ibcPacket.destination_channel][denom]; - if (denomAddress == address(0)) { - denomAddress = address(new ERC20Denom(token.denom)); - denomToAddress[ibcPacket.destination_channel][denom] = - denomAddress; - addressToDenom[ibcPacket.destination_channel][denomAddress] - = denom; - emit RelayLib.DenomCreated( - ibcPacket.sequence, - ibcPacket.source_channel, - denom, - denomAddress - ); - } + denomAddress = getOrCreateWrapper( + ibcPacket.sequence, ibcPacket.destination_channel, denom + ); IERC20Denom(denomAddress).mint(receiver, token.amount); } emit RelayLib.Received( @@ -425,6 +577,23 @@ contract UCS01Relay is } } + function onRecvPacketProcessing( + IbcCoreChannelV1Packet.Data calldata ibcPacket, + address + ) public { + if (msg.sender != address(this)) { + revert RelayLib.ErrUnauthorized(); + } + uint8 tag = abi.decode(ibcPacket.data, (uint8)); + if (tag == RelayLib.PACKET_TAG_RELAY) { + onRecvRelayPacket(ibcPacket); + } else if (tag == RelayLib.PACKET_TAG_UPDATE_METADATA) { + onRecvUpdateMetadataPacket(ibcPacket); + } else { + revert RelayLib.ErrUnknownPacketTag(); + } + } + function onRecvPacket( IbcCoreChannelV1Packet.Data calldata ibcPacket, address relayer @@ -463,13 +632,16 @@ contract UCS01Relay is ) { revert RelayLib.ErrInvalidAcknowledgement(); } - // Counterparty failed to execute the transfer, we refund. if (acknowledgement[0] == RelayLib.ACK_FAILURE) { - refundTokens( - ibcPacket.sequence, - ibcPacket.source_channel, - RelayPacketLib.decode(ibcPacket.data) - ); + uint8 tag = abi.decode(ibcPacket.data, (uint8)); + // Counterparty failed to execute the transfer, we refund. + if (tag == RelayLib.PACKET_TAG_RELAY) { + refundTokens( + ibcPacket.sequence, + ibcPacket.source_channel, + RelayPacketLib.decode(ibcPacket.data) + ); + } } } @@ -477,11 +649,14 @@ contract UCS01Relay is IbcCoreChannelV1Packet.Data calldata ibcPacket, address ) external override(IBCAppBase, IIBCModule) onlyIBC { - refundTokens( - ibcPacket.sequence, - ibcPacket.source_channel, - RelayPacketLib.decode(ibcPacket.data) - ); + uint8 tag = abi.decode(ibcPacket.data, (uint8)); + if (tag == RelayLib.PACKET_TAG_RELAY) { + refundTokens( + ibcPacket.sequence, + ibcPacket.source_channel, + RelayPacketLib.decode(ibcPacket.data) + ); + } } function onChanOpenInit( diff --git a/evm/evm.nix b/evm/evm.nix index bac01628b1..a480dd99bf 100644 --- a/evm/evm.nix +++ b/evm/evm.nix @@ -278,9 +278,9 @@ buildInputs = [ wrappedForge ]; buildPhase = '' forge --version - FOUNDRY_PROFILE=script forge build --sizes + FOUNDRY_PROFILE=script forge build ''; - doCheck = false; + doCheck = true; checkPhase = '' FOUNDRY_PROFILE=test forge test -vvv --out=tests-out --cache-path=tests-cache ''; diff --git a/evm/tests/src/25-handler/IBCPacketHandler.t.sol b/evm/tests/src/25-handler/IBCPacketHandler.t.sol index 7600606334..680d64bbd4 100644 --- a/evm/tests/src/25-handler/IBCPacketHandler.t.sol +++ b/evm/tests/src/25-handler/IBCPacketHandler.t.sol @@ -1152,16 +1152,11 @@ contract IBCPacketHandlerTest is TestPlus { function test_timeoutPacket_timestamp_ok( address relayer, - uint64 timeoutTimestamp, bytes memory payload ) public { vm.assume(relayer != address(0) && relayer != address(app)); - vm.assume(timeoutTimestamp > LATEST_TIMESTAMP + 1); - vm.assume( - timeoutTimestamp - < (type(uint64).max / 1e9) - Cometbls.MAX_CLOCK_DRIFT - 1 - ); vm.prank(address(app)); + uint64 timeoutTimestamp = LATEST_TIMESTAMP + 2; handler.sendPacket( channelId, ClientHeight.Data({revision_number: 0, revision_height: 0}), @@ -1195,15 +1190,10 @@ contract IBCPacketHandlerTest is TestPlus { function test_timeoutPacket_timestamp_notReached( address relayer, - uint64 timeoutTimestamp, bytes memory payload ) public { vm.assume(relayer != address(0) && relayer != address(app)); - vm.assume(timeoutTimestamp > LATEST_TIMESTAMP + 1); - vm.assume( - timeoutTimestamp - < (type(uint64).max / 1e9) - Cometbls.MAX_CLOCK_DRIFT - 1 - ); + uint64 timeoutTimestamp = LATEST_TIMESTAMP + 2; vm.prank(address(app)); handler.sendPacket( channelId, diff --git a/evm/tests/src/apps/ucs/01-relay/Relay.t.sol b/evm/tests/src/apps/ucs/01-relay/Relay.t.sol index bf1fa70011..e0baee262f 100644 --- a/evm/tests/src/apps/ucs/01-relay/Relay.t.sol +++ b/evm/tests/src/apps/ucs/01-relay/Relay.t.sol @@ -222,6 +222,7 @@ contract RelayTests is Test { destination_channel: destinationChannel, data: RelayPacketLib.encode( RelayPacket({ + tag: RelayLib.PACKET_TAG_RELAY, sender: sender, receiver: abi.encodePacked(receiver), tokens: tokens, @@ -495,6 +496,7 @@ contract RelayTests is Test { } function test_onRecvPacketProcessing_onlySelf( + address malicious, uint64 sequence, string memory sourcePort, string memory sourceChannel, @@ -505,8 +507,9 @@ contract RelayTests is Test { uint64 timeoutTimestamp, address relayer ) public { + vm.assume(malicious != address(relay)); vm.expectRevert(RelayLib.ErrUnauthorized.selector); - vm.prank(address(ibcHandler)); + vm.prank(address(malicious)); UCS01Relay(address(relay)).onRecvPacketProcessing( IbcCoreChannelV1Packet.Data({ sequence: sequence, @@ -593,6 +596,7 @@ contract RelayTests is Test { destination_channel: destinationChannel, data: RelayPacketLib.encode( RelayPacket({ + tag: RelayLib.PACKET_TAG_RELAY, sender: abi.encodePacked(sender), receiver: abi.encodePacked(sender), tokens: tokens, @@ -613,148 +617,160 @@ contract RelayTests is Test { assertEq(writes.length, 0); } - function test_receive_localToken( - uint64 sequence, - string memory sourcePort, - string memory sourceChannel, - string memory destinationPort, - string memory destinationChannel, - uint64 timeoutRevisionNumber, - uint64 timeoutRevisionHeight, - uint64 timeoutTimestamp, - address sender, - bytes memory receiver, - address relayer, - string memory denomName, - uint128 amount, - string memory extension - ) public { - vm.assume(sender != address(0)); - vm.assume(relayer != address(0)); - vm.assume(amount > 0); + struct ReceiveLocalToken { + uint64 sequence; + string sourcePort; + string sourceChannel; + string destinationPort; + string destinationChannel; + uint64 timeoutRevisionNumber; + uint64 timeoutRevisionHeight; + uint64 timeoutTimestamp; + address sender; + bytes receiver; + address relayer; + string denomName; + uint128 amount; + string extension; + } + + function test_receive_localToken(ReceiveLocalToken memory args) public { + vm.assume(args.sender != address(0)); + vm.assume(args.relayer != address(0)); + vm.assume(args.amount > 0); initChannel( - sourcePort, sourceChannel, destinationPort, destinationChannel + args.sourcePort, + args.sourceChannel, + args.destinationPort, + args.destinationChannel ); - address denomAddress = address(new ERC20Denom(denomName)); - IERC20Denom(denomAddress).mint(address(sender), amount); + address denomAddress = address(new ERC20Denom(args.denomName)); + IERC20Denom(denomAddress).mint(address(args.sender), args.amount); LocalToken[] memory localTokens = new LocalToken[](1); localTokens[0].denom = denomAddress; - localTokens[0].amount = amount; + localTokens[0].amount = args.amount; - vm.prank(sender); - IERC20Denom(denomAddress).approve(address(relay), amount); + vm.prank(args.sender); + IERC20Denom(denomAddress).approve(address(relay), args.amount); // A single transfer without mint as the token was previously escrowed vm.expectEmit(); - emit IERC20.Transfer(address(sender), address(relay), amount); + emit IERC20.Transfer(address(args.sender), address(relay), args.amount); vm.expectEmit(false, false, false, false); emit RelayLib.Sent( - sequence, sourceChannel, address(0), "", "", address(0), 0 + args.sequence, args.sourceChannel, address(0), "", "", address(0), 0 ); - vm.prank(sender); + vm.prank(args.sender); relay.send( - destinationChannel, - receiver, + args.destinationChannel, + args.receiver, localTokens, - extension, + args.extension, IBCHeight.zero(), 0 ); Token[] memory tokens = new Token[](1); tokens[0].denom = RelayLib.makeForeignDenom( - sourcePort, sourceChannel, denomAddress.toHexString() + args.sourcePort, args.sourceChannel, denomAddress.toHexString() ); - tokens[0].amount = amount; + tokens[0].amount = args.amount; // A single transfer without mint as the token was previously escrowed vm.expectEmit(false, false, false, false); - emit IERC20.Transfer(address(0), address(sender), amount); + emit IERC20.Transfer(address(0), address(args.sender), args.amount); vm.expectEmit(false, false, false, false); emit RelayLib.Received( - sequence, sourceChannel, "", address(0), "", address(0), 0 + args.sequence, args.sourceChannel, "", address(0), "", address(0), 0 ); uint256 outstandingBefore = - relay.getOutstanding(destinationChannel, denomAddress); + relay.getOutstanding(args.destinationChannel, denomAddress); vm.prank(address(ibcHandler)); relay.onRecvPacket( IbcCoreChannelV1Packet.Data({ - sequence: sequence, - source_port: sourcePort, - source_channel: sourceChannel, - destination_port: destinationPort, - destination_channel: destinationChannel, + sequence: args.sequence, + source_port: args.sourcePort, + source_channel: args.sourceChannel, + destination_port: args.destinationPort, + destination_channel: args.destinationChannel, data: RelayPacketLib.encode( RelayPacket({ - sender: receiver, - receiver: abi.encodePacked(sender), + tag: RelayLib.PACKET_TAG_RELAY, + sender: args.receiver, + receiver: abi.encodePacked(args.sender), tokens: tokens, - extension: extension + extension: args.extension }) ), timeout_height: IbcCoreClientV1Height.Data({ - revision_number: timeoutRevisionNumber, - revision_height: timeoutRevisionHeight + revision_number: args.timeoutRevisionNumber, + revision_height: args.timeoutRevisionHeight }), - timeout_timestamp: timeoutTimestamp + timeout_timestamp: args.timeoutTimestamp }), - relayer + args.relayer ); // Local tokens are tracked, outstanding for the channel must be diminished by the amount assertEq( - relay.getOutstanding(destinationChannel, denomAddress) + amount, + relay.getOutstanding(args.destinationChannel, denomAddress) + + args.amount, outstandingBefore ); } - function test_receive_remoteToken( - uint64 sequence, - string memory sourcePort, - string memory sourceChannel, - string memory destinationPort, - string memory destinationChannel, - uint64 timeoutRevisionNumber, - uint64 timeoutRevisionHeight, - uint64 timeoutTimestamp, - bytes memory sender, - address receiver, - address relayer, - string memory denomName, - uint128 amount, - string memory extension - ) public { - vm.assume(receiver != address(0)); - vm.assume(relayer != address(0)); - vm.assume(amount > 0); + struct ReceiveRemoteToken { + uint64 sequence; + string sourcePort; + string sourceChannel; + string destinationPort; + string destinationChannel; + uint64 timeoutRevisionNumber; + uint64 timeoutRevisionHeight; + uint64 timeoutTimestamp; + bytes sender; + address receiver; + address relayer; + string denomName; + uint128 amount; + string extension; + } + + function test_receive_remoteToken(ReceiveRemoteToken memory args) public { + vm.assume(args.receiver != address(0)); + vm.assume(args.relayer != address(0)); + vm.assume(args.amount > 0); initChannel( - sourcePort, sourceChannel, destinationPort, destinationChannel + args.sourcePort, + args.sourceChannel, + args.destinationPort, + args.destinationChannel ); receiveRemoteToken( - sequence, - sourcePort, - sourceChannel, - destinationPort, - destinationChannel, - timeoutRevisionNumber, - timeoutRevisionHeight, - timeoutTimestamp, - sender, - receiver, - relayer, - denomName, - amount, - extension + args.sequence, + args.sourcePort, + args.sourceChannel, + args.destinationPort, + args.destinationChannel, + args.timeoutRevisionNumber, + args.timeoutRevisionHeight, + args.timeoutTimestamp, + args.sender, + args.receiver, + args.relayer, + args.denomName, + args.amount, + args.extension ); } @@ -849,49 +865,47 @@ contract RelayTests is Test { extension ); - { - address denomAddress = relay.getDenomAddress( - destinationChannel, - RelayLib.makeForeignDenom( - destinationPort, destinationChannel, denomName - ) - ); + address denomAddress = relay.getDenomAddress( + destinationChannel, + RelayLib.makeForeignDenom( + destinationPort, destinationChannel, denomName + ) + ); - LocalToken[] memory localTokens = new LocalToken[](1); - localTokens[0].denom = denomAddress; - localTokens[0].amount = amount; + LocalToken[] memory localTokens = new LocalToken[](1); + localTokens[0].denom = denomAddress; + localTokens[0].amount = amount; - vm.prank(receiver); - IERC20Denom(denomAddress).approve(address(relay), amount); + vm.prank(receiver); + IERC20Denom(denomAddress).approve(address(relay), amount); - // Burn from sender to zero - vm.expectEmit(); - emit IERC20.Transfer(address(receiver), address(0), amount); + // Burn from sender to zero + vm.expectEmit(); + emit IERC20.Transfer(address(receiver), address(0), amount); - vm.expectEmit(false, false, false, false); - emit RelayLib.Sent( - sequence, sourceChannel, address(0), "", "", address(0), 0 - ); + vm.expectEmit(false, false, false, false); + emit RelayLib.Sent( + sequence, sourceChannel, address(0), "", "", address(0), 0 + ); - uint256 outstandingBefore = - relay.getOutstanding(destinationChannel, denomAddress); + uint256 outstandingBefore = + relay.getOutstanding(destinationChannel, denomAddress); - vm.prank(receiver); - relay.send( - destinationChannel, - abi.encodePacked(receiver), - localTokens, - extension, - IBCHeight.zero(), - 0 - ); + vm.prank(receiver); + relay.send( + destinationChannel, + abi.encodePacked(receiver), + localTokens, + extension, + IBCHeight.zero(), + 0 + ); - uint256 outstandingAfter = - relay.getOutstanding(destinationChannel, denomAddress); + uint256 outstandingAfter = + relay.getOutstanding(destinationChannel, denomAddress); - // Remote tokens are not tracked as outstanding - assertEq(outstandingBefore, outstandingAfter); - } + // Remote tokens are not tracked as outstanding + assertEq(outstandingBefore, outstandingAfter); } function test_send_local_from_remote( @@ -996,62 +1010,76 @@ contract RelayTests is Test { } } - function test_receive_remote_no_collision( - uint64 sequence, - string memory destinationPort, - string memory sourcePort, - string memory sourceChannel, - uint64 timeoutRevisionNumber, - uint64 timeoutRevisionHeight, - uint64 timeoutTimestamp, - bytes memory sender, - address receiver, - address relayer, - string memory denomName, - uint128 amount, - string memory extension - ) public { - vm.assume(sequence < 1000000000); - vm.assume(receiver != address(0)); - vm.assume(relayer != address(0)); - vm.assume(amount > 0); + struct NoCollision { + uint64 sequence; + string destinationPort; + string sourcePort; + string sourceChannel; + uint64 timeoutRevisionNumber; + uint64 timeoutRevisionHeight; + uint64 timeoutTimestamp; + bytes sender; + address receiver; + address relayer; + string denomName; + uint128 amount; + string extension; + } + + function test_receive_remote_no_collision(NoCollision calldata args) + public + { + vm.assume(args.sequence < 1000000000); + vm.assume(args.receiver != address(0)); + vm.assume(args.relayer != address(0)); + vm.assume(args.amount > 0); // Open two different local channels with the same counterparty - initChannel(sourcePort, sourceChannel, destinationPort, "channel-1"); - initChannel(sourcePort, sourceChannel, destinationPort, "channel-2"); + initChannel( + args.sourcePort, + args.sourceChannel, + args.destinationPort, + "channel-1" + ); + initChannel( + args.sourcePort, + args.sourceChannel, + args.destinationPort, + "channel-2" + ); receiveRemoteToken( - sequence, - sourcePort, - sourceChannel, - destinationPort, + args.sequence, + args.sourcePort, + args.sourceChannel, + args.destinationPort, "channel-1", - timeoutRevisionNumber, - timeoutRevisionHeight, - timeoutTimestamp, - sender, - receiver, - relayer, - denomName, - amount, - extension + args.timeoutRevisionNumber, + args.timeoutRevisionHeight, + args.timeoutTimestamp, + args.sender, + args.receiver, + args.relayer, + args.denomName, + args.amount, + args.extension ); receiveRemoteToken( - sequence + 1, - sourcePort, - sourceChannel, - destinationPort, + args.sequence + 1, + args.sourcePort, + args.sourceChannel, + args.destinationPort, "channel-2", - timeoutRevisionNumber, - timeoutRevisionHeight, - timeoutTimestamp, - sender, - receiver, - relayer, - denomName, - amount, - extension + args.timeoutRevisionNumber, + args.timeoutRevisionHeight, + args.timeoutTimestamp, + args.sender, + args.receiver, + args.relayer, + args.denomName, + args.amount, + args.extension ); } @@ -1140,88 +1168,101 @@ contract RelayTests is Test { assertEq(relay.getOutstanding(destinationChannel, denomAddress), 0); } - function test_onTimeout_refund_remote( - uint64 sequence, - string memory sourcePort, - string memory sourceChannel, - string memory destinationPort, - string memory destinationChannel, - uint64 timeoutRevisionNumber, - uint64 timeoutRevisionHeight, - uint64 timeoutTimestamp, - bytes memory sender, - address receiver, - address relayer, - string memory denomName, - uint128 amount, - string memory extension - ) public { + struct OnTimeoutRefundRemote { + uint64 sequence; + string sourcePort; + string sourceChannel; + string destinationPort; + string destinationChannel; + uint64 timeoutRevisionNumber; + uint64 timeoutRevisionHeight; + uint64 timeoutTimestamp; + bytes sender; + address receiver; + address relayer; + string denomName; + uint128 amount; + string extension; + } + + function test_onTimeout_refund_remote(OnTimeoutRefundRemote memory args) + public + { vm.assume( !RelayLib.isFromChannel( - destinationPort, destinationChannel, denomName + args.destinationPort, args.destinationChannel, args.denomName ) ); - vm.assume(receiver != address(0)); - vm.assume(relayer != address(0)); - vm.assume(amount > 0); + vm.assume(args.receiver != address(0)); + vm.assume(args.relayer != address(0)); + vm.assume(args.amount > 0); initChannel( - sourcePort, sourceChannel, destinationPort, destinationChannel + args.sourcePort, + args.sourceChannel, + args.destinationPort, + args.destinationChannel ); receiveRemoteToken( - sequence, - sourcePort, - sourceChannel, - destinationPort, - destinationChannel, - timeoutRevisionNumber, - timeoutRevisionHeight, - timeoutTimestamp, - sender, - receiver, - relayer, - denomName, - amount, - extension + args.sequence, + args.sourcePort, + args.sourceChannel, + args.destinationPort, + args.destinationChannel, + args.timeoutRevisionNumber, + args.timeoutRevisionHeight, + args.timeoutTimestamp, + args.sender, + args.receiver, + args.relayer, + args.denomName, + args.amount, + args.extension ); address denomAddress = relay.getDenomAddress( - destinationChannel, + args.destinationChannel, RelayLib.makeForeignDenom( - destinationPort, destinationChannel, denomName + args.destinationPort, args.destinationChannel, args.denomName ) ); sendRemoteToken( - destinationPort, - destinationChannel, - sender, - receiver, + args.destinationPort, + args.destinationChannel, + args.sender, + args.receiver, denomAddress, - amount, - extension + args.amount, + args.extension ); IbcCoreChannelV1Packet.Data memory packet = ibcHandler.lastPacket(); vm.expectEmit(); - emit IERC20.Transfer(address(0), address(receiver), amount); + emit IERC20.Transfer(address(0), address(args.receiver), args.amount); vm.expectEmit(false, false, false, false); emit RelayLib.Refunded( - sequence, sourceChannel, address(0), "", "", address(this), 0 + args.sequence, + args.sourceChannel, + address(0), + "", + "", + address(this), + 0 ); uint256 outstandingBefore = - relay.getOutstanding(destinationChannel, denomAddress); + relay.getOutstanding(args.destinationChannel, denomAddress); vm.prank(address(ibcHandler)); - relay.onTimeoutPacket(packet, relayer); + relay.onTimeoutPacket(packet, args.relayer); // Outstanding must not be touched assertEq( - relay.getOutstanding(destinationChannel, denomAddress), + relay.getOutstanding(args.destinationChannel, denomAddress), outstandingBefore ); } @@ -1277,90 +1318,103 @@ contract RelayTests is Test { assertEq(relay.getOutstanding(destinationChannel, denomAddress), 0); } - function test_ack_failure_refund_remote( - uint64 sequence, - string memory sourcePort, - string memory sourceChannel, - string memory destinationPort, - string memory destinationChannel, - uint64 timeoutRevisionNumber, - uint64 timeoutRevisionHeight, - uint64 timeoutTimestamp, - bytes memory sender, - address receiver, - address relayer, - string memory denomName, - uint128 amount, - string memory extension - ) public { + struct AckFailureRefundRemote { + uint64 sequence; + string sourcePort; + string sourceChannel; + string destinationPort; + string destinationChannel; + uint64 timeoutRevisionNumber; + uint64 timeoutRevisionHeight; + uint64 timeoutTimestamp; + bytes sender; + address receiver; + address relayer; + string denomName; + uint128 amount; + string extension; + } + + function test_ack_failure_refund_remote(AckFailureRefundRemote memory args) + public + { vm.assume( !RelayLib.isFromChannel( - destinationPort, destinationChannel, denomName + args.destinationPort, args.destinationChannel, args.denomName ) ); - vm.assume(receiver != address(0)); - vm.assume(relayer != address(0)); - vm.assume(amount > 0); + vm.assume(args.receiver != address(0)); + vm.assume(args.relayer != address(0)); + vm.assume(args.amount > 0); initChannel( - sourcePort, sourceChannel, destinationPort, destinationChannel + args.sourcePort, + args.sourceChannel, + args.destinationPort, + args.destinationChannel ); receiveRemoteToken( - sequence, - sourcePort, - sourceChannel, - destinationPort, - destinationChannel, - timeoutRevisionNumber, - timeoutRevisionHeight, - timeoutTimestamp, - sender, - receiver, - relayer, - denomName, - amount, - extension + args.sequence, + args.sourcePort, + args.sourceChannel, + args.destinationPort, + args.destinationChannel, + args.timeoutRevisionNumber, + args.timeoutRevisionHeight, + args.timeoutTimestamp, + args.sender, + args.receiver, + args.relayer, + args.denomName, + args.amount, + args.extension ); address denomAddress = relay.getDenomAddress( - destinationChannel, + args.destinationChannel, RelayLib.makeForeignDenom( - destinationPort, destinationChannel, denomName + args.destinationPort, args.destinationChannel, args.denomName ) ); sendRemoteToken( - destinationPort, - destinationChannel, - sender, - receiver, + args.destinationPort, + args.destinationChannel, + args.sender, + args.receiver, denomAddress, - amount, - extension + args.amount, + args.extension ); IbcCoreChannelV1Packet.Data memory packet = ibcHandler.lastPacket(); vm.expectEmit(); - emit IERC20.Transfer(address(0), address(receiver), amount); + emit IERC20.Transfer(address(0), address(args.receiver), args.amount); vm.expectEmit(false, false, false, false); emit RelayLib.Refunded( - sequence, sourceChannel, address(0), "", "", address(this), 0 + args.sequence, + args.sourceChannel, + address(0), + "", + "", + address(this), + 0 ); uint256 outstandingBefore = - relay.getOutstanding(destinationChannel, denomAddress); + relay.getOutstanding(args.destinationChannel, denomAddress); vm.prank(address(ibcHandler)); relay.onAcknowledgementPacket( - packet, abi.encodePacked(RelayLib.ACK_FAILURE), relayer + packet, abi.encodePacked(RelayLib.ACK_FAILURE), args.relayer ); // Outstanding must not be touched assertEq( - relay.getOutstanding(destinationChannel, denomAddress), + relay.getOutstanding(args.destinationChannel, denomAddress), outstandingBefore ); } @@ -1478,4 +1532,105 @@ contract RelayTests is Test { (, bytes32[] memory writes) = vm.accesses(address(relay)); assertEq(writes.length, 0); } + + struct UpdateTokenMetadataArg { + string name; + string symbol; + uint8 decimals; + } + + function test_updateTokenMetadata_ok( + ReceiveRemoteToken memory args, + UpdateTokenMetadataArg memory updateArgs + ) public { + vm.assume(args.receiver != address(0)); + vm.assume(args.relayer != address(0)); + vm.assume(args.sequence > 1); + vm.assume(args.amount > 0); + vm.assume( + keccak256(bytes(args.denomName)) + != keccak256(bytes(updateArgs.name)) + ); + + initChannel( + args.sourcePort, + args.sourceChannel, + args.destinationPort, + args.destinationChannel + ); + + receiveRemoteToken( + args.sequence - 1, + args.sourcePort, + args.sourceChannel, + args.destinationPort, + args.destinationChannel, + args.timeoutRevisionNumber, + args.timeoutRevisionHeight, + args.timeoutTimestamp, + args.sender, + args.receiver, + args.relayer, + args.denomName, + args.amount, + args.extension + ); + + IERC20Denom denom = IERC20Denom( + relay.getDenomAddress( + args.destinationChannel, + RelayLib.makeForeignDenom( + args.destinationPort, + args.destinationChannel, + args.denomName + ) + ) + ); + + TokenMetadata[] memory tokensMetadata = new TokenMetadata[](1); + tokensMetadata[0].denom = args.denomName; + tokensMetadata[0].name = updateArgs.name; + tokensMetadata[0].symbol = updateArgs.symbol; + tokensMetadata[0].decimals = updateArgs.decimals; + + vm.expectEmit(); + emit RelayLib.TokenMetadataUpdated( + args.sequence, + args.destinationChannel, + address(denom), + args.denomName, + updateArgs.name, + updateArgs.symbol, + updateArgs.decimals + ); + + vm.prank(address(ibcHandler)); + bytes memory ack = relay.onRecvPacket( + IbcCoreChannelV1Packet.Data({ + sequence: args.sequence, + source_port: args.sourcePort, + source_channel: args.sourceChannel, + destination_port: args.destinationPort, + destination_channel: args.destinationChannel, + data: UpdateMetadataPacketLib.encode( + UpdateMetadataPacket({ + tag: RelayLib.PACKET_TAG_UPDATE_METADATA, + tokensMetadata: tokensMetadata + }) + ), + timeout_height: IbcCoreClientV1Height.Data({ + revision_number: args.timeoutRevisionNumber, + revision_height: args.timeoutRevisionHeight + }), + timeout_timestamp: args.timeoutTimestamp + }), + args.relayer + ); + + assertEq(ack, abi.encodePacked(RelayLib.ACK_SUCCESS)); + + assertEq(updateArgs.name, denom.name()); + assertEq(updateArgs.symbol, denom.symbol()); + assertEq(updateArgs.decimals, denom.decimals()); + } }