wormhole/sui/token_bridge/sources/messages/transfer_with_payload.move

353 lines
11 KiB
Plaintext

// 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<u8>,
}
/// 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<u8>
): 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<u8>
): 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<u8> {
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<u8> {
self.payload
}
/// Decode Wormhole message payload as `TransferWithPayload`.
public fun deserialize(transfer: vector<u8>): 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<u8> {
let TransferWithPayload {
amount,
token_address,
token_chain,
redeemer,
redeemer_chain,
sender,
payload
} = transfer;
let buf = vector::empty<u8>();
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
}
}