312 lines
8.9 KiB
Plaintext
312 lines
8.9 KiB
Plaintext
|
// SPDX-License-Identifier: Apache 2
|
||
|
|
||
|
/// This module implements serialization and deserialization for token transfer
|
||
|
/// with an optional relayer fee. This message is a specific Wormhole message
|
||
|
/// payload for Token Bridge.
|
||
|
///
|
||
|
/// When this transfer is redeemed, the relayer fee will be subtracted from the
|
||
|
/// transfer amount. If the transaction sender is the same address of the
|
||
|
/// recipient, the recipient will collect the full amount.
|
||
|
///
|
||
|
/// See `transfer_tokens` and `complete_transfer` modules for more details.
|
||
|
module token_bridge::transfer {
|
||
|
use std::vector::{Self};
|
||
|
use wormhole::bytes::{Self};
|
||
|
use wormhole::cursor::{Self};
|
||
|
use wormhole::external_address::{Self, ExternalAddress};
|
||
|
|
||
|
use token_bridge::normalized_amount::{Self, NormalizedAmount};
|
||
|
|
||
|
friend token_bridge::complete_transfer;
|
||
|
friend token_bridge::transfer_tokens;
|
||
|
|
||
|
/// Message payload is not `Transfer`.
|
||
|
const E_INVALID_PAYLOAD: u64 = 0;
|
||
|
|
||
|
/// Message identifier.
|
||
|
const PAYLOAD_ID: u8 = 1;
|
||
|
|
||
|
/// Container that warehouses transfer information. This struct is used only
|
||
|
/// by `transfer_tokens` and `complete_transfer` modules.
|
||
|
struct Transfer {
|
||
|
// Amount being transferred.
|
||
|
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,
|
||
|
// Address of the recipient. Left-zero-padded if shorter than 32 bytes.
|
||
|
recipient: ExternalAddress,
|
||
|
// Chain ID of the recipient.
|
||
|
recipient_chain: u16,
|
||
|
// Amount of tokens that the user is willing to pay as relayer fee.
|
||
|
// Must be <= amount.
|
||
|
relayer_fee: NormalizedAmount,
|
||
|
}
|
||
|
|
||
|
/// Create new `Transfer`.
|
||
|
public(friend) fun new(
|
||
|
amount: NormalizedAmount,
|
||
|
token_address: ExternalAddress,
|
||
|
token_chain: u16,
|
||
|
recipient: ExternalAddress,
|
||
|
recipient_chain: u16,
|
||
|
relayer_fee: NormalizedAmount,
|
||
|
): Transfer {
|
||
|
Transfer {
|
||
|
amount,
|
||
|
token_address,
|
||
|
token_chain,
|
||
|
recipient,
|
||
|
recipient_chain,
|
||
|
relayer_fee,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#[test_only]
|
||
|
public fun new_test_only(
|
||
|
amount: NormalizedAmount,
|
||
|
token_address: ExternalAddress,
|
||
|
token_chain: u16,
|
||
|
recipient: ExternalAddress,
|
||
|
recipient_chain: u16,
|
||
|
relayer_fee: NormalizedAmount,
|
||
|
): Transfer {
|
||
|
new(
|
||
|
amount,
|
||
|
token_address,
|
||
|
token_chain,
|
||
|
recipient,
|
||
|
recipient_chain,
|
||
|
relayer_fee
|
||
|
)
|
||
|
}
|
||
|
|
||
|
/// Decompose `Transfer` into its members.
|
||
|
public(friend) fun unpack(
|
||
|
transfer: Transfer
|
||
|
): (
|
||
|
NormalizedAmount,
|
||
|
ExternalAddress,
|
||
|
u16,
|
||
|
ExternalAddress,
|
||
|
u16,
|
||
|
NormalizedAmount
|
||
|
) {
|
||
|
let Transfer {
|
||
|
amount,
|
||
|
token_address,
|
||
|
token_chain,
|
||
|
recipient,
|
||
|
recipient_chain,
|
||
|
relayer_fee,
|
||
|
} = transfer;
|
||
|
|
||
|
(
|
||
|
amount,
|
||
|
token_address,
|
||
|
token_chain,
|
||
|
recipient,
|
||
|
recipient_chain,
|
||
|
relayer_fee
|
||
|
)
|
||
|
}
|
||
|
|
||
|
#[test_only]
|
||
|
public fun unpack_test_only(
|
||
|
transfer: Transfer
|
||
|
): (
|
||
|
NormalizedAmount,
|
||
|
ExternalAddress,
|
||
|
u16,
|
||
|
ExternalAddress,
|
||
|
u16,
|
||
|
NormalizedAmount
|
||
|
) {
|
||
|
unpack(transfer)
|
||
|
}
|
||
|
|
||
|
/// Decode Wormhole message payload as `Transfer`.
|
||
|
public(friend) fun deserialize(buf: vector<u8>): Transfer {
|
||
|
let cur = cursor::new(buf);
|
||
|
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 recipient = external_address::take_bytes(&mut cur);
|
||
|
let recipient_chain = bytes::take_u16_be(&mut cur);
|
||
|
let relayer_fee = normalized_amount::take_bytes(&mut cur);
|
||
|
cursor::destroy_empty(cur);
|
||
|
|
||
|
Transfer {
|
||
|
amount,
|
||
|
token_address,
|
||
|
token_chain,
|
||
|
recipient,
|
||
|
recipient_chain,
|
||
|
relayer_fee,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#[test_only]
|
||
|
public fun deserialize_test_only(buf: vector<u8>): Transfer {
|
||
|
deserialize(buf)
|
||
|
}
|
||
|
|
||
|
/// Encode `Transfer` for Wormhole message payload.
|
||
|
public(friend) fun serialize(transfer: Transfer): vector<u8> {
|
||
|
let (
|
||
|
amount,
|
||
|
token_address,
|
||
|
token_chain,
|
||
|
recipient,
|
||
|
recipient_chain,
|
||
|
relayer_fee,
|
||
|
) = unpack(transfer);
|
||
|
|
||
|
let buf = vector::empty<u8>();
|
||
|
bytes::push_u8(&mut buf, PAYLOAD_ID);
|
||
|
vector::append(&mut buf, normalized_amount::to_bytes(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(recipient));
|
||
|
bytes::push_u16_be(&mut buf, recipient_chain);
|
||
|
vector::append(&mut buf, normalized_amount::to_bytes(relayer_fee));
|
||
|
|
||
|
buf
|
||
|
}
|
||
|
|
||
|
#[test_only]
|
||
|
public fun serialize_test_only(transfer: Transfer): vector<u8> {
|
||
|
serialize(transfer)
|
||
|
}
|
||
|
|
||
|
#[test_only]
|
||
|
public fun amount(self: &Transfer): NormalizedAmount {
|
||
|
self.amount
|
||
|
}
|
||
|
|
||
|
#[test_only]
|
||
|
public fun raw_amount(self: &Transfer, decimals: u8): u64 {
|
||
|
normalized_amount::to_raw(self.amount, decimals)
|
||
|
}
|
||
|
|
||
|
#[test_only]
|
||
|
public fun token_address(self: &Transfer): ExternalAddress {
|
||
|
self.token_address
|
||
|
}
|
||
|
|
||
|
#[test_only]
|
||
|
public fun token_chain(self: &Transfer): u16 {
|
||
|
self.token_chain
|
||
|
}
|
||
|
|
||
|
#[test_only]
|
||
|
public fun recipient(self: &Transfer): ExternalAddress {
|
||
|
self.recipient
|
||
|
}
|
||
|
|
||
|
#[test_only]
|
||
|
public fun recipient_as_address(self: &Transfer): address {
|
||
|
external_address::to_address(self.recipient)
|
||
|
}
|
||
|
|
||
|
#[test_only]
|
||
|
public fun recipient_chain(self: &Transfer): u16 {
|
||
|
self.recipient_chain
|
||
|
}
|
||
|
|
||
|
#[test_only]
|
||
|
public fun relayer_fee(self: &Transfer): NormalizedAmount {
|
||
|
self.relayer_fee
|
||
|
}
|
||
|
|
||
|
#[test_only]
|
||
|
public fun raw_relayer_fee(self: &Transfer, decimals: u8): u64 {
|
||
|
normalized_amount::to_raw(self.relayer_fee, decimals)
|
||
|
}
|
||
|
|
||
|
#[test_only]
|
||
|
public fun destroy(transfer: Transfer) {
|
||
|
unpack(transfer);
|
||
|
}
|
||
|
|
||
|
#[test_only]
|
||
|
public fun payload_id(): u8 {
|
||
|
PAYLOAD_ID
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#[test_only]
|
||
|
module token_bridge::transfer_tests {
|
||
|
use std::vector::{Self};
|
||
|
use wormhole::external_address::{Self};
|
||
|
|
||
|
use token_bridge::dummy_message::{Self};
|
||
|
use token_bridge::transfer::{Self};
|
||
|
use token_bridge::normalized_amount::{Self};
|
||
|
|
||
|
#[test]
|
||
|
fun test_serialize_deserialize() {
|
||
|
let decimals = 8;
|
||
|
let expected_amount = normalized_amount::from_raw(234567890, decimals);
|
||
|
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_relayer_fee =
|
||
|
normalized_amount::from_raw(123456789, decimals);
|
||
|
|
||
|
let serialized =
|
||
|
transfer::serialize_test_only(
|
||
|
transfer::new_test_only(
|
||
|
expected_amount,
|
||
|
expected_token_address,
|
||
|
expected_token_chain,
|
||
|
expected_recipient,
|
||
|
expected_recipient_chain,
|
||
|
expected_relayer_fee,
|
||
|
)
|
||
|
);
|
||
|
assert!(serialized == dummy_message::encoded_transfer(), 0);
|
||
|
|
||
|
let (
|
||
|
amount,
|
||
|
token_address,
|
||
|
token_chain,
|
||
|
recipient,
|
||
|
recipient_chain,
|
||
|
relayer_fee
|
||
|
) = transfer::unpack_test_only(
|
||
|
transfer::deserialize_test_only(serialized)
|
||
|
);
|
||
|
assert!(amount == expected_amount, 0);
|
||
|
assert!(token_address == expected_token_address, 0);
|
||
|
assert!(token_chain == expected_token_chain, 0);
|
||
|
assert!(recipient == expected_recipient, 0);
|
||
|
assert!(recipient_chain == expected_recipient_chain, 0);
|
||
|
assert!(relayer_fee == expected_relayer_fee, 0);
|
||
|
}
|
||
|
|
||
|
#[test]
|
||
|
#[expected_failure(abort_code = transfer::E_INVALID_PAYLOAD)]
|
||
|
fun test_cannot_deserialize_invalid_payload() {
|
||
|
let invalid_payload = dummy_message::encoded_transfer_with_payload();
|
||
|
|
||
|
// Show that the first byte is not the expected payload ID.
|
||
|
assert!(
|
||
|
*vector::borrow(&invalid_payload, 0) != transfer::payload_id(),
|
||
|
0
|
||
|
);
|
||
|
|
||
|
// You shall not pass!
|
||
|
let parsed = transfer::deserialize_test_only(invalid_payload);
|
||
|
|
||
|
// Clean up.
|
||
|
transfer::destroy(parsed);
|
||
|
|
||
|
abort 42
|
||
|
}
|
||
|
}
|