wormhole/sui/token_bridge/sources/vaa.move

352 lines
12 KiB
Plaintext

// SPDX-License-Identifier: Apache 2
/// This module builds on Wormhole's `vaa::parse_and_verify` method by adding
/// emitter verification and replay protection.
///
/// Token Bridge only cares about other Token Bridge messages, so the emitter
/// address must be a registered Token Bridge emitter according to the VAA's
/// emitter chain ID.
///
/// Token Bridge does not allow replaying any of its VAAs, so its hash is stored
/// in its `State`. If the encoded VAA passes through `parse_and_verify` again,
/// it will abort.
module token_bridge::vaa {
use sui::table::{Self};
use wormhole::external_address::{ExternalAddress};
use wormhole::vaa::{Self, VAA};
use token_bridge::state::{Self, State};
friend token_bridge::create_wrapped;
friend token_bridge::complete_transfer;
friend token_bridge::complete_transfer_with_payload;
/// For a given chain ID, Token Bridge is non-existent.
const E_UNREGISTERED_EMITTER: u64 = 0;
/// Encoded emitter address does not match registered Token Bridge.
const E_EMITTER_ADDRESS_MISMATCH: u64 = 1;
/// This type represents VAA data whose emitter is a registered Token Bridge
/// emitter. This message is also representative of a VAA that cannot be
/// replayed.
struct TokenBridgeMessage {
/// Wormhole chain ID from which network the message originated from.
emitter_chain: u16,
/// Address of Token Bridge (standardized to 32 bytes) that produced
/// this message.
emitter_address: ExternalAddress,
/// Sequence number of Token Bridge's Wormhole message.
sequence: u64,
/// Token Bridge payload.
payload: vector<u8>
}
/// Parses and verifies encoded VAA. Because Token Bridge does not allow
/// VAAs to be replayed, the VAA hash is stored in a set, which is checked
/// against the next time the same VAA is used to make sure it cannot be
/// used again.
///
/// In its verification, this method checks whether the emitter is a
/// registered Token Bridge contract on another network.
///
/// NOTE: It is important for integrators to refrain from calling this
/// method within their contracts. This method is meant to be called within
/// a transaction block, passing the `TokenBridgeMessage` to one of the
/// Token Bridge methods that consumes this type. If in a circumstance where
/// this module has a breaking change in an upgrade, another method (e.g.
/// `complete_transfer_with_payload`) will not be affected by this change.
public fun verify_only_once(
token_bridge_state: &mut State,
verified_vaa: VAA
): TokenBridgeMessage {
// This capability ensures that the current build version is used.
let latest_only = state::assert_latest_only(token_bridge_state);
// First parse and verify VAA using Wormhole. This also consumes the VAA
// hash to prevent replay.
vaa::consume(
state::borrow_mut_consumed_vaas(&latest_only, token_bridge_state),
&verified_vaa
);
// Does the emitter agree with a registered Token Bridge?
assert_registered_emitter(token_bridge_state, &verified_vaa);
// Take emitter info, sequence and payload.
let sequence = vaa::sequence(&verified_vaa);
let (
emitter_chain,
emitter_address,
payload
) = vaa::take_emitter_info_and_payload(verified_vaa);
TokenBridgeMessage {
emitter_chain,
emitter_address,
sequence,
payload
}
}
public fun emitter_chain(self: &TokenBridgeMessage): u16 {
self.emitter_chain
}
public fun emitter_address(self: &TokenBridgeMessage): ExternalAddress {
self.emitter_address
}
public fun sequence(self: &TokenBridgeMessage): u64 {
self.sequence
}
/// Destroy `TokenBridgeMessage` and extract payload, which is the same
/// payload in the `VAA`.
///
/// NOTE: This is a privileged method, which only friends within the Token
/// Bridge package can use. This guarantees that no other package can redeem
/// a VAA intended for Token Bridge as a denial-of-service by calling
/// `verify_only_once` and then destroying it by calling it this method.
public(friend) fun take_payload(msg: TokenBridgeMessage): vector<u8> {
let TokenBridgeMessage {
emitter_chain: _,
emitter_address: _,
sequence: _,
payload
} = msg;
payload
}
/// Assert that a given emitter equals one that is registered as a foreign
/// Token Bridge.
fun assert_registered_emitter(
token_bridge_state: &State,
verified_vaa: &VAA
) {
let chain = vaa::emitter_chain(verified_vaa);
let registry = state::borrow_emitter_registry(token_bridge_state);
assert!(table::contains(registry, chain), E_UNREGISTERED_EMITTER);
let registered = table::borrow(registry, chain);
let emitter_addr = vaa::emitter_address(verified_vaa);
assert!(*registered == emitter_addr, E_EMITTER_ADDRESS_MISMATCH);
}
#[test_only]
public fun destroy(msg: TokenBridgeMessage) {
take_payload(msg);
}
}
#[test_only]
module token_bridge::vaa_tests {
use sui::test_scenario::{Self};
use wormhole::external_address::{Self};
use wormhole::wormhole_scenario::{parse_and_verify_vaa};
use token_bridge::state::{Self};
use token_bridge::token_bridge_scenario::{
person,
register_dummy_emitter,
return_state,
set_up_wormhole_and_token_bridge,
take_state
};
use token_bridge::vaa::{Self};
/// VAA sent from the ethereum token bridge 0xdeadbeef.
const VAA: vector<u8> =
x"01000000000100102d399190fa61daccb11c2ea4f7a3db3a9365e5936bcda4cded87c1b9eeb095173514f226256d5579af71d4089eb89496befb998075ba94cd1d4460c5c57b84000000000100000001000200000000000000000000000000000000000000000000000000000000deadbeef0000000002634973000200000000000000000000000000000000000000000000000000000000beefface00020c0000000000000000000000000000000000000000000000000000000042454546000000000000000000000000000000000042656566206661636520546f6b656e";
#[test]
#[expected_failure(abort_code = vaa::E_UNREGISTERED_EMITTER)]
fun test_cannot_verify_only_once_unregistered_chain() {
let caller = person();
let my_scenario = test_scenario::begin(caller);
let scenario = &mut my_scenario;
// Set up contracts.
let wormhole_fee = 350;
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
// Ignore effects.
test_scenario::next_tx(scenario, caller);
let token_bridge_state = take_state(scenario);
let verified_vaa = parse_and_verify_vaa(scenario, VAA);
// You shall not pass!
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
// Clean up.
vaa::destroy(msg);
abort 42
}
#[test]
#[expected_failure(abort_code = vaa::E_EMITTER_ADDRESS_MISMATCH)]
fun test_cannot_verify_only_once_emitter_address_mismatch() {
let caller = person();
let my_scenario = test_scenario::begin(caller);
let scenario = &mut my_scenario;
// Set up contracts.
let wormhole_fee = 350;
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
// Ignore effects.
test_scenario::next_tx(scenario, caller);
let token_bridge_state = take_state(scenario);
// First register emitter.
let emitter_chain = 2;
let emitter_addr = external_address::from_address(@0xdeafbeef);
token_bridge::register_chain::register_new_emitter_test_only(
&mut token_bridge_state,
emitter_chain,
emitter_addr
);
// Confirm that encoded emitter disagrees with registered emitter.
let verified_vaa = parse_and_verify_vaa(scenario, VAA);
assert!(
wormhole::vaa::emitter_address(&verified_vaa) != emitter_addr,
0
);
// You shall not pass!
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
// Clean up.
vaa::destroy(msg);
abort 42
}
#[test]
fun test_verify_only_once() {
let caller = person();
let my_scenario = test_scenario::begin(caller);
let scenario = &mut my_scenario;
// Set up contracts.
let wormhole_fee = 350;
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
// Register foreign emitter.
let expected_source_chain = 2;
register_dummy_emitter(scenario, expected_source_chain);
// Ignore effects.
test_scenario::next_tx(scenario, caller);
let token_bridge_state = take_state(scenario);
// Confirm VAA originated from where we expect.
let verified_vaa = parse_and_verify_vaa(scenario, VAA);
assert!(
wormhole::vaa::emitter_chain(&verified_vaa) == expected_source_chain,
0
);
// Finally verify.
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
// Clean up.
vaa::destroy(msg);
return_state(token_bridge_state);
// Done.
test_scenario::end(my_scenario);
}
#[test]
#[expected_failure(abort_code = wormhole::set::E_KEY_ALREADY_EXISTS)]
fun test_cannot_verify_only_once_again() {
let caller = person();
let my_scenario = test_scenario::begin(caller);
let scenario = &mut my_scenario;
// Set up contracts.
let wormhole_fee = 350;
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
// Register foreign emitter.
let expected_source_chain = 2;
register_dummy_emitter(scenario, expected_source_chain);
// Ignore effects.
test_scenario::next_tx(scenario, caller);
let token_bridge_state = take_state(scenario);
// Confirm VAA originated from where we expect.
let verified_vaa = parse_and_verify_vaa(scenario, VAA);
assert!(
wormhole::vaa::emitter_chain(&verified_vaa) == expected_source_chain,
0
);
// Finally verify.
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
vaa::destroy(msg);
let verified_vaa = parse_and_verify_vaa(scenario, VAA);
// You shall not pass!
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
// Clean up.
vaa::destroy(msg);
abort 42
}
#[test]
#[expected_failure(abort_code = wormhole::package_utils::E_NOT_CURRENT_VERSION)]
fun test_cannot_verify_only_once_outdated_version() {
let caller = person();
let my_scenario = test_scenario::begin(caller);
let scenario = &mut my_scenario;
// Set up contracts.
let wormhole_fee = 350;
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
// Register foreign emitter.
let expected_source_chain = 2;
register_dummy_emitter(scenario, expected_source_chain);
// Ignore effects.
test_scenario::next_tx(scenario, caller);
let token_bridge_state = take_state(scenario);
// Verify VAA.
let verified_vaa = parse_and_verify_vaa(scenario, VAA);
// Conveniently roll version back.
state::reverse_migrate_version(&mut token_bridge_state);
// Simulate executing with an outdated build by upticking the minimum
// required version for `publish_message` to something greater than
// this build.
state::migrate_version_test_only(
&mut token_bridge_state,
token_bridge::version_control::previous_version_test_only(),
token_bridge::version_control::next_version()
);
// You shall not pass!
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
// Clean up.
vaa::destroy(msg);
abort 42
}
}