From 06a548a42823d1086d5dbeb99d1b7e0060a08994 Mon Sep 17 00:00:00 2001 From: Eveline Molnar Date: Wed, 25 Feb 2026 17:02:37 +0400 Subject: [PATCH 01/30] mint burn tokens support --- sources/safe.move | 103 +++++++++++-- sources/version_control.move | 2 +- tests/deposit_transfer_tests.move | 240 ++++++++++++++++++++++++++++++ tests/safe_edge_case_tests.move | 2 +- tests/safe_internal_tests.move | 2 +- 5 files changed, 337 insertions(+), 12 deletions(-) diff --git a/sources/safe.move b/sources/safe.move index f492784..a956e3c 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -18,10 +18,17 @@ use std::u64::{min, max}; use sui::bag::{Self, Bag}; use sui::clock::{Self, Clock}; use sui::coin::{Self, Coin}; +use sui::dynamic_object_field as dof; use sui::event; use sui::table::{Self, Table}; use sui::vec_set::{Self, VecSet}; +// === Structs === + +public struct MintBurnCapKey has copy, drop, store { + token_type: vector, +} + // === Migration Events === public struct MigrationStarted has copy, drop { @@ -56,6 +63,11 @@ const EInvalidTokenLimits: u64 = 15; const EMigrationStarted: u64 = 16; const EMigrationNotStarted: u64 = 17; const ENotPendingVersion: u64 = 18; +const ENotNativeToken: u64 = 19; +const EMintBurnCapNotFound: u64 = 20; +const EMintBurnCapAlreadyRegistered: u64 = 21; +const EIncompatibleTokenFlags: u64 = 22; +const ETokenStillWhitelisted: u64 = 23; const MAX_U64: u64 = 18446744073709551615; const DEFAULT_BATCH_TIMEOUT_MS: u64 = 5 * 1000; // 5 seconds @@ -276,6 +288,10 @@ public fun set_token_is_native(safe: &mut BridgeSafe, is_native: bool, ctx: & safe.roles.owner_role().assert_sender_is_active_role(ctx); let key = utils::type_name_bytes(); + if (is_native) { + let cfg_ref = table::borrow(&safe.token_cfg, key); + assert!(!shared_structs::token_config_is_mint_burn(cfg_ref), EIncompatibleTokenFlags); + }; let cfg = borrow_token_cfg_mut(safe, key); shared_structs::set_token_config_is_native(cfg, is_native); @@ -300,12 +316,47 @@ public fun set_token_is_mint_burn( safe.roles.owner_role().assert_sender_is_active_role(ctx); let key = utils::type_name_bytes(); + if (is_mint_burn) { + let cfg_ref = table::borrow(&safe.token_cfg, key); + assert!(!shared_structs::token_config_is_native(cfg_ref), EIncompatibleTokenFlags); + }; let cfg = borrow_token_cfg_mut(safe, key); shared_structs::set_token_config_is_mint_burn(cfg, is_mint_burn); events::emit_token_is_mint_burn_updated(key, is_mint_burn); } +public fun register_mint_burn_cap( + safe: &mut BridgeSafe, + cap: coin::TreasuryCap, + ctx: &TxContext, +) { + 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_is_mint_burn(cfg_ref), EIncompatibleTokenFlags); + let cap_key = MintBurnCapKey { token_type: key }; + assert!(!dof::exists_(&safe.id, cap_key), EMintBurnCapAlreadyRegistered); + dof::add(&mut safe.id, cap_key, cap); +} + +#[allow(lint(self_transfer))] +public fun deregister_mint_burn_cap(safe: &mut BridgeSafe, ctx: &TxContext) { + 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), ETokenStillWhitelisted); + let cap_key = MintBurnCapKey { token_type: key }; + assert!( + dof::exists_with_type>(&safe.id, cap_key), + EMintBurnCapNotFound, + ); + let cap = dof::remove>(&mut safe.id, cap_key); + transfer::public_transfer(cap, ctx.sender()); +} + public fun set_bridge_addr(safe: &mut BridgeSafe, new_bridge_addr: address, ctx: &TxContext) { safe.roles.owner_role().assert_sender_is_active_role(ctx); @@ -323,7 +374,7 @@ public fun init_supply(safe: &mut BridgeSafe, coin_in: Coin, ctx: &mut TxC 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!(shared_structs::token_config_is_native(cfg_ref), ENotNativeToken); let amount = coin::value(&coin_in); @@ -347,7 +398,7 @@ public fun sync_supply(safe: &mut BridgeSafe, mut coin_in: Coin, ctx: &mut 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!(shared_structs::token_config_is_native(cfg_ref), ENotNativeToken); let expected_balance = shared_structs::token_config_total_balance(cfg_ref); @@ -401,6 +452,15 @@ public fun deposit( assert!(amount >= shared_structs::token_config_min_limit(cfg_ref), EAmountBelowMinimum); assert!(amount <= shared_structs::token_config_max_limit(cfg_ref), EAmountAboveMaximum); + let is_mint_burn = shared_structs::token_config_is_mint_burn(cfg_ref); + if (is_mint_burn) { + let cap_key = MintBurnCapKey { token_type: key }; + assert!( + dof::exists_with_type>(&safe.id, cap_key), + EMintBurnCapNotFound, + ); + }; + if (should_create_new_batch_internal(safe, clock)) { create_new_batch_internal(safe, clock, ctx); }; @@ -431,11 +491,17 @@ public fun deposit( 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); + if (is_mint_burn) { + let cap_key = MintBurnCapKey { token_type: key }; + let cap = dof::borrow_mut>(&mut safe.id, cap_key); + let _ = coin::burn(cap, coin_in); } else { - bag::add(&mut safe.coin_storage, key, coin_in); + 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( @@ -595,18 +661,37 @@ public(package) fun transfer( return false }; - let cfg_ref = table::borrow(&safe.token_cfg, key); + let (is_mint_burn, is_locked, current_balance) = { + let cfg_ref = table::borrow(&safe.token_cfg, key); + ( + shared_structs::token_config_is_mint_burn(cfg_ref), + shared_structs::get_token_config_is_locked(cfg_ref), + shared_structs::token_config_total_balance(cfg_ref), + ) + }; - let current_balance = shared_structs::token_config_total_balance(cfg_ref); if (current_balance < amount) { return false }; + if (is_mint_burn) { + let cap_key = MintBurnCapKey { token_type: key }; + if (!dof::exists_with_type>(&safe.id, cap_key)) { + return false + }; + let cap = dof::borrow_mut>(&mut safe.id, cap_key); + let minted_coin = coin::mint(cap, amount, ctx); + transfer::public_transfer(minted_coin, receiver); + let cfg_mut = borrow_token_cfg_mut(safe, key); + shared_structs::subtract_from_token_config_total_balance(cfg_mut, amount); + return true + }; + if (!bag::contains(&safe.coin_storage, key)) { return false }; - if (!shared_structs::get_token_config_is_locked(cfg_ref)) { + if (!is_locked) { let stored_coin = bag::borrow_mut, Coin>(&mut safe.coin_storage, key); let coin_value = coin::value(stored_coin); if (coin_value < amount) { diff --git a/sources/version_control.move b/sources/version_control.move index e1cdd6d..65109da 100644 --- a/sources/version_control.move +++ b/sources/version_control.move @@ -8,7 +8,7 @@ module bridge_safe::bridge_version_control; use sui::vec_set::VecSet; /// The current version of the package. -const VERSION: u64 = 1; +const VERSION: u64 = 3; // === Errors === const EIncompatibleVersion: u64 = 0; diff --git a/tests/deposit_transfer_tests.move b/tests/deposit_transfer_tests.move index 53f461a..973f017 100644 --- a/tests/deposit_transfer_tests.move +++ b/tests/deposit_transfer_tests.move @@ -2,6 +2,7 @@ module bridge_safe::deposit_transfer_tests; use bridge_safe::bridge_roles::BridgeCap; +use bridge_safe::mint_burn_coin::{Self as mbc, MINT_BURN_COIN}; use bridge_safe::pausable; use bridge_safe::safe::{Self, BridgeSafe}; use locked_token::bridge_token::{Self as br, BRIDGE_TOKEN}; @@ -902,3 +903,242 @@ fun test_deposit_then_transfer_integration() { ts::end(scenario); } + +// ============================================================ +// Mint-burn token tests +// ============================================================ + +/// Sets up a scenario with both bridge-token infrastructure and a whitelisted +/// mint-burn token whose TreasuryCap is registered in the safe. +/// Pre-mints DEPOSIT_AMOUNT * 2 coins and sends them to USER. +fun setup_mint_burn(): Scenario { + let mut s = setup(); + + // Initialize MINT_BURN_COIN currency — TreasuryCap goes to ADMIN + s.next_tx(ADMIN); + mbc::init_for_testing(ts::ctx(&mut s)); + + // Whitelist, flip is_mint_burn, pre-mint test coins, then register cap + s.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&s); + + safe::whitelist_token( + &mut safe, + MIN_AMOUNT, + MAX_AMOUNT, + false, // is_native + false, // is_locked + ts::ctx(&mut s), + ); + safe::set_token_is_mint_burn(&mut safe, true, ts::ctx(&mut s)); + + // Mint coins now (before handing cap to safe) so supply tracking is consistent + let mut treasury_cap = ts::take_from_address>(&s, ADMIN); + let pre_minted = coin::mint(&mut treasury_cap, DEPOSIT_AMOUNT * 2, ts::ctx(&mut s)); + transfer::public_transfer(pre_minted, USER); + + safe::register_mint_burn_cap(&mut safe, treasury_cap, ts::ctx(&mut s)); + + ts::return_shared(safe); + }; + + s +} + +#[test] +fun test_deposit_mint_burn_burns_coin() { + let mut scenario = setup_mint_burn(); + + scenario.next_tx(USER); + { + let mut safe = ts::take_shared(&scenario); + let clock = clock::create_for_testing(ts::ctx(&mut scenario)); + + let full_coin = ts::take_from_address>(&scenario, USER); + let mut rest = full_coin; + let deposit_coin = coin::split(&mut rest, DEPOSIT_AMOUNT, ts::ctx(&mut scenario)); + transfer::public_transfer(rest, USER); + + safe::deposit( + &mut safe, + deposit_coin, + RECIPIENT_VECTOR, + &clock, + ts::ctx(&mut scenario), + ); + + // Coin must NOT be stored in the bag — it was burned + assert!(safe::get_coin_storage_balance(&safe) == 0, 0); + // total_balance tracks the burned amount available for future minting + assert!(safe::get_stored_coin_balance(&mut safe) == DEPOSIT_AMOUNT, 1); + assert!(safe::get_deposits_count(&safe) == 1, 2); + + clock::destroy_for_testing(clock); + ts::return_shared(safe); + }; + + ts::end(scenario); +} + +#[test] +fun test_transfer_mint_burn_mints_coin() { + let mut scenario = setup_mint_burn(); + + // Deposit first + scenario.next_tx(USER); + { + let mut safe = ts::take_shared(&scenario); + let clock = clock::create_for_testing(ts::ctx(&mut scenario)); + let full_coin = ts::take_from_address>(&scenario, USER); + let mut rest = full_coin; + let deposit_coin = coin::split(&mut rest, DEPOSIT_AMOUNT, ts::ctx(&mut scenario)); + transfer::public_transfer(rest, USER); + safe::deposit(&mut safe, deposit_coin, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario)); + clock::destroy_for_testing(clock); + ts::return_shared(safe); + }; + + // Execute transfer — bridge mints fresh coins for the recipient + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + let mut treasury = ts::take_shared>(&scenario); + let bridge_cap = ts::take_from_address(&scenario, ADMIN); + + let success = safe::transfer( + &mut safe, + &bridge_cap, + RECIPIENT, + DEPOSIT_AMOUNT, + &mut treasury, + ts::ctx(&mut scenario), + ); + + assert!(success, 0); + assert!(safe::get_stored_coin_balance(&mut safe) == 0, 1); + assert!(safe::get_coin_storage_balance(&safe) == 0, 2); + + ts::return_shared(safe); + ts::return_shared(treasury); + ts::return_to_address(ADMIN, bridge_cap); + }; + + // Verify recipient received the freshly minted coin + scenario.next_tx(RECIPIENT); + { + let received = ts::take_from_address>(&scenario, RECIPIENT); + assert!(coin::value(&received) == DEPOSIT_AMOUNT, 3); + ts::return_to_address(RECIPIENT, received); + }; + + ts::end(scenario); +} + +#[test] +#[expected_failure(abort_code = safe::EMintBurnCapNotFound)] +fun test_deposit_mint_burn_no_cap_fails() { + let mut scenario = setup(); + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + safe::whitelist_token( + &mut safe, MIN_AMOUNT, MAX_AMOUNT, false, false, ts::ctx(&mut scenario), + ); + safe::set_token_is_mint_burn(&mut safe, true, ts::ctx(&mut scenario)); + // Intentionally NOT registering the TreasuryCap + ts::return_shared(safe); + }; + + scenario.next_tx(USER); + { + let mut safe = ts::take_shared(&scenario); + let clock = clock::create_for_testing(ts::ctx(&mut scenario)); + let coin_in = coin::mint_for_testing(DEPOSIT_AMOUNT, ts::ctx(&mut scenario)); + // Must abort with EMintBurnCapNotFound + safe::deposit(&mut safe, coin_in, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario)); + clock::destroy_for_testing(clock); + ts::return_shared(safe); + }; + + ts::end(scenario); +} + +#[test] +fun test_transfer_mint_burn_exceeds_burned_fails() { + let mut scenario = setup_mint_burn(); + + scenario.next_tx(USER); + { + let mut safe = ts::take_shared(&scenario); + let clock = clock::create_for_testing(ts::ctx(&mut scenario)); + let full_coin = ts::take_from_address>(&scenario, USER); + let mut rest = full_coin; + let deposit_coin = coin::split(&mut rest, DEPOSIT_AMOUNT, ts::ctx(&mut scenario)); + transfer::public_transfer(rest, USER); + safe::deposit(&mut safe, deposit_coin, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario)); + clock::destroy_for_testing(clock); + ts::return_shared(safe); + }; + + // Attempt to transfer one more than was burned — must return false + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + let mut treasury = ts::take_shared>(&scenario); + let bridge_cap = ts::take_from_address(&scenario, ADMIN); + + let success = safe::transfer( + &mut safe, + &bridge_cap, + RECIPIENT, + DEPOSIT_AMOUNT + 1, + &mut treasury, + ts::ctx(&mut scenario), + ); + assert!(!success, 0); + + ts::return_shared(safe); + ts::return_shared(treasury); + ts::return_to_address(ADMIN, bridge_cap); + }; + + ts::end(scenario); +} + +#[test] +#[expected_failure(abort_code = safe::EIncompatibleTokenFlags)] +fun test_incompatible_token_flags_native_and_mint_burn() { + let mut scenario = setup(); + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + // Whitelist as native first + safe::whitelist_token( + &mut safe, MIN_AMOUNT, MAX_AMOUNT, true, false, ts::ctx(&mut scenario), + ); + // Setting mint-burn on a native token must abort with EIncompatibleTokenFlags + safe::set_token_is_mint_burn(&mut safe, true, ts::ctx(&mut scenario)); + ts::return_shared(safe); + }; + + ts::end(scenario); +} + +#[test] +#[expected_failure(abort_code = safe::ETokenStillWhitelisted)] +fun test_deregister_cap_requires_dewhitelisting() { + let mut scenario = setup_mint_burn(); + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + // Token is still whitelisted — must abort with ETokenStillWhitelisted + safe::deregister_mint_burn_cap(&mut safe, ts::ctx(&mut scenario)); + ts::return_shared(safe); + }; + + ts::end(scenario); +} diff --git a/tests/safe_edge_case_tests.move b/tests/safe_edge_case_tests.move index 63de240..2dc0374 100644 --- a/tests/safe_edge_case_tests.move +++ b/tests/safe_edge_case_tests.move @@ -214,7 +214,7 @@ fun test_init_supply_zero_amount() { // Test init_supply with non-native token (should fail) #[test] -#[expected_failure(abort_code = safe::EInsufficientBalance)] +#[expected_failure(abort_code = safe::ENotNativeToken)] fun test_init_supply_non_native_token() { let mut scenario = setup(); scenario.next_tx(ADMIN); diff --git a/tests/safe_internal_tests.move b/tests/safe_internal_tests.move index 65079ee..af5bb8d 100644 --- a/tests/safe_internal_tests.move +++ b/tests/safe_internal_tests.move @@ -1259,7 +1259,7 @@ fun test_sync_supply_insufficient_coin() { } #[test] -#[expected_failure(abort_code = safe::EInsufficientBalance)] +#[expected_failure(abort_code = safe::ENotNativeToken)] fun test_sync_supply_not_native() { let mut scenario = setup(); scenario.next_tx(ADMIN); From 291927c0257f216678cdab335a3abcd13e8caf90 Mon Sep 17 00:00:00 2001 From: Eveline Molnar Date: Tue, 3 Mar 2026 20:06:53 +0400 Subject: [PATCH 02/30] test mint cap --- Move.toml | 11 +----- sources/safe.move | 96 +++++++++++++++++++++++++++++++++-------------- 2 files changed, 69 insertions(+), 38 deletions(-) diff --git a/Move.toml b/Move.toml index 08a3fc0..dc7ae2c 100644 --- a/Move.toml +++ b/Move.toml @@ -4,19 +4,10 @@ edition = "2024.beta" [dependencies] locked_token = { git = "https://github.com/multiversx/mx-locked-token-sc-sui.git", rev = "master", subdir = "" } +treasury = { git = "https://github.com/multiversx/stablecoin-sui.git", rev="xmn-launch", subdir = "packages/treasury" } [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" diff --git a/sources/safe.move b/sources/safe.move index a956e3c..d11c6e0 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -13,6 +13,7 @@ use bridge_safe::upgrade_service_bridge; use bridge_safe::utils; use locked_token::bridge_token::BRIDGE_TOKEN; use locked_token::treasury; +use treasury::treasury::{MintCap, Treasury as XmnTreasury}; use shared_structs::shared_structs::{Self, TokenConfig, Batch, Deposit}; use std::u64::{min, max}; use sui::bag::{Self, Bag}; @@ -136,22 +137,42 @@ fun borrow_token_cfg_mut(safe: &mut BridgeSafe, key: vector): &mut TokenConf table::borrow_mut(&mut safe.token_cfg, key) } -public fun whitelist_token( +fun assert_token_is_whitelisted(safe: &BridgeSafe, key: vector) { + assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); + let cfg = table::borrow(&safe.token_cfg, key); + assert!(shared_structs::token_config_whitelisted(cfg), ETokenNotWhitelisted); +} + +fun assert_token_is_not_whitelisted(safe: &BridgeSafe, key: vector) { + assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); + let cfg = table::borrow(&safe.token_cfg, key); + assert!(!shared_structs::token_config_whitelisted(cfg), ETokenStillWhitelisted); +} + +fun assert_token_is_mint_burn(safe: &BridgeSafe, key: vector) { + assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); + let cfg = table::borrow(&safe.token_cfg, key); + assert!(shared_structs::token_config_is_mint_burn(cfg), EIncompatibleTokenFlags); +} + +fun whitelist_token_internal( safe: &mut BridgeSafe, minimum_amount: u64, maximum_amount: u64, is_native: bool, is_locked: bool, + is_mint_burn: bool, ctx: &mut TxContext, ) { safe.roles.owner_role().assert_sender_is_active_role(ctx); - let key = utils::type_name_bytes(); - let exists = table::contains(&safe.token_cfg, key); - + assert!(!(is_mint_burn && is_native), EIncompatibleTokenFlags); assert!(minimum_amount > 0, EZeroAmount); assert!(minimum_amount <= maximum_amount, EInvalidTokenLimits); + let key = utils::type_name_bytes(); + let exists = table::contains(&safe.token_cfg, key); + if (exists) { let cfg = table::borrow(&safe.token_cfg, key); let is_currently_whitelisted = shared_structs::token_config_whitelisted(cfg); @@ -160,17 +181,19 @@ public fun whitelist_token( 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_is_mint_burn(cfg_mut, is_mint_burn); 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); } else { - let cfg = shared_structs::create_token_config( + let mut cfg = shared_structs::create_token_config( true, is_native, minimum_amount, maximum_amount, is_locked, ); + shared_structs::set_token_config_is_mint_burn(&mut cfg, is_mint_burn); table::add(&mut safe.token_cfg, key, cfg); }; @@ -179,11 +202,31 @@ public fun whitelist_token( minimum_amount, maximum_amount, is_native, - false, + is_mint_burn, is_locked, ); } +public fun whitelist_token( + safe: &mut BridgeSafe, + minimum_amount: u64, + maximum_amount: u64, + is_native: bool, + is_locked: bool, + ctx: &mut TxContext, +) { + whitelist_token_internal(safe, minimum_amount, maximum_amount, is_native, is_locked, false, ctx); +} + +public fun whitelist_token_mint_burn( + safe: &mut BridgeSafe, + minimum_amount: u64, + maximum_amount: u64, + ctx: &mut TxContext, +) { + whitelist_token_internal(safe, minimum_amount, maximum_amount, false, false, true, ctx); +} + 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(); @@ -288,11 +331,11 @@ public fun set_token_is_native(safe: &mut BridgeSafe, is_native: bool, ctx: & safe.roles.owner_role().assert_sender_is_active_role(ctx); let key = utils::type_name_bytes(); - if (is_native) { - let cfg_ref = table::borrow(&safe.token_cfg, key); - assert!(!shared_structs::token_config_is_mint_burn(cfg_ref), EIncompatibleTokenFlags); - }; let cfg = borrow_token_cfg_mut(safe, key); + assert!( + !(is_native && shared_structs::token_config_is_mint_burn(cfg)), + EIncompatibleTokenFlags, + ); shared_structs::set_token_config_is_native(cfg, is_native); events::emit_token_is_native_updated(key, is_native); @@ -316,26 +359,27 @@ public fun set_token_is_mint_burn( safe.roles.owner_role().assert_sender_is_active_role(ctx); let key = utils::type_name_bytes(); - if (is_mint_burn) { - let cfg_ref = table::borrow(&safe.token_cfg, key); - assert!(!shared_structs::token_config_is_native(cfg_ref), EIncompatibleTokenFlags); - }; let cfg = borrow_token_cfg_mut(safe, key); + assert!( + !(is_mint_burn && shared_structs::token_config_is_native(cfg)), + EIncompatibleTokenFlags, + ); shared_structs::set_token_config_is_mint_burn(cfg, is_mint_burn); events::emit_token_is_mint_burn_updated(key, is_mint_burn); } +/// Register a legacy coin::TreasuryCap for a mint-burn token. +/// Signature is unchanged from the v2 deployment for upgrade compatibility. public fun register_mint_burn_cap( safe: &mut BridgeSafe, - cap: coin::TreasuryCap, + cap: MintCap, ctx: &TxContext, ) { 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_is_mint_burn(cfg_ref), EIncompatibleTokenFlags); + assert_token_is_whitelisted(safe, key); + assert_token_is_mint_burn(safe, key); let cap_key = MintBurnCapKey { token_type: key }; assert!(!dof::exists_(&safe.id, cap_key), EMintBurnCapAlreadyRegistered); dof::add(&mut safe.id, cap_key, cap); @@ -345,15 +389,13 @@ public fun register_mint_burn_cap( public fun deregister_mint_burn_cap(safe: &mut BridgeSafe, ctx: &TxContext) { 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), ETokenStillWhitelisted); + assert_token_is_not_whitelisted(safe, key); let cap_key = MintBurnCapKey { token_type: key }; assert!( - dof::exists_with_type>(&safe.id, cap_key), + dof::exists_with_type>(&safe.id, cap_key), EMintBurnCapNotFound, ); - let cap = dof::remove>(&mut safe.id, cap_key); + let cap = dof::remove>(&mut safe.id, cap_key); transfer::public_transfer(cap, ctx.sender()); } @@ -370,10 +412,8 @@ public fun init_supply(safe: &mut BridgeSafe, coin_in: Coin, ctx: &mut TxC let key = utils::type_name_bytes(); - assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); + assert_token_is_whitelisted(safe, key); 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), ENotNativeToken); let amount = coin::value(&coin_in); @@ -395,9 +435,8 @@ public fun sync_supply(safe: &mut BridgeSafe, mut coin_in: Coin, ctx: &mut let key = utils::type_name_bytes(); - assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); + assert_token_is_whitelisted(safe, key); 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), ENotNativeToken); let expected_balance = shared_structs::token_config_total_balance(cfg_ref); @@ -430,6 +469,7 @@ public fun sync_supply(safe: &mut BridgeSafe, mut coin_in: Coin, ctx: &mut }; } + /// 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( From ca3787852599bda1b9b902b11a498519efe04406 Mon Sep 17 00:00:00 2001 From: Cristi Corcoveanu Date: Tue, 10 Mar 2026 12:20:25 +0200 Subject: [PATCH 03/30] small refactors --- Move.lock | 126 ++++++++-------- Move.toml | 2 +- sources/safe.move | 88 +++++------ sources/shared_structs.move | 43 +++++- tests/deposit_transfer_tests.move | 239 ------------------------------ tests/shared_structs_tests.move | 32 ++-- 6 files changed, 167 insertions(+), 363 deletions(-) diff --git a/Move.lock b/Move.lock index fba1734..83d5371 100644 --- a/Move.lock +++ b/Move.lock @@ -1,77 +1,77 @@ -# @generated by Move, please check-in and do not edit manually. +# Generated by move; do not edit +# This file should be checked in. [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" }, -] +version = 4 -[[move.package]] -id = "Bridge" -source = { git = "https://github.com/MystenLabs/sui.git", rev = "664b05b3b047c5bb03979d093660176176ea6175", subdir = "crates/sui-framework/packages/bridge" } +[pinned.mainnet.MoveStdlib] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "655576caa3d7faaed39ebbc15d1bcc91d0761aee" } +use_environment = "mainnet" +manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363" +deps = {} -dependencies = [ - { id = "MoveStdlib", name = "MoveStdlib" }, - { id = "Sui", name = "Sui" }, - { id = "SuiSystem", name = "SuiSystem" }, -] +[pinned.mainnet.Sui] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "655576caa3d7faaed39ebbc15d1bcc91d0761aee" } +use_environment = "mainnet" +manifest_digest = "CD547CB1ACCE0880C835DAED2D8FFCB91D56C833AE5240D3AA5B918398263195" +deps = { MoveStdlib = "MoveStdlib" } -[[move.package]] -id = "MoveStdlib" -source = { git = "https://github.com/MystenLabs/sui.git", rev = "664b05b3b047c5bb03979d093660176176ea6175", subdir = "crates/sui-framework/packages/move-stdlib" } +[pinned.mainnet.locked_token] +source = { git = "https://github.com/multiversx/mx-locked-token-sc-sui.git", subdir = ".", rev = "fdeec1e875b5a36aa42f62da293e65e15623bba6" } +use_environment = "mainnet" +manifest_digest = "74D499A47226D8F7A178930B275B879EE47F383206AD6C7E5C4A353ECFF5DD5E" +deps = { std = "MoveStdlib", sui = "Sui", sui_extensions = "sui_extensions" } -[[move.package]] -id = "Sui" -source = { git = "https://github.com/MystenLabs/sui.git", rev = "664b05b3b047c5bb03979d093660176176ea6175", subdir = "crates/sui-framework/packages/sui-framework" } +[pinned.mainnet.mx_bridge_sc_sui] +source = { root = true } +use_environment = "mainnet" +manifest_digest = "738E2A0AD4F874F7CC5A487CB194A6962F83D7308931512299820CE9B9301812" +deps = { locked_token = "locked_token", std = "MoveStdlib", sui = "Sui", treasury = "treasury" } -dependencies = [ - { id = "MoveStdlib", name = "MoveStdlib" }, -] +[pinned.mainnet.sui_extensions] +source = { git = "https://github.com/multiversx/stablecoin-sui.git", subdir = "packages/sui_extensions", rev = "cf6a0cd6cf2272c889be49051e8c3bac5251cd83" } +use_environment = "mainnet" +manifest_digest = "E41BBD67BE8940D26C79D78B028477EF5B33BA217A1282C78ACB344CF8A5ECF6" +deps = { std = "MoveStdlib", sui = "Sui" } -[[move.package]] -id = "SuiSystem" -source = { git = "https://github.com/MystenLabs/sui.git", rev = "664b05b3b047c5bb03979d093660176176ea6175", subdir = "crates/sui-framework/packages/sui-system" } +[pinned.mainnet.treasury] +source = { git = "https://github.com/multiversx/stablecoin-sui.git", subdir = "packages/treasury", rev = "cf6a0cd6cf2272c889be49051e8c3bac5251cd83" } +use_environment = "mainnet" +manifest_digest = "6B08AFABA09DC0D0216E97EB27C33CD7A3791E8DC3631C94FB08528F77C1E3AD" +deps = { std = "MoveStdlib", sui = "Sui", sui_extensions = "sui_extensions" } -dependencies = [ - { id = "MoveStdlib", name = "MoveStdlib" }, - { id = "Sui", name = "Sui" }, -] +[pinned.testnet.MoveStdlib] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "655576caa3d7faaed39ebbc15d1bcc91d0761aee" } +use_environment = "testnet" +manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363" +deps = {} -[[move.package]] -id = "locked_token" -source = { git = "https://github.com/multiversx/mx-locked-token-sc-sui.git", rev = "master", subdir = "" } +[pinned.testnet.Sui] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "655576caa3d7faaed39ebbc15d1bcc91d0761aee" } +use_environment = "testnet" +manifest_digest = "7AFB66695545775FBFBB2D3078ADFD084244D5002392E837FDE21D9EA1C6D01C" +deps = { MoveStdlib = "MoveStdlib" } -dependencies = [ - { id = "Bridge", name = "Bridge" }, - { id = "MoveStdlib", name = "MoveStdlib" }, - { id = "Sui", name = "Sui" }, - { id = "SuiSystem", name = "SuiSystem" }, - { id = "sui_extensions", name = "sui_extensions" }, -] +[pinned.testnet.locked_token] +source = { git = "https://github.com/multiversx/mx-locked-token-sc-sui.git", subdir = ".", rev = "fdeec1e875b5a36aa42f62da293e65e15623bba6" } +use_environment = "testnet" +manifest_digest = "F2724BA0C80232B5E6AB2D4ABD3C023C3CF01390712D263D5BDE10231E1830D4" +deps = { std = "MoveStdlib", sui = "Sui", sui_extensions = "sui_extensions" } -[[move.package]] -id = "sui_extensions" -source = { git = "https://github.com/circlefin/stablecoin-sui.git", rev = "master", subdir = "packages/sui_extensions" } +[pinned.testnet.mx_bridge_sc_sui] +source = { root = true } +use_environment = "testnet" +manifest_digest = "D8B8C5E8B49057924BC8F61F7F6EE18449CAE93F1153F6C53FB1173BA951D47C" +deps = { locked_token = "locked_token", std = "MoveStdlib", sui = "Sui", treasury = "treasury" } -dependencies = [ - { id = "Sui", name = "Sui" }, -] +[pinned.testnet.sui_extensions] +source = { git = "https://github.com/multiversx/stablecoin-sui.git", subdir = "packages/sui_extensions", rev = "cf6a0cd6cf2272c889be49051e8c3bac5251cd83" } +use_environment = "testnet" +manifest_digest = "5745706258F61D6CE210904B3E6AE87A73CE9D31A6F93BE4718C442529332A87" +deps = { std = "MoveStdlib", sui = "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" +[pinned.testnet.treasury] +source = { git = "https://github.com/multiversx/stablecoin-sui.git", subdir = "packages/treasury", rev = "cf6a0cd6cf2272c889be49051e8c3bac5251cd83" } +use_environment = "testnet" +manifest_digest = "54E813390EFE2FEE4DA372CD4F8B20971B2E7A89DA6E51212EDE743C2DC4EA46" +deps = { std = "MoveStdlib", sui = "Sui", sui_extensions = "sui_extensions" } diff --git a/Move.toml b/Move.toml index dc7ae2c..4c1461a 100644 --- a/Move.toml +++ b/Move.toml @@ -3,7 +3,7 @@ name = "mx-bridge-sc-sui" edition = "2024.beta" [dependencies] -locked_token = { git = "https://github.com/multiversx/mx-locked-token-sc-sui.git", rev = "master", subdir = "" } +locked_token = { git = "https://github.com/multiversx/mx-locked-token-sc-sui.git", rev = "dependency-fix", subdir = "" } treasury = { git = "https://github.com/multiversx/stablecoin-sui.git", rev="xmn-launch", subdir = "packages/treasury" } [addresses] diff --git a/sources/safe.move b/sources/safe.move index d11c6e0..6e530b0 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. @@ -68,7 +68,6 @@ const ENotNativeToken: u64 = 19; const EMintBurnCapNotFound: u64 = 20; const EMintBurnCapAlreadyRegistered: u64 = 21; const EIncompatibleTokenFlags: u64 = 22; -const ETokenStillWhitelisted: u64 = 23; const MAX_U64: u64 = 18446744073709551615; const DEFAULT_BATCH_TIMEOUT_MS: u64 = 5 * 1000; // 5 seconds @@ -137,24 +136,6 @@ fun borrow_token_cfg_mut(safe: &mut BridgeSafe, key: vector): &mut TokenConf table::borrow_mut(&mut safe.token_cfg, key) } -fun assert_token_is_whitelisted(safe: &BridgeSafe, key: vector) { - assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); - let cfg = table::borrow(&safe.token_cfg, key); - assert!(shared_structs::token_config_whitelisted(cfg), ETokenNotWhitelisted); -} - -fun assert_token_is_not_whitelisted(safe: &BridgeSafe, key: vector) { - assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); - let cfg = table::borrow(&safe.token_cfg, key); - assert!(!shared_structs::token_config_whitelisted(cfg), ETokenStillWhitelisted); -} - -fun assert_token_is_mint_burn(safe: &BridgeSafe, key: vector) { - assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); - let cfg = table::borrow(&safe.token_cfg, key); - assert!(shared_structs::token_config_is_mint_burn(cfg), EIncompatibleTokenFlags); -} - fun whitelist_token_internal( safe: &mut BridgeSafe, minimum_amount: u64, @@ -172,30 +153,20 @@ fun whitelist_token_internal( let key = utils::type_name_bytes(); let exists = table::contains(&safe.token_cfg, key); - 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_is_mint_burn(cfg_mut, is_mint_burn); - 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); - } else { - let mut cfg = shared_structs::create_token_config( - true, - is_native, - minimum_amount, - maximum_amount, - is_locked, - ); - shared_structs::set_token_config_is_mint_burn(&mut cfg, is_mint_burn); - table::add(&mut safe.token_cfg, key, cfg); + assert_token_is_not_whitelisted(safe, key); }; + + shared_structs::upsert_token_config( + &mut safe.token_cfg, + key, + true, + is_native, + minimum_amount, + maximum_amount, + is_locked, + is_mint_burn, + ); events::emit_token_whitelisted( key, @@ -504,6 +475,7 @@ public fun deposit( 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); @@ -516,9 +488,11 @@ public fun deposit( 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); @@ -806,6 +780,30 @@ public fun accept_ownership(safe: &mut BridgeSafe, ctx: &TxContext) { safe.roles_mut().owner_role_mut().accept_role(ctx) } +// === Asserts === + +public(package) fun assert_is_compatible(safe: &BridgeSafe) { + bridge_version_control::assert_object_version_is_compatible_with_package(safe.compatible_versions); +} + +fun assert_token_is_whitelisted(safe: &BridgeSafe, key: vector) { + assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); + let cfg = table::borrow(&safe.token_cfg, key); + assert!(shared_structs::token_config_whitelisted(cfg), ETokenNotWhitelisted); +} + +fun assert_token_is_not_whitelisted(safe: &BridgeSafe, key: vector) { + assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); + let cfg = table::borrow(&safe.token_cfg, key); + assert!(!shared_structs::token_config_whitelisted(cfg), ETokenAlreadyExists); +} + +fun assert_token_is_mint_burn(safe: &BridgeSafe, key: vector) { + assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); + let cfg = table::borrow(&safe.token_cfg, key); + assert!(shared_structs::token_config_is_mint_burn(cfg), EIncompatibleTokenFlags); +} + // === Upgrade Management === /// Returns the compatible versions for the safe @@ -894,12 +892,6 @@ 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. -public(package) fun assert_is_compatible(safe: &BridgeSafe) { - bridge_version_control::assert_object_version_is_compatible_with_package(safe.compatible_versions); -} - #[test_only] public fun init_for_testing(from_cap: treasury::FromCoinCap, ctx: &mut TxContext) { initialize(from_cap, ctx); diff --git a/sources/shared_structs.move b/sources/shared_structs.move index 0214507..134790d 100644 --- a/sources/shared_structs.move +++ b/sources/shared_structs.move @@ -1,5 +1,7 @@ module shared_structs::shared_structs; +use sui::table::{Self, Table}; + public enum DepositStatus has copy, drop, store { None, Pending, @@ -192,20 +194,59 @@ public(package) fun set_batch_last_updated_timestamp_ms(batch: &mut Batch, times batch.last_updated_timestamp_ms = timestamp_ms; } +public fun upsert_token_config( + config: &mut Table, TokenConfig>, + key: vector, + whitelisted: bool, + is_native: bool, + min_limit: u64, + max_limit: u64, + is_locked: bool, + is_mint_burn: bool, +) { + if (table::contains(config, key)) { + let cfg = table::borrow_mut(config, key); + set_token_config(cfg, whitelisted, is_native, min_limit, max_limit, is_locked, is_mint_burn); + + return; + }; + + let mut cfg = create_token_config(whitelisted, is_native, min_limit, max_limit, is_locked, is_mint_burn); + table::add(config, key, cfg); +} + +public fun set_token_config( + config: &mut TokenConfig, + whitelisted: bool, + is_native: bool, + min_limit: u64, + max_limit: u64, + is_locked: bool, + is_mint_burn: bool, +) { + set_token_config_whitelisted(config, whitelisted); + set_token_config_is_native(config, is_native); + set_token_config_is_mint_burn(config, is_mint_burn); + set_token_config_min_limit(config, min_limit); + set_token_config_max_limit(config, max_limit); + set_token_config_is_locked(config, is_locked); +} + public fun create_token_config( whitelisted: bool, is_native: bool, min_limit: u64, max_limit: u64, is_locked: bool, + is_mint_burn: bool, ): TokenConfig { TokenConfig { whitelisted, is_native, - is_mint_burn: false, min_limit, max_limit, total_balance: 0, is_locked, + is_mint_burn, } } diff --git a/tests/deposit_transfer_tests.move b/tests/deposit_transfer_tests.move index 973f017..5705b86 100644 --- a/tests/deposit_transfer_tests.move +++ b/tests/deposit_transfer_tests.move @@ -2,7 +2,6 @@ module bridge_safe::deposit_transfer_tests; use bridge_safe::bridge_roles::BridgeCap; -use bridge_safe::mint_burn_coin::{Self as mbc, MINT_BURN_COIN}; use bridge_safe::pausable; use bridge_safe::safe::{Self, BridgeSafe}; use locked_token::bridge_token::{Self as br, BRIDGE_TOKEN}; @@ -904,241 +903,3 @@ fun test_deposit_then_transfer_integration() { ts::end(scenario); } -// ============================================================ -// Mint-burn token tests -// ============================================================ - -/// Sets up a scenario with both bridge-token infrastructure and a whitelisted -/// mint-burn token whose TreasuryCap is registered in the safe. -/// Pre-mints DEPOSIT_AMOUNT * 2 coins and sends them to USER. -fun setup_mint_burn(): Scenario { - let mut s = setup(); - - // Initialize MINT_BURN_COIN currency — TreasuryCap goes to ADMIN - s.next_tx(ADMIN); - mbc::init_for_testing(ts::ctx(&mut s)); - - // Whitelist, flip is_mint_burn, pre-mint test coins, then register cap - s.next_tx(ADMIN); - { - let mut safe = ts::take_shared(&s); - - safe::whitelist_token( - &mut safe, - MIN_AMOUNT, - MAX_AMOUNT, - false, // is_native - false, // is_locked - ts::ctx(&mut s), - ); - safe::set_token_is_mint_burn(&mut safe, true, ts::ctx(&mut s)); - - // Mint coins now (before handing cap to safe) so supply tracking is consistent - let mut treasury_cap = ts::take_from_address>(&s, ADMIN); - let pre_minted = coin::mint(&mut treasury_cap, DEPOSIT_AMOUNT * 2, ts::ctx(&mut s)); - transfer::public_transfer(pre_minted, USER); - - safe::register_mint_burn_cap(&mut safe, treasury_cap, ts::ctx(&mut s)); - - ts::return_shared(safe); - }; - - s -} - -#[test] -fun test_deposit_mint_burn_burns_coin() { - let mut scenario = setup_mint_burn(); - - scenario.next_tx(USER); - { - let mut safe = ts::take_shared(&scenario); - let clock = clock::create_for_testing(ts::ctx(&mut scenario)); - - let full_coin = ts::take_from_address>(&scenario, USER); - let mut rest = full_coin; - let deposit_coin = coin::split(&mut rest, DEPOSIT_AMOUNT, ts::ctx(&mut scenario)); - transfer::public_transfer(rest, USER); - - safe::deposit( - &mut safe, - deposit_coin, - RECIPIENT_VECTOR, - &clock, - ts::ctx(&mut scenario), - ); - - // Coin must NOT be stored in the bag — it was burned - assert!(safe::get_coin_storage_balance(&safe) == 0, 0); - // total_balance tracks the burned amount available for future minting - assert!(safe::get_stored_coin_balance(&mut safe) == DEPOSIT_AMOUNT, 1); - assert!(safe::get_deposits_count(&safe) == 1, 2); - - clock::destroy_for_testing(clock); - ts::return_shared(safe); - }; - - ts::end(scenario); -} - -#[test] -fun test_transfer_mint_burn_mints_coin() { - let mut scenario = setup_mint_burn(); - - // Deposit first - scenario.next_tx(USER); - { - let mut safe = ts::take_shared(&scenario); - let clock = clock::create_for_testing(ts::ctx(&mut scenario)); - let full_coin = ts::take_from_address>(&scenario, USER); - let mut rest = full_coin; - let deposit_coin = coin::split(&mut rest, DEPOSIT_AMOUNT, ts::ctx(&mut scenario)); - transfer::public_transfer(rest, USER); - safe::deposit(&mut safe, deposit_coin, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario)); - clock::destroy_for_testing(clock); - ts::return_shared(safe); - }; - - // Execute transfer — bridge mints fresh coins for the recipient - scenario.next_tx(ADMIN); - { - let mut safe = ts::take_shared(&scenario); - let mut treasury = ts::take_shared>(&scenario); - let bridge_cap = ts::take_from_address(&scenario, ADMIN); - - let success = safe::transfer( - &mut safe, - &bridge_cap, - RECIPIENT, - DEPOSIT_AMOUNT, - &mut treasury, - ts::ctx(&mut scenario), - ); - - assert!(success, 0); - assert!(safe::get_stored_coin_balance(&mut safe) == 0, 1); - assert!(safe::get_coin_storage_balance(&safe) == 0, 2); - - ts::return_shared(safe); - ts::return_shared(treasury); - ts::return_to_address(ADMIN, bridge_cap); - }; - - // Verify recipient received the freshly minted coin - scenario.next_tx(RECIPIENT); - { - let received = ts::take_from_address>(&scenario, RECIPIENT); - assert!(coin::value(&received) == DEPOSIT_AMOUNT, 3); - ts::return_to_address(RECIPIENT, received); - }; - - ts::end(scenario); -} - -#[test] -#[expected_failure(abort_code = safe::EMintBurnCapNotFound)] -fun test_deposit_mint_burn_no_cap_fails() { - let mut scenario = setup(); - - scenario.next_tx(ADMIN); - { - let mut safe = ts::take_shared(&scenario); - safe::whitelist_token( - &mut safe, MIN_AMOUNT, MAX_AMOUNT, false, false, ts::ctx(&mut scenario), - ); - safe::set_token_is_mint_burn(&mut safe, true, ts::ctx(&mut scenario)); - // Intentionally NOT registering the TreasuryCap - ts::return_shared(safe); - }; - - scenario.next_tx(USER); - { - let mut safe = ts::take_shared(&scenario); - let clock = clock::create_for_testing(ts::ctx(&mut scenario)); - let coin_in = coin::mint_for_testing(DEPOSIT_AMOUNT, ts::ctx(&mut scenario)); - // Must abort with EMintBurnCapNotFound - safe::deposit(&mut safe, coin_in, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario)); - clock::destroy_for_testing(clock); - ts::return_shared(safe); - }; - - ts::end(scenario); -} - -#[test] -fun test_transfer_mint_burn_exceeds_burned_fails() { - let mut scenario = setup_mint_burn(); - - scenario.next_tx(USER); - { - let mut safe = ts::take_shared(&scenario); - let clock = clock::create_for_testing(ts::ctx(&mut scenario)); - let full_coin = ts::take_from_address>(&scenario, USER); - let mut rest = full_coin; - let deposit_coin = coin::split(&mut rest, DEPOSIT_AMOUNT, ts::ctx(&mut scenario)); - transfer::public_transfer(rest, USER); - safe::deposit(&mut safe, deposit_coin, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario)); - clock::destroy_for_testing(clock); - ts::return_shared(safe); - }; - - // Attempt to transfer one more than was burned — must return false - scenario.next_tx(ADMIN); - { - let mut safe = ts::take_shared(&scenario); - let mut treasury = ts::take_shared>(&scenario); - let bridge_cap = ts::take_from_address(&scenario, ADMIN); - - let success = safe::transfer( - &mut safe, - &bridge_cap, - RECIPIENT, - DEPOSIT_AMOUNT + 1, - &mut treasury, - ts::ctx(&mut scenario), - ); - assert!(!success, 0); - - ts::return_shared(safe); - ts::return_shared(treasury); - ts::return_to_address(ADMIN, bridge_cap); - }; - - ts::end(scenario); -} - -#[test] -#[expected_failure(abort_code = safe::EIncompatibleTokenFlags)] -fun test_incompatible_token_flags_native_and_mint_burn() { - let mut scenario = setup(); - - scenario.next_tx(ADMIN); - { - let mut safe = ts::take_shared(&scenario); - // Whitelist as native first - safe::whitelist_token( - &mut safe, MIN_AMOUNT, MAX_AMOUNT, true, false, ts::ctx(&mut scenario), - ); - // Setting mint-burn on a native token must abort with EIncompatibleTokenFlags - safe::set_token_is_mint_burn(&mut safe, true, ts::ctx(&mut scenario)); - ts::return_shared(safe); - }; - - ts::end(scenario); -} - -#[test] -#[expected_failure(abort_code = safe::ETokenStillWhitelisted)] -fun test_deregister_cap_requires_dewhitelisting() { - let mut scenario = setup_mint_burn(); - - scenario.next_tx(ADMIN); - { - let mut safe = ts::take_shared(&scenario); - // Token is still whitelisted — must abort with ETokenStillWhitelisted - safe::deregister_mint_burn_cap(&mut safe, ts::ctx(&mut scenario)); - ts::return_shared(safe); - }; - - ts::end(scenario); -} diff --git a/tests/shared_structs_tests.move b/tests/shared_structs_tests.move index 8a254e5..64d9639 100644 --- a/tests/shared_structs_tests.move +++ b/tests/shared_structs_tests.move @@ -12,7 +12,8 @@ fun test_token_config_is_mint_burn() { false, 100, 1000, - false + false, + false, ); shared_structs::set_token_config_is_mint_burn(&mut config, true); @@ -45,7 +46,8 @@ fun test_subtract_from_token_config_total_balance() { false, 100, 1000, - false + false, + false, ); shared_structs::add_to_token_config_total_balance(&mut config, 500); @@ -66,7 +68,8 @@ fun test_subtract_from_token_config_total_balance_underflow() { false, 100, 1000, - false + false, + false, ); shared_structs::subtract_from_token_config_total_balance(&mut config, 1); @@ -80,7 +83,8 @@ fun test_subtract_from_token_config_total_balance_insufficient_funds() { false, 100, 1000, - false + false, + false, ); shared_structs::add_to_token_config_total_balance(&mut config, 100); @@ -95,7 +99,8 @@ fun test_add_to_token_config_total_balance() { false, 100, 1000, - false + false, + false, ); assert!(shared_structs::token_config_total_balance(&config) == 0, 0); @@ -115,7 +120,8 @@ fun test_add_to_token_config_total_balance_overflow() { false, 100, 1000, - false + false, + false, ); shared_structs::add_to_token_config_total_balance(&mut config, MAX_U64); @@ -131,7 +137,8 @@ fun test_add_to_token_config_total_balance_near_max_overflow() { false, 100, 1000, - false + false, + false, ); shared_structs::add_to_token_config_total_balance(&mut config, MAX_U64 - 5); @@ -146,7 +153,8 @@ fun test_set_token_config_is_native() { false, 100, 1000, - false + false, + false, ); assert!(shared_structs::token_config_is_native(&config) == false, 0); @@ -165,7 +173,8 @@ fun test_set_token_config_is_locked() { false, 100, 1000, - false + false, + false, ); @@ -248,7 +257,8 @@ fun test_combined_operations() { false, 100, 1000, - false + false, + false, ); let mut batch = shared_structs::create_batch(42, 5000); @@ -276,4 +286,4 @@ fun test_combined_operations() { shared_structs::add_to_token_config_total_balance(&mut config, 100); assert!(shared_structs::token_config_total_balance(&config) == 400, 9); -} \ No newline at end of file +} From 23dd42bff007ae1bdade980a5570aa34db339613 Mon Sep 17 00:00:00 2001 From: Cristi Corcoveanu Date: Tue, 10 Mar 2026 15:22:25 +0200 Subject: [PATCH 04/30] correctly use MintCap --- Move.toml | 1 + sources/bridge_module.move | 10 ++++++++-- sources/safe.move | 23 +++++++++++++---------- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/Move.toml b/Move.toml index 4c1461a..2223401 100644 --- a/Move.toml +++ b/Move.toml @@ -1,6 +1,7 @@ [package] name = "mx-bridge-sc-sui" edition = "2024.beta" +version = "0.0.1" [dependencies] locked_token = { git = "https://github.com/multiversx/mx-locked-token-sc-sui.git", rev = "dependency-fix", subdir = "" } diff --git a/sources/bridge_module.move b/sources/bridge_module.move index 30dfc2f..a519aa4 100644 --- a/sources/bridge_module.move +++ b/sources/bridge_module.move @@ -14,6 +14,8 @@ use bridge_safe::utils; use bridge_safe::bridge_version_control; use locked_token::bridge_token::BRIDGE_TOKEN; use locked_token::treasury; +use treasury::treasury::Treasury as XmnTreasury; +use sui::deny_list::DenyList; use shared_structs::shared_structs::{Self, Deposit, Batch, CrossTransferStatus, DepositStatus}; use std::u64::{min, max}; use sui::address; @@ -270,6 +272,8 @@ public fun execute_transfer( signatures: vector>, is_batch_complete: bool, treasury: &mut treasury::Treasury, + xmn_treasury: &mut XmnTreasury, + deny_list: &DenyList, clock: &Clock, ctx: &mut TxContext, ) { @@ -306,7 +310,7 @@ public fun execute_transfer( let recipient = *vector::borrow(&recipients, i); let amount = *vector::borrow(&amounts, i); - let success = safe::transfer(safe, &bridge.bridge_cap, recipient, amount, treasury, ctx); + let success = safe::transfer(safe, &bridge.bridge_cap, recipient, amount, treasury, xmn_treasury, deny_list, ctx); if (success) { vector::push_back( &mut bridge.transfer_statuses, @@ -554,6 +558,8 @@ public fun execute_transfer_for_testing( batch_nonce_mvx: u64, is_batch_complete: bool, treasury: &mut treasury::Treasury, + xmn_treasury: &mut XmnTreasury, + deny_list: &DenyList, clock: &Clock, ctx: &mut TxContext, ) { @@ -572,7 +578,7 @@ public fun execute_transfer_for_testing( let recipient = *vector::borrow(&recipients, i); let amount = *vector::borrow(&amounts, i); - let success = safe::transfer(safe, &bridge.bridge_cap, recipient, amount, treasury, ctx); + let success = safe::transfer(safe, &bridge.bridge_cap, recipient, amount, treasury, xmn_treasury, deny_list, ctx); if (success) { vector::push_back( &mut bridge.transfer_statuses, diff --git a/sources/safe.move b/sources/safe.move index 6e530b0..98b1bbc 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -13,7 +13,8 @@ use bridge_safe::upgrade_service_bridge; use bridge_safe::utils; use locked_token::bridge_token::BRIDGE_TOKEN; use locked_token::treasury; -use treasury::treasury::{MintCap, Treasury as XmnTreasury}; +use treasury::treasury::{Self as stablecoin_treasury, MintCap, Treasury as XmnTreasury}; +use sui::deny_list::DenyList; use shared_structs::shared_structs::{Self, TokenConfig, Batch, Deposit}; use std::u64::{min, max}; use sui::bag::{Self, Bag}; @@ -340,8 +341,7 @@ public fun set_token_is_mint_burn( events::emit_token_is_mint_burn_updated(key, is_mint_burn); } -/// Register a legacy coin::TreasuryCap for a mint-burn token. -/// Signature is unchanged from the v2 deployment for upgrade compatibility. +/// Register a MintCap for a mint-burn token. public fun register_mint_burn_cap( safe: &mut BridgeSafe, cap: MintCap, @@ -448,6 +448,8 @@ public fun deposit( coin_in: Coin, recipient: vector, clock: &Clock, + xmn_treasury: &mut XmnTreasury, + deny_list: &DenyList, ctx: &mut TxContext, ) { pausable::assert_not_paused(&safe.pause); @@ -467,7 +469,7 @@ public fun deposit( if (is_mint_burn) { let cap_key = MintBurnCapKey { token_type: key }; assert!( - dof::exists_with_type>(&safe.id, cap_key), + dof::exists_with_type>(&safe.id, cap_key), EMintBurnCapNotFound, ); }; @@ -507,8 +509,8 @@ public fun deposit( if (is_mint_burn) { let cap_key = MintBurnCapKey { token_type: key }; - let cap = dof::borrow_mut>(&mut safe.id, cap_key); - let _ = coin::burn(cap, coin_in); + let cap = dof::borrow>(&safe.id, cap_key); + stablecoin_treasury::burn(xmn_treasury, cap, deny_list, coin_in, ctx); } else { if (bag::contains(&safe.coin_storage, key)) { let existing_coin = bag::borrow_mut, Coin>(&mut safe.coin_storage, key); @@ -667,6 +669,8 @@ public(package) fun transfer( receiver: address, amount: u64, treasury: &mut treasury::Treasury, + xmn_treasury: &mut XmnTreasury, + deny_list: &DenyList, ctx: &mut TxContext, ): bool { let key = utils::type_name_bytes(); @@ -690,12 +694,11 @@ public(package) fun transfer( if (is_mint_burn) { let cap_key = MintBurnCapKey { token_type: key }; - if (!dof::exists_with_type>(&safe.id, cap_key)) { + if (!dof::exists_with_type>(&safe.id, cap_key)) { return false }; - let cap = dof::borrow_mut>(&mut safe.id, cap_key); - let minted_coin = coin::mint(cap, amount, ctx); - transfer::public_transfer(minted_coin, receiver); + let cap = dof::borrow>(&safe.id, cap_key); + stablecoin_treasury::mint(xmn_treasury, cap, deny_list, amount, receiver, ctx); let cfg_mut = borrow_token_cfg_mut(safe, key); shared_structs::subtract_from_token_config_total_balance(cfg_mut, amount); return true From 96891c8eb218f42a61a17246a1faf90d28de27a2 Mon Sep 17 00:00:00 2001 From: Cristi Corcoveanu Date: Wed, 11 Mar 2026 13:25:14 +0200 Subject: [PATCH 05/30] first impl of mint/burn with is_locked logic removed --- sources/bridge_module.move | 196 +++++++++++++++++++- sources/events.move | 12 -- sources/safe.move | 248 ++++++++++++++++---------- sources/shared_structs.move | 25 ++- tests/bridge_comprehensive_tests.move | 68 +------ tests/deposit_transfer_tests.move | 71 +------- tests/events_tests.move | 37 +--- tests/safe_edge_case_tests.move | 30 +--- tests/safe_internal_tests.move | 49 +---- tests/security_tests.move | 21 +-- tests/shared_structs_tests.move | 45 ++--- tests/upgrade_tests.move | 17 +- 12 files changed, 374 insertions(+), 445 deletions(-) diff --git a/sources/bridge_module.move b/sources/bridge_module.move index a519aa4..333b282 100644 --- a/sources/bridge_module.move +++ b/sources/bridge_module.move @@ -12,8 +12,6 @@ use bridge_safe::pausable::{Self, Pause}; use bridge_safe::safe::{Self, BridgeSafe}; use bridge_safe::utils; use bridge_safe::bridge_version_control; -use locked_token::bridge_token::BRIDGE_TOKEN; -use locked_token::treasury; use treasury::treasury::Treasury as XmnTreasury; use sui::deny_list::DenyList; use shared_structs::shared_structs::{Self, Deposit, Batch, CrossTransferStatus, DepositStatus}; @@ -271,7 +269,109 @@ public fun execute_transfer( batch_nonce_mvx: u64, signatures: vector>, is_batch_complete: bool, - treasury: &mut treasury::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( + 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; + } else { + table::add(&mut bridge.execution_timestamps, 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, 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(), + ); + }; + 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, + }); + }; +} + +public fun execute_transfer_mint_burn( + 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, @@ -310,14 +410,13 @@ public fun execute_transfer( let recipient = *vector::borrow(&recipients, i); let amount = *vector::borrow(&amounts, i); - let success = safe::transfer(safe, &bridge.bridge_cap, recipient, amount, treasury, xmn_treasury, deny_list, ctx); + let success = safe::transfer_mint_burn(safe, &bridge.bridge_cap, recipient, amount, xmn_treasury, deny_list, 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, @@ -557,7 +656,88 @@ public fun execute_transfer_for_testing( amounts: vector, batch_nonce_mvx: u64, is_batch_complete: bool, - treasury: &mut treasury::Treasury, + clock: &Clock, + ctx: &mut TxContext, +) { + 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 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, ctx); + if (success) { + vector::push_back( + &mut bridge.transfer_statuses, + shared_structs::deposit_status_executed(), + ); + + 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(), + ); + }; + 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, + }); + }; +} + +#[test_only] +public fun execute_transfer_mint_burn_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, @@ -578,14 +758,13 @@ public fun execute_transfer_for_testing( let recipient = *vector::borrow(&recipients, i); let amount = *vector::borrow(&amounts, i); - let success = safe::transfer(safe, &bridge.bridge_cap, recipient, amount, treasury, xmn_treasury, deny_list, ctx); + let success = safe::transfer_mint_burn(safe, &bridge.bridge_cap, recipient, amount, xmn_treasury, deny_list, 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, @@ -600,7 +779,6 @@ public fun execute_transfer_for_testing( &mut bridge.transfer_statuses, shared_structs::deposit_status_rejected(), ); - }; i = i + 1; }; diff --git a/sources/events.move b/sources/events.move index 98ffc12..f740ec0 100644 --- a/sources/events.move +++ b/sources/events.move @@ -46,7 +46,6 @@ public struct TokenWhitelisted has copy, drop { max_limit: u64, is_native: bool, is_mint_burn: bool, - is_locked: bool, } public struct TokenRemovedFromWhitelist has copy, drop { @@ -64,11 +63,6 @@ public struct TokenIsNativeUpdated has copy, drop { is_native: bool, } -public struct TokenIsLockedUpdated has copy, drop { - token_type: vector, - is_locked: bool, -} - public struct TokenIsMintBurnUpdated has copy, drop { token_type: vector, is_mint_burn: bool, @@ -136,7 +130,6 @@ public(package) fun emit_token_whitelisted( max_limit: u64, is_native: bool, is_mint_burn: bool, - is_locked: bool, ) { event::emit(TokenWhitelisted { token_type, @@ -144,7 +137,6 @@ public(package) fun emit_token_whitelisted( max_limit, is_native, is_mint_burn, - is_locked, }); } @@ -164,10 +156,6 @@ public(package) fun emit_token_is_native_updated(token_type: vector, is_nati event::emit(TokenIsNativeUpdated { token_type, is_native }); } -public(package) fun emit_token_is_locked_updated(token_type: vector, is_locked: bool) { - event::emit(TokenIsLockedUpdated { token_type, is_locked }); -} - public(package) fun emit_token_is_mint_burn_updated(token_type: vector, is_mint_burn: bool) { event::emit(TokenIsMintBurnUpdated { token_type, is_mint_burn }); } diff --git a/sources/safe.move b/sources/safe.move index 98b1bbc..8e80293 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -11,8 +11,6 @@ use bridge_safe::events; 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 treasury::treasury::{Self as stablecoin_treasury, MintCap, Treasury as XmnTreasury}; use sui::deny_list::DenyList; use shared_structs::shared_structs::{Self, TokenConfig, Batch, Deposit}; @@ -88,14 +86,13 @@ public struct BridgeSafe has key { batches: Table, batch_deposits: Table>, coin_storage: Bag, - from_coin_cap: treasury::FromCoinCap, compatible_versions: VecSet, } public struct SAFE has drop {} #[allow(lint(self_transfer))] -public fun initialize(from_coin_cap: treasury::FromCoinCap, ctx: &mut TxContext) { +public fun initialize(ctx: &mut TxContext) { let deployer = tx_context::sender(ctx); let w = bridge_roles::grant_witness(); let (bridge_cap) = bridge_roles::publish_caps(w, ctx); @@ -114,7 +111,6 @@ public fun initialize(from_coin_cap: treasury::FromCoinCap, ctx: & batches: table::new(ctx), batch_deposits: table::new(ctx), coin_storage: bag::new(ctx), - from_coin_cap, compatible_versions: vec_set::singleton(bridge_version_control::current_version()), }; @@ -142,7 +138,7 @@ fun whitelist_token_internal( minimum_amount: u64, maximum_amount: u64, is_native: bool, - is_locked: bool, + treasury_id: Option, is_mint_burn: bool, ctx: &mut TxContext, ) { @@ -157,7 +153,7 @@ fun whitelist_token_internal( if (exists) { assert_token_is_not_whitelisted(safe, key); }; - + shared_structs::upsert_token_config( &mut safe.token_cfg, key, @@ -165,7 +161,7 @@ fun whitelist_token_internal( is_native, minimum_amount, maximum_amount, - is_locked, + treasury_id, is_mint_burn, ); @@ -175,7 +171,6 @@ fun whitelist_token_internal( maximum_amount, is_native, is_mint_burn, - is_locked, ); } @@ -183,20 +178,19 @@ public fun whitelist_token( safe: &mut BridgeSafe, minimum_amount: u64, maximum_amount: u64, - is_native: bool, - is_locked: bool, ctx: &mut TxContext, ) { - whitelist_token_internal(safe, minimum_amount, maximum_amount, is_native, is_locked, false, ctx); + whitelist_token_internal(safe, minimum_amount, maximum_amount, true, option::none(), false, ctx); } public fun whitelist_token_mint_burn( safe: &mut BridgeSafe, minimum_amount: u64, maximum_amount: u64, + treasury_id: ID, ctx: &mut TxContext, ) { - whitelist_token_internal(safe, minimum_amount, maximum_amount, false, false, true, ctx); + whitelist_token_internal(safe, minimum_amount, maximum_amount, false, option::some(treasury_id), true, ctx); } public fun remove_token_from_whitelist(safe: &mut BridgeSafe, ctx: &mut TxContext) { @@ -313,16 +307,6 @@ public fun set_token_is_native(safe: &mut BridgeSafe, is_native: bool, ctx: & events::emit_token_is_native_updated(key, is_native); } -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); - - let key = utils::type_name_bytes(); - let cfg = borrow_token_cfg_mut(safe, key); - shared_structs::set_token_config_is_locked(cfg, is_locked); - - events::emit_token_is_locked_updated(key, is_locked); -} - public fun set_token_is_mint_burn( safe: &mut BridgeSafe, is_mint_burn: bool, @@ -441,9 +425,80 @@ public fun sync_supply(safe: &mut BridgeSafe, mut coin_in: Coin, ctx: &mut } -/// 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 +/// 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, +) { + 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); + assert!(!shared_structs::token_config_is_mint_burn(cfg_ref), EIncompatibleTokenFlags); + + 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( + batch_nonce, + dep_nonce, + tx_context::sender(ctx), + recipient, + amount, + key, + ); +} + +/// Deposit function for mint-burn tokens: coin is burned immediately via the stablecoin-sui treasury. +public fun deposit_mint_burn( safe: &mut BridgeSafe, coin_in: Coin, recipient: vector, @@ -459,20 +514,18 @@ public fun deposit( let key = utils::type_name_bytes(); let cfg_ref = table::borrow(&safe.token_cfg, key); assert!(shared_structs::token_config_whitelisted(cfg_ref), ETokenNotWhitelisted); + assert!(shared_structs::token_config_is_mint_burn(cfg_ref), EIncompatibleTokenFlags); 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); - let is_mint_burn = shared_structs::token_config_is_mint_burn(cfg_ref); - if (is_mint_burn) { - let cap_key = MintBurnCapKey { token_type: key }; - assert!( - dof::exists_with_type>(&safe.id, cap_key), - EMintBurnCapNotFound, - ); - }; + let cap_key = MintBurnCapKey { token_type: key }; + assert!( + dof::exists_with_type>(&safe.id, cap_key), + EMintBurnCapNotFound, + ); if (should_create_new_batch_internal(safe, clock)) { create_new_batch_internal(safe, clock, ctx); @@ -507,18 +560,8 @@ public fun deposit( let cfg = borrow_token_cfg_mut(safe, key); shared_structs::add_to_token_config_total_balance(cfg, amount); - if (is_mint_burn) { - let cap_key = MintBurnCapKey { token_type: key }; - let cap = dof::borrow>(&safe.id, cap_key); - stablecoin_treasury::burn(xmn_treasury, cap, deny_list, coin_in, ctx); - } else { - 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); - }; - }; + let cap = dof::borrow>(&safe.id, cap_key); + stablecoin_treasury::burn(xmn_treasury, cap, deny_list, coin_in, ctx); events::emit_deposit( batch_nonce, @@ -660,17 +703,13 @@ public fun get_batch_deposits_count(batch: &Batch): u16 { shared_structs::batch_deposits_count(batch) } -/// 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 +/// 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, _bridge_cap: &bridge_roles::BridgeCap, receiver: address, amount: u64, - treasury: &mut treasury::Treasury, - xmn_treasury: &mut XmnTreasury, - deny_list: &DenyList, ctx: &mut TxContext, ): bool { let key = utils::type_name_bytes(); @@ -679,68 +718,87 @@ public(package) fun transfer( return false }; - let (is_mint_burn, is_locked, current_balance) = { + let (is_mint_burn, current_balance) = { let cfg_ref = table::borrow(&safe.token_cfg, key); ( shared_structs::token_config_is_mint_burn(cfg_ref), - shared_structs::get_token_config_is_locked(cfg_ref), shared_structs::token_config_total_balance(cfg_ref), ) }; - if (current_balance < amount) { + if (is_mint_burn) { return false }; - if (is_mint_burn) { - let cap_key = MintBurnCapKey { token_type: key }; - if (!dof::exists_with_type>(&safe.id, cap_key)) { - return false - }; - let cap = dof::borrow>(&safe.id, cap_key); - stablecoin_treasury::mint(xmn_treasury, cap, deny_list, amount, receiver, ctx); - let cfg_mut = borrow_token_cfg_mut(safe, key); - shared_structs::subtract_from_token_config_total_balance(cfg_mut, amount); - return true + if (current_balance < amount) { + return false }; if (!bag::contains(&safe.coin_storage, key)) { return false }; - if (!is_locked) { - 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 - }; + 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 + }; - let coin_to_transfer = coin::split(stored_coin, amount, ctx); + let coin_to_transfer = coin::split(stored_coin, amount, ctx); - 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); - - treasury::transfer_from_coin( - treasury, - receiver, - &safe.from_coin_cap, - coin_bt, - ctx, - ); + 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); + + let cfg_mut = borrow_token_cfg_mut(safe, key); + shared_structs::subtract_from_token_config_total_balance(cfg_mut, amount); + + true +} + +/// Transfer function for mint-burn tokens: mints fresh coin directly to receiver via the stablecoin-sui treasury. +/// Only the bridge role can call this function. +public(package) fun transfer_mint_burn( + safe: &mut BridgeSafe, + _bridge_cap: &bridge_roles::BridgeCap, + receiver: address, + amount: u64, + xmn_treasury: &mut XmnTreasury, + deny_list: &DenyList, + ctx: &mut TxContext, +): bool { + let key = utils::type_name_bytes(); + + if (!table::contains(&safe.token_cfg, key)) { + return false + }; + + let (is_mint_burn, current_balance) = { + let cfg_ref = table::borrow(&safe.token_cfg, key); + ( + shared_structs::token_config_is_mint_burn(cfg_ref), + shared_structs::token_config_total_balance(cfg_ref), + ) }; + if (!is_mint_burn) { + return false + }; + + if (current_balance < amount) { + return false + }; + + let cap_key = MintBurnCapKey { token_type: key }; + if (!dof::exists_with_type>(&safe.id, cap_key)) { + return false + }; + + let cap = dof::borrow>(&safe.id, cap_key); + stablecoin_treasury::mint(xmn_treasury, cap, deny_list, amount, receiver, ctx); + let cfg_mut = borrow_token_cfg_mut(safe, key); shared_structs::subtract_from_token_config_total_balance(cfg_mut, amount); @@ -896,8 +954,8 @@ public fun is_migration_in_progress(safe: &BridgeSafe): bool { } #[test_only] -public fun init_for_testing(from_cap: treasury::FromCoinCap, ctx: &mut TxContext) { - initialize(from_cap, ctx); +public fun init_for_testing(ctx: &mut TxContext) { + initialize(ctx); } #[test_only] diff --git a/sources/shared_structs.move b/sources/shared_structs.move index 134790d..b4a9f99 100644 --- a/sources/shared_structs.move +++ b/sources/shared_structs.move @@ -1,5 +1,6 @@ module shared_structs::shared_structs; +use sui::object::ID; use sui::table::{Self, Table}; public enum DepositStatus has copy, drop, store { @@ -37,7 +38,7 @@ public struct TokenConfig has copy, drop, store { min_limit: u64, max_limit: u64, total_balance: u64, - is_locked: bool, + treasury_id: Option, } public struct AdminRole has key { @@ -135,16 +136,12 @@ public(package) fun set_token_config_is_native(config: &mut TokenConfig, is_nati config.is_native = is_native; } -public(package) fun set_token_config_is_locked(config: &mut TokenConfig, is_locked: bool) { - config.is_locked = is_locked; -} - public(package) fun set_token_config_is_mint_burn(config: &mut TokenConfig, is_mint_burn: bool) { config.is_mint_burn = is_mint_burn; } -public fun get_token_config_is_locked(config: &TokenConfig): bool { - config.is_locked +public fun token_config_treasury_id(config: &TokenConfig): Option { + config.treasury_id } const EUnderflow: u64 = 0; @@ -201,17 +198,17 @@ public fun upsert_token_config( is_native: bool, min_limit: u64, max_limit: u64, - is_locked: bool, + treasury_id: Option, is_mint_burn: bool, ) { if (table::contains(config, key)) { let cfg = table::borrow_mut(config, key); - set_token_config(cfg, whitelisted, is_native, min_limit, max_limit, is_locked, is_mint_burn); + set_token_config(cfg, whitelisted, is_native, min_limit, max_limit, treasury_id, is_mint_burn); return; }; - let mut cfg = create_token_config(whitelisted, is_native, min_limit, max_limit, is_locked, is_mint_burn); + let cfg = create_token_config(whitelisted, is_native, min_limit, max_limit, treasury_id, is_mint_burn); table::add(config, key, cfg); } @@ -221,7 +218,7 @@ public fun set_token_config( is_native: bool, min_limit: u64, max_limit: u64, - is_locked: bool, + treasury_id: Option, is_mint_burn: bool, ) { set_token_config_whitelisted(config, whitelisted); @@ -229,7 +226,7 @@ public fun set_token_config( set_token_config_is_mint_burn(config, is_mint_burn); set_token_config_min_limit(config, min_limit); set_token_config_max_limit(config, max_limit); - set_token_config_is_locked(config, is_locked); + config.treasury_id = treasury_id; } public fun create_token_config( @@ -237,7 +234,7 @@ public fun create_token_config( is_native: bool, min_limit: u64, max_limit: u64, - is_locked: bool, + treasury_id: Option, is_mint_burn: bool, ): TokenConfig { TokenConfig { @@ -246,7 +243,7 @@ public fun create_token_config( min_limit, max_limit, total_balance: 0, - is_locked, + treasury_id, is_mint_burn, } } diff --git a/tests/bridge_comprehensive_tests.move b/tests/bridge_comprehensive_tests.move index 11f58e5..18b392d 100644 --- a/tests/bridge_comprehensive_tests.move +++ b/tests/bridge_comprehensive_tests.move @@ -4,8 +4,6 @@ module bridge_safe::bridge_comprehensive_tests; use bridge_safe::bridge::{Self, Bridge}; use bridge_safe::bridge_roles::BridgeCap; use bridge_safe::safe::{Self, BridgeSafe}; -use locked_token::bridge_token::{Self as br, BRIDGE_TOKEN}; -use locked_token::treasury::{Self as lkt, Treasury, FromCoinCap}; use sui::clock; use sui::test_scenario::{Self as ts, Scenario}; use sui_extensions::two_step_role::ESenderNotActiveRole; @@ -29,20 +27,9 @@ const PK4: vector = b"98765432109876543210987654321098"; fun setup(): Scenario { let mut s = ts::begin(ADMIN); - br::init_for_testing(s.ctx()); - - s.next_tx(ADMIN); - { - let mut treasury = s.take_shared>(); - lkt::transfer_to_coin_cap(&mut treasury, ADMIN, s.ctx()); - lkt::transfer_from_coin_cap(&mut treasury, ADMIN, s.ctx()); - ts::return_shared(treasury); - }; - s.next_tx(ADMIN); { - let from_cap_db = s.take_from_address>(ADMIN); - safe::init_for_testing(from_cap_db, s.ctx()); + safe::init_for_testing(s.ctx()); }; s @@ -61,8 +48,6 @@ fun test_initialize_bridge_success() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -145,8 +130,6 @@ fun test_set_quorum_success() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -192,8 +175,6 @@ fun test_set_quorum_not_admin() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -237,8 +218,6 @@ fun test_add_relayer_success() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -286,8 +265,6 @@ fun test_remove_relayer_success() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -341,8 +318,6 @@ fun test_remove_relayer_below_quorum() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -386,8 +361,6 @@ fun test_pause_unpause_contract() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -442,8 +415,6 @@ fun test_getter_functions() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -514,8 +485,6 @@ fun test_set_batch_settle_timeout_success() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -575,8 +544,6 @@ fun test_set_batch_settle_timeout_not_admin() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -629,8 +596,6 @@ fun test_execute_transfer_invalid_signature_length() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -655,7 +620,6 @@ fun test_execute_transfer_invalid_signature_length() { { let mut bridge = ts::take_shared(&scenario); let mut safe = ts::take_shared(&scenario); - let mut treasury = scenario.take_shared>(); let clock = clock::create_for_testing(ts::ctx(&mut scenario)); let recipients = vector[USER]; @@ -674,13 +638,11 @@ fun test_execute_transfer_invalid_signature_length() { batch_nonce_mvx, invalid_signatures, false, - &mut treasury, &clock, ts::ctx(&mut scenario), ); ts::return_shared(bridge); - ts::return_shared(treasury); ts::return_shared(safe); clock::destroy_for_testing(clock); }; @@ -702,8 +664,6 @@ fun test_execute_transfer_insufficient_signatures() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -728,7 +688,6 @@ fun test_execute_transfer_insufficient_signatures() { { let mut bridge = ts::take_shared(&scenario); let mut safe = ts::take_shared(&scenario); - let mut treasury = scenario.take_shared>(); let clock = clock::create_for_testing(ts::ctx(&mut scenario)); let recipients = vector[USER]; @@ -758,14 +717,12 @@ fun test_execute_transfer_insufficient_signatures() { batch_nonce_mvx, signatures, false, - &mut treasury, &clock, ts::ctx(&mut scenario), ); ts::return_shared(bridge); ts::return_shared(safe); - ts::return_shared(treasury); clock::destroy_for_testing(clock); }; @@ -786,8 +743,6 @@ fun test_add_relayer_invalid_public_key_length() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -833,8 +788,6 @@ fun test_add_relayer_not_admin() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -879,8 +832,6 @@ fun test_remove_relayer_not_admin() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -925,8 +876,6 @@ fun test_pause_contract_not_admin() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -971,8 +920,6 @@ fun test_unpause_contract_not_admin() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -1051,21 +998,10 @@ fun setup_bridge_with_relayers_for_quorum(): (Scenario, vector>, vect let addr4 = bridge::getAddressFromPublicKeyTest(&pk4); let relayer_addresses = vector[addr1, addr2, addr3, addr4]; - - br::init_for_testing(scenario.ctx()); - - scenario.next_tx(ADMIN); - { - let mut treasury = scenario.take_shared>(); - lkt::transfer_to_coin_cap(&mut treasury, ADMIN, scenario.ctx()); - lkt::transfer_from_coin_cap(&mut treasury, ADMIN, scenario.ctx()); - ts::return_shared(treasury); - }; scenario.next_tx(ADMIN); { - let from_cap_db = scenario.take_from_address>(ADMIN); - safe::init_for_testing(from_cap_db, scenario.ctx()); + safe::init_for_testing(scenario.ctx()); }; scenario.next_tx(ADMIN); diff --git a/tests/deposit_transfer_tests.move b/tests/deposit_transfer_tests.move index 5705b86..6c2b14f 100644 --- a/tests/deposit_transfer_tests.move +++ b/tests/deposit_transfer_tests.move @@ -4,8 +4,6 @@ module bridge_safe::deposit_transfer_tests; use bridge_safe::bridge_roles::BridgeCap; use bridge_safe::pausable; use bridge_safe::safe::{Self, BridgeSafe}; -use locked_token::bridge_token::{Self as br, BRIDGE_TOKEN}; -use locked_token::treasury::{Self as lkt, Treasury, FromCoinCap}; use sui::clock; use sui::coin; use sui::test_scenario::{Self as ts, Scenario}; @@ -27,20 +25,9 @@ const DEPOSIT_AMOUNT: u64 = 50000; fun setup(): Scenario { let mut s = ts::begin(ADMIN); - br::init_for_testing(s.ctx()); - - s.next_tx(ADMIN); - { - let mut treasury = s.take_shared>(); - lkt::transfer_to_coin_cap(&mut treasury, ADMIN, s.ctx()); - lkt::transfer_from_coin_cap(&mut treasury, ADMIN, s.ctx()); - ts::return_shared(treasury); - }; - s.next_tx(ADMIN); { - let from_cap_db = s.take_from_address>(ADMIN); - safe::init_for_testing(from_cap_db, s.ctx()); + safe::init_for_testing(s.ctx()); }; s @@ -59,8 +46,6 @@ fun test_deposit_basic() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -122,8 +107,6 @@ fun test_deposit_multiple_same_batch() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -195,8 +178,6 @@ fun test_deposit_triggers_new_batch() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -267,8 +248,6 @@ fun test_deposit_invalid_recipient() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -321,8 +300,6 @@ fun test_deposit_zero_amount() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -356,8 +333,6 @@ fun test_deposit_amount_below_minimum() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -391,8 +366,6 @@ fun test_deposit_amount_above_maximum() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -426,8 +399,6 @@ fun test_deposit_when_paused() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -463,8 +434,6 @@ fun test_transfer_basic() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -477,7 +446,6 @@ fun test_transfer_basic() { scenario.next_tx(BRIDGE); { let mut safe = ts::take_shared(&scenario); - let mut treasury = scenario.take_shared>(); let bridge_cap = ts::take_from_address(&scenario, ADMIN); // Verify initial balance @@ -489,7 +457,6 @@ fun test_transfer_basic() { &bridge_cap, RECIPIENT, DEPOSIT_AMOUNT, - &mut treasury, ts::ctx(&mut scenario), ); @@ -501,7 +468,6 @@ fun test_transfer_basic() { assert!(bag_balance == safe::get_stored_coin_balance(&mut safe), 11); ts::return_shared(safe); - ts::return_shared(treasury); ts::return_to_address(ADMIN, bridge_cap); }; @@ -528,8 +494,6 @@ fun test_transfer_exact_balance() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -543,7 +507,6 @@ fun test_transfer_exact_balance() { scenario.next_tx(BRIDGE); { let mut safe = ts::take_shared(&scenario); - let mut treasury = scenario.take_shared>(); let bridge_cap = ts::take_from_address(&scenario, ADMIN); // Transfer entire balance @@ -552,7 +515,6 @@ fun test_transfer_exact_balance() { &bridge_cap, RECIPIENT, DEPOSIT_AMOUNT, - &mut treasury, ts::ctx(&mut scenario), ); @@ -564,7 +526,6 @@ fun test_transfer_exact_balance() { assert!(bag_balance == safe::get_stored_coin_balance(&mut safe), 11); ts::return_shared(safe); - ts::return_shared(treasury); ts::return_to_address(ADMIN, bridge_cap); }; @@ -578,7 +539,6 @@ fun test_transfer_token_not_whitelisted() { scenario.next_tx(BRIDGE); { let mut safe = ts::take_shared(&scenario); - let mut treasury = scenario.take_shared>(); let bridge_cap = ts::take_from_address(&scenario, ADMIN); // Try to transfer non-whitelisted token - should return false @@ -587,7 +547,6 @@ fun test_transfer_token_not_whitelisted() { &bridge_cap, RECIPIENT, DEPOSIT_AMOUNT, - &mut treasury, ts::ctx(&mut scenario), ); @@ -598,7 +557,6 @@ fun test_transfer_token_not_whitelisted() { assert!(bag_balance == safe::get_stored_coin_balance(&mut safe), 11); ts::return_shared(safe); - ts::return_shared(treasury); ts::return_to_address(ADMIN, bridge_cap); }; @@ -618,8 +576,6 @@ fun test_transfer_token_removed_from_whitelist() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -634,7 +590,6 @@ fun test_transfer_token_removed_from_whitelist() { scenario.next_tx(BRIDGE); { let mut safe = ts::take_shared(&scenario); - let mut treasury = scenario.take_shared>(); let bridge_cap = ts::take_from_address(&scenario, ADMIN); // Try to transfer removed token - should be okay - we will use whitelisted check only for deposits @@ -643,7 +598,6 @@ fun test_transfer_token_removed_from_whitelist() { &bridge_cap, RECIPIENT, DEPOSIT_AMOUNT, - &mut treasury, ts::ctx(&mut scenario), ); @@ -654,7 +608,6 @@ fun test_transfer_token_removed_from_whitelist() { assert!(bag_balance == safe::get_stored_coin_balance(&mut safe), 11); ts::return_shared(safe); - ts::return_shared(treasury); ts::return_to_address(ADMIN, bridge_cap); }; @@ -673,8 +626,6 @@ fun test_transfer_insufficient_balance() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -688,7 +639,6 @@ fun test_transfer_insufficient_balance() { scenario.next_tx(BRIDGE); { let mut safe = ts::take_shared(&scenario); - let mut treasury = scenario.take_shared>(); let bridge_cap = ts::take_from_address(&scenario, ADMIN); // Try to transfer more than balance - should return false @@ -697,7 +647,6 @@ fun test_transfer_insufficient_balance() { &bridge_cap, RECIPIENT, DEPOSIT_AMOUNT, // Much larger than 1000 - &mut treasury, ts::ctx(&mut scenario), ); @@ -709,7 +658,6 @@ fun test_transfer_insufficient_balance() { assert!(bag_balance == safe::get_stored_coin_balance(&mut safe), 11); ts::return_shared(safe); - ts::return_shared(treasury); ts::return_to_address(ADMIN, bridge_cap); }; @@ -729,8 +677,6 @@ fun test_transfer_no_coin_storage() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -740,7 +686,6 @@ fun test_transfer_no_coin_storage() { scenario.next_tx(BRIDGE); { let mut safe = ts::take_shared(&scenario); - let mut treasury = scenario.take_shared>(); let bridge_cap = ts::take_from_address(&scenario, ADMIN); // Try to transfer when no coins stored - should return false @@ -749,7 +694,6 @@ fun test_transfer_no_coin_storage() { &bridge_cap, RECIPIENT, DEPOSIT_AMOUNT, - &mut treasury, ts::ctx(&mut scenario), ); @@ -760,7 +704,6 @@ fun test_transfer_no_coin_storage() { assert!(bag_balance == safe::get_stored_coin_balance(&mut safe), 11); ts::return_shared(safe); - ts::return_shared(treasury); ts::return_to_address(ADMIN, bridge_cap); }; @@ -779,8 +722,6 @@ fun test_transfer_multiple_partial() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -793,7 +734,6 @@ fun test_transfer_multiple_partial() { scenario.next_tx(BRIDGE); { let mut safe = ts::take_shared(&scenario); - let mut treasury = scenario.take_shared>(); let bridge_cap = ts::take_from_address(&scenario, ADMIN); // Multiple transfers @@ -802,7 +742,6 @@ fun test_transfer_multiple_partial() { &bridge_cap, RECIPIENT, 10000, - &mut treasury, ts::ctx(&mut scenario), ); let success2 = safe::transfer( @@ -810,7 +749,6 @@ fun test_transfer_multiple_partial() { &bridge_cap, RECIPIENT, 20000, - &mut treasury, ts::ctx(&mut scenario), ); let success3 = safe::transfer( @@ -818,7 +756,6 @@ fun test_transfer_multiple_partial() { &bridge_cap, RECIPIENT, 30000, - &mut treasury, ts::ctx(&mut scenario), ); @@ -832,7 +769,6 @@ fun test_transfer_multiple_partial() { assert!(bag_balance == safe::get_stored_coin_balance(&mut safe), 11); ts::return_shared(safe); - ts::return_shared(treasury); ts::return_to_address(ADMIN, bridge_cap); }; @@ -851,8 +787,6 @@ fun test_deposit_then_transfer_integration() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - false, - false, // is_locked ts::ctx(&mut scenario), ); @@ -876,7 +810,6 @@ fun test_deposit_then_transfer_integration() { scenario.next_tx(BRIDGE); { let mut safe = ts::take_shared(&scenario); - let mut treasury = scenario.take_shared>(); let bridge_cap = ts::take_from_address(&scenario, ADMIN); let success = safe::transfer( @@ -884,7 +817,6 @@ fun test_deposit_then_transfer_integration() { &bridge_cap, RECIPIENT, DEPOSIT_AMOUNT, - &mut treasury, ts::ctx(&mut scenario), ); @@ -896,7 +828,6 @@ fun test_deposit_then_transfer_integration() { assert!(bag_balance == safe::get_stored_coin_balance(&mut safe), 11); ts::return_shared(safe); - ts::return_shared(treasury); ts::return_to_address(ADMIN, bridge_cap); }; diff --git a/tests/events_tests.move b/tests/events_tests.move index 20fcc7f..835c865 100644 --- a/tests/events_tests.move +++ b/tests/events_tests.move @@ -120,8 +120,7 @@ fun test_emit_token_whitelisted() { 100, // min_limit 10000, // max_limit true, // is_native - false, // is_mint_burn - true // is_locked + false // is_mint_burn ); }; @@ -140,8 +139,7 @@ fun test_emit_token_whitelisted_all_false() { 50, // min_limit 5000, // max_limit false, // is_native - false, // is_mint_burn - false // is_locked + false // is_mint_burn ); }; @@ -160,8 +158,7 @@ fun test_emit_token_whitelisted_mint_burn() { 1, // min_limit 1000000, // max_limit false, // is_native - true, // is_mint_burn - false // is_locked + true // is_mint_burn ); }; @@ -224,32 +221,6 @@ fun test_emit_token_is_native_updated_false() { ts::end(scenario); } -#[test] -fun test_emit_token_is_locked_updated() { - let mut scenario = ts::begin(ADMIN); - - scenario.next_tx(ADMIN); - { - // Test emitting a token is locked updated event - true - events::emit_token_is_locked_updated(b"LOCKED_TOKEN", true); - }; - - ts::end(scenario); -} - -#[test] -fun test_emit_token_is_locked_updated_false() { - let mut scenario = ts::begin(ADMIN); - - scenario.next_tx(ADMIN); - { - // Test emitting a token is locked updated event - false - events::emit_token_is_locked_updated(b"UNLOCKED_TOKEN", false); - }; - - ts::end(scenario); -} - #[test] fun test_emit_token_is_mint_burn_updated() { let mut scenario = ts::begin(ADMIN); @@ -410,7 +381,7 @@ fun test_multiple_events_in_sequence() { events::emit_admin_role_transferred(ADMIN, NEW_ADMIN); events::emit_pause(true); events::emit_relayer_added(RELAYER, ADMIN); - events::emit_token_whitelisted(b"SEQ_TOKEN", 1, 1000, true, false, true); + events::emit_token_whitelisted(b"SEQ_TOKEN", 1, 1000, true, false); events::emit_batch_created(1, 100); events::emit_transfer_executed(USER, 500, b"SEQ_TOKEN", true); events::emit_pause(false); diff --git a/tests/safe_edge_case_tests.move b/tests/safe_edge_case_tests.move index 2dc0374..0fb6e7a 100644 --- a/tests/safe_edge_case_tests.move +++ b/tests/safe_edge_case_tests.move @@ -3,8 +3,6 @@ module bridge_safe::safe_edge_case_tests; use bridge_safe::pausable::{Self, EContractNotPaused}; use bridge_safe::safe::{Self, BridgeSafe}; -use locked_token::bridge_token::{Self as br, BRIDGE_TOKEN}; -use locked_token::treasury::{Self as lkt, Treasury, FromCoinCap}; use sui::clock; use sui::coin; use sui::test_scenario::{Self as ts, Scenario}; @@ -23,20 +21,9 @@ const MAX_AMOUNT: u64 = 1000000; fun setup(): Scenario { let mut s = ts::begin(ADMIN); - br::init_for_testing(s.ctx()); - - s.next_tx(ADMIN); - { - let mut treasury = s.take_shared>(); - lkt::transfer_to_coin_cap(&mut treasury, ADMIN, s.ctx()); - lkt::transfer_from_coin_cap(&mut treasury, ADMIN, s.ctx()); - ts::return_shared(treasury); - }; - s.next_tx(ADMIN); { - let from_cap_db = s.take_from_address>(ADMIN); - safe::init_for_testing(from_cap_db, s.ctx()); + safe::init_for_testing(s.ctx()); }; s @@ -95,8 +82,6 @@ fun test_multiple_token_whitelist() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -105,8 +90,6 @@ fun test_multiple_token_whitelist() { &mut safe, MIN_AMOUNT * 2, MAX_AMOUNT * 2, - false, // not native - false, // is_locked ts::ctx(&mut scenario), ); @@ -137,8 +120,6 @@ fun test_token_limit_updates() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -192,8 +173,6 @@ fun test_init_supply_zero_amount() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -212,9 +191,8 @@ fun test_init_supply_zero_amount() { ts::end(scenario); } -// Test init_supply with non-native token (should fail) +// Test init_supply with native token (whitelist_token always creates native) #[test] -#[expected_failure(abort_code = safe::ENotNativeToken)] fun test_init_supply_non_native_token() { let mut scenario = setup(); scenario.next_tx(ADMIN); @@ -226,8 +204,6 @@ fun test_init_supply_non_native_token() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - false, - false, // is_locked ts::ctx(&mut scenario), ); @@ -257,8 +233,6 @@ fun test_init_supply_removed_token() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); diff --git a/tests/safe_internal_tests.move b/tests/safe_internal_tests.move index af5bb8d..3fffa4f 100644 --- a/tests/safe_internal_tests.move +++ b/tests/safe_internal_tests.move @@ -7,8 +7,6 @@ use bridge_safe::safe::{Self, BridgeSafe}; use sui::clock; use sui::coin; use sui::test_scenario::{Self as ts, Scenario}; -use locked_token::bridge_token::{Self as br, BRIDGE_TOKEN}; -use locked_token::treasury::{Self as lkt, Treasury, FromCoinCap}; use sui_extensions::two_step_role::ESenderNotActiveRole; public struct TEST_COIN has drop {} @@ -27,20 +25,9 @@ const MAX_AMOUNT: u64 = 1000000; fun setup(): Scenario { let mut s = ts::begin(ADMIN); - br::init_for_testing(s.ctx()); - - s.next_tx(ADMIN); - { - let mut treasury = s.take_shared>(); - lkt::transfer_to_coin_cap(&mut treasury, ADMIN, s.ctx()); - lkt::transfer_from_coin_cap(&mut treasury, ADMIN, s.ctx()); - ts::return_shared(treasury); - }; - s.next_tx(ADMIN); { - let from_cap_db = s.take_from_address>(ADMIN); - safe::init_for_testing(from_cap_db, s.ctx()); + safe::init_for_testing(s.ctx()); }; s @@ -85,8 +72,6 @@ fun test_whitelist_token() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, // is_native - false, // is_locked ts::ctx(&mut scenario), ); @@ -113,8 +98,6 @@ fun test_whitelist_token_already_exists() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -123,8 +106,6 @@ fun test_whitelist_token_already_exists() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -148,8 +129,6 @@ fun test_whitelist_token_not_admin() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -170,8 +149,6 @@ fun test_remove_token_from_whitelist() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -363,8 +340,6 @@ fun test_set_token_min_limit() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -395,8 +370,6 @@ fun test_set_token_max_limit() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut scenario), ); @@ -447,8 +420,6 @@ fun test_init_supply() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, // is_native = true - false, // is_locked ts::ctx(&mut scenario), ); @@ -481,8 +452,6 @@ fun test_init_supply_multiple_times() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, // is_native = true - false, // is_locked ts::ctx(&mut scenario), ); @@ -1058,8 +1027,6 @@ fun test_sync_supply() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, ts::ctx(&mut scenario), ); @@ -1101,8 +1068,6 @@ fun test_sync_supply_exact_amount() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, ts::ctx(&mut scenario), ); @@ -1133,8 +1098,6 @@ fun test_sync_supply_no_existing_bag_entry() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, ts::ctx(&mut scenario), ); @@ -1166,8 +1129,6 @@ fun test_sync_supply_not_owner() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, ts::ctx(&mut scenario), ); @@ -1212,8 +1173,6 @@ fun test_sync_supply_no_deficit() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, ts::ctx(&mut scenario), ); @@ -1240,8 +1199,6 @@ fun test_sync_supply_insufficient_coin() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, ts::ctx(&mut scenario), ); @@ -1259,7 +1216,7 @@ fun test_sync_supply_insufficient_coin() { } #[test] -#[expected_failure(abort_code = safe::ENotNativeToken)] +#[expected_failure(abort_code = safe::EInsufficientBalance)] fun test_sync_supply_not_native() { let mut scenario = setup(); scenario.next_tx(ADMIN); @@ -1270,8 +1227,6 @@ fun test_sync_supply_not_native() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - false, - false, ts::ctx(&mut scenario), ); diff --git a/tests/security_tests.move b/tests/security_tests.move index 42ca210..0f161ee 100644 --- a/tests/security_tests.move +++ b/tests/security_tests.move @@ -4,8 +4,6 @@ module bridge_safe::security_tests; use bridge_safe::bridge::{Self, Bridge}; use bridge_safe::bridge_roles::BridgeCap; use bridge_safe::safe::{Self, BridgeSafe}; -use locked_token::bridge_token::{Self as br, BRIDGE_TOKEN}; -use locked_token::treasury::{Self as lkt, Treasury, FromCoinCap}; use sui::clock; use sui::coin; use sui::test_scenario::{Self as ts, Scenario}; @@ -29,20 +27,9 @@ const PK3: vector = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456"; fun setup(): Scenario { let mut s = ts::begin(ADMIN); - br::init_for_testing(s.ctx()); - - s.next_tx(ADMIN); - { - let mut treasury = s.take_shared>(); - lkt::transfer_to_coin_cap(&mut treasury, ADMIN, s.ctx()); - lkt::transfer_from_coin_cap(&mut treasury, ADMIN, s.ctx()); - ts::return_shared(treasury); - }; - s.next_tx(ADMIN); { - let from_cap_db = s.take_from_address>(ADMIN); - safe::init_for_testing(from_cap_db, s.ctx()); + safe::init_for_testing(s.ctx()); }; s.next_tx(ADMIN); @@ -54,8 +41,6 @@ fun setup(): Scenario { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked ts::ctx(&mut s), ); @@ -144,7 +129,6 @@ fun test_replay_allows_double_spend_with_same_deposit_nonce() { { let mut bridge = ts::take_shared(&scenario); let mut safe = ts::take_shared(&scenario); - let mut treasury = ts::take_shared>(&scenario); let clock = clock::create_for_testing(ts::ctx(&mut scenario)); // First call should succeed @@ -155,7 +139,6 @@ fun test_replay_allows_double_spend_with_same_deposit_nonce() { vector[DRAIN_AMOUNT], 1, // batch nonce false, - &mut treasury, &clock, ts::ctx(&mut scenario), ); @@ -168,7 +151,6 @@ fun test_replay_allows_double_spend_with_same_deposit_nonce() { vector[DRAIN_AMOUNT], 1, // same batch nonce false, - &mut treasury, &clock, ts::ctx(&mut scenario), ); @@ -181,7 +163,6 @@ fun test_replay_allows_double_spend_with_same_deposit_nonce() { ts::return_shared(bridge); ts::return_shared(safe); - ts::return_shared(treasury); clock::destroy_for_testing(clock); }; diff --git a/tests/shared_structs_tests.move b/tests/shared_structs_tests.move index 64d9639..da65ddc 100644 --- a/tests/shared_structs_tests.move +++ b/tests/shared_structs_tests.move @@ -12,7 +12,7 @@ fun test_token_config_is_mint_burn() { false, 100, 1000, - false, + option::none(), false, ); @@ -46,7 +46,7 @@ fun test_subtract_from_token_config_total_balance() { false, 100, 1000, - false, + option::none(), false, ); @@ -68,7 +68,7 @@ fun test_subtract_from_token_config_total_balance_underflow() { false, 100, 1000, - false, + option::none(), false, ); @@ -83,7 +83,7 @@ fun test_subtract_from_token_config_total_balance_insufficient_funds() { false, 100, 1000, - false, + option::none(), false, ); @@ -99,7 +99,7 @@ fun test_add_to_token_config_total_balance() { false, 100, 1000, - false, + option::none(), false, ); @@ -120,7 +120,7 @@ fun test_add_to_token_config_total_balance_overflow() { false, 100, 1000, - false, + option::none(), false, ); @@ -137,7 +137,7 @@ fun test_add_to_token_config_total_balance_near_max_overflow() { false, 100, 1000, - false, + option::none(), false, ); @@ -153,7 +153,7 @@ fun test_set_token_config_is_native() { false, 100, 1000, - false, + option::none(), false, ); @@ -166,29 +166,6 @@ fun test_set_token_config_is_native() { assert!(shared_structs::token_config_is_native(&config) == false, 2); } -#[test] -fun test_set_token_config_is_locked() { - let mut config = shared_structs::create_token_config( - true, - false, - 100, - 1000, - false, - false, - ); - - - assert!(shared_structs::get_token_config_is_locked(&config) == false, 0); - - - shared_structs::set_token_config_is_locked(&mut config, true); - assert!(shared_structs::get_token_config_is_locked(&config) == true, 1); - - // Set back to false - shared_structs::set_token_config_is_locked(&mut config, false); - assert!(shared_structs::get_token_config_is_locked(&config) == false, 2); -} - #[test] fun test_cross_transfer_status_statuses() { let statuses = vector[ @@ -257,19 +234,17 @@ fun test_combined_operations() { false, 100, 1000, - false, + option::none(), false, ); let mut batch = shared_structs::create_batch(42, 5000); shared_structs::set_token_config_is_native(&mut config, true); - shared_structs::set_token_config_is_locked(&mut config, true); shared_structs::set_token_config_is_mint_burn(&mut config, true); shared_structs::add_to_token_config_total_balance(&mut config, 500); - + assert!(shared_structs::token_config_is_native(&config) == true, 0); - assert!(shared_structs::get_token_config_is_locked(&config) == true, 1); assert!(shared_structs::token_config_is_mint_burn(&config) == true, 2); assert!(shared_structs::token_config_total_balance(&config) == 500, 3); diff --git a/tests/upgrade_tests.move b/tests/upgrade_tests.move index f0c3831..9b8cd32 100644 --- a/tests/upgrade_tests.move +++ b/tests/upgrade_tests.move @@ -6,8 +6,6 @@ use bridge_safe::bridge_roles::BridgeCap; use bridge_safe::safe::{Self, BridgeSafe}; use bridge_safe::upgrade_manager; use bridge_safe::bridge_version_control; -use locked_token::bridge_token::{Self as br, BRIDGE_TOKEN}; -use locked_token::treasury::{Self as lkt, Treasury, FromCoinCap}; use sui::test_scenario::{Self as ts, Scenario}; public struct TEST_COIN has drop {} @@ -25,20 +23,9 @@ const PK3: vector = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456"; fun setup(): Scenario { let mut s = ts::begin(ADMIN); - br::init_for_testing(s.ctx()); - - s.next_tx(ADMIN); - { - let mut treasury = s.take_shared>(); - lkt::transfer_to_coin_cap(&mut treasury, ADMIN, s.ctx()); - lkt::transfer_from_coin_cap(&mut treasury, ADMIN, s.ctx()); - ts::return_shared(treasury); - }; - s.next_tx(ADMIN); { - let from_cap_db = s.take_from_address>(ADMIN); - safe::init_for_testing(from_cap_db, s.ctx()); + safe::init_for_testing(s.ctx()); }; s.next_tx(ADMIN); @@ -50,8 +37,6 @@ fun setup(): Scenario { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, s.ctx(), ); From f502cf2e360af6d3307b082d039817b561e67af1 Mon Sep 17 00:00:00 2001 From: Cristi Corcoveanu Date: Wed, 11 Mar 2026 14:18:52 +0200 Subject: [PATCH 06/30] refactored deposit methods a bit --- sources/safe.move | 118 +++++++++++++--------------------------------- 1 file changed, 32 insertions(+), 86 deletions(-) diff --git a/sources/safe.move b/sources/safe.move index 8e80293..4f126b7 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -425,24 +425,26 @@ public fun sync_supply(safe: &mut BridgeSafe, mut coin_in: Coin, ctx: &mut } -/// Deposit function for native tokens: coin is stored in the safe's coin_storage bag. -public fun deposit( +/// Shared helper: validates deposit preconditions, manages batching, records the deposit, +/// and updates the token balance. Returns (key, amount, batch_nonce, dep_nonce). +/// `expect_mint_burn` drives the variant guard: false for native, true for mint-burn. +fun deposit_validate_and_record( safe: &mut BridgeSafe, - coin_in: Coin, + coin_in: &Coin, recipient: vector, + expect_mint_burn: bool, clock: &Clock, ctx: &mut TxContext, -) { +): (vector, u64, u64, u64) { 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); - assert!(!shared_structs::token_config_is_mint_burn(cfg_ref), EIncompatibleTokenFlags); + assert!(shared_structs::token_config_is_mint_burn(cfg_ref) == expect_mint_burn, EIncompatibleTokenFlags); - let amount = coin::value(&coin_in); + 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); @@ -456,18 +458,11 @@ public fun deposit( 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, - ); + 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); @@ -480,21 +475,27 @@ public fun deposit( let cfg = borrow_token_cfg_mut(safe, key); shared_structs::add_to_token_config_total_balance(cfg, amount); + (key, amount, batch_nonce, dep_nonce) +} + +/// 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, +) { + let (key, amount, batch_nonce, dep_nonce) = + deposit_validate_and_record(safe, &coin_in, recipient, false, clock, ctx); + 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); + coin::join(bag::borrow_mut, Coin>(&mut safe.coin_storage, key), coin_in); } else { bag::add(&mut safe.coin_storage, key, coin_in); }; - events::emit_deposit( - batch_nonce, - dep_nonce, - tx_context::sender(ctx), - recipient, - amount, - key, - ); + events::emit_deposit(batch_nonce, dep_nonce, tx_context::sender(ctx), recipient, amount, key); } /// Deposit function for mint-burn tokens: coin is burned immediately via the stablecoin-sui treasury. @@ -507,70 +508,15 @@ public fun deposit_mint_burn( deny_list: &DenyList, ctx: &mut TxContext, ) { - pausable::assert_not_paused(&safe.pause); - - assert!(vector::length(&recipient) == 32, EInvalidRecipient); + let cap_key = MintBurnCapKey { token_type: utils::type_name_bytes() }; + assert!(dof::exists_with_type>(&safe.id, cap_key), EMintBurnCapNotFound); - let key = utils::type_name_bytes(); - let cfg_ref = table::borrow(&safe.token_cfg, key); - assert!(shared_structs::token_config_whitelisted(cfg_ref), ETokenNotWhitelisted); - assert!(shared_structs::token_config_is_mint_burn(cfg_ref), EIncompatibleTokenFlags); + let (key, amount, batch_nonce, dep_nonce) = + deposit_validate_and_record(safe, &coin_in, recipient, true, clock, ctx); - 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); + stablecoin_treasury::burn(xmn_treasury, dof::borrow>(&safe.id, cap_key), deny_list, coin_in, ctx); - let cap_key = MintBurnCapKey { token_type: key }; - assert!( - dof::exists_with_type>(&safe.id, cap_key), - EMintBurnCapNotFound, - ); - - 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); - - let cap = dof::borrow>(&safe.id, cap_key); - stablecoin_treasury::burn(xmn_treasury, cap, deny_list, coin_in, ctx); - - events::emit_deposit( - batch_nonce, - dep_nonce, - tx_context::sender(ctx), - recipient, - amount, - key, - ); + events::emit_deposit(batch_nonce, dep_nonce, tx_context::sender(ctx), recipient, amount, key); } public(package) fun checkOwnerRole(safe: &BridgeSafe, ctx: &TxContext) { From 66242372f0fc33a2ca03f8fde4ea6fa136721ad7 Mon Sep 17 00:00:00 2001 From: Cristi Corcoveanu Date: Thu, 12 Mar 2026 14:33:36 +0200 Subject: [PATCH 07/30] first working version with unit tests --- sources/safe.move | 19 ++ tests/deposit_transfer_tests.move | 409 ++++++++++++++++++++++++++++++ 2 files changed, 428 insertions(+) diff --git a/sources/safe.move b/sources/safe.move index 4f126b7..fde4db9 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -916,3 +916,22 @@ public fun add_to_balance_for_testing(safe: &mut BridgeSafe, amount: u64) { shared_structs::add_to_token_config_total_balance(cfg_mut, amount); } +/// Test helper that performs a mint-burn deposit without calling the real treasury burn. +/// Validates all deposit rules and records the batch, but destroys the coin in-place +/// instead of burning via treasury. Use this to test deposit recording logic without +/// needing a fully configured stablecoin-sui treasury. +#[test_only] +public fun deposit_mint_burn_for_testing( + safe: &mut BridgeSafe, + coin_in: Coin, + recipient: vector, + clock: &Clock, + ctx: &mut TxContext, +) { + use sui::test_utils; + let (key, amount, batch_nonce, dep_nonce) = + deposit_validate_and_record(safe, &coin_in, recipient, true, clock, ctx); + test_utils::destroy(coin_in); + events::emit_deposit(batch_nonce, dep_nonce, tx_context::sender(ctx), recipient, amount, key); +} + diff --git a/tests/deposit_transfer_tests.move b/tests/deposit_transfer_tests.move index 6c2b14f..556cb2f 100644 --- a/tests/deposit_transfer_tests.move +++ b/tests/deposit_transfer_tests.move @@ -6,11 +6,17 @@ use bridge_safe::pausable; use bridge_safe::safe::{Self, BridgeSafe}; use sui::clock; use sui::coin; +use sui::deny_list::DenyList; use sui::test_scenario::{Self as ts, Scenario}; +use sui::test_utils; +use treasury::treasury::{Self as stablecoin_treasury, Treasury as XmnTreasury}; public struct TEST_COIN has drop {} public struct NATIVE_COIN has drop {} public struct NON_NATIVE_COIN has drop {} +public struct MINT_BURN_COIN has drop {} +// DEPOSIT_TRANSFER_TESTS matches the module name — required for OTW use in create_regulated_currency_v2 +public struct DEPOSIT_TRANSFER_TESTS has drop {} const ADMIN: address = @0xa11ce; const USER: address = @0xb0b; @@ -834,3 +840,406 @@ fun test_deposit_then_transfer_integration() { ts::end(scenario); } +// =========================== +// Mint-Burn Deposit Tests +// =========================== + +/// Whitelist MINT_BURN_COIN as a mint-burn token, using a dummy treasury ID. +/// The treasury ID is only stored for SDK reference — not validated on deposit. +fun setup_mint_burn(): Scenario { + let mut s = setup(); + s.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&s); + safe::whitelist_token_mint_burn( + &mut safe, + MIN_AMOUNT, + MAX_AMOUNT, + object::id_from_address(@0x1234), + s.ctx(), + ); + ts::return_shared(safe); + }; + s +} + +/// Set up a BridgeSafe + Treasury + DenyList. +/// Used for tests that call the real deposit_mint_burn (e.g. cap-not-registered). +fun setup_with_treasury(): Scenario { + // deny_list::create_for_test requires sender == @0x0 (system address) + let mut s = ts::begin(@0x0); + s.next_tx(@0x0); + { + sui::deny_list::create_for_testing(s.ctx()); + let otw = test_utils::create_one_time_witness(); + let (treasury_cap, deny_cap, metadata) = coin::create_regulated_currency_v2( + otw, + 6, + b"DT", + b"Deposit Transfer Test", + b"", + option::none(), + true, + s.ctx(), + ); + let t = stablecoin_treasury::new( + treasury_cap, + deny_cap, + ADMIN, + ADMIN, + ADMIN, + ADMIN, + ADMIN, + s.ctx(), + ); + transfer::public_share_object(t); + transfer::public_share_object(metadata); + }; + // BridgeSafe created by ADMIN so ADMIN is the owner for whitelist calls + s.next_tx(ADMIN); + { safe::init_for_testing(s.ctx()); }; + s +} + +#[test] +fun test_deposit_mint_burn_basic() { + let mut scenario = setup_mint_burn(); + + scenario.next_tx(USER); + { + let mut safe = ts::take_shared(&scenario); + let clock = clock::create_for_testing(ts::ctx(&mut scenario)); + let coin = coin::mint_for_testing(DEPOSIT_AMOUNT, ts::ctx(&mut scenario)); + + assert!(safe::get_deposits_count(&safe) == 0, 0); + assert!(safe::get_batches_count(&safe) == 0, 1); + + safe::deposit_mint_burn_for_testing( + &mut safe, + coin, + RECIPIENT_VECTOR, + &clock, + ts::ctx(&mut scenario), + ); + + assert!(safe::get_deposits_count(&safe) == 1, 2); + assert!(safe::get_batches_count(&safe) == 1, 3); + // Coin was burned, not stored in bag + assert!(safe::get_coin_storage_balance(&safe) == 0, 4); + // But total_balance accounting is updated + assert!(safe::get_stored_coin_balance(&mut safe) == DEPOSIT_AMOUNT, 5); + + clock::destroy_for_testing(clock); + ts::return_shared(safe); + }; + ts::end(scenario); +} + +#[test] +fun test_deposit_mint_burn_batch_data() { + let mut scenario = setup_mint_burn(); + + scenario.next_tx(USER); + { + let mut safe = ts::take_shared(&scenario); + let clock = clock::create_for_testing(ts::ctx(&mut scenario)); + let coin = coin::mint_for_testing(DEPOSIT_AMOUNT, ts::ctx(&mut scenario)); + + safe::deposit_mint_burn_for_testing( + &mut safe, + coin, + RECIPIENT_VECTOR, + &clock, + ts::ctx(&mut scenario), + ); + + let (batch, _) = safe::get_batch(&safe, 1, &clock); + assert!(safe::get_batch_nonce(&batch) == 1, 0); + assert!(safe::get_batch_deposits_count(&batch) == 1, 1); + + let (deposits, _) = safe::get_deposits(&safe, 1, &clock); + assert!(vector::length(&deposits) == 1, 2); + + clock::destroy_for_testing(clock); + ts::return_shared(safe); + }; + ts::end(scenario); +} + +#[test] +fun test_deposit_mint_burn_multiple_same_batch() { + let mut scenario = setup_mint_burn(); + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + safe::set_batch_size(&mut safe, 5, ts::ctx(&mut scenario)); + ts::return_shared(safe); + }; + + scenario.next_tx(USER); + { + let mut safe = ts::take_shared(&scenario); + let clock = clock::create_for_testing(ts::ctx(&mut scenario)); + + let coin1 = coin::mint_for_testing(1000, ts::ctx(&mut scenario)); + let coin2 = coin::mint_for_testing(2000, ts::ctx(&mut scenario)); + let coin3 = coin::mint_for_testing(3000, ts::ctx(&mut scenario)); + + safe::deposit_mint_burn_for_testing( + &mut safe, coin1, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario), + ); + safe::deposit_mint_burn_for_testing( + &mut safe, coin2, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario), + ); + safe::deposit_mint_burn_for_testing( + &mut safe, coin3, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario), + ); + + assert!(safe::get_deposits_count(&safe) == 3, 0); + assert!(safe::get_batches_count(&safe) == 1, 1); + assert!(safe::get_stored_coin_balance(&mut safe) == 6000, 2); + + let (batch, _) = safe::get_batch(&safe, 1, &clock); + assert!(safe::get_batch_deposits_count(&batch) == 3, 3); + + clock::destroy_for_testing(clock); + ts::return_shared(safe); + }; + ts::end(scenario); +} + +#[test] +fun test_deposit_mint_burn_triggers_new_batch() { + let mut scenario = setup_mint_burn(); + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + safe::set_batch_size(&mut safe, 2, ts::ctx(&mut scenario)); + ts::return_shared(safe); + }; + + scenario.next_tx(USER); + { + let mut safe = ts::take_shared(&scenario); + let clock = clock::create_for_testing(ts::ctx(&mut scenario)); + + let coin1 = coin::mint_for_testing(1000, ts::ctx(&mut scenario)); + let coin2 = coin::mint_for_testing(2000, ts::ctx(&mut scenario)); + let coin3 = coin::mint_for_testing(3000, ts::ctx(&mut scenario)); + + safe::deposit_mint_burn_for_testing( + &mut safe, coin1, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario), + ); + safe::deposit_mint_burn_for_testing( + &mut safe, coin2, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario), + ); + // Third deposit overflows batch_size=2, creates a new batch + safe::deposit_mint_burn_for_testing( + &mut safe, coin3, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario), + ); + + assert!(safe::get_deposits_count(&safe) == 3, 0); + assert!(safe::get_batches_count(&safe) == 2, 1); + + let (batch1, _) = safe::get_batch(&safe, 1, &clock); + assert!(safe::get_batch_deposits_count(&batch1) == 2, 2); + + let (batch2, _) = safe::get_batch(&safe, 2, &clock); + assert!(safe::get_batch_deposits_count(&batch2) == 1, 3); + + clock::destroy_for_testing(clock); + ts::return_shared(safe); + }; + ts::end(scenario); +} + +#[test] +#[expected_failure(abort_code = safe::EInvalidRecipient)] +fun test_deposit_mint_burn_invalid_recipient() { + let mut scenario = setup_mint_burn(); + + scenario.next_tx(USER); + { + let mut safe = ts::take_shared(&scenario); + let clock = clock::create_for_testing(ts::ctx(&mut scenario)); + let coin = coin::mint_for_testing(DEPOSIT_AMOUNT, ts::ctx(&mut scenario)); + + safe::deposit_mint_burn_for_testing( + &mut safe, coin, b"0x0", &clock, ts::ctx(&mut scenario), + ); + + clock::destroy_for_testing(clock); + ts::return_shared(safe); + }; + ts::end(scenario); +} + +#[test] +#[expected_failure(abort_code = safe::EZeroAmount)] +fun test_deposit_mint_burn_zero_amount() { + let mut scenario = setup_mint_burn(); + + scenario.next_tx(USER); + { + let mut safe = ts::take_shared(&scenario); + let clock = clock::create_for_testing(ts::ctx(&mut scenario)); + let coin = coin::mint_for_testing(0, ts::ctx(&mut scenario)); + + safe::deposit_mint_burn_for_testing( + &mut safe, coin, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario), + ); + + clock::destroy_for_testing(clock); + ts::return_shared(safe); + }; + ts::end(scenario); +} + +#[test] +#[expected_failure(abort_code = safe::EAmountBelowMinimum)] +fun test_deposit_mint_burn_below_minimum() { + let mut scenario = setup_mint_burn(); + + scenario.next_tx(USER); + { + let mut safe = ts::take_shared(&scenario); + let clock = clock::create_for_testing(ts::ctx(&mut scenario)); + let coin = coin::mint_for_testing(MIN_AMOUNT - 1, ts::ctx(&mut scenario)); + + safe::deposit_mint_burn_for_testing( + &mut safe, coin, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario), + ); + + clock::destroy_for_testing(clock); + ts::return_shared(safe); + }; + ts::end(scenario); +} + +#[test] +#[expected_failure(abort_code = safe::EAmountAboveMaximum)] +fun test_deposit_mint_burn_above_maximum() { + let mut scenario = setup_mint_burn(); + + scenario.next_tx(USER); + { + let mut safe = ts::take_shared(&scenario); + let clock = clock::create_for_testing(ts::ctx(&mut scenario)); + let coin = coin::mint_for_testing(MAX_AMOUNT + 1, ts::ctx(&mut scenario)); + + safe::deposit_mint_burn_for_testing( + &mut safe, coin, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario), + ); + + clock::destroy_for_testing(clock); + ts::return_shared(safe); + }; + ts::end(scenario); +} + +#[test] +#[expected_failure(abort_code = safe::EIncompatibleTokenFlags)] +fun test_deposit_mint_burn_wrong_variant() { + let mut scenario = setup(); + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + // Whitelist as NATIVE — calling deposit_mint_burn_for_testing should fail + safe::whitelist_token(&mut safe, MIN_AMOUNT, MAX_AMOUNT, ts::ctx(&mut scenario)); + ts::return_shared(safe); + }; + + scenario.next_tx(USER); + { + let mut safe = ts::take_shared(&scenario); + let clock = clock::create_for_testing(ts::ctx(&mut scenario)); + let coin = coin::mint_for_testing(DEPOSIT_AMOUNT, ts::ctx(&mut scenario)); + + safe::deposit_mint_burn_for_testing( + &mut safe, coin, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario), + ); + + clock::destroy_for_testing(clock); + ts::return_shared(safe); + }; + ts::end(scenario); +} + +/// Tests that deposit_mint_burn aborts when no MintCap is registered for the token. +/// Uses the real deposit_mint_burn function (not the testing bypass) with a proper +/// Treasury + DenyList. The abort happens before the treasury is accessed. +#[test] +#[expected_failure(abort_code = safe::EMintBurnCapNotFound)] +fun test_deposit_mint_burn_cap_not_registered() { + let mut scenario = setup_with_treasury(); + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + let mut treasury = ts::take_shared>(&scenario); + let deny_list = ts::take_shared(&scenario); + let clock = clock::create_for_testing(ts::ctx(&mut scenario)); + + safe::whitelist_token_mint_burn( + &mut safe, + MIN_AMOUNT, + MAX_AMOUNT, + object::id(&treasury), + ts::ctx(&mut scenario), + ); + + let coin = coin::mint_for_testing( + DEPOSIT_AMOUNT, + ts::ctx(&mut scenario), + ); + + // No MintCap registered — expect EMintBurnCapNotFound + safe::deposit_mint_burn( + &mut safe, + coin, + RECIPIENT_VECTOR, + &clock, + &mut treasury, + &deny_list, + ts::ctx(&mut scenario), + ); + + clock::destroy_for_testing(clock); + ts::return_shared(safe); + ts::return_shared(treasury); + ts::return_shared(deny_list); + }; + ts::end(scenario); +} + +#[test] +#[expected_failure(abort_code = pausable::EContractPaused)] +fun test_deposit_mint_burn_when_paused() { + let mut scenario = setup_mint_burn(); + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + safe::pause_contract(&mut safe, ts::ctx(&mut scenario)); + ts::return_shared(safe); + }; + + scenario.next_tx(USER); + { + let mut safe = ts::take_shared(&scenario); + let clock = clock::create_for_testing(ts::ctx(&mut scenario)); + let coin = coin::mint_for_testing(DEPOSIT_AMOUNT, ts::ctx(&mut scenario)); + + safe::deposit_mint_burn_for_testing( + &mut safe, coin, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario), + ); + + clock::destroy_for_testing(clock); + ts::return_shared(safe); + }; + ts::end(scenario); +} + From 44886e358c1c0c414eeec7a4c64cba4c41582694 Mon Sep 17 00:00:00 2001 From: Cristi Corcoveanu Date: Thu, 12 Mar 2026 15:08:10 +0200 Subject: [PATCH 08/30] refactored mint/burn as an adapter --- sources/bridge_module.move | 5 +- sources/safe.move | 129 +++++------------------------- tests/deposit_transfer_tests.move | 5 +- 3 files changed, 26 insertions(+), 113 deletions(-) diff --git a/sources/bridge_module.move b/sources/bridge_module.move index 333b282..9774575 100644 --- a/sources/bridge_module.move +++ b/sources/bridge_module.move @@ -12,6 +12,7 @@ use bridge_safe::pausable::{Self, Pause}; use bridge_safe::safe::{Self, BridgeSafe}; use bridge_safe::utils; use bridge_safe::bridge_version_control; +use bridge_safe::xmn_mint_cap_adapter; use treasury::treasury::Treasury as XmnTreasury; use sui::deny_list::DenyList; use shared_structs::shared_structs::{Self, Deposit, Batch, CrossTransferStatus, DepositStatus}; @@ -410,7 +411,7 @@ public fun execute_transfer_mint_burn( let recipient = *vector::borrow(&recipients, i); let amount = *vector::borrow(&amounts, i); - let success = safe::transfer_mint_burn(safe, &bridge.bridge_cap, recipient, amount, xmn_treasury, deny_list, ctx); + let success = xmn_mint_cap_adapter::transfer_mint_burn(safe, &bridge.bridge_cap, recipient, amount, xmn_treasury, deny_list, ctx); if (success) { vector::push_back( &mut bridge.transfer_statuses, @@ -758,7 +759,7 @@ public fun execute_transfer_mint_burn_for_testing( let recipient = *vector::borrow(&recipients, i); let amount = *vector::borrow(&amounts, i); - let success = safe::transfer_mint_burn(safe, &bridge.bridge_cap, recipient, amount, xmn_treasury, deny_list, ctx); + let success = xmn_mint_cap_adapter::transfer_mint_burn(safe, &bridge.bridge_cap, recipient, amount, xmn_treasury, deny_list, ctx); if (success) { vector::push_back( &mut bridge.transfer_statuses, diff --git a/sources/safe.move b/sources/safe.move index fde4db9..0f09fa7 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -11,24 +11,16 @@ use bridge_safe::events; use bridge_safe::pausable::{Self, Pause}; use bridge_safe::upgrade_service_bridge; use bridge_safe::utils; -use treasury::treasury::{Self as stablecoin_treasury, MintCap, Treasury as XmnTreasury}; -use sui::deny_list::DenyList; use shared_structs::shared_structs::{Self, TokenConfig, Batch, Deposit}; +use sui::object::UID; use std::u64::{min, max}; use sui::bag::{Self, Bag}; use sui::clock::{Self, Clock}; use sui::coin::{Self, Coin}; -use sui::dynamic_object_field as dof; use sui::event; use sui::table::{Self, Table}; use sui::vec_set::{Self, VecSet}; -// === Structs === - -public struct MintBurnCapKey has copy, drop, store { - token_type: vector, -} - // === Migration Events === public struct MigrationStarted has copy, drop { @@ -64,8 +56,6 @@ const EMigrationStarted: u64 = 16; const EMigrationNotStarted: u64 = 17; const ENotPendingVersion: u64 = 18; const ENotNativeToken: u64 = 19; -const EMintBurnCapNotFound: u64 = 20; -const EMintBurnCapAlreadyRegistered: u64 = 21; const EIncompatibleTokenFlags: u64 = 22; const MAX_U64: u64 = 18446744073709551615; @@ -325,35 +315,6 @@ public fun set_token_is_mint_burn( events::emit_token_is_mint_burn_updated(key, is_mint_burn); } -/// Register a MintCap for a mint-burn token. -public fun register_mint_burn_cap( - safe: &mut BridgeSafe, - cap: MintCap, - ctx: &TxContext, -) { - safe.roles.owner_role().assert_sender_is_active_role(ctx); - let key = utils::type_name_bytes(); - assert_token_is_whitelisted(safe, key); - assert_token_is_mint_burn(safe, key); - let cap_key = MintBurnCapKey { token_type: key }; - assert!(!dof::exists_(&safe.id, cap_key), EMintBurnCapAlreadyRegistered); - dof::add(&mut safe.id, cap_key, cap); -} - -#[allow(lint(self_transfer))] -public fun deregister_mint_burn_cap(safe: &mut BridgeSafe, ctx: &TxContext) { - safe.roles.owner_role().assert_sender_is_active_role(ctx); - let key = utils::type_name_bytes(); - assert_token_is_not_whitelisted(safe, key); - let cap_key = MintBurnCapKey { token_type: key }; - assert!( - dof::exists_with_type>(&safe.id, cap_key), - EMintBurnCapNotFound, - ); - let cap = dof::remove>(&mut safe.id, cap_key); - transfer::public_transfer(cap, ctx.sender()); -} - public fun set_bridge_addr(safe: &mut BridgeSafe, new_bridge_addr: address, ctx: &TxContext) { safe.roles.owner_role().assert_sender_is_active_role(ctx); @@ -428,7 +389,7 @@ public fun sync_supply(safe: &mut BridgeSafe, mut coin_in: Coin, ctx: &mut /// Shared helper: validates deposit preconditions, manages batching, records the deposit, /// and updates the token balance. Returns (key, amount, batch_nonce, dep_nonce). /// `expect_mint_burn` drives the variant guard: false for native, true for mint-burn. -fun deposit_validate_and_record( +public(package) fun deposit_validate_and_record( safe: &mut BridgeSafe, coin_in: &Coin, recipient: vector, @@ -498,29 +459,26 @@ public fun deposit( events::emit_deposit(batch_nonce, dep_nonce, tx_context::sender(ctx), recipient, amount, key); } -/// Deposit function for mint-burn tokens: coin is burned immediately via the stablecoin-sui treasury. -public fun deposit_mint_burn( - safe: &mut BridgeSafe, - coin_in: Coin, - recipient: vector, - clock: &Clock, - xmn_treasury: &mut XmnTreasury, - deny_list: &DenyList, - ctx: &mut TxContext, -) { - let cap_key = MintBurnCapKey { token_type: utils::type_name_bytes() }; - assert!(dof::exists_with_type>(&safe.id, cap_key), EMintBurnCapNotFound); +public(package) fun checkOwnerRole(safe: &BridgeSafe, ctx: &TxContext) { + safe.roles.owner_role().assert_sender_is_active_role(ctx); +} - let (key, amount, batch_nonce, dep_nonce) = - deposit_validate_and_record(safe, &coin_in, recipient, true, clock, ctx); +public(package) fun uid(safe: &BridgeSafe): &UID { + &safe.id +} - stablecoin_treasury::burn(xmn_treasury, dof::borrow>(&safe.id, cap_key), deny_list, coin_in, ctx); +public(package) fun uid_mut(safe: &mut BridgeSafe): &mut UID { + &mut safe.id +} - events::emit_deposit(batch_nonce, dep_nonce, tx_context::sender(ctx), recipient, amount, key); +public(package) fun has_token_config(safe: &BridgeSafe): bool { + table::contains(&safe.token_cfg, utils::type_name_bytes()) } -public(package) fun checkOwnerRole(safe: &BridgeSafe, ctx: &TxContext) { - safe.roles.owner_role().assert_sender_is_active_role(ctx); +public(package) fun subtract_token_balance(safe: &mut BridgeSafe, amount: u64) { + let key = utils::type_name_bytes(); + let cfg = table::borrow_mut(&mut safe.token_cfg, key); + shared_structs::subtract_from_token_config_total_balance(cfg, amount); } public fun get_batch(safe: &BridgeSafe, batch_nonce: u64, clock: &Clock): (Batch, bool) { @@ -704,53 +662,6 @@ public(package) fun transfer( true } -/// Transfer function for mint-burn tokens: mints fresh coin directly to receiver via the stablecoin-sui treasury. -/// Only the bridge role can call this function. -public(package) fun transfer_mint_burn( - safe: &mut BridgeSafe, - _bridge_cap: &bridge_roles::BridgeCap, - receiver: address, - amount: u64, - xmn_treasury: &mut XmnTreasury, - deny_list: &DenyList, - ctx: &mut TxContext, -): bool { - let key = utils::type_name_bytes(); - - if (!table::contains(&safe.token_cfg, key)) { - return false - }; - - let (is_mint_burn, current_balance) = { - let cfg_ref = table::borrow(&safe.token_cfg, key); - ( - shared_structs::token_config_is_mint_burn(cfg_ref), - shared_structs::token_config_total_balance(cfg_ref), - ) - }; - - if (!is_mint_burn) { - return false - }; - - if (current_balance < amount) { - return false - }; - - let cap_key = MintBurnCapKey { token_type: key }; - if (!dof::exists_with_type>(&safe.id, cap_key)) { - return false - }; - - let cap = dof::borrow>(&safe.id, cap_key); - stablecoin_treasury::mint(xmn_treasury, cap, deny_list, amount, receiver, ctx); - - let cfg_mut = borrow_token_cfg_mut(safe, key); - shared_structs::subtract_from_token_config_total_balance(cfg_mut, amount); - - true -} - public fun get_stored_coin_balance(safe: &mut BridgeSafe): u64 { let key = utils::type_name_bytes(); if (!table::contains(&safe.token_cfg, key)) { @@ -793,19 +704,19 @@ public(package) fun assert_is_compatible(safe: &BridgeSafe) { bridge_version_control::assert_object_version_is_compatible_with_package(safe.compatible_versions); } -fun assert_token_is_whitelisted(safe: &BridgeSafe, key: vector) { +public(package) fun assert_token_is_whitelisted(safe: &BridgeSafe, key: vector) { assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); let cfg = table::borrow(&safe.token_cfg, key); assert!(shared_structs::token_config_whitelisted(cfg), ETokenNotWhitelisted); } -fun assert_token_is_not_whitelisted(safe: &BridgeSafe, key: vector) { +public(package) fun assert_token_is_not_whitelisted(safe: &BridgeSafe, key: vector) { assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); let cfg = table::borrow(&safe.token_cfg, key); assert!(!shared_structs::token_config_whitelisted(cfg), ETokenAlreadyExists); } -fun assert_token_is_mint_burn(safe: &BridgeSafe, key: vector) { +public(package) fun assert_token_is_mint_burn(safe: &BridgeSafe, key: vector) { assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); let cfg = table::borrow(&safe.token_cfg, key); assert!(shared_structs::token_config_is_mint_burn(cfg), EIncompatibleTokenFlags); diff --git a/tests/deposit_transfer_tests.move b/tests/deposit_transfer_tests.move index 556cb2f..82f6645 100644 --- a/tests/deposit_transfer_tests.move +++ b/tests/deposit_transfer_tests.move @@ -4,6 +4,7 @@ module bridge_safe::deposit_transfer_tests; use bridge_safe::bridge_roles::BridgeCap; use bridge_safe::pausable; use bridge_safe::safe::{Self, BridgeSafe}; +use bridge_safe::xmn_mint_cap_adapter; use sui::clock; use sui::coin; use sui::deny_list::DenyList; @@ -1172,7 +1173,7 @@ fun test_deposit_mint_burn_wrong_variant() { /// Uses the real deposit_mint_burn function (not the testing bypass) with a proper /// Treasury + DenyList. The abort happens before the treasury is accessed. #[test] -#[expected_failure(abort_code = safe::EMintBurnCapNotFound)] +#[expected_failure(abort_code = xmn_mint_cap_adapter::EMintBurnCapNotFound)] fun test_deposit_mint_burn_cap_not_registered() { let mut scenario = setup_with_treasury(); @@ -1197,7 +1198,7 @@ fun test_deposit_mint_burn_cap_not_registered() { ); // No MintCap registered — expect EMintBurnCapNotFound - safe::deposit_mint_burn( + xmn_mint_cap_adapter::deposit_mint_burn( &mut safe, coin, RECIPIENT_VECTOR, From 14bb6f541f389d9474d42f88899dc44bee40d857 Mon Sep 17 00:00:00 2001 From: Cristi Corcoveanu Date: Thu, 12 Mar 2026 15:08:31 +0200 Subject: [PATCH 09/30] refactored mint/burn as an adapter - add missing file --- .../xmn_mint_cap_adapter.move | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 sources/mint_burn_adapters/xmn_mint_cap_adapter.move 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..c80f7f0 --- /dev/null +++ b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move @@ -0,0 +1,140 @@ +/// Mint-burn adapter for the stablecoin-sui treasury (MintCap mechanism). +/// +/// Owns the full mint-burn deposit and transfer entry points. Stores a MintCap +/// inside BridgeSafe's dynamic object fields and calls the treasury's burn/mint. +/// Adding a new mint-burn mechanism means adding a sibling adapter module here +/// with its own entry points — safe.move stays mechanism-agnostic. +module bridge_safe::xmn_mint_cap_adapter; + +use bridge_safe::bridge_roles::BridgeCap; +use bridge_safe::events; +use bridge_safe::safe::{Self, 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 sui::object::UID; +use sui::transfer; +use sui::tx_context; +use treasury::treasury::{Self as stablecoin_treasury, MintCap, Treasury as XmnTreasury}; + +// DOF key — scoped to this module so it can't clash with other adapters. +public struct CapKey has copy, drop, store { + token_type: vector, +} + +const EMintBurnCapNotFound: u64 = 20; +const EMintBurnCapAlreadyRegistered: u64 = 21; + +// === Internal helpers === + +fun cap_key(): CapKey { + CapKey { token_type: utils::type_name_bytes() } +} + +// === Low-level UID-based operations (package-internal) === + +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()); + stablecoin_treasury::burn(xmn_treasury, 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()); + stablecoin_treasury::mint(xmn_treasury, cap, deny_list, amount, receiver, ctx); +} + +// === High-level BridgeSafe-aware entry points === + +/// Register a MintCap for a mint-burn token. Only callable by the safe owner. +public fun register_mint_burn_cap( + safe: &mut BridgeSafe, + cap: MintCap, + ctx: &TxContext, +) { + safe::checkOwnerRole(safe, ctx); + let key = utils::type_name_bytes(); + safe::assert_token_is_whitelisted(safe, key); + safe::assert_token_is_mint_burn(safe, key); + assert!(!has_cap(safe::uid(safe)), EMintBurnCapAlreadyRegistered); + register(safe::uid_mut(safe), cap); +} + +/// Remove a MintCap — only allowed once the token is de-whitelisted. +public fun deregister_mint_burn_cap(safe: &mut BridgeSafe, ctx: &TxContext) { + safe::checkOwnerRole(safe, ctx); + let key = utils::type_name_bytes(); + safe::assert_token_is_not_whitelisted(safe, key); + assert!(has_cap(safe::uid(safe)), EMintBurnCapNotFound); + deregister(safe::uid_mut(safe), ctx.sender()); +} + +/// Deposit for mint-burn tokens: coin is burned immediately via the stablecoin-sui treasury. +public fun deposit_mint_burn( + safe: &mut BridgeSafe, + coin_in: Coin, + recipient: vector, + clock: &Clock, + xmn_treasury: &mut XmnTreasury, + deny_list: &DenyList, + ctx: &mut TxContext, +) { + assert!(has_cap(safe::uid(safe)), EMintBurnCapNotFound); + + let (key, amount, batch_nonce, dep_nonce) = + safe::deposit_validate_and_record(safe, &coin_in, recipient, true, clock, ctx); + + burn(safe::uid(safe), xmn_treasury, deny_list, coin_in, ctx); + + events::emit_deposit(batch_nonce, dep_nonce, tx_context::sender(ctx), recipient, amount, key); +} + +/// Transfer for mint-burn tokens: mints fresh coin to receiver via the stablecoin-sui treasury. +/// Only callable by the bridge role. +public(package) fun transfer_mint_burn( + 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(safe)) { return false }; + if (!safe::get_token_is_mint_burn(safe)) { return false }; + if (safe::get_stored_coin_balance(safe) < amount) { return false }; + if (!has_cap(safe::uid(safe))) { return false }; + + mint(safe::uid(safe), xmn_treasury, deny_list, amount, receiver, ctx); + safe::subtract_token_balance(safe, amount); + + true +} From 55e806d1d94cc3f1ff6384d679be0f533dedbbcf Mon Sep 17 00:00:00 2001 From: Cristi Corcoveanu Date: Thu, 12 Mar 2026 15:23:53 +0200 Subject: [PATCH 10/30] refactored comments and function ordering a bit --- .../xmn_mint_cap_adapter.move | 101 ++++++++---------- 1 file changed, 42 insertions(+), 59 deletions(-) diff --git a/sources/mint_burn_adapters/xmn_mint_cap_adapter.move b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move index c80f7f0..aa58a97 100644 --- a/sources/mint_burn_adapters/xmn_mint_cap_adapter.move +++ b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move @@ -1,9 +1,3 @@ -/// Mint-burn adapter for the stablecoin-sui treasury (MintCap mechanism). -/// -/// Owns the full mint-burn deposit and transfer entry points. Stores a MintCap -/// inside BridgeSafe's dynamic object fields and calls the treasury's burn/mint. -/// Adding a new mint-burn mechanism means adding a sibling adapter module here -/// with its own entry points — safe.move stays mechanism-agnostic. module bridge_safe::xmn_mint_cap_adapter; use bridge_safe::bridge_roles::BridgeCap; @@ -14,12 +8,8 @@ use sui::clock::Clock; use sui::coin::Coin; use sui::deny_list::DenyList; use sui::dynamic_object_field as dof; -use sui::object::UID; -use sui::transfer; -use sui::tx_context; use treasury::treasury::{Self as stablecoin_treasury, MintCap, Treasury as XmnTreasury}; -// DOF key — scoped to this module so it can't clash with other adapters. public struct CapKey has copy, drop, store { token_type: vector, } @@ -27,14 +17,54 @@ public struct CapKey has copy, drop, store { const EMintBurnCapNotFound: u64 = 20; const EMintBurnCapAlreadyRegistered: u64 = 21; +// === Public API === + +public fun deposit_mint_burn( + safe: &mut BridgeSafe, + coin_in: Coin, + recipient: vector, + clock: &Clock, + xmn_treasury: &mut XmnTreasury, + deny_list: &DenyList, + ctx: &mut TxContext, +) { + assert!(has_cap(safe::uid(safe)), EMintBurnCapNotFound); + + let (key, amount, batch_nonce, dep_nonce) = + safe::deposit_validate_and_record(safe, &coin_in, recipient, true, clock, ctx); + + burn(safe::uid(safe), xmn_treasury, deny_list, coin_in, ctx); + + events::emit_deposit(batch_nonce, dep_nonce, tx_context::sender(ctx), recipient, amount, key); +} + +public fun register_mint_burn_cap( + safe: &mut BridgeSafe, + cap: MintCap, + ctx: &TxContext, +) { + safe::checkOwnerRole(safe, ctx); + let key = utils::type_name_bytes(); + safe::assert_token_is_whitelisted(safe, key); + safe::assert_token_is_mint_burn(safe, key); + assert!(!has_cap(safe::uid(safe)), EMintBurnCapAlreadyRegistered); + register(safe::uid_mut(safe), cap); +} + +public fun deregister_mint_burn_cap(safe: &mut BridgeSafe, ctx: &TxContext) { + safe::checkOwnerRole(safe, ctx); + let key = utils::type_name_bytes(); + safe::assert_token_is_not_whitelisted(safe, key); + assert!(has_cap(safe::uid(safe)), EMintBurnCapNotFound); + deregister(safe::uid_mut(safe), ctx.sender()); +} + // === Internal helpers === fun cap_key(): CapKey { CapKey { token_type: utils::type_name_bytes() } } -// === Low-level UID-based operations (package-internal) === - public(package) fun register(id: &mut UID, cap: MintCap) { dof::add(id, cap_key(), cap); } @@ -72,53 +102,6 @@ public(package) fun mint( stablecoin_treasury::mint(xmn_treasury, cap, deny_list, amount, receiver, ctx); } -// === High-level BridgeSafe-aware entry points === - -/// Register a MintCap for a mint-burn token. Only callable by the safe owner. -public fun register_mint_burn_cap( - safe: &mut BridgeSafe, - cap: MintCap, - ctx: &TxContext, -) { - safe::checkOwnerRole(safe, ctx); - let key = utils::type_name_bytes(); - safe::assert_token_is_whitelisted(safe, key); - safe::assert_token_is_mint_burn(safe, key); - assert!(!has_cap(safe::uid(safe)), EMintBurnCapAlreadyRegistered); - register(safe::uid_mut(safe), cap); -} - -/// Remove a MintCap — only allowed once the token is de-whitelisted. -public fun deregister_mint_burn_cap(safe: &mut BridgeSafe, ctx: &TxContext) { - safe::checkOwnerRole(safe, ctx); - let key = utils::type_name_bytes(); - safe::assert_token_is_not_whitelisted(safe, key); - assert!(has_cap(safe::uid(safe)), EMintBurnCapNotFound); - deregister(safe::uid_mut(safe), ctx.sender()); -} - -/// Deposit for mint-burn tokens: coin is burned immediately via the stablecoin-sui treasury. -public fun deposit_mint_burn( - safe: &mut BridgeSafe, - coin_in: Coin, - recipient: vector, - clock: &Clock, - xmn_treasury: &mut XmnTreasury, - deny_list: &DenyList, - ctx: &mut TxContext, -) { - assert!(has_cap(safe::uid(safe)), EMintBurnCapNotFound); - - let (key, amount, batch_nonce, dep_nonce) = - safe::deposit_validate_and_record(safe, &coin_in, recipient, true, clock, ctx); - - burn(safe::uid(safe), xmn_treasury, deny_list, coin_in, ctx); - - events::emit_deposit(batch_nonce, dep_nonce, tx_context::sender(ctx), recipient, amount, key); -} - -/// Transfer for mint-burn tokens: mints fresh coin to receiver via the stablecoin-sui treasury. -/// Only callable by the bridge role. public(package) fun transfer_mint_burn( safe: &mut BridgeSafe, _bridge_cap: &BridgeCap, From 61c65ce7e723b5f4c5530a6216a74ff6a8eeef7f Mon Sep 17 00:00:00 2001 From: Cristi Corcoveanu Date: Thu, 12 Mar 2026 16:07:58 +0200 Subject: [PATCH 11/30] reordered functions --- sources/safe.move | 909 +++++++++++++++++++++++----------------------- 1 file changed, 456 insertions(+), 453 deletions(-) diff --git a/sources/safe.move b/sources/safe.move index 0f09fa7..796d551 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -12,7 +12,6 @@ use bridge_safe::pausable::{Self, Pause}; use bridge_safe::upgrade_service_bridge; use bridge_safe::utils; use shared_structs::shared_structs::{Self, TokenConfig, Batch, Deposit}; -use sui::object::UID; use std::u64::{min, max}; use sui::bag::{Self, Bag}; use sui::clock::{Self, Clock}; @@ -81,6 +80,17 @@ public struct BridgeSafe has key { 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(ctx: &mut TxContext) { let deployer = tx_context::sender(ctx); @@ -108,377 +118,112 @@ public fun initialize(ctx: &mut TxContext) { transfer::share_object(safe); } -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); -} - -fun borrow_token_cfg_mut(safe: &mut BridgeSafe, key: vector): &mut TokenConfig { - table::borrow_mut(&mut safe.token_cfg, key) -} - -fun whitelist_token_internal( +/// Deposit function for native tokens: coin is stored in the safe's coin_storage bag. +public fun deposit( safe: &mut BridgeSafe, - minimum_amount: u64, - maximum_amount: u64, - is_native: bool, - treasury_id: Option, - is_mint_burn: bool, + coin_in: Coin, + recipient: vector, + clock: &Clock, ctx: &mut TxContext, ) { - safe.roles.owner_role().assert_sender_is_active_role(ctx); - - assert!(!(is_mint_burn && is_native), EIncompatibleTokenFlags); - assert!(minimum_amount > 0, EZeroAmount); - assert!(minimum_amount <= maximum_amount, EInvalidTokenLimits); + let (key, amount, batch_nonce, dep_nonce) = + deposit_validate_and_record(safe, &coin_in, recipient, false, clock, ctx); - let key = utils::type_name_bytes(); - let exists = table::contains(&safe.token_cfg, key); - if (exists) { - assert_token_is_not_whitelisted(safe, key); + if (bag::contains(&safe.coin_storage, key)) { + coin::join(bag::borrow_mut, Coin>(&mut safe.coin_storage, key), coin_in); + } else { + bag::add(&mut safe.coin_storage, key, coin_in); }; - shared_structs::upsert_token_config( - &mut safe.token_cfg, - key, - true, - is_native, - minimum_amount, - maximum_amount, - treasury_id, - is_mint_burn, - ); - - events::emit_token_whitelisted( - key, - minimum_amount, - maximum_amount, - is_native, - is_mint_burn, - ); -} - -public fun whitelist_token( - safe: &mut BridgeSafe, - minimum_amount: u64, - maximum_amount: u64, - ctx: &mut TxContext, -) { - whitelist_token_internal(safe, minimum_amount, maximum_amount, true, option::none(), false, ctx); + events::emit_deposit(batch_nonce, dep_nonce, tx_context::sender(ctx), recipient, amount, key); } -public fun whitelist_token_mint_burn( +/// 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, - treasury_id: ID, + _bridge_cap: &bridge_roles::BridgeCap, + receiver: address, + amount: u64, ctx: &mut TxContext, -) { - whitelist_token_internal(safe, minimum_amount, maximum_amount, false, option::some(treasury_id), true, ctx); -} - -public fun remove_token_from_whitelist(safe: &mut BridgeSafe, ctx: &mut TxContext) { - safe.roles.owner_role().assert_sender_is_active_role(ctx); +): bool { let key = utils::type_name_bytes(); - let cfg = borrow_token_cfg_mut(safe, key); - shared_structs::set_token_config_whitelisted(cfg, false); - - events::emit_token_removed_from_whitelist(key); -} -public fun is_token_whitelisted(safe: &BridgeSafe): bool { - let key = utils::type_name_bytes(); if (!table::contains(&safe.token_cfg, key)) { return false }; - let cfg = table::borrow(&safe.token_cfg, key); - shared_structs::token_config_whitelisted(cfg) -} - -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 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 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 set_token_min_limit(safe: &mut BridgeSafe, amount: u64, 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); - let old_max = shared_structs::token_config_max_limit(cfg); - - assert!(amount > 0, EZeroAmount); - assert!(amount <= old_max, EInvalidTokenLimits); - - shared_structs::set_token_config_min_limit(cfg, amount); - - events::emit_token_limits_updated(key, amount, old_max); -} - -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(package) fun roles_mut(safe: &mut BridgeSafe): &mut Roles { - &mut safe.roles -} - -public fun set_token_max_limit(safe: &mut BridgeSafe, amount: u64, 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); - let old_min = shared_structs::token_config_min_limit(cfg); - - assert!(amount >= old_min, EInvalidTokenLimits); - shared_structs::set_token_config_max_limit(cfg, amount); - - events::emit_token_limits_updated(key, old_min, amount); -} - -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_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_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 set_token_is_native(safe: &mut BridgeSafe, is_native: bool, 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); - assert!( - !(is_native && shared_structs::token_config_is_mint_burn(cfg)), - EIncompatibleTokenFlags, - ); - shared_structs::set_token_config_is_native(cfg, is_native); - - events::emit_token_is_native_updated(key, is_native); -} - -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); - - let key = utils::type_name_bytes(); - let cfg = borrow_token_cfg_mut(safe, key); - assert!( - !(is_mint_burn && shared_structs::token_config_is_native(cfg)), - EIncompatibleTokenFlags, - ); - shared_structs::set_token_config_is_mint_burn(cfg, is_mint_burn); - - events::emit_token_is_mint_burn_updated(key, is_mint_burn); -} -public fun set_bridge_addr(safe: &mut BridgeSafe, new_bridge_addr: address, ctx: &TxContext) { - 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 init_supply(safe: &mut BridgeSafe, coin_in: Coin, ctx: &mut TxContext) { - safe.roles.owner_role().assert_sender_is_active_role(ctx); - - let key = utils::type_name_bytes(); - - assert_token_is_whitelisted(safe, key); - let cfg_ref = table::borrow(&safe.token_cfg, key); - assert!(shared_structs::token_config_is_native(cfg_ref), 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); - - 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); + let (is_mint_burn, current_balance) = { + let cfg_ref = table::borrow(&safe.token_cfg, key); + ( + shared_structs::token_config_is_mint_burn(cfg_ref), + shared_structs::token_config_total_balance(cfg_ref), + ) }; -} -#[allow(lint(self_transfer))] -public fun sync_supply(safe: &mut BridgeSafe, mut coin_in: Coin, ctx: &mut TxContext) { - safe.roles.owner_role().assert_sender_is_active_role(ctx); - - let key = utils::type_name_bytes(); - - assert_token_is_whitelisted(safe, key); - let cfg_ref = table::borrow(&safe.token_cfg, key); - assert!(shared_structs::token_config_is_native(cfg_ref), ENotNativeToken); - - let expected_balance = shared_structs::token_config_total_balance(cfg_ref); - - let actual_balance = if (bag::contains(&safe.coin_storage, key)) { - let stored_coin = bag::borrow, Coin>(&safe.coin_storage, key); - coin::value(stored_coin) - } else { - 0 + if (is_mint_burn) { + return false }; - assert!(expected_balance > actual_balance, EInsufficientBalance); - - let deficit = expected_balance - actual_balance; - assert!(coin::value(&coin_in) >= deficit, EInsufficientBalance); - - let top_up_coin = coin::split(&mut coin_in, 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); - } else { - bag::add(&mut safe.coin_storage, key, top_up_coin); + if (current_balance < amount) { + return false }; - if (coin::value(&coin_in) == 0) { - coin::destroy_zero(coin_in); - } else { - transfer::public_transfer(coin_in, tx_context::sender(ctx)); + if (!bag::contains(&safe.coin_storage, key)) { + return false }; -} - -/// Shared helper: validates deposit preconditions, manages batching, records the deposit, -/// and updates the token balance. Returns (key, amount, batch_nonce, dep_nonce). -/// `expect_mint_burn` drives the variant guard: false for native, true for mint-burn. -public(package) fun deposit_validate_and_record( - safe: &mut BridgeSafe, - coin_in: &Coin, - recipient: vector, - expect_mint_burn: bool, - clock: &Clock, - ctx: &mut TxContext, -): (vector, u64, u64, u64) { - 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); - assert!(shared_structs::token_config_is_mint_burn(cfg_ref) == expect_mint_burn, EIncompatibleTokenFlags); - - 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 stored_coin = bag::borrow_mut, Coin>(&mut safe.coin_storage, key); + let coin_value = coin::value(stored_coin); + if (coin_value < amount) { + return false }; - 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); + let coin_to_transfer = coin::split(stored_coin, amount, ctx); - if (!table::contains(&safe.batch_deposits, batch_index)) { - table::add(&mut safe.batch_deposits, batch_index, vector::empty()); + if (coin::value(stored_coin) == 0) { + let empty_coin = bag::remove, Coin>(&mut safe.coin_storage, key); + coin::destroy_zero(empty_coin); }; - 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); + transfer::public_transfer(coin_to_transfer, receiver); - let cfg = borrow_token_cfg_mut(safe, key); - shared_structs::add_to_token_config_total_balance(cfg, amount); + let cfg_mut = borrow_token_cfg_mut(safe, key); + shared_structs::subtract_from_token_config_total_balance(cfg_mut, amount); - (key, amount, batch_nonce, dep_nonce) + true } -/// 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, -) { - let (key, amount, batch_nonce, dep_nonce) = - deposit_validate_and_record(safe, &coin_in, recipient, false, clock, ctx); - - if (bag::contains(&safe.coin_storage, key)) { - coin::join(bag::borrow_mut, Coin>(&mut safe.coin_storage, key), coin_in); - } else { - bag::add(&mut safe.coin_storage, key, coin_in); +public fun is_token_whitelisted(safe: &BridgeSafe): bool { + let key = utils::type_name_bytes(); + if (!table::contains(&safe.token_cfg, key)) { + return false }; - - events::emit_deposit(batch_nonce, dep_nonce, tx_context::sender(ctx), recipient, amount, key); -} - -public(package) fun checkOwnerRole(safe: &BridgeSafe, ctx: &TxContext) { - safe.roles.owner_role().assert_sender_is_active_role(ctx); + let cfg = table::borrow(&safe.token_cfg, key); + shared_structs::token_config_whitelisted(cfg) } -public(package) fun uid(safe: &BridgeSafe): &UID { - &safe.id +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(package) fun uid_mut(safe: &mut BridgeSafe): &mut UID { - &mut safe.id +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(package) fun has_token_config(safe: &BridgeSafe): bool { - table::contains(&safe.token_cfg, utils::type_name_bytes()) +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(package) fun subtract_token_balance(safe: &mut BridgeSafe, amount: u64) { +public fun get_token_is_native(safe: &BridgeSafe): bool { let key = utils::type_name_bytes(); - let cfg = table::borrow_mut(&mut safe.token_cfg, key); - shared_structs::subtract_from_token_config_total_balance(cfg, amount); + let cfg = table::borrow(&safe.token_cfg, key); + shared_structs::token_config_is_native(cfg) } public fun get_batch(safe: &BridgeSafe, batch_nonce: u64, clock: &Clock): (Batch, bool) { @@ -520,43 +265,6 @@ 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 } @@ -607,119 +315,226 @@ public fun get_batch_deposits_count(batch: &Batch): u16 { shared_structs::batch_deposits_count(batch) } -/// 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, - _bridge_cap: &bridge_roles::BridgeCap, - receiver: address, - amount: u64, - ctx: &mut TxContext, -): bool { +public fun get_stored_coin_balance(safe: &mut BridgeSafe): u64 { let key = utils::type_name_bytes(); - if (!table::contains(&safe.token_cfg, key)) { - return false + return 0 }; + let cfg_ref = table::borrow(&safe.token_cfg, key); + shared_structs::token_config_total_balance(cfg_ref) +} - let (is_mint_burn, current_balance) = { - let cfg_ref = table::borrow(&safe.token_cfg, key); - ( - shared_structs::token_config_is_mint_burn(cfg_ref), - shared_structs::token_config_total_balance(cfg_ref), - ) +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) +} - if (is_mint_burn) { - return false - }; +// === Admin Management === - if (current_balance < amount) { - return false - }; +public fun pause_contract(safe: &mut BridgeSafe, ctx: &mut TxContext) { + safe.roles.owner_role().assert_sender_is_active_role(ctx); + pausable::pause(&mut safe.pause); +} - if (!bag::contains(&safe.coin_storage, key)) { - return false - }; +public fun unpause_contract(safe: &mut BridgeSafe, ctx: &mut TxContext) { + safe.roles.owner_role().assert_sender_is_active_role(ctx); + pausable::unpause(&mut safe.pause); +} - 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 - }; +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 coin_to_transfer = coin::split(stored_coin, amount, ctx); +public fun accept_ownership(safe: &mut BridgeSafe, ctx: &TxContext) { + safe.roles_mut().owner_role_mut().accept_role(ctx) +} - 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); +public fun init_supply(safe: &mut BridgeSafe, coin_in: Coin, ctx: &mut TxContext) { + safe.roles.owner_role().assert_sender_is_active_role(ctx); + + let key = utils::type_name_bytes(); + + assert_token_is_whitelisted(safe, key); + let cfg_ref = table::borrow(&safe.token_cfg, key); + assert!(shared_structs::token_config_is_native(cfg_ref), ENotNativeToken); + + let amount = coin::value(&coin_in); let cfg_mut = borrow_token_cfg_mut(safe, key); - shared_structs::subtract_from_token_config_total_balance(cfg_mut, amount); + shared_structs::add_to_token_config_total_balance(cfg_mut, amount); - true + 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); + }; } -public fun get_stored_coin_balance(safe: &mut BridgeSafe): u64 { +#[allow(lint(self_transfer))] +public fun sync_supply(safe: &mut BridgeSafe, mut coin_in: Coin, ctx: &mut TxContext) { + safe.roles.owner_role().assert_sender_is_active_role(ctx); + let key = utils::type_name_bytes(); - if (!table::contains(&safe.token_cfg, key)) { - return 0 - }; + + assert_token_is_whitelisted(safe, key); let cfg_ref = table::borrow(&safe.token_cfg, key); - shared_structs::token_config_total_balance(cfg_ref) + assert!(shared_structs::token_config_is_native(cfg_ref), ENotNativeToken); + + let expected_balance = shared_structs::token_config_total_balance(cfg_ref); + + let actual_balance = if (bag::contains(&safe.coin_storage, key)) { + let stored_coin = bag::borrow, Coin>(&safe.coin_storage, key); + coin::value(stored_coin) + } else { + 0 + }; + + assert!(expected_balance > actual_balance, EInsufficientBalance); + + let deficit = expected_balance - actual_balance; + assert!(coin::value(&coin_in) >= deficit, EInsufficientBalance); + + let top_up_coin = coin::split(&mut coin_in, 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); + } else { + bag::add(&mut safe.coin_storage, key, top_up_coin); + }; + + if (coin::value(&coin_in) == 0) { + coin::destroy_zero(coin_in); + } else { + transfer::public_transfer(coin_in, tx_context::sender(ctx)); + }; } -public fun get_coin_storage_balance(safe: &BridgeSafe): u64 { +public fun whitelist_token( + safe: &mut BridgeSafe, + minimum_amount: u64, + maximum_amount: u64, + ctx: &mut TxContext, +) { + whitelist_token_internal(safe, minimum_amount, maximum_amount, true, option::none(), false, ctx); +} + +public fun whitelist_token_mint_burn( + safe: &mut BridgeSafe, + minimum_amount: u64, + maximum_amount: u64, + treasury_id: ID, + ctx: &mut TxContext, +) { + whitelist_token_internal(safe, minimum_amount, maximum_amount, false, option::some(treasury_id), true, ctx); +} + +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(); - 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); + shared_structs::set_token_config_whitelisted(cfg, false); + + events::emit_token_removed_from_whitelist(key); } -public fun pause_contract(safe: &mut BridgeSafe, ctx: &mut TxContext) { +public fun set_bridge_addr(safe: &mut BridgeSafe, new_bridge_addr: address, ctx: &TxContext) { safe.roles.owner_role().assert_sender_is_active_role(ctx); - pausable::pause(&mut safe.pause); + + let previous_bridge = safe.bridge_addr; + safe.bridge_addr = new_bridge_addr; + events::emit_bridge_transferred(previous_bridge, new_bridge_addr); } -public fun unpause_contract(safe: &mut BridgeSafe, ctx: &mut TxContext) { +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); - pausable::unpause(&mut safe.pause); + assert!(new_timeout_ms <= safe.batch_settle_timeout_ms, EBatchBlockLimitExceedsSettle); + safe.batch_timeout_ms = new_timeout_ms; +} + +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 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 set_token_min_limit(safe: &mut BridgeSafe, amount: u64, 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); + let old_max = shared_structs::token_config_max_limit(cfg); + + assert!(amount > 0, EZeroAmount); + assert!(amount <= old_max, EInvalidTokenLimits); + + shared_structs::set_token_config_min_limit(cfg, amount); + + events::emit_token_limits_updated(key, amount, old_max); } -public fun transfer_ownership(safe: &mut BridgeSafe, new_owner: address, ctx: &TxContext) { - safe.roles_mut().owner_role_mut().begin_role_transfer(new_owner, ctx) -} +public fun set_token_max_limit(safe: &mut BridgeSafe, amount: u64, 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); + let old_min = shared_structs::token_config_min_limit(cfg); + + assert!(amount >= old_min, EInvalidTokenLimits); + shared_structs::set_token_config_max_limit(cfg, amount); -public fun accept_ownership(safe: &mut BridgeSafe, ctx: &TxContext) { - safe.roles_mut().owner_role_mut().accept_role(ctx) + events::emit_token_limits_updated(key, old_min, amount); } -// === Asserts === +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(package) fun assert_is_compatible(safe: &BridgeSafe) { - bridge_version_control::assert_object_version_is_compatible_with_package(safe.compatible_versions); -} + let key = utils::type_name_bytes(); + let cfg = borrow_token_cfg_mut(safe, key); + assert!( + !(is_native && shared_structs::token_config_is_mint_burn(cfg)), + EIncompatibleTokenFlags, + ); + shared_structs::set_token_config_is_native(cfg, is_native); -public(package) fun assert_token_is_whitelisted(safe: &BridgeSafe, key: vector) { - assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); - let cfg = table::borrow(&safe.token_cfg, key); - assert!(shared_structs::token_config_whitelisted(cfg), ETokenNotWhitelisted); + events::emit_token_is_native_updated(key, is_native); } -public(package) fun assert_token_is_not_whitelisted(safe: &BridgeSafe, key: vector) { - assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); - let cfg = table::borrow(&safe.token_cfg, key); - assert!(!shared_structs::token_config_whitelisted(cfg), ETokenAlreadyExists); -} +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(package) fun assert_token_is_mint_burn(safe: &BridgeSafe, key: vector) { - assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); - let cfg = table::borrow(&safe.token_cfg, key); - assert!(shared_structs::token_config_is_mint_burn(cfg), EIncompatibleTokenFlags); + let key = utils::type_name_bytes(); + let cfg = borrow_token_cfg_mut(safe, key); + assert!( + !(is_mint_burn && shared_structs::token_config_is_native(cfg)), + EIncompatibleTokenFlags, + ); + shared_structs::set_token_config_is_mint_burn(cfg, is_mint_burn); + + events::emit_token_is_mint_burn_updated(key, is_mint_burn); } // === Upgrade Management === @@ -810,21 +625,193 @@ public fun is_migration_in_progress(safe: &BridgeSafe): bool { safe.compatible_versions.length() > 1 } -#[test_only] -public fun init_for_testing(ctx: &mut TxContext) { - initialize(ctx); +// === Asserts === + +public(package) fun assert_is_compatible(safe: &BridgeSafe) { + bridge_version_control::assert_object_version_is_compatible_with_package(safe.compatible_versions); } -#[test_only] -public fun create_batch_for_testing(safe: &mut BridgeSafe, clock: &Clock, ctx: &mut TxContext) { - create_new_batch_internal(safe, clock, ctx); +public(package) fun assert_token_is_whitelisted(safe: &BridgeSafe, key: vector) { + assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); + let cfg = table::borrow(&safe.token_cfg, key); + assert!(shared_structs::token_config_whitelisted(cfg), ETokenNotWhitelisted); } -#[test_only] -public fun add_to_balance_for_testing(safe: &mut BridgeSafe, amount: u64) { +public(package) fun assert_token_is_not_whitelisted(safe: &BridgeSafe, key: vector) { + assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); + let cfg = table::borrow(&safe.token_cfg, key); + assert!(!shared_structs::token_config_whitelisted(cfg), ETokenAlreadyExists); +} + +public(package) fun assert_token_is_mint_burn(safe: &BridgeSafe, key: vector) { + assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); + let cfg = table::borrow(&safe.token_cfg, key); + assert!(shared_structs::token_config_is_mint_burn(cfg), EIncompatibleTokenFlags); +} + +/// ==== Internal logic helpers ==== + +fun whitelist_token_internal( + safe: &mut BridgeSafe, + minimum_amount: u64, + maximum_amount: u64, + is_native: bool, + treasury_id: Option, + is_mint_burn: bool, + ctx: &TxContext, +) { + safe.roles.owner_role().assert_sender_is_active_role(ctx); + + assert!(!(is_mint_burn && is_native), EIncompatibleTokenFlags); + assert!(minimum_amount > 0, EZeroAmount); + assert!(minimum_amount <= maximum_amount, EInvalidTokenLimits); + let key = utils::type_name_bytes(); - let cfg_mut = borrow_token_cfg_mut(safe, key); - shared_structs::add_to_token_config_total_balance(cfg_mut, amount); + let exists = table::contains(&safe.token_cfg, key); + if (exists) { + assert_token_is_not_whitelisted(safe, key); + }; + + shared_structs::upsert_token_config( + &mut safe.token_cfg, + key, + true, + is_native, + minimum_amount, + maximum_amount, + treasury_id, + is_mint_burn, + ); + + events::emit_token_whitelisted( + key, + minimum_amount, + maximum_amount, + is_native, + is_mint_burn, + ); +} + +/// Shared helper: validates deposit preconditions, manages batching, records the deposit, +/// and updates the token balance. Returns (key, amount, batch_nonce, dep_nonce). +/// `expect_mint_burn` drives the variant guard: false for native, true for mint-burn. +public(package) fun deposit_validate_and_record( + safe: &mut BridgeSafe, + coin_in: &Coin, + recipient: vector, + expect_mint_burn: bool, + clock: &Clock, + ctx: &mut TxContext, +): (vector, u64, u64, u64) { + 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); + assert!(shared_structs::token_config_is_mint_burn(cfg_ref) == expect_mint_burn, EIncompatibleTokenFlags); + + 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); + + (key, amount, batch_nonce, dep_nonce) +} + +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) +} + +/// ==== Internal helpers ==== + +public(package) fun checkOwnerRole(safe: &BridgeSafe, ctx: &TxContext) { + safe.roles.owner_role().assert_sender_is_active_role(ctx); +} + +public(package) fun uid(safe: &BridgeSafe): &UID { + &safe.id +} + +public(package) fun uid_mut(safe: &mut BridgeSafe): &mut UID { + &mut safe.id +} + +public(package) fun has_token_config(safe: &BridgeSafe): bool { + table::contains(&safe.token_cfg, utils::type_name_bytes()) +} + +public(package) fun subtract_token_balance(safe: &mut BridgeSafe, amount: u64) { + let key = utils::type_name_bytes(); + let cfg = table::borrow_mut(&mut safe.token_cfg, key); + shared_structs::subtract_from_token_config_total_balance(cfg, amount); +} + +fun borrow_token_cfg_mut(safe: &mut BridgeSafe, key: vector): &mut TokenConfig { + table::borrow_mut(&mut safe.token_cfg, key) +} + +public(package) fun roles_mut(safe: &mut BridgeSafe): &mut Roles { + &mut safe.roles } /// Test helper that performs a mint-burn deposit without calling the real treasury burn. @@ -846,3 +833,19 @@ public fun deposit_mint_burn_for_testing( events::emit_deposit(batch_nonce, dep_nonce, tx_context::sender(ctx), recipient, amount, key); } +#[test_only] +public fun init_for_testing(ctx: &mut TxContext) { + initialize(ctx); +} + +#[test_only] +public fun create_batch_for_testing(safe: &mut BridgeSafe, clock: &Clock, ctx: &mut TxContext) { + create_new_batch_internal(safe, clock, ctx); +} + +#[test_only] +public fun add_to_balance_for_testing(safe: &mut BridgeSafe, amount: u64) { + let key = utils::type_name_bytes(); + let cfg_mut = borrow_token_cfg_mut(safe, key); + shared_structs::add_to_token_config_total_balance(cfg_mut, amount); +} From ff00c98309a2132194822332d83aefd053c0de54 Mon Sep 17 00:00:00 2001 From: Cristi Corcoveanu Date: Thu, 12 Mar 2026 16:28:22 +0200 Subject: [PATCH 12/30] refactored the adapter to also do the whitelist together with capturing the mint cap --- sources/bridge_module.move | 4 +- .../xmn_mint_cap_adapter.move | 62 ++++++++++--------- sources/safe.move | 12 +--- tests/deposit_transfer_tests.move | 14 +++-- 4 files changed, 44 insertions(+), 48 deletions(-) diff --git a/sources/bridge_module.move b/sources/bridge_module.move index 9774575..4b27296 100644 --- a/sources/bridge_module.move +++ b/sources/bridge_module.move @@ -411,7 +411,7 @@ public fun execute_transfer_mint_burn( let recipient = *vector::borrow(&recipients, i); let amount = *vector::borrow(&amounts, i); - let success = xmn_mint_cap_adapter::transfer_mint_burn(safe, &bridge.bridge_cap, recipient, amount, xmn_treasury, deny_list, ctx); + let success = xmn_mint_cap_adapter::transfer(safe, &bridge.bridge_cap, recipient, amount, xmn_treasury, deny_list, ctx); if (success) { vector::push_back( &mut bridge.transfer_statuses, @@ -759,7 +759,7 @@ public fun execute_transfer_mint_burn_for_testing( let recipient = *vector::borrow(&recipients, i); let amount = *vector::borrow(&amounts, i); - let success = xmn_mint_cap_adapter::transfer_mint_burn(safe, &bridge.bridge_cap, recipient, amount, xmn_treasury, deny_list, ctx); + let success = xmn_mint_cap_adapter::transfer(safe, &bridge.bridge_cap, recipient, amount, xmn_treasury, deny_list, ctx); if (success) { vector::push_back( &mut bridge.transfer_statuses, diff --git a/sources/mint_burn_adapters/xmn_mint_cap_adapter.move b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move index aa58a97..6642fbb 100644 --- a/sources/mint_burn_adapters/xmn_mint_cap_adapter.move +++ b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move @@ -19,7 +19,7 @@ const EMintBurnCapAlreadyRegistered: u64 = 21; // === Public API === -public fun deposit_mint_burn( +public fun deposit( safe: &mut BridgeSafe, coin_in: Coin, recipient: vector, @@ -38,25 +38,47 @@ public fun deposit_mint_burn( events::emit_deposit(batch_nonce, dep_nonce, tx_context::sender(ctx), recipient, amount, key); } -public fun register_mint_burn_cap( +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(safe)) { return false }; + if (!safe::get_token_is_mint_burn(safe)) { return false }; + if (safe::get_stored_coin_balance(safe) < amount) { return false }; + if (!has_cap(safe::uid(safe))) { return false }; + + mint(safe::uid(safe), xmn_treasury, deny_list, amount, receiver, ctx); + safe::subtract_token_balance(safe, amount); + + true +} + +// === Admin Management === + +public fun whitelist_token( + safe: &mut BridgeSafe, + minimum_amount: u64, + maximum_amount: u64, cap: MintCap, + treasury_id: ID, ctx: &TxContext, ) { - safe::checkOwnerRole(safe, ctx); - let key = utils::type_name_bytes(); - safe::assert_token_is_whitelisted(safe, key); - safe::assert_token_is_mint_burn(safe, key); assert!(!has_cap(safe::uid(safe)), EMintBurnCapAlreadyRegistered); + safe::whitelist_token_internal(safe, minimum_amount, maximum_amount, false, option::some(treasury_id), true, ctx); register(safe::uid_mut(safe), cap); } -public fun deregister_mint_burn_cap(safe: &mut BridgeSafe, ctx: &TxContext) { - safe::checkOwnerRole(safe, ctx); - let key = utils::type_name_bytes(); - safe::assert_token_is_not_whitelisted(safe, key); +/// 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) { assert!(has_cap(safe::uid(safe)), EMintBurnCapNotFound); deregister(safe::uid_mut(safe), ctx.sender()); + safe::remove_token_from_whitelist(safe, ctx); } // === Internal helpers === @@ -101,23 +123,3 @@ public(package) fun mint( let cap = dof::borrow(id, cap_key()); stablecoin_treasury::mint(xmn_treasury, cap, deny_list, amount, receiver, ctx); } - -public(package) fun transfer_mint_burn( - 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(safe)) { return false }; - if (!safe::get_token_is_mint_burn(safe)) { return false }; - if (safe::get_stored_coin_balance(safe) < amount) { return false }; - if (!has_cap(safe::uid(safe))) { return false }; - - mint(safe::uid(safe), xmn_treasury, deny_list, amount, receiver, ctx); - safe::subtract_token_balance(safe, amount); - - true -} diff --git a/sources/safe.move b/sources/safe.move index 796d551..41fcf31 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -424,16 +424,6 @@ public fun whitelist_token( whitelist_token_internal(safe, minimum_amount, maximum_amount, true, option::none(), false, ctx); } -public fun whitelist_token_mint_burn( - safe: &mut BridgeSafe, - minimum_amount: u64, - maximum_amount: u64, - treasury_id: ID, - ctx: &mut TxContext, -) { - whitelist_token_internal(safe, minimum_amount, maximum_amount, false, option::some(treasury_id), true, ctx); -} - 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(); @@ -651,7 +641,7 @@ public(package) fun assert_token_is_mint_burn(safe: &BridgeSafe, key: vector /// ==== Internal logic helpers ==== -fun whitelist_token_internal( +public(package) fun whitelist_token_internal( safe: &mut BridgeSafe, minimum_amount: u64, maximum_amount: u64, diff --git a/tests/deposit_transfer_tests.move b/tests/deposit_transfer_tests.move index 82f6645..e6c3891 100644 --- a/tests/deposit_transfer_tests.move +++ b/tests/deposit_transfer_tests.move @@ -852,11 +852,13 @@ fun setup_mint_burn(): Scenario { s.next_tx(ADMIN); { let mut safe = ts::take_shared(&s); - safe::whitelist_token_mint_burn( + safe::whitelist_token_internal( &mut safe, MIN_AMOUNT, MAX_AMOUNT, - object::id_from_address(@0x1234), + false, + option::some(object::id_from_address(@0x1234)), + true, s.ctx(), ); ts::return_shared(safe); @@ -1184,11 +1186,13 @@ fun test_deposit_mint_burn_cap_not_registered() { let deny_list = ts::take_shared(&scenario); let clock = clock::create_for_testing(ts::ctx(&mut scenario)); - safe::whitelist_token_mint_burn( + safe::whitelist_token_internal( &mut safe, MIN_AMOUNT, MAX_AMOUNT, - object::id(&treasury), + false, + option::some(object::id(&treasury)), + true, ts::ctx(&mut scenario), ); @@ -1198,7 +1202,7 @@ fun test_deposit_mint_burn_cap_not_registered() { ); // No MintCap registered — expect EMintBurnCapNotFound - xmn_mint_cap_adapter::deposit_mint_burn( + xmn_mint_cap_adapter::deposit( &mut safe, coin, RECIPIENT_VECTOR, From 1789c0005a641e7e5fc915c458c3c94d273084d3 Mon Sep 17 00:00:00 2001 From: Cristi Corcoveanu Date: Thu, 12 Mar 2026 17:03:45 +0200 Subject: [PATCH 13/30] moved bridge mint logic to adapter --- sources/bridge_module.move | 378 ++++-------------- .../xmn_mint_cap_adapter.move | 78 +++- sources/shared_structs.move | 1 - tests/bridge_comprehensive_tests.move | 4 +- 4 files changed, 142 insertions(+), 319 deletions(-) diff --git a/sources/bridge_module.move b/sources/bridge_module.move index 4b27296..d08ff1a 100644 --- a/sources/bridge_module.move +++ b/sources/bridge_module.move @@ -12,9 +12,6 @@ use bridge_safe::pausable::{Self, Pause}; use bridge_safe::safe::{Self, BridgeSafe}; use bridge_safe::utils; use bridge_safe::bridge_version_control; -use bridge_safe::xmn_mint_cap_adapter; -use treasury::treasury::Treasury as XmnTreasury; -use sui::deny_list::DenyList; use shared_structs::shared_structs::{Self, Deposit, Batch, CrossTransferStatus, DepositStatus}; use std::u64::{min, max}; use sui::address; @@ -273,199 +270,17 @@ public fun execute_transfer( 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); + pre_execute_transfer(bridge, batch_nonce_mvx, &recipients, &amounts, &deposit_nonces, &signatures, clock, ctx); let len = vector::length(&recipients); - assert!(vector::length(&amounts) == len, EInvalidAmountsLength); - assert!(vector::length(&deposit_nonces) == 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; - } else { - table::add(&mut bridge.execution_timestamps, 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, 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(safe, &bridge.bridge_cap, *vector::borrow(&recipients, i), *vector::borrow(&amounts, i), 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, - }); - }; -} - -public fun execute_transfer_mint_burn( - 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, -) { - 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( - 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; - } else { - table::add(&mut bridge.execution_timestamps, 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 = xmn_mint_cap_adapter::transfer(safe, &bridge.bridge_cap, recipient, amount, xmn_treasury, deny_list, ctx); - if (success) { - vector::push_back( - &mut bridge.transfer_statuses, - shared_structs::deposit_status_executed(), - ); - - 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(), - ); - }; - 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( @@ -660,46 +475,75 @@ public fun execute_transfer_for_testing( clock: &Clock, ctx: &mut TxContext, ) { + pre_execute_transfer_for_testing(bridge, batch_nonce_mvx, clock); + + let len = vector::length(&recipients); + let mut i = 0; + while (i < len) { + let success = safe::transfer(safe, &bridge.bridge_cap, *vector::borrow(&recipients, i), *vector::borrow(&amounts, i), 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, tx_context::sender(ctx)); + 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(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; + *table::borrow_mut(&mut bridge.execution_timestamps, batch_nonce_mvx) = now; } else { table::add(&mut bridge.execution_timestamps, 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, ctx); - if (success) { - vector::push_back( - &mut bridge.transfer_statuses, - shared_structs::deposit_status_executed(), - ); - - 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) { + vector::push_back(&mut bridge.transfer_statuses, shared_structs::deposit_status_executed()); + if (table::contains(&bridge.successful_transfers_by_batch, batch_nonce_mvx)) { + let count = table::borrow_mut(&mut bridge.successful_transfers_by_batch, batch_nonce_mvx); + *count = *count + 1; } else { - vector::push_back( - &mut bridge.transfer_statuses, - shared_structs::deposit_status_rejected(), - ); + table::add(&mut bridge.successful_transfers_by_batch, batch_nonce_mvx, 1); }; - i = i + 1; + } else { + vector::push_back(&mut bridge.transfer_statuses, 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); let cross_status = shared_structs::create_cross_transfer_status( @@ -707,112 +551,38 @@ public fun execute_transfer_for_testing( 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) + let successful_count = if (table::contains(&bridge.successful_transfers_by_batch, batch_nonce_mvx)) { + let count = *table::borrow(&bridge.successful_transfers_by_batch, batch_nonce_mvx); + table::remove(&mut bridge.successful_transfers_by_batch, 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, - successful_transfers: successful_count, - }); + event::emit(BatchExecuted { batch_nonce_mvx, transfers_count: total_transfers, successful_transfers: successful_count }); }; } +/// Exposes the bridge_cap for adapter transfer calls. +public(package) fun bridge_cap(bridge: &Bridge): &BridgeCap { + &bridge.bridge_cap +} + #[test_only] -public fun execute_transfer_mint_burn_for_testing( +public(package) fun pre_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, ) { 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; + *table::borrow_mut(&mut bridge.execution_timestamps, batch_nonce_mvx) = now; } else { table::add(&mut bridge.execution_timestamps, 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 = xmn_mint_cap_adapter::transfer(safe, &bridge.bridge_cap, recipient, amount, xmn_treasury, deny_list, ctx); - if (success) { - vector::push_back( - &mut bridge.transfer_statuses, - shared_structs::deposit_status_executed(), - ); - - 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(), - ); - }; - 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, - }); - }; } // === Upgrade Management for Bridge === diff --git a/sources/mint_burn_adapters/xmn_mint_cap_adapter.move b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move index 6642fbb..0410c8d 100644 --- a/sources/mint_burn_adapters/xmn_mint_cap_adapter.move +++ b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move @@ -1,5 +1,6 @@ module bridge_safe::xmn_mint_cap_adapter; +use bridge_safe::bridge::{Self as bridge_module, Bridge}; use bridge_safe::bridge_roles::BridgeCap; use bridge_safe::events; use bridge_safe::safe::{Self, BridgeSafe}; @@ -38,24 +39,31 @@ public fun deposit( events::emit_deposit(batch_nonce, dep_nonce, tx_context::sender(ctx), recipient, amount, key); } -public(package) fun transfer( +public fun execute_transfer( + bridge: &mut Bridge, safe: &mut BridgeSafe, - _bridge_cap: &BridgeCap, - receiver: address, - amount: u64, + 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, -): bool { - if (!safe::has_token_config(safe)) { return false }; - if (!safe::get_token_is_mint_burn(safe)) { return false }; - if (safe::get_stored_coin_balance(safe) < amount) { return false }; - if (!has_cap(safe::uid(safe))) { return false }; +) { + bridge_module::pre_execute_transfer(bridge, batch_nonce_mvx, &recipients, &amounts, &deposit_nonces, &signatures, clock, ctx); - mint(safe::uid(safe), xmn_treasury, deny_list, amount, receiver, ctx); - safe::subtract_token_balance(safe, amount); + let len = vector::length(&recipients); + let mut i = 0; + while (i < len) { + let success = transfer(safe, bridge_module::bridge_cap(bridge), *vector::borrow(&recipients, i), *vector::borrow(&amounts, i), xmn_treasury, deny_list, ctx); + bridge_module::record_transfer_result(bridge, batch_nonce_mvx, success); + i = i + 1; + }; - true + bridge_module::finalize_batch(bridge, batch_nonce_mvx, len, is_batch_complete, clock); } // === Admin Management === @@ -83,6 +91,26 @@ public fun remove_token_from_whitelist(safe: &mut BridgeSafe, ctx: &mut TxCon // === 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(safe)) { return false }; + if (!safe::get_token_is_mint_burn(safe)) { return false }; + if (safe::get_stored_coin_balance(safe) < amount) { return false }; + if (!has_cap(safe::uid(safe))) { return false }; + + mint(safe::uid(safe), xmn_treasury, deny_list, amount, receiver, ctx); + safe::subtract_token_balance(safe, amount); + + true +} + fun cap_key(): CapKey { CapKey { token_type: utils::type_name_bytes() } } @@ -123,3 +151,29 @@ public(package) fun mint( let cap = dof::borrow(id, cap_key()); stablecoin_treasury::mint(xmn_treasury, 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_module::pre_execute_transfer_for_testing(bridge, batch_nonce_mvx, clock); + + let len = vector::length(&recipients); + let mut i = 0; + while (i < len) { + let success = transfer(safe, bridge_module::bridge_cap(bridge), *vector::borrow(&recipients, i), *vector::borrow(&amounts, i), xmn_treasury, deny_list, ctx); + bridge_module::record_transfer_result(bridge, batch_nonce_mvx, success); + i = i + 1; + }; + + bridge_module::finalize_batch(bridge, batch_nonce_mvx, len, is_batch_complete, clock); +} diff --git a/sources/shared_structs.move b/sources/shared_structs.move index b4a9f99..6521aac 100644 --- a/sources/shared_structs.move +++ b/sources/shared_structs.move @@ -1,6 +1,5 @@ module shared_structs::shared_structs; -use sui::object::ID; use sui::table::{Self, Table}; public enum DepositStatus has copy, drop, store { diff --git a/tests/bridge_comprehensive_tests.move b/tests/bridge_comprehensive_tests.move index 18b392d..e9aa601 100644 --- a/tests/bridge_comprehensive_tests.move +++ b/tests/bridge_comprehensive_tests.move @@ -1026,8 +1026,8 @@ fun setup_bridge_with_relayers_for_quorum(): (Scenario, vector>, vect fun create_test_signature_for_quorum(public_key: &vector): vector { let mut signature = vector::empty(); - let mut i = 0; - while (i < 64) { + let mut i = 0u64; + while (i < 64u64) { vector::push_back(&mut signature, (i % 256) as u8); i = i + 1; }; From f818aa591a0ca217706f878add9adfa32e8c5c4c Mon Sep 17 00:00:00 2001 From: Cristi Corcoveanu Date: Thu, 12 Mar 2026 17:37:03 +0200 Subject: [PATCH 14/30] added some more unit tests --- tests/deposit_transfer_tests.move | 170 ++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/tests/deposit_transfer_tests.move b/tests/deposit_transfer_tests.move index e6c3891..7bda79f 100644 --- a/tests/deposit_transfer_tests.move +++ b/tests/deposit_transfer_tests.move @@ -1248,3 +1248,173 @@ fun test_deposit_mint_burn_when_paused() { ts::end(scenario); } + +#[test] +fun test_transfer_mint_burn_not_configured() { + let mut scenario = setup_with_treasury(); + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + let mut treasury = ts::take_shared>(&scenario); + let deny_list = ts::take_shared(&scenario); + let bridge_cap = ts::take_from_address(&scenario, ADMIN); + + let success = xmn_mint_cap_adapter::transfer( + &mut safe, &bridge_cap, RECIPIENT, DEPOSIT_AMOUNT, &mut treasury, &deny_list, ts::ctx(&mut scenario), + ); + assert!(!success, 0); + ts::return_shared(safe); + ts::return_shared(treasury); + ts::return_shared(deny_list); + ts::return_to_address(ADMIN, bridge_cap); + }; + + ts::end(scenario); +} + +#[test] +fun test_transfer_mint_burn_wrong_variant() { + let mut scenario = setup_with_treasury(); + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + + safe::whitelist_token(&mut safe, MIN_AMOUNT, MAX_AMOUNT, ts::ctx(&mut scenario)); + let supply = coin::mint_for_testing(DEPOSIT_AMOUNT * 2, ts::ctx(&mut scenario)); + safe::init_supply(&mut safe, supply, ts::ctx(&mut scenario)); + ts::return_shared(safe); + }; + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + let mut treasury = ts::take_shared>(&scenario); + let deny_list = ts::take_shared(&scenario); + let bridge_cap = ts::take_from_address(&scenario, ADMIN); + + let success = xmn_mint_cap_adapter::transfer( + &mut safe, &bridge_cap, RECIPIENT, DEPOSIT_AMOUNT, &mut treasury, &deny_list, ts::ctx(&mut scenario), + ); + assert!(!success, 0); + ts::return_shared(safe); + ts::return_shared(treasury); + ts::return_shared(deny_list); + ts::return_to_address(ADMIN, bridge_cap); + }; + + ts::end(scenario); +} + +#[test] +fun test_transfer_mint_burn_zero_balance() { + let mut scenario = setup_with_treasury(); + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + let treasury = ts::take_shared>(&scenario); + + safe::whitelist_token_internal( + &mut safe, MIN_AMOUNT, MAX_AMOUNT, false, + option::some(object::id(&treasury)), true, ts::ctx(&mut scenario), + ); + ts::return_shared(safe); + ts::return_shared(treasury); + }; + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + let mut treasury = ts::take_shared>(&scenario); + let deny_list = ts::take_shared(&scenario); + let bridge_cap = ts::take_from_address(&scenario, ADMIN); + // balance=0 < DEPOSIT_AMOUNT → returns false + let success = xmn_mint_cap_adapter::transfer( + &mut safe, &bridge_cap, RECIPIENT, DEPOSIT_AMOUNT, &mut treasury, &deny_list, ts::ctx(&mut scenario), + ); + assert!(!success, 0); + ts::return_shared(safe); + ts::return_shared(treasury); + ts::return_shared(deny_list); + ts::return_to_address(ADMIN, bridge_cap); + }; + + ts::end(scenario); +} + +#[test] +fun test_transfer_mint_burn_insufficient_balance() { + let mut scenario = setup_with_treasury(); + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + let treasury = ts::take_shared>(&scenario); + safe::whitelist_token_internal( + &mut safe, MIN_AMOUNT, MAX_AMOUNT, false, + option::some(object::id(&treasury)), true, ts::ctx(&mut scenario), + ); + + safe::add_to_balance_for_testing(&mut safe, DEPOSIT_AMOUNT - 1); + ts::return_shared(safe); + ts::return_shared(treasury); + }; + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + let mut treasury = ts::take_shared>(&scenario); + let deny_list = ts::take_shared(&scenario); + let bridge_cap = ts::take_from_address(&scenario, ADMIN); + let success = xmn_mint_cap_adapter::transfer( + &mut safe, &bridge_cap, RECIPIENT, DEPOSIT_AMOUNT, &mut treasury, &deny_list, ts::ctx(&mut scenario), + ); + assert!(!success, 0); + ts::return_shared(safe); + ts::return_shared(treasury); + ts::return_shared(deny_list); + ts::return_to_address(ADMIN, bridge_cap); + }; + + ts::end(scenario); +} + +#[test] +fun test_transfer_mint_burn_cap_not_registered() { + let mut scenario = setup_with_treasury(); + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + let treasury = ts::take_shared>(&scenario); + safe::whitelist_token_internal( + &mut safe, MIN_AMOUNT, MAX_AMOUNT, false, + option::some(object::id(&treasury)), true, ts::ctx(&mut scenario), + ); + + safe::add_to_balance_for_testing(&mut safe, DEPOSIT_AMOUNT); + ts::return_shared(safe); + ts::return_shared(treasury); + }; + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + let mut treasury = ts::take_shared>(&scenario); + let deny_list = ts::take_shared(&scenario); + let bridge_cap = ts::take_from_address(&scenario, ADMIN); + let success = xmn_mint_cap_adapter::transfer( + &mut safe, &bridge_cap, RECIPIENT, DEPOSIT_AMOUNT, &mut treasury, &deny_list, ts::ctx(&mut scenario), + ); + assert!(!success, 0); + ts::return_shared(safe); + ts::return_shared(treasury); + ts::return_shared(deny_list); + ts::return_to_address(ADMIN, bridge_cap); + }; + + ts::end(scenario); +} + From 50f41079aa95345eb40c0972ddb5747be7d668c6 Mon Sep 17 00:00:00 2001 From: Cristi Corcoveanu Date: Thu, 12 Mar 2026 17:50:22 +0200 Subject: [PATCH 15/30] fixed merged deposit event method on new code --- sources/mint_burn_adapters/xmn_mint_cap_adapter.move | 2 +- sources/safe.move | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sources/mint_burn_adapters/xmn_mint_cap_adapter.move b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move index 0410c8d..4b736ca 100644 --- a/sources/mint_burn_adapters/xmn_mint_cap_adapter.move +++ b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move @@ -36,7 +36,7 @@ public fun deposit( burn(safe::uid(safe), xmn_treasury, deny_list, coin_in, ctx); - events::emit_deposit(batch_nonce, dep_nonce, tx_context::sender(ctx), recipient, amount, key); + events::emit_deposit_v1(batch_nonce, dep_nonce, tx_context::sender(ctx), recipient, amount, key); } public fun execute_transfer( diff --git a/sources/safe.move b/sources/safe.move index 41fcf31..c4fcfe9 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -135,7 +135,7 @@ public fun deposit( bag::add(&mut safe.coin_storage, key, coin_in); }; - events::emit_deposit(batch_nonce, dep_nonce, tx_context::sender(ctx), recipient, amount, key); + events::emit_deposit_v1(batch_nonce, dep_nonce, tx_context::sender(ctx), recipient, amount, key); } /// Transfer function for native tokens: splits coin from the safe's bag and sends to receiver. @@ -820,7 +820,7 @@ public fun deposit_mint_burn_for_testing( let (key, amount, batch_nonce, dep_nonce) = deposit_validate_and_record(safe, &coin_in, recipient, true, clock, ctx); test_utils::destroy(coin_in); - events::emit_deposit(batch_nonce, dep_nonce, tx_context::sender(ctx), recipient, amount, key); + events::emit_deposit_v1(batch_nonce, dep_nonce, tx_context::sender(ctx), recipient, amount, key); } #[test_only] From 88155b975cdc859ada76bdab38c98228821ac697 Mon Sep 17 00:00:00 2001 From: Cristi Corcoveanu Date: Fri, 13 Mar 2026 11:16:47 +0200 Subject: [PATCH 16/30] removed locked_token dependency --- Move.lock | 77 ------------------------------------------------------- Move.toml | 1 - 2 files changed, 78 deletions(-) delete mode 100644 Move.lock diff --git a/Move.lock b/Move.lock deleted file mode 100644 index 83d5371..0000000 --- a/Move.lock +++ /dev/null @@ -1,77 +0,0 @@ -# Generated by move; do not edit -# This file should be checked in. - -[move] -version = 4 - -[pinned.mainnet.MoveStdlib] -source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "655576caa3d7faaed39ebbc15d1bcc91d0761aee" } -use_environment = "mainnet" -manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363" -deps = {} - -[pinned.mainnet.Sui] -source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "655576caa3d7faaed39ebbc15d1bcc91d0761aee" } -use_environment = "mainnet" -manifest_digest = "CD547CB1ACCE0880C835DAED2D8FFCB91D56C833AE5240D3AA5B918398263195" -deps = { MoveStdlib = "MoveStdlib" } - -[pinned.mainnet.locked_token] -source = { git = "https://github.com/multiversx/mx-locked-token-sc-sui.git", subdir = ".", rev = "fdeec1e875b5a36aa42f62da293e65e15623bba6" } -use_environment = "mainnet" -manifest_digest = "74D499A47226D8F7A178930B275B879EE47F383206AD6C7E5C4A353ECFF5DD5E" -deps = { std = "MoveStdlib", sui = "Sui", sui_extensions = "sui_extensions" } - -[pinned.mainnet.mx_bridge_sc_sui] -source = { root = true } -use_environment = "mainnet" -manifest_digest = "738E2A0AD4F874F7CC5A487CB194A6962F83D7308931512299820CE9B9301812" -deps = { locked_token = "locked_token", std = "MoveStdlib", sui = "Sui", treasury = "treasury" } - -[pinned.mainnet.sui_extensions] -source = { git = "https://github.com/multiversx/stablecoin-sui.git", subdir = "packages/sui_extensions", rev = "cf6a0cd6cf2272c889be49051e8c3bac5251cd83" } -use_environment = "mainnet" -manifest_digest = "E41BBD67BE8940D26C79D78B028477EF5B33BA217A1282C78ACB344CF8A5ECF6" -deps = { std = "MoveStdlib", sui = "Sui" } - -[pinned.mainnet.treasury] -source = { git = "https://github.com/multiversx/stablecoin-sui.git", subdir = "packages/treasury", rev = "cf6a0cd6cf2272c889be49051e8c3bac5251cd83" } -use_environment = "mainnet" -manifest_digest = "6B08AFABA09DC0D0216E97EB27C33CD7A3791E8DC3631C94FB08528F77C1E3AD" -deps = { std = "MoveStdlib", sui = "Sui", sui_extensions = "sui_extensions" } - -[pinned.testnet.MoveStdlib] -source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "655576caa3d7faaed39ebbc15d1bcc91d0761aee" } -use_environment = "testnet" -manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363" -deps = {} - -[pinned.testnet.Sui] -source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "655576caa3d7faaed39ebbc15d1bcc91d0761aee" } -use_environment = "testnet" -manifest_digest = "7AFB66695545775FBFBB2D3078ADFD084244D5002392E837FDE21D9EA1C6D01C" -deps = { MoveStdlib = "MoveStdlib" } - -[pinned.testnet.locked_token] -source = { git = "https://github.com/multiversx/mx-locked-token-sc-sui.git", subdir = ".", rev = "fdeec1e875b5a36aa42f62da293e65e15623bba6" } -use_environment = "testnet" -manifest_digest = "F2724BA0C80232B5E6AB2D4ABD3C023C3CF01390712D263D5BDE10231E1830D4" -deps = { std = "MoveStdlib", sui = "Sui", sui_extensions = "sui_extensions" } - -[pinned.testnet.mx_bridge_sc_sui] -source = { root = true } -use_environment = "testnet" -manifest_digest = "D8B8C5E8B49057924BC8F61F7F6EE18449CAE93F1153F6C53FB1173BA951D47C" -deps = { locked_token = "locked_token", std = "MoveStdlib", sui = "Sui", treasury = "treasury" } - -[pinned.testnet.sui_extensions] -source = { git = "https://github.com/multiversx/stablecoin-sui.git", subdir = "packages/sui_extensions", rev = "cf6a0cd6cf2272c889be49051e8c3bac5251cd83" } -use_environment = "testnet" -manifest_digest = "5745706258F61D6CE210904B3E6AE87A73CE9D31A6F93BE4718C442529332A87" -deps = { std = "MoveStdlib", sui = "Sui" } - -[pinned.testnet.treasury] -source = { git = "https://github.com/multiversx/stablecoin-sui.git", subdir = "packages/treasury", rev = "cf6a0cd6cf2272c889be49051e8c3bac5251cd83" } -use_environment = "testnet" -manifest_digest = "54E813390EFE2FEE4DA372CD4F8B20971B2E7A89DA6E51212EDE743C2DC4EA46" -deps = { std = "MoveStdlib", sui = "Sui", sui_extensions = "sui_extensions" } diff --git a/Move.toml b/Move.toml index 2223401..1e8619d 100644 --- a/Move.toml +++ b/Move.toml @@ -4,7 +4,6 @@ edition = "2024.beta" version = "0.0.1" [dependencies] -locked_token = { git = "https://github.com/multiversx/mx-locked-token-sc-sui.git", rev = "dependency-fix", subdir = "" } treasury = { git = "https://github.com/multiversx/stablecoin-sui.git", rev="xmn-launch", subdir = "packages/treasury" } [addresses] From 7f0a9443505560a7fea1348c593eaf0b2cd1c145 Mon Sep 17 00:00:00 2001 From: georgedigkas Date: Fri, 20 Mar 2026 09:19:31 +0200 Subject: [PATCH 17/30] Format code --- sources/bridge_module.move | 62 +++++-- sources/bridge_roles.move | 12 +- sources/events.move | 2 +- .../xmn_mint_cap_adapter.move | 60 ++++++- sources/pausable.move | 2 +- sources/safe.move | 61 ++++++- sources/shared_structs.move | 19 ++- sources/upgrade_manager.move | 26 +-- sources/utils.move | 2 +- sources/version_control.move | 2 +- tests/bridge_comprehensive_tests.move | 111 +++++++------ tests/bridge_roles_tests.move | 106 ++++++------ tests/deposit_transfer_tests.move | 153 ++++++++++++++--- tests/events_tests.move | 145 ++++++++--------- tests/safe_internal_tests.move | 59 ++++--- tests/shared_structs_tests.move | 154 +++++++++--------- tests/upgrade_tests.move | 36 ++-- 17 files changed, 627 insertions(+), 385 deletions(-) diff --git a/sources/bridge_module.move b/sources/bridge_module.move index d08ff1a..74238fb 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,11 +7,11 @@ 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::utils; -use bridge_safe::bridge_version_control; use shared_structs::shared_structs::{Self, Deposit, Batch, CrossTransferStatus, DepositStatus}; use std::u64::{min, max}; use sui::address; @@ -270,12 +270,27 @@ public fun execute_transfer( clock: &Clock, ctx: &mut TxContext, ) { - pre_execute_transfer(bridge, batch_nonce_mvx, &recipients, &amounts, &deposit_nonces, &signatures, clock, ctx); + pre_execute_transfer( + bridge, + batch_nonce_mvx, + &recipients, + &amounts, + &deposit_nonces, + &signatures, + clock, + ctx, + ); let len = vector::length(&recipients); let mut i = 0; while (i < len) { - let success = safe::transfer(safe, &bridge.bridge_cap, *vector::borrow(&recipients, i), *vector::borrow(&amounts, i), ctx); + let success = safe::transfer( + safe, + &bridge.bridge_cap, + *vector::borrow(&recipients, i), + *vector::borrow(&amounts, i), + ctx, + ); record_transfer_result(bridge, batch_nonce_mvx, success); i = i + 1; }; @@ -283,12 +298,12 @@ public fun execute_transfer( 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); + 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); } @@ -480,7 +495,13 @@ public fun execute_transfer_for_testing( let len = vector::length(&recipients); let mut i = 0; while (i < len) { - let success = safe::transfer(safe, &bridge.bridge_cap, *vector::borrow(&recipients, i), *vector::borrow(&amounts, i), ctx); + let success = safe::transfer( + safe, + &bridge.bridge_cap, + *vector::borrow(&recipients, i), + *vector::borrow(&amounts, i), + ctx, + ); record_transfer_result(bridge, batch_nonce_mvx, success); i = i + 1; }; @@ -522,11 +543,18 @@ public(package) fun pre_execute_transfer( } /// 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) { +public(package) fun record_transfer_result( + bridge: &mut Bridge, + batch_nonce_mvx: u64, + success: bool, +) { if (success) { vector::push_back(&mut bridge.transfer_statuses, shared_structs::deposit_status_executed()); if (table::contains(&bridge.successful_transfers_by_batch, batch_nonce_mvx)) { - let count = table::borrow_mut(&mut bridge.successful_transfers_by_batch, batch_nonce_mvx); + let count = table::borrow_mut( + &mut bridge.successful_transfers_by_batch, + batch_nonce_mvx, + ); *count = *count + 1; } else { table::add(&mut bridge.successful_transfers_by_batch, batch_nonce_mvx, 1); @@ -553,7 +581,9 @@ public(package) fun finalize_batch( table::add(&mut bridge.cross_transfer_statuses, batch_nonce_mvx, cross_status); bridge.transfer_statuses = vector::empty(); - let successful_count = if (table::contains(&bridge.successful_transfers_by_batch, batch_nonce_mvx)) { + let successful_count = if ( + table::contains(&bridge.successful_transfers_by_batch, batch_nonce_mvx) + ) { let count = *table::borrow(&bridge.successful_transfers_by_batch, batch_nonce_mvx); table::remove(&mut bridge.successful_transfers_by_batch, batch_nonce_mvx); count @@ -561,7 +591,11 @@ public(package) fun finalize_batch( 0 }; - event::emit(BatchExecuted { batch_nonce_mvx, transfers_count: total_transfers, successful_transfers: successful_count }); + event::emit(BatchExecuted { + batch_nonce_mvx, + transfers_count: total_transfers, + successful_transfers: successful_count, + }); }; } 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 bff45c5..751546e 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. diff --git a/sources/mint_burn_adapters/xmn_mint_cap_adapter.move b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move index 4b736ca..8317f3c 100644 --- a/sources/mint_burn_adapters/xmn_mint_cap_adapter.move +++ b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move @@ -31,12 +31,25 @@ public fun deposit( ) { assert!(has_cap(safe::uid(safe)), EMintBurnCapNotFound); - let (key, amount, batch_nonce, dep_nonce) = - safe::deposit_validate_and_record(safe, &coin_in, recipient, true, clock, ctx); + let (key, amount, batch_nonce, dep_nonce) = safe::deposit_validate_and_record( + safe, + &coin_in, + recipient, + true, + clock, + ctx, + ); burn(safe::uid(safe), xmn_treasury, deny_list, coin_in, ctx); - events::emit_deposit_v1(batch_nonce, dep_nonce, tx_context::sender(ctx), recipient, amount, key); + events::emit_deposit_v1( + batch_nonce, + dep_nonce, + tx_context::sender(ctx), + recipient, + amount, + key, + ); } public fun execute_transfer( @@ -53,12 +66,29 @@ public fun execute_transfer( clock: &Clock, ctx: &mut TxContext, ) { - bridge_module::pre_execute_transfer(bridge, batch_nonce_mvx, &recipients, &amounts, &deposit_nonces, &signatures, clock, ctx); + bridge_module::pre_execute_transfer( + bridge, + batch_nonce_mvx, + &recipients, + &amounts, + &deposit_nonces, + &signatures, + clock, + ctx, + ); let len = vector::length(&recipients); let mut i = 0; while (i < len) { - let success = transfer(safe, bridge_module::bridge_cap(bridge), *vector::borrow(&recipients, i), *vector::borrow(&amounts, i), xmn_treasury, deny_list, ctx); + let success = transfer( + safe, + bridge_module::bridge_cap(bridge), + *vector::borrow(&recipients, i), + *vector::borrow(&amounts, i), + xmn_treasury, + deny_list, + ctx, + ); bridge_module::record_transfer_result(bridge, batch_nonce_mvx, success); i = i + 1; }; @@ -77,7 +107,15 @@ public fun whitelist_token( ctx: &TxContext, ) { assert!(!has_cap(safe::uid(safe)), EMintBurnCapAlreadyRegistered); - safe::whitelist_token_internal(safe, minimum_amount, maximum_amount, false, option::some(treasury_id), true, ctx); + safe::whitelist_token_internal( + safe, + minimum_amount, + maximum_amount, + false, + option::some(treasury_id), + true, + ctx, + ); register(safe::uid_mut(safe), cap); } @@ -170,7 +208,15 @@ public fun execute_transfer_for_testing( let len = vector::length(&recipients); let mut i = 0; while (i < len) { - let success = transfer(safe, bridge_module::bridge_cap(bridge), *vector::borrow(&recipients, i), *vector::borrow(&amounts, i), xmn_treasury, deny_list, ctx); + let success = transfer( + safe, + bridge_module::bridge_cap(bridge), + *vector::borrow(&recipients, i), + *vector::borrow(&amounts, i), + xmn_treasury, + deny_list, + ctx, + ); bridge_module::record_transfer_result(bridge, batch_nonce_mvx, success); i = i + 1; }; diff --git a/sources/pausable.move b/sources/pausable.move index 97fcf98..81b0cfe 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; diff --git a/sources/safe.move b/sources/safe.move index c4fcfe9..55c3c36 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -126,8 +126,14 @@ public fun deposit( clock: &Clock, ctx: &mut TxContext, ) { - let (key, amount, batch_nonce, dep_nonce) = - deposit_validate_and_record(safe, &coin_in, recipient, false, clock, ctx); + let (key, amount, batch_nonce, dep_nonce) = deposit_validate_and_record( + safe, + &coin_in, + recipient, + false, + clock, + ctx, + ); if (bag::contains(&safe.coin_storage, key)) { coin::join(bag::borrow_mut, Coin>(&mut safe.coin_storage, key), coin_in); @@ -135,7 +141,14 @@ public fun deposit( bag::add(&mut safe.coin_storage, key, coin_in); }; - events::emit_deposit_v1(batch_nonce, dep_nonce, tx_context::sender(ctx), recipient, amount, key); + events::emit_deposit_v1( + batch_nonce, + dep_nonce, + tx_context::sender(ctx), + recipient, + amount, + key, + ); } /// Transfer function for native tokens: splits coin from the safe's bag and sends to receiver. @@ -421,7 +434,15 @@ public fun whitelist_token( maximum_amount: u64, ctx: &mut TxContext, ) { - whitelist_token_internal(safe, minimum_amount, maximum_amount, true, option::none(), false, ctx); + whitelist_token_internal( + safe, + minimum_amount, + maximum_amount, + true, + option::none(), + false, + ctx, + ); } public fun remove_token_from_whitelist(safe: &mut BridgeSafe, ctx: &mut TxContext) { @@ -699,7 +720,10 @@ public(package) fun deposit_validate_and_record( let key = utils::type_name_bytes(); let cfg_ref = table::borrow(&safe.token_cfg, key); assert!(shared_structs::token_config_whitelisted(cfg_ref), ETokenNotWhitelisted); - assert!(shared_structs::token_config_is_mint_burn(cfg_ref) == expect_mint_burn, EIncompatibleTokenFlags); + assert!( + shared_structs::token_config_is_mint_burn(cfg_ref) == expect_mint_burn, + EIncompatibleTokenFlags, + ); let amount = coin::value(coin_in); assert!(amount > 0, EZeroAmount); @@ -715,7 +739,13 @@ public(package) fun deposit_validate_and_record( 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); + 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()); @@ -817,10 +847,23 @@ public fun deposit_mint_burn_for_testing( ctx: &mut TxContext, ) { use sui::test_utils; - let (key, amount, batch_nonce, dep_nonce) = - deposit_validate_and_record(safe, &coin_in, recipient, true, clock, ctx); + let (key, amount, batch_nonce, dep_nonce) = deposit_validate_and_record( + safe, + &coin_in, + recipient, + true, + clock, + ctx, + ); test_utils::destroy(coin_in); - events::emit_deposit_v1(batch_nonce, dep_nonce, tx_context::sender(ctx), recipient, amount, key); + events::emit_deposit_v1( + batch_nonce, + dep_nonce, + tx_context::sender(ctx), + recipient, + amount, + key, + ); } #[test_only] diff --git a/sources/shared_structs.move b/sources/shared_structs.move index 6521aac..2cd7f82 100644 --- a/sources/shared_structs.move +++ b/sources/shared_structs.move @@ -202,12 +202,27 @@ public fun upsert_token_config( ) { if (table::contains(config, key)) { let cfg = table::borrow_mut(config, key); - set_token_config(cfg, whitelisted, is_native, min_limit, max_limit, treasury_id, is_mint_burn); + set_token_config( + cfg, + whitelisted, + is_native, + min_limit, + max_limit, + treasury_id, + is_mint_burn, + ); return; }; - let cfg = create_token_config(whitelisted, is_native, min_limit, max_limit, treasury_id, is_mint_burn); + let cfg = create_token_config( + whitelisted, + is_native, + min_limit, + max_limit, + treasury_id, + is_mint_burn, + ); table::add(config, key, cfg); } diff --git a/sources/upgrade_manager.move b/sources/upgrade_manager.move index 488b44f..889528d 100644 --- a/sources/upgrade_manager.move +++ b/sources/upgrade_manager.move @@ -1,13 +1,13 @@ /// Upgrade Manager - Coordinated System Upgrades -/// +/// /// This module manages coordinated upgrades across both Bridge and BridgeSafe objects, /// ensuring version compatibility and providing a unified upgrade interface. module bridge_safe::upgrade_manager; use bridge_safe::bridge::{Self, Bridge}; -use bridge_safe::safe::{Self, BridgeSafe}; use bridge_safe::bridge_version_control; +use bridge_safe::safe::{Self, BridgeSafe}; use sui::event; // === Events === @@ -24,18 +24,14 @@ public struct SystemUpgradeCompleted has copy, drop { } /// Start coordinated migration across both Safe and Bridge -public fun start_system_migration( - safe: &mut BridgeSafe, - bridge: &mut Bridge, - ctx: &mut TxContext, -) { +public fun start_system_migration(safe: &mut BridgeSafe, bridge: &mut Bridge, ctx: &mut TxContext) { // Verify ownership through safe safe::checkOwnerRole(safe, ctx); - + // Start migration for both components safe::start_migration(safe, ctx); bridge::start_bridge_migration(bridge, safe, ctx); - + event::emit(SystemUpgradeInitiated { safe_versions: safe::compatible_versions(safe), bridge_versions: bridge::bridge_compatible_versions(bridge), @@ -50,11 +46,11 @@ public fun complete_system_migration( ctx: &mut TxContext, ) { safe::checkOwnerRole(safe, ctx); - + // Complete migration for both components safe::complete_migration(safe, ctx); bridge::complete_bridge_migration(bridge, safe, ctx); - + event::emit(SystemUpgradeCompleted { new_version: bridge_version_control::current_version(), previous_versions: vector[bridge_version_control::current_version() - 1], // Previous version @@ -62,13 +58,9 @@ public fun complete_system_migration( } /// Abort coordinated migration if needed -public fun abort_system_migration( - safe: &mut BridgeSafe, - bridge: &mut Bridge, - ctx: &mut TxContext, -) { +public fun abort_system_migration(safe: &mut BridgeSafe, bridge: &mut Bridge, ctx: &mut TxContext) { safe::checkOwnerRole(safe, ctx); - + // Abort migration for both components safe::abort_migration(safe, ctx); bridge::abort_bridge_migration(bridge, safe, ctx); diff --git a/sources/utils.move b/sources/utils.move index 5c8eee9..49e40c7 100644 --- a/sources/utils.move +++ b/sources/utils.move @@ -1,5 +1,5 @@ /// Utils Module - Utility Functions -/// +/// /// This module provides common utility functions used across the bridge system. module bridge_safe::utils; diff --git a/sources/version_control.move b/sources/version_control.move index 65109da..db3c1e8 100644 --- a/sources/version_control.move +++ b/sources/version_control.move @@ -1,5 +1,5 @@ /// Version Control - Package Version Management -/// +/// /// This module manages package versioning and compatibility checks /// for the bridge system upgrade process. diff --git a/tests/bridge_comprehensive_tests.move b/tests/bridge_comprehensive_tests.move index e9aa601..bcb0273 100644 --- a/tests/bridge_comprehensive_tests.move +++ b/tests/bridge_comprehensive_tests.move @@ -984,19 +984,19 @@ fun test_getAddressFromPublicKey() { fun setup_bridge_with_relayers_for_quorum(): (Scenario, vector>, vector
) { let mut scenario = ts::begin(ADMIN); - + let pk1 = x"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; let pk2 = x"abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"; let pk3 = x"fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321"; let pk4 = x"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; - + let public_keys = vector[pk1, pk2, pk3, pk4]; - + let addr1 = bridge::getAddressFromPublicKeyTest(&pk1); let addr2 = bridge::getAddressFromPublicKeyTest(&pk2); let addr3 = bridge::getAddressFromPublicKeyTest(&pk3); let addr4 = bridge::getAddressFromPublicKeyTest(&pk4); - + let relayer_addresses = vector[addr1, addr2, addr3, addr4]; scenario.next_tx(ADMIN); @@ -1008,57 +1008,70 @@ fun setup_bridge_with_relayers_for_quorum(): (Scenario, vector>, vect { let safe = scenario.take_shared(); let bridge_cap = scenario.take_from_address(ADMIN); - + bridge::initialize( public_keys, - 3, + 3, object::id_address(&safe), bridge_cap, - scenario.ctx() + scenario.ctx(), ); - + ts::return_shared(safe); }; - + (scenario, public_keys, relayer_addresses) } fun create_test_signature_for_quorum(public_key: &vector): vector { let mut signature = vector::empty(); - + let mut i = 0u64; while (i < 64u64) { vector::push_back(&mut signature, (i % 256) as u8); i = i + 1; }; - + vector::append(&mut signature, *public_key); - + signature } #[test] -#[expected_failure(abort_code = bridge::EInvalidSignature)] // Expecting failure at signature verification +#[ + expected_failure( + abort_code = bridge::EInvalidSignature, + ), +] // Expecting failure at signature verification fun test_validate_quorum_reaches_signature_verification() { let (mut scenario, public_keys, _relayer_addresses) = setup_bridge_with_relayers_for_quorum(); - + scenario.next_tx(ADMIN); { let bridge = scenario.take_shared(); - + // Create test data let batch_id = 1u64; let recipients = vector[@0x123, @0x456, @0x789]; let amounts = vector[100u64, 200u64, 300u64]; let deposit_nonces = vector[1u64, 2u64, 3u64]; - + // Create signatures for 3 out of 4 relayers (meeting quorum of 3) // These will have correct format but invalid cryptographic signatures let mut signatures = vector::empty>(); - vector::push_back(&mut signatures, create_test_signature_for_quorum(vector::borrow(&public_keys, 0))); - vector::push_back(&mut signatures, create_test_signature_for_quorum(vector::borrow(&public_keys, 1))); - vector::push_back(&mut signatures, create_test_signature_for_quorum(vector::borrow(&public_keys, 2))); - + vector::push_back( + &mut signatures, + create_test_signature_for_quorum(vector::borrow(&public_keys, 0)), + ); + vector::push_back( + &mut signatures, + create_test_signature_for_quorum(vector::borrow(&public_keys, 1)), + ); + vector::push_back( + &mut signatures, + create_test_signature_for_quorum(vector::borrow(&public_keys, 2)), + ); + // This should fail at signature verification (proving we got through initial checks) bridge::validate_quorum_for_testing( &bridge, @@ -1066,12 +1079,12 @@ fun test_validate_quorum_reaches_signature_verification() { &recipients, &amounts, &signatures, - &deposit_nonces + &deposit_nonces, ); - + ts::return_shared(bridge); }; - + ts::end(scenario); } @@ -1079,22 +1092,28 @@ fun test_validate_quorum_reaches_signature_verification() { #[expected_failure(abort_code = bridge::EQuorumNotReached)] fun test_validate_quorum_insufficient_signatures() { let (mut scenario, public_keys, _relayer_addresses) = setup_bridge_with_relayers_for_quorum(); - + scenario.next_tx(ADMIN); { let bridge = scenario.take_shared(); - + // Create test data let batch_id = 1u64; let recipients = vector[@0x123, @0x456]; let amounts = vector[100u64, 200u64]; let deposit_nonces = vector[1u64, 2u64]; - + // Create signatures for only 2 out of 4 relayers (below quorum of 3) let mut signatures = vector::empty>(); - vector::push_back(&mut signatures, create_test_signature_for_quorum(vector::borrow(&public_keys, 0))); - vector::push_back(&mut signatures, create_test_signature_for_quorum(vector::borrow(&public_keys, 1))); - + vector::push_back( + &mut signatures, + create_test_signature_for_quorum(vector::borrow(&public_keys, 0)), + ); + vector::push_back( + &mut signatures, + create_test_signature_for_quorum(vector::borrow(&public_keys, 1)), + ); + // This should fail as we have fewer signatures than quorum bridge::validate_quorum_for_testing( &bridge, @@ -1102,12 +1121,12 @@ fun test_validate_quorum_insufficient_signatures() { &recipients, &amounts, &signatures, - &deposit_nonces + &deposit_nonces, ); - + ts::return_shared(bridge); }; - + ts::end(scenario); } @@ -1115,24 +1134,24 @@ fun test_validate_quorum_insufficient_signatures() { #[expected_failure(abort_code = bridge::EInvalidSignatureLength)] fun test_validate_quorum_invalid_signature_length() { let (mut scenario, _public_keys, _relayer_addresses) = setup_bridge_with_relayers_for_quorum(); - + scenario.next_tx(ADMIN); { let bridge = scenario.take_shared(); - + // Create test data let batch_id = 1u64; let recipients = vector[@0x123]; let amounts = vector[100u64]; let deposit_nonces = vector[1u64]; - + // Create signatures with invalid length (should be 96 bytes) let mut signatures = vector::empty>(); let invalid_signature = vector[1u8, 2u8, 3u8]; // Only 3 bytes instead of 96 vector::push_back(&mut signatures, invalid_signature); vector::push_back(&mut signatures, invalid_signature); vector::push_back(&mut signatures, invalid_signature); - + // This should fail due to invalid signature length bridge::validate_quorum_for_testing( &bridge, @@ -1140,12 +1159,12 @@ fun test_validate_quorum_invalid_signature_length() { &recipients, &amounts, &signatures, - &deposit_nonces + &deposit_nonces, ); - + ts::return_shared(bridge); }; - + ts::end(scenario); } @@ -1153,24 +1172,24 @@ fun test_validate_quorum_invalid_signature_length() { #[expected_failure(abort_code = bridge::ERelayerNotFound)] fun test_validate_quorum_unknown_relayer() { let (mut scenario, _public_keys, _relayer_addresses) = setup_bridge_with_relayers_for_quorum(); - + scenario.next_tx(ADMIN); { let bridge = scenario.take_shared(); - + // Create test data let batch_id = 1u64; let recipients = vector[@0x123]; let amounts = vector[100u64]; let deposit_nonces = vector[1u64]; - + // Create signatures with unknown public keys (not in relayer list) let unknown_pk = x"9999999999999999999999999999999999999999999999999999999999999999"; let mut signatures = vector::empty>(); vector::push_back(&mut signatures, create_test_signature_for_quorum(&unknown_pk)); vector::push_back(&mut signatures, create_test_signature_for_quorum(&unknown_pk)); vector::push_back(&mut signatures, create_test_signature_for_quorum(&unknown_pk)); - + // This should fail because the public key is not from a known relayer bridge::validate_quorum_for_testing( &bridge, @@ -1178,11 +1197,11 @@ fun test_validate_quorum_unknown_relayer() { &recipients, &amounts, &signatures, - &deposit_nonces + &deposit_nonces, ); - + ts::return_shared(bridge); }; - + ts::end(scenario); } diff --git a/tests/bridge_roles_tests.move b/tests/bridge_roles_tests.move index bb19a8d..3179f82 100644 --- a/tests/bridge_roles_tests.move +++ b/tests/bridge_roles_tests.move @@ -2,7 +2,7 @@ module bridge_safe::bridge_roles_tests; use bridge_safe::bridge_roles::{Self, BridgeCap, BridgeSafeTag}; -use sui::test_scenario::{Self as ts}; +use sui::test_scenario as ts; use sui_extensions::two_step_role; const ADMIN: address = @0xa11ce; @@ -12,97 +12,97 @@ const INVALID_ADDRESS: address = @0x0; #[test] fun test_new_roles() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { let roles = bridge_roles::new(ADMIN, scenario.ctx()); - + // Verify the owner is set correctly assert!(bridge_roles::owner(&roles) == ADMIN, 0); - + // Verify there's no pending owner initially assert!(bridge_roles::pending_owner(&roles).is_none(), 1); - + // Since Roles doesn't have key ability, we just drop it sui::test_utils::destroy(roles); }; - + ts::end(scenario); } #[test] fun test_owner_functions() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { let roles = bridge_roles::new(ADMIN, scenario.ctx()); - + // Test owner getter assert!(bridge_roles::owner(&roles) == ADMIN, 0); - + // Test pending_owner getter (should be none initially) let pending = bridge_roles::pending_owner(&roles); assert!(pending.is_none(), 1); - + sui::test_utils::destroy(roles); }; - + ts::end(scenario); } #[test] fun test_grant_witness() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { // Test granting witness - witness has drop ability so this should work let _witness = bridge_roles::grant_witness(); // Witness is automatically dropped }; - + ts::end(scenario); } #[test] fun test_publish_caps() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { // Test publishing capabilities let witness = bridge_roles::grant_witness(); let bridge_cap = bridge_roles::publish_caps(witness, scenario.ctx()); - + // BridgeCap should be created successfully // Transfer it to admin for cleanup transfer::public_transfer(bridge_cap, ADMIN); }; - + ts::end(scenario); } #[test] fun test_transfer_bridge_capability() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { let witness = bridge_roles::grant_witness(); let bridge_cap = bridge_roles::publish_caps(witness, scenario.ctx()); - + // Test transferring bridge capability to a valid address bridge_roles::transfer_bridge_capability(bridge_cap, NEW_ADMIN); }; - + // Verify the capability was transferred scenario.next_tx(NEW_ADMIN); { let bridge_cap = scenario.take_from_address(NEW_ADMIN); transfer::public_transfer(bridge_cap, NEW_ADMIN); }; - + ts::end(scenario); } @@ -110,150 +110,150 @@ fun test_transfer_bridge_capability() { #[expected_failure(abort_code = 0)] fun test_transfer_bridge_capability_to_zero_address() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { let witness = bridge_roles::grant_witness(); let bridge_cap = bridge_roles::publish_caps(witness, scenario.ctx()); - + // This should fail because we're trying to transfer to zero address bridge_roles::transfer_bridge_capability(bridge_cap, INVALID_ADDRESS); }; - + ts::end(scenario); } #[test] fun test_owner_role_access() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { let mut roles = bridge_roles::new(ADMIN, scenario.ctx()); - + // Test immutable access to owner role let owner_role = bridge_roles::owner_role(&roles); assert!(two_step_role::active_address(owner_role) == ADMIN, 0); assert!(two_step_role::pending_address(owner_role).is_none(), 1); - + // Test mutable access to owner role let owner_role_mut = bridge_roles::owner_role_mut(&mut roles); assert!(two_step_role::active_address(owner_role_mut) == ADMIN, 2); - + sui::test_utils::destroy(roles); }; - + ts::end(scenario); } #[test] fun test_two_step_role_transfer_initiate() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { let mut roles = bridge_roles::new(ADMIN, scenario.ctx()); - + // Initiate transfer to new admin let owner_role_mut = bridge_roles::owner_role_mut(&mut roles); two_step_role::begin_role_transfer(owner_role_mut, NEW_ADMIN, scenario.ctx()); - + // Verify the transfer was initiated - assert!(bridge_roles::owner(&roles) == ADMIN, 0); // Still the current owner + assert!(bridge_roles::owner(&roles) == ADMIN, 0); // Still the current owner assert!(bridge_roles::pending_owner(&roles).is_some(), 1); // Has pending owner assert!(*bridge_roles::pending_owner(&roles).borrow() == NEW_ADMIN, 2); - + sui::test_utils::destroy(roles); }; - + ts::end(scenario); } #[test] fun test_multiple_roles_instances() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { // Create multiple roles instances let roles1 = bridge_roles::new(ADMIN, scenario.ctx()); let roles2 = bridge_roles::new(NEW_ADMIN, scenario.ctx()); - + // Verify they have different owners assert!(bridge_roles::owner(&roles1) == ADMIN, 0); assert!(bridge_roles::owner(&roles2) == NEW_ADMIN, 1); - + // Both should have no pending owners initially assert!(bridge_roles::pending_owner(&roles1).is_none(), 2); assert!(bridge_roles::pending_owner(&roles2).is_none(), 3); - + sui::test_utils::destroy(roles1); sui::test_utils::destroy(roles2); }; - + ts::end(scenario); } #[test] fun test_bridge_cap_properties() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { let witness = bridge_roles::grant_witness(); let bridge_cap = bridge_roles::publish_caps(witness, scenario.ctx()); - + // BridgeCap should have key and store abilities // We can transfer it, which tests both key and store transfer::public_transfer(bridge_cap, ADMIN); }; - + scenario.next_tx(ADMIN); { // Should be able to take it back, confirming it has proper abilities let bridge_cap = scenario.take_from_address(ADMIN); transfer::public_transfer(bridge_cap, ADMIN); }; - + ts::end(scenario); } #[test] fun test_witness_usage_pattern() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { let witness1 = bridge_roles::grant_witness(); let witness2 = bridge_roles::grant_witness(); - + let cap1 = bridge_roles::publish_caps(witness1, scenario.ctx()); let cap2 = bridge_roles::publish_caps(witness2, scenario.ctx()); - + transfer::public_transfer(cap1, ADMIN); transfer::public_transfer(cap2, ADMIN); }; - + ts::end(scenario); } #[test] fun test_role_transfer_edge_cases() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { let mut roles = bridge_roles::new(ADMIN, scenario.ctx()); - + let owner_role_mut = bridge_roles::owner_role_mut(&mut roles); two_step_role::begin_role_transfer(owner_role_mut, ADMIN, scenario.ctx()); - + assert!(bridge_roles::owner(&roles) == ADMIN, 0); assert!(bridge_roles::pending_owner(&roles).is_some(), 1); assert!(*bridge_roles::pending_owner(&roles).borrow() == ADMIN, 2); - + sui::test_utils::destroy(roles); }; - + ts::end(scenario); -} \ No newline at end of file +} diff --git a/tests/deposit_transfer_tests.move b/tests/deposit_transfer_tests.move index 7bda79f..45509dc 100644 --- a/tests/deposit_transfer_tests.move +++ b/tests/deposit_transfer_tests.move @@ -990,13 +990,25 @@ fun test_deposit_mint_burn_multiple_same_batch() { let coin3 = coin::mint_for_testing(3000, ts::ctx(&mut scenario)); safe::deposit_mint_burn_for_testing( - &mut safe, coin1, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario), + &mut safe, + coin1, + RECIPIENT_VECTOR, + &clock, + ts::ctx(&mut scenario), ); safe::deposit_mint_burn_for_testing( - &mut safe, coin2, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario), + &mut safe, + coin2, + RECIPIENT_VECTOR, + &clock, + ts::ctx(&mut scenario), ); safe::deposit_mint_burn_for_testing( - &mut safe, coin3, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario), + &mut safe, + coin3, + RECIPIENT_VECTOR, + &clock, + ts::ctx(&mut scenario), ); assert!(safe::get_deposits_count(&safe) == 3, 0); @@ -1033,14 +1045,26 @@ fun test_deposit_mint_burn_triggers_new_batch() { let coin3 = coin::mint_for_testing(3000, ts::ctx(&mut scenario)); safe::deposit_mint_burn_for_testing( - &mut safe, coin1, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario), + &mut safe, + coin1, + RECIPIENT_VECTOR, + &clock, + ts::ctx(&mut scenario), ); safe::deposit_mint_burn_for_testing( - &mut safe, coin2, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario), + &mut safe, + coin2, + RECIPIENT_VECTOR, + &clock, + ts::ctx(&mut scenario), ); // Third deposit overflows batch_size=2, creates a new batch safe::deposit_mint_burn_for_testing( - &mut safe, coin3, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario), + &mut safe, + coin3, + RECIPIENT_VECTOR, + &clock, + ts::ctx(&mut scenario), ); assert!(safe::get_deposits_count(&safe) == 3, 0); @@ -1070,7 +1094,11 @@ fun test_deposit_mint_burn_invalid_recipient() { let coin = coin::mint_for_testing(DEPOSIT_AMOUNT, ts::ctx(&mut scenario)); safe::deposit_mint_burn_for_testing( - &mut safe, coin, b"0x0", &clock, ts::ctx(&mut scenario), + &mut safe, + coin, + b"0x0", + &clock, + ts::ctx(&mut scenario), ); clock::destroy_for_testing(clock); @@ -1091,7 +1119,11 @@ fun test_deposit_mint_burn_zero_amount() { let coin = coin::mint_for_testing(0, ts::ctx(&mut scenario)); safe::deposit_mint_burn_for_testing( - &mut safe, coin, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario), + &mut safe, + coin, + RECIPIENT_VECTOR, + &clock, + ts::ctx(&mut scenario), ); clock::destroy_for_testing(clock); @@ -1112,7 +1144,11 @@ fun test_deposit_mint_burn_below_minimum() { let coin = coin::mint_for_testing(MIN_AMOUNT - 1, ts::ctx(&mut scenario)); safe::deposit_mint_burn_for_testing( - &mut safe, coin, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario), + &mut safe, + coin, + RECIPIENT_VECTOR, + &clock, + ts::ctx(&mut scenario), ); clock::destroy_for_testing(clock); @@ -1133,7 +1169,11 @@ fun test_deposit_mint_burn_above_maximum() { let coin = coin::mint_for_testing(MAX_AMOUNT + 1, ts::ctx(&mut scenario)); safe::deposit_mint_burn_for_testing( - &mut safe, coin, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario), + &mut safe, + coin, + RECIPIENT_VECTOR, + &clock, + ts::ctx(&mut scenario), ); clock::destroy_for_testing(clock); @@ -1162,7 +1202,11 @@ fun test_deposit_mint_burn_wrong_variant() { let coin = coin::mint_for_testing(DEPOSIT_AMOUNT, ts::ctx(&mut scenario)); safe::deposit_mint_burn_for_testing( - &mut safe, coin, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario), + &mut safe, + coin, + RECIPIENT_VECTOR, + &clock, + ts::ctx(&mut scenario), ); clock::destroy_for_testing(clock); @@ -1239,7 +1283,11 @@ fun test_deposit_mint_burn_when_paused() { let coin = coin::mint_for_testing(DEPOSIT_AMOUNT, ts::ctx(&mut scenario)); safe::deposit_mint_burn_for_testing( - &mut safe, coin, RECIPIENT_VECTOR, &clock, ts::ctx(&mut scenario), + &mut safe, + coin, + RECIPIENT_VECTOR, + &clock, + ts::ctx(&mut scenario), ); clock::destroy_for_testing(clock); @@ -1248,7 +1296,6 @@ fun test_deposit_mint_burn_when_paused() { ts::end(scenario); } - #[test] fun test_transfer_mint_burn_not_configured() { let mut scenario = setup_with_treasury(); @@ -1261,7 +1308,13 @@ fun test_transfer_mint_burn_not_configured() { let bridge_cap = ts::take_from_address(&scenario, ADMIN); let success = xmn_mint_cap_adapter::transfer( - &mut safe, &bridge_cap, RECIPIENT, DEPOSIT_AMOUNT, &mut treasury, &deny_list, ts::ctx(&mut scenario), + &mut safe, + &bridge_cap, + RECIPIENT, + DEPOSIT_AMOUNT, + &mut treasury, + &deny_list, + ts::ctx(&mut scenario), ); assert!(!success, 0); ts::return_shared(safe); @@ -1281,8 +1334,16 @@ fun test_transfer_mint_burn_wrong_variant() { { let mut safe = ts::take_shared(&scenario); - safe::whitelist_token(&mut safe, MIN_AMOUNT, MAX_AMOUNT, ts::ctx(&mut scenario)); - let supply = coin::mint_for_testing(DEPOSIT_AMOUNT * 2, ts::ctx(&mut scenario)); + safe::whitelist_token( + &mut safe, + MIN_AMOUNT, + MAX_AMOUNT, + ts::ctx(&mut scenario), + ); + let supply = coin::mint_for_testing( + DEPOSIT_AMOUNT * 2, + ts::ctx(&mut scenario), + ); safe::init_supply(&mut safe, supply, ts::ctx(&mut scenario)); ts::return_shared(safe); }; @@ -1295,7 +1356,13 @@ fun test_transfer_mint_burn_wrong_variant() { let bridge_cap = ts::take_from_address(&scenario, ADMIN); let success = xmn_mint_cap_adapter::transfer( - &mut safe, &bridge_cap, RECIPIENT, DEPOSIT_AMOUNT, &mut treasury, &deny_list, ts::ctx(&mut scenario), + &mut safe, + &bridge_cap, + RECIPIENT, + DEPOSIT_AMOUNT, + &mut treasury, + &deny_list, + ts::ctx(&mut scenario), ); assert!(!success, 0); ts::return_shared(safe); @@ -1317,8 +1384,13 @@ fun test_transfer_mint_burn_zero_balance() { let treasury = ts::take_shared>(&scenario); safe::whitelist_token_internal( - &mut safe, MIN_AMOUNT, MAX_AMOUNT, false, - option::some(object::id(&treasury)), true, ts::ctx(&mut scenario), + &mut safe, + MIN_AMOUNT, + MAX_AMOUNT, + false, + option::some(object::id(&treasury)), + true, + ts::ctx(&mut scenario), ); ts::return_shared(safe); ts::return_shared(treasury); @@ -1332,7 +1404,13 @@ fun test_transfer_mint_burn_zero_balance() { let bridge_cap = ts::take_from_address(&scenario, ADMIN); // balance=0 < DEPOSIT_AMOUNT → returns false let success = xmn_mint_cap_adapter::transfer( - &mut safe, &bridge_cap, RECIPIENT, DEPOSIT_AMOUNT, &mut treasury, &deny_list, ts::ctx(&mut scenario), + &mut safe, + &bridge_cap, + RECIPIENT, + DEPOSIT_AMOUNT, + &mut treasury, + &deny_list, + ts::ctx(&mut scenario), ); assert!(!success, 0); ts::return_shared(safe); @@ -1353,8 +1431,13 @@ fun test_transfer_mint_burn_insufficient_balance() { let mut safe = ts::take_shared(&scenario); let treasury = ts::take_shared>(&scenario); safe::whitelist_token_internal( - &mut safe, MIN_AMOUNT, MAX_AMOUNT, false, - option::some(object::id(&treasury)), true, ts::ctx(&mut scenario), + &mut safe, + MIN_AMOUNT, + MAX_AMOUNT, + false, + option::some(object::id(&treasury)), + true, + ts::ctx(&mut scenario), ); safe::add_to_balance_for_testing(&mut safe, DEPOSIT_AMOUNT - 1); @@ -1369,7 +1452,13 @@ fun test_transfer_mint_burn_insufficient_balance() { let deny_list = ts::take_shared(&scenario); let bridge_cap = ts::take_from_address(&scenario, ADMIN); let success = xmn_mint_cap_adapter::transfer( - &mut safe, &bridge_cap, RECIPIENT, DEPOSIT_AMOUNT, &mut treasury, &deny_list, ts::ctx(&mut scenario), + &mut safe, + &bridge_cap, + RECIPIENT, + DEPOSIT_AMOUNT, + &mut treasury, + &deny_list, + ts::ctx(&mut scenario), ); assert!(!success, 0); ts::return_shared(safe); @@ -1390,8 +1479,13 @@ fun test_transfer_mint_burn_cap_not_registered() { let mut safe = ts::take_shared(&scenario); let treasury = ts::take_shared>(&scenario); safe::whitelist_token_internal( - &mut safe, MIN_AMOUNT, MAX_AMOUNT, false, - option::some(object::id(&treasury)), true, ts::ctx(&mut scenario), + &mut safe, + MIN_AMOUNT, + MAX_AMOUNT, + false, + option::some(object::id(&treasury)), + true, + ts::ctx(&mut scenario), ); safe::add_to_balance_for_testing(&mut safe, DEPOSIT_AMOUNT); @@ -1406,7 +1500,13 @@ fun test_transfer_mint_burn_cap_not_registered() { let deny_list = ts::take_shared(&scenario); let bridge_cap = ts::take_from_address(&scenario, ADMIN); let success = xmn_mint_cap_adapter::transfer( - &mut safe, &bridge_cap, RECIPIENT, DEPOSIT_AMOUNT, &mut treasury, &deny_list, ts::ctx(&mut scenario), + &mut safe, + &bridge_cap, + RECIPIENT, + DEPOSIT_AMOUNT, + &mut treasury, + &deny_list, + ts::ctx(&mut scenario), ); assert!(!success, 0); ts::return_shared(safe); @@ -1417,4 +1517,3 @@ fun test_transfer_mint_burn_cap_not_registered() { ts::end(scenario); } - diff --git a/tests/events_tests.move b/tests/events_tests.move index ae66970..a6c619d 100644 --- a/tests/events_tests.move +++ b/tests/events_tests.move @@ -2,7 +2,7 @@ module bridge_safe::events_tests; use bridge_safe::events; -use sui::test_scenario::{Self as ts}; +use sui::test_scenario as ts; const ADMIN: address = @0xa11ce; const USER: address = @0xb0b; @@ -13,7 +13,7 @@ const NEW_BRIDGE: address = @0xfeed; #[test] fun test_emit_deposit() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { // Test emitting a deposit event @@ -23,95 +23,95 @@ fun test_emit_deposit() { USER, // sender b"recipient_address_bytes", // recipient 1000, // amount - b"TEST_TOKEN" // token_type + b"TEST_TOKEN", // token_type ); }; - + ts::end(scenario); } #[test] fun test_emit_admin_role_transferred() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { // Test emitting an admin role transferred event events::emit_admin_role_transferred(ADMIN, NEW_ADMIN); }; - + ts::end(scenario); } #[test] fun test_emit_bridge_transferred() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { // Test emitting a bridge transferred event events::emit_bridge_transferred(@0x1234, NEW_BRIDGE); }; - + ts::end(scenario); } #[test] fun test_emit_relayer_added() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { // Test emitting a relayer added event events::emit_relayer_added(RELAYER, ADMIN); }; - + ts::end(scenario); } #[test] fun test_emit_relayer_removed() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { // Test emitting a relayer removed event events::emit_relayer_removed(RELAYER, ADMIN); }; - + ts::end(scenario); } #[test] fun test_emit_pause_true() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { // Test emitting a pause event with true events::emit_pause(true); }; - + ts::end(scenario); } #[test] fun test_emit_pause_false() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { // Test emitting a pause event with false events::emit_pause(false); }; - + ts::end(scenario); } #[test] fun test_emit_token_whitelisted() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { // Test emitting a token whitelisted event @@ -120,17 +120,17 @@ fun test_emit_token_whitelisted() { 100, // min_limit 10000, // max_limit true, // is_native - false // is_mint_burn + false, // is_mint_burn ); }; - + ts::end(scenario); } #[test] fun test_emit_token_whitelisted_all_false() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { // Test emitting a token whitelisted event with all boolean flags false @@ -139,17 +139,17 @@ fun test_emit_token_whitelisted_all_false() { 50, // min_limit 5000, // max_limit false, // is_native - false // is_mint_burn + false, // is_mint_burn ); }; - + ts::end(scenario); } #[test] fun test_emit_token_whitelisted_mint_burn() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { // Test emitting a token whitelisted event with mint_burn true @@ -158,115 +158,115 @@ fun test_emit_token_whitelisted_mint_burn() { 1, // min_limit 1000000, // max_limit false, // is_native - true // is_mint_burn + true, // is_mint_burn ); }; - + ts::end(scenario); } #[test] fun test_emit_token_removed_from_whitelist() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { // Test emitting a token removed from whitelist event events::emit_token_removed_from_whitelist(b"REMOVED_TOKEN"); }; - + ts::end(scenario); } #[test] fun test_emit_token_limits_updated() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { // Test emitting a token limits updated event events::emit_token_limits_updated( b"UPDATED_TOKEN", // token_type 200, // new_min_limit - 20000 // new_max_limit + 20000, // new_max_limit ); }; - + ts::end(scenario); } #[test] fun test_emit_token_is_native_updated() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { // Test emitting a token is native updated event - true events::emit_token_is_native_updated(b"NATIVE_TOKEN", true); }; - + ts::end(scenario); } #[test] fun test_emit_token_is_native_updated_false() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { // Test emitting a token is native updated event - false events::emit_token_is_native_updated(b"NON_NATIVE_TOKEN", false); }; - + ts::end(scenario); } #[test] fun test_emit_token_is_mint_burn_updated() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { // Test emitting a token is mint burn updated event - true events::emit_token_is_mint_burn_updated(b"MINT_BURN_TOKEN", true); }; - + ts::end(scenario); } #[test] fun test_emit_token_is_mint_burn_updated_false() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { // Test emitting a token is mint burn updated event - false events::emit_token_is_mint_burn_updated(b"REGULAR_TOKEN", false); }; - + ts::end(scenario); } #[test] fun test_emit_batch_created() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { // Test emitting a batch created event events::emit_batch_created( 789, // batch_nonce - 1000000 // block_number + 1000000, // block_number ); }; - + ts::end(scenario); } #[test] fun test_emit_transfer_executed_success() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { // Test emitting a transfer executed event - success @@ -274,17 +274,17 @@ fun test_emit_transfer_executed_success() { USER, // recipient 5000, // amount b"TRANSFER_TOKEN", // token_type - true // success + true, // success ); }; - + ts::end(scenario); } #[test] fun test_emit_transfer_executed_failure() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { // Test emitting a transfer executed event - failure @@ -292,83 +292,82 @@ fun test_emit_transfer_executed_failure() { USER, // recipient 5000, // amount b"FAILED_TOKEN", // token_type - false // success + false, // success ); }; - + ts::end(scenario); } #[test] fun test_emit_batch_settings_updated() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { // Test emitting a batch settings updated event events::emit_batch_settings_updated( 100, // batch_size (u16) 50, // batch_block_limit (u8) - 25 // batch_settle_limit (u8) + 25, // batch_settle_limit (u8) ); }; - + ts::end(scenario); } #[test] fun test_emit_batch_settings_updated_max_values() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { events::emit_batch_settings_updated( 65535, // batch_size (u16 max) 255, // batch_block_limit (u8 max) - 255 // batch_settle_limit (u8 max) + 255, // batch_settle_limit (u8 max) ); }; - + ts::end(scenario); } - #[test] fun test_emit_deposit_with_empty_vectors() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { // Test emitting a deposit event with empty vectors events::emit_deposit_v1( - 0, - 0, + 0, + 0, @0x0, - vector::empty(), - 0, - vector::empty() + vector::empty(), + 0, + vector::empty(), ); }; - + ts::end(scenario); } #[test] fun test_emit_deposit_with_large_values() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { events::emit_deposit_v1( - 18446744073709551615, - 18446744073709551615, - @0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, - b"very_long_recipient_address_that_could_potentially_be_quite_large", - 18446744073709551615, - b"VERY_LONG_TOKEN_TYPE_NAME_FOR_TESTING_PURPOSES" + 18446744073709551615, + 18446744073709551615, + @0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, + b"very_long_recipient_address_that_could_potentially_be_quite_large", + 18446744073709551615, + b"VERY_LONG_TOKEN_TYPE_NAME_FOR_TESTING_PURPOSES", ); }; - + ts::end(scenario); } @@ -385,7 +384,7 @@ fun test_old_emit_deposit_aborts() { @0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, b"very_long_recipient_address_that_could_potentially_be_quite_large", 18446744073709551615, - b"VERY_LONG_TOKEN_TYPE_NAME_FOR_TESTING_PURPOSES" + b"VERY_LONG_TOKEN_TYPE_NAME_FOR_TESTING_PURPOSES", ); }; @@ -395,7 +394,7 @@ fun test_old_emit_deposit_aborts() { #[test] fun test_multiple_events_in_sequence() { let mut scenario = ts::begin(ADMIN); - + scenario.next_tx(ADMIN); { events::emit_admin_role_transferred(ADMIN, NEW_ADMIN); @@ -406,6 +405,6 @@ fun test_multiple_events_in_sequence() { events::emit_transfer_executed(USER, 500, b"SEQ_TOKEN", true); events::emit_pause(false); }; - + ts::end(scenario); -} \ No newline at end of file +} diff --git a/tests/safe_internal_tests.move b/tests/safe_internal_tests.move index 3fffa4f..29236c3 100644 --- a/tests/safe_internal_tests.move +++ b/tests/safe_internal_tests.move @@ -1,8 +1,8 @@ #[test_only] module bridge_safe::safe_unit_tests; +use bridge_safe::bridge_roles::BridgeCap; use bridge_safe::pausable; -use bridge_safe::bridge_roles::{BridgeCap}; use bridge_safe::safe::{Self, BridgeSafe}; use sui::clock; use sui::coin; @@ -294,7 +294,6 @@ fun test_set_batch_size() { safe::set_batch_size( &mut safe, - new_size, ts::ctx(&mut scenario), ); @@ -800,12 +799,12 @@ fun test_initial_ownership() { scenario.next_tx(ADMIN); { let safe = ts::take_shared(&scenario); - + assert!(safe::get_owner(&safe) == ADMIN, 0); - + let pending = safe::get_pending_owner(&safe); assert!(pending.is_none(), 1); - + ts::return_shared(safe); }; @@ -819,15 +818,15 @@ fun test_transfer_ownership_initiate() { scenario.next_tx(ADMIN); { let mut safe = ts::take_shared(&scenario); - + safe::transfer_ownership(&mut safe, NEW_OWNER, scenario.ctx()); - + assert!(safe::get_owner(&safe) == ADMIN, 0); - + let pending = safe::get_pending_owner(&safe); assert!(pending.is_some(), 1); assert!(*pending.borrow() == NEW_OWNER, 2); - + ts::return_shared(safe); }; @@ -849,12 +848,12 @@ fun test_complete_ownership_transfer() { { let mut safe = ts::take_shared(&scenario); safe::accept_ownership(&mut safe, scenario.ctx()); - + assert!(safe::get_owner(&safe) == NEW_OWNER, 0); - + let pending = safe::get_pending_owner(&safe); assert!(pending.is_none(), 1); - + ts::return_shared(safe); }; @@ -869,9 +868,9 @@ fun test_transfer_ownership_not_owner() { scenario.next_tx(THIRD_PARTY); { let mut safe = ts::take_shared(&scenario); - + safe::transfer_ownership(&mut safe, NEW_OWNER, scenario.ctx()); - + ts::return_shared(safe); }; @@ -885,15 +884,15 @@ fun test_transfer_ownership_to_same_address() { scenario.next_tx(ADMIN); { let mut safe = ts::take_shared(&scenario); - + safe::transfer_ownership(&mut safe, ADMIN, scenario.ctx()); - + assert!(safe::get_owner(&safe) == ADMIN, 0); - + let pending = safe::get_pending_owner(&safe); assert!(pending.is_some(), 1); assert!(*pending.borrow() == ADMIN, 2); - + ts::return_shared(safe); }; @@ -901,11 +900,11 @@ fun test_transfer_ownership_to_same_address() { { let mut safe = ts::take_shared(&scenario); safe::accept_ownership(&mut safe, scenario.ctx()); - + assert!(safe::get_owner(&safe) == ADMIN, 3); let pending = safe::get_pending_owner(&safe); assert!(pending.is_none(), 4); - + ts::return_shared(safe); }; @@ -972,11 +971,11 @@ fun test_overwrite_pending_ownership_transfer() { { let mut safe = ts::take_shared(&scenario); safe::transfer_ownership(&mut safe, NEW_OWNER, scenario.ctx()); - + let pending = safe::get_pending_owner(&safe); assert!(pending.is_some(), 0); assert!(*pending.borrow() == NEW_OWNER, 1); - + ts::return_shared(safe); }; @@ -984,22 +983,22 @@ fun test_overwrite_pending_ownership_transfer() { { let mut safe = ts::take_shared(&scenario); safe::transfer_ownership(&mut safe, THIRD_PARTY, scenario.ctx()); - + let pending = safe::get_pending_owner(&safe); assert!(pending.is_some(), 2); assert!(*pending.borrow() == THIRD_PARTY, 3); - + ts::return_shared(safe); }; scenario.next_tx(NEW_OWNER); { let safe = ts::take_shared(&scenario); - + let pending = safe::get_pending_owner(&safe); assert!(pending.is_some(), 5); assert!(*pending.borrow() == THIRD_PARTY, 6); - + ts::return_shared(safe); }; @@ -1007,9 +1006,9 @@ fun test_overwrite_pending_ownership_transfer() { { let mut safe = ts::take_shared(&scenario); safe::accept_ownership(&mut safe, scenario.ctx()); - + assert!(safe::get_owner(&safe) == THIRD_PARTY, 4); - + ts::return_shared(safe); }; @@ -1260,9 +1259,9 @@ fun test_old_owner_cannot_use_owner_functions_after_transfer() { scenario.next_tx(ADMIN); { let mut safe = ts::take_shared(&scenario); - + safe::pause_contract(&mut safe, scenario.ctx()); - + ts::return_shared(safe); }; diff --git a/tests/shared_structs_tests.move b/tests/shared_structs_tests.move index da65ddc..6edb403 100644 --- a/tests/shared_structs_tests.move +++ b/tests/shared_structs_tests.move @@ -8,17 +8,17 @@ const MAX_U64: u64 = 18446744073709551615; #[test] fun test_token_config_is_mint_burn() { let mut config = shared_structs::create_token_config( - true, - false, - 100, - 1000, + true, + false, + 100, + 1000, option::none(), false, ); - + shared_structs::set_token_config_is_mint_burn(&mut config, true); assert!(shared_structs::token_config_is_mint_burn(&config) == true, 0); - + shared_structs::set_token_config_is_mint_burn(&mut config, false); assert!(shared_structs::token_config_is_mint_burn(&config) == false, 1); } @@ -26,15 +26,15 @@ fun test_token_config_is_mint_burn() { #[test] fun test_set_batch_deposits_count() { let mut batch = shared_structs::create_batch(1, 1000); - + assert!(shared_structs::batch_deposits_count(&batch) == 0, 0); - + shared_structs::set_batch_deposits_count(&mut batch, 5); assert!(shared_structs::batch_deposits_count(&batch) == 5, 1); - + shared_structs::set_batch_deposits_count(&mut batch, 65535); assert!(shared_structs::batch_deposits_count(&batch) == 65535, 2); - + shared_structs::set_batch_deposits_count(&mut batch, 0); assert!(shared_structs::batch_deposits_count(&batch) == 0, 3); } @@ -42,20 +42,20 @@ fun test_set_batch_deposits_count() { #[test] fun test_subtract_from_token_config_total_balance() { let mut config = shared_structs::create_token_config( - true, - false, - 100, - 1000, + true, + false, + 100, + 1000, option::none(), false, ); - + shared_structs::add_to_token_config_total_balance(&mut config, 500); assert!(shared_structs::token_config_total_balance(&config) == 500, 0); - + shared_structs::subtract_from_token_config_total_balance(&mut config, 200); assert!(shared_structs::token_config_total_balance(&config) == 300, 1); - + shared_structs::subtract_from_token_config_total_balance(&mut config, 300); assert!(shared_structs::token_config_total_balance(&config) == 0, 2); } @@ -64,14 +64,14 @@ fun test_subtract_from_token_config_total_balance() { #[expected_failure(abort_code = shared_structs::EUnderflow)] fun test_subtract_from_token_config_total_balance_underflow() { let mut config = shared_structs::create_token_config( - true, - false, - 100, - 1000, + true, + false, + 100, + 1000, option::none(), false, ); - + shared_structs::subtract_from_token_config_total_balance(&mut config, 1); } @@ -79,35 +79,35 @@ fun test_subtract_from_token_config_total_balance_underflow() { #[expected_failure(abort_code = shared_structs::EUnderflow)] fun test_subtract_from_token_config_total_balance_insufficient_funds() { let mut config = shared_structs::create_token_config( - true, - false, - 100, - 1000, + true, + false, + 100, + 1000, option::none(), false, ); - + shared_structs::add_to_token_config_total_balance(&mut config, 100); - + shared_structs::subtract_from_token_config_total_balance(&mut config, 101); } #[test] fun test_add_to_token_config_total_balance() { let mut config = shared_structs::create_token_config( - true, - false, - 100, - 1000, + true, + false, + 100, + 1000, option::none(), false, ); - + assert!(shared_structs::token_config_total_balance(&config) == 0, 0); - + shared_structs::add_to_token_config_total_balance(&mut config, 250); assert!(shared_structs::token_config_total_balance(&config) == 250, 1); - + shared_structs::add_to_token_config_total_balance(&mut config, 750); assert!(shared_structs::token_config_total_balance(&config) == 1000, 2); } @@ -116,16 +116,16 @@ fun test_add_to_token_config_total_balance() { #[expected_failure(abort_code = shared_structs::EOverflow)] fun test_add_to_token_config_total_balance_overflow() { let mut config = shared_structs::create_token_config( - true, - false, - 100, - 1000, + true, + false, + 100, + 1000, option::none(), false, ); - + shared_structs::add_to_token_config_total_balance(&mut config, MAX_U64); - + shared_structs::add_to_token_config_total_balance(&mut config, 1); } @@ -133,35 +133,35 @@ fun test_add_to_token_config_total_balance_overflow() { #[expected_failure(abort_code = shared_structs::EOverflow)] fun test_add_to_token_config_total_balance_near_max_overflow() { let mut config = shared_structs::create_token_config( - true, - false, - 100, - 1000, + true, + false, + 100, + 1000, option::none(), false, ); - + shared_structs::add_to_token_config_total_balance(&mut config, MAX_U64 - 5); - + shared_structs::add_to_token_config_total_balance(&mut config, 10); } #[test] fun test_set_token_config_is_native() { let mut config = shared_structs::create_token_config( - true, - false, - 100, - 1000, + true, + false, + 100, + 1000, option::none(), false, ); - + assert!(shared_structs::token_config_is_native(&config) == false, 0); - + shared_structs::set_token_config_is_native(&mut config, true); assert!(shared_structs::token_config_is_native(&config) == true, 1); - + shared_structs::set_token_config_is_native(&mut config, false); assert!(shared_structs::token_config_is_native(&config) == false, 2); } @@ -170,14 +170,14 @@ fun test_set_token_config_is_native() { fun test_cross_transfer_status_statuses() { let statuses = vector[ shared_structs::deposit_status_executed(), - shared_structs::deposit_status_rejected() + shared_structs::deposit_status_rejected(), ]; - + let cross_transfer_status = shared_structs::create_cross_transfer_status( statuses, - 1234567890 + 1234567890, ); - + let retrieved_statuses = shared_structs::cross_transfer_status_statuses(&cross_transfer_status); assert!(retrieved_statuses.length() == 2, 0); } @@ -186,13 +186,15 @@ fun test_cross_transfer_status_statuses() { fun test_cross_transfer_status_created_timestamp_ms() { let statuses = vector[shared_structs::deposit_status_executed()]; let timestamp = 1234567890; - + let cross_transfer_status = shared_structs::create_cross_transfer_status( statuses, - timestamp + timestamp, + ); + + let retrieved_timestamp = shared_structs::cross_transfer_status_created_timestamp_ms( + &cross_transfer_status, ); - - let retrieved_timestamp = shared_structs::cross_transfer_status_created_timestamp_ms(&cross_transfer_status); assert!(retrieved_timestamp == timestamp, 0); } @@ -213,33 +215,33 @@ fun test_deposit_status_rejected() { #[test] fun test_update_batch_last_updated() { let mut batch = shared_structs::create_batch(1, 1000); - + assert!(shared_structs::batch_last_updated_timestamp_ms(&batch) == 1000, 0); - + let new_timestamp = 2000; shared_structs::update_batch_last_updated(&mut batch, new_timestamp); assert!(shared_structs::batch_last_updated_timestamp_ms(&batch) == new_timestamp, 1); - + let another_timestamp = 3000; shared_structs::update_batch_last_updated(&mut batch, another_timestamp); assert!(shared_structs::batch_last_updated_timestamp_ms(&batch) == another_timestamp, 2); - + assert!(shared_structs::batch_timestamp_ms(&batch) == 1000, 3); } #[test] fun test_combined_operations() { let mut config = shared_structs::create_token_config( - true, - false, - 100, - 1000, + true, + false, + 100, + 1000, option::none(), false, ); - + let mut batch = shared_structs::create_batch(42, 5000); - + shared_structs::set_token_config_is_native(&mut config, true); shared_structs::set_token_config_is_mint_burn(&mut config, true); shared_structs::add_to_token_config_total_balance(&mut config, 500); @@ -247,18 +249,18 @@ fun test_combined_operations() { assert!(shared_structs::token_config_is_native(&config) == true, 0); assert!(shared_structs::token_config_is_mint_burn(&config) == true, 2); assert!(shared_structs::token_config_total_balance(&config) == 500, 3); - + shared_structs::set_batch_deposits_count(&mut batch, 10); shared_structs::update_batch_last_updated(&mut batch, 6000); - + assert!(shared_structs::batch_deposits_count(&batch) == 10, 4); assert!(shared_structs::batch_last_updated_timestamp_ms(&batch) == 6000, 5); assert!(shared_structs::batch_nonce(&batch) == 42, 6); assert!(shared_structs::batch_timestamp_ms(&batch) == 5000, 7); - + shared_structs::subtract_from_token_config_total_balance(&mut config, 200); assert!(shared_structs::token_config_total_balance(&config) == 300, 8); - + shared_structs::add_to_token_config_total_balance(&mut config, 100); assert!(shared_structs::token_config_total_balance(&config) == 400, 9); } diff --git a/tests/upgrade_tests.move b/tests/upgrade_tests.move index 9b8cd32..1e4e967 100644 --- a/tests/upgrade_tests.move +++ b/tests/upgrade_tests.move @@ -3,9 +3,9 @@ module bridge_safe::upgrade_tests; use bridge_safe::bridge::{Self, Bridge}; use bridge_safe::bridge_roles::BridgeCap; +use bridge_safe::bridge_version_control; use bridge_safe::safe::{Self, BridgeSafe}; use bridge_safe::upgrade_manager; -use bridge_safe::bridge_version_control; use sui::test_scenario::{Self as ts, Scenario}; public struct TEST_COIN has drop {} @@ -59,79 +59,79 @@ fun setup(): Scenario { #[test] fun test_upgrade_workflow() { let mut scenario = setup(); - + scenario.next_tx(ADMIN); { let safe = ts::take_shared(&scenario); let bridge = ts::take_shared(&scenario); - + // Check initial versions let safe_versions = safe::compatible_versions(&safe); let bridge_versions = bridge::bridge_compatible_versions(&bridge); - + assert!(safe_versions.length() == 1, 0); assert!(bridge_versions.length() == 1, 1); assert!(safe_versions[0] == bridge_version_control::current_version(), 2); assert!(bridge_versions[0] == bridge_version_control::current_version(), 3); - + // Check that no migration is in progress assert!(!safe::is_migration_in_progress(&safe), 4); assert!(!bridge::is_bridge_migration_in_progress(&bridge), 5); assert!(!upgrade_manager::is_system_migration_in_progress(&safe, &bridge), 6); - + ts::return_shared(safe); ts::return_shared(bridge); }; - + ts::end(scenario); } #[test] fun test_system_upgrade_status() { let mut scenario = setup(); - + scenario.next_tx(ADMIN); { let safe = ts::take_shared(&scenario); let bridge = ts::take_shared(&scenario); - + // Test version functions let safe_active = safe::current_active_version(&safe); let bridge_active = bridge::bridge_current_active_version(&bridge); - + assert!(safe_active == bridge_version_control::current_version(), 4); assert!(bridge_active == bridge_version_control::current_version(), 5); - + // Test pending versions (should be none) let safe_pending = safe::pending_version(&safe); let bridge_pending = bridge::bridge_pending_version(&bridge); - + assert!(safe_pending.is_none(), 6); assert!(bridge_pending.is_none(), 7); - + ts::return_shared(safe); ts::return_shared(bridge); }; - + ts::end(scenario); } #[test] fun test_compatibility_assertions() { let mut scenario = setup(); - + scenario.next_tx(ADMIN); { let safe = ts::take_shared(&scenario); let bridge = ts::take_shared(&scenario); - + // These should not abort since objects are compatible safe::assert_is_compatible(&safe); bridge::assert_bridge_is_compatible(&bridge); - + ts::return_shared(safe); ts::return_shared(bridge); }; - + ts::end(scenario); } From 6a62e31926e983ac8b1c542a5ef672a9f13a4f56 Mon Sep 17 00:00:00 2001 From: georgedigkas Date: Fri, 20 Mar 2026 09:43:25 +0200 Subject: [PATCH 18/30] Audit fixes --- sources/bridge_module.move | 10 + sources/events.move | 24 ++ .../xmn_mint_cap_adapter.move | 9 +- sources/pausable.move | 4 +- sources/safe.move | 37 ++- sources/shared_structs.move | 17 +- tests/audit_fixes_tests.move | 277 ++++++++++++++++++ tests/safe_edge_case_tests.move | 22 +- 8 files changed, 385 insertions(+), 15 deletions(-) create mode 100644 tests/audit_fixes_tests.move diff --git a/sources/bridge_module.move b/sources/bridge_module.move index 74238fb..1120efe 100644 --- a/sources/bridge_module.move +++ b/sources/bridge_module.move @@ -101,6 +101,7 @@ public fun initialize( ctx: &mut TxContext, ) { assert!(initial_quorum >= MINIMUM_QUORUM, EQuorumTooLow); + assert!(initial_quorum <= vector::length(&public_keys), EQuorumExceedsRelayers); let mut relayers = vec_set::empty
(); let mut relayer_public_keys = table::new>(ctx); @@ -161,6 +162,7 @@ public fun set_quorum( new_quorum: u64, ctx: &mut TxContext, ) { + assert_bridge_is_compatible(bridge); safe::checkOwnerRole(safe, ctx); assert!(new_quorum >= MINIMUM_QUORUM, EQuorumTooLow); @@ -177,6 +179,7 @@ public fun set_batch_settle_timeout_ms( clock: &Clock, ctx: &mut TxContext, ) { + assert_bridge_is_compatible(bridge); safe::checkOwnerRole(safe, ctx); pausable::assert_paused(&bridge.pause); @@ -184,6 +187,7 @@ public fun set_batch_settle_timeout_ms( assert!(!safe::is_any_batch_in_progress(safe, clock), EPendingBatches); bridge.batch_settle_timeout_ms = new_timeout_ms; + events::emit_batch_settle_timeout_updated(new_timeout_ms); } // === Relayer Management === @@ -195,6 +199,7 @@ public fun add_relayer( public_key: vector, ctx: &mut TxContext, ) { + assert_bridge_is_compatible(bridge); safe::checkOwnerRole(safe, ctx); assert!(vector::length(&public_key) == ED25519_PUBLIC_KEY_LENGTH, EInvalidPublicKeyLength); @@ -212,6 +217,7 @@ public fun remove_relayer( relayer: address, ctx: &mut TxContext, ) { + assert_bridge_is_compatible(bridge); safe::checkOwnerRole(safe, ctx); let current_count = vec_set::length(&bridge.relayers); @@ -270,6 +276,8 @@ public fun execute_transfer( clock: &Clock, ctx: &mut TxContext, ) { + assert_bridge_is_compatible(bridge); + safe::assert_is_compatible(safe); pre_execute_transfer( bridge, batch_nonce_mvx, @@ -352,11 +360,13 @@ public fun get_relayer_count(bridge: &Bridge): u64 { } public fun pause_contract(bridge: &mut Bridge, safe: &BridgeSafe, ctx: &mut TxContext) { + assert_bridge_is_compatible(bridge); safe::checkOwnerRole(safe, ctx); pausable::pause(&mut bridge.pause); } public fun unpause_contract(bridge: &mut Bridge, safe: &BridgeSafe, ctx: &mut TxContext) { + assert_bridge_is_compatible(bridge); safe::checkOwnerRole(safe, ctx); pausable::unpause(&mut bridge.pause); } diff --git a/sources/events.move b/sources/events.move index 751546e..c05e26a 100644 --- a/sources/events.move +++ b/sources/events.move @@ -88,6 +88,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, @@ -202,3 +214,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 index 8317f3c..99aa326 100644 --- a/sources/mint_burn_adapters/xmn_mint_cap_adapter.move +++ b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move @@ -29,6 +29,7 @@ public fun deposit( deny_list: &DenyList, ctx: &mut TxContext, ) { + safe::assert_is_compatible(safe); assert!(has_cap(safe::uid(safe)), EMintBurnCapNotFound); let (key, amount, batch_nonce, dep_nonce) = safe::deposit_validate_and_record( @@ -66,6 +67,8 @@ public fun execute_transfer( clock: &Clock, ctx: &mut TxContext, ) { + bridge_module::assert_bridge_is_compatible(bridge); + safe::assert_is_compatible(safe); bridge_module::pre_execute_transfer( bridge, batch_nonce_mvx, @@ -106,6 +109,7 @@ public fun whitelist_token( treasury_id: ID, ctx: &TxContext, ) { + safe::assert_is_compatible(safe); assert!(!has_cap(safe::uid(safe)), EMintBurnCapAlreadyRegistered); safe::whitelist_token_internal( safe, @@ -122,9 +126,12 @@ public fun whitelist_token( /// 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); + safe::checkOwnerRole(safe, ctx); assert!(has_cap(safe::uid(safe)), EMintBurnCapNotFound); deregister(safe::uid_mut(safe), ctx.sender()); - safe::remove_token_from_whitelist(safe, ctx); + let key = utils::type_name_bytes(); + safe::unwhitelist_token(safe, key); } // === Internal helpers === diff --git a/sources/pausable.move b/sources/pausable.move index 81b0cfe..aa94b3f 100644 --- a/sources/pausable.move +++ b/sources/pausable.move @@ -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 55c3c36..4f89845 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -55,8 +55,11 @@ const EMigrationStarted: u64 = 16; const EMigrationNotStarted: u64 = 17; const ENotPendingVersion: u64 = 18; const ENotNativeToken: u64 = 19; +const EBatchTimeoutTooLow: u64 = 20; const EIncompatibleTokenFlags: u64 = 22; +const MINIMUM_BATCH_TIMEOUT_MS: u64 = 1000; + const MAX_U64: u64 = 18446744073709551615; const DEFAULT_BATCH_TIMEOUT_MS: u64 = 5 * 1000; // 5 seconds const DEFAULT_BATCH_SETTLE_TIMEOUT_MS: u64 = 10 * 1000; // 10 seconds @@ -126,6 +129,7 @@ public fun deposit( clock: &Clock, ctx: &mut TxContext, ) { + assert_is_compatible(safe); let (key, amount, batch_nonce, dep_nonce) = deposit_validate_and_record( safe, &coin_in, @@ -316,7 +320,7 @@ public fun get_pause(safe: &BridgeSafe): &Pause { &safe.pause } -public fun get_pause_mut(safe: &mut BridgeSafe): &mut Pause { +public(package) fun get_pause_mut(safe: &mut BridgeSafe): &mut Pause { &mut safe.pause } @@ -349,24 +353,29 @@ public fun get_coin_storage_balance(safe: &BridgeSafe): u64 { // === 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); pausable::pause(&mut safe.pause); } public fun unpause_contract(safe: &mut BridgeSafe, ctx: &mut TxContext) { + assert_is_compatible(safe); 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) { + 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(); @@ -390,6 +399,7 @@ public fun init_supply(safe: &mut BridgeSafe, coin_in: Coin, ctx: &mut TxC #[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(); @@ -434,6 +444,7 @@ public fun whitelist_token( maximum_amount: u64, ctx: &mut TxContext, ) { + assert_is_compatible(safe); whitelist_token_internal( safe, minimum_amount, @@ -445,16 +456,27 @@ public fun whitelist_token( ); } +/// 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 = table::borrow(&safe.token_cfg, key); + assert!(!shared_structs::token_config_is_mint_burn(cfg_ref), EIncompatibleTokenFlags); + unwhitelist_token(safe, key); +} + +/// 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); shared_structs::set_token_config_whitelisted(cfg, false); - events::emit_token_removed_from_whitelist(key); } 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; @@ -463,9 +485,12 @@ public fun set_bridge_addr(safe: &mut BridgeSafe, new_bridge_addr: address, ctx: } 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 >= MINIMUM_BATCH_TIMEOUT_MS, EBatchTimeoutTooLow); 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); } public fun set_batch_settle_timeout_ms( @@ -474,21 +499,26 @@ public fun set_batch_settle_timeout_ms( clock: &Clock, ctx: &mut TxContext, ) { + assert_is_compatible(safe); 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; + events::emit_batch_settle_timeout_updated(new_timeout_ms); } 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); } 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 key = utils::type_name_bytes(); @@ -504,6 +534,7 @@ public fun set_token_min_limit(safe: &mut BridgeSafe, amount: u64, ctx: &mut } 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); let key = utils::type_name_bytes(); @@ -517,6 +548,7 @@ public fun set_token_max_limit(safe: &mut BridgeSafe, amount: u64, ctx: &mut } 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); let key = utils::type_name_bytes(); @@ -535,6 +567,7 @@ public fun set_token_is_mint_burn( is_mint_burn: bool, ctx: &mut TxContext, ) { + assert_is_compatible(safe); safe.roles.owner_role().assert_sender_is_active_role(ctx); let key = utils::type_name_bytes(); diff --git a/sources/shared_structs.move b/sources/shared_structs.move index 2cd7f82..3fd548d 100644 --- a/sources/shared_structs.move +++ b/sources/shared_structs.move @@ -95,11 +95,11 @@ public fun deposit_status_rejected(): DepositStatus { DepositStatus::Rejected } -public fun update_batch_last_updated(batch: &mut Batch, timestamp_ms: u64) { +public(package) fun update_batch_last_updated(batch: &mut Batch, timestamp_ms: u64) { batch.last_updated_timestamp_ms = timestamp_ms; } -public fun increment_batch_deposits(batch: &mut Batch) { +public(package) fun increment_batch_deposits(batch: &mut Batch) { batch.deposits_count = batch.deposits_count + 1; } @@ -148,12 +148,15 @@ const EOverflow: u64 = 1; const MAX_U64: u64 = 18446744073709551615; -public fun add_to_token_config_total_balance(config: &mut TokenConfig, amount: u64) { +public(package) fun add_to_token_config_total_balance(config: &mut TokenConfig, amount: u64) { assert!(config.total_balance <= MAX_U64 - amount, EOverflow); config.total_balance = config.total_balance + amount; } -public fun subtract_from_token_config_total_balance(config: &mut TokenConfig, amount: u64) { +public(package) fun subtract_from_token_config_total_balance( + config: &mut TokenConfig, + amount: u64, +) { assert!(config.total_balance >= amount, EUnderflow); config.total_balance = config.total_balance - amount; } @@ -182,7 +185,7 @@ public fun batch_timestamp_ms(batch: &Batch): u64 { batch.timestamp_ms } -public fun set_batch_deposits_count(batch: &mut Batch, count: u16) { +public(package) fun set_batch_deposits_count(batch: &mut Batch, count: u16) { batch.deposits_count = count; } @@ -190,7 +193,7 @@ public(package) fun set_batch_last_updated_timestamp_ms(batch: &mut Batch, times batch.last_updated_timestamp_ms = timestamp_ms; } -public fun upsert_token_config( +public(package) fun upsert_token_config( config: &mut Table, TokenConfig>, key: vector, whitelisted: bool, @@ -226,7 +229,7 @@ public fun upsert_token_config( table::add(config, key, cfg); } -public fun set_token_config( +public(package) fun set_token_config( config: &mut TokenConfig, whitelisted: bool, is_native: bool, diff --git a/tests/audit_fixes_tests.move b/tests/audit_fixes_tests.move new file mode 100644 index 0000000..b02423a --- /dev/null +++ b/tests/audit_fixes_tests.move @@ -0,0 +1,277 @@ +#[test_only] +module bridge_safe::audit_fixes_tests; + +use bridge_safe::bridge::{Self, Bridge}; +use bridge_safe::bridge_roles::BridgeCap; +use bridge_safe::safe::{Self, BridgeSafe}; +use sui::test_scenario::{Self as ts, Scenario}; + +public struct TEST_COIN has drop {} + +const ADMIN: address = @0xa11ce; + +const INITIAL_QUORUM: u64 = 3; +const MIN_AMOUNT: u64 = 100; +const MAX_AMOUNT: u64 = 1_000_000; + +const PK1: vector = b"12345678901234567890123456789012"; +const PK2: vector = b"abcdefghijklmnopqrstuvwxyz123456"; +const PK3: vector = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456"; + +fun setup(): Scenario { + let mut s = ts::begin(ADMIN); + + s.next_tx(ADMIN); + { + safe::init_for_testing(s.ctx()); + }; + + s +} + +fun setup_with_bridge(): Scenario { + let mut s = setup(); + + s.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&s); + let bridge_cap = ts::take_from_sender(&s); + + safe::whitelist_token( + &mut safe, + MIN_AMOUNT, + MAX_AMOUNT, + ts::ctx(&mut s), + ); + + let safe_addr = object::id_address(&safe); + let public_keys = vector[PK1, PK2, PK3]; + + bridge::initialize( + public_keys, + INITIAL_QUORUM, + safe_addr, + bridge_cap, + ts::ctx(&mut s), + ); + + ts::return_shared(safe); + }; + + s +} + +// === ISSUE-4: Quorum > relayer count on initialization === + +#[test] +#[expected_failure(abort_code = bridge::EQuorumExceedsRelayers)] +fun test_initialize_quorum_exceeds_relayers() { + let mut scenario = setup(); + + scenario.next_tx(ADMIN); + { + let safe = ts::take_shared(&scenario); + let bridge_cap = ts::take_from_sender(&scenario); + + let public_keys = vector[PK1, PK2, PK3]; + + // Quorum of 4 but only 3 relayers - should fail + bridge::initialize( + public_keys, + 4, + object::id_address(&safe), + bridge_cap, + ts::ctx(&mut scenario), + ); + + ts::return_shared(safe); + }; + ts::end(scenario); +} + +#[test] +fun test_initialize_quorum_equals_relayers() { + let mut scenario = setup(); + + scenario.next_tx(ADMIN); + { + let safe = ts::take_shared(&scenario); + let bridge_cap = ts::take_from_sender(&scenario); + + let public_keys = vector[PK1, PK2, PK3]; + + // Quorum of 3 with 3 relayers - should succeed + bridge::initialize( + public_keys, + 3, + object::id_address(&safe), + bridge_cap, + ts::ctx(&mut scenario), + ); + + ts::return_shared(safe); + }; + + scenario.next_tx(ADMIN); + { + let bridge = ts::take_shared(&scenario); + assert!(bridge::get_quorum(&bridge) == 3, 0); + assert!(bridge::get_relayer_count(&bridge) == 3, 1); + ts::return_shared(bridge); + }; + + ts::end(scenario); +} + +// === ISSUE-5: Direct de-whitelisting of mint-burn token === + +#[test] +#[expected_failure(abort_code = safe::EIncompatibleTokenFlags)] +fun test_remove_mint_burn_token_via_safe_fails() { + let mut scenario = setup(); + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + + // Whitelist as native first + safe::whitelist_token( + &mut safe, + MIN_AMOUNT, + MAX_AMOUNT, + ts::ctx(&mut scenario), + ); + + // Change to non-native, then set mint-burn + safe::set_token_is_native(&mut safe, false, ts::ctx(&mut scenario)); + safe::set_token_is_mint_burn(&mut safe, true, ts::ctx(&mut scenario)); + + // Try to remove via safe directly - should fail because it's mint-burn + safe::remove_token_from_whitelist(&mut safe, ts::ctx(&mut scenario)); + + ts::return_shared(safe); + }; + ts::end(scenario); +} + +#[test] +fun test_remove_native_token_via_safe_succeeds() { + let mut scenario = setup(); + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + + // Whitelist as native + safe::whitelist_token( + &mut safe, + MIN_AMOUNT, + MAX_AMOUNT, + ts::ctx(&mut scenario), + ); + + assert!(safe::is_token_whitelisted(&safe), 0); + + // Remove via safe directly - should succeed because it's native + safe::remove_token_from_whitelist(&mut safe, ts::ctx(&mut scenario)); + + assert!(!safe::is_token_whitelisted(&safe), 1); + + ts::return_shared(safe); + }; + ts::end(scenario); +} + +// === ISSUE-7: Batch timeout minimum === + +#[test] +#[expected_failure(abort_code = safe::EBatchTimeoutTooLow)] +fun test_batch_timeout_zero_rejected() { + let mut scenario = setup(); + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + + safe::set_batch_timeout_ms(&mut safe, 0, ts::ctx(&mut scenario)); + + ts::return_shared(safe); + }; + ts::end(scenario); +} + +#[test] +#[expected_failure(abort_code = safe::EBatchTimeoutTooLow)] +fun test_batch_timeout_below_minimum_rejected() { + let mut scenario = setup(); + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + + // 999ms is below the 1000ms minimum + safe::set_batch_timeout_ms(&mut safe, 999, ts::ctx(&mut scenario)); + + ts::return_shared(safe); + }; + ts::end(scenario); +} + +#[test] +fun test_batch_timeout_at_minimum_accepted() { + let mut scenario = setup(); + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + + // Exactly 1000ms should work + safe::set_batch_timeout_ms(&mut safe, 1000, ts::ctx(&mut scenario)); + assert!(safe::get_batch_timeout_ms(&safe) == 1000, 0); + + ts::return_shared(safe); + }; + ts::end(scenario); +} + +// === ISSUE-8: Events for config updates (verify no aborts) === + +#[test] +fun test_config_update_events_emitted() { + let mut scenario = setup_with_bridge(); + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + let mut bridge = ts::take_shared(&scenario); + let clock = sui::clock::create_for_testing(ts::ctx(&mut scenario)); + + // All of these should succeed and emit events + safe::set_batch_timeout_ms(&mut safe, 3000, ts::ctx(&mut scenario)); + assert!(safe::get_batch_timeout_ms(&safe) == 3000, 0); + + safe::set_batch_size(&mut safe, 20, ts::ctx(&mut scenario)); + assert!(safe::get_batch_size(&safe) == 20, 1); + + // Pause to update settle timeouts + safe::pause_contract(&mut safe, ts::ctx(&mut scenario)); + bridge::pause_contract(&mut bridge, &safe, ts::ctx(&mut scenario)); + + safe::set_batch_settle_timeout_ms(&mut safe, 20000, &clock, ts::ctx(&mut scenario)); + assert!(safe::get_batch_settle_timeout_ms(&safe) == 20000, 2); + + bridge::set_batch_settle_timeout_ms( + &mut bridge, + &safe, + 20000, + &clock, + ts::ctx(&mut scenario), + ); + assert!(bridge::get_batch_settle_timeout_ms(&bridge) == 20000, 3); + + sui::clock::destroy_for_testing(clock); + ts::return_shared(bridge); + ts::return_shared(safe); + }; + ts::end(scenario); +} diff --git a/tests/safe_edge_case_tests.move b/tests/safe_edge_case_tests.move index 0fb6e7a..9233cd8 100644 --- a/tests/safe_edge_case_tests.move +++ b/tests/safe_edge_case_tests.move @@ -56,9 +56,9 @@ fun test_batch_timeout_edge_cases() { { let mut safe = ts::take_shared(&scenario); - // Test setting batch timeout to 0 (should be allowed) - safe::set_batch_timeout_ms(&mut safe, 0, ts::ctx(&mut scenario)); - assert!(safe::get_batch_timeout_ms(&safe) == 0, 0); + // Test setting batch timeout to minimum allowed value (1000ms) + safe::set_batch_timeout_ms(&mut safe, 1000, ts::ctx(&mut scenario)); + assert!(safe::get_batch_timeout_ms(&safe) == 1000, 0); // Test setting batch timeout equal to settle timeout (should be allowed) let settle_timeout = safe::get_batch_settle_timeout_ms(&safe); @@ -70,6 +70,22 @@ fun test_batch_timeout_edge_cases() { ts::end(scenario); } +#[test] +#[expected_failure(abort_code = safe::EBatchTimeoutTooLow)] +fun test_batch_timeout_below_minimum() { + let mut scenario = setup(); + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + + // Setting batch timeout to 0 should fail (minimum is 1000ms) + safe::set_batch_timeout_ms(&mut safe, 0, ts::ctx(&mut scenario)); + + ts::return_shared(safe); + }; + ts::end(scenario); +} + #[test] fun test_multiple_token_whitelist() { let mut scenario = setup(); From f3368fc12775a2b0b5b3ee4476d6340aa2a396fe Mon Sep 17 00:00:00 2001 From: georgedigkas Date: Fri, 20 Mar 2026 09:59:15 +0200 Subject: [PATCH 19/30] improve comments --- tests/audit_fixes_tests.move | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/audit_fixes_tests.move b/tests/audit_fixes_tests.move index b02423a..f607cd0 100644 --- a/tests/audit_fixes_tests.move +++ b/tests/audit_fixes_tests.move @@ -61,7 +61,7 @@ fun setup_with_bridge(): Scenario { s } -// === ISSUE-4: Quorum > relayer count on initialization === +// === Quorum > relayer count on initialization === #[test] #[expected_failure(abort_code = bridge::EQuorumExceedsRelayers)] @@ -123,7 +123,7 @@ fun test_initialize_quorum_equals_relayers() { ts::end(scenario); } -// === ISSUE-5: Direct de-whitelisting of mint-burn token === +// === Direct de-whitelisting of mint-burn token === #[test] #[expected_failure(abort_code = safe::EIncompatibleTokenFlags)] @@ -182,7 +182,7 @@ fun test_remove_native_token_via_safe_succeeds() { ts::end(scenario); } -// === ISSUE-7: Batch timeout minimum === +// === Batch timeout minimum === #[test] #[expected_failure(abort_code = safe::EBatchTimeoutTooLow)] @@ -234,7 +234,7 @@ fun test_batch_timeout_at_minimum_accepted() { ts::end(scenario); } -// === ISSUE-8: Events for config updates (verify no aborts) === +// === Events for config updates (verify no aborts) === #[test] fun test_config_update_events_emitted() { From d0c53d9f5f8ae1802247b5274568dff4612ea4c6 Mon Sep 17 00:00:00 2001 From: georgedigkas Date: Fri, 20 Mar 2026 10:03:47 +0200 Subject: [PATCH 20/30] remove minimum batch timeout checks and update related tests --- sources/safe.move | 4 --- tests/audit_fixes_tests.move | 52 --------------------------------- tests/safe_edge_case_tests.move | 22 ++------------ 3 files changed, 3 insertions(+), 75 deletions(-) diff --git a/sources/safe.move b/sources/safe.move index 4f89845..e22bc66 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -55,11 +55,8 @@ const EMigrationStarted: u64 = 16; const EMigrationNotStarted: u64 = 17; const ENotPendingVersion: u64 = 18; const ENotNativeToken: u64 = 19; -const EBatchTimeoutTooLow: u64 = 20; const EIncompatibleTokenFlags: u64 = 22; -const MINIMUM_BATCH_TIMEOUT_MS: u64 = 1000; - const MAX_U64: u64 = 18446744073709551615; const DEFAULT_BATCH_TIMEOUT_MS: u64 = 5 * 1000; // 5 seconds const DEFAULT_BATCH_SETTLE_TIMEOUT_MS: u64 = 10 * 1000; // 10 seconds @@ -487,7 +484,6 @@ public fun set_bridge_addr(safe: &mut BridgeSafe, new_bridge_addr: address, ctx: 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 >= MINIMUM_BATCH_TIMEOUT_MS, EBatchTimeoutTooLow); 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); diff --git a/tests/audit_fixes_tests.move b/tests/audit_fixes_tests.move index f607cd0..174ff00 100644 --- a/tests/audit_fixes_tests.move +++ b/tests/audit_fixes_tests.move @@ -182,58 +182,6 @@ fun test_remove_native_token_via_safe_succeeds() { ts::end(scenario); } -// === Batch timeout minimum === - -#[test] -#[expected_failure(abort_code = safe::EBatchTimeoutTooLow)] -fun test_batch_timeout_zero_rejected() { - let mut scenario = setup(); - - scenario.next_tx(ADMIN); - { - let mut safe = ts::take_shared(&scenario); - - safe::set_batch_timeout_ms(&mut safe, 0, ts::ctx(&mut scenario)); - - ts::return_shared(safe); - }; - ts::end(scenario); -} - -#[test] -#[expected_failure(abort_code = safe::EBatchTimeoutTooLow)] -fun test_batch_timeout_below_minimum_rejected() { - let mut scenario = setup(); - - scenario.next_tx(ADMIN); - { - let mut safe = ts::take_shared(&scenario); - - // 999ms is below the 1000ms minimum - safe::set_batch_timeout_ms(&mut safe, 999, ts::ctx(&mut scenario)); - - ts::return_shared(safe); - }; - ts::end(scenario); -} - -#[test] -fun test_batch_timeout_at_minimum_accepted() { - let mut scenario = setup(); - - scenario.next_tx(ADMIN); - { - let mut safe = ts::take_shared(&scenario); - - // Exactly 1000ms should work - safe::set_batch_timeout_ms(&mut safe, 1000, ts::ctx(&mut scenario)); - assert!(safe::get_batch_timeout_ms(&safe) == 1000, 0); - - ts::return_shared(safe); - }; - ts::end(scenario); -} - // === Events for config updates (verify no aborts) === #[test] diff --git a/tests/safe_edge_case_tests.move b/tests/safe_edge_case_tests.move index 9233cd8..0fb6e7a 100644 --- a/tests/safe_edge_case_tests.move +++ b/tests/safe_edge_case_tests.move @@ -56,9 +56,9 @@ fun test_batch_timeout_edge_cases() { { let mut safe = ts::take_shared(&scenario); - // Test setting batch timeout to minimum allowed value (1000ms) - safe::set_batch_timeout_ms(&mut safe, 1000, ts::ctx(&mut scenario)); - assert!(safe::get_batch_timeout_ms(&safe) == 1000, 0); + // Test setting batch timeout to 0 (should be allowed) + safe::set_batch_timeout_ms(&mut safe, 0, ts::ctx(&mut scenario)); + assert!(safe::get_batch_timeout_ms(&safe) == 0, 0); // Test setting batch timeout equal to settle timeout (should be allowed) let settle_timeout = safe::get_batch_settle_timeout_ms(&safe); @@ -70,22 +70,6 @@ fun test_batch_timeout_edge_cases() { ts::end(scenario); } -#[test] -#[expected_failure(abort_code = safe::EBatchTimeoutTooLow)] -fun test_batch_timeout_below_minimum() { - let mut scenario = setup(); - scenario.next_tx(ADMIN); - { - let mut safe = ts::take_shared(&scenario); - - // Setting batch timeout to 0 should fail (minimum is 1000ms) - safe::set_batch_timeout_ms(&mut safe, 0, ts::ctx(&mut scenario)); - - ts::return_shared(safe); - }; - ts::end(scenario); -} - #[test] fun test_multiple_token_whitelist() { let mut scenario = setup(); From f5266d99fdd62dfe1621f1e399342f2344ddd3a6 Mon Sep 17 00:00:00 2001 From: georgedigkas Date: Fri, 20 Mar 2026 10:11:32 +0200 Subject: [PATCH 21/30] move 2024 --- sources/bridge_module.move | 62 +++++++++---------- .../xmn_mint_cap_adapter.move | 8 +-- sources/safe.move | 2 +- sources/shared_structs.move | 2 +- 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/sources/bridge_module.move b/sources/bridge_module.move index 1120efe..0175451 100644 --- a/sources/bridge_module.move +++ b/sources/bridge_module.move @@ -101,14 +101,14 @@ public fun initialize( ctx: &mut TxContext, ) { assert!(initial_quorum >= MINIMUM_QUORUM, EQuorumTooLow); - assert!(initial_quorum <= vector::length(&public_keys), EQuorumExceedsRelayers); + 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)) { + while (i < public_keys.length()) { let pk = *vector::borrow(&public_keys, i); - assert!(vector::length(&pk) == ED25519_PUBLIC_KEY_LENGTH, EInvalidPublicKeyLength); + assert!(pk.length() == ED25519_PUBLIC_KEY_LENGTH, EInvalidPublicKeyLength); let relayer_address = getAddressFromPublicKey(&pk); vec_set::insert(&mut relayers, relayer_address); @@ -166,7 +166,7 @@ public fun set_quorum( safe::checkOwnerRole(safe, 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 }); @@ -202,7 +202,7 @@ public fun add_relayer( assert_bridge_is_compatible(bridge); safe::checkOwnerRole(safe, 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); @@ -220,7 +220,7 @@ public fun remove_relayer( assert_bridge_is_compatible(bridge); safe::checkOwnerRole(safe, 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); @@ -289,14 +289,14 @@ public fun execute_transfer( ctx, ); - let len = vector::length(&recipients); + let len = recipients.length(); let mut i = 0; while (i < len) { let success = safe::transfer( safe, &bridge.bridge_cap, - *vector::borrow(&recipients, i), - *vector::borrow(&amounts, i), + *recipients.borrow(i), + *amounts.borrow(i), ctx, ); record_transfer_result(bridge, batch_nonce_mvx, success); @@ -356,7 +356,7 @@ public fun get_relayers(bridge: &Bridge): &vector
{ } 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) { @@ -380,7 +380,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); @@ -389,9 +389,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); @@ -409,7 +409,7 @@ fun validate_quorum( i = i + 1; }; - assert!(vec_set::length(&verified_relayers) >= bridge.quorum, EQuorumNotReached); + assert!(verified_relayers.length() >= bridge.quorum, EQuorumNotReached); } public fun compute_message( @@ -436,10 +436,10 @@ 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); + while (i < recipients.length()) { + let recipient = recipients.borrow(i); + let amount = amounts.borrow(i); + let deposit_nonce = deposit_nonces.borrow(i); vector::append(&mut message, bcs::to_bytes(token)); vector::append(&mut message, bcs::to_bytes(recipient)); @@ -453,9 +453,9 @@ 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 i = signature.length() - ED25519_PUBLIC_KEY_LENGTH; + while (i < signature.length()) { + vector::push_back(&mut public_key, *signature.borrow(i)); i = i + 1; }; public_key @@ -464,8 +464,8 @@ fun extract_public_key(signature: &vector): vector { fun extract_signature(signature: &vector): vector { let mut sig_bytes = vector::empty(); 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) { + vector::push_back(&mut sig_bytes, *signature.borrow(i)); i = i + 1; }; sig_bytes @@ -475,8 +475,8 @@ fun find_relayer_by_public_key(bridge: &Bridge, public_key: &vector): Option let relayers = vec_set::keys(&bridge.relayers); let mut i = 0; - while (i < vector::length(relayers)) { - let relayer = *vector::borrow(relayers, i); + while (i < relayers.length()) { + let relayer = *relayers.borrow(i); if (table::contains(&bridge.relayer_public_keys, relayer)) { let stored_pk = table::borrow(&bridge.relayer_public_keys, relayer); if (stored_pk == public_key) { @@ -502,14 +502,14 @@ public fun execute_transfer_for_testing( ) { pre_execute_transfer_for_testing(bridge, batch_nonce_mvx, clock); - let len = vector::length(&recipients); + let len = recipients.length(); let mut i = 0; while (i < len) { let success = safe::transfer( safe, &bridge.bridge_cap, - *vector::borrow(&recipients, i), - *vector::borrow(&amounts, i), + *recipients.borrow(i), + *amounts.borrow(i), ctx, ); record_transfer_result(bridge, batch_nonce_mvx, success); @@ -537,9 +537,9 @@ public(package) fun pre_execute_transfer( 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); + 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); diff --git a/sources/mint_burn_adapters/xmn_mint_cap_adapter.move b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move index 99aa326..e19a586 100644 --- a/sources/mint_burn_adapters/xmn_mint_cap_adapter.move +++ b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move @@ -80,7 +80,7 @@ public fun execute_transfer( ctx, ); - let len = vector::length(&recipients); + let len = recipients.length(); let mut i = 0; while (i < len) { let success = transfer( @@ -212,14 +212,14 @@ public fun execute_transfer_for_testing( ) { bridge_module::pre_execute_transfer_for_testing(bridge, batch_nonce_mvx, clock); - let len = vector::length(&recipients); + let len = recipients.length(); let mut i = 0; while (i < len) { let success = transfer( safe, bridge_module::bridge_cap(bridge), - *vector::borrow(&recipients, i), - *vector::borrow(&amounts, i), + *recipients.borrow(i), + *amounts.borrow(i), xmn_treasury, deny_list, ctx, diff --git a/sources/safe.move b/sources/safe.move index e22bc66..2ee7ed8 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -744,7 +744,7 @@ public(package) fun deposit_validate_and_record( ctx: &mut TxContext, ): (vector, u64, u64, u64) { pausable::assert_not_paused(&safe.pause); - assert!(vector::length(&recipient) == 32, EInvalidRecipient); + assert!(recipient.length() == 32, EInvalidRecipient); let key = utils::type_name_bytes(); let cfg_ref = table::borrow(&safe.token_cfg, key); diff --git a/sources/shared_structs.move b/sources/shared_structs.move index 3fd548d..188749f 100644 --- a/sources/shared_structs.move +++ b/sources/shared_structs.move @@ -215,7 +215,7 @@ public(package) fun upsert_token_config( is_mint_burn, ); - return; + return }; let cfg = create_token_config( From 469d00e56c6b6d82880cb192a7549cfcdd0b07f3 Mon Sep 17 00:00:00 2001 From: georgedigkas Date: Fri, 20 Mar 2026 10:30:20 +0200 Subject: [PATCH 22/30] Move 2024 --- sources/bridge_module.move | 70 +++++------- .../xmn_mint_cap_adapter.move | 4 +- sources/safe.move | 106 +++++++++--------- sources/shared_structs.move | 8 +- sources/utils.move | 3 +- 5 files changed, 91 insertions(+), 100 deletions(-) diff --git a/sources/bridge_module.move b/sources/bridge_module.move index 0175451..0bed80a 100644 --- a/sources/bridge_module.move +++ b/sources/bridge_module.move @@ -107,12 +107,12 @@ public fun initialize( let mut relayer_public_keys = table::new>(ctx); let mut i = 0; while (i < public_keys.length()) { - let pk = *vector::borrow(&public_keys, i); + 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); + relayer_public_keys.add(relayer_address, pk); i = i + 1; }; @@ -150,7 +150,7 @@ fun getAddressFromPublicKey(public_key: &vector): address { /// 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 === @@ -204,10 +204,10 @@ public fun add_relayer( 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); + bridge.relayers.insert(relayer_address); + bridge.relayer_public_keys.add(relayer_address, public_key); events::emit_relayer_added(relayer_address, tx_context::sender(ctx)); } @@ -223,9 +223,9 @@ public fun remove_relayer( 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)); } @@ -243,7 +243,7 @@ public fun get_batch_deposits( } 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( @@ -251,8 +251,8 @@ 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); + if (bridge.cross_transfer_statuses.contains(batch_nonce_mvx)) { + let cross_status = bridge.cross_transfer_statuses.borrow(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, @@ -308,10 +308,7 @@ public fun execute_transfer( 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, - ); + assert!(!bridge.executed_transfer_by_batch_type_arg.contains(&key), EDepositAlreadyExecuted); vec_set::insert(&mut bridge.executed_transfer_by_batch_type_arg, key); } @@ -340,7 +337,7 @@ 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 { @@ -352,7 +349,7 @@ 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 { @@ -401,11 +398,11 @@ 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; }; @@ -477,8 +474,8 @@ fun find_relayer_by_public_key(bridge: &Bridge, public_key: &vector): Option while (i < relayers.length()) { let relayer = *relayers.borrow(i); - if (table::contains(&bridge.relayer_public_keys, relayer)) { - let stored_pk = table::borrow(&bridge.relayer_public_keys, relayer); + 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) }; @@ -545,10 +542,10 @@ public(package) fun pre_execute_transfer( 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)) { - *table::borrow_mut(&mut bridge.execution_timestamps, batch_nonce_mvx) = now; + 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); }; } @@ -560,14 +557,11 @@ public(package) fun record_transfer_result( ) { if (success) { vector::push_back(&mut bridge.transfer_statuses, shared_structs::deposit_status_executed()); - if (table::contains(&bridge.successful_transfers_by_batch, batch_nonce_mvx)) { - let count = table::borrow_mut( - &mut bridge.successful_transfers_by_batch, - batch_nonce_mvx, - ); + 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 { - table::add(&mut bridge.successful_transfers_by_batch, batch_nonce_mvx, 1); + bridge.successful_transfers_by_batch.add(batch_nonce_mvx, 1); }; } else { vector::push_back(&mut bridge.transfer_statuses, shared_structs::deposit_status_rejected()); @@ -591,11 +585,9 @@ public(package) fun finalize_batch( table::add(&mut bridge.cross_transfer_statuses, batch_nonce_mvx, cross_status); bridge.transfer_statuses = vector::empty(); - let successful_count = if ( - table::contains(&bridge.successful_transfers_by_batch, batch_nonce_mvx) - ) { - let count = *table::borrow(&bridge.successful_transfers_by_batch, batch_nonce_mvx); - table::remove(&mut 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 @@ -622,10 +614,10 @@ public(package) fun pre_execute_transfer_for_testing( ) { 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)) { - *table::borrow_mut(&mut bridge.execution_timestamps, batch_nonce_mvx) = now; + 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); }; } diff --git a/sources/mint_burn_adapters/xmn_mint_cap_adapter.move b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move index e19a586..5eebd86 100644 --- a/sources/mint_burn_adapters/xmn_mint_cap_adapter.move +++ b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move @@ -86,8 +86,8 @@ public fun execute_transfer( let success = transfer( safe, bridge_module::bridge_cap(bridge), - *vector::borrow(&recipients, i), - *vector::borrow(&amounts, i), + *recipients.borrow(i), + *amounts.borrow(i), xmn_treasury, deny_list, ctx, diff --git a/sources/safe.move b/sources/safe.move index 2ee7ed8..fc9ab66 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -136,10 +136,10 @@ public fun deposit( ctx, ); - if (bag::contains(&safe.coin_storage, key)) { - coin::join(bag::borrow_mut, Coin>(&mut safe.coin_storage, key), coin_in); + if (safe.coin_storage.contains(key)) { + coin::join(safe.coin_storage.borrow_mut, Coin>(key), coin_in); } else { - bag::add(&mut safe.coin_storage, key, coin_in); + safe.coin_storage.add(key, coin_in); }; events::emit_deposit_v1( @@ -163,12 +163,12 @@ public(package) fun transfer( ): bool { let key = utils::type_name_bytes(); - if (!table::contains(&safe.token_cfg, key)) { + if (!safe.token_cfg.contains(key)) { return false }; let (is_mint_burn, current_balance) = { - let cfg_ref = table::borrow(&safe.token_cfg, key); + let cfg_ref = safe.token_cfg.borrow(key); ( shared_structs::token_config_is_mint_burn(cfg_ref), shared_structs::token_config_total_balance(cfg_ref), @@ -183,11 +183,11 @@ public(package) fun transfer( return false }; - if (!bag::contains(&safe.coin_storage, key)) { + if (!safe.coin_storage.contains(key)) { return false }; - let stored_coin = bag::borrow_mut, Coin>(&mut safe.coin_storage, key); + let stored_coin = safe.coin_storage.borrow_mut, Coin>(key); let coin_value = coin::value(stored_coin); if (coin_value < amount) { return false @@ -196,7 +196,7 @@ public(package) fun transfer( let coin_to_transfer = coin::split(stored_coin, amount, ctx); if (coin::value(stored_coin) == 0) { - let empty_coin = bag::remove, Coin>(&mut safe.coin_storage, key); + let empty_coin = safe.coin_storage.remove, Coin>(key); coin::destroy_zero(empty_coin); }; transfer::public_transfer(coin_to_transfer, receiver); @@ -209,34 +209,34 @@ public(package) fun transfer( 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); + let cfg = safe.token_cfg.borrow(key); shared_structs::token_config_whitelisted(cfg) } public fun get_token_min_limit(safe: &BridgeSafe): u64 { let key = utils::type_name_bytes(); - let cfg = table::borrow(&safe.token_cfg, key); + let cfg = safe.token_cfg.borrow(key); shared_structs::token_config_min_limit(cfg) } public fun get_token_max_limit(safe: &BridgeSafe): u64 { let key = utils::type_name_bytes(); - let cfg = table::borrow(&safe.token_cfg, key); + let cfg = safe.token_cfg.borrow(key); shared_structs::token_config_max_limit(cfg) } public fun get_token_is_mint_burn(safe: &BridgeSafe): bool { let key = utils::type_name_bytes(); - let cfg = table::borrow(&safe.token_cfg, key); + let cfg = safe.token_cfg.borrow(key); shared_structs::token_config_is_mint_burn(cfg) } public fun get_token_is_native(safe: &BridgeSafe): bool { let key = utils::type_name_bytes(); - let cfg = table::borrow(&safe.token_cfg, key); + let cfg = safe.token_cfg.borrow(key); shared_structs::token_config_is_native(cfg) } @@ -244,12 +244,12 @@ public fun get_batch(safe: &BridgeSafe, batch_nonce: u64, clock: &Clock): (Batch assert!(batch_nonce > 0, EBatchNotFound); let batch_index = batch_nonce - 1; - if (!table::contains(&safe.batches, batch_index)) { + if (!safe.batches.contains(batch_index)) { let empty_batch = shared_structs::create_batch(0, 0); return (empty_batch, false) }; - let batch = *table::borrow(&safe.batches, batch_index); + let batch = *safe.batches.borrow(batch_index); let is_final = is_batch_final_internal(safe, &batch, clock); (batch, is_final) } @@ -261,16 +261,16 @@ public fun get_deposits( ): (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) + let deposits = if (safe.batch_deposits.contains(batch_index)) { + *safe.batch_deposits.borrow(batch_index) } else { vector::empty() }; - if (!table::contains(&safe.batches, batch_index)) { + if (!safe.batches.contains(batch_index)) { return (deposits, false) }; - let batch = table::borrow(&safe.batches, batch_index); + let batch = safe.batches.borrow(batch_index); let is_final = is_batch_final_internal(safe, batch, clock); (deposits, is_final) } @@ -331,19 +331,19 @@ public fun get_batch_deposits_count(batch: &Batch): u16 { public fun get_stored_coin_balance(safe: &mut BridgeSafe): u64 { let key = utils::type_name_bytes(); - if (!table::contains(&safe.token_cfg, key)) { + if (!safe.token_cfg.contains(key)) { return 0 }; - let cfg_ref = table::borrow(&safe.token_cfg, key); + let cfg_ref = safe.token_cfg.borrow(key); shared_structs::token_config_total_balance(cfg_ref) } public fun get_coin_storage_balance(safe: &BridgeSafe): u64 { let key = utils::type_name_bytes(); - if (!bag::contains(&safe.coin_storage, key)) { + if (!safe.coin_storage.contains(key)) { return 0 }; - let stored_coin = bag::borrow, Coin>(&safe.coin_storage, key); + let stored_coin = safe.coin_storage.borrow, Coin>(key); coin::value(stored_coin) } @@ -378,7 +378,7 @@ public fun init_supply(safe: &mut BridgeSafe, coin_in: Coin, ctx: &mut TxC let key = utils::type_name_bytes(); assert_token_is_whitelisted(safe, key); - let cfg_ref = table::borrow(&safe.token_cfg, key); + let cfg_ref = safe.token_cfg.borrow(key); assert!(shared_structs::token_config_is_native(cfg_ref), ENotNativeToken); let amount = coin::value(&coin_in); @@ -386,11 +386,11 @@ public fun init_supply(safe: &mut BridgeSafe, coin_in: Coin, ctx: &mut TxC let cfg_mut = borrow_token_cfg_mut(safe, key); shared_structs::add_to_token_config_total_balance(cfg_mut, amount); - if (bag::contains(&safe.coin_storage, key)) { - let existing_coin = bag::borrow_mut, Coin>(&mut safe.coin_storage, key); + if (safe.coin_storage.contains(key)) { + let existing_coin = safe.coin_storage.borrow_mut, Coin>(key); coin::join(existing_coin, coin_in); } else { - bag::add(&mut safe.coin_storage, key, coin_in); + safe.coin_storage.add(key, coin_in); }; } @@ -402,13 +402,13 @@ public fun sync_supply(safe: &mut BridgeSafe, mut coin_in: Coin, ctx: &mut let key = utils::type_name_bytes(); assert_token_is_whitelisted(safe, key); - let cfg_ref = table::borrow(&safe.token_cfg, key); + let cfg_ref = safe.token_cfg.borrow(key); assert!(shared_structs::token_config_is_native(cfg_ref), ENotNativeToken); let expected_balance = shared_structs::token_config_total_balance(cfg_ref); - let actual_balance = if (bag::contains(&safe.coin_storage, key)) { - let stored_coin = bag::borrow, Coin>(&safe.coin_storage, key); + let actual_balance = if (safe.coin_storage.contains(key)) { + let stored_coin = safe.coin_storage.borrow, Coin>(key); coin::value(stored_coin) } else { 0 @@ -421,11 +421,11 @@ public fun sync_supply(safe: &mut BridgeSafe, mut coin_in: Coin, ctx: &mut let top_up_coin = coin::split(&mut coin_in, deficit, ctx); - if (bag::contains(&safe.coin_storage, key)) { - let existing_coin = bag::borrow_mut, Coin>(&mut safe.coin_storage, key); + if (safe.coin_storage.contains(key)) { + let existing_coin = safe.coin_storage.borrow_mut, Coin>(key); coin::join(existing_coin, 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) { @@ -459,7 +459,7 @@ public fun remove_token_from_whitelist(safe: &mut BridgeSafe, ctx: &mut TxCon assert_is_compatible(safe); safe.roles.owner_role().assert_sender_is_active_role(ctx); let key = utils::type_name_bytes(); - let cfg_ref = table::borrow(&safe.token_cfg, key); + let cfg_ref = safe.token_cfg.borrow(key); assert!(!shared_structs::token_config_is_mint_burn(cfg_ref), EIncompatibleTokenFlags); unwhitelist_token(safe, key); } @@ -672,20 +672,20 @@ public(package) fun assert_is_compatible(safe: &BridgeSafe) { } public(package) fun assert_token_is_whitelisted(safe: &BridgeSafe, key: vector) { - assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); - let cfg = table::borrow(&safe.token_cfg, key); + assert!(safe.token_cfg.contains(key), ETokenNotWhitelisted); + let cfg = safe.token_cfg.borrow(key); assert!(shared_structs::token_config_whitelisted(cfg), ETokenNotWhitelisted); } public(package) fun assert_token_is_not_whitelisted(safe: &BridgeSafe, key: vector) { - assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); - let cfg = table::borrow(&safe.token_cfg, key); + assert!(safe.token_cfg.contains(key), ETokenNotWhitelisted); + let cfg = safe.token_cfg.borrow(key); assert!(!shared_structs::token_config_whitelisted(cfg), ETokenAlreadyExists); } public(package) fun assert_token_is_mint_burn(safe: &BridgeSafe, key: vector) { - assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); - let cfg = table::borrow(&safe.token_cfg, key); + assert!(safe.token_cfg.contains(key), ETokenNotWhitelisted); + let cfg = safe.token_cfg.borrow(key); assert!(shared_structs::token_config_is_mint_burn(cfg), EIncompatibleTokenFlags); } @@ -707,7 +707,7 @@ public(package) fun whitelist_token_internal( assert!(minimum_amount <= maximum_amount, EInvalidTokenLimits); let key = utils::type_name_bytes(); - let exists = table::contains(&safe.token_cfg, key); + let exists = safe.token_cfg.contains(key); if (exists) { assert_token_is_not_whitelisted(safe, key); }; @@ -747,7 +747,7 @@ public(package) fun deposit_validate_and_record( assert!(recipient.length() == 32, EInvalidRecipient); let key = utils::type_name_bytes(); - let cfg_ref = table::borrow(&safe.token_cfg, key); + let cfg_ref = safe.token_cfg.borrow(key); assert!(shared_structs::token_config_whitelisted(cfg_ref), ETokenNotWhitelisted); assert!( shared_structs::token_config_is_mint_burn(cfg_ref) == expect_mint_burn, @@ -764,7 +764,7 @@ public(package) fun deposit_validate_and_record( }; let batch_index = safe.batches_count - 1; - let batch = table::borrow_mut(&mut safe.batches, batch_index); + let batch = safe.batches.borrow_mut(batch_index); assert!(safe.deposits_count < MAX_U64, EOverflow); let dep_nonce = safe.deposits_count + 1; @@ -776,10 +776,10 @@ public(package) fun deposit_validate_and_record( recipient, ); - if (!table::contains(&safe.batch_deposits, batch_index)) { - table::add(&mut safe.batch_deposits, batch_index, vector::empty()); + if (!safe.batch_deposits.contains(batch_index)) { + safe.batch_deposits.add(batch_index, vector::empty()); }; - let vec_ref = table::borrow_mut(&mut safe.batch_deposits, batch_index); + let vec_ref = safe.batch_deposits.borrow_mut(batch_index); vector::push_back(vec_ref, dep); safe.deposits_count = dep_nonce; @@ -798,14 +798,14 @@ fun create_new_batch_internal(safe: &mut BridgeSafe, clock: &Clock, _ctx: &mut T 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.add(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); + let batch = safe.batches.borrow(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) } @@ -827,7 +827,7 @@ 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); + let batch = safe.batches.borrow(last_index); !is_batch_final_internal(safe, batch, clock) } @@ -846,17 +846,17 @@ public(package) fun uid_mut(safe: &mut BridgeSafe): &mut UID { } public(package) fun has_token_config(safe: &BridgeSafe): bool { - table::contains(&safe.token_cfg, utils::type_name_bytes()) + safe.token_cfg.contains(utils::type_name_bytes()) } public(package) fun subtract_token_balance(safe: &mut BridgeSafe, amount: u64) { let key = utils::type_name_bytes(); - let cfg = table::borrow_mut(&mut safe.token_cfg, key); + let cfg = safe.token_cfg.borrow_mut(key); shared_structs::subtract_from_token_config_total_balance(cfg, amount); } fun borrow_token_cfg_mut(safe: &mut BridgeSafe, key: vector): &mut TokenConfig { - table::borrow_mut(&mut safe.token_cfg, key) + safe.token_cfg.borrow_mut(key) } public(package) fun roles_mut(safe: &mut BridgeSafe): &mut Roles { diff --git a/sources/shared_structs.move b/sources/shared_structs.move index 188749f..c6722c5 100644 --- a/sources/shared_structs.move +++ b/sources/shared_structs.move @@ -1,6 +1,6 @@ module shared_structs::shared_structs; -use sui::table::{Self, Table}; +use sui::table::Table; public enum DepositStatus has copy, drop, store { None, @@ -203,8 +203,8 @@ public(package) fun upsert_token_config( treasury_id: Option, is_mint_burn: bool, ) { - if (table::contains(config, key)) { - let cfg = table::borrow_mut(config, key); + if (config.contains(key)) { + let cfg = config.borrow_mut(key); set_token_config( cfg, whitelisted, @@ -226,7 +226,7 @@ public(package) fun upsert_token_config( treasury_id, is_mint_burn, ); - table::add(config, key, cfg); + config.add(key, cfg); } public(package) fun set_token_config( diff --git a/sources/utils.move b/sources/utils.move index 49e40c7..f3b7b48 100644 --- a/sources/utils.move +++ b/sources/utils.move @@ -4,12 +4,11 @@ module bridge_safe::utils; -use std::ascii; use std::type_name; /// Returns the type name as bytes for use as storage keys public fun type_name_bytes(): vector { let type_name = type_name::with_defining_ids(); let type_name_string = type_name::into_string(type_name); - ascii::into_bytes(type_name_string) + type_name_string.into_bytes() } From 80ab1d91454388083341494ba5029b2ab6d660b8 Mon Sep 17 00:00:00 2001 From: georgedigkas Date: Fri, 20 Mar 2026 10:45:35 +0200 Subject: [PATCH 23/30] Move 2024 --- sources/bridge_module.move | 22 +++---- .../xmn_mint_cap_adapter.move | 28 ++++----- sources/safe.move | 58 ++++++++----------- sources/upgrade_manager.move | 6 +- tests/shared_structs_tests.move | 26 ++++----- 5 files changed, 64 insertions(+), 76 deletions(-) diff --git a/sources/bridge_module.move b/sources/bridge_module.move index 0bed80a..dc48b31 100644 --- a/sources/bridge_module.move +++ b/sources/bridge_module.move @@ -163,7 +163,7 @@ public fun set_quorum( ctx: &mut TxContext, ) { assert_bridge_is_compatible(bridge); - safe::checkOwnerRole(safe, ctx); + safe.checkOwnerRole(ctx); assert!(new_quorum >= MINIMUM_QUORUM, EQuorumTooLow); assert!(new_quorum <= bridge.relayers.length(), EQuorumExceedsRelayers); @@ -180,7 +180,7 @@ public fun set_batch_settle_timeout_ms( ctx: &mut TxContext, ) { assert_bridge_is_compatible(bridge); - safe::checkOwnerRole(safe, ctx); + safe.checkOwnerRole(ctx); pausable::assert_paused(&bridge.pause); assert!(new_timeout_ms >= safe::get_batch_timeout_ms(safe), ESettleTimeoutBelowSafeBatch); @@ -200,7 +200,7 @@ public fun add_relayer( ctx: &mut TxContext, ) { assert_bridge_is_compatible(bridge); - safe::checkOwnerRole(safe, ctx); + safe.checkOwnerRole(ctx); assert!(public_key.length() == ED25519_PUBLIC_KEY_LENGTH, EInvalidPublicKeyLength); let relayer_address = getAddressFromPublicKey(&public_key); @@ -218,7 +218,7 @@ public fun remove_relayer( ctx: &mut TxContext, ) { assert_bridge_is_compatible(bridge); - safe::checkOwnerRole(safe, ctx); + safe.checkOwnerRole(ctx); let current_count = bridge.relayers.length(); assert!(current_count > bridge.quorum, ECannotRemoveRelayerBelowQuorum); @@ -277,7 +277,7 @@ public fun execute_transfer( ctx: &mut TxContext, ) { assert_bridge_is_compatible(bridge); - safe::assert_is_compatible(safe); + safe.assert_is_compatible(); pre_execute_transfer( bridge, batch_nonce_mvx, @@ -358,13 +358,13 @@ public fun get_relayer_count(bridge: &Bridge): u64 { public fun pause_contract(bridge: &mut Bridge, safe: &BridgeSafe, ctx: &mut TxContext) { assert_bridge_is_compatible(bridge); - safe::checkOwnerRole(safe, ctx); + safe.checkOwnerRole(ctx); pausable::pause(&mut bridge.pause); } public fun unpause_contract(bridge: &mut Bridge, safe: &BridgeSafe, ctx: &mut TxContext) { assert_bridge_is_compatible(bridge); - safe::checkOwnerRole(safe, ctx); + safe.checkOwnerRole(ctx); pausable::unpause(&mut bridge.pause); } @@ -531,7 +531,7 @@ public(package) fun pre_execute_transfer( ctx: &TxContext, ) { assert_relayer(bridge, tx_context::sender(ctx)); - pausable::assert_not_paused(&bridge.pause); + bridge.pause.assert_not_paused(); assert!(!was_batch_executed(bridge, batch_nonce_mvx), EBatchAlreadyExecuted); let len = recipients.length(); @@ -650,7 +650,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]; @@ -665,7 +665,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( @@ -683,7 +683,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/mint_burn_adapters/xmn_mint_cap_adapter.move b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move index 5eebd86..339ab89 100644 --- a/sources/mint_burn_adapters/xmn_mint_cap_adapter.move +++ b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move @@ -29,8 +29,8 @@ public fun deposit( deny_list: &DenyList, ctx: &mut TxContext, ) { - safe::assert_is_compatible(safe); - assert!(has_cap(safe::uid(safe)), EMintBurnCapNotFound); + safe.assert_is_compatible(); + assert!(has_cap(safe.uid()), EMintBurnCapNotFound); let (key, amount, batch_nonce, dep_nonce) = safe::deposit_validate_and_record( safe, @@ -41,7 +41,7 @@ public fun deposit( ctx, ); - burn(safe::uid(safe), xmn_treasury, deny_list, coin_in, ctx); + burn(safe.uid(), xmn_treasury, deny_list, coin_in, ctx); events::emit_deposit_v1( batch_nonce, @@ -67,8 +67,8 @@ public fun execute_transfer( clock: &Clock, ctx: &mut TxContext, ) { - bridge_module::assert_bridge_is_compatible(bridge); - safe::assert_is_compatible(safe); + bridge.assert_bridge_is_compatible(); + safe.assert_is_compatible(); bridge_module::pre_execute_transfer( bridge, batch_nonce_mvx, @@ -109,8 +109,8 @@ public fun whitelist_token( treasury_id: ID, ctx: &TxContext, ) { - safe::assert_is_compatible(safe); - assert!(!has_cap(safe::uid(safe)), EMintBurnCapAlreadyRegistered); + safe.assert_is_compatible(); + assert!(!has_cap(safe.uid()), EMintBurnCapAlreadyRegistered); safe::whitelist_token_internal( safe, minimum_amount, @@ -120,16 +120,16 @@ public fun whitelist_token( true, ctx, ); - register(safe::uid_mut(safe), cap); + 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); - safe::checkOwnerRole(safe, ctx); - assert!(has_cap(safe::uid(safe)), EMintBurnCapNotFound); - deregister(safe::uid_mut(safe), ctx.sender()); + 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(safe, key); } @@ -148,9 +148,9 @@ public(package) fun transfer( if (!safe::has_token_config(safe)) { return false }; if (!safe::get_token_is_mint_burn(safe)) { return false }; if (safe::get_stored_coin_balance(safe) < amount) { return false }; - if (!has_cap(safe::uid(safe))) { return false }; + if (!has_cap(safe.uid())) { return false }; - mint(safe::uid(safe), xmn_treasury, deny_list, amount, receiver, ctx); + mint(safe.uid(), xmn_treasury, deny_list, amount, receiver, ctx); safe::subtract_token_balance(safe, amount); true diff --git a/sources/safe.move b/sources/safe.move index fc9ab66..7268f93 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -169,10 +169,7 @@ public(package) fun transfer( let (is_mint_burn, current_balance) = { let cfg_ref = safe.token_cfg.borrow(key); - ( - shared_structs::token_config_is_mint_burn(cfg_ref), - shared_structs::token_config_total_balance(cfg_ref), - ) + (cfg_ref.token_config_is_mint_burn(), cfg_ref.token_config_total_balance()) }; if (is_mint_burn) { @@ -202,7 +199,7 @@ public(package) fun transfer( transfer::public_transfer(coin_to_transfer, receiver); let cfg_mut = borrow_token_cfg_mut(safe, key); - shared_structs::subtract_from_token_config_total_balance(cfg_mut, amount); + cfg_mut.subtract_from_token_config_total_balance(amount); true } @@ -213,31 +210,31 @@ public fun is_token_whitelisted(safe: &BridgeSafe): bool { return false }; let cfg = safe.token_cfg.borrow(key); - shared_structs::token_config_whitelisted(cfg) + cfg.token_config_whitelisted() } public fun get_token_min_limit(safe: &BridgeSafe): u64 { let key = utils::type_name_bytes(); let cfg = safe.token_cfg.borrow(key); - shared_structs::token_config_min_limit(cfg) + cfg.token_config_min_limit() } public fun get_token_max_limit(safe: &BridgeSafe): u64 { let key = utils::type_name_bytes(); let cfg = safe.token_cfg.borrow(key); - shared_structs::token_config_max_limit(cfg) + cfg.token_config_max_limit() } public fun get_token_is_mint_burn(safe: &BridgeSafe): bool { let key = utils::type_name_bytes(); let cfg = safe.token_cfg.borrow(key); - shared_structs::token_config_is_mint_burn(cfg) + cfg.token_config_is_mint_burn() } public fun get_token_is_native(safe: &BridgeSafe): bool { let key = utils::type_name_bytes(); let cfg = safe.token_cfg.borrow(key); - shared_structs::token_config_is_native(cfg) + cfg.token_config_is_native() } public fun get_batch(safe: &BridgeSafe, batch_nonce: u64, clock: &Clock): (Batch, bool) { @@ -460,7 +457,7 @@ public fun remove_token_from_whitelist(safe: &mut BridgeSafe, ctx: &mut TxCon 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!(!shared_structs::token_config_is_mint_burn(cfg_ref), EIncompatibleTokenFlags); + assert!(!cfg_ref.token_config_is_mint_burn(), EIncompatibleTokenFlags); unwhitelist_token(safe, key); } @@ -468,7 +465,7 @@ public fun remove_token_from_whitelist(safe: &mut BridgeSafe, ctx: &mut TxCon /// 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); - shared_structs::set_token_config_whitelisted(cfg, false); + cfg.set_token_config_whitelisted(false); events::emit_token_removed_from_whitelist(key); } @@ -519,12 +516,12 @@ public fun set_token_min_limit(safe: &mut BridgeSafe, amount: u64, ctx: &mut 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 old_max = cfg.token_config_max_limit(); assert!(amount > 0, EZeroAmount); assert!(amount <= old_max, EInvalidTokenLimits); - shared_structs::set_token_config_min_limit(cfg, amount); + cfg.set_token_config_min_limit(amount); events::emit_token_limits_updated(key, amount, old_max); } @@ -535,10 +532,10 @@ public fun set_token_max_limit(safe: &mut BridgeSafe, amount: u64, ctx: &mut let key = utils::type_name_bytes(); let cfg = borrow_token_cfg_mut(safe, key); - let old_min = shared_structs::token_config_min_limit(cfg); + let old_min = cfg.token_config_min_limit(); assert!(amount >= old_min, EInvalidTokenLimits); - shared_structs::set_token_config_max_limit(cfg, amount); + cfg.set_token_config_max_limit(amount); events::emit_token_limits_updated(key, old_min, amount); } @@ -549,11 +546,8 @@ public fun set_token_is_native(safe: &mut BridgeSafe, is_native: bool, ctx: & let key = utils::type_name_bytes(); let cfg = borrow_token_cfg_mut(safe, key); - assert!( - !(is_native && shared_structs::token_config_is_mint_burn(cfg)), - EIncompatibleTokenFlags, - ); - shared_structs::set_token_config_is_native(cfg, is_native); + 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); } @@ -568,11 +562,8 @@ public fun set_token_is_mint_burn( let key = utils::type_name_bytes(); let cfg = borrow_token_cfg_mut(safe, key); - assert!( - !(is_mint_burn && shared_structs::token_config_is_native(cfg)), - EIncompatibleTokenFlags, - ); - shared_structs::set_token_config_is_mint_burn(cfg, is_mint_burn); + 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); } @@ -686,7 +677,7 @@ public(package) fun assert_token_is_not_whitelisted(safe: &BridgeSafe, key: vect public(package) fun assert_token_is_mint_burn(safe: &BridgeSafe, key: vector) { assert!(safe.token_cfg.contains(key), ETokenNotWhitelisted); let cfg = safe.token_cfg.borrow(key); - assert!(shared_structs::token_config_is_mint_burn(cfg), EIncompatibleTokenFlags); + assert!(cfg.token_config_is_mint_burn(), EIncompatibleTokenFlags); } /// ==== Internal logic helpers ==== @@ -743,21 +734,18 @@ public(package) fun deposit_validate_and_record( clock: &Clock, ctx: &mut TxContext, ): (vector, u64, u64, u64) { - pausable::assert_not_paused(&safe.pause); + safe.pause.assert_not_paused(); assert!(recipient.length() == 32, EInvalidRecipient); let key = utils::type_name_bytes(); let cfg_ref = safe.token_cfg.borrow(key); - assert!(shared_structs::token_config_whitelisted(cfg_ref), ETokenNotWhitelisted); - assert!( - shared_structs::token_config_is_mint_burn(cfg_ref) == expect_mint_burn, - EIncompatibleTokenFlags, - ); + assert!(cfg_ref.token_config_whitelisted(), ETokenNotWhitelisted); + assert!(cfg_ref.token_config_is_mint_burn() == expect_mint_burn, EIncompatibleTokenFlags); 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); + assert!(amount >= cfg_ref.token_config_min_limit(), EAmountBelowMinimum); + assert!(amount <= cfg_ref.token_config_max_limit(), EAmountAboveMaximum); if (should_create_new_batch_internal(safe, clock)) { create_new_batch_internal(safe, clock, ctx); diff --git a/sources/upgrade_manager.move b/sources/upgrade_manager.move index 889528d..0d37a56 100644 --- a/sources/upgrade_manager.move +++ b/sources/upgrade_manager.move @@ -26,7 +26,7 @@ public struct SystemUpgradeCompleted has copy, drop { /// Start coordinated migration across both Safe and Bridge public fun start_system_migration(safe: &mut BridgeSafe, bridge: &mut Bridge, ctx: &mut TxContext) { // Verify ownership through safe - safe::checkOwnerRole(safe, ctx); + safe.checkOwnerRole(ctx); // Start migration for both components safe::start_migration(safe, ctx); @@ -45,7 +45,7 @@ public fun complete_system_migration( bridge: &mut Bridge, ctx: &mut TxContext, ) { - safe::checkOwnerRole(safe, ctx); + safe.checkOwnerRole(ctx); // Complete migration for both components safe::complete_migration(safe, ctx); @@ -59,7 +59,7 @@ public fun complete_system_migration( /// Abort coordinated migration if needed public fun abort_system_migration(safe: &mut BridgeSafe, bridge: &mut Bridge, ctx: &mut TxContext) { - safe::checkOwnerRole(safe, ctx); + safe.checkOwnerRole(ctx); // Abort migration for both components safe::abort_migration(safe, ctx); diff --git a/tests/shared_structs_tests.move b/tests/shared_structs_tests.move index 6edb403..e99c053 100644 --- a/tests/shared_structs_tests.move +++ b/tests/shared_structs_tests.move @@ -17,10 +17,10 @@ fun test_token_config_is_mint_burn() { ); shared_structs::set_token_config_is_mint_burn(&mut config, true); - assert!(shared_structs::token_config_is_mint_burn(&config) == true, 0); + assert!(config.token_config_is_mint_burn() == true, 0); shared_structs::set_token_config_is_mint_burn(&mut config, false); - assert!(shared_structs::token_config_is_mint_burn(&config) == false, 1); + assert!(config.token_config_is_mint_burn() == false, 1); } #[test] @@ -242,21 +242,21 @@ fun test_combined_operations() { let mut batch = shared_structs::create_batch(42, 5000); - shared_structs::set_token_config_is_native(&mut config, true); - shared_structs::set_token_config_is_mint_burn(&mut config, true); - shared_structs::add_to_token_config_total_balance(&mut config, 500); + config.set_token_config_is_native(true); + config.set_token_config_is_mint_burn(true); + config.add_to_token_config_total_balance(500); - assert!(shared_structs::token_config_is_native(&config) == true, 0); - assert!(shared_structs::token_config_is_mint_burn(&config) == true, 2); - assert!(shared_structs::token_config_total_balance(&config) == 500, 3); + assert!(config.token_config_is_native() == true, 0); + assert!(config.token_config_is_mint_burn() == true, 2); + assert!(config.token_config_total_balance() == 500, 3); - shared_structs::set_batch_deposits_count(&mut batch, 10); + batch.set_batch_deposits_count(10); shared_structs::update_batch_last_updated(&mut batch, 6000); - assert!(shared_structs::batch_deposits_count(&batch) == 10, 4); - assert!(shared_structs::batch_last_updated_timestamp_ms(&batch) == 6000, 5); - assert!(shared_structs::batch_nonce(&batch) == 42, 6); - assert!(shared_structs::batch_timestamp_ms(&batch) == 5000, 7); + assert!(batch.batch_deposits_count() == 10, 4); + assert!(batch.batch_last_updated_timestamp_ms() == 6000, 5); + assert!(batch.batch_nonce() == 42, 6); + assert!(batch.batch_timestamp_ms() == 5000, 7); shared_structs::subtract_from_token_config_total_balance(&mut config, 200); assert!(shared_structs::token_config_total_balance(&config) == 300, 8); From da869692ef825f89b51e593b76a99d2e95077f87 Mon Sep 17 00:00:00 2001 From: georgedigkas Date: Fri, 20 Mar 2026 15:33:09 +0200 Subject: [PATCH 24/30] Move 2024 --- sources/bridge_module.move | 78 +++++++++---------- .../xmn_mint_cap_adapter.move | 45 +++++------ sources/safe.move | 74 +++++++++--------- sources/upgrade_manager.move | 22 +++--- sources/utils.move | 3 +- 5 files changed, 107 insertions(+), 115 deletions(-) diff --git a/sources/bridge_module.move b/sources/bridge_module.move index dc48b31..3c33b13 100644 --- a/sources/bridge_module.move +++ b/sources/bridge_module.move @@ -10,13 +10,13 @@ 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 shared_structs::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; @@ -111,7 +111,7 @@ public fun initialize( assert!(pk.length() == ED25519_PUBLIC_KEY_LENGTH, EInvalidPublicKeyLength); let relayer_address = getAddressFromPublicKey(&pk); - vec_set::insert(&mut relayers, relayer_address); + relayers.insert(relayer_address); relayer_public_keys.add(relayer_address, pk); i = i + 1; }; @@ -143,7 +143,7 @@ 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) } @@ -182,9 +182,9 @@ public fun set_batch_settle_timeout_ms( 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); @@ -208,7 +208,7 @@ public fun add_relayer( bridge.relayers.insert(relayer_address); bridge.relayer_public_keys.add(relayer_address, public_key); - events::emit_relayer_added(relayer_address, tx_context::sender(ctx)); + events::emit_relayer_added(relayer_address, ctx.sender()); } public fun remove_relayer( @@ -227,11 +227,11 @@ public fun 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( @@ -239,7 +239,7 @@ 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 { @@ -253,10 +253,8 @@ public fun get_statuses_after_execution( ): (vector, bool) { if (bridge.cross_transfer_statuses.contains(batch_nonce_mvx)) { let cross_status = bridge.cross_transfer_statuses.borrow(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, - ); + 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 { @@ -292,8 +290,7 @@ public fun execute_transfer( let len = recipients.length(); let mut i = 0; while (i < len) { - let success = safe::transfer( - safe, + let success = safe.transfer( &bridge.bridge_cap, *recipients.borrow(i), *amounts.borrow(i), @@ -309,13 +306,13 @@ public fun execute_transfer( fun mark_deposits_executed_in_batch_or_abort(bridge: &mut Bridge, batch_nonce_mvx: u64) { let key = derive_key(batch_nonce_mvx); assert!(!bridge.executed_transfer_by_batch_type_arg.contains(&key), EDepositAlreadyExecuted); - vec_set::insert(&mut bridge.executed_transfer_by_batch_type_arg, key); + 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) } @@ -324,7 +321,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() } } @@ -341,7 +338,7 @@ public fun is_relayer(bridge: &Bridge, addr: address): bool { } public fun get_admin(safe: &BridgeSafe): address { - safe::get_owner(safe) + safe.get_owner() } public fun get_pause(bridge: &Bridge): bool { @@ -359,13 +356,13 @@ public fun get_relayer_count(bridge: &Bridge): u64 { public fun pause_contract(bridge: &mut Bridge, safe: &BridgeSafe, ctx: &mut TxContext) { assert_bridge_is_compatible(bridge); safe.checkOwnerRole(ctx); - pausable::pause(&mut bridge.pause); + bridge.pause.pause(); } public fun unpause_contract(bridge: &mut Bridge, safe: &BridgeSafe, ctx: &mut TxContext) { assert_bridge_is_compatible(bridge); safe.checkOwnerRole(ctx); - pausable::unpause(&mut bridge.pause); + bridge.pause.unpause(); } fun validate_quorum( @@ -419,7 +416,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) } @@ -438,10 +435,10 @@ fun construct_batch_message( let amount = amounts.borrow(i); let deposit_nonce = deposit_nonces.borrow(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)); + 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; }; @@ -452,7 +449,7 @@ fun extract_public_key(signature: &vector): vector { let mut public_key = vector::empty(); let mut i = signature.length() - ED25519_PUBLIC_KEY_LENGTH; while (i < signature.length()) { - vector::push_back(&mut public_key, *signature.borrow(i)); + public_key.push_back(*signature.borrow(i)); i = i + 1; }; public_key @@ -462,14 +459,14 @@ fun extract_signature(signature: &vector): vector { let mut sig_bytes = vector::empty(); let mut i = 0; while (i < signature.length() - ED25519_PUBLIC_KEY_LENGTH) { - vector::push_back(&mut sig_bytes, *signature.borrow(i)); + 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 < relayers.length()) { @@ -502,8 +499,7 @@ public fun execute_transfer_for_testing( let len = recipients.length(); let mut i = 0; while (i < len) { - let success = safe::transfer( - safe, + let success = safe.transfer( &bridge.bridge_cap, *recipients.borrow(i), *amounts.borrow(i), @@ -530,7 +526,7 @@ public(package) fun pre_execute_transfer( clock: &Clock, ctx: &TxContext, ) { - assert_relayer(bridge, tx_context::sender(ctx)); + assert_relayer(bridge, ctx.sender()); bridge.pause.assert_not_paused(); assert!(!was_batch_executed(bridge, batch_nonce_mvx), EBatchAlreadyExecuted); @@ -541,7 +537,7 @@ public(package) fun pre_execute_transfer( 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); + let now = clock.timestamp_ms(); if (bridge.execution_timestamps.contains(batch_nonce_mvx)) { *bridge.execution_timestamps.borrow_mut(batch_nonce_mvx) = now; } else { @@ -556,7 +552,7 @@ public(package) fun record_transfer_result( success: bool, ) { if (success) { - vector::push_back(&mut bridge.transfer_statuses, shared_structs::deposit_status_executed()); + 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; @@ -564,7 +560,7 @@ public(package) fun record_transfer_result( bridge.successful_transfers_by_batch.add(batch_nonce_mvx, 1); }; } else { - vector::push_back(&mut bridge.transfer_statuses, shared_structs::deposit_status_rejected()); + bridge.transfer_statuses.push_back(shared_structs::deposit_status_rejected()); }; } @@ -577,12 +573,12 @@ public(package) fun finalize_batch( 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); + bridge.cross_transfer_statuses.add(batch_nonce_mvx, cross_status); bridge.transfer_statuses = vector::empty(); let successful_count = if (bridge.successful_transfers_by_batch.contains(batch_nonce_mvx)) { @@ -613,7 +609,7 @@ public(package) fun pre_execute_transfer_for_testing( clock: &Clock, ) { mark_deposits_executed_in_batch_or_abort(bridge, batch_nonce_mvx); - let now = clock::timestamp_ms(clock); + let now = clock.timestamp_ms(); if (bridge.execution_timestamps.contains(batch_nonce_mvx)) { *bridge.execution_timestamps.borrow_mut(batch_nonce_mvx) = now; } else { diff --git a/sources/mint_burn_adapters/xmn_mint_cap_adapter.move b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move index 339ab89..1cc446a 100644 --- a/sources/mint_burn_adapters/xmn_mint_cap_adapter.move +++ b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move @@ -1,15 +1,15 @@ module bridge_safe::xmn_mint_cap_adapter; -use bridge_safe::bridge::{Self as bridge_module, Bridge}; +use bridge_safe::bridge::Bridge; use bridge_safe::bridge_roles::BridgeCap; use bridge_safe::events; -use bridge_safe::safe::{Self, BridgeSafe}; +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::{Self as stablecoin_treasury, MintCap, Treasury as XmnTreasury}; +use treasury::treasury::{MintCap, Treasury as XmnTreasury}; public struct CapKey has copy, drop, store { token_type: vector, @@ -32,8 +32,7 @@ public fun deposit( safe.assert_is_compatible(); assert!(has_cap(safe.uid()), EMintBurnCapNotFound); - let (key, amount, batch_nonce, dep_nonce) = safe::deposit_validate_and_record( - safe, + let (key, amount, batch_nonce, dep_nonce) = safe.deposit_validate_and_record( &coin_in, recipient, true, @@ -46,7 +45,7 @@ public fun deposit( events::emit_deposit_v1( batch_nonce, dep_nonce, - tx_context::sender(ctx), + ctx.sender(), recipient, amount, key, @@ -69,8 +68,7 @@ public fun execute_transfer( ) { bridge.assert_bridge_is_compatible(); safe.assert_is_compatible(); - bridge_module::pre_execute_transfer( - bridge, + bridge.pre_execute_transfer( batch_nonce_mvx, &recipients, &amounts, @@ -85,18 +83,18 @@ public fun execute_transfer( while (i < len) { let success = transfer( safe, - bridge_module::bridge_cap(bridge), + bridge.bridge_cap(), *recipients.borrow(i), *amounts.borrow(i), xmn_treasury, deny_list, ctx, ); - bridge_module::record_transfer_result(bridge, batch_nonce_mvx, success); + bridge.record_transfer_result(batch_nonce_mvx, success); i = i + 1; }; - bridge_module::finalize_batch(bridge, batch_nonce_mvx, len, is_batch_complete, clock); + bridge.finalize_batch(batch_nonce_mvx, len, is_batch_complete, clock); } // === Admin Management === @@ -111,8 +109,7 @@ public fun whitelist_token( ) { safe.assert_is_compatible(); assert!(!has_cap(safe.uid()), EMintBurnCapAlreadyRegistered); - safe::whitelist_token_internal( - safe, + safe.whitelist_token_internal( minimum_amount, maximum_amount, false, @@ -131,7 +128,7 @@ public fun remove_token_from_whitelist(safe: &mut BridgeSafe, ctx: &mut TxCon assert!(has_cap(safe.uid()), EMintBurnCapNotFound); deregister(safe.uid_mut(), ctx.sender()); let key = utils::type_name_bytes(); - safe::unwhitelist_token(safe, key); + safe.unwhitelist_token(key); } // === Internal helpers === @@ -145,13 +142,13 @@ public(package) fun transfer( deny_list: &DenyList, ctx: &mut TxContext, ): bool { - if (!safe::has_token_config(safe)) { return false }; - if (!safe::get_token_is_mint_burn(safe)) { return false }; - if (safe::get_stored_coin_balance(safe) < amount) { return false }; + 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(safe, amount); + safe.subtract_token_balance(amount); true } @@ -182,7 +179,7 @@ public(package) fun burn( ctx: &TxContext, ) { let cap = dof::borrow(id, cap_key()); - stablecoin_treasury::burn(xmn_treasury, cap, deny_list, coin_in, ctx); + xmn_treasury.burn(cap, deny_list, coin_in, ctx); } public(package) fun mint( @@ -194,7 +191,7 @@ public(package) fun mint( ctx: &mut TxContext, ) { let cap = dof::borrow(id, cap_key()); - stablecoin_treasury::mint(xmn_treasury, cap, deny_list, amount, receiver, ctx); + xmn_treasury.mint(cap, deny_list, amount, receiver, ctx); } #[test_only] @@ -210,23 +207,23 @@ public fun execute_transfer_for_testing( clock: &Clock, ctx: &mut TxContext, ) { - bridge_module::pre_execute_transfer_for_testing(bridge, batch_nonce_mvx, clock); + 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_module::bridge_cap(bridge), + bridge.bridge_cap(), *recipients.borrow(i), *amounts.borrow(i), xmn_treasury, deny_list, ctx, ); - bridge_module::record_transfer_result(bridge, batch_nonce_mvx, success); + bridge.record_transfer_result(batch_nonce_mvx, success); i = i + 1; }; - bridge_module::finalize_batch(bridge, batch_nonce_mvx, len, is_batch_complete, clock); + bridge.finalize_batch(batch_nonce_mvx, len, is_batch_complete, clock); } diff --git a/sources/safe.move b/sources/safe.move index 7268f93..a88aa87 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -93,9 +93,9 @@ fun init(witness: SAFE, ctx: &mut TxContext) { #[allow(lint(self_transfer))] public fun initialize(ctx: &mut TxContext) { - let deployer = tx_context::sender(ctx); + 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), @@ -137,7 +137,7 @@ public fun deposit( ); if (safe.coin_storage.contains(key)) { - coin::join(safe.coin_storage.borrow_mut, Coin>(key), coin_in); + safe.coin_storage.borrow_mut, Coin>(key).join(coin_in); } else { safe.coin_storage.add(key, coin_in); }; @@ -145,7 +145,7 @@ public fun deposit( events::emit_deposit_v1( batch_nonce, dep_nonce, - tx_context::sender(ctx), + ctx.sender(), recipient, amount, key, @@ -185,16 +185,16 @@ public(package) fun transfer( }; let stored_coin = safe.coin_storage.borrow_mut, Coin>(key); - let coin_value = coin::value(stored_coin); + let coin_value = stored_coin.value(); if (coin_value < amount) { return false }; - let coin_to_transfer = coin::split(stored_coin, amount, ctx); + let coin_to_transfer = stored_coin.split(amount, ctx); - if (coin::value(stored_coin) == 0) { + if (stored_coin.value() == 0) { let empty_coin = safe.coin_storage.remove, Coin>(key); - coin::destroy_zero(empty_coin); + empty_coin.destroy_zero(); }; transfer::public_transfer(coin_to_transfer, receiver); @@ -261,7 +261,7 @@ public fun get_deposits( let deposits = if (safe.batch_deposits.contains(batch_index)) { *safe.batch_deposits.borrow(batch_index) } else { - vector::empty() + vector[] }; if (!safe.batches.contains(batch_index)) { return (deposits, false) @@ -282,12 +282,12 @@ public fun get_bridge_addr(safe: &BridgeSafe): address { /// Get the current owner address public fun get_owner(safe: &BridgeSafe): address { - bridge_roles::owner(&safe.roles) + safe.roles.owner() } /// Get the pending owner address (if any) public fun get_pending_owner(safe: &BridgeSafe): Option
{ - bridge_roles::pending_owner(&safe.roles) + safe.roles.pending_owner() } public fun get_batch_size(safe: &BridgeSafe): u16 { @@ -319,11 +319,11 @@ public(package) fun get_pause_mut(safe: &mut BridgeSafe): &mut Pause { } public fun get_batch_nonce(batch: &Batch): u64 { - shared_structs::batch_nonce(batch) + batch.batch_nonce() } public fun get_batch_deposits_count(batch: &Batch): u16 { - shared_structs::batch_deposits_count(batch) + batch.batch_deposits_count() } public fun get_stored_coin_balance(safe: &mut BridgeSafe): u64 { @@ -332,7 +332,7 @@ public fun get_stored_coin_balance(safe: &mut BridgeSafe): u64 { return 0 }; let cfg_ref = safe.token_cfg.borrow(key); - shared_structs::token_config_total_balance(cfg_ref) + cfg_ref.token_config_total_balance() } public fun get_coin_storage_balance(safe: &BridgeSafe): u64 { @@ -341,7 +341,7 @@ public fun get_coin_storage_balance(safe: &BridgeSafe): u64 { return 0 }; let stored_coin = safe.coin_storage.borrow, Coin>(key); - coin::value(stored_coin) + stored_coin.value() } // === Admin Management === @@ -349,13 +349,13 @@ public fun get_coin_storage_balance(safe: &BridgeSafe): u64 { public fun pause_contract(safe: &mut BridgeSafe, ctx: &mut TxContext) { assert_is_compatible(safe); safe.roles.owner_role().assert_sender_is_active_role(ctx); - pausable::pause(&mut safe.pause); + safe.pause.pause(); } public fun unpause_contract(safe: &mut BridgeSafe, ctx: &mut TxContext) { assert_is_compatible(safe); safe.roles.owner_role().assert_sender_is_active_role(ctx); - pausable::unpause(&mut safe.pause); + safe.pause.unpause(); } public fun transfer_ownership(safe: &mut BridgeSafe, new_owner: address, ctx: &TxContext) { @@ -376,7 +376,7 @@ public fun init_supply(safe: &mut BridgeSafe, coin_in: Coin, ctx: &mut TxC assert_token_is_whitelisted(safe, key); let cfg_ref = safe.token_cfg.borrow(key); - assert!(shared_structs::token_config_is_native(cfg_ref), ENotNativeToken); + assert!(cfg_ref.token_config_is_native(), ENotNativeToken); let amount = coin::value(&coin_in); @@ -385,7 +385,7 @@ public fun init_supply(safe: &mut BridgeSafe, coin_in: Coin, ctx: &mut TxC if (safe.coin_storage.contains(key)) { let existing_coin = safe.coin_storage.borrow_mut, Coin>(key); - coin::join(existing_coin, coin_in); + existing_coin.join(coin_in); } else { safe.coin_storage.add(key, coin_in); }; @@ -400,13 +400,13 @@ public fun sync_supply(safe: &mut BridgeSafe, mut coin_in: Coin, ctx: &mut assert_token_is_whitelisted(safe, key); let cfg_ref = safe.token_cfg.borrow(key); - assert!(shared_structs::token_config_is_native(cfg_ref), ENotNativeToken); + 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 (safe.coin_storage.contains(key)) { let stored_coin = safe.coin_storage.borrow, Coin>(key); - coin::value(stored_coin) + stored_coin.value() } else { 0 }; @@ -414,21 +414,21 @@ 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 (safe.coin_storage.contains(key)) { let existing_coin = safe.coin_storage.borrow_mut, Coin>(key); - coin::join(existing_coin, top_up_coin); + existing_coin.join(top_up_coin); } else { 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()); }; } @@ -493,7 +493,7 @@ public fun set_batch_settle_timeout_ms( ctx: &mut TxContext, ) { assert_is_compatible(safe); - pausable::assert_paused(&safe.pause); + 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); @@ -742,7 +742,7 @@ public(package) fun deposit_validate_and_record( assert!(cfg_ref.token_config_whitelisted(), ETokenNotWhitelisted); assert!(cfg_ref.token_config_is_mint_burn() == expect_mint_burn, EIncompatibleTokenFlags); - let amount = coin::value(coin_in); + let amount = coin_in.value(); assert!(amount > 0, EZeroAmount); assert!(amount >= cfg_ref.token_config_min_limit(), EAmountBelowMinimum); assert!(amount <= cfg_ref.token_config_max_limit(), EAmountAboveMaximum); @@ -760,15 +760,15 @@ public(package) fun deposit_validate_and_record( dep_nonce, key, amount, - tx_context::sender(ctx), + ctx.sender(), recipient, ); if (!safe.batch_deposits.contains(batch_index)) { - safe.batch_deposits.add(batch_index, vector::empty()); + safe.batch_deposits.add(batch_index, vector[]); }; let vec_ref = safe.batch_deposits.borrow_mut(batch_index); - vector::push_back(vec_ref, dep); + vec_ref.push_back(dep); safe.deposits_count = dep_nonce; shared_structs::increment_batch_deposits(batch); @@ -785,7 +785,7 @@ public(package) fun deposit_validate_and_record( 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)); + let batch = shared_structs::create_batch(nonce, clock.timestamp_ms()); safe.batches.add(safe.batches_count, batch); safe.batches_count = nonce; } @@ -804,11 +804,11 @@ fun is_batch_progress_over_internal( clock: &Clock, ): bool { if (dep_count == 0) { return false }; - (timestamp_ms + safe.batch_timeout_ms) <= clock::timestamp_ms(clock) + (timestamp_ms + safe.batch_timeout_ms) <= clock.timestamp_ms() } 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) + (shared_structs::batch_last_updated_timestamp_ms(batch) + safe.batch_settle_timeout_ms) <= clock.timestamp_ms() } fun is_any_batch_in_progress_internal(safe: &BridgeSafe, clock: &Clock): bool { @@ -876,7 +876,7 @@ public fun deposit_mint_burn_for_testing( events::emit_deposit_v1( batch_nonce, dep_nonce, - tx_context::sender(ctx), + ctx.sender(), recipient, amount, key, diff --git a/sources/upgrade_manager.move b/sources/upgrade_manager.move index 0d37a56..4d498c6 100644 --- a/sources/upgrade_manager.move +++ b/sources/upgrade_manager.move @@ -5,9 +5,9 @@ module bridge_safe::upgrade_manager; -use bridge_safe::bridge::{Self, Bridge}; +use bridge_safe::bridge::Bridge; use bridge_safe::bridge_version_control; -use bridge_safe::safe::{Self, BridgeSafe}; +use bridge_safe::safe::BridgeSafe; use sui::event; // === Events === @@ -29,12 +29,12 @@ public fun start_system_migration(safe: &mut BridgeSafe, bridge: &mut Bridge, ct safe.checkOwnerRole(ctx); // Start migration for both components - safe::start_migration(safe, ctx); - bridge::start_bridge_migration(bridge, safe, ctx); + safe.start_migration(ctx); + bridge.start_bridge_migration(safe, ctx); event::emit(SystemUpgradeInitiated { - safe_versions: safe::compatible_versions(safe), - bridge_versions: bridge::bridge_compatible_versions(bridge), + safe_versions: safe.compatible_versions(), + bridge_versions: bridge.bridge_compatible_versions(), initiator: ctx.sender(), }); } @@ -48,8 +48,8 @@ public fun complete_system_migration( safe.checkOwnerRole(ctx); // Complete migration for both components - safe::complete_migration(safe, ctx); - bridge::complete_bridge_migration(bridge, safe, ctx); + safe.complete_migration(ctx); + bridge.complete_bridge_migration(safe, ctx); event::emit(SystemUpgradeCompleted { new_version: bridge_version_control::current_version(), @@ -62,11 +62,11 @@ public fun abort_system_migration(safe: &mut BridgeSafe, bridge: &mut Bridge, ct safe.checkOwnerRole(ctx); // Abort migration for both components - safe::abort_migration(safe, ctx); - bridge::abort_bridge_migration(bridge, safe, ctx); + safe.abort_migration(ctx); + bridge.abort_bridge_migration(safe, ctx); } /// Check if system-wide migration is in progress public fun is_system_migration_in_progress(safe: &BridgeSafe, bridge: &Bridge): bool { - safe::is_migration_in_progress(safe) || bridge::is_bridge_migration_in_progress(bridge) + safe.is_migration_in_progress() || bridge.is_bridge_migration_in_progress() } diff --git a/sources/utils.move b/sources/utils.move index f3b7b48..eb17b84 100644 --- a/sources/utils.move +++ b/sources/utils.move @@ -9,6 +9,5 @@ use std::type_name; /// Returns the type name as bytes for use as storage keys public fun type_name_bytes(): vector { let type_name = type_name::with_defining_ids(); - let type_name_string = type_name::into_string(type_name); - type_name_string.into_bytes() + type_name.into_string().into_bytes() } From 77fa73f0773a8233e3e3f35441936174ec58c916 Mon Sep 17 00:00:00 2001 From: georgedigkas Date: Fri, 20 Mar 2026 16:28:00 +0200 Subject: [PATCH 25/30] Move 2024 --- sources/bridge_module.move | 10 +++++----- sources/safe.move | 24 ++++++++++++------------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/sources/bridge_module.move b/sources/bridge_module.move index 3c33b13..bbea44b 100644 --- a/sources/bridge_module.move +++ b/sources/bridge_module.move @@ -126,7 +126,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>(), @@ -258,7 +258,7 @@ public fun get_statuses_after_execution( let is_final = is_mvx_batch_final(bridge, created_timestamp, clock); (statuses, is_final) } else { - (vector::empty(), false) + (vector[], false) } } @@ -446,7 +446,7 @@ fun construct_batch_message( } fun extract_public_key(signature: &vector): vector { - let mut public_key = vector::empty(); + 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)); @@ -456,7 +456,7 @@ fun extract_public_key(signature: &vector): vector { } fun extract_signature(signature: &vector): vector { - let mut sig_bytes = vector::empty(); + let mut sig_bytes = vector[]; let mut i = 0; while (i < signature.length() - ED25519_PUBLIC_KEY_LENGTH) { sig_bytes.push_back(*signature.borrow(i)); @@ -579,7 +579,7 @@ public(package) fun finalize_batch( clock.timestamp_ms(), ); bridge.cross_transfer_statuses.add(batch_nonce_mvx, cross_status); - bridge.transfer_statuses = vector::empty(); + bridge.transfer_statuses = vector[]; let successful_count = if (bridge.successful_transfers_by_batch.contains(batch_nonce_mvx)) { let count = *bridge.successful_transfers_by_batch.borrow(batch_nonce_mvx); diff --git a/sources/safe.move b/sources/safe.move index a88aa87..0319d7e 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -14,7 +14,7 @@ use bridge_safe::utils; use shared_structs::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}; @@ -381,7 +381,7 @@ public fun init_supply(safe: &mut BridgeSafe, coin_in: Coin, ctx: &mut TxC 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 (safe.coin_storage.contains(key)) { let existing_coin = safe.coin_storage.borrow_mut, Coin>(key); @@ -665,13 +665,13 @@ public(package) fun assert_is_compatible(safe: &BridgeSafe) { public(package) fun assert_token_is_whitelisted(safe: &BridgeSafe, key: vector) { assert!(safe.token_cfg.contains(key), ETokenNotWhitelisted); let cfg = safe.token_cfg.borrow(key); - assert!(shared_structs::token_config_whitelisted(cfg), ETokenNotWhitelisted); + assert!(cfg.token_config_whitelisted(), ETokenNotWhitelisted); } public(package) fun assert_token_is_not_whitelisted(safe: &BridgeSafe, key: vector) { assert!(safe.token_cfg.contains(key), ETokenNotWhitelisted); let cfg = safe.token_cfg.borrow(key); - assert!(!shared_structs::token_config_whitelisted(cfg), ETokenAlreadyExists); + assert!(!cfg.token_config_whitelisted(), ETokenAlreadyExists); } public(package) fun assert_token_is_mint_burn(safe: &BridgeSafe, key: vector) { @@ -771,13 +771,13 @@ public(package) fun deposit_validate_and_record( vec_ref.push_back(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)); + batch.increment_batch_deposits(); + batch.set_batch_last_updated_timestamp_ms(clock.timestamp_ms()); - let batch_nonce = shared_structs::batch_nonce(batch); + let batch_nonce = batch.batch_nonce(); let cfg = borrow_token_cfg_mut(safe, key); - shared_structs::add_to_token_config_total_balance(cfg, amount); + cfg.add_to_token_config_total_balance(amount); (key, amount, batch_nonce, dep_nonce) } @@ -794,7 +794,7 @@ 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 = safe.batches.borrow(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) + is_batch_progress_over_internal(safe, batch.batch_deposits_count(), batch.batch_timestamp_ms(), clock) || (batch.batch_deposits_count() >= safe.batch_size) } fun is_batch_progress_over_internal( @@ -808,7 +808,7 @@ fun is_batch_progress_over_internal( } 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() + (batch.batch_last_updated_timestamp_ms() + safe.batch_settle_timeout_ms) <= clock.timestamp_ms() } fun is_any_batch_in_progress_internal(safe: &BridgeSafe, clock: &Clock): bool { @@ -840,7 +840,7 @@ public(package) fun has_token_config(safe: &BridgeSafe): bool { public(package) fun subtract_token_balance(safe: &mut BridgeSafe, amount: u64) { let key = utils::type_name_bytes(); let cfg = safe.token_cfg.borrow_mut(key); - shared_structs::subtract_from_token_config_total_balance(cfg, amount); + cfg.subtract_from_token_config_total_balance(amount); } fun borrow_token_cfg_mut(safe: &mut BridgeSafe, key: vector): &mut TokenConfig { @@ -897,5 +897,5 @@ public fun create_batch_for_testing(safe: &mut BridgeSafe, clock: &Clock, ctx: & public fun add_to_balance_for_testing(safe: &mut BridgeSafe, amount: u64) { let key = utils::type_name_bytes(); 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); } From 343f39c74af1f7edf4bac25c299520a1341e24d3 Mon Sep 17 00:00:00 2001 From: Eveline Molnar Date: Wed, 25 Mar 2026 15:07:41 +0200 Subject: [PATCH 26/30] revert locked token changes --- Move.toml | 1 + sources/bridge_module.move | 22 +- sources/events.move | 12 + .../xmn_mint_cap_adapter.move | 11 +- sources/safe.move | 86 +++++-- sources/shared_structs.move | 35 ++- tests/bridge_comprehensive_tests.move | 50 +++- tests/deposit_transfer_tests.move | 232 +++++++++++++++++- tests/events_tests.move | 37 ++- tests/safe_edge_case_tests.move | 21 +- tests/safe_internal_tests.move | 31 ++- tests/security_tests.move | 20 +- tests/shared_structs_tests.move | 9 + tests/upgrade_tests.move | 16 +- 14 files changed, 539 insertions(+), 44 deletions(-) diff --git a/Move.toml b/Move.toml index 1e8619d..ef502ba 100644 --- a/Move.toml +++ b/Move.toml @@ -5,6 +5,7 @@ version = "0.0.1" [dependencies] treasury = { git = "https://github.com/multiversx/stablecoin-sui.git", rev="xmn-launch", subdir = "packages/treasury" } +locked_token = { git = "https://github.com/multiversx/mx-locked-token-sc-sui.git", rev = "fdeec1e875b5a36aa42f62da293e65e15623bba6", subdir = "" } [addresses] shared_structs = "0x0" diff --git a/sources/bridge_module.move b/sources/bridge_module.move index d08ff1a..40261d8 100644 --- a/sources/bridge_module.move +++ b/sources/bridge_module.move @@ -12,6 +12,8 @@ use bridge_safe::pausable::{Self, Pause}; use bridge_safe::safe::{Self, 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 std::u64::{min, max}; use sui::address; @@ -267,6 +269,7 @@ public fun execute_transfer( batch_nonce_mvx: u64, signatures: vector>, is_batch_complete: bool, + treasury: &mut treasury::Treasury, clock: &Clock, ctx: &mut TxContext, ) { @@ -275,7 +278,14 @@ public fun execute_transfer( let len = vector::length(&recipients); let mut i = 0; while (i < len) { - let success = safe::transfer(safe, &bridge.bridge_cap, *vector::borrow(&recipients, i), *vector::borrow(&amounts, i), ctx); + let success = safe::transfer( + safe, + &bridge.bridge_cap, + *vector::borrow(&recipients, i), + *vector::borrow(&amounts, i), + treasury, + ctx, + ); record_transfer_result(bridge, batch_nonce_mvx, success); i = i + 1; }; @@ -472,6 +482,7 @@ public fun execute_transfer_for_testing( amounts: vector, batch_nonce_mvx: u64, is_batch_complete: bool, + treasury: &mut treasury::Treasury, clock: &Clock, ctx: &mut TxContext, ) { @@ -480,7 +491,14 @@ public fun execute_transfer_for_testing( let len = vector::length(&recipients); let mut i = 0; while (i < len) { - let success = safe::transfer(safe, &bridge.bridge_cap, *vector::borrow(&recipients, i), *vector::borrow(&amounts, i), ctx); + let success = safe::transfer( + safe, + &bridge.bridge_cap, + *vector::borrow(&recipients, i), + *vector::borrow(&amounts, i), + treasury, + ctx, + ); record_transfer_result(bridge, batch_nonce_mvx, success); i = i + 1; }; diff --git a/sources/events.move b/sources/events.move index bff45c5..18be9be 100644 --- a/sources/events.move +++ b/sources/events.move @@ -48,6 +48,7 @@ public struct TokenWhitelisted has copy, drop { max_limit: u64, is_native: bool, is_mint_burn: bool, + is_locked: bool, } public struct TokenRemovedFromWhitelist has copy, drop { @@ -65,6 +66,11 @@ public struct TokenIsNativeUpdated has copy, drop { is_native: bool, } +public struct TokenIsLockedUpdated has copy, drop { + token_type: vector, + is_locked: bool, +} + public struct TokenIsMintBurnUpdated has copy, drop { token_type: vector, is_mint_burn: bool, @@ -143,6 +149,7 @@ public(package) fun emit_token_whitelisted( max_limit: u64, is_native: bool, is_mint_burn: bool, + is_locked: bool, ) { event::emit(TokenWhitelisted { token_type, @@ -150,6 +157,7 @@ public(package) fun emit_token_whitelisted( max_limit, is_native, is_mint_burn, + is_locked, }); } @@ -169,6 +177,10 @@ public(package) fun emit_token_is_native_updated(token_type: vector, is_nati event::emit(TokenIsNativeUpdated { token_type, is_native }); } +public(package) fun emit_token_is_locked_updated(token_type: vector, is_locked: bool) { + event::emit(TokenIsLockedUpdated { token_type, is_locked }); +} + public(package) fun emit_token_is_mint_burn_updated(token_type: vector, is_mint_burn: bool) { event::emit(TokenIsMintBurnUpdated { token_type, is_mint_burn }); } diff --git a/sources/mint_burn_adapters/xmn_mint_cap_adapter.move b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move index 4b736ca..a8e16a8 100644 --- a/sources/mint_burn_adapters/xmn_mint_cap_adapter.move +++ b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move @@ -77,7 +77,16 @@ public fun whitelist_token( ctx: &TxContext, ) { assert!(!has_cap(safe::uid(safe)), EMintBurnCapAlreadyRegistered); - safe::whitelist_token_internal(safe, minimum_amount, maximum_amount, false, option::some(treasury_id), true, ctx); + safe::whitelist_token_internal( + safe, + minimum_amount, + maximum_amount, + false, + option::some(treasury_id), + true, + false, + ctx, + ); register(safe::uid_mut(safe), cap); } diff --git a/sources/safe.move b/sources/safe.move index c4fcfe9..ddbb06d 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -11,6 +11,8 @@ use bridge_safe::events; 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::{Self as lkt}; use shared_structs::shared_structs::{Self, TokenConfig, Batch, Deposit}; use std::u64::{min, max}; use sui::bag::{Self, Bag}; @@ -75,6 +77,7 @@ public struct BridgeSafe has key { batches: Table, batch_deposits: Table>, coin_storage: Bag, + from_coin_cap: lkt::FromCoinCap, compatible_versions: VecSet, } @@ -92,7 +95,7 @@ fun init(witness: SAFE, ctx: &mut TxContext) { } #[allow(lint(self_transfer))] -public fun initialize(ctx: &mut TxContext) { +public fun initialize(from_coin_cap: lkt::FromCoinCap, ctx: &mut TxContext) { let deployer = tx_context::sender(ctx); let w = bridge_roles::grant_witness(); let (bridge_cap) = bridge_roles::publish_caps(w, ctx); @@ -111,6 +114,7 @@ public fun initialize(ctx: &mut TxContext) { batches: table::new(ctx), batch_deposits: table::new(ctx), coin_storage: bag::new(ctx), + from_coin_cap, compatible_versions: vec_set::singleton(bridge_version_control::current_version()), }; @@ -145,6 +149,7 @@ public(package) fun transfer( _bridge_cap: &bridge_roles::BridgeCap, receiver: address, amount: u64, + treasury: &mut lkt::Treasury, ctx: &mut TxContext, ): bool { let key = utils::type_name_bytes(); @@ -153,11 +158,12 @@ public(package) fun transfer( return false }; - let (is_mint_burn, current_balance) = { + let (is_mint_burn, current_balance, is_locked) = { let cfg_ref = table::borrow(&safe.token_cfg, key); ( shared_structs::token_config_is_mint_burn(cfg_ref), shared_structs::token_config_total_balance(cfg_ref), + shared_structs::get_token_config_is_locked(cfg_ref), ) }; @@ -173,19 +179,46 @@ public(package) fun transfer( return false }; - 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 - }; + if (!is_locked) { + 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 + }; - let coin_to_transfer = coin::split(stored_coin, amount, ctx); + let coin_to_transfer = coin::split(stored_coin, amount, ctx); - if (coin::value(stored_coin) == 0) { - let empty_coin = bag::remove, Coin>(&mut safe.coin_storage, key); - coin::destroy_zero(empty_coin); + 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, Coin>( + &mut safe.coin_storage, + key, + ); + let coin_value = coin::value(stored_bt_coin); + if (coin_value < amount) { + return false + }; + + let coin_bt = coin::split(stored_bt_coin, amount, ctx); + if (coin::value(stored_bt_coin) == 0) { + let empty_coin = bag::remove, Coin>( + &mut safe.coin_storage, + key, + ); + coin::destroy_zero(empty_coin); + }; + lkt::transfer_from_coin( + treasury, + receiver, + &safe.from_coin_cap, + coin_bt, + ctx, + ); }; - transfer::public_transfer(coin_to_transfer, receiver); let cfg_mut = borrow_token_cfg_mut(safe, key); shared_structs::subtract_from_token_config_total_balance(cfg_mut, amount); @@ -419,9 +452,10 @@ public fun whitelist_token( safe: &mut BridgeSafe, minimum_amount: u64, maximum_amount: u64, + is_locked: bool, ctx: &mut TxContext, ) { - whitelist_token_internal(safe, minimum_amount, maximum_amount, true, option::none(), false, ctx); + whitelist_token_internal(safe, minimum_amount, maximum_amount, true, option::none(), false, is_locked, ctx); } public fun remove_token_from_whitelist(safe: &mut BridgeSafe, ctx: &mut TxContext) { @@ -522,11 +556,29 @@ public fun set_token_is_mint_burn( !(is_mint_burn && shared_structs::token_config_is_native(cfg)), EIncompatibleTokenFlags, ); + assert!( + !(is_mint_burn && shared_structs::get_token_config_is_locked(cfg)), + EIncompatibleTokenFlags, + ); shared_structs::set_token_config_is_mint_burn(cfg, is_mint_burn); events::emit_token_is_mint_burn_updated(key, is_mint_burn); } +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); + + 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); + + events::emit_token_is_locked_updated(key, is_locked); +} + // === Upgrade Management === /// Returns the compatible versions for the safe @@ -648,11 +700,13 @@ public(package) fun whitelist_token_internal( is_native: bool, treasury_id: Option, is_mint_burn: bool, + is_locked: bool, ctx: &TxContext, ) { safe.roles.owner_role().assert_sender_is_active_role(ctx); assert!(!(is_mint_burn && is_native), EIncompatibleTokenFlags); + assert!(!(is_mint_burn && is_locked), EIncompatibleTokenFlags); assert!(minimum_amount > 0, EZeroAmount); assert!(minimum_amount <= maximum_amount, EInvalidTokenLimits); @@ -671,6 +725,7 @@ public(package) fun whitelist_token_internal( maximum_amount, treasury_id, is_mint_burn, + is_locked, ); events::emit_token_whitelisted( @@ -679,6 +734,7 @@ public(package) fun whitelist_token_internal( maximum_amount, is_native, is_mint_burn, + is_locked, ); } @@ -824,8 +880,8 @@ public fun deposit_mint_burn_for_testing( } #[test_only] -public fun init_for_testing(ctx: &mut TxContext) { - initialize(ctx); +public fun init_for_testing(from_cap: lkt::FromCoinCap, ctx: &mut TxContext) { + initialize(from_cap, ctx); } #[test_only] diff --git a/sources/shared_structs.move b/sources/shared_structs.move index 6521aac..650ad14 100644 --- a/sources/shared_structs.move +++ b/sources/shared_structs.move @@ -2,6 +2,10 @@ module shared_structs::shared_structs; use sui::table::{Self, Table}; +const EUnderflow: u64 = 0; +const EOverflow: u64 = 1; +const MAX_U64: u64 = 18446744073709551615; + public enum DepositStatus has copy, drop, store { None, Pending, @@ -38,6 +42,7 @@ public struct TokenConfig has copy, drop, store { max_limit: u64, total_balance: u64, treasury_id: Option, + is_locked: bool, } public struct AdminRole has key { @@ -143,10 +148,13 @@ public fun token_config_treasury_id(config: &TokenConfig): Option { config.treasury_id } -const EUnderflow: u64 = 0; -const EOverflow: u64 = 1; +public(package) fun set_token_config_is_locked(config: &mut TokenConfig, is_locked: bool) { + config.is_locked = is_locked; +} -const MAX_U64: u64 = 18446744073709551615; +public fun get_token_config_is_locked(config: &TokenConfig): bool { + config.is_locked +} public fun add_to_token_config_total_balance(config: &mut TokenConfig, amount: u64) { assert!(config.total_balance <= MAX_U64 - amount, EOverflow); @@ -199,15 +207,24 @@ public fun upsert_token_config( max_limit: u64, treasury_id: Option, is_mint_burn: bool, + is_locked: bool ) { if (table::contains(config, key)) { let cfg = table::borrow_mut(config, key); - set_token_config(cfg, whitelisted, is_native, min_limit, max_limit, treasury_id, is_mint_burn); + set_token_config(cfg, whitelisted, is_native, min_limit, max_limit, treasury_id, is_mint_burn, is_locked); return; }; - let cfg = create_token_config(whitelisted, is_native, min_limit, max_limit, treasury_id, is_mint_burn); + let cfg = create_token_config( + whitelisted, + is_native, + is_mint_burn, + min_limit, + max_limit, + treasury_id, + is_locked, + ); table::add(config, key, cfg); } @@ -219,6 +236,7 @@ public fun set_token_config( max_limit: u64, treasury_id: Option, is_mint_burn: bool, + is_locked: bool, ) { set_token_config_whitelisted(config, whitelisted); set_token_config_is_native(config, is_native); @@ -226,23 +244,26 @@ public fun set_token_config( set_token_config_min_limit(config, min_limit); set_token_config_max_limit(config, max_limit); config.treasury_id = treasury_id; + set_token_config_is_locked(config, is_locked); } public fun create_token_config( whitelisted: bool, is_native: bool, + is_mint_burn: bool, min_limit: u64, max_limit: u64, treasury_id: Option, - is_mint_burn: bool, + is_locked: bool, ): TokenConfig { TokenConfig { whitelisted, is_native, + is_mint_burn, min_limit, max_limit, total_balance: 0, treasury_id, - is_mint_burn, + is_locked, } } diff --git a/tests/bridge_comprehensive_tests.move b/tests/bridge_comprehensive_tests.move index e9aa601..6d3b1ae 100644 --- a/tests/bridge_comprehensive_tests.move +++ b/tests/bridge_comprehensive_tests.move @@ -4,6 +4,8 @@ module bridge_safe::bridge_comprehensive_tests; use bridge_safe::bridge::{Self, Bridge}; use bridge_safe::bridge_roles::BridgeCap; use bridge_safe::safe::{Self, BridgeSafe}; +use locked_token::bridge_token::{Self as br, BRIDGE_TOKEN}; +use locked_token::treasury::{Self as lkt, Treasury, FromCoinCap}; use sui::clock; use sui::test_scenario::{Self as ts, Scenario}; use sui_extensions::two_step_role::ESenderNotActiveRole; @@ -27,9 +29,20 @@ const PK4: vector = b"98765432109876543210987654321098"; fun setup(): Scenario { let mut s = ts::begin(ADMIN); + br::init_for_testing(s.ctx()); + s.next_tx(ADMIN); { - safe::init_for_testing(s.ctx()); + let mut treasury = s.take_shared>(); + lkt::transfer_to_coin_cap(&mut treasury, ADMIN, s.ctx()); + lkt::transfer_from_coin_cap(&mut treasury, ADMIN, s.ctx()); + ts::return_shared(treasury); + }; + + s.next_tx(ADMIN); + { + let from_cap_db = s.take_from_address>(ADMIN); + safe::init_for_testing(from_cap_db, s.ctx()); }; s @@ -48,6 +61,7 @@ fun test_initialize_bridge_success() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -130,6 +144,7 @@ fun test_set_quorum_success() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -175,6 +190,7 @@ fun test_set_quorum_not_admin() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -218,6 +234,7 @@ fun test_add_relayer_success() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -265,6 +282,7 @@ fun test_remove_relayer_success() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -318,6 +336,7 @@ fun test_remove_relayer_below_quorum() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -361,6 +380,7 @@ fun test_pause_unpause_contract() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -415,6 +435,7 @@ fun test_getter_functions() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -485,6 +506,7 @@ fun test_set_batch_settle_timeout_success() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -544,6 +566,7 @@ fun test_set_batch_settle_timeout_not_admin() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -596,6 +619,7 @@ fun test_execute_transfer_invalid_signature_length() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -620,6 +644,7 @@ fun test_execute_transfer_invalid_signature_length() { { let mut bridge = ts::take_shared(&scenario); let mut safe = ts::take_shared(&scenario); + let mut treasury = ts::take_shared>(&scenario); let clock = clock::create_for_testing(ts::ctx(&mut scenario)); let recipients = vector[USER]; @@ -638,12 +663,14 @@ fun test_execute_transfer_invalid_signature_length() { batch_nonce_mvx, invalid_signatures, false, + &mut treasury, &clock, ts::ctx(&mut scenario), ); ts::return_shared(bridge); ts::return_shared(safe); + ts::return_shared(treasury); clock::destroy_for_testing(clock); }; @@ -664,6 +691,7 @@ fun test_execute_transfer_insufficient_signatures() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -688,6 +716,7 @@ fun test_execute_transfer_insufficient_signatures() { { let mut bridge = ts::take_shared(&scenario); let mut safe = ts::take_shared(&scenario); + let mut treasury = ts::take_shared>(&scenario); let clock = clock::create_for_testing(ts::ctx(&mut scenario)); let recipients = vector[USER]; @@ -717,12 +746,14 @@ fun test_execute_transfer_insufficient_signatures() { batch_nonce_mvx, signatures, false, + &mut treasury, &clock, ts::ctx(&mut scenario), ); ts::return_shared(bridge); ts::return_shared(safe); + ts::return_shared(treasury); clock::destroy_for_testing(clock); }; @@ -743,6 +774,7 @@ fun test_add_relayer_invalid_public_key_length() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -788,6 +820,7 @@ fun test_add_relayer_not_admin() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -832,6 +865,7 @@ fun test_remove_relayer_not_admin() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -876,6 +910,7 @@ fun test_pause_contract_not_admin() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -920,6 +955,7 @@ fun test_unpause_contract_not_admin() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -984,6 +1020,7 @@ fun test_getAddressFromPublicKey() { fun setup_bridge_with_relayers_for_quorum(): (Scenario, vector>, vector
) { let mut scenario = ts::begin(ADMIN); + br::init_for_testing(scenario.ctx()); let pk1 = x"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; let pk2 = x"abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"; @@ -1001,7 +1038,16 @@ fun setup_bridge_with_relayers_for_quorum(): (Scenario, vector>, vect scenario.next_tx(ADMIN); { - safe::init_for_testing(scenario.ctx()); + let mut treasury = scenario.take_shared>(); + lkt::transfer_to_coin_cap(&mut treasury, ADMIN, scenario.ctx()); + lkt::transfer_from_coin_cap(&mut treasury, ADMIN, scenario.ctx()); + ts::return_shared(treasury); + }; + + scenario.next_tx(ADMIN); + { + let from_cap_db = scenario.take_from_address>(ADMIN); + safe::init_for_testing(from_cap_db, scenario.ctx()); }; scenario.next_tx(ADMIN); diff --git a/tests/deposit_transfer_tests.move b/tests/deposit_transfer_tests.move index 7bda79f..ef4c4b5 100644 --- a/tests/deposit_transfer_tests.move +++ b/tests/deposit_transfer_tests.move @@ -5,6 +5,8 @@ use bridge_safe::bridge_roles::BridgeCap; use bridge_safe::pausable; use bridge_safe::safe::{Self, BridgeSafe}; use bridge_safe::xmn_mint_cap_adapter; +use locked_token::bridge_token::{Self as br, BRIDGE_TOKEN}; +use locked_token::treasury::{Self as lkt, Treasury, FromCoinCap}; use sui::clock; use sui::coin; use sui::deny_list::DenyList; @@ -32,9 +34,20 @@ const DEPOSIT_AMOUNT: u64 = 50000; fun setup(): Scenario { let mut s = ts::begin(ADMIN); + br::init_for_testing(s.ctx()); + + s.next_tx(ADMIN); + { + let mut treasury = s.take_shared>(); + lkt::transfer_to_coin_cap(&mut treasury, ADMIN, s.ctx()); + lkt::transfer_from_coin_cap(&mut treasury, ADMIN, s.ctx()); + ts::return_shared(treasury); + }; + s.next_tx(ADMIN); { - safe::init_for_testing(s.ctx()); + let from_cap_db = s.take_from_address>(ADMIN); + safe::init_for_testing(from_cap_db, s.ctx()); }; s @@ -53,6 +66,7 @@ fun test_deposit_basic() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -114,6 +128,7 @@ fun test_deposit_multiple_same_batch() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -185,6 +200,7 @@ fun test_deposit_triggers_new_batch() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -255,6 +271,7 @@ fun test_deposit_invalid_recipient() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -307,6 +324,7 @@ fun test_deposit_zero_amount() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -340,6 +358,7 @@ fun test_deposit_amount_below_minimum() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -373,6 +392,7 @@ fun test_deposit_amount_above_maximum() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -406,6 +426,7 @@ fun test_deposit_when_paused() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -441,6 +462,7 @@ fun test_transfer_basic() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -454,6 +476,7 @@ fun test_transfer_basic() { { let mut safe = ts::take_shared(&scenario); let bridge_cap = ts::take_from_address(&scenario, ADMIN); + let mut treasury = ts::take_shared>(&scenario); // Verify initial balance assert!(safe::get_stored_coin_balance(&mut safe) == 100000, 0); @@ -464,6 +487,7 @@ fun test_transfer_basic() { &bridge_cap, RECIPIENT, DEPOSIT_AMOUNT, + &mut treasury, ts::ctx(&mut scenario), ); @@ -474,6 +498,7 @@ fun test_transfer_basic() { assert!(bag_balance == 100000 - DEPOSIT_AMOUNT, 10); assert!(bag_balance == safe::get_stored_coin_balance(&mut safe), 11); + ts::return_shared(treasury); ts::return_shared(safe); ts::return_to_address(ADMIN, bridge_cap); }; @@ -501,6 +526,7 @@ fun test_transfer_exact_balance() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -515,6 +541,7 @@ fun test_transfer_exact_balance() { { let mut safe = ts::take_shared(&scenario); let bridge_cap = ts::take_from_address(&scenario, ADMIN); + let mut treasury = ts::take_shared>(&scenario); // Transfer entire balance let success = safe::transfer( @@ -522,6 +549,7 @@ fun test_transfer_exact_balance() { &bridge_cap, RECIPIENT, DEPOSIT_AMOUNT, + &mut treasury, ts::ctx(&mut scenario), ); @@ -532,6 +560,7 @@ fun test_transfer_exact_balance() { assert!(bag_balance == 0, 10); assert!(bag_balance == safe::get_stored_coin_balance(&mut safe), 11); + ts::return_shared(treasury); ts::return_shared(safe); ts::return_to_address(ADMIN, bridge_cap); }; @@ -547,6 +576,7 @@ fun test_transfer_token_not_whitelisted() { { let mut safe = ts::take_shared(&scenario); let bridge_cap = ts::take_from_address(&scenario, ADMIN); + let mut treasury = ts::take_shared>(&scenario); // Try to transfer non-whitelisted token - should return false let success = safe::transfer( @@ -554,6 +584,7 @@ fun test_transfer_token_not_whitelisted() { &bridge_cap, RECIPIENT, DEPOSIT_AMOUNT, + &mut treasury, ts::ctx(&mut scenario), ); @@ -563,6 +594,7 @@ fun test_transfer_token_not_whitelisted() { assert!(bag_balance == 0, 10); assert!(bag_balance == safe::get_stored_coin_balance(&mut safe), 11); + ts::return_shared(treasury); ts::return_shared(safe); ts::return_to_address(ADMIN, bridge_cap); }; @@ -583,6 +615,7 @@ fun test_transfer_token_removed_from_whitelist() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -598,6 +631,7 @@ fun test_transfer_token_removed_from_whitelist() { { let mut safe = ts::take_shared(&scenario); let bridge_cap = ts::take_from_address(&scenario, ADMIN); + let mut treasury = ts::take_shared>(&scenario); // Try to transfer removed token - should be okay - we will use whitelisted check only for deposits let success = safe::transfer( @@ -605,6 +639,7 @@ fun test_transfer_token_removed_from_whitelist() { &bridge_cap, RECIPIENT, DEPOSIT_AMOUNT, + &mut treasury, ts::ctx(&mut scenario), ); @@ -614,6 +649,7 @@ fun test_transfer_token_removed_from_whitelist() { assert!(bag_balance == 100000 - DEPOSIT_AMOUNT, 10); assert!(bag_balance == safe::get_stored_coin_balance(&mut safe), 11); + ts::return_shared(treasury); ts::return_shared(safe); ts::return_to_address(ADMIN, bridge_cap); }; @@ -633,6 +669,7 @@ fun test_transfer_insufficient_balance() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -647,6 +684,7 @@ fun test_transfer_insufficient_balance() { { let mut safe = ts::take_shared(&scenario); let bridge_cap = ts::take_from_address(&scenario, ADMIN); + let mut treasury = ts::take_shared>(&scenario); // Try to transfer more than balance - should return false let success = safe::transfer( @@ -654,6 +692,7 @@ fun test_transfer_insufficient_balance() { &bridge_cap, RECIPIENT, DEPOSIT_AMOUNT, // Much larger than 1000 + &mut treasury, ts::ctx(&mut scenario), ); @@ -664,6 +703,7 @@ fun test_transfer_insufficient_balance() { assert!(bag_balance == 1000, 10); assert!(bag_balance == safe::get_stored_coin_balance(&mut safe), 11); + ts::return_shared(treasury); ts::return_shared(safe); ts::return_to_address(ADMIN, bridge_cap); }; @@ -684,6 +724,7 @@ fun test_transfer_no_coin_storage() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -694,6 +735,7 @@ fun test_transfer_no_coin_storage() { { let mut safe = ts::take_shared(&scenario); let bridge_cap = ts::take_from_address(&scenario, ADMIN); + let mut treasury = ts::take_shared>(&scenario); // Try to transfer when no coins stored - should return false let success = safe::transfer( @@ -701,6 +743,7 @@ fun test_transfer_no_coin_storage() { &bridge_cap, RECIPIENT, DEPOSIT_AMOUNT, + &mut treasury, ts::ctx(&mut scenario), ); @@ -710,6 +753,7 @@ fun test_transfer_no_coin_storage() { assert!(bag_balance == 0, 10); assert!(bag_balance == safe::get_stored_coin_balance(&mut safe), 11); + ts::return_shared(treasury); ts::return_shared(safe); ts::return_to_address(ADMIN, bridge_cap); }; @@ -729,6 +773,7 @@ fun test_transfer_multiple_partial() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -742,6 +787,7 @@ fun test_transfer_multiple_partial() { { let mut safe = ts::take_shared(&scenario); let bridge_cap = ts::take_from_address(&scenario, ADMIN); + let mut treasury = ts::take_shared>(&scenario); // Multiple transfers let success1 = safe::transfer( @@ -749,6 +795,7 @@ fun test_transfer_multiple_partial() { &bridge_cap, RECIPIENT, 10000, + &mut treasury, ts::ctx(&mut scenario), ); let success2 = safe::transfer( @@ -756,6 +803,7 @@ fun test_transfer_multiple_partial() { &bridge_cap, RECIPIENT, 20000, + &mut treasury, ts::ctx(&mut scenario), ); let success3 = safe::transfer( @@ -763,6 +811,7 @@ fun test_transfer_multiple_partial() { &bridge_cap, RECIPIENT, 30000, + &mut treasury, ts::ctx(&mut scenario), ); @@ -775,6 +824,7 @@ fun test_transfer_multiple_partial() { assert!(bag_balance == 40000, 10); assert!(bag_balance == safe::get_stored_coin_balance(&mut safe), 11); + ts::return_shared(treasury); ts::return_shared(safe); ts::return_to_address(ADMIN, bridge_cap); }; @@ -794,6 +844,7 @@ fun test_deposit_then_transfer_integration() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -818,12 +869,14 @@ fun test_deposit_then_transfer_integration() { { let mut safe = ts::take_shared(&scenario); let bridge_cap = ts::take_from_address(&scenario, ADMIN); + let mut treasury = ts::take_shared>(&scenario); let success = safe::transfer( &mut safe, &bridge_cap, RECIPIENT, DEPOSIT_AMOUNT, + &mut treasury, ts::ctx(&mut scenario), ); @@ -834,6 +887,147 @@ fun test_deposit_then_transfer_integration() { assert!(bag_balance == 0, 10); assert!(bag_balance == safe::get_stored_coin_balance(&mut safe), 11); + ts::return_shared(treasury); + ts::return_shared(safe); + ts::return_to_address(ADMIN, bridge_cap); + }; + + ts::end(scenario); +} + +#[test] +#[expected_failure(abort_code = safe::EIncompatibleTokenFlags)] +fun test_whitelist_rejects_mint_burn_and_locked_combination() { + let mut scenario = setup(); + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + + safe::whitelist_token_internal( + &mut safe, + MIN_AMOUNT, + MAX_AMOUNT, + false, + option::some(object::id_from_address(@0x1234)), + true, + true, + ts::ctx(&mut scenario), + ); + + ts::return_shared(safe); + }; + + ts::end(scenario); +} + +#[test] +#[expected_failure(abort_code = safe::EIncompatibleTokenFlags)] +fun test_set_token_is_locked_rejects_mint_burn_token() { + let mut scenario = setup_mint_burn(); + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + + safe::set_token_is_locked(&mut safe, true, ts::ctx(&mut scenario)); + + ts::return_shared(safe); + }; + + ts::end(scenario); +} + +#[test] +#[expected_failure(abort_code = safe::EIncompatibleTokenFlags)] +fun test_set_token_is_mint_burn_rejects_locked_token() { + let mut scenario = setup(); + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + + safe::whitelist_token( + &mut safe, + MIN_AMOUNT, + MAX_AMOUNT, + true, + ts::ctx(&mut scenario), + ); + safe::set_token_is_mint_burn(&mut safe, true, ts::ctx(&mut scenario)); + + ts::return_shared(safe); + }; + + ts::end(scenario); +} + +#[test] +fun test_transfer_locked_token_path_decreases_balance() { + let mut scenario = setup(); + + scenario.next_tx(ADMIN); + { + let mut safe = ts::take_shared(&scenario); + let mut treasury = ts::take_shared>(&scenario); + + safe::whitelist_token( + &mut safe, + MIN_AMOUNT, + MAX_AMOUNT, + true, + ts::ctx(&mut scenario), + ); + + lkt::mint_coin_to_receiver( + &mut treasury, + DEPOSIT_AMOUNT, + USER, + ts::ctx(&mut scenario), + ); + + ts::return_shared(treasury); + ts::return_shared(safe); + }; + + scenario.next_tx(USER); + { + let mut safe = ts::take_shared(&scenario); + let clock = clock::create_for_testing(ts::ctx(&mut scenario)); + let bridge_coin = ts::take_from_sender>(&scenario); + safe::deposit( + &mut safe, + bridge_coin, + RECIPIENT_VECTOR, + &clock, + ts::ctx(&mut scenario), + ); + + assert!(safe::get_stored_coin_balance(&mut safe) == DEPOSIT_AMOUNT, 0); + + clock::destroy_for_testing(clock); + ts::return_shared(safe); + }; + + scenario.next_tx(BRIDGE); + { + let mut safe = ts::take_shared(&scenario); + let bridge_cap = ts::take_from_address(&scenario, ADMIN); + let mut treasury = ts::take_shared>(&scenario); + + let success = safe::transfer( + &mut safe, + &bridge_cap, + RECIPIENT, + DEPOSIT_AMOUNT, + &mut treasury, + ts::ctx(&mut scenario), + ); + + assert!(success, 1); + assert!(safe::get_stored_coin_balance(&mut safe) == 0, 2); + + ts::return_shared(treasury); ts::return_shared(safe); ts::return_to_address(ADMIN, bridge_cap); }; @@ -859,6 +1053,7 @@ fun setup_mint_burn(): Scenario { false, option::some(object::id_from_address(@0x1234)), true, + false, s.ctx(), ); ts::return_shared(safe); @@ -871,6 +1066,7 @@ fun setup_mint_burn(): Scenario { fun setup_with_treasury(): Scenario { // deny_list::create_for_test requires sender == @0x0 (system address) let mut s = ts::begin(@0x0); + br::init_for_testing(s.ctx()); s.next_tx(@0x0); { sui::deny_list::create_for_testing(s.ctx()); @@ -897,10 +1093,18 @@ fun setup_with_treasury(): Scenario { ); transfer::public_share_object(t); transfer::public_share_object(metadata); + + let mut bridge_token_treasury = s.take_shared>(); + lkt::transfer_to_coin_cap(&mut bridge_token_treasury, ADMIN, s.ctx()); + lkt::transfer_from_coin_cap(&mut bridge_token_treasury, ADMIN, s.ctx()); + ts::return_shared(bridge_token_treasury); }; // BridgeSafe created by ADMIN so ADMIN is the owner for whitelist calls s.next_tx(ADMIN); - { safe::init_for_testing(s.ctx()); }; + { + let from_cap_db = s.take_from_address>(ADMIN); + safe::init_for_testing(from_cap_db, s.ctx()); + }; s } @@ -1151,7 +1355,13 @@ fun test_deposit_mint_burn_wrong_variant() { { let mut safe = ts::take_shared(&scenario); // Whitelist as NATIVE — calling deposit_mint_burn_for_testing should fail - safe::whitelist_token(&mut safe, MIN_AMOUNT, MAX_AMOUNT, ts::ctx(&mut scenario)); + safe::whitelist_token( + &mut safe, + MIN_AMOUNT, + MAX_AMOUNT, + false, + ts::ctx(&mut scenario), + ); ts::return_shared(safe); }; @@ -1193,6 +1403,7 @@ fun test_deposit_mint_burn_cap_not_registered() { false, option::some(object::id(&treasury)), true, + false, ts::ctx(&mut scenario), ); @@ -1281,7 +1492,13 @@ fun test_transfer_mint_burn_wrong_variant() { { let mut safe = ts::take_shared(&scenario); - safe::whitelist_token(&mut safe, MIN_AMOUNT, MAX_AMOUNT, ts::ctx(&mut scenario)); + safe::whitelist_token( + &mut safe, + MIN_AMOUNT, + MAX_AMOUNT, + false, + ts::ctx(&mut scenario), + ); let supply = coin::mint_for_testing(DEPOSIT_AMOUNT * 2, ts::ctx(&mut scenario)); safe::init_supply(&mut safe, supply, ts::ctx(&mut scenario)); ts::return_shared(safe); @@ -1318,7 +1535,7 @@ fun test_transfer_mint_burn_zero_balance() { safe::whitelist_token_internal( &mut safe, MIN_AMOUNT, MAX_AMOUNT, false, - option::some(object::id(&treasury)), true, ts::ctx(&mut scenario), + option::some(object::id(&treasury)), true, false, ts::ctx(&mut scenario), ); ts::return_shared(safe); ts::return_shared(treasury); @@ -1354,7 +1571,7 @@ fun test_transfer_mint_burn_insufficient_balance() { let treasury = ts::take_shared>(&scenario); safe::whitelist_token_internal( &mut safe, MIN_AMOUNT, MAX_AMOUNT, false, - option::some(object::id(&treasury)), true, ts::ctx(&mut scenario), + option::some(object::id(&treasury)), true, false, ts::ctx(&mut scenario), ); safe::add_to_balance_for_testing(&mut safe, DEPOSIT_AMOUNT - 1); @@ -1391,7 +1608,7 @@ fun test_transfer_mint_burn_cap_not_registered() { let treasury = ts::take_shared>(&scenario); safe::whitelist_token_internal( &mut safe, MIN_AMOUNT, MAX_AMOUNT, false, - option::some(object::id(&treasury)), true, ts::ctx(&mut scenario), + option::some(object::id(&treasury)), true, false, ts::ctx(&mut scenario), ); safe::add_to_balance_for_testing(&mut safe, DEPOSIT_AMOUNT); @@ -1417,4 +1634,3 @@ fun test_transfer_mint_burn_cap_not_registered() { ts::end(scenario); } - diff --git a/tests/events_tests.move b/tests/events_tests.move index ae66970..80109a3 100644 --- a/tests/events_tests.move +++ b/tests/events_tests.move @@ -120,7 +120,8 @@ fun test_emit_token_whitelisted() { 100, // min_limit 10000, // max_limit true, // is_native - false // is_mint_burn + false, // is_mint_burn + true // is_locked ); }; @@ -139,7 +140,8 @@ fun test_emit_token_whitelisted_all_false() { 50, // min_limit 5000, // max_limit false, // is_native - false // is_mint_burn + false, // is_mint_burn + false // is_locked ); }; @@ -158,7 +160,8 @@ fun test_emit_token_whitelisted_mint_burn() { 1, // min_limit 1000000, // max_limit false, // is_native - true // is_mint_burn + true, // is_mint_burn + false // is_locked ); }; @@ -221,6 +224,30 @@ fun test_emit_token_is_native_updated_false() { ts::end(scenario); } +#[test] +fun test_emit_token_is_locked_updated() { + let mut scenario = ts::begin(ADMIN); + + scenario.next_tx(ADMIN); + { + events::emit_token_is_locked_updated(b"LOCKED_TOKEN", true); + }; + + ts::end(scenario); +} + +#[test] +fun test_emit_token_is_locked_updated_false() { + let mut scenario = ts::begin(ADMIN); + + scenario.next_tx(ADMIN); + { + events::emit_token_is_locked_updated(b"UNLOCKED_TOKEN", false); + }; + + ts::end(scenario); +} + #[test] fun test_emit_token_is_mint_burn_updated() { let mut scenario = ts::begin(ADMIN); @@ -401,11 +428,11 @@ fun test_multiple_events_in_sequence() { events::emit_admin_role_transferred(ADMIN, NEW_ADMIN); events::emit_pause(true); events::emit_relayer_added(RELAYER, ADMIN); - events::emit_token_whitelisted(b"SEQ_TOKEN", 1, 1000, true, false); + events::emit_token_whitelisted(b"SEQ_TOKEN", 1, 1000, true, false, false); events::emit_batch_created(1, 100); events::emit_transfer_executed(USER, 500, b"SEQ_TOKEN", true); events::emit_pause(false); }; ts::end(scenario); -} \ No newline at end of file +} diff --git a/tests/safe_edge_case_tests.move b/tests/safe_edge_case_tests.move index 0fb6e7a..0055688 100644 --- a/tests/safe_edge_case_tests.move +++ b/tests/safe_edge_case_tests.move @@ -3,6 +3,8 @@ module bridge_safe::safe_edge_case_tests; use bridge_safe::pausable::{Self, EContractNotPaused}; use bridge_safe::safe::{Self, BridgeSafe}; +use locked_token::bridge_token::{Self as br, BRIDGE_TOKEN}; +use locked_token::treasury::{Self as lkt, Treasury, FromCoinCap}; use sui::clock; use sui::coin; use sui::test_scenario::{Self as ts, Scenario}; @@ -21,9 +23,20 @@ const MAX_AMOUNT: u64 = 1000000; fun setup(): Scenario { let mut s = ts::begin(ADMIN); + br::init_for_testing(s.ctx()); + + s.next_tx(ADMIN); + { + let mut treasury = s.take_shared>(); + lkt::transfer_to_coin_cap(&mut treasury, ADMIN, s.ctx()); + lkt::transfer_from_coin_cap(&mut treasury, ADMIN, s.ctx()); + ts::return_shared(treasury); + }; + s.next_tx(ADMIN); { - safe::init_for_testing(s.ctx()); + let from_cap_db = s.take_from_address>(ADMIN); + safe::init_for_testing(from_cap_db, s.ctx()); }; s @@ -82,6 +95,7 @@ fun test_multiple_token_whitelist() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -90,6 +104,7 @@ fun test_multiple_token_whitelist() { &mut safe, MIN_AMOUNT * 2, MAX_AMOUNT * 2, + false, ts::ctx(&mut scenario), ); @@ -120,6 +135,7 @@ fun test_token_limit_updates() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -173,6 +189,7 @@ fun test_init_supply_zero_amount() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -204,6 +221,7 @@ fun test_init_supply_non_native_token() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -233,6 +251,7 @@ fun test_init_supply_removed_token() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); diff --git a/tests/safe_internal_tests.move b/tests/safe_internal_tests.move index 3fffa4f..1604a56 100644 --- a/tests/safe_internal_tests.move +++ b/tests/safe_internal_tests.move @@ -4,6 +4,8 @@ module bridge_safe::safe_unit_tests; use bridge_safe::pausable; use bridge_safe::bridge_roles::{BridgeCap}; use bridge_safe::safe::{Self, BridgeSafe}; +use locked_token::bridge_token::{Self as br, BRIDGE_TOKEN}; +use locked_token::treasury::{Self as lkt, Treasury, FromCoinCap}; use sui::clock; use sui::coin; use sui::test_scenario::{Self as ts, Scenario}; @@ -25,9 +27,20 @@ const MAX_AMOUNT: u64 = 1000000; fun setup(): Scenario { let mut s = ts::begin(ADMIN); + br::init_for_testing(s.ctx()); + + s.next_tx(ADMIN); + { + let mut treasury = s.take_shared>(); + lkt::transfer_to_coin_cap(&mut treasury, ADMIN, s.ctx()); + lkt::transfer_from_coin_cap(&mut treasury, ADMIN, s.ctx()); + ts::return_shared(treasury); + }; + s.next_tx(ADMIN); { - safe::init_for_testing(s.ctx()); + let from_cap_db = s.take_from_address>(ADMIN); + safe::init_for_testing(from_cap_db, s.ctx()); }; s @@ -72,6 +85,7 @@ fun test_whitelist_token() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -98,6 +112,7 @@ fun test_whitelist_token_already_exists() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -106,6 +121,7 @@ fun test_whitelist_token_already_exists() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -129,6 +145,7 @@ fun test_whitelist_token_not_admin() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -149,6 +166,7 @@ fun test_remove_token_from_whitelist() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -340,6 +358,7 @@ fun test_set_token_min_limit() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -370,6 +389,7 @@ fun test_set_token_max_limit() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -420,6 +440,7 @@ fun test_init_supply() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -452,6 +473,7 @@ fun test_init_supply_multiple_times() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -1027,6 +1049,7 @@ fun test_sync_supply() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -1068,6 +1091,7 @@ fun test_sync_supply_exact_amount() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -1098,6 +1122,7 @@ fun test_sync_supply_no_existing_bag_entry() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -1129,6 +1154,7 @@ fun test_sync_supply_not_owner() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -1173,6 +1199,7 @@ fun test_sync_supply_no_deficit() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -1199,6 +1226,7 @@ fun test_sync_supply_insufficient_coin() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); @@ -1227,6 +1255,7 @@ fun test_sync_supply_not_native() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut scenario), ); diff --git a/tests/security_tests.move b/tests/security_tests.move index 0f161ee..c34190b 100644 --- a/tests/security_tests.move +++ b/tests/security_tests.move @@ -4,6 +4,8 @@ module bridge_safe::security_tests; use bridge_safe::bridge::{Self, Bridge}; use bridge_safe::bridge_roles::BridgeCap; use bridge_safe::safe::{Self, BridgeSafe}; +use locked_token::bridge_token::{Self as br, BRIDGE_TOKEN}; +use locked_token::treasury::{Self as lkt, Treasury, FromCoinCap}; use sui::clock; use sui::coin; use sui::test_scenario::{Self as ts, Scenario}; @@ -27,9 +29,20 @@ const PK3: vector = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456"; fun setup(): Scenario { let mut s = ts::begin(ADMIN); + br::init_for_testing(s.ctx()); + + s.next_tx(ADMIN); + { + let mut treasury = s.take_shared>(); + lkt::transfer_to_coin_cap(&mut treasury, ADMIN, s.ctx()); + lkt::transfer_from_coin_cap(&mut treasury, ADMIN, s.ctx()); + ts::return_shared(treasury); + }; + s.next_tx(ADMIN); { - safe::init_for_testing(s.ctx()); + let from_cap_db = s.take_from_address>(ADMIN); + safe::init_for_testing(from_cap_db, s.ctx()); }; s.next_tx(ADMIN); @@ -41,6 +54,7 @@ fun setup(): Scenario { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, ts::ctx(&mut s), ); @@ -129,6 +143,7 @@ fun test_replay_allows_double_spend_with_same_deposit_nonce() { { let mut bridge = ts::take_shared(&scenario); let mut safe = ts::take_shared(&scenario); + let mut treasury = ts::take_shared>(&scenario); let clock = clock::create_for_testing(ts::ctx(&mut scenario)); // First call should succeed @@ -139,6 +154,7 @@ fun test_replay_allows_double_spend_with_same_deposit_nonce() { vector[DRAIN_AMOUNT], 1, // batch nonce false, + &mut treasury, &clock, ts::ctx(&mut scenario), ); @@ -151,6 +167,7 @@ fun test_replay_allows_double_spend_with_same_deposit_nonce() { vector[DRAIN_AMOUNT], 1, // same batch nonce false, + &mut treasury, &clock, ts::ctx(&mut scenario), ); @@ -163,6 +180,7 @@ fun test_replay_allows_double_spend_with_same_deposit_nonce() { ts::return_shared(bridge); ts::return_shared(safe); + ts::return_shared(treasury); clock::destroy_for_testing(clock); }; diff --git a/tests/shared_structs_tests.move b/tests/shared_structs_tests.move index da65ddc..df8152a 100644 --- a/tests/shared_structs_tests.move +++ b/tests/shared_structs_tests.move @@ -10,6 +10,7 @@ fun test_token_config_is_mint_burn() { let mut config = shared_structs::create_token_config( true, false, + false, 100, 1000, option::none(), @@ -44,6 +45,7 @@ fun test_subtract_from_token_config_total_balance() { let mut config = shared_structs::create_token_config( true, false, + false, 100, 1000, option::none(), @@ -66,6 +68,7 @@ fun test_subtract_from_token_config_total_balance_underflow() { let mut config = shared_structs::create_token_config( true, false, + false, 100, 1000, option::none(), @@ -81,6 +84,7 @@ fun test_subtract_from_token_config_total_balance_insufficient_funds() { let mut config = shared_structs::create_token_config( true, false, + false, 100, 1000, option::none(), @@ -97,6 +101,7 @@ fun test_add_to_token_config_total_balance() { let mut config = shared_structs::create_token_config( true, false, + false, 100, 1000, option::none(), @@ -118,6 +123,7 @@ fun test_add_to_token_config_total_balance_overflow() { let mut config = shared_structs::create_token_config( true, false, + false, 100, 1000, option::none(), @@ -135,6 +141,7 @@ fun test_add_to_token_config_total_balance_near_max_overflow() { let mut config = shared_structs::create_token_config( true, false, + false, 100, 1000, option::none(), @@ -151,6 +158,7 @@ fun test_set_token_config_is_native() { let mut config = shared_structs::create_token_config( true, false, + false, 100, 1000, option::none(), @@ -232,6 +240,7 @@ fun test_combined_operations() { let mut config = shared_structs::create_token_config( true, false, + false, 100, 1000, option::none(), diff --git a/tests/upgrade_tests.move b/tests/upgrade_tests.move index 9b8cd32..7639c95 100644 --- a/tests/upgrade_tests.move +++ b/tests/upgrade_tests.move @@ -6,6 +6,8 @@ use bridge_safe::bridge_roles::BridgeCap; use bridge_safe::safe::{Self, BridgeSafe}; use bridge_safe::upgrade_manager; use bridge_safe::bridge_version_control; +use locked_token::bridge_token::{Self as br, BRIDGE_TOKEN}; +use locked_token::treasury::{Self as lkt, Treasury, FromCoinCap}; use sui::test_scenario::{Self as ts, Scenario}; public struct TEST_COIN has drop {} @@ -23,9 +25,20 @@ const PK3: vector = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456"; fun setup(): Scenario { let mut s = ts::begin(ADMIN); + br::init_for_testing(s.ctx()); + + s.next_tx(ADMIN); + { + let mut treasury = s.take_shared>(); + lkt::transfer_to_coin_cap(&mut treasury, ADMIN, s.ctx()); + lkt::transfer_from_coin_cap(&mut treasury, ADMIN, s.ctx()); + ts::return_shared(treasury); + }; + s.next_tx(ADMIN); { - safe::init_for_testing(s.ctx()); + let from_cap_db = s.take_from_address>(ADMIN); + safe::init_for_testing(from_cap_db, s.ctx()); }; s.next_tx(ADMIN); @@ -37,6 +50,7 @@ fun setup(): Scenario { &mut safe, MIN_AMOUNT, MAX_AMOUNT, + false, s.ctx(), ); From c21752648ac9afb8901564a6805d5510a174e527 Mon Sep 17 00:00:00 2001 From: Cristi Corcoveanu Date: Wed, 25 Mar 2026 15:18:35 +0200 Subject: [PATCH 27/30] dependency fix --- .gitignore | 4 +++- Move.toml | 9 ++++----- sources/shared_structs.move | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) 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.toml b/Move.toml index 1e8619d..c417742 100644 --- a/Move.toml +++ b/Move.toml @@ -1,13 +1,12 @@ [package] -name = "mx-bridge-sc-sui" +name = "mx_bridge_sc_sui" edition = "2024.beta" version = "0.0.1" [dependencies] -treasury = { git = "https://github.com/multiversx/stablecoin-sui.git", rev="xmn-launch", subdir = "packages/treasury" } +treasury = { git = "https://github.com/multiversx/stablecoin-sui.git", rev="xmn-launch", subdir = "packages/treasury", published-at = "0x28182c116f8f9a015b348f61e6330bdccdae7c192b9ba7293d4e296e912ed070" } +sui_extensions = { git = "https://github.com/multiversx/stablecoin-sui.git", rev="xmn-launch", subdir = "packages/sui_extensions" } [addresses] -shared_structs = "0x0" -safe_module = "0x0" bridge_safe = "0x0" -utils = "0x0" +shared_structs = "0x0" diff --git a/sources/shared_structs.move b/sources/shared_structs.move index 6521aac..a793dc6 100644 --- a/sources/shared_structs.move +++ b/sources/shared_structs.move @@ -204,7 +204,7 @@ public fun upsert_token_config( let cfg = table::borrow_mut(config, key); set_token_config(cfg, whitelisted, is_native, min_limit, max_limit, treasury_id, is_mint_burn); - return; + return }; let cfg = create_token_config(whitelisted, is_native, min_limit, max_limit, treasury_id, is_mint_burn); From 5f69b002f627093ee01602ef1fe80edcdf886e7b Mon Sep 17 00:00:00 2001 From: Eveline Molnar Date: Wed, 25 Mar 2026 16:18:05 +0200 Subject: [PATCH 28/30] fix move toml dependencies --- Move.toml | 11 ++++------- sources/bridge_module.move | 8 ++++---- sources/safe.move | 2 +- sources/shared_structs.move | 2 +- tests/shared_structs_tests.move | 4 ++-- 5 files changed, 12 insertions(+), 15 deletions(-) diff --git a/Move.toml b/Move.toml index c417742..4c6f95d 100644 --- a/Move.toml +++ b/Move.toml @@ -1,12 +1,9 @@ [package] -name = "mx_bridge_sc_sui" +name = "bridge_safe" edition = "2024.beta" version = "0.0.1" [dependencies] -treasury = { git = "https://github.com/multiversx/stablecoin-sui.git", rev="xmn-launch", subdir = "packages/treasury", published-at = "0x28182c116f8f9a015b348f61e6330bdccdae7c192b9ba7293d4e296e912ed070" } -sui_extensions = { git = "https://github.com/multiversx/stablecoin-sui.git", rev="xmn-launch", subdir = "packages/sui_extensions" } - -[addresses] -bridge_safe = "0x0" -shared_structs = "0x0" +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 40261d8..a042903 100644 --- a/sources/bridge_module.move +++ b/sources/bridge_module.move @@ -13,8 +13,8 @@ use bridge_safe::safe::{Self, 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; @@ -269,7 +269,7 @@ 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, ) { @@ -482,7 +482,7 @@ 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, ) { diff --git a/sources/safe.move b/sources/safe.move index ddbb06d..4d901d5 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -13,7 +13,7 @@ use bridge_safe::upgrade_service_bridge; use bridge_safe::utils; use locked_token::bridge_token::BRIDGE_TOKEN; use locked_token::treasury::{Self as lkt}; -use shared_structs::shared_structs::{Self, TokenConfig, Batch, Deposit}; +use bridge_safe::shared_structs::{Self, TokenConfig, Batch, Deposit}; use std::u64::{min, max}; use sui::bag::{Self, Bag}; use sui::clock::{Self, Clock}; diff --git a/sources/shared_structs.move b/sources/shared_structs.move index 82ced87..56c62ad 100644 --- a/sources/shared_structs.move +++ b/sources/shared_structs.move @@ -1,4 +1,4 @@ -module shared_structs::shared_structs; +module bridge_safe::shared_structs; use sui::table::{Self, Table}; diff --git a/tests/shared_structs_tests.move b/tests/shared_structs_tests.move index df8152a..efdc323 100644 --- a/tests/shared_structs_tests.move +++ b/tests/shared_structs_tests.move @@ -1,7 +1,7 @@ #[test_only] -module shared_structs::shared_structs_tests; +module bridge_safe::shared_structs_tests; -use shared_structs::shared_structs; +use bridge_safe::shared_structs; const MAX_U64: u64 = 18446744073709551615; From e472e64bc5f3c946351a9819fc3ee8e294c0e320 Mon Sep 17 00:00:00 2001 From: Cristi Corcoveanu Date: Wed, 1 Apr 2026 14:09:39 +0300 Subject: [PATCH 29/30] removed unused import --- sources/shared_structs.move | 1 - 1 file changed, 1 deletion(-) diff --git a/sources/shared_structs.move b/sources/shared_structs.move index a705ff8..5b807c7 100644 --- a/sources/shared_structs.move +++ b/sources/shared_structs.move @@ -1,7 +1,6 @@ module bridge_safe::shared_structs; use sui::table::Table; -use sui::kiosk::is_locked; const EUnderflow: u64 = 0; const EOverflow: u64 = 1; From 037c6a15a03675db28c6c604617f08efb0fab6b8 Mon Sep 17 00:00:00 2001 From: Cristi Corcoveanu Date: Wed, 1 Apr 2026 15:34:25 +0300 Subject: [PATCH 30/30] fixed upsert is_locked argument pass-over --- sources/shared_structs.move | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sources/shared_structs.move b/sources/shared_structs.move index 5b807c7..ef0d7c4 100644 --- a/sources/shared_structs.move +++ b/sources/shared_structs.move @@ -235,7 +235,7 @@ public(package) fun upsert_token_config( min_limit, max_limit, treasury_id, - is_mint_burn, + is_locked, ); config.add(key, cfg); }