diff --git a/.gitignore b/.gitignore
index 78811d1..da882a5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,6 @@ build/*
.vscode
.cursor
.prettierrc
-node_modules/
\ No newline at end of file
+.replay/
+node_modules/
+vendor/
\ No newline at end of file
diff --git a/Move.lock b/Move.lock
deleted file mode 100644
index fba1734..0000000
--- a/Move.lock
+++ /dev/null
@@ -1,77 +0,0 @@
-# @generated by Move, please check-in and do not edit manually.
-
-[move]
-version = 3
-manifest_digest = "5D85B40033B1406A10E38D3C3FCCF032577FBCF8B3DA4F402451BDE210D9B95F"
-deps_digest = "397E6A9F7A624706DBDFEE056CE88391A15876868FD18A88504DA74EB458D697"
-dependencies = [
- { id = "Bridge", name = "Bridge" },
- { id = "MoveStdlib", name = "MoveStdlib" },
- { id = "Sui", name = "Sui" },
- { id = "SuiSystem", name = "SuiSystem" },
- { id = "locked_token", name = "locked_token" },
-]
-
-[[move.package]]
-id = "Bridge"
-source = { git = "https://github.com/MystenLabs/sui.git", rev = "664b05b3b047c5bb03979d093660176176ea6175", subdir = "crates/sui-framework/packages/bridge" }
-
-dependencies = [
- { id = "MoveStdlib", name = "MoveStdlib" },
- { id = "Sui", name = "Sui" },
- { id = "SuiSystem", name = "SuiSystem" },
-]
-
-[[move.package]]
-id = "MoveStdlib"
-source = { git = "https://github.com/MystenLabs/sui.git", rev = "664b05b3b047c5bb03979d093660176176ea6175", subdir = "crates/sui-framework/packages/move-stdlib" }
-
-[[move.package]]
-id = "Sui"
-source = { git = "https://github.com/MystenLabs/sui.git", rev = "664b05b3b047c5bb03979d093660176176ea6175", subdir = "crates/sui-framework/packages/sui-framework" }
-
-dependencies = [
- { id = "MoveStdlib", name = "MoveStdlib" },
-]
-
-[[move.package]]
-id = "SuiSystem"
-source = { git = "https://github.com/MystenLabs/sui.git", rev = "664b05b3b047c5bb03979d093660176176ea6175", subdir = "crates/sui-framework/packages/sui-system" }
-
-dependencies = [
- { id = "MoveStdlib", name = "MoveStdlib" },
- { id = "Sui", name = "Sui" },
-]
-
-[[move.package]]
-id = "locked_token"
-source = { git = "https://github.com/multiversx/mx-locked-token-sc-sui.git", rev = "master", subdir = "" }
-
-dependencies = [
- { id = "Bridge", name = "Bridge" },
- { id = "MoveStdlib", name = "MoveStdlib" },
- { id = "Sui", name = "Sui" },
- { id = "SuiSystem", name = "SuiSystem" },
- { id = "sui_extensions", name = "sui_extensions" },
-]
-
-[[move.package]]
-id = "sui_extensions"
-source = { git = "https://github.com/circlefin/stablecoin-sui.git", rev = "master", subdir = "packages/sui_extensions" }
-
-dependencies = [
- { id = "Sui", name = "Sui" },
-]
-
-[move.toolchain-version]
-compiler-version = "1.61.2"
-edition = "2024.beta"
-flavor = "sui"
-
-[env]
-
-[env.testnet]
-chain-id = "4c78adac"
-original-published-id = "0x797933e12440a65f84f46d1b650408e21efeb64f73bcb4e274d576035462833a"
-latest-published-id = "0x797933e12440a65f84f46d1b650408e21efeb64f73bcb4e274d576035462833a"
-published-version = "1"
diff --git a/Move.toml b/Move.toml
index 08a3fc0..4c6f95d 100644
--- a/Move.toml
+++ b/Move.toml
@@ -1,22 +1,9 @@
[package]
-name = "mx-bridge-sc-sui"
+name = "bridge_safe"
edition = "2024.beta"
+version = "0.0.1"
[dependencies]
-locked_token = { git = "https://github.com/multiversx/mx-locked-token-sc-sui.git", rev = "master", subdir = "" }
-
-[addresses]
-shared_structs = "0x0"
-safe_module = "0x0"
-bridge_safe = "0x0"
-utils = "0x0"
-
-[dev-dependencies]
-# The dev-dependencies section allows overriding dependencies for `--test` and
-# `--dev` modes. You can introduce test-only dependencies here.
-# Local = { local = "../path/to/dev-build" }
-
-[dev-addresses]
-# The dev-addresses section allows overwriting named addresses for the `--test`
-# and `--dev` modes.
-# alice = "0xB0B"
+treasury = { git = "https://github.com/multiversx/stablecoin-sui.git", rev="xmn-launch", subdir = "packages/treasury", published-at = "0x28182c116f8f9a015b348f61e6330bdccdae7c192b9ba7293d4e296e912ed070"}
+locked_token = { git = "https://github.com/multiversx/mx-locked-token-sc-sui.git", rev = "master", subdir = "", published-at = "0xcbcfc2873899ee4c54e16aa8e63403852001fd4587558a9371e358603189c528"}
+sui_extensions = { git = "https://github.com/multiversx/stablecoin-sui.git", rev="xmn-launch", subdir = "packages/sui_extensions", override = true }
diff --git a/sources/bridge_module.move b/sources/bridge_module.move
index 30dfc2f..c2cdf84 100644
--- a/sources/bridge_module.move
+++ b/sources/bridge_module.move
@@ -1,5 +1,5 @@
/// Bridge Module - Cross-chain Bridge Implementation
-///
+///
/// This module implements a secure cross-chain bridge that allows transferring
/// tokens between different blockchain networks. It includes relayer management,
/// batch processing, signature validation, and migration support.
@@ -7,23 +7,24 @@
module bridge_safe::bridge;
use bridge_safe::bridge_roles::BridgeCap;
+use bridge_safe::bridge_version_control;
use bridge_safe::events;
use bridge_safe::pausable::{Self, Pause};
-use bridge_safe::safe::{Self, BridgeSafe};
+use bridge_safe::safe::BridgeSafe;
use bridge_safe::utils;
-use bridge_safe::bridge_version_control;
use locked_token::bridge_token::BRIDGE_TOKEN;
-use locked_token::treasury;
-use shared_structs::shared_structs::{Self, Deposit, Batch, CrossTransferStatus, DepositStatus};
+use locked_token::treasury::{Self as lkt};
+use bridge_safe::shared_structs::{Self, Deposit, Batch, CrossTransferStatus, DepositStatus};
use std::u64::{min, max};
use sui::address;
use sui::bcs;
-use sui::clock::{Self, Clock};
+use sui::clock::Clock;
use sui::ed25519;
use sui::event;
use sui::hash::blake2b256;
use sui::table::{Self, Table};
use sui::vec_set::{Self, VecSet};
+use treasury::treasury;
// === Error Constants ===
const EQuorumTooLow: u64 = 0;
@@ -103,17 +104,18 @@ public fun initialize(
ctx: &mut TxContext,
) {
assert!(initial_quorum >= MINIMUM_QUORUM, EQuorumTooLow);
+ assert!(initial_quorum <= public_keys.length(), EQuorumExceedsRelayers);
let mut relayers = vec_set::empty
();
let mut relayer_public_keys = table::new>(ctx);
let mut i = 0;
- while (i < vector::length(&public_keys)) {
- let pk = *vector::borrow(&public_keys, i);
- assert!(vector::length(&pk) == ED25519_PUBLIC_KEY_LENGTH, EInvalidPublicKeyLength);
+ while (i < public_keys.length()) {
+ let pk = *public_keys.borrow(i);
+ assert!(pk.length() == ED25519_PUBLIC_KEY_LENGTH, EInvalidPublicKeyLength);
let relayer_address = getAddressFromPublicKey(&pk);
- vec_set::insert(&mut relayers, relayer_address);
- table::add(&mut relayer_public_keys, relayer_address, pk);
+ relayers.insert(relayer_address);
+ relayer_public_keys.add(relayer_address, pk);
i = i + 1;
};
@@ -127,7 +129,7 @@ public fun initialize(
executed_batches: vec_set::empty(),
execution_timestamps: table::new(ctx),
cross_transfer_statuses: table::new(ctx),
- transfer_statuses: vector::empty(),
+ transfer_statuses: vector[],
safe: safe_address,
bridge_cap,
executed_transfer_by_batch_type_arg: vec_set::empty>(),
@@ -144,14 +146,14 @@ public fun initialize(
/// address = blake2b256( 0x00 || ed25519_pubkey )
fun getAddressFromPublicKey(public_key: &vector): address {
let mut long_public_key = vector[0u8];
- vector::append(&mut long_public_key, *public_key);
+ long_public_key.append(*public_key);
let relayer_bytes = sui::hash::blake2b256(&long_public_key);
address::from_bytes(relayer_bytes)
}
/// Asserts that the caller is a registered relayer
fun assert_relayer(bridge: &Bridge, signer: address) {
- assert!(vec_set::contains(&bridge.relayers, &signer), ENotRelayer);
+ assert!(bridge.relayers.contains(&signer), ENotRelayer);
}
// === Configuration Management ===
@@ -163,10 +165,11 @@ public fun set_quorum(
new_quorum: u64,
ctx: &mut TxContext,
) {
- safe::checkOwnerRole(safe, ctx);
+ assert_bridge_is_compatible(bridge);
+ safe.checkOwnerRole(ctx);
assert!(new_quorum >= MINIMUM_QUORUM, EQuorumTooLow);
- assert!(new_quorum <= vec_set::length(&bridge.relayers), EQuorumExceedsRelayers);
+ assert!(new_quorum <= bridge.relayers.length(), EQuorumExceedsRelayers);
bridge.quorum = new_quorum;
event::emit(QuorumChanged { new_quorum });
@@ -179,13 +182,15 @@ public fun set_batch_settle_timeout_ms(
clock: &Clock,
ctx: &mut TxContext,
) {
- safe::checkOwnerRole(safe, ctx);
+ assert_bridge_is_compatible(bridge);
+ safe.checkOwnerRole(ctx);
- pausable::assert_paused(&bridge.pause);
- assert!(new_timeout_ms >= safe::get_batch_timeout_ms(safe), ESettleTimeoutBelowSafeBatch);
- assert!(!safe::is_any_batch_in_progress(safe, clock), EPendingBatches);
+ bridge.pause.assert_paused();
+ assert!(new_timeout_ms >= safe.get_batch_timeout_ms(), ESettleTimeoutBelowSafeBatch);
+ assert!(!safe.is_any_batch_in_progress(clock), EPendingBatches);
bridge.batch_settle_timeout_ms = new_timeout_ms;
+ events::emit_batch_settle_timeout_updated(new_timeout_ms);
}
// === Relayer Management ===
@@ -197,15 +202,16 @@ public fun add_relayer(
public_key: vector,
ctx: &mut TxContext,
) {
- safe::checkOwnerRole(safe, ctx);
+ assert_bridge_is_compatible(bridge);
+ safe.checkOwnerRole(ctx);
- assert!(vector::length(&public_key) == ED25519_PUBLIC_KEY_LENGTH, EInvalidPublicKeyLength);
+ assert!(public_key.length() == ED25519_PUBLIC_KEY_LENGTH, EInvalidPublicKeyLength);
let relayer_address = getAddressFromPublicKey(&public_key);
- assert!(!vec_set::contains(&bridge.relayers, &relayer_address), ERelayerAlreadyExists);
+ assert!(!bridge.relayers.contains(&relayer_address), ERelayerAlreadyExists);
- vec_set::insert(&mut bridge.relayers, relayer_address);
- table::add(&mut bridge.relayer_public_keys, relayer_address, public_key);
- events::emit_relayer_added(relayer_address, tx_context::sender(ctx));
+ bridge.relayers.insert(relayer_address);
+ bridge.relayer_public_keys.add(relayer_address, public_key);
+ events::emit_relayer_added(relayer_address, ctx.sender());
}
public fun remove_relayer(
@@ -214,20 +220,21 @@ public fun remove_relayer(
relayer: address,
ctx: &mut TxContext,
) {
- safe::checkOwnerRole(safe, ctx);
+ assert_bridge_is_compatible(bridge);
+ safe.checkOwnerRole(ctx);
- let current_count = vec_set::length(&bridge.relayers);
+ let current_count = bridge.relayers.length();
assert!(current_count > bridge.quorum, ECannotRemoveRelayerBelowQuorum);
- vec_set::remove(&mut bridge.relayers, &relayer);
- if (table::contains(&bridge.relayer_public_keys, relayer)) {
- table::remove(&mut bridge.relayer_public_keys, relayer);
+ bridge.relayers.remove(&relayer);
+ if (bridge.relayer_public_keys.contains(relayer)) {
+ bridge.relayer_public_keys.remove(relayer);
};
- events::emit_relayer_removed(relayer, tx_context::sender(ctx));
+ events::emit_relayer_removed(relayer, ctx.sender());
}
public fun get_batch(safe: &BridgeSafe, batch_nonce: u64, clock: &Clock): (Batch, bool) {
- safe::get_batch(safe, batch_nonce, clock)
+ safe.get_batch(batch_nonce, clock)
}
public fun get_batch_deposits(
@@ -235,11 +242,11 @@ public fun get_batch_deposits(
batch_nonce: u64,
clock: &Clock,
): (vector, bool) {
- safe::get_deposits(safe, batch_nonce, clock)
+ safe.get_deposits(batch_nonce, clock)
}
public fun was_batch_executed(bridge: &Bridge, batch_nonce_mvx: u64): bool {
- vec_set::contains(&bridge.executed_batches, &batch_nonce_mvx)
+ bridge.executed_batches.contains(&batch_nonce_mvx)
}
public fun get_statuses_after_execution(
@@ -247,16 +254,14 @@ public fun get_statuses_after_execution(
batch_nonce_mvx: u64,
clock: &Clock,
): (vector, bool) {
- if (table::contains(&bridge.cross_transfer_statuses, batch_nonce_mvx)) {
- let cross_status = table::borrow(&bridge.cross_transfer_statuses, batch_nonce_mvx);
- let statuses = shared_structs::cross_transfer_status_statuses(cross_status);
- let created_timestamp = shared_structs::cross_transfer_status_created_timestamp_ms(
- cross_status,
- );
+ if (bridge.cross_transfer_statuses.contains(batch_nonce_mvx)) {
+ let cross_status = bridge.cross_transfer_statuses.borrow(batch_nonce_mvx);
+ let statuses = cross_status.cross_transfer_status_statuses();
+ let created_timestamp = cross_status.cross_transfer_status_created_timestamp_ms();
let is_final = is_mvx_batch_final(bridge, created_timestamp, clock);
(statuses, is_final)
} else {
- (vector::empty(), false)
+ (vector[], false)
}
}
@@ -269,114 +274,50 @@ public fun execute_transfer(
batch_nonce_mvx: u64,
signatures: vector>,
is_batch_complete: bool,
- treasury: &mut treasury::Treasury,
+ treasury: &mut lkt::Treasury,
clock: &Clock,
ctx: &mut TxContext,
) {
- let signer = tx_context::sender(ctx);
- assert_relayer(bridge, signer);
- pausable::assert_not_paused(&bridge.pause);
- assert!(!was_batch_executed(bridge, batch_nonce_mvx), EBatchAlreadyExecuted);
-
- let len = vector::length(&recipients);
- assert!(vector::length(&amounts) == len, EInvalidAmountsLength);
- assert!(vector::length(&deposit_nonces) == len, EInvalidDepositNoncesLength);
-
- validate_quorum(
+ assert_bridge_is_compatible(bridge);
+ safe.assert_is_compatible();
+ pre_execute_transfer(
bridge,
batch_nonce_mvx,
&recipients,
&amounts,
- &signatures,
&deposit_nonces,
+ &signatures,
+ clock,
+ ctx,
);
- mark_deposits_executed_in_batch_or_abort(bridge, batch_nonce_mvx);
-
- let now = clock::timestamp_ms(clock);
- if (table::contains(&bridge.execution_timestamps, batch_nonce_mvx)) {
- let t = table::borrow_mut(&mut bridge.execution_timestamps, batch_nonce_mvx);
- *t = now;
- } else {
- table::add(&mut bridge.execution_timestamps, batch_nonce_mvx, now);
- };
-
+ let len = recipients.length();
let mut i = 0;
- while (i < vector::length(&recipients)) {
- let recipient = *vector::borrow(&recipients, i);
- let amount = *vector::borrow(&amounts, i);
-
- let success = safe::transfer(safe, &bridge.bridge_cap, recipient, amount, treasury, ctx);
- if (success) {
- vector::push_back(
- &mut bridge.transfer_statuses,
- shared_structs::deposit_status_executed(),
- );
-
- // Increment successful deposits count
- if (table::contains(&bridge.successful_transfers_by_batch, batch_nonce_mvx)) {
- let current_count = table::borrow_mut(
- &mut bridge.successful_transfers_by_batch,
- batch_nonce_mvx,
- );
- *current_count = *current_count + 1;
- } else {
- table::add(&mut bridge.successful_transfers_by_batch, batch_nonce_mvx, 1);
- };
- } else {
- vector::push_back(
- &mut bridge.transfer_statuses,
- shared_structs::deposit_status_rejected(),
- );
- };
+ while (i < len) {
+ let success = safe.transfer(
+ &bridge.bridge_cap,
+ *recipients.borrow(i),
+ *amounts.borrow(i),
+ treasury,
+ ctx,
+ );
+ record_transfer_result(bridge, batch_nonce_mvx, success);
i = i + 1;
};
- if (is_batch_complete) {
- vec_set::insert(&mut bridge.executed_batches, batch_nonce_mvx);
-
- let cross_status = shared_structs::create_cross_transfer_status(
- bridge.transfer_statuses,
- clock::timestamp_ms(clock),
- );
- table::add(&mut bridge.cross_transfer_statuses, batch_nonce_mvx, cross_status);
-
- let total_transfers = vector::length(&recipients);
- bridge.transfer_statuses = vector::empty();
-
- let successful_count = if (
- table::contains(&bridge.successful_transfers_by_batch, batch_nonce_mvx)
- ) {
- *table::borrow(&bridge.successful_transfers_by_batch, batch_nonce_mvx)
- } else {
- 0
- };
-
- if (table::contains(&bridge.successful_transfers_by_batch, batch_nonce_mvx)) {
- table::remove(&mut bridge.successful_transfers_by_batch, batch_nonce_mvx);
- };
-
- event::emit(BatchExecuted {
- batch_nonce_mvx,
- transfers_count: total_transfers,
- successful_transfers: successful_count,
- });
- };
+ finalize_batch(bridge, batch_nonce_mvx, len, is_batch_complete, clock);
}
-fun mark_deposits_executed_in_batch_or_abort(
- bridge: &mut Bridge,
- batch_nonce_mvx: u64,
-) {
+fun mark_deposits_executed_in_batch_or_abort(bridge: &mut Bridge, batch_nonce_mvx: u64) {
let key = derive_key(batch_nonce_mvx);
- assert!(!vec_set::contains(&bridge.executed_transfer_by_batch_type_arg, &key), EDepositAlreadyExecuted);
- vec_set::insert(&mut bridge.executed_transfer_by_batch_type_arg, key);
+ assert!(!bridge.executed_transfer_by_batch_type_arg.contains(&key), EDepositAlreadyExecuted);
+ bridge.executed_transfer_by_batch_type_arg.insert(key);
}
fun derive_key(batch_nonce: u64): vector {
let mut data = bcs::to_bytes(&batch_nonce);
let type_bytes = utils::type_name_bytes();
- vector::append(&mut data, type_bytes);
+ data.append(type_bytes);
blake2b256(&data)
}
@@ -385,7 +326,7 @@ fun is_mvx_batch_final(bridge: &Bridge, created_timestamp_ms: u64, clock: &Clock
if (created_timestamp_ms == 0) {
false
} else {
- (created_timestamp_ms + bridge.batch_settle_timeout_ms) <= clock::timestamp_ms(clock)
+ (created_timestamp_ms + bridge.batch_settle_timeout_ms) <= clock.timestamp_ms()
}
}
@@ -398,11 +339,11 @@ public fun get_batch_settle_timeout_ms(bridge: &Bridge): u64 {
}
public fun is_relayer(bridge: &Bridge, addr: address): bool {
- vec_set::contains(&bridge.relayers, &addr)
+ bridge.relayers.contains(&addr)
}
public fun get_admin(safe: &BridgeSafe): address {
- safe::get_owner(safe)
+ safe.get_owner()
}
public fun get_pause(bridge: &Bridge): bool {
@@ -410,21 +351,23 @@ public fun get_pause(bridge: &Bridge): bool {
}
public fun get_relayers(bridge: &Bridge): &vector {
- vec_set::keys(&bridge.relayers)
+ bridge.relayers.keys()
}
public fun get_relayer_count(bridge: &Bridge): u64 {
- vec_set::length(&bridge.relayers)
+ bridge.relayers.length()
}
public fun pause_contract(bridge: &mut Bridge, safe: &BridgeSafe, ctx: &mut TxContext) {
- safe::checkOwnerRole(safe, ctx);
- pausable::pause(&mut bridge.pause);
+ assert_bridge_is_compatible(bridge);
+ safe.checkOwnerRole(ctx);
+ bridge.pause.pause();
}
public fun unpause_contract(bridge: &mut Bridge, safe: &BridgeSafe, ctx: &mut TxContext) {
- safe::checkOwnerRole(safe, ctx);
- pausable::unpause(&mut bridge.pause);
+ assert_bridge_is_compatible(bridge);
+ safe.checkOwnerRole(ctx);
+ bridge.pause.unpause();
}
fun validate_quorum(
@@ -436,7 +379,7 @@ fun validate_quorum(
deposit_nonces: &vector,
) {
let token_bytes = utils::type_name_bytes();
- let num_signatures = vector::length(signatures);
+ let num_signatures = signatures.length();
assert!(num_signatures >= bridge.quorum, EQuorumNotReached);
let message = compute_message(batch_id, &token_bytes, recipients, amounts, deposit_nonces);
@@ -445,9 +388,9 @@ fun validate_quorum(
let mut i = 0;
while (i < num_signatures) {
- let signature = vector::borrow(signatures, i);
+ let signature = signatures.borrow(i);
- assert!(vector::length(signature) == SIGNATURE_LENGTH, EInvalidSignatureLength);
+ assert!(signature.length() == SIGNATURE_LENGTH, EInvalidSignatureLength);
let public_key = extract_public_key(signature);
let sig_bytes = extract_signature(signature);
@@ -457,15 +400,15 @@ fun validate_quorum(
let relayer = option::extract(&mut relayer_opt);
- assert!(!vec_set::contains(&verified_relayers, &relayer), EDuplicateSignature);
+ assert!(!verified_relayers.contains(&relayer), EDuplicateSignature);
assert!(ed25519::ed25519_verify(&sig_bytes, &public_key, &message), EInvalidSignature);
- vec_set::insert(&mut verified_relayers, relayer);
+ verified_relayers.insert(relayer);
i = i + 1;
};
- assert!(vec_set::length(&verified_relayers) >= bridge.quorum, EQuorumNotReached);
+ assert!(verified_relayers.length() >= bridge.quorum, EQuorumNotReached);
}
public fun compute_message(
@@ -478,7 +421,7 @@ public fun compute_message(
let message = construct_batch_message(batch_id, token, recipients, amounts, deposit_nonces);
let encoded_msg = bcs::to_bytes(&message);
let mut intent_message = vector[3u8, 0u8, 0u8];
- vector::append(&mut intent_message, encoded_msg);
+ intent_message.append(encoded_msg);
sui::hash::blake2b256(&intent_message)
}
@@ -492,15 +435,15 @@ fun construct_batch_message(
let mut message = bcs::to_bytes(&batch_id);
let mut i = 0;
- while (i < vector::length(recipients)) {
- let recipient = vector::borrow(recipients, i);
- let amount = vector::borrow(amounts, i);
- let deposit_nonce = vector::borrow(deposit_nonces, i);
-
- vector::append(&mut message, bcs::to_bytes(token));
- vector::append(&mut message, bcs::to_bytes(recipient));
- vector::append(&mut message, bcs::to_bytes(amount));
- vector::append(&mut message, bcs::to_bytes(deposit_nonce));
+ while (i < recipients.length()) {
+ let recipient = recipients.borrow(i);
+ let amount = amounts.borrow(i);
+ let deposit_nonce = deposit_nonces.borrow(i);
+
+ message.append(bcs::to_bytes(token));
+ message.append(bcs::to_bytes(recipient));
+ message.append(bcs::to_bytes(amount));
+ message.append(bcs::to_bytes(deposit_nonce));
i = i + 1;
};
@@ -508,33 +451,33 @@ fun construct_batch_message(
}
fun extract_public_key(signature: &vector): vector {
- let mut public_key = vector::empty();
- let mut i = vector::length(signature) - ED25519_PUBLIC_KEY_LENGTH;
- while (i < vector::length(signature)) {
- vector::push_back(&mut public_key, *vector::borrow(signature, i));
+ let mut public_key = vector[];
+ let mut i = signature.length() - ED25519_PUBLIC_KEY_LENGTH;
+ while (i < signature.length()) {
+ public_key.push_back(*signature.borrow(i));
i = i + 1;
};
public_key
}
fun extract_signature(signature: &vector): vector {
- let mut sig_bytes = vector::empty();
+ let mut sig_bytes = vector[];
let mut i = 0;
- while (i < vector::length(signature) - ED25519_PUBLIC_KEY_LENGTH) {
- vector::push_back(&mut sig_bytes, *vector::borrow(signature, i));
+ while (i < signature.length() - ED25519_PUBLIC_KEY_LENGTH) {
+ sig_bytes.push_back(*signature.borrow(i));
i = i + 1;
};
sig_bytes
}
fun find_relayer_by_public_key(bridge: &Bridge, public_key: &vector): Option {
- let relayers = vec_set::keys(&bridge.relayers);
+ let relayers = bridge.relayers.keys();
let mut i = 0;
- while (i < vector::length(relayers)) {
- let relayer = *vector::borrow(relayers, i);
- if (table::contains(&bridge.relayer_public_keys, relayer)) {
- let stored_pk = table::borrow(&bridge.relayer_public_keys, relayer);
+ while (i < relayers.length()) {
+ let relayer = *relayers.borrow(i);
+ if (bridge.relayer_public_keys.contains(relayer)) {
+ let stored_pk = bridge.relayer_public_keys.borrow(relayer);
if (stored_pk == public_key) {
return option::some(relayer)
};
@@ -553,75 +496,106 @@ public fun execute_transfer_for_testing(
amounts: vector,
batch_nonce_mvx: u64,
is_batch_complete: bool,
- treasury: &mut treasury::Treasury,
+ treasury: &mut lkt::Treasury,
clock: &Clock,
ctx: &mut TxContext,
) {
+ pre_execute_transfer_for_testing(bridge, batch_nonce_mvx, clock);
+
+ let len = recipients.length();
+ let mut i = 0;
+ while (i < len) {
+ let success = safe.transfer(
+ &bridge.bridge_cap,
+ *recipients.borrow(i),
+ *amounts.borrow(i),
+ treasury,
+ ctx,
+ );
+ record_transfer_result(bridge, batch_nonce_mvx, success);
+ i = i + 1;
+ };
+
+ finalize_batch(bridge, batch_nonce_mvx, len, is_batch_complete, clock);
+}
+
+// === Package-internal helpers for adapters ===
+
+/// Validates all preconditions and quorum for an execute_transfer call.
+/// Adapters call this at the start of their own execute_transfer entry points.
+public(package) fun pre_execute_transfer(
+ bridge: &mut Bridge,
+ batch_nonce_mvx: u64,
+ recipients: &vector,
+ amounts: &vector,
+ deposit_nonces: &vector,
+ signatures: &vector>,
+ clock: &Clock,
+ ctx: &TxContext,
+) {
+ assert_relayer(bridge, ctx.sender());
+ bridge.pause.assert_not_paused();
+ assert!(!was_batch_executed(bridge, batch_nonce_mvx), EBatchAlreadyExecuted);
+
+ let len = recipients.length();
+ assert!(amounts.length() == len, EInvalidAmountsLength);
+ assert!(deposit_nonces.length() == len, EInvalidDepositNoncesLength);
+
+ validate_quorum(bridge, batch_nonce_mvx, recipients, amounts, signatures, deposit_nonces);
mark_deposits_executed_in_batch_or_abort(bridge, batch_nonce_mvx);
- let now = clock::timestamp_ms(clock);
- if (table::contains(&bridge.execution_timestamps, batch_nonce_mvx)) {
- let t = table::borrow_mut(&mut bridge.execution_timestamps, batch_nonce_mvx);
- *t = now;
+ let now = clock.timestamp_ms();
+ if (bridge.execution_timestamps.contains(batch_nonce_mvx)) {
+ *bridge.execution_timestamps.borrow_mut(batch_nonce_mvx) = now;
} else {
- table::add(&mut bridge.execution_timestamps, batch_nonce_mvx, now);
+ bridge.execution_timestamps.add(batch_nonce_mvx, now);
};
+}
- let mut i = 0;
- while (i < vector::length(&recipients)) {
- let recipient = *vector::borrow(&recipients, i);
- let amount = *vector::borrow(&amounts, i);
-
- let success = safe::transfer(safe, &bridge.bridge_cap, recipient, amount, treasury, ctx);
- if (success) {
- vector::push_back(
- &mut bridge.transfer_statuses,
- shared_structs::deposit_status_executed(),
- );
-
- // Increment successful deposits count
- if (table::contains(&bridge.successful_transfers_by_batch, batch_nonce_mvx)) {
- let current_count = table::borrow_mut(
- &mut bridge.successful_transfers_by_batch,
- batch_nonce_mvx,
- );
- *current_count = *current_count + 1;
- } else {
- table::add(&mut bridge.successful_transfers_by_batch, batch_nonce_mvx, 1);
- };
+/// Records success or failure for one recipient in the current batch.
+public(package) fun record_transfer_result(
+ bridge: &mut Bridge,
+ batch_nonce_mvx: u64,
+ success: bool,
+) {
+ if (success) {
+ bridge.transfer_statuses.push_back(shared_structs::deposit_status_executed());
+ if (bridge.successful_transfers_by_batch.contains(batch_nonce_mvx)) {
+ let count = bridge.successful_transfers_by_batch.borrow_mut(batch_nonce_mvx);
+ *count = *count + 1;
} else {
- vector::push_back(
- &mut bridge.transfer_statuses,
- shared_structs::deposit_status_rejected(),
- );
-
+ bridge.successful_transfers_by_batch.add(batch_nonce_mvx, 1);
};
- i = i + 1;
+ } else {
+ bridge.transfer_statuses.push_back(shared_structs::deposit_status_rejected());
};
+}
+/// Finalizes a batch: records CrossTransferStatus and emits BatchExecuted if complete.
+public(package) fun finalize_batch(
+ bridge: &mut Bridge,
+ batch_nonce_mvx: u64,
+ total_transfers: u64,
+ is_batch_complete: bool,
+ clock: &Clock,
+) {
if (is_batch_complete) {
- vec_set::insert(&mut bridge.executed_batches, batch_nonce_mvx);
+ bridge.executed_batches.insert(batch_nonce_mvx);
let cross_status = shared_structs::create_cross_transfer_status(
bridge.transfer_statuses,
- clock::timestamp_ms(clock),
+ clock.timestamp_ms(),
);
- table::add(&mut bridge.cross_transfer_statuses, batch_nonce_mvx, cross_status);
-
- let total_transfers = vector::length(&recipients);
- bridge.transfer_statuses = vector::empty();
+ bridge.cross_transfer_statuses.add(batch_nonce_mvx, cross_status);
+ bridge.transfer_statuses = vector[];
- let successful_count = if (
- table::contains(&bridge.successful_transfers_by_batch, batch_nonce_mvx)
- ) {
- *table::borrow(&bridge.successful_transfers_by_batch, batch_nonce_mvx)
+ let successful_count = if (bridge.successful_transfers_by_batch.contains(batch_nonce_mvx)) {
+ let count = *bridge.successful_transfers_by_batch.borrow(batch_nonce_mvx);
+ bridge.successful_transfers_by_batch.remove(batch_nonce_mvx);
+ count
} else {
0
};
- if (table::contains(&bridge.successful_transfers_by_batch, batch_nonce_mvx)) {
- table::remove(&mut bridge.successful_transfers_by_batch, batch_nonce_mvx);
- };
-
event::emit(BatchExecuted {
batch_nonce_mvx,
transfers_count: total_transfers,
@@ -630,6 +604,26 @@ public fun execute_transfer_for_testing(
};
}
+/// Exposes the bridge_cap for adapter transfer calls.
+public(package) fun bridge_cap(bridge: &Bridge): &BridgeCap {
+ &bridge.bridge_cap
+}
+
+#[test_only]
+public(package) fun pre_execute_transfer_for_testing(
+ bridge: &mut Bridge,
+ batch_nonce_mvx: u64,
+ clock: &Clock,
+) {
+ mark_deposits_executed_in_batch_or_abort(bridge, batch_nonce_mvx);
+ let now = clock.timestamp_ms();
+ if (bridge.execution_timestamps.contains(batch_nonce_mvx)) {
+ *bridge.execution_timestamps.borrow_mut(batch_nonce_mvx) = now;
+ } else {
+ bridge.execution_timestamps.add(batch_nonce_mvx, now);
+ };
+}
+
// === Upgrade Management for Bridge ===
/// Returns the compatible versions for the bridge
@@ -659,7 +653,7 @@ public fun bridge_pending_version(bridge: &Bridge): Option {
/// Starts the migration process for the bridge
public fun start_bridge_migration(bridge: &mut Bridge, safe: &BridgeSafe, ctx: &TxContext) {
- safe::checkOwnerRole(safe, ctx);
+ safe.checkOwnerRole(ctx);
assert!(bridge.compatible_versions.length() == 1, EMigrationStarted);
let active_version = bridge.compatible_versions.keys()[0];
@@ -674,7 +668,7 @@ public fun start_bridge_migration(bridge: &mut Bridge, safe: &BridgeSafe, ctx: &
/// Aborts the migration process for the bridge
public fun abort_bridge_migration(bridge: &mut Bridge, safe: &BridgeSafe, ctx: &TxContext) {
- safe::checkOwnerRole(safe, ctx);
+ safe.checkOwnerRole(ctx);
assert!(bridge.compatible_versions.length() == 2, EMigrationNotStarted);
let pending_version = max(
@@ -692,7 +686,7 @@ public fun abort_bridge_migration(bridge: &mut Bridge, safe: &BridgeSafe, ctx: &
/// Completes the migration process for the bridge
public fun complete_bridge_migration(bridge: &mut Bridge, safe: &BridgeSafe, ctx: &TxContext) {
- safe::checkOwnerRole(safe, ctx);
+ safe.checkOwnerRole(ctx);
assert!(bridge.compatible_versions.length() == 2, EMigrationNotStarted);
let (version_a, version_b) = (
diff --git a/sources/bridge_roles.move b/sources/bridge_roles.move
index 74ceede..27d58e7 100644
--- a/sources/bridge_roles.move
+++ b/sources/bridge_roles.move
@@ -1,5 +1,5 @@
/// Bridge Roles Module - Access Control and Capabilities
-///
+///
/// This module manages roles, permissions, and capabilities for the bridge system.
module bridge_safe::bridge_roles;
@@ -36,10 +36,7 @@ public(package) fun publish_caps(_w: BridgeWitness, ctx: &mut TxContext): (Bridg
(BridgeCap { id: object::new(ctx) })
}
-public(package) fun transfer_bridge_capability(
- bridge_cap: BridgeCap,
- new_bridge: address,
-) {
+public(package) fun transfer_bridge_capability(bridge_cap: BridgeCap, new_bridge: address) {
assert!(new_bridge != @0x0, 0);
transfer::public_transfer(bridge_cap, new_bridge);
}
@@ -64,10 +61,7 @@ public fun pending_owner(roles: &Roles): Option {
roles.owner_role().pending_address()
}
-public(package) fun new(
- owner: address,
- ctx: &mut TxContext,
-): Roles {
+public(package) fun new(owner: address, ctx: &mut TxContext): Roles {
let mut data = bag::new(ctx);
data.add(OwnerKey {}, two_step_role::new(OwnerRole {}, owner));
Roles {
diff --git a/sources/events.move b/sources/events.move
index 18be9be..d1da96b 100644
--- a/sources/events.move
+++ b/sources/events.move
@@ -1,5 +1,5 @@
/// Events Module - Event Definitions for Bridge Operations
-///
+///
/// This module defines all event structures used across the bridge system
/// for monitoring deposits, admin actions, relayer management, and token operations.
@@ -94,6 +94,18 @@ public struct BatchSettingsUpdated has copy, drop {
batch_settle_limit: u8,
}
+public struct BatchTimeoutUpdated has copy, drop {
+ new_timeout_ms: u64,
+}
+
+public struct BatchSettleTimeoutUpdated has copy, drop {
+ new_settle_timeout_ms: u64,
+}
+
+public struct BatchSizeUpdated has copy, drop {
+ new_batch_size: u16,
+}
+
public fun emit_deposit(
_batch_id: u64,
_deposit_nonce: u64,
@@ -214,3 +226,15 @@ public(package) fun emit_batch_settings_updated(
batch_settle_limit,
});
}
+
+public(package) fun emit_batch_timeout_updated(new_timeout_ms: u64) {
+ event::emit(BatchTimeoutUpdated { new_timeout_ms });
+}
+
+public(package) fun emit_batch_settle_timeout_updated(new_settle_timeout_ms: u64) {
+ event::emit(BatchSettleTimeoutUpdated { new_settle_timeout_ms });
+}
+
+public(package) fun emit_batch_size_updated(new_batch_size: u16) {
+ event::emit(BatchSizeUpdated { new_batch_size });
+}
diff --git a/sources/mint_burn_adapters/xmn_mint_cap_adapter.move b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move
new file mode 100644
index 0000000..41849e6
--- /dev/null
+++ b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move
@@ -0,0 +1,230 @@
+module bridge_safe::xmn_mint_cap_adapter;
+
+use bridge_safe::bridge::Bridge;
+use bridge_safe::bridge_roles::BridgeCap;
+use bridge_safe::events;
+use bridge_safe::safe::BridgeSafe;
+use bridge_safe::utils;
+use sui::clock::Clock;
+use sui::coin::Coin;
+use sui::deny_list::DenyList;
+use sui::dynamic_object_field as dof;
+use treasury::treasury::{MintCap, Treasury as XmnTreasury};
+
+public struct CapKey has copy, drop, store {
+ token_type: vector,
+}
+
+const EMintBurnCapNotFound: u64 = 20;
+const EMintBurnCapAlreadyRegistered: u64 = 21;
+
+// === Public API ===
+
+public fun deposit(
+ safe: &mut BridgeSafe,
+ coin_in: Coin,
+ recipient: vector,
+ clock: &Clock,
+ xmn_treasury: &mut XmnTreasury,
+ deny_list: &DenyList,
+ ctx: &mut TxContext,
+) {
+ safe.assert_is_compatible();
+ assert!(has_cap(safe.uid()), EMintBurnCapNotFound);
+
+ let (key, amount, batch_nonce, dep_nonce) = safe.deposit_validate_and_record(
+ &coin_in,
+ recipient,
+ true,
+ clock,
+ ctx,
+ );
+
+ burn(safe.uid(), xmn_treasury, deny_list, coin_in, ctx);
+
+ events::emit_deposit_v1(
+ batch_nonce,
+ dep_nonce,
+ ctx.sender(),
+ recipient,
+ amount,
+ key,
+ );
+}
+
+public fun execute_transfer(
+ bridge: &mut Bridge,
+ safe: &mut BridgeSafe,
+ recipients: vector,
+ amounts: vector,
+ deposit_nonces: vector,
+ batch_nonce_mvx: u64,
+ signatures: vector>,
+ is_batch_complete: bool,
+ xmn_treasury: &mut XmnTreasury,
+ deny_list: &DenyList,
+ clock: &Clock,
+ ctx: &mut TxContext,
+) {
+ bridge.assert_bridge_is_compatible();
+ safe.assert_is_compatible();
+ bridge.pre_execute_transfer(
+ batch_nonce_mvx,
+ &recipients,
+ &amounts,
+ &deposit_nonces,
+ &signatures,
+ clock,
+ ctx,
+ );
+
+ let len = recipients.length();
+ let mut i = 0;
+ while (i < len) {
+ let success = transfer(
+ safe,
+ bridge.bridge_cap(),
+ *recipients.borrow(i),
+ *amounts.borrow(i),
+ xmn_treasury,
+ deny_list,
+ ctx,
+ );
+ bridge.record_transfer_result(batch_nonce_mvx, success);
+ i = i + 1;
+ };
+
+ bridge.finalize_batch(batch_nonce_mvx, len, is_batch_complete, clock);
+}
+
+// === Admin Management ===
+
+public fun whitelist_token(
+ safe: &mut BridgeSafe,
+ minimum_amount: u64,
+ maximum_amount: u64,
+ cap: MintCap,
+ treasury_id: ID,
+ ctx: &TxContext,
+) {
+ safe.assert_is_compatible();
+ assert!(!has_cap(safe.uid()), EMintBurnCapAlreadyRegistered);
+ safe.whitelist_token_internal(
+ minimum_amount,
+ maximum_amount,
+ false,
+ option::some(treasury_id),
+ true,
+ false,
+ ctx,
+ );
+ register(safe.uid_mut(), cap);
+}
+
+/// Remove a mint-burn token from the whitelist and deregister its MintCap in one atomic operation.
+#[allow(lint(self_transfer))]
+public fun remove_token_from_whitelist(safe: &mut BridgeSafe, ctx: &mut TxContext) {
+ safe.assert_is_compatible();
+ safe.checkOwnerRole(ctx);
+ assert!(has_cap(safe.uid()), EMintBurnCapNotFound);
+ deregister(safe.uid_mut(), ctx.sender());
+ let key = utils::type_name_bytes();
+ safe.unwhitelist_token(key);
+}
+
+// === Internal helpers ===
+
+public(package) fun transfer(
+ safe: &mut BridgeSafe,
+ _bridge_cap: &BridgeCap,
+ receiver: address,
+ amount: u64,
+ xmn_treasury: &mut XmnTreasury,
+ deny_list: &DenyList,
+ ctx: &mut TxContext,
+): bool {
+ if (!safe.has_token_config()) { return false };
+ if (!safe.get_token_is_mint_burn()) { return false };
+ if (safe.get_stored_coin_balance() < amount) { return false };
+ if (!has_cap(safe.uid())) { return false };
+
+ mint(safe.uid(), xmn_treasury, deny_list, amount, receiver, ctx);
+ safe.subtract_token_balance(amount);
+
+ true
+}
+
+fun cap_key(): CapKey {
+ CapKey { token_type: utils::type_name_bytes() }
+}
+
+public(package) fun register(id: &mut UID, cap: MintCap) {
+ dof::add(id, cap_key(), cap);
+}
+
+#[allow(lint(self_transfer))]
+public(package) fun deregister(id: &mut UID, recipient: address) {
+ let cap = dof::remove>(id, cap_key());
+ transfer::public_transfer(cap, recipient);
+}
+
+public(package) fun has_cap(id: &UID): bool {
+ dof::exists_with_type>(id, cap_key())
+}
+
+public(package) fun burn(
+ id: &UID,
+ xmn_treasury: &mut XmnTreasury,
+ deny_list: &DenyList,
+ coin_in: Coin,
+ ctx: &TxContext,
+) {
+ let cap = dof::borrow(id, cap_key());
+ xmn_treasury.burn(cap, deny_list, coin_in, ctx);
+}
+
+public(package) fun mint(
+ id: &UID,
+ xmn_treasury: &mut XmnTreasury,
+ deny_list: &DenyList,
+ amount: u64,
+ receiver: address,
+ ctx: &mut TxContext,
+) {
+ let cap = dof::borrow(id, cap_key());
+ xmn_treasury.mint(cap, deny_list, amount, receiver, ctx);
+}
+
+#[test_only]
+public fun execute_transfer_for_testing(
+ bridge: &mut Bridge,
+ safe: &mut BridgeSafe,
+ recipients: vector,
+ amounts: vector,
+ batch_nonce_mvx: u64,
+ is_batch_complete: bool,
+ xmn_treasury: &mut XmnTreasury,
+ deny_list: &DenyList,
+ clock: &Clock,
+ ctx: &mut TxContext,
+) {
+ bridge.pre_execute_transfer_for_testing(batch_nonce_mvx, clock);
+
+ let len = recipients.length();
+ let mut i = 0;
+ while (i < len) {
+ let success = transfer(
+ safe,
+ bridge.bridge_cap(),
+ *recipients.borrow(i),
+ *amounts.borrow(i),
+ xmn_treasury,
+ deny_list,
+ ctx,
+ );
+ bridge.record_transfer_result(batch_nonce_mvx, success);
+ i = i + 1;
+ };
+
+ bridge.finalize_batch(batch_nonce_mvx, len, is_batch_complete, clock);
+}
diff --git a/sources/pausable.move b/sources/pausable.move
index 97fcf98..aa94b3f 100644
--- a/sources/pausable.move
+++ b/sources/pausable.move
@@ -1,5 +1,5 @@
/// Pausable Module - Emergency Stop Functionality
-///
+///
/// This module provides pausable functionality for emergency stops.
module bridge_safe::pausable;
@@ -17,14 +17,14 @@ public fun new(): Pause {
Pause { paused: false }
}
-public fun pause(p: &mut Pause) {
+public(package) fun pause(p: &mut Pause) {
if (!p.paused) {
p.paused = true;
events::emit_pause(true);
}
}
-public fun unpause(p: &mut Pause) {
+public(package) fun unpause(p: &mut Pause) {
if (p.paused) {
p.paused = false;
events::emit_pause(false);
diff --git a/sources/safe.move b/sources/safe.move
index ddfb71b..1f0fb0f 100644
--- a/sources/safe.move
+++ b/sources/safe.move
@@ -1,4 +1,4 @@
-/// Safe Module - Token Management and Batch Processing
+/// Safe Module - Token Management ani Batch Processing
///
/// This module manages token deposits, batching, and secure transfers.
/// It handles whitelisting, token limits, and coordinates with the bridge module.
@@ -12,11 +12,11 @@ use bridge_safe::pausable::{Self, Pause};
use bridge_safe::upgrade_service_bridge;
use bridge_safe::utils;
use locked_token::bridge_token::BRIDGE_TOKEN;
-use locked_token::treasury;
-use shared_structs::shared_structs::{Self, TokenConfig, Batch, Deposit};
+use locked_token::treasury::{Self as lkt};
+use bridge_safe::shared_structs::{Self, TokenConfig, Batch, Deposit};
use std::u64::{min, max};
use sui::bag::{Self, Bag};
-use sui::clock::{Self, Clock};
+use sui::clock::Clock;
use sui::coin::{Self, Coin};
use sui::event;
use sui::table::{Self, Table};
@@ -56,6 +56,8 @@ const EInvalidTokenLimits: u64 = 15;
const EMigrationStarted: u64 = 16;
const EMigrationNotStarted: u64 = 17;
const ENotPendingVersion: u64 = 18;
+const ENotNativeToken: u64 = 19;
+const EIncompatibleTokenFlags: u64 = 22;
const MAX_U64: u64 = 18446744073709551615;
const DEFAULT_BATCH_TIMEOUT_MS: u64 = 5 * 1000; // 5 seconds
@@ -75,17 +77,28 @@ public struct BridgeSafe has key {
batches: Table,
batch_deposits: Table>,
coin_storage: Bag,
- from_coin_cap: treasury::FromCoinCap,
+ from_coin_cap: lkt::FromCoinCap,
compatible_versions: VecSet,
}
public struct SAFE has drop {}
+fun init(witness: SAFE, ctx: &mut TxContext) {
+ let (upgrade_service, _witness) = upgrade_service_bridge::new(
+ witness,
+ ctx.sender(),
+ ctx,
+ );
+
+ // Share the upgrade service object
+ transfer::public_share_object(upgrade_service);
+}
+
#[allow(lint(self_transfer))]
-public fun initialize(from_coin_cap: treasury::FromCoinCap, ctx: &mut TxContext) {
- let deployer = tx_context::sender(ctx);
+public fun initialize(from_coin_cap: lkt::FromCoinCap, ctx: &mut TxContext) {
+ let deployer = ctx.sender();
let w = bridge_roles::grant_witness();
- let (bridge_cap) = bridge_roles::publish_caps(w, ctx);
+ let (bridge_cap) = w.publish_caps(ctx);
let safe = BridgeSafe {
id: object::new(ctx),
@@ -109,251 +122,322 @@ public fun initialize(from_coin_cap: treasury::FromCoinCap, ctx: &
transfer::share_object(safe);
}
-fun init(witness: SAFE, ctx: &mut TxContext) {
- let (upgrade_service, _witness) = upgrade_service_bridge::new(
- witness,
- ctx.sender(),
+/// Deposit function for native tokens: coin is stored in the safe's coin_storage bag.
+public fun deposit(
+ safe: &mut BridgeSafe,
+ coin_in: Coin,
+ recipient: vector,
+ clock: &Clock,
+ ctx: &mut TxContext,
+) {
+ assert_is_compatible(safe);
+ let (key, amount, batch_nonce, dep_nonce) = deposit_validate_and_record(
+ safe,
+ &coin_in,
+ recipient,
+ false,
+ clock,
ctx,
);
- // Share the upgrade service object
- transfer::public_share_object(upgrade_service);
-}
+ if (safe.coin_storage.contains(key)) {
+ safe.coin_storage.borrow_mut, Coin>(key).join(coin_in);
+ } else {
+ safe.coin_storage.add(key, coin_in);
+ };
-fun borrow_token_cfg_mut(safe: &mut BridgeSafe, key: vector): &mut TokenConfig {
- table::borrow_mut(&mut safe.token_cfg, key)
+ events::emit_deposit_v1(
+ batch_nonce,
+ dep_nonce,
+ ctx.sender(),
+ recipient,
+ amount,
+ key,
+ );
}
-public fun whitelist_token(
+/// Transfer function for native tokens: splits coin from the safe's bag and sends to receiver.
+/// Only the bridge role can call this function.
+public(package) fun transfer(
safe: &mut BridgeSafe,
- minimum_amount: u64,
- maximum_amount: u64,
- is_native: bool,
- is_locked: bool,
+ _bridge_cap: &bridge_roles::BridgeCap,
+ receiver: address,
+ amount: u64,
+ treasury: &mut lkt::Treasury,
ctx: &mut TxContext,
-) {
- safe.roles.owner_role().assert_sender_is_active_role(ctx);
-
+): bool {
let key = utils::type_name_bytes();
- let exists = table::contains(&safe.token_cfg, key);
- assert!(minimum_amount > 0, EZeroAmount);
- assert!(minimum_amount <= maximum_amount, EInvalidTokenLimits);
+ if (!safe.token_cfg.contains(key)) {
+ return false
+ };
- if (exists) {
- let cfg = table::borrow(&safe.token_cfg, key);
- let is_currently_whitelisted = shared_structs::token_config_whitelisted(cfg);
- assert!(!is_currently_whitelisted, ETokenAlreadyExists);
-
- let cfg_mut = borrow_token_cfg_mut(safe, key);
- shared_structs::set_token_config_whitelisted(cfg_mut, true);
- shared_structs::set_token_config_is_native(cfg_mut, is_native);
- shared_structs::set_token_config_min_limit(cfg_mut, minimum_amount);
- shared_structs::set_token_config_max_limit(cfg_mut, maximum_amount);
- shared_structs::set_token_config_is_locked(cfg_mut, is_locked);
+ let (is_mint_burn, current_balance, is_locked) = {
+ let cfg_ref = safe.token_cfg.borrow(key);
+ (cfg_ref.token_config_is_mint_burn(), cfg_ref.token_config_total_balance(), cfg_ref.get_token_config_is_locked())
+ };
+
+ if (is_mint_burn) {
+ return false
+ };
+
+ if (current_balance < amount) {
+ return false
+ };
+
+ if (!safe.coin_storage.contains(key)) {
+ return false
+ };
+
+ if (!is_locked) {
+ let stored_coin = safe.coin_storage.borrow_mut, Coin>(key);
+ let coin_value = stored_coin.value();
+ if (coin_value < amount) {
+ return false
+ };
+
+ let coin_to_transfer = stored_coin.split(amount, ctx);
+
+ if (stored_coin.value() == 0) {
+ let empty_coin = safe.coin_storage.remove, Coin>(key);
+ empty_coin.destroy_zero();
+ };
+
+ transfer::public_transfer(coin_to_transfer, receiver);
+
} else {
- let cfg = shared_structs::create_token_config(
- true,
- is_native,
- minimum_amount,
- maximum_amount,
- is_locked,
+ let stored_bt_coin = safe.coin_storage.borrow_mut, Coin>(key);
+
+ let coin_value = stored_bt_coin.value();
+ if (coin_value < amount) {
+ return false
+ };
+
+ let coin_bt = stored_bt_coin.split(amount, ctx);
+ if (stored_bt_coin.value() == 0) {
+ let empty_coin = safe.coin_storage.remove, Coin>(
+ key,
+ );
+ empty_coin.destroy_zero();
+ };
+ lkt::transfer_from_coin(
+ treasury,
+ receiver,
+ &safe.from_coin_cap,
+ coin_bt,
+ ctx,
);
- table::add(&mut safe.token_cfg, key, cfg);
};
- events::emit_token_whitelisted(
- key,
- minimum_amount,
- maximum_amount,
- is_native,
- false,
- is_locked,
- );
-}
-
-public fun remove_token_from_whitelist(safe: &mut BridgeSafe, ctx: &mut TxContext) {
- safe.roles.owner_role().assert_sender_is_active_role(ctx);
- let key = utils::type_name_bytes();
- let cfg = borrow_token_cfg_mut(safe, key);
- shared_structs::set_token_config_whitelisted(cfg, false);
+ let cfg_mut = borrow_token_cfg_mut(safe, key);
+ cfg_mut.subtract_from_token_config_total_balance(amount);
- events::emit_token_removed_from_whitelist(key);
+ true
}
public fun is_token_whitelisted(safe: &BridgeSafe): bool {
let key = utils::type_name_bytes();
- if (!table::contains(&safe.token_cfg, key)) {
+ if (!safe.token_cfg.contains(key)) {
return false
};
- let cfg = table::borrow(&safe.token_cfg, key);
- shared_structs::token_config_whitelisted(cfg)
+ let cfg = safe.token_cfg.borrow(key);
+ cfg.token_config_whitelisted()
}
-public fun set_batch_timeout_ms(safe: &mut BridgeSafe, new_timeout_ms: u64, ctx: &mut TxContext) {
- safe.roles.owner_role().assert_sender_is_active_role(ctx);
- assert!(new_timeout_ms <= safe.batch_settle_timeout_ms, EBatchBlockLimitExceedsSettle);
- safe.batch_timeout_ms = new_timeout_ms;
+public fun get_token_min_limit(safe: &BridgeSafe): u64 {
+ let key = utils::type_name_bytes();
+ let cfg = safe.token_cfg.borrow(key);
+ cfg.token_config_min_limit()
}
-public fun set_batch_settle_timeout_ms(
- safe: &mut BridgeSafe,
- new_timeout_ms: u64,
- clock: &Clock,
- ctx: &mut TxContext,
-) {
- pausable::assert_paused(&safe.pause);
- safe.roles.owner_role().assert_sender_is_active_role(ctx);
- assert!(new_timeout_ms >= safe.batch_timeout_ms, EBatchSettleLimitBelowBlock);
- assert!(!is_any_batch_in_progress_internal(safe, clock), EBatchInProgress);
- safe.batch_settle_timeout_ms = new_timeout_ms;
+public fun get_token_max_limit(safe: &BridgeSafe): u64 {
+ let key = utils::type_name_bytes();
+ let cfg = safe.token_cfg.borrow(key);
+ cfg.token_config_max_limit()
}
-public fun set_batch_size(safe: &mut BridgeSafe, new_size: u16, ctx: &mut TxContext) {
- safe.roles.owner_role().assert_sender_is_active_role(ctx);
- assert!(new_size > 0, EBatchSizeZero);
- assert!(new_size <= 100, EBatchSizeTooLarge);
- safe.batch_size = new_size;
+public fun get_token_is_mint_burn(safe: &BridgeSafe): bool {
+ let key = utils::type_name_bytes();
+ let cfg = safe.token_cfg.borrow(key);
+ cfg.token_config_is_mint_burn()
}
-public fun set_token_min_limit(safe: &mut BridgeSafe, amount: u64, ctx: &mut TxContext) {
- safe.roles.owner_role().assert_sender_is_active_role(ctx);
-
+public fun get_token_is_native(safe: &BridgeSafe): bool {
let key = utils::type_name_bytes();
- let cfg = borrow_token_cfg_mut(safe, key);
- let old_max = shared_structs::token_config_max_limit(cfg);
+ let cfg = safe.token_cfg.borrow(key);
+ cfg.token_config_is_native()
+}
- assert!(amount > 0, EZeroAmount);
- assert!(amount <= old_max, EInvalidTokenLimits);
+public fun get_batch(safe: &BridgeSafe, batch_nonce: u64, clock: &Clock): (Batch, bool) {
+ assert!(batch_nonce > 0, EBatchNotFound);
+ let batch_index = batch_nonce - 1;
- shared_structs::set_token_config_min_limit(cfg, amount);
+ if (!safe.batches.contains(batch_index)) {
+ let empty_batch = shared_structs::create_batch(0, 0);
+ return (empty_batch, false)
+ };
- events::emit_token_limits_updated(key, amount, old_max);
+ let batch = *safe.batches.borrow(batch_index);
+ let is_final = is_batch_final_internal(safe, &batch, clock);
+ (batch, is_final)
}
-public fun get_token_min_limit(safe: &BridgeSafe): u64 {
- let key = utils::type_name_bytes();
- let cfg = table::borrow(&safe.token_cfg, key);
- shared_structs::token_config_min_limit(cfg)
-}
+public fun get_deposits(
+ safe: &BridgeSafe,
+ batch_nonce: u64,
+ clock: &Clock,
+): (vector, bool) {
+ assert!(batch_nonce > 0, EBatchNotFound);
+ let batch_index = batch_nonce - 1;
+ let deposits = if (safe.batch_deposits.contains(batch_index)) {
+ *safe.batch_deposits.borrow(batch_index)
+ } else {
+ vector[]
+ };
+ if (!safe.batches.contains(batch_index)) {
+ return (deposits, false)
+ };
-public(package) fun roles_mut(safe: &mut BridgeSafe): &mut Roles {
- &mut safe.roles
+ let batch = safe.batches.borrow(batch_index);
+ let is_final = is_batch_final_internal(safe, batch, clock);
+ (deposits, is_final)
}
-public fun set_token_max_limit(safe: &mut BridgeSafe, amount: u64, ctx: &mut TxContext) {
- safe.roles.owner_role().assert_sender_is_active_role(ctx);
+public fun is_any_batch_in_progress(safe: &BridgeSafe, clock: &Clock): bool {
+ is_any_batch_in_progress_internal(safe, clock)
+}
- let key = utils::type_name_bytes();
- let cfg = borrow_token_cfg_mut(safe, key);
- let old_min = shared_structs::token_config_min_limit(cfg);
+public fun get_bridge_addr(safe: &BridgeSafe): address {
+ safe.bridge_addr
+}
- assert!(amount >= old_min, EInvalidTokenLimits);
- shared_structs::set_token_config_max_limit(cfg, amount);
+/// Get the current owner address
+public fun get_owner(safe: &BridgeSafe): address {
+ safe.roles.owner()
+}
- events::emit_token_limits_updated(key, old_min, amount);
+/// Get the pending owner address (if any)
+public fun get_pending_owner(safe: &BridgeSafe): Option {
+ safe.roles.pending_owner()
}
-public fun get_token_max_limit(safe: &BridgeSafe): u64 {
- let key = utils::type_name_bytes();
- let cfg = table::borrow(&safe.token_cfg, key);
- shared_structs::token_config_max_limit(cfg)
+public fun get_batch_size(safe: &BridgeSafe): u16 {
+ safe.batch_size
}
-public fun get_token_is_mint_burn(safe: &BridgeSafe): bool {
- let key = utils::type_name_bytes();
- let cfg = table::borrow(&safe.token_cfg, key);
- shared_structs::token_config_is_mint_burn(cfg)
+public fun get_batch_timeout_ms(safe: &BridgeSafe): u64 {
+ safe.batch_timeout_ms
}
-public fun get_token_is_native(safe: &BridgeSafe): bool {
- let key = utils::type_name_bytes();
- let cfg = table::borrow(&safe.token_cfg, key);
- shared_structs::token_config_is_native(cfg)
+public fun get_batch_settle_timeout_ms(safe: &BridgeSafe): u64 {
+ safe.batch_settle_timeout_ms
}
-public fun set_token_is_native(safe: &mut BridgeSafe, is_native: bool, ctx: &mut TxContext) {
- safe.roles.owner_role().assert_sender_is_active_role(ctx);
+public fun get_batches_count(safe: &BridgeSafe): u64 {
+ safe.batches_count
+}
- let key = utils::type_name_bytes();
- let cfg = borrow_token_cfg_mut(safe, key);
- shared_structs::set_token_config_is_native(cfg, is_native);
+public fun get_deposits_count(safe: &BridgeSafe): u64 {
+ safe.deposits_count
+}
- events::emit_token_is_native_updated(key, is_native);
+public fun get_pause(safe: &BridgeSafe): &Pause {
+ &safe.pause
}
-public fun set_token_is_locked(safe: &mut BridgeSafe, is_locked: bool, ctx: &mut TxContext) {
- safe.roles.owner_role().assert_sender_is_active_role(ctx);
+public(package) fun get_pause_mut(safe: &mut BridgeSafe): &mut Pause {
+ &mut safe.pause
+}
- let key = utils::type_name_bytes();
- let cfg = borrow_token_cfg_mut(safe, key);
- shared_structs::set_token_config_is_locked(cfg, is_locked);
+public fun get_batch_nonce(batch: &Batch): u64 {
+ batch.batch_nonce()
+}
- events::emit_token_is_locked_updated(key, is_locked);
+public fun get_batch_deposits_count(batch: &Batch): u16 {
+ batch.batch_deposits_count()
}
-public fun set_token_is_mint_burn(
- safe: &mut BridgeSafe,
- is_mint_burn: bool,
- ctx: &mut TxContext,
-) {
- safe.roles.owner_role().assert_sender_is_active_role(ctx);
+public fun get_stored_coin_balance(safe: &mut BridgeSafe): u64 {
+ let key = utils::type_name_bytes();
+ if (!safe.token_cfg.contains(key)) {
+ return 0
+ };
+ let cfg_ref = safe.token_cfg.borrow(key);
+ cfg_ref.token_config_total_balance()
+}
+public fun get_coin_storage_balance(safe: &BridgeSafe): u64 {
let key = utils::type_name_bytes();
- let cfg = borrow_token_cfg_mut(safe, key);
- shared_structs::set_token_config_is_mint_burn(cfg, is_mint_burn);
+ if (!safe.coin_storage.contains(key)) {
+ return 0
+ };
+ let stored_coin = safe.coin_storage.borrow, Coin>(key);
+ stored_coin.value()
+}
- events::emit_token_is_mint_burn_updated(key, is_mint_burn);
+// === Admin Management ===
+
+public fun pause_contract(safe: &mut BridgeSafe, ctx: &mut TxContext) {
+ assert_is_compatible(safe);
+ safe.roles.owner_role().assert_sender_is_active_role(ctx);
+ safe.pause.pause();
}
-public fun set_bridge_addr(safe: &mut BridgeSafe, new_bridge_addr: address, ctx: &TxContext) {
+public fun unpause_contract(safe: &mut BridgeSafe, ctx: &mut TxContext) {
+ assert_is_compatible(safe);
safe.roles.owner_role().assert_sender_is_active_role(ctx);
+ safe.pause.unpause();
+}
- let previous_bridge = safe.bridge_addr;
- safe.bridge_addr = new_bridge_addr;
- events::emit_bridge_transferred(previous_bridge, new_bridge_addr);
+public fun transfer_ownership(safe: &mut BridgeSafe, new_owner: address, ctx: &TxContext) {
+ assert_is_compatible(safe);
+ safe.roles_mut().owner_role_mut().begin_role_transfer(new_owner, ctx)
+}
+
+public fun accept_ownership(safe: &mut BridgeSafe, ctx: &TxContext) {
+ assert_is_compatible(safe);
+ safe.roles_mut().owner_role_mut().accept_role(ctx)
}
public fun init_supply(safe: &mut BridgeSafe, coin_in: Coin, ctx: &mut TxContext) {
+ assert_is_compatible(safe);
safe.roles.owner_role().assert_sender_is_active_role(ctx);
let key = utils::type_name_bytes();
- assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted);
- let cfg_ref = table::borrow(&safe.token_cfg, key);
- assert!(shared_structs::token_config_whitelisted(cfg_ref), ETokenNotWhitelisted);
-
- assert!(shared_structs::token_config_is_native(cfg_ref), EInsufficientBalance);
+ assert_token_is_whitelisted(safe, key);
+ let cfg_ref = safe.token_cfg.borrow(key);
+ assert!(cfg_ref.token_config_is_native(), ENotNativeToken);
let amount = coin::value(&coin_in);
let cfg_mut = borrow_token_cfg_mut(safe, key);
- shared_structs::add_to_token_config_total_balance(cfg_mut, amount);
+ cfg_mut.add_to_token_config_total_balance(amount);
- if (bag::contains(&safe.coin_storage, key)) {
- let existing_coin = bag::borrow_mut, Coin>(&mut safe.coin_storage, key);
- coin::join(existing_coin, coin_in);
+ if (safe.coin_storage.contains(key)) {
+ let existing_coin = safe.coin_storage.borrow_mut, Coin>(key);
+ existing_coin.join(coin_in);
} else {
- bag::add(&mut safe.coin_storage, key, coin_in);
+ safe.coin_storage.add(key, coin_in);
};
}
#[allow(lint(self_transfer))]
public fun sync_supply(safe: &mut BridgeSafe, mut coin_in: Coin, ctx: &mut TxContext) {
+ assert_is_compatible(safe);
safe.roles.owner_role().assert_sender_is_active_role(ctx);
let key = utils::type_name_bytes();
- assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted);
- let cfg_ref = table::borrow(&safe.token_cfg, key);
- assert!(shared_structs::token_config_whitelisted(cfg_ref), ETokenNotWhitelisted);
- assert!(shared_structs::token_config_is_native(cfg_ref), EInsufficientBalance);
+ assert_token_is_whitelisted(safe, key);
+ let cfg_ref = safe.token_cfg.borrow(key);
+ assert!(cfg_ref.token_config_is_native(), ENotNativeToken);
- let expected_balance = shared_structs::token_config_total_balance(cfg_ref);
+ let expected_balance = cfg_ref.token_config_total_balance();
- let actual_balance = if (bag::contains(&safe.coin_storage, key)) {
- let stored_coin = bag::borrow, Coin>(&safe.coin_storage, key);
- coin::value(stored_coin)
+ let actual_balance = if (safe.coin_storage.contains(key)) {
+ let stored_coin = safe.coin_storage.borrow, Coin>(key);
+ stored_coin.value()
} else {
0
};
@@ -361,324 +445,174 @@ public fun sync_supply(safe: &mut BridgeSafe, mut coin_in: Coin, ctx: &mut
assert!(expected_balance > actual_balance, EInsufficientBalance);
let deficit = expected_balance - actual_balance;
- assert!(coin::value(&coin_in) >= deficit, EInsufficientBalance);
+ assert!(coin_in.value() >= deficit, EInsufficientBalance);
- let top_up_coin = coin::split(&mut coin_in, deficit, ctx);
+ let top_up_coin = coin_in.split(deficit, ctx);
- if (bag::contains(&safe.coin_storage, key)) {
- let existing_coin = bag::borrow_mut, Coin>(&mut safe.coin_storage, key);
- coin::join(existing_coin, top_up_coin);
+ if (safe.coin_storage.contains(key)) {
+ let existing_coin = safe.coin_storage.borrow_mut, Coin>(key);
+ existing_coin.join(top_up_coin);
} else {
- bag::add(&mut safe.coin_storage, key, top_up_coin);
+ safe.coin_storage.add(key, top_up_coin);
};
- if (coin::value(&coin_in) == 0) {
- coin::destroy_zero(coin_in);
+ if (coin_in.value() == 0) {
+ coin_in.destroy_zero();
} else {
- transfer::public_transfer(coin_in, tx_context::sender(ctx));
+ transfer::public_transfer(coin_in, ctx.sender());
};
}
-/// Deposit function: Users send coins FROM their wallet TO the bridge safe contract
-/// The coins are stored in the contract's coin_storage for later transfer
-public fun deposit(
+public fun whitelist_token(
safe: &mut BridgeSafe,
- coin_in: Coin,
- recipient: vector,
- clock: &Clock,
+ minimum_amount: u64,
+ maximum_amount: u64,
+ is_locked: bool,
ctx: &mut TxContext,
) {
- pausable::assert_not_paused(&safe.pause);
-
- assert!(vector::length(&recipient) == 32, EInvalidRecipient);
-
- let key = utils::type_name_bytes();
- let cfg_ref = table::borrow(&safe.token_cfg, key);
- assert!(shared_structs::token_config_whitelisted(cfg_ref), ETokenNotWhitelisted);
-
- let amount = coin::value(&coin_in);
- assert!(amount > 0, EZeroAmount);
- assert!(amount >= shared_structs::token_config_min_limit(cfg_ref), EAmountBelowMinimum);
- assert!(amount <= shared_structs::token_config_max_limit(cfg_ref), EAmountAboveMaximum);
-
- if (should_create_new_batch_internal(safe, clock)) {
- create_new_batch_internal(safe, clock, ctx);
- };
- let batch_index = safe.batches_count - 1;
- let batch = table::borrow_mut(&mut safe.batches, batch_index);
-
- assert!(safe.deposits_count < MAX_U64, EOverflow);
- let dep_nonce = safe.deposits_count + 1;
- let dep = shared_structs::create_deposit(
- dep_nonce,
- key,
- amount,
- tx_context::sender(ctx),
- recipient,
- );
- if (!table::contains(&safe.batch_deposits, batch_index)) {
- table::add(&mut safe.batch_deposits, batch_index, vector::empty());
- };
- let vec_ref = table::borrow_mut(&mut safe.batch_deposits, batch_index);
- vector::push_back(vec_ref, dep);
-
- safe.deposits_count = dep_nonce;
- shared_structs::increment_batch_deposits(batch);
- shared_structs::set_batch_last_updated_timestamp_ms(batch, clock::timestamp_ms(clock));
-
- let batch_nonce = shared_structs::batch_nonce(batch);
-
- let cfg = borrow_token_cfg_mut(safe, key);
- shared_structs::add_to_token_config_total_balance(cfg, amount);
-
- if (bag::contains(&safe.coin_storage, key)) {
- let existing_coin = bag::borrow_mut, Coin>(&mut safe.coin_storage, key);
- coin::join(existing_coin, coin_in);
- } else {
- bag::add(&mut safe.coin_storage, key, coin_in);
- };
-
- events::emit_deposit_v1(
- batch_nonce,
- dep_nonce,
- tx_context::sender(ctx),
- recipient,
- amount,
- key,
+ assert_is_compatible(safe);
+ whitelist_token_internal(
+ safe,
+ minimum_amount,
+ maximum_amount,
+ true,
+ option::none(),
+ false,
+ is_locked,
+ ctx,
);
}
-public(package) fun checkOwnerRole(safe: &BridgeSafe, ctx: &TxContext) {
+/// Removes a native (non-mint-burn) token from the whitelist.
+/// For mint-burn tokens, use the adapter's remove_token_from_whitelist instead.
+public fun remove_token_from_whitelist(safe: &mut BridgeSafe, ctx: &mut TxContext) {
+ assert_is_compatible(safe);
safe.roles.owner_role().assert_sender_is_active_role(ctx);
+ let key = utils::type_name_bytes();
+ let cfg_ref = safe.token_cfg.borrow(key);
+ assert!(!cfg_ref.token_config_is_mint_burn(), EIncompatibleTokenFlags);
+ unwhitelist_token(safe, key);
}
-public fun get_batch(safe: &BridgeSafe, batch_nonce: u64, clock: &Clock): (Batch, bool) {
- assert!(batch_nonce > 0, EBatchNotFound);
- let batch_index = batch_nonce - 1;
-
- if (!table::contains(&safe.batches, batch_index)) {
- let empty_batch = shared_structs::create_batch(0, 0);
- return (empty_batch, false)
- };
-
- let batch = *table::borrow(&safe.batches, batch_index);
- let is_final = is_batch_final_internal(safe, &batch, clock);
- (batch, is_final)
-}
-
-public fun get_deposits(
- safe: &BridgeSafe,
- batch_nonce: u64,
- clock: &Clock,
-): (vector, bool) {
- assert!(batch_nonce > 0, EBatchNotFound);
- let batch_index = batch_nonce - 1;
- let deposits = if (table::contains(&safe.batch_deposits, batch_index)) {
- *table::borrow(&safe.batch_deposits, batch_index)
- } else {
- vector::empty()
- };
- if (!table::contains(&safe.batches, batch_index)) {
- return (deposits, false)
- };
-
- let batch = table::borrow(&safe.batches, batch_index);
- let is_final = is_batch_final_internal(safe, batch, clock);
- (deposits, is_final)
-}
-
-public fun is_any_batch_in_progress(safe: &BridgeSafe, clock: &Clock): bool {
- is_any_batch_in_progress_internal(safe, clock)
-}
-
-fun create_new_batch_internal(safe: &mut BridgeSafe, clock: &Clock, _ctx: &mut TxContext) {
- assert!(safe.batches_count < MAX_U64, EOverflow);
- let nonce = safe.batches_count + 1;
- let batch = shared_structs::create_batch(nonce, clock::timestamp_ms(clock));
- table::add(&mut safe.batches, safe.batches_count, batch);
- safe.batches_count = nonce;
-}
-
-fun should_create_new_batch_internal(safe: &BridgeSafe, clock: &Clock): bool {
- if (safe.batches_count == 0) { return true };
- let last_index = safe.batches_count - 1;
- let batch = table::borrow(&safe.batches, last_index);
- is_batch_progress_over_internal(safe, shared_structs::batch_deposits_count(batch), shared_structs::batch_timestamp_ms(batch), clock) || (shared_structs::batch_deposits_count(batch) >= safe.batch_size)
-}
-
-fun is_batch_progress_over_internal(
- safe: &BridgeSafe,
- dep_count: u16,
- timestamp_ms: u64,
- clock: &Clock,
-): bool {
- if (dep_count == 0) { return false };
- (timestamp_ms + safe.batch_timeout_ms) <= clock::timestamp_ms(clock)
-}
-
-fun is_batch_final_internal(safe: &BridgeSafe, batch: &Batch, clock: &Clock): bool {
- (shared_structs::batch_last_updated_timestamp_ms(batch) + safe.batch_settle_timeout_ms) <= clock::timestamp_ms(clock)
-}
-
-fun is_any_batch_in_progress_internal(safe: &BridgeSafe, clock: &Clock): bool {
- if (safe.batches_count == 0) { return false };
- let last_index = safe.batches_count - 1;
- if (!should_create_new_batch_internal(safe, clock)) { return true };
- let batch = table::borrow(&safe.batches, last_index);
- !is_batch_final_internal(safe, batch, clock)
-}
-
-public fun get_bridge_addr(safe: &BridgeSafe): address {
- safe.bridge_addr
-}
-
-/// Get the current owner address
-public fun get_owner(safe: &BridgeSafe): address {
- bridge_roles::owner(&safe.roles)
-}
-
-/// Get the pending owner address (if any)
-public fun get_pending_owner(safe: &BridgeSafe): Option {
- bridge_roles::pending_owner(&safe.roles)
-}
-
-public fun get_batch_size(safe: &BridgeSafe): u16 {
- safe.batch_size
-}
-
-public fun get_batch_timeout_ms(safe: &BridgeSafe): u64 {
- safe.batch_timeout_ms
-}
-
-public fun get_batch_settle_timeout_ms(safe: &BridgeSafe): u64 {
- safe.batch_settle_timeout_ms
-}
-
-public fun get_batches_count(safe: &BridgeSafe): u64 {
- safe.batches_count
-}
-
-public fun get_deposits_count(safe: &BridgeSafe): u64 {
- safe.deposits_count
-}
-
-public fun get_pause(safe: &BridgeSafe): &Pause {
- &safe.pause
-}
-
-public fun get_pause_mut(safe: &mut BridgeSafe): &mut Pause {
- &mut safe.pause
+/// Package-internal: marks a token as not whitelisted without the mint-burn guard.
+/// Used by the adapter which handles MintCap cleanup separately.
+public(package) fun unwhitelist_token(safe: &mut BridgeSafe, key: vector) {
+ let cfg = borrow_token_cfg_mut(safe, key);
+ cfg.set_token_config_whitelisted(false);
+ events::emit_token_removed_from_whitelist(key);
}
-public fun get_batch_nonce(batch: &Batch): u64 {
- shared_structs::batch_nonce(batch)
+public fun set_bridge_addr(safe: &mut BridgeSafe, new_bridge_addr: address, ctx: &TxContext) {
+ assert_is_compatible(safe);
+ safe.roles.owner_role().assert_sender_is_active_role(ctx);
+
+ let previous_bridge = safe.bridge_addr;
+ safe.bridge_addr = new_bridge_addr;
+ events::emit_bridge_transferred(previous_bridge, new_bridge_addr);
}
-public fun get_batch_deposits_count(batch: &Batch): u16 {
- shared_structs::batch_deposits_count(batch)
+public fun set_batch_timeout_ms(safe: &mut BridgeSafe, new_timeout_ms: u64, ctx: &mut TxContext) {
+ assert_is_compatible(safe);
+ safe.roles.owner_role().assert_sender_is_active_role(ctx);
+ assert!(new_timeout_ms <= safe.batch_settle_timeout_ms, EBatchBlockLimitExceedsSettle);
+ safe.batch_timeout_ms = new_timeout_ms;
+ events::emit_batch_timeout_updated(new_timeout_ms);
}
-/// Transfer function: Bridge sends coins FROM the bridge safe contract TO recipient
-/// Only the bridge role can call this function
-/// The coins are taken from the contract's storage and sent to recipient
-public(package) fun transfer(
+public fun set_batch_settle_timeout_ms(
safe: &mut BridgeSafe,
- _bridge_cap: &bridge_roles::BridgeCap,
- receiver: address,
- amount: u64,
- treasury: &mut treasury::Treasury,
+ new_timeout_ms: u64,
+ clock: &Clock,
ctx: &mut TxContext,
-): bool {
- let key = utils::type_name_bytes();
+) {
+ assert_is_compatible(safe);
+ safe.pause.assert_paused();
+ safe.roles.owner_role().assert_sender_is_active_role(ctx);
+ assert!(new_timeout_ms >= safe.batch_timeout_ms, EBatchSettleLimitBelowBlock);
+ assert!(!is_any_batch_in_progress_internal(safe, clock), EBatchInProgress);
+ safe.batch_settle_timeout_ms = new_timeout_ms;
+ events::emit_batch_settle_timeout_updated(new_timeout_ms);
+}
- if (!table::contains(&safe.token_cfg, key)) {
- return false
- };
+public fun set_batch_size(safe: &mut BridgeSafe, new_size: u16, ctx: &mut TxContext) {
+ assert_is_compatible(safe);
+ safe.roles.owner_role().assert_sender_is_active_role(ctx);
+ assert!(new_size > 0, EBatchSizeZero);
+ assert!(new_size <= 100, EBatchSizeTooLarge);
+ safe.batch_size = new_size;
+ events::emit_batch_size_updated(new_size);
+}
- let cfg_ref = table::borrow(&safe.token_cfg, key);
+public fun set_token_min_limit(safe: &mut BridgeSafe, amount: u64, ctx: &mut TxContext) {
+ assert_is_compatible(safe);
+ safe.roles.owner_role().assert_sender_is_active_role(ctx);
- let current_balance = shared_structs::token_config_total_balance(cfg_ref);
- if (current_balance < amount) {
- return false
- };
+ let key = utils::type_name_bytes();
+ let cfg = borrow_token_cfg_mut(safe, key);
+ let old_max = cfg.token_config_max_limit();
- if (!bag::contains(&safe.coin_storage, key)) {
- return false
- };
+ assert!(amount > 0, EZeroAmount);
+ assert!(amount <= old_max, EInvalidTokenLimits);
- if (!shared_structs::get_token_config_is_locked(cfg_ref)) {
- let stored_coin = bag::borrow_mut, Coin>(&mut safe.coin_storage, key);
- let coin_value = coin::value(stored_coin);
- if (coin_value < amount) {
- return false
- };
+ cfg.set_token_config_min_limit(amount);
- let coin_to_transfer = coin::split(stored_coin, amount, ctx);
+ events::emit_token_limits_updated(key, amount, old_max);
+}
- if (coin::value(stored_coin) == 0) {
- let empty_coin = bag::remove, Coin>(&mut safe.coin_storage, key);
- coin::destroy_zero(empty_coin);
- };
- transfer::public_transfer(coin_to_transfer, receiver);
- } else {
- let stored_bt_coin = bag::borrow_mut<
- vector,
- Coin,
- >(
- &mut safe.coin_storage,
- key,
- );
- let coin_bt = coin::split(stored_bt_coin, amount, ctx);
+public fun set_token_max_limit(safe: &mut BridgeSafe, amount: u64, ctx: &mut TxContext) {
+ assert_is_compatible(safe);
+ safe.roles.owner_role().assert_sender_is_active_role(ctx);
- treasury::transfer_from_coin(
- treasury,
- receiver,
- &safe.from_coin_cap,
- coin_bt,
- ctx,
- );
- };
+ let key = utils::type_name_bytes();
+ let cfg = borrow_token_cfg_mut(safe, key);
+ let old_min = cfg.token_config_min_limit();
- let cfg_mut = borrow_token_cfg_mut(safe, key);
- shared_structs::subtract_from_token_config_total_balance(cfg_mut, amount);
+ assert!(amount >= old_min, EInvalidTokenLimits);
+ cfg.set_token_config_max_limit(amount);
- true
+ events::emit_token_limits_updated(key, old_min, amount);
}
-public fun get_stored_coin_balance(safe: &mut BridgeSafe): u64 {
- let key = utils::type_name_bytes();
- if (!table::contains(&safe.token_cfg, key)) {
- return 0
- };
- let cfg_ref = table::borrow(&safe.token_cfg, key);
- shared_structs::token_config_total_balance(cfg_ref)
-}
+public fun set_token_is_native(safe: &mut BridgeSafe, is_native: bool, ctx: &mut TxContext) {
+ assert_is_compatible(safe);
+ safe.roles.owner_role().assert_sender_is_active_role(ctx);
-public fun get_coin_storage_balance(safe: &BridgeSafe): u64 {
let key = utils::type_name_bytes();
- if (!bag::contains(&safe.coin_storage, key)) {
- return 0
- };
- let stored_coin = bag::borrow, Coin>(&safe.coin_storage, key);
- coin::value(stored_coin)
+ let cfg = borrow_token_cfg_mut(safe, key);
+ assert!(!(is_native && cfg.token_config_is_mint_burn()), EIncompatibleTokenFlags);
+ cfg.set_token_config_is_native(is_native);
+
+ events::emit_token_is_native_updated(key, is_native);
}
-public fun pause_contract(safe: &mut BridgeSafe, ctx: &mut TxContext) {
+public fun set_token_is_mint_burn(
+ safe: &mut BridgeSafe,
+ is_mint_burn: bool,
+ ctx: &mut TxContext,
+) {
+ assert_is_compatible(safe);
safe.roles.owner_role().assert_sender_is_active_role(ctx);
- pausable::pause(&mut safe.pause);
+
+ let key = utils::type_name_bytes();
+ let cfg = borrow_token_cfg_mut(safe, key);
+ assert!(!(is_mint_burn && cfg.token_config_is_native()), EIncompatibleTokenFlags);
+ cfg.set_token_config_is_mint_burn(is_mint_burn);
+
+ events::emit_token_is_mint_burn_updated(key, is_mint_burn);
}
-public fun unpause_contract(safe: &mut BridgeSafe, ctx: &mut TxContext) {
+public fun set_token_is_locked(safe: &mut BridgeSafe, is_locked: bool, ctx: &mut TxContext) {
safe.roles.owner_role().assert_sender_is_active_role(ctx);
- pausable::unpause(&mut safe.pause);
-}
-public fun transfer_ownership(safe: &mut BridgeSafe, new_owner: address, ctx: &TxContext) {
- safe.roles_mut().owner_role_mut().begin_role_transfer(new_owner, ctx)
-}
+ let key = utils::type_name_bytes();
+ let cfg = borrow_token_cfg_mut(safe, key);
+ assert!(
+ !(is_locked && shared_structs::token_config_is_mint_burn(cfg)),
+ EIncompatibleTokenFlags,
+ );
+ shared_structs::set_token_config_is_locked(cfg, is_locked);
-public fun accept_ownership(safe: &mut BridgeSafe, ctx: &TxContext) {
- safe.roles_mut().owner_role_mut().accept_role(ctx)
+ events::emit_token_is_locked_updated(key, is_locked);
}
// === Upgrade Management ===
@@ -769,14 +703,239 @@ public fun is_migration_in_progress(safe: &BridgeSafe): bool {
safe.compatible_versions.length() > 1
}
-/// [Package private] Asserts that the Safe object
-/// is compatible with the package's version.
+// === Asserts ===
+
public(package) fun assert_is_compatible(safe: &BridgeSafe) {
bridge_version_control::assert_object_version_is_compatible_with_package(safe.compatible_versions);
}
+public(package) fun assert_token_is_whitelisted(safe: &BridgeSafe, key: vector