Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/devnet-consts.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@
},
"21": {
"contracts": {
"tokenBridgeEmitterAddress": "50d9394da9f8812b21764b2206fcc2da0ffac108738132a091feec93c8164ec5"
"tokenBridgeEmitterAddress": "c2bfb149b1f3e55d054891b27a8e249930e57368e755c11e8fadb9384afce495"
}
},
"22": {
Expand Down
2 changes: 1 addition & 1 deletion sdk/devnet_consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var knownDevnetTokenbridgeEmitters = map[vaa.ChainID]string{
vaa.ChainIDBSC: "0000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16",
vaa.ChainIDAlgorand: "8ec299cb7f3efec28f542397e07f07118d74c875f85409ed8e6b93c17b60e992",
vaa.ChainIDWormchain: "c9138c6e5bd7a2ab79c1a87486c9d7349d064b35ac9f7498f3b207b3a61e6013",
vaa.ChainIDSui: "50d9394da9f8812b21764b2206fcc2da0ffac108738132a091feec93c8164ec5",
vaa.ChainIDSui: "c2bfb149b1f3e55d054891b27a8e249930e57368e755c11e8fadb9384afce495",
}

// KnownDevnetNFTBridgeEmitters is a map of known NFT emitters used during development.
Expand Down
2 changes: 1 addition & 1 deletion sdk/js/src/sui/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const getCoinBuildOutput = async (
}

const bytecodeHex =
"a11ceb0b060000000901000a020a14031e1704350405392d07669f01088502600ae502050cea02160004010b010c0205020d000002000201020003030c020001000104020700000700010001090801010c020a050600030803040202000302010702080007080100020800080303090002070801010b020209000901010608010105010b0202080008030209000504434f494e095478436f6e7465787408565f5f305f325f3011577261707065644173736574536574757004636f696e0e6372656174655f777261707065640b64756d6d795f6669656c6404696e697414707265706172655f726567697374726174696f6e0f7075626c69635f7472616e736665720673656e646572087472616e736665720a74785f636f6e746578740f76657273696f6e5f636f6e74726f6c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002" +
"a11ceb0b060000000901000a020a14031e1704350405392d07669f01088502600ae502050cea02160004010b010c0205020d000002000201020003030c020001000104020700000700010001090801010c020a050600030803040202000302010702080007080100020800080303090002070801010b020209000901010608010105010b0202080008030209000504434f494e095478436f6e7465787408565f5f305f335f3011577261707065644173736574536574757004636f696e0e6372656174655f777261707065640b64756d6d795f6669656c6404696e697414707265706172655f726567697374726174696f6e0f7075626c69635f7472616e736665720673656e646572087472616e736665720a74785f636f6e746578740f76657273696f6e5f636f6e74726f6c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002" +
strippedTokenBridgePackageId +
"00020106010000000001090b0031" +
decimals.toString(16).padStart(2, "0") +
Expand Down
2 changes: 1 addition & 1 deletion sdk/js/src/utils/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -831,7 +831,7 @@ const DEVNET = {
sui: {
core: "0xea31c369d1f873d87d37f313ec37f1ee20a0b8136f06e3d3521330ee467312a4", // wormhole module State object ID
token_bridge:
"0xbab50719a0e35350677b75bddc25f20bdc7a8c4788c7a0746b9796956380a2d2", // token_bridge module State object ID
"0xe536b9894dd511802508cbf5625a4d479c1e64c5aa98b725b1c83d8ef379dee9", // token_bridge module State object ID
nft_bridge: undefined,
},
moonbeam: {
Expand Down
4 changes: 2 additions & 2 deletions sui/examples/coins/sources/coin.move
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module coins::coin {
struct COIN has drop {}

fun init(witness: COIN, ctx: &mut TxContext) {
use token_bridge::version_control::{V__0_2_0 as V__CURRENT};
use token_bridge::version_control::{V__0_3_0 as V__CURRENT};

transfer::public_transfer(
create_wrapped::prepare_registration<COIN, V__CURRENT>(
Expand Down Expand Up @@ -62,7 +62,7 @@ module coins::coin_tests {
use wormhole::external_address::{Self};
use wormhole::wormhole_scenario::{parse_and_verify_vaa};

use token_bridge::version_control::{V__0_2_0 as V__CURRENT};
use token_bridge::version_control::{V__0_3_0 as V__CURRENT};

use coins::coin::{COIN};

Expand Down
2 changes: 1 addition & 1 deletion sui/token_bridge/Move.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "token_bridge"
version = "0.2.0"
version = "0.3.0"

[dependencies]
wormhole = { local = "../wormhole" }
1 change: 1 addition & 0 deletions sui/token_bridge/sources/attest_token.move
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ module token_bridge::attest_token {
): MessageTicket {
// This capability ensures that the current build version is used.
let latest_only = state::assert_latest_only(token_bridge_state);
state::assert_not_paused(token_bridge_state);

// Encode Wormhole message payload.
let encoded_asset_meta =
Expand Down
1 change: 1 addition & 0 deletions sui/token_bridge/sources/complete_transfer.move
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ module token_bridge::complete_transfer {
): RelayerReceipt<CoinType> {
// This capability ensures that the current build version is used.
let latest_only = state::assert_latest_only(token_bridge_state);
state::assert_not_paused(token_bridge_state);

// Emitting the transfer being redeemed (and disregard return value).
emit_transfer_redeemed(&msg);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ module token_bridge::complete_transfer_with_payload {
): RedeemerReceipt<CoinType> {
// This capability ensures that the current build version is used.
let latest_only = state::assert_latest_only(token_bridge_state);
state::assert_not_paused(token_bridge_state);

// Emitting the transfer being redeemed.
//
Expand Down
6 changes: 4 additions & 2 deletions sui/token_bridge/sources/create_wrapped.move
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ module token_bridge::create_wrapped {
use token_bridge::wrapped_asset::{Self};

#[test_only]
use token_bridge::version_control::{Self, V__0_2_0 as V__CURRENT};
use token_bridge::version_control::{Self, V__0_3_0 as V__CURRENT};

/// Failed one-time witness verification.
const E_BAD_WITNESS: u64 = 0;
Expand Down Expand Up @@ -160,6 +160,7 @@ module token_bridge::create_wrapped {
// created using the current package.
let latest_only =
state::assert_latest_only_specified<Version>(token_bridge_state);
state::assert_not_paused(token_bridge_state);

let WrappedAssetSetup {
id,
Expand Down Expand Up @@ -196,6 +197,7 @@ module token_bridge::create_wrapped {
) {
// This capability ensures that the current build version is used.
let latest_only = state::assert_latest_only(token_bridge_state);
state::assert_not_paused(token_bridge_state);

// Deserialize to `AssetMeta`.
let token_meta = asset_meta::deserialize(vaa::take_payload(msg));
Expand Down Expand Up @@ -300,7 +302,7 @@ module token_bridge::create_wrapped_tests {
};
use token_bridge::token_registry::{Self};
use token_bridge::vaa::{Self};
use token_bridge::version_control::{V__0_2_0 as V__CURRENT};
use token_bridge::version_control::{V__0_3_0 as V__CURRENT};
use token_bridge::wrapped_asset::{Self};

struct NOT_A_WITNESS has drop {}
Expand Down
254 changes: 254 additions & 0 deletions sui/token_bridge/sources/governance/set_pauser_addresses.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
// SPDX-License-Identifier: Apache 2

/// This module implements handling a governance VAA to (re)assign the pauser,
/// freezer, and unpauser for the Token Bridge emergency pause mechanism.
///
/// The VAA encodes the OWNER address that should receive each capability. The
/// handler MINTS a fresh `PauserCap`/`FreezerCap`/`UnpauserCap`, transfers it to
/// that owner, and records the new cap's object id as the active id in `State`
/// (see `token_bridge::pause`). Because the handler mints and transfers, the
/// active cap is always an owned object — never shared — so only its owner can
/// invoke the corresponding entry point.
///
/// Each `SetPauserAddresses` mints NEW caps. Rotation = new cap to the new
/// owner; any previously minted cap becomes inert (its id no longer matches the
/// recorded active id). A zero/empty owner records `none` (unassigned) and mints
/// nothing.
///
/// On Sui the owner is a 32-byte address (an EOA, or an object that should own
/// the cap). A Sui address is 32 bytes — the same size as on SVM — so the
/// canonical action-4 wire format is unchanged; the Guardian treats the value
/// as opaque length-prefixed bytes and the whitepaper delegates interpretation
/// to the receiving runtime.
///
/// Wire format (action 4, per whitepaper 0003):
/// ```
/// PauserLen(1) | Pauser(PauserLen) |
/// FreezerLen(1) | Freezer(FreezerLen) |
/// UnpauserLen(1) | Unpauser(UnpauserLen)
/// ```
///
/// Validation:
/// - Each length must be 0 (unassigned) or 32 (Sui address size).
/// - An all-zero 32-byte value is treated as unassigned (`none`).
/// - No trailing bytes allowed (cursor must be fully consumed after the three
/// addresses).
module token_bridge::set_pauser_addresses {
use std::option::{Self, Option};
use sui::object::{ID};
use sui::transfer::{Self};
use sui::tx_context::{TxContext};
use wormhole::bytes::{Self};
use wormhole::cursor::{Self};
use wormhole::governance_message::{Self, DecreeTicket, DecreeReceipt};

use token_bridge::pause::{Self};
use token_bridge::state::{Self, State};

/// Address length is not 0 or 32.
const E_INVALID_ADDRESS_LENGTH: u64 = 0;

/// Governance action ID for SetPauserAddresses (canonical, per whitepaper).
const ACTION_SET_PAUSER_ADDRESSES: u8 = 4;

/// Expected address size for Sui (32 bytes).
const SUI_ADDRESS_SIZE: u8 = 32;

struct GovernanceWitness has drop {}

/// Event emitted when pauser/freezer/unpauser caps are (re)assigned via
/// governance. Each field is the newly minted cap object id, or `none` when
/// that role was left unassigned (no cap minted).
struct PauserAddressesSet has drop, copy {
pauser: Option<ID>,
freezer: Option<ID>,
unpauser: Option<ID>
}

/// Create `DecreeTicket` for `SetPauserAddresses` governance VAA.
/// Uses `authorize_verify_local` (chain-specific, chain == 21 for Sui).
public fun authorize_governance(
token_bridge_state: &State
): DecreeTicket<GovernanceWitness> {
governance_message::authorize_verify_local(
GovernanceWitness {},
state::governance_chain(token_bridge_state),
state::governance_contract(token_bridge_state),
state::governance_module(),
ACTION_SET_PAUSER_ADDRESSES
)
}

/// Execute the `SetPauserAddresses` governance action. Parses the three
/// owner addresses (pauser, freezer, unpauser), mints a cap for each present
/// owner and transfers it there, and records the new cap ids (or `none`) as
/// active.
public fun set_pauser_addresses(
token_bridge_state: &mut State,
receipt: DecreeReceipt<GovernanceWitness>,
ctx: &mut TxContext
) {
// This capability ensures that the current build version is used.
let latest_only = state::assert_latest_only(token_bridge_state);

let payload =
governance_message::take_payload(
state::borrow_mut_consumed_vaas(
&latest_only,
token_bridge_state
),
receipt
);

// Parse the length-prefixed owner addresses (`none` = unassigned).
let cur = cursor::new(payload);
let pauser_owner = take_owner_length_prefixed(&mut cur);
let freezer_owner = take_owner_length_prefixed(&mut cur);
let unpauser_owner = take_owner_length_prefixed(&mut cur);

// No trailing bytes allowed.
cursor::destroy_empty(cur);

// Mint + transfer + record for each role.
let pauser_id = assign_pauser(token_bridge_state, &latest_only, pauser_owner, ctx);
let freezer_id =
assign_freezer(token_bridge_state, &latest_only, freezer_owner, ctx);
let unpauser_id =
assign_unpauser(token_bridge_state, &latest_only, unpauser_owner, ctx);

sui::event::emit(
PauserAddressesSet {
pauser: pauser_id,
freezer: freezer_id,
unpauser: unpauser_id
}
);
}

/// Mint a `PauserCap` for `owner` and record its id as active. A `none`
/// owner unassigns the role (records `none`, mints nothing). Returns the
/// recorded id (`some(cap_id)` or `none`).
fun assign_pauser(
token_bridge_state: &mut State,
latest_only: &state::LatestOnly,
owner: Option<address>,
ctx: &mut TxContext
): Option<ID> {
if (option::is_none(&owner)) {
state::set_pauser(latest_only, token_bridge_state, option::none());
return option::none()
};
let cap = pause::new_pauser_cap(ctx);
let cap_id = pause::pauser_cap_id(&cap);
transfer::public_transfer(cap, option::destroy_some(owner));
state::set_pauser(latest_only, token_bridge_state, option::some(cap_id));
option::some(cap_id)
}

/// Mint a `FreezerCap` for `owner` and record its id as active. A `none`
/// owner unassigns the role. Returns the recorded id (`some(cap_id)` or
/// `none`).
fun assign_freezer(
token_bridge_state: &mut State,
latest_only: &state::LatestOnly,
owner: Option<address>,
ctx: &mut TxContext
): Option<ID> {
if (option::is_none(&owner)) {
state::set_freezer(latest_only, token_bridge_state, option::none());
return option::none()
};
let cap = pause::new_freezer_cap(ctx);
let cap_id = pause::freezer_cap_id(&cap);
transfer::public_transfer(cap, option::destroy_some(owner));
state::set_freezer(latest_only, token_bridge_state, option::some(cap_id));
option::some(cap_id)
}

/// Mint an `UnpauserCap` for `owner` and record its id as active. A `none`
/// owner unassigns the role. Returns the recorded id (`some(cap_id)` or
/// `none`).
fun assign_unpauser(
token_bridge_state: &mut State,
latest_only: &state::LatestOnly,
owner: Option<address>,
ctx: &mut TxContext
): Option<ID> {
if (option::is_none(&owner)) {
state::set_unpauser(latest_only, token_bridge_state, option::none());
return option::none()
};
let cap = pause::new_unpauser_cap(ctx);
let cap_id = pause::unpauser_cap_id(&cap);
transfer::public_transfer(cap, option::destroy_some(owner));
state::set_unpauser(latest_only, token_bridge_state, option::some(cap_id));
option::some(cap_id)
}

/// Parse a length-prefixed 32-byte owner address from the cursor. Length
/// must be 0 (returns `none`, unassigned) or SUI_ADDRESS_SIZE (32). A
/// 32-byte all-zero value is also treated as `none` (unassigned).
fun take_owner_length_prefixed(cur: &mut cursor::Cursor<u8>): Option<address> {
let len = bytes::take_u8(cur);
if (len == 0) {
return option::none()
};
assert!((len as u64) == (SUI_ADDRESS_SIZE as u64), E_INVALID_ADDRESS_LENGTH);

let addr_bytes = bytes::take_bytes(cur, (SUI_ADDRESS_SIZE as u64));

// Convert to address. An all-zero address is treated as unassigned.
let owner = sui::address::from_bytes(addr_bytes);
if (owner == @0x0) {
option::none()
} else {
option::some(owner)
}
}

#[test_only]
public fun action(): u8 {
ACTION_SET_PAUSER_ADDRESSES
}

#[test_only]
/// Parse a raw SetPauserAddresses payload (the part after the governance
/// header) into the three owner addresses, exercising the exact decode path
/// used by `set_pauser_addresses` (length validation + no-trailing-bytes).
public fun parse_payload_test_only(
payload: vector<u8>
): (Option<address>, Option<address>, Option<address>) {
let cur = cursor::new(payload);
let pauser_owner = take_owner_length_prefixed(&mut cur);
let freezer_owner = take_owner_length_prefixed(&mut cur);
let unpauser_owner = take_owner_length_prefixed(&mut cur);
cursor::destroy_empty(cur);
(pauser_owner, freezer_owner, unpauser_owner)
}

#[test_only]
public fun e_invalid_address_length(): u64 {
E_INVALID_ADDRESS_LENGTH
}

#[test_only]
/// Directly assign pauser/freezer/unpauser owners for tests, bypassing the
/// VAA. Mints + transfers caps exactly like the governance handler. Returns
/// the recorded (pauser_id, freezer_id, unpauser_id).
public fun set_pauser_addresses_test_only(
token_bridge_state: &mut State,
pauser_owner: Option<address>,
freezer_owner: Option<address>,
unpauser_owner: Option<address>,
ctx: &mut TxContext
): (Option<ID>, Option<ID>, Option<ID>) {
let latest_only = state::assert_latest_only(token_bridge_state);
let pauser_id =
assign_pauser(token_bridge_state, &latest_only, pauser_owner, ctx);
let freezer_id =
assign_freezer(token_bridge_state, &latest_only, freezer_owner, ctx);
let unpauser_id =
assign_unpauser(token_bridge_state, &latest_only, unpauser_owner, ctx);
(pauser_id, freezer_id, unpauser_id)
}
}
2 changes: 1 addition & 1 deletion sui/token_bridge/sources/migrate.move
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ module token_bridge::migrate {
token_bridge_state: &mut State,
receipt: DecreeReceipt<upgrade_contract::GovernanceWitness>
) {
state::migrate__v__0_2_0(token_bridge_state);
state::migrate__v__0_3_0(token_bridge_state);

// Perform standard migrate.
handle_migrate(token_bridge_state, receipt);
Expand Down
Loading
Loading