Module bridge::bridge
- Struct
Bridge
- Struct
BridgeInner
- Struct
TokenDepositedEvent
- Struct
EmergencyOpEvent
- Struct
BridgeRecord
- Struct
TokenTransferApproved
- Struct
TokenTransferClaimed
- Struct
TokenTransferAlreadyApproved
- Struct
TokenTransferAlreadyClaimed
- Struct
TokenTransferLimitExceed
- Constants
- Function
create
- Function
init_bridge_committee
- Function
committee_registration
- Function
update_node_url
- Function
register_foreign_token
- Function
send_token
- Function
approve_token_transfer
- Function
claim_token
- Function
claim_and_transfer_token
- Function
execute_system_message
- Function
get_token_transfer_action_status
- Function
get_token_transfer_action_signatures
- Function
load_inner
- Function
load_inner_mut
- Function
claim_token_internal
- Function
execute_emergency_op
- Function
execute_update_bridge_limit
- Function
execute_update_asset_price
- Function
execute_add_tokens_on_sui
- Function
get_current_seq_num_and_increment
- Function
get_parsed_token_transfer_message
use bridge::chain_ids;
use bridge::committee;
use bridge::crypto;
use bridge::limiter;
use bridge::message;
use bridge::message_types;
use bridge::treasury;
use std::address;
use std::ascii;
use std::bcs;
use std::option;
use std::string;
use std::type_name;
use std::u64;
use std::vector;
use sui::address;
use sui::bag;
use sui::balance;
use sui::bcs;
use sui::clock;
use sui::coin;
use sui::config;
use sui::deny_list;
use sui::dynamic_field;
use sui::dynamic_object_field;
use sui::ecdsa_k1;
use sui::event;
use sui::hash;
use sui::hex;
use sui::linked_table;
use sui::object;
use sui::object_bag;
use sui::package;
use sui::party;
use sui::priority_queue;
use sui::sui;
use sui::table;
use sui::table_vec;
use sui::transfer;
use sui::tx_context;
use sui::types;
use sui::url;
use sui::vec_map;
use sui::vec_set;
use sui::versioned;
use sui_system::stake_subsidy;
use sui_system::staking_pool;
use sui_system::storage_fund;
use sui_system::sui_system;
use sui_system::sui_system_state_inner;
use sui_system::validator;
use sui_system::validator_cap;
use sui_system::validator_set;
use sui_system::validator_wrapper;
use sui_system::voting_power;
Struct Bridge
public struct Bridge has key
Click to open
Fields
- id: sui::object::UID
- inner: sui::versioned::Versioned
Struct BridgeInner
public struct BridgeInner has store
Click to open
Fields
- bridge_version: u64
- message_version: u8
- chain_id: u8
- sequence_nums: sui::vec_map::VecMap<u8, u64>
- committee: bridge::committee::BridgeCommittee
- treasury: bridge::treasury::BridgeTreasury
- token_transfer_records: sui::linked_table::LinkedTable<bridge::message::BridgeMessageKey, bridge::bridge::BridgeRecord>
- limiter: bridge::limiter::TransferLimiter
- paused: bool
Struct TokenDepositedEvent
public struct TokenDepositedEvent has copy, drop
Click to open
Fields
- seq_num: u64
- source_chain: u8
- sender_address: vector<u8>
- target_chain: u8
- target_address: vector<u8>
- token_type: u8
- amount: u64
Struct EmergencyOpEvent
public struct EmergencyOpEvent has copy, drop
Click to open
Fields
- frozen: bool
Struct BridgeRecord
public struct BridgeRecord has drop, store
Click to open
Fields
- message: bridge::message::BridgeMessage
- verified_signatures: std::option::Option<vector<vector<u8>>>
- claimed: bool
Struct TokenTransferApproved
public struct TokenTransferApproved has copy, drop
Click to open
Fields
- message_key: bridge::message::BridgeMessageKey
Struct TokenTransferClaimed
public struct TokenTransferClaimed has copy, drop
Click to open
Fields
- message_key: bridge::message::BridgeMessageKey
Struct TokenTransferAlreadyApproved
public struct TokenTransferAlreadyApproved has copy, drop
Click to open
Fields
- message_key: bridge::message::BridgeMessageKey
Struct TokenTransferAlreadyClaimed
public struct TokenTransferAlreadyClaimed has copy, drop
Click to open
Fields
- message_key: bridge::message::BridgeMessageKey
Struct TokenTransferLimitExceed
public struct TokenTransferLimitExceed has copy, drop
Click to open
Fields
- message_key: bridge::message::BridgeMessageKey
Constants
const MESSAGE_VERSION: u8 = 1;
const TRANSFER_STATUS_PENDING: u8 = 0;
const TRANSFER_STATUS_APPROVED: u8 = 1;
const TRANSFER_STATUS_CLAIMED: u8 = 2;
const TRANSFER_STATUS_NOT_FOUND: u8 = 3;
const EVM_ADDRESS_LENGTH: u64 = 20;
const EUnexpectedMessageType: u64 = 0;
const EUnauthorisedClaim: u64 = 1;
const EMalformedMessageError: u64 = 2;
const EUnexpectedTokenType: u64 = 3;
const EUnexpectedChainID: u64 = 4;
const ENotSystemAddress: u64 = 5;
const EUnexpectedSeqNum: u64 = 6;
const EWrongInnerVersion: u64 = 7;
const EBridgeUnavailable: u64 = 8;
const EUnexpectedOperation: u64 = 9;
const EInvariantSuiInitializedTokenTransferShouldNotBeClaimed: u64 = 10;
const EMessageNotFoundInRecords: u64 = 11;
const EUnexpectedMessageVersion: u64 = 12;
const EBridgeAlreadyPaused: u64 = 13;
const EBridgeNotPaused: u64 = 14;
const ETokenAlreadyClaimedOrHitLimit: u64 = 15;
const EInvalidBridgeRoute: u64 = 16;
const EMustBeTokenMessage: u64 = 17;
const EInvalidEvmAddress: u64 = 18;
const ETokenValueIsZero: u64 = 19;
const CURRENT_VERSION: u64 = 1;
Function create
fun create(id: sui::object::UID, chain_id: u8, ctx: &mut sui::tx_context::TxContext)
Click to open
Implementation
fun create(id: UID, chain_id: u8, ctx: &mut TxContext) {
assert!(ctx.sender() == @0x0, ENotSystemAddress);
let bridge_inner = BridgeInner {
bridge_version: CURRENT_VERSION,
message_version: MESSAGE_VERSION,
chain_id,
sequence_nums: vec_map::empty(),
committee: committee::create(ctx),
treasury: treasury::create(ctx),
token_transfer_records: linked_table::new(ctx),
limiter: limiter::new(),
paused: false,
};
let bridge = Bridge {
id,
inner: versioned::create(CURRENT_VERSION, bridge_inner, ctx),
};
transfer::share_object(bridge);
}
Function init_bridge_committee
fun init_bridge_committee(bridge: &mut bridge::bridge::Bridge, active_validator_voting_power: sui::vec_map::VecMap<address, u64>, min_stake_participation_percentage: u64, ctx: &sui::tx_context::TxContext)
Click to open
Implementation
fun init_bridge_committee(
bridge: &mut Bridge,
active_validator_voting_power: VecMap<address, u64>,
min_stake_participation_percentage: u64,
ctx: &TxContext,
) {
assert!(ctx.sender() == @0x0, ENotSystemAddress);
let inner = load_inner_mut(bridge);
if (inner.committee.committee_members().is_empty()) {
inner
.committee
.try_create_next_committee(
active_validator_voting_power,
min_stake_participation_percentage,
ctx,
)
}
}
Function committee_registration
public fun committee_registration(bridge: &mut bridge::bridge::Bridge, system_state: &mut sui_system::sui_system::SuiSystemState, bridge_pubkey_bytes: vector<u8>, http_rest_url: vector<u8>, ctx: &sui::tx_context::TxContext)
Click to open
Implementation
public fun committee_registration(
bridge: &mut Bridge,
system_state: &mut SuiSystemState,
bridge_pubkey_bytes: vector<u8>,
http_rest_url: vector<u8>,
ctx: &TxContext,
) {
load_inner_mut(bridge)
.committee
.register(system_state, bridge_pubkey_bytes, http_rest_url, ctx);
}
Function update_node_url
public fun update_node_url(bridge: &mut bridge::bridge::Bridge, new_url: vector<u8>, ctx: &sui::tx_context::TxContext)
Click to open
Implementation
public fun update_node_url(bridge: &mut Bridge, new_url: vector<u8>, ctx: &TxContext) {
load_inner_mut(bridge).committee.update_node_url(new_url, ctx);
}
Function register_foreign_token
public fun register_foreign_token<T>(bridge: &mut bridge::bridge::Bridge, tc: sui::coin::TreasuryCap<T>, uc: sui::package::UpgradeCap, metadata: &sui::coin::CoinMetadata<T>)
Click to open
Implementation
public fun register_foreign_token<T>(
bridge: &mut Bridge,
tc: TreasuryCap<T>,
uc: UpgradeCap,
metadata: &CoinMetadata<T>,
) {
load_inner_mut(bridge).treasury.register_foreign_token<T>(tc, uc, metadata)
}
Function send_token
public fun send_token<T>(bridge: &mut bridge::bridge::Bridge, target_chain: u8, target_address: vector<u8>, token: sui::coin::Coin<T>, ctx: &mut sui::tx_context::TxContext)
Click to open
Implementation
public fun send_token<T>(
bridge: &mut Bridge,
target_chain: u8,
target_address: vector<u8>,
token: Coin<T>,
ctx: &mut TxContext,
) {
let inner = load_inner_mut(bridge);
assert!(!inner.paused, EBridgeUnavailable);
assert!(chain_ids::is_valid_route(inner.chain_id, target_chain), EInvalidBridgeRoute);
assert!(target_address.length() == EVM_ADDRESS_LENGTH, EInvalidEvmAddress);
let bridge_seq_num = inner.get_current_seq_num_and_increment(message_types::token());
let token_id = inner.treasury.token_id<T>();
let token_amount = token.balance().value();
assert!(token_amount > 0, ETokenValueIsZero);
// create bridge message
let message = message::create_token_bridge_message(
inner.chain_id,
bridge_seq_num,
address::to_bytes(ctx.sender()),
target_chain,
target_address,
token_id,
token_amount,
);
// burn / escrow token, unsupported coins will fail in this step
inner.treasury.burn(token);
// Store pending bridge request
inner
.token_transfer_records
.push_back(
message.key(),
BridgeRecord {
message,
verified_signatures: option::none(),
claimed: false,
},
);
// emit event
event::emit(TokenDepositedEvent {
seq_num: bridge_seq_num,
source_chain: inner.chain_id,
sender_address: address::to_bytes(ctx.sender()),
target_chain,
target_address,
token_type: token_id,
amount: token_amount,
});
}
Function approve_token_transfer
public fun approve_token_transfer(bridge: &mut bridge::bridge::Bridge, message: bridge::message::BridgeMessage, signatures: vector<vector<u8>>)
Click to open
Implementation
public fun approve_token_transfer(
bridge: &mut Bridge,
message: BridgeMessage,
signatures: vector<vector<u8>>,
) {
let inner = load_inner_mut(bridge);
assert!(!inner.paused, EBridgeUnavailable);
// verify signatures
inner.committee.verify_signatures(message, signatures);
assert!(message.message_type() == message_types::token(), EMustBeTokenMessage);
assert!(message.message_version() == MESSAGE_VERSION, EUnexpectedMessageVersion);
let token_payload = message.extract_token_bridge_payload();
let target_chain = token_payload.token_target_chain();
assert!(
message.source_chain() == inner.chain_id || target_chain == inner.chain_id,
EUnexpectedChainID,
);
let message_key = message.key();
// retrieve pending message if source chain is Sui, the initial message
// must exist on chain
if (message.source_chain() == inner.chain_id) {
let record = &mut inner.token_transfer_records[message_key];
assert!(record.message == message, EMalformedMessageError);
assert!(!record.claimed, EInvariantSuiInitializedTokenTransferShouldNotBeClaimed);
// If record already has verified signatures, it means the message has been approved
// Then we exit early.
if (record.verified_signatures.is_some()) {
event::emit(TokenTransferAlreadyApproved { message_key });
return
};
// Store approval
record.verified_signatures = option::some(signatures)
} else {
// At this point, if this message is in token_transfer_records, we know
// it's already approved because we only add a message to token_transfer_records
// after verifying the signatures
if (inner.token_transfer_records.contains(message_key)) {
event::emit(TokenTransferAlreadyApproved { message_key });
return
};
// Store message and approval
inner
.token_transfer_records
.push_back(
message_key,
BridgeRecord {
message,
verified_signatures: option::some(signatures),
claimed: false,
},
);
};
event::emit(TokenTransferApproved { message_key });
}
Function claim_token
public fun claim_token<T>(bridge: &mut bridge::bridge::Bridge, clock: &sui::clock::Clock, source_chain: u8, bridge_seq_num: u64, ctx: &mut sui::tx_context::TxContext): sui::coin::Coin<T>
Click to open
Implementation
public fun claim_token<T>(
bridge: &mut Bridge,
clock: &Clock,
source_chain: u8,
bridge_seq_num: u64,
ctx: &mut TxContext,
): Coin<T> {
let (maybe_token, owner) = bridge.claim_token_internal<T>(
clock,
source_chain,
bridge_seq_num,
ctx,
);
// Only token owner can claim the token
assert!(ctx.sender() == owner, EUnauthorisedClaim);
assert!(maybe_token.is_some(), ETokenAlreadyClaimedOrHitLimit);
maybe_token.destroy_some()
}
Function claim_and_transfer_token
public fun claim_and_transfer_token<T>(bridge: &mut bridge::bridge::Bridge, clock: &sui::clock::Clock, source_chain: u8, bridge_seq_num: u64, ctx: &mut sui::tx_context::TxContext)
Click to open
Implementation
public fun claim_and_transfer_token<T>(
bridge: &mut Bridge,
clock: &Clock,
source_chain: u8,
bridge_seq_num: u64,
ctx: &mut TxContext,
) {
let (token, owner) = bridge.claim_token_internal<T>(clock, source_chain, bridge_seq_num, ctx);
if (token.is_some()) {
transfer::public_transfer(token.destroy_some(), owner)
} else {
token.destroy_none();
};
}
Function execute_system_message
public fun execute_system_message(bridge: &mut bridge::bridge::Bridge, message: bridge::message::BridgeMessage, signatures: vector<vector<u8>>)
Click to open
Implementation
public fun execute_system_message(
bridge: &mut Bridge,
message: BridgeMessage,
signatures: vector<vector<u8>>,
) {
let message_type = message.message_type();
// TODO: test version mismatch
assert!(message.message_version() == MESSAGE_VERSION, EUnexpectedMessageVersion);
let inner = load_inner_mut(bridge);
assert!(message.source_chain() == inner.chain_id, EUnexpectedChainID);
// check system ops seq number and increment it
let expected_seq_num = inner.get_current_seq_num_and_increment(message_type);
assert!(message.seq_num() == expected_seq_num, EUnexpectedSeqNum);
inner.committee.verify_signatures(message, signatures);
if (message_type == message_types::emergency_op()) {
let payload = message.extract_emergency_op_payload();
inner.execute_emergency_op(payload);
} else if (message_type == message_types::committee_blocklist()) {
let payload = message.extract_blocklist_payload();
inner.committee.execute_blocklist(payload);
} else if (message_type == message_types::update_bridge_limit()) {
let payload = message.extract_update_bridge_limit();
inner.execute_update_bridge_limit(payload);
} else if (message_type == message_types::update_asset_price()) {
let payload = message.extract_update_asset_price();
inner.execute_update_asset_price(payload);
} else if (message_type == message_types::add_tokens_on_sui()) {
let payload = message.extract_add_tokens_on_sui();
inner.execute_add_tokens_on_sui(payload);
} else {
abort EUnexpectedMessageType
};
}
Function get_token_transfer_action_status
fun get_token_transfer_action_status(bridge: &bridge::bridge::Bridge, source_chain: u8, bridge_seq_num: u64): u8
Click to open
Implementation
fun get_token_transfer_action_status(bridge: &Bridge, source_chain: u8, bridge_seq_num: u64): u8 {
let inner = load_inner(bridge);
let key = message::create_key(
source_chain,
message_types::token(),
bridge_seq_num,
);
if (!inner.token_transfer_records.contains(key)) {
return TRANSFER_STATUS_NOT_FOUND
};
let record = &inner.token_transfer_records[key];
if (record.claimed) {
return TRANSFER_STATUS_CLAIMED
};
if (record.verified_signatures.is_some()) {
return TRANSFER_STATUS_APPROVED
};
TRANSFER_STATUS_PENDING
}
Function get_token_transfer_action_signatures
fun get_token_transfer_action_signatures(bridge: &bridge::bridge::Bridge, source_chain: u8, bridge_seq_num: u64): std::option::Option<vector<vector<u8>>>
Click to open
Implementation
fun get_token_transfer_action_signatures(
bridge: &Bridge,
source_chain: u8,
bridge_seq_num: u64,
): Option<vector<vector<u8>>> {
let inner = load_inner(bridge);
let key = message::create_key(
source_chain,
message_types::token(),
bridge_seq_num,
);
if (!inner.token_transfer_records.contains(key)) {
return option::none()
};
let record = &inner.token_transfer_records[key];
record.verified_signatures
}
Function load_inner
fun load_inner(bridge: &bridge::bridge::Bridge): &bridge::bridge::BridgeInner
Click to open
Implementation
fun load_inner(bridge: &Bridge): &BridgeInner {
let version = bridge.inner.version();
// TODO: Replace this with a lazy update function when we add a new version of the inner object.
assert!(version == CURRENT_VERSION, EWrongInnerVersion);
let inner: &BridgeInner = bridge.inner.load_value();
assert!(inner.bridge_version == version, EWrongInnerVersion);
inner
}
Function load_inner_mut
fun load_inner_mut(bridge: &mut bridge::bridge::Bridge): &mut bridge::bridge::BridgeInner
Click to open
Implementation
fun load_inner_mut(bridge: &mut Bridge): &mut BridgeInner {
let version = bridge.inner.version();
// TODO: Replace this with a lazy update function when we add a new version of the inner object.
assert!(version == CURRENT_VERSION, EWrongInnerVersion);
let inner: &mut BridgeInner = bridge.inner.load_value_mut();
assert!(inner.bridge_version == version, EWrongInnerVersion);
inner
}
Function claim_token_internal
fun claim_token_internal<T>(bridge: &mut bridge::bridge::Bridge, clock: &sui::clock::Clock, source_chain: u8, bridge_seq_num: u64, ctx: &mut sui::tx_context::TxContext): (std::option::Option<sui::coin::Coin<T>>, address)
Click to open
Implementation
fun claim_token_internal<T>(
bridge: &mut Bridge,
clock: &Clock,
source_chain: u8,
bridge_seq_num: u64,
ctx: &mut TxContext,
): (Option<Coin<T>>, address) {
let inner = load_inner_mut(bridge);
assert!(!inner.paused, EBridgeUnavailable);
let key = message::create_key(source_chain, message_types::token(), bridge_seq_num);
assert!(inner.token_transfer_records.contains(key), EMessageNotFoundInRecords);
// retrieve approved bridge message
let record = &mut inner.token_transfer_records[key];
// ensure this is a token bridge message
assert!(&record.message.message_type() == message_types::token(), EUnexpectedMessageType);
// Ensure it's signed
assert!(record.verified_signatures.is_some(), EUnauthorisedClaim);
// extract token message
let token_payload = record.message.extract_token_bridge_payload();
// get owner address
let owner = address::from_bytes(token_payload.token_target_address());
// If already claimed, exit early
if (record.claimed) {
event::emit(TokenTransferAlreadyClaimed { message_key: key });
return (option::none(), owner)
};
let target_chain = token_payload.token_target_chain();
// ensure target chain matches bridge.chain_id
assert!(target_chain == inner.chain_id, EUnexpectedChainID);
// TODO: why do we check validity of the route here? what if inconsistency?
// Ensure route is valid
// TODO: add unit tests
// `get_route` abort if route is invalid
let route = chain_ids::get_route(source_chain, target_chain);
// check token type
assert!(
treasury::token_id<T>(&inner.treasury) == token_payload.token_type(),
EUnexpectedTokenType,
);
let amount = token_payload.token_amount();
// Make sure transfer is within limit.
if (
!inner
.limiter
.check_and_record_sending_transfer<T>(
&inner.treasury,
clock,
route,
amount,
)
) {
event::emit(TokenTransferLimitExceed { message_key: key });
return (option::none(), owner)
};
// claim from treasury
let token = inner.treasury.mint<T>(amount, ctx);
// Record changes
record.claimed = true;
event::emit(TokenTransferClaimed { message_key: key });
(option::some(token), owner)
}
Function execute_emergency_op
fun execute_emergency_op(inner: &mut bridge::bridge::BridgeInner, payload: bridge::message::EmergencyOp)
Click to open
Implementation
fun execute_emergency_op(inner: &mut BridgeInner, payload: EmergencyOp) {
let op = payload.emergency_op_type();
if (op == message::emergency_op_pause()) {
assert!(!inner.paused, EBridgeAlreadyPaused);
inner.paused = true;
event::emit(EmergencyOpEvent { frozen: true });
} else if (op == message::emergency_op_unpause()) {
assert!(inner.paused, EBridgeNotPaused);
inner.paused = false;
event::emit(EmergencyOpEvent { frozen: false });
} else {
abort EUnexpectedOperation
};
}
Function execute_update_bridge_limit
fun execute_update_bridge_limit(inner: &mut bridge::bridge::BridgeInner, payload: bridge::message::UpdateBridgeLimit)
Click to open
Implementation
fun execute_update_bridge_limit(inner: &mut BridgeInner, payload: UpdateBridgeLimit) {
let receiving_chain = payload.update_bridge_limit_payload_receiving_chain();
assert!(receiving_chain == inner.chain_id, EUnexpectedChainID);
let route = chain_ids::get_route(
payload.update_bridge_limit_payload_sending_chain(),
receiving_chain,
);
inner
.limiter
.update_route_limit(
&route,
payload.update_bridge_limit_payload_limit(),
)
}
Function execute_update_asset_price
fun execute_update_asset_price(inner: &mut bridge::bridge::BridgeInner, payload: bridge::message::UpdateAssetPrice)
Click to open
Implementation
fun execute_update_asset_price(inner: &mut BridgeInner, payload: UpdateAssetPrice) {
inner
.treasury
.update_asset_notional_price(
payload.update_asset_price_payload_token_id(),
payload.update_asset_price_payload_new_price(),
)
}
Function execute_add_tokens_on_sui
fun execute_add_tokens_on_sui(inner: &mut bridge::bridge::BridgeInner, payload: bridge::message::AddTokenOnSui)
Click to open
Implementation
fun execute_add_tokens_on_sui(inner: &mut BridgeInner, payload: AddTokenOnSui) {
// FIXME: assert native_token to be false and add test
let native_token = payload.is_native();
let mut token_ids = payload.token_ids();
let mut token_type_names = payload.token_type_names();
let mut token_prices = payload.token_prices();
// Make sure token data is consistent
assert!(token_ids.length() == token_type_names.length(), EMalformedMessageError);
assert!(token_ids.length() == token_prices.length(), EMalformedMessageError);
while (token_ids.length() > 0) {
let token_id = token_ids.pop_back();
let token_type_name = token_type_names.pop_back();
let token_price = token_prices.pop_back();
inner.treasury.add_new_token(token_type_name, token_id, native_token, token_price)
}
}
Function get_current_seq_num_and_increment
fun get_current_seq_num_and_increment(bridge: &mut bridge::bridge::BridgeInner, msg_type: u8): u64
Click to open
Implementation
fun get_current_seq_num_and_increment(bridge: &mut BridgeInner, msg_type: u8): u64 {
if (!bridge.sequence_nums.contains(&msg_type)) {
bridge.sequence_nums.insert(msg_type, 1);
return 0
};
let entry = &mut bridge.sequence_nums[&msg_type];
let seq_num = *entry;
*entry = seq_num + 1;
seq_num
}
Function get_parsed_token_transfer_message
fun get_parsed_token_transfer_message(bridge: &bridge::bridge::Bridge, source_chain: u8, bridge_seq_num: u64): std::option::Option<bridge::message::ParsedTokenTransferMessage>
Click to open
Implementation
fun get_parsed_token_transfer_message(
bridge: &Bridge,
source_chain: u8,
bridge_seq_num: u64,
): Option<ParsedTokenTransferMessage> {
let inner = load_inner(bridge);
let key = message::create_key(
source_chain,
message_types::token(),
bridge_seq_num,
);
if (!inner.token_transfer_records.contains(key)) {
return option::none()
};
let record = &inner.token_transfer_records[key];
let message = &record.message;
option::some(to_parsed_token_transfer_message(message))
}