// SPDX-License-Identifier: Apache 2 /// This module implements serialization and deserialization for token transfer /// with an arbitrary payload. This message is a specific Wormhole message /// payload for Token Bridge. /// /// In order to redeem these types of transfers, one must have an `EmitterCap` /// and the specified `redeemer` must agree with this capability. /// /// See `transfer_tokens_with_payload` and `complete_transfer_with_payload` /// modules for more details. module token_bridge::transfer_with_payload { use std::vector::{Self}; use sui::object::{Self, ID}; use wormhole::bytes::{Self}; use wormhole::cursor::{Self}; use wormhole::external_address::{Self, ExternalAddress}; use token_bridge::normalized_amount::{Self, NormalizedAmount}; friend token_bridge::transfer_tokens_with_payload; /// Message payload is not `TransferWithPayload`. const E_INVALID_PAYLOAD: u64 = 0; /// Message identifier. const PAYLOAD_ID: u8 = 3; /// Container that warehouses transfer information, including arbitrary /// payload. /// /// NOTE: This struct has `drop` because we do not want to require an /// integrator receiving transfer information to have to manually destroy. struct TransferWithPayload has drop { // Transfer amount. amount: NormalizedAmount, // Address of the token. Left-zero-padded if shorter than 32 bytes. token_address: ExternalAddress, // Chain ID of the token. token_chain: u16, // A.K.A. 32-byte representation of `EmitterCap`. redeemer: ExternalAddress, // Chain ID of the redeemer. redeemer_chain: u16, // Address of the message sender. sender: ExternalAddress, // An arbitrary payload. payload: vector, } /// Create new `TransferWithPayload` using a Token Bridge integrator's /// emitter cap ID as the sender. public(friend) fun new( sender: ID, amount: NormalizedAmount, token_address: ExternalAddress, token_chain: u16, redeemer: ExternalAddress, redeemer_chain: u16, payload: vector ): TransferWithPayload { TransferWithPayload { amount, token_address, token_chain, redeemer, redeemer_chain, sender: external_address::from_id(sender), payload } } #[test_only] public fun new_test_only( sender: ID, amount: NormalizedAmount, token_address: ExternalAddress, token_chain: u16, redeemer: ExternalAddress, redeemer_chain: u16, payload: vector ): TransferWithPayload { new( sender, amount, token_address, token_chain, redeemer, redeemer_chain, payload ) } /// Destroy `TransferWithPayload` and take only its payload. public fun take_payload(transfer: TransferWithPayload): vector { let TransferWithPayload { amount: _, token_address: _, token_chain: _, redeemer: _, redeemer_chain: _, sender: _, payload } = transfer; payload } /// Retrieve normalized amount of token transfer. public fun amount(self: &TransferWithPayload): NormalizedAmount { self.amount } // Retrieve token's canonical address. public fun token_address(self: &TransferWithPayload): ExternalAddress { self.token_address } /// Retrieve token's canonical chain ID. public fun token_chain(self: &TransferWithPayload): u16 { self.token_chain } /// Retrieve redeemer. public fun redeemer(self: &TransferWithPayload): ExternalAddress { self.redeemer } // Retrieve redeemer as `ID`. public fun redeemer_id(self: &TransferWithPayload): ID { object::id_from_bytes(external_address::to_bytes(self.redeemer)) } /// Retrieve target chain for redeemer. public fun redeemer_chain(self: &TransferWithPayload): u16 { self.redeemer_chain } /// Retrieve transfer sender. public fun sender(self: &TransferWithPayload): ExternalAddress { self.sender } /// Retrieve arbitrary payload. public fun payload(self: &TransferWithPayload): vector { self.payload } /// Decode Wormhole message payload as `TransferWithPayload`. public fun deserialize(transfer: vector): TransferWithPayload { let cur = cursor::new(transfer); assert!(bytes::take_u8(&mut cur) == PAYLOAD_ID, E_INVALID_PAYLOAD); let amount = normalized_amount::take_bytes(&mut cur); let token_address = external_address::take_bytes(&mut cur); let token_chain = bytes::take_u16_be(&mut cur); let redeemer = external_address::take_bytes(&mut cur); let redeemer_chain = bytes::take_u16_be(&mut cur); let sender = external_address::take_bytes(&mut cur); TransferWithPayload { amount, token_address, token_chain, redeemer, redeemer_chain, sender, payload: cursor::take_rest(cur) } } /// Encode `TransferWithPayload` for Wormhole message payload. public fun serialize(transfer: TransferWithPayload): vector { let TransferWithPayload { amount, token_address, token_chain, redeemer, redeemer_chain, sender, payload } = transfer; let buf = vector::empty(); bytes::push_u8(&mut buf, PAYLOAD_ID); bytes::push_u256_be(&mut buf, normalized_amount::to_u256(amount)); vector::append(&mut buf, external_address::to_bytes(token_address)); bytes::push_u16_be(&mut buf, token_chain); vector::append(&mut buf, external_address::to_bytes(redeemer)); bytes::push_u16_be(&mut buf, redeemer_chain); vector::append(&mut buf, external_address::to_bytes(sender)); vector::append(&mut buf, payload); buf } #[test_only] public fun destroy(transfer: TransferWithPayload) { take_payload(transfer); } #[test_only] public fun payload_id(): u8 { PAYLOAD_ID } } #[test_only] module token_bridge::transfer_with_payload_tests { use std::vector::{Self}; use sui::object::{Self}; use wormhole::emitter::{Self}; use wormhole::external_address::{Self}; use token_bridge::dummy_message::{Self}; use token_bridge::normalized_amount::{Self}; use token_bridge::transfer_with_payload::{Self}; #[test] fun test_serialize() { let emitter_cap = emitter::dummy(); let amount = normalized_amount::from_raw(234567890, 8); let token_address = external_address::from_address(@0xbeef); let token_chain = 1; let redeemer = external_address::from_address(@0xcafe); let redeemer_chain = 7; let payload = b"All your base are belong to us."; let new_transfer = transfer_with_payload::new_test_only( object::id(&emitter_cap), amount, token_address, token_chain, redeemer, redeemer_chain, payload ); // Verify getters. assert!( transfer_with_payload::amount(&new_transfer) == amount, 0 ); assert!( transfer_with_payload::token_address(&new_transfer) == token_address, 0 ); assert!( transfer_with_payload::token_chain(&new_transfer) == token_chain, 0 ); assert!( transfer_with_payload::redeemer(&new_transfer) == redeemer, 0 ); assert!( transfer_with_payload::redeemer_chain(&new_transfer) == redeemer_chain, 0 ); let expected_sender = external_address::from_id(object::id(&emitter_cap)); assert!( transfer_with_payload::sender(&new_transfer) == expected_sender, 0 ); assert!( transfer_with_payload::payload(&new_transfer) == payload, 0 ); let serialized = transfer_with_payload::serialize(new_transfer); let expected_serialized = dummy_message::encoded_transfer_with_payload(); assert!(serialized == expected_serialized, 0); // Clean up. emitter::destroy_test_only(emitter_cap); } #[test] fun test_deserialize() { let expected_amount = normalized_amount::from_raw(234567890, 8); let expected_token_address = external_address::from_address(@0xbeef); let expected_token_chain = 1; let expected_recipient = external_address::from_address(@0xcafe); let expected_recipient_chain = 7; let expected_sender = external_address::from_address( @0x381dd9078c322a4663c392761a0211b527c127b29583851217f948d62131f409 ); let expected_payload = b"All your base are belong to us."; let parsed = transfer_with_payload::deserialize( dummy_message::encoded_transfer_with_payload() ); // Verify getters. assert!( transfer_with_payload::amount(&parsed) == expected_amount, 0 ); assert!( transfer_with_payload::token_address(&parsed) == expected_token_address, 0 ); assert!( transfer_with_payload::token_chain(&parsed) == expected_token_chain, 0 ); assert!( transfer_with_payload::redeemer(&parsed) == expected_recipient, 0 ); assert!( transfer_with_payload::redeemer_chain(&parsed) == expected_recipient_chain, 0 ); assert!( transfer_with_payload::sender(&parsed) == expected_sender, 0 ); assert!( transfer_with_payload::payload(&parsed) == expected_payload, 0 ); let payload = transfer_with_payload::take_payload(parsed); assert!(payload == expected_payload, 0); } #[test] #[expected_failure(abort_code = transfer_with_payload::E_INVALID_PAYLOAD)] fun test_cannot_deserialize_invalid_payload() { let invalid_payload = token_bridge::dummy_message::encoded_transfer(); // Show that the first byte is not the expected payload ID. assert!( *vector::borrow(&invalid_payload, 0) != transfer_with_payload::payload_id(), 0 ); // You shall not pass! let parsed = transfer_with_payload::deserialize(invalid_payload); // Clean up. transfer_with_payload::destroy(parsed); abort 42 } }