diff --git a/.gitignore b/.gitignore index 78811d1..da882a5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ build/* .vscode .cursor .prettierrc -node_modules/ \ No newline at end of file +.replay/ +node_modules/ +vendor/ \ No newline at end of file diff --git a/Move.lock b/Move.lock deleted file mode 100644 index fba1734..0000000 --- a/Move.lock +++ /dev/null @@ -1,77 +0,0 @@ -# @generated by Move, please check-in and do not edit manually. - -[move] -version = 3 -manifest_digest = "5D85B40033B1406A10E38D3C3FCCF032577FBCF8B3DA4F402451BDE210D9B95F" -deps_digest = "397E6A9F7A624706DBDFEE056CE88391A15876868FD18A88504DA74EB458D697" -dependencies = [ - { id = "Bridge", name = "Bridge" }, - { id = "MoveStdlib", name = "MoveStdlib" }, - { id = "Sui", name = "Sui" }, - { id = "SuiSystem", name = "SuiSystem" }, - { id = "locked_token", name = "locked_token" }, -] - -[[move.package]] -id = "Bridge" -source = { git = "https://github.com/MystenLabs/sui.git", rev = "664b05b3b047c5bb03979d093660176176ea6175", subdir = "crates/sui-framework/packages/bridge" } - -dependencies = [ - { id = "MoveStdlib", name = "MoveStdlib" }, - { id = "Sui", name = "Sui" }, - { id = "SuiSystem", name = "SuiSystem" }, -] - -[[move.package]] -id = "MoveStdlib" -source = { git = "https://github.com/MystenLabs/sui.git", rev = "664b05b3b047c5bb03979d093660176176ea6175", subdir = "crates/sui-framework/packages/move-stdlib" } - -[[move.package]] -id = "Sui" -source = { git = "https://github.com/MystenLabs/sui.git", rev = "664b05b3b047c5bb03979d093660176176ea6175", subdir = "crates/sui-framework/packages/sui-framework" } - -dependencies = [ - { id = "MoveStdlib", name = "MoveStdlib" }, -] - -[[move.package]] -id = "SuiSystem" -source = { git = "https://github.com/MystenLabs/sui.git", rev = "664b05b3b047c5bb03979d093660176176ea6175", subdir = "crates/sui-framework/packages/sui-system" } - -dependencies = [ - { id = "MoveStdlib", name = "MoveStdlib" }, - { id = "Sui", name = "Sui" }, -] - -[[move.package]] -id = "locked_token" -source = { git = "https://github.com/multiversx/mx-locked-token-sc-sui.git", rev = "master", subdir = "" } - -dependencies = [ - { id = "Bridge", name = "Bridge" }, - { id = "MoveStdlib", name = "MoveStdlib" }, - { id = "Sui", name = "Sui" }, - { id = "SuiSystem", name = "SuiSystem" }, - { id = "sui_extensions", name = "sui_extensions" }, -] - -[[move.package]] -id = "sui_extensions" -source = { git = "https://github.com/circlefin/stablecoin-sui.git", rev = "master", subdir = "packages/sui_extensions" } - -dependencies = [ - { id = "Sui", name = "Sui" }, -] - -[move.toolchain-version] -compiler-version = "1.61.2" -edition = "2024.beta" -flavor = "sui" - -[env] - -[env.testnet] -chain-id = "4c78adac" -original-published-id = "0x797933e12440a65f84f46d1b650408e21efeb64f73bcb4e274d576035462833a" -latest-published-id = "0x797933e12440a65f84f46d1b650408e21efeb64f73bcb4e274d576035462833a" -published-version = "1" diff --git a/Move.toml b/Move.toml index 08a3fc0..4c6f95d 100644 --- a/Move.toml +++ b/Move.toml @@ -1,22 +1,9 @@ [package] -name = "mx-bridge-sc-sui" +name = "bridge_safe" edition = "2024.beta" +version = "0.0.1" [dependencies] -locked_token = { git = "https://github.com/multiversx/mx-locked-token-sc-sui.git", rev = "master", subdir = "" } - -[addresses] -shared_structs = "0x0" -safe_module = "0x0" -bridge_safe = "0x0" -utils = "0x0" - -[dev-dependencies] -# The dev-dependencies section allows overriding dependencies for `--test` and -# `--dev` modes. You can introduce test-only dependencies here. -# Local = { local = "../path/to/dev-build" } - -[dev-addresses] -# The dev-addresses section allows overwriting named addresses for the `--test` -# and `--dev` modes. -# alice = "0xB0B" +treasury = { git = "https://github.com/multiversx/stablecoin-sui.git", rev="xmn-launch", subdir = "packages/treasury", published-at = "0x28182c116f8f9a015b348f61e6330bdccdae7c192b9ba7293d4e296e912ed070"} +locked_token = { git = "https://github.com/multiversx/mx-locked-token-sc-sui.git", rev = "master", subdir = "", published-at = "0xcbcfc2873899ee4c54e16aa8e63403852001fd4587558a9371e358603189c528"} +sui_extensions = { git = "https://github.com/multiversx/stablecoin-sui.git", rev="xmn-launch", subdir = "packages/sui_extensions", override = true } diff --git a/sources/bridge_module.move b/sources/bridge_module.move index 30dfc2f..c2cdf84 100644 --- a/sources/bridge_module.move +++ b/sources/bridge_module.move @@ -1,5 +1,5 @@ /// Bridge Module - Cross-chain Bridge Implementation -/// +/// /// This module implements a secure cross-chain bridge that allows transferring /// tokens between different blockchain networks. It includes relayer management, /// batch processing, signature validation, and migration support. @@ -7,23 +7,24 @@ module bridge_safe::bridge; use bridge_safe::bridge_roles::BridgeCap; +use bridge_safe::bridge_version_control; use bridge_safe::events; use bridge_safe::pausable::{Self, Pause}; -use bridge_safe::safe::{Self, BridgeSafe}; +use bridge_safe::safe::BridgeSafe; use bridge_safe::utils; -use bridge_safe::bridge_version_control; use locked_token::bridge_token::BRIDGE_TOKEN; -use locked_token::treasury; -use shared_structs::shared_structs::{Self, Deposit, Batch, CrossTransferStatus, DepositStatus}; +use locked_token::treasury::{Self as lkt}; +use bridge_safe::shared_structs::{Self, Deposit, Batch, CrossTransferStatus, DepositStatus}; use std::u64::{min, max}; use sui::address; use sui::bcs; -use sui::clock::{Self, Clock}; +use sui::clock::Clock; use sui::ed25519; use sui::event; use sui::hash::blake2b256; use sui::table::{Self, Table}; use sui::vec_set::{Self, VecSet}; +use treasury::treasury; // === Error Constants === const EQuorumTooLow: u64 = 0; @@ -103,17 +104,18 @@ public fun initialize( ctx: &mut TxContext, ) { assert!(initial_quorum >= MINIMUM_QUORUM, EQuorumTooLow); + assert!(initial_quorum <= public_keys.length(), EQuorumExceedsRelayers); let mut relayers = vec_set::empty
(); let mut relayer_public_keys = table::new>(ctx); let mut i = 0; - while (i < vector::length(&public_keys)) { - let pk = *vector::borrow(&public_keys, i); - assert!(vector::length(&pk) == ED25519_PUBLIC_KEY_LENGTH, EInvalidPublicKeyLength); + while (i < public_keys.length()) { + let pk = *public_keys.borrow(i); + assert!(pk.length() == ED25519_PUBLIC_KEY_LENGTH, EInvalidPublicKeyLength); let relayer_address = getAddressFromPublicKey(&pk); - vec_set::insert(&mut relayers, relayer_address); - table::add(&mut relayer_public_keys, relayer_address, pk); + relayers.insert(relayer_address); + relayer_public_keys.add(relayer_address, pk); i = i + 1; }; @@ -127,7 +129,7 @@ public fun initialize( executed_batches: vec_set::empty(), execution_timestamps: table::new(ctx), cross_transfer_statuses: table::new(ctx), - transfer_statuses: vector::empty(), + transfer_statuses: vector[], safe: safe_address, bridge_cap, executed_transfer_by_batch_type_arg: vec_set::empty>(), @@ -144,14 +146,14 @@ public fun initialize( /// address = blake2b256( 0x00 || ed25519_pubkey ) fun getAddressFromPublicKey(public_key: &vector): address { let mut long_public_key = vector[0u8]; - vector::append(&mut long_public_key, *public_key); + long_public_key.append(*public_key); let relayer_bytes = sui::hash::blake2b256(&long_public_key); address::from_bytes(relayer_bytes) } /// Asserts that the caller is a registered relayer fun assert_relayer(bridge: &Bridge, signer: address) { - assert!(vec_set::contains(&bridge.relayers, &signer), ENotRelayer); + assert!(bridge.relayers.contains(&signer), ENotRelayer); } // === Configuration Management === @@ -163,10 +165,11 @@ public fun set_quorum( new_quorum: u64, ctx: &mut TxContext, ) { - safe::checkOwnerRole(safe, ctx); + assert_bridge_is_compatible(bridge); + safe.checkOwnerRole(ctx); assert!(new_quorum >= MINIMUM_QUORUM, EQuorumTooLow); - assert!(new_quorum <= vec_set::length(&bridge.relayers), EQuorumExceedsRelayers); + assert!(new_quorum <= bridge.relayers.length(), EQuorumExceedsRelayers); bridge.quorum = new_quorum; event::emit(QuorumChanged { new_quorum }); @@ -179,13 +182,15 @@ public fun set_batch_settle_timeout_ms( clock: &Clock, ctx: &mut TxContext, ) { - safe::checkOwnerRole(safe, ctx); + assert_bridge_is_compatible(bridge); + safe.checkOwnerRole(ctx); - pausable::assert_paused(&bridge.pause); - assert!(new_timeout_ms >= safe::get_batch_timeout_ms(safe), ESettleTimeoutBelowSafeBatch); - assert!(!safe::is_any_batch_in_progress(safe, clock), EPendingBatches); + bridge.pause.assert_paused(); + assert!(new_timeout_ms >= safe.get_batch_timeout_ms(), ESettleTimeoutBelowSafeBatch); + assert!(!safe.is_any_batch_in_progress(clock), EPendingBatches); bridge.batch_settle_timeout_ms = new_timeout_ms; + events::emit_batch_settle_timeout_updated(new_timeout_ms); } // === Relayer Management === @@ -197,15 +202,16 @@ public fun add_relayer( public_key: vector, ctx: &mut TxContext, ) { - safe::checkOwnerRole(safe, ctx); + assert_bridge_is_compatible(bridge); + safe.checkOwnerRole(ctx); - assert!(vector::length(&public_key) == ED25519_PUBLIC_KEY_LENGTH, EInvalidPublicKeyLength); + assert!(public_key.length() == ED25519_PUBLIC_KEY_LENGTH, EInvalidPublicKeyLength); let relayer_address = getAddressFromPublicKey(&public_key); - assert!(!vec_set::contains(&bridge.relayers, &relayer_address), ERelayerAlreadyExists); + assert!(!bridge.relayers.contains(&relayer_address), ERelayerAlreadyExists); - vec_set::insert(&mut bridge.relayers, relayer_address); - table::add(&mut bridge.relayer_public_keys, relayer_address, public_key); - events::emit_relayer_added(relayer_address, tx_context::sender(ctx)); + bridge.relayers.insert(relayer_address); + bridge.relayer_public_keys.add(relayer_address, public_key); + events::emit_relayer_added(relayer_address, ctx.sender()); } public fun remove_relayer( @@ -214,20 +220,21 @@ public fun remove_relayer( relayer: address, ctx: &mut TxContext, ) { - safe::checkOwnerRole(safe, ctx); + assert_bridge_is_compatible(bridge); + safe.checkOwnerRole(ctx); - let current_count = vec_set::length(&bridge.relayers); + let current_count = bridge.relayers.length(); assert!(current_count > bridge.quorum, ECannotRemoveRelayerBelowQuorum); - vec_set::remove(&mut bridge.relayers, &relayer); - if (table::contains(&bridge.relayer_public_keys, relayer)) { - table::remove(&mut bridge.relayer_public_keys, relayer); + bridge.relayers.remove(&relayer); + if (bridge.relayer_public_keys.contains(relayer)) { + bridge.relayer_public_keys.remove(relayer); }; - events::emit_relayer_removed(relayer, tx_context::sender(ctx)); + events::emit_relayer_removed(relayer, ctx.sender()); } public fun get_batch(safe: &BridgeSafe, batch_nonce: u64, clock: &Clock): (Batch, bool) { - safe::get_batch(safe, batch_nonce, clock) + safe.get_batch(batch_nonce, clock) } public fun get_batch_deposits( @@ -235,11 +242,11 @@ public fun get_batch_deposits( batch_nonce: u64, clock: &Clock, ): (vector, bool) { - safe::get_deposits(safe, batch_nonce, clock) + safe.get_deposits(batch_nonce, clock) } public fun was_batch_executed(bridge: &Bridge, batch_nonce_mvx: u64): bool { - vec_set::contains(&bridge.executed_batches, &batch_nonce_mvx) + bridge.executed_batches.contains(&batch_nonce_mvx) } public fun get_statuses_after_execution( @@ -247,16 +254,14 @@ public fun get_statuses_after_execution( batch_nonce_mvx: u64, clock: &Clock, ): (vector, bool) { - if (table::contains(&bridge.cross_transfer_statuses, batch_nonce_mvx)) { - let cross_status = table::borrow(&bridge.cross_transfer_statuses, batch_nonce_mvx); - let statuses = shared_structs::cross_transfer_status_statuses(cross_status); - let created_timestamp = shared_structs::cross_transfer_status_created_timestamp_ms( - cross_status, - ); + if (bridge.cross_transfer_statuses.contains(batch_nonce_mvx)) { + let cross_status = bridge.cross_transfer_statuses.borrow(batch_nonce_mvx); + let statuses = cross_status.cross_transfer_status_statuses(); + let created_timestamp = cross_status.cross_transfer_status_created_timestamp_ms(); let is_final = is_mvx_batch_final(bridge, created_timestamp, clock); (statuses, is_final) } else { - (vector::empty(), false) + (vector[], false) } } @@ -269,114 +274,50 @@ public fun execute_transfer( batch_nonce_mvx: u64, signatures: vector>, is_batch_complete: bool, - treasury: &mut treasury::Treasury, + treasury: &mut lkt::Treasury, clock: &Clock, ctx: &mut TxContext, ) { - let signer = tx_context::sender(ctx); - assert_relayer(bridge, signer); - pausable::assert_not_paused(&bridge.pause); - assert!(!was_batch_executed(bridge, batch_nonce_mvx), EBatchAlreadyExecuted); - - let len = vector::length(&recipients); - assert!(vector::length(&amounts) == len, EInvalidAmountsLength); - assert!(vector::length(&deposit_nonces) == len, EInvalidDepositNoncesLength); - - validate_quorum( + assert_bridge_is_compatible(bridge); + safe.assert_is_compatible(); + pre_execute_transfer( bridge, batch_nonce_mvx, &recipients, &amounts, - &signatures, &deposit_nonces, + &signatures, + clock, + ctx, ); - mark_deposits_executed_in_batch_or_abort(bridge, batch_nonce_mvx); - - let now = clock::timestamp_ms(clock); - if (table::contains(&bridge.execution_timestamps, batch_nonce_mvx)) { - let t = table::borrow_mut(&mut bridge.execution_timestamps, batch_nonce_mvx); - *t = now; - } else { - table::add(&mut bridge.execution_timestamps, batch_nonce_mvx, now); - }; - + let len = recipients.length(); let mut i = 0; - while (i < vector::length(&recipients)) { - let recipient = *vector::borrow(&recipients, i); - let amount = *vector::borrow(&amounts, i); - - let success = safe::transfer(safe, &bridge.bridge_cap, recipient, amount, treasury, ctx); - if (success) { - vector::push_back( - &mut bridge.transfer_statuses, - shared_structs::deposit_status_executed(), - ); - - // Increment successful deposits count - if (table::contains(&bridge.successful_transfers_by_batch, batch_nonce_mvx)) { - let current_count = table::borrow_mut( - &mut bridge.successful_transfers_by_batch, - batch_nonce_mvx, - ); - *current_count = *current_count + 1; - } else { - table::add(&mut bridge.successful_transfers_by_batch, batch_nonce_mvx, 1); - }; - } else { - vector::push_back( - &mut bridge.transfer_statuses, - shared_structs::deposit_status_rejected(), - ); - }; + while (i < len) { + let success = safe.transfer( + &bridge.bridge_cap, + *recipients.borrow(i), + *amounts.borrow(i), + treasury, + ctx, + ); + record_transfer_result(bridge, batch_nonce_mvx, success); i = i + 1; }; - if (is_batch_complete) { - vec_set::insert(&mut bridge.executed_batches, batch_nonce_mvx); - - let cross_status = shared_structs::create_cross_transfer_status( - bridge.transfer_statuses, - clock::timestamp_ms(clock), - ); - table::add(&mut bridge.cross_transfer_statuses, batch_nonce_mvx, cross_status); - - let total_transfers = vector::length(&recipients); - bridge.transfer_statuses = vector::empty(); - - let successful_count = if ( - table::contains(&bridge.successful_transfers_by_batch, batch_nonce_mvx) - ) { - *table::borrow(&bridge.successful_transfers_by_batch, batch_nonce_mvx) - } else { - 0 - }; - - if (table::contains(&bridge.successful_transfers_by_batch, batch_nonce_mvx)) { - table::remove(&mut bridge.successful_transfers_by_batch, batch_nonce_mvx); - }; - - event::emit(BatchExecuted { - batch_nonce_mvx, - transfers_count: total_transfers, - successful_transfers: successful_count, - }); - }; + finalize_batch(bridge, batch_nonce_mvx, len, is_batch_complete, clock); } -fun mark_deposits_executed_in_batch_or_abort( - bridge: &mut Bridge, - batch_nonce_mvx: u64, -) { +fun mark_deposits_executed_in_batch_or_abort(bridge: &mut Bridge, batch_nonce_mvx: u64) { let key = derive_key(batch_nonce_mvx); - assert!(!vec_set::contains(&bridge.executed_transfer_by_batch_type_arg, &key), EDepositAlreadyExecuted); - vec_set::insert(&mut bridge.executed_transfer_by_batch_type_arg, key); + assert!(!bridge.executed_transfer_by_batch_type_arg.contains(&key), EDepositAlreadyExecuted); + bridge.executed_transfer_by_batch_type_arg.insert(key); } fun derive_key(batch_nonce: u64): vector { let mut data = bcs::to_bytes(&batch_nonce); let type_bytes = utils::type_name_bytes(); - vector::append(&mut data, type_bytes); + data.append(type_bytes); blake2b256(&data) } @@ -385,7 +326,7 @@ fun is_mvx_batch_final(bridge: &Bridge, created_timestamp_ms: u64, clock: &Clock if (created_timestamp_ms == 0) { false } else { - (created_timestamp_ms + bridge.batch_settle_timeout_ms) <= clock::timestamp_ms(clock) + (created_timestamp_ms + bridge.batch_settle_timeout_ms) <= clock.timestamp_ms() } } @@ -398,11 +339,11 @@ public fun get_batch_settle_timeout_ms(bridge: &Bridge): u64 { } public fun is_relayer(bridge: &Bridge, addr: address): bool { - vec_set::contains(&bridge.relayers, &addr) + bridge.relayers.contains(&addr) } public fun get_admin(safe: &BridgeSafe): address { - safe::get_owner(safe) + safe.get_owner() } public fun get_pause(bridge: &Bridge): bool { @@ -410,21 +351,23 @@ public fun get_pause(bridge: &Bridge): bool { } public fun get_relayers(bridge: &Bridge): &vector
{ - vec_set::keys(&bridge.relayers) + bridge.relayers.keys() } public fun get_relayer_count(bridge: &Bridge): u64 { - vec_set::length(&bridge.relayers) + bridge.relayers.length() } public fun pause_contract(bridge: &mut Bridge, safe: &BridgeSafe, ctx: &mut TxContext) { - safe::checkOwnerRole(safe, ctx); - pausable::pause(&mut bridge.pause); + assert_bridge_is_compatible(bridge); + safe.checkOwnerRole(ctx); + bridge.pause.pause(); } public fun unpause_contract(bridge: &mut Bridge, safe: &BridgeSafe, ctx: &mut TxContext) { - safe::checkOwnerRole(safe, ctx); - pausable::unpause(&mut bridge.pause); + assert_bridge_is_compatible(bridge); + safe.checkOwnerRole(ctx); + bridge.pause.unpause(); } fun validate_quorum( @@ -436,7 +379,7 @@ fun validate_quorum( deposit_nonces: &vector, ) { let token_bytes = utils::type_name_bytes(); - let num_signatures = vector::length(signatures); + let num_signatures = signatures.length(); assert!(num_signatures >= bridge.quorum, EQuorumNotReached); let message = compute_message(batch_id, &token_bytes, recipients, amounts, deposit_nonces); @@ -445,9 +388,9 @@ fun validate_quorum( let mut i = 0; while (i < num_signatures) { - let signature = vector::borrow(signatures, i); + let signature = signatures.borrow(i); - assert!(vector::length(signature) == SIGNATURE_LENGTH, EInvalidSignatureLength); + assert!(signature.length() == SIGNATURE_LENGTH, EInvalidSignatureLength); let public_key = extract_public_key(signature); let sig_bytes = extract_signature(signature); @@ -457,15 +400,15 @@ fun validate_quorum( let relayer = option::extract(&mut relayer_opt); - assert!(!vec_set::contains(&verified_relayers, &relayer), EDuplicateSignature); + assert!(!verified_relayers.contains(&relayer), EDuplicateSignature); assert!(ed25519::ed25519_verify(&sig_bytes, &public_key, &message), EInvalidSignature); - vec_set::insert(&mut verified_relayers, relayer); + verified_relayers.insert(relayer); i = i + 1; }; - assert!(vec_set::length(&verified_relayers) >= bridge.quorum, EQuorumNotReached); + assert!(verified_relayers.length() >= bridge.quorum, EQuorumNotReached); } public fun compute_message( @@ -478,7 +421,7 @@ public fun compute_message( let message = construct_batch_message(batch_id, token, recipients, amounts, deposit_nonces); let encoded_msg = bcs::to_bytes(&message); let mut intent_message = vector[3u8, 0u8, 0u8]; - vector::append(&mut intent_message, encoded_msg); + intent_message.append(encoded_msg); sui::hash::blake2b256(&intent_message) } @@ -492,15 +435,15 @@ fun construct_batch_message( let mut message = bcs::to_bytes(&batch_id); let mut i = 0; - while (i < vector::length(recipients)) { - let recipient = vector::borrow(recipients, i); - let amount = vector::borrow(amounts, i); - let deposit_nonce = vector::borrow(deposit_nonces, i); - - vector::append(&mut message, bcs::to_bytes(token)); - vector::append(&mut message, bcs::to_bytes(recipient)); - vector::append(&mut message, bcs::to_bytes(amount)); - vector::append(&mut message, bcs::to_bytes(deposit_nonce)); + while (i < recipients.length()) { + let recipient = recipients.borrow(i); + let amount = amounts.borrow(i); + let deposit_nonce = deposit_nonces.borrow(i); + + message.append(bcs::to_bytes(token)); + message.append(bcs::to_bytes(recipient)); + message.append(bcs::to_bytes(amount)); + message.append(bcs::to_bytes(deposit_nonce)); i = i + 1; }; @@ -508,33 +451,33 @@ fun construct_batch_message( } fun extract_public_key(signature: &vector): vector { - let mut public_key = vector::empty(); - let mut i = vector::length(signature) - ED25519_PUBLIC_KEY_LENGTH; - while (i < vector::length(signature)) { - vector::push_back(&mut public_key, *vector::borrow(signature, i)); + let mut public_key = vector[]; + let mut i = signature.length() - ED25519_PUBLIC_KEY_LENGTH; + while (i < signature.length()) { + public_key.push_back(*signature.borrow(i)); i = i + 1; }; public_key } fun extract_signature(signature: &vector): vector { - let mut sig_bytes = vector::empty(); + let mut sig_bytes = vector[]; let mut i = 0; - while (i < vector::length(signature) - ED25519_PUBLIC_KEY_LENGTH) { - vector::push_back(&mut sig_bytes, *vector::borrow(signature, i)); + while (i < signature.length() - ED25519_PUBLIC_KEY_LENGTH) { + sig_bytes.push_back(*signature.borrow(i)); i = i + 1; }; sig_bytes } fun find_relayer_by_public_key(bridge: &Bridge, public_key: &vector): Option
{ - let relayers = vec_set::keys(&bridge.relayers); + let relayers = bridge.relayers.keys(); let mut i = 0; - while (i < vector::length(relayers)) { - let relayer = *vector::borrow(relayers, i); - if (table::contains(&bridge.relayer_public_keys, relayer)) { - let stored_pk = table::borrow(&bridge.relayer_public_keys, relayer); + while (i < relayers.length()) { + let relayer = *relayers.borrow(i); + if (bridge.relayer_public_keys.contains(relayer)) { + let stored_pk = bridge.relayer_public_keys.borrow(relayer); if (stored_pk == public_key) { return option::some(relayer) }; @@ -553,75 +496,106 @@ public fun execute_transfer_for_testing( amounts: vector, batch_nonce_mvx: u64, is_batch_complete: bool, - treasury: &mut treasury::Treasury, + treasury: &mut lkt::Treasury, clock: &Clock, ctx: &mut TxContext, ) { + pre_execute_transfer_for_testing(bridge, batch_nonce_mvx, clock); + + let len = recipients.length(); + let mut i = 0; + while (i < len) { + let success = safe.transfer( + &bridge.bridge_cap, + *recipients.borrow(i), + *amounts.borrow(i), + treasury, + ctx, + ); + record_transfer_result(bridge, batch_nonce_mvx, success); + i = i + 1; + }; + + finalize_batch(bridge, batch_nonce_mvx, len, is_batch_complete, clock); +} + +// === Package-internal helpers for adapters === + +/// Validates all preconditions and quorum for an execute_transfer call. +/// Adapters call this at the start of their own execute_transfer entry points. +public(package) fun pre_execute_transfer( + bridge: &mut Bridge, + batch_nonce_mvx: u64, + recipients: &vector
, + amounts: &vector, + deposit_nonces: &vector, + signatures: &vector>, + clock: &Clock, + ctx: &TxContext, +) { + assert_relayer(bridge, ctx.sender()); + bridge.pause.assert_not_paused(); + assert!(!was_batch_executed(bridge, batch_nonce_mvx), EBatchAlreadyExecuted); + + let len = recipients.length(); + assert!(amounts.length() == len, EInvalidAmountsLength); + assert!(deposit_nonces.length() == len, EInvalidDepositNoncesLength); + + validate_quorum(bridge, batch_nonce_mvx, recipients, amounts, signatures, deposit_nonces); mark_deposits_executed_in_batch_or_abort(bridge, batch_nonce_mvx); - let now = clock::timestamp_ms(clock); - if (table::contains(&bridge.execution_timestamps, batch_nonce_mvx)) { - let t = table::borrow_mut(&mut bridge.execution_timestamps, batch_nonce_mvx); - *t = now; + let now = clock.timestamp_ms(); + if (bridge.execution_timestamps.contains(batch_nonce_mvx)) { + *bridge.execution_timestamps.borrow_mut(batch_nonce_mvx) = now; } else { - table::add(&mut bridge.execution_timestamps, batch_nonce_mvx, now); + bridge.execution_timestamps.add(batch_nonce_mvx, now); }; +} - let mut i = 0; - while (i < vector::length(&recipients)) { - let recipient = *vector::borrow(&recipients, i); - let amount = *vector::borrow(&amounts, i); - - let success = safe::transfer(safe, &bridge.bridge_cap, recipient, amount, treasury, ctx); - if (success) { - vector::push_back( - &mut bridge.transfer_statuses, - shared_structs::deposit_status_executed(), - ); - - // Increment successful deposits count - if (table::contains(&bridge.successful_transfers_by_batch, batch_nonce_mvx)) { - let current_count = table::borrow_mut( - &mut bridge.successful_transfers_by_batch, - batch_nonce_mvx, - ); - *current_count = *current_count + 1; - } else { - table::add(&mut bridge.successful_transfers_by_batch, batch_nonce_mvx, 1); - }; +/// Records success or failure for one recipient in the current batch. +public(package) fun record_transfer_result( + bridge: &mut Bridge, + batch_nonce_mvx: u64, + success: bool, +) { + if (success) { + bridge.transfer_statuses.push_back(shared_structs::deposit_status_executed()); + if (bridge.successful_transfers_by_batch.contains(batch_nonce_mvx)) { + let count = bridge.successful_transfers_by_batch.borrow_mut(batch_nonce_mvx); + *count = *count + 1; } else { - vector::push_back( - &mut bridge.transfer_statuses, - shared_structs::deposit_status_rejected(), - ); - + bridge.successful_transfers_by_batch.add(batch_nonce_mvx, 1); }; - i = i + 1; + } else { + bridge.transfer_statuses.push_back(shared_structs::deposit_status_rejected()); }; +} +/// Finalizes a batch: records CrossTransferStatus and emits BatchExecuted if complete. +public(package) fun finalize_batch( + bridge: &mut Bridge, + batch_nonce_mvx: u64, + total_transfers: u64, + is_batch_complete: bool, + clock: &Clock, +) { if (is_batch_complete) { - vec_set::insert(&mut bridge.executed_batches, batch_nonce_mvx); + bridge.executed_batches.insert(batch_nonce_mvx); let cross_status = shared_structs::create_cross_transfer_status( bridge.transfer_statuses, - clock::timestamp_ms(clock), + clock.timestamp_ms(), ); - table::add(&mut bridge.cross_transfer_statuses, batch_nonce_mvx, cross_status); - - let total_transfers = vector::length(&recipients); - bridge.transfer_statuses = vector::empty(); + bridge.cross_transfer_statuses.add(batch_nonce_mvx, cross_status); + bridge.transfer_statuses = vector[]; - let successful_count = if ( - table::contains(&bridge.successful_transfers_by_batch, batch_nonce_mvx) - ) { - *table::borrow(&bridge.successful_transfers_by_batch, batch_nonce_mvx) + let successful_count = if (bridge.successful_transfers_by_batch.contains(batch_nonce_mvx)) { + let count = *bridge.successful_transfers_by_batch.borrow(batch_nonce_mvx); + bridge.successful_transfers_by_batch.remove(batch_nonce_mvx); + count } else { 0 }; - if (table::contains(&bridge.successful_transfers_by_batch, batch_nonce_mvx)) { - table::remove(&mut bridge.successful_transfers_by_batch, batch_nonce_mvx); - }; - event::emit(BatchExecuted { batch_nonce_mvx, transfers_count: total_transfers, @@ -630,6 +604,26 @@ public fun execute_transfer_for_testing( }; } +/// Exposes the bridge_cap for adapter transfer calls. +public(package) fun bridge_cap(bridge: &Bridge): &BridgeCap { + &bridge.bridge_cap +} + +#[test_only] +public(package) fun pre_execute_transfer_for_testing( + bridge: &mut Bridge, + batch_nonce_mvx: u64, + clock: &Clock, +) { + mark_deposits_executed_in_batch_or_abort(bridge, batch_nonce_mvx); + let now = clock.timestamp_ms(); + if (bridge.execution_timestamps.contains(batch_nonce_mvx)) { + *bridge.execution_timestamps.borrow_mut(batch_nonce_mvx) = now; + } else { + bridge.execution_timestamps.add(batch_nonce_mvx, now); + }; +} + // === Upgrade Management for Bridge === /// Returns the compatible versions for the bridge @@ -659,7 +653,7 @@ public fun bridge_pending_version(bridge: &Bridge): Option { /// Starts the migration process for the bridge public fun start_bridge_migration(bridge: &mut Bridge, safe: &BridgeSafe, ctx: &TxContext) { - safe::checkOwnerRole(safe, ctx); + safe.checkOwnerRole(ctx); assert!(bridge.compatible_versions.length() == 1, EMigrationStarted); let active_version = bridge.compatible_versions.keys()[0]; @@ -674,7 +668,7 @@ public fun start_bridge_migration(bridge: &mut Bridge, safe: &BridgeSafe, ctx: & /// Aborts the migration process for the bridge public fun abort_bridge_migration(bridge: &mut Bridge, safe: &BridgeSafe, ctx: &TxContext) { - safe::checkOwnerRole(safe, ctx); + safe.checkOwnerRole(ctx); assert!(bridge.compatible_versions.length() == 2, EMigrationNotStarted); let pending_version = max( @@ -692,7 +686,7 @@ public fun abort_bridge_migration(bridge: &mut Bridge, safe: &BridgeSafe, ctx: & /// Completes the migration process for the bridge public fun complete_bridge_migration(bridge: &mut Bridge, safe: &BridgeSafe, ctx: &TxContext) { - safe::checkOwnerRole(safe, ctx); + safe.checkOwnerRole(ctx); assert!(bridge.compatible_versions.length() == 2, EMigrationNotStarted); let (version_a, version_b) = ( diff --git a/sources/bridge_roles.move b/sources/bridge_roles.move index 74ceede..27d58e7 100644 --- a/sources/bridge_roles.move +++ b/sources/bridge_roles.move @@ -1,5 +1,5 @@ /// Bridge Roles Module - Access Control and Capabilities -/// +/// /// This module manages roles, permissions, and capabilities for the bridge system. module bridge_safe::bridge_roles; @@ -36,10 +36,7 @@ public(package) fun publish_caps(_w: BridgeWitness, ctx: &mut TxContext): (Bridg (BridgeCap { id: object::new(ctx) }) } -public(package) fun transfer_bridge_capability( - bridge_cap: BridgeCap, - new_bridge: address, -) { +public(package) fun transfer_bridge_capability(bridge_cap: BridgeCap, new_bridge: address) { assert!(new_bridge != @0x0, 0); transfer::public_transfer(bridge_cap, new_bridge); } @@ -64,10 +61,7 @@ public fun pending_owner(roles: &Roles): Option
{ roles.owner_role().pending_address() } -public(package) fun new( - owner: address, - ctx: &mut TxContext, -): Roles { +public(package) fun new(owner: address, ctx: &mut TxContext): Roles { let mut data = bag::new(ctx); data.add(OwnerKey {}, two_step_role::new(OwnerRole {}, owner)); Roles { diff --git a/sources/events.move b/sources/events.move index 18be9be..d1da96b 100644 --- a/sources/events.move +++ b/sources/events.move @@ -1,5 +1,5 @@ /// Events Module - Event Definitions for Bridge Operations -/// +/// /// This module defines all event structures used across the bridge system /// for monitoring deposits, admin actions, relayer management, and token operations. @@ -94,6 +94,18 @@ public struct BatchSettingsUpdated has copy, drop { batch_settle_limit: u8, } +public struct BatchTimeoutUpdated has copy, drop { + new_timeout_ms: u64, +} + +public struct BatchSettleTimeoutUpdated has copy, drop { + new_settle_timeout_ms: u64, +} + +public struct BatchSizeUpdated has copy, drop { + new_batch_size: u16, +} + public fun emit_deposit( _batch_id: u64, _deposit_nonce: u64, @@ -214,3 +226,15 @@ public(package) fun emit_batch_settings_updated( batch_settle_limit, }); } + +public(package) fun emit_batch_timeout_updated(new_timeout_ms: u64) { + event::emit(BatchTimeoutUpdated { new_timeout_ms }); +} + +public(package) fun emit_batch_settle_timeout_updated(new_settle_timeout_ms: u64) { + event::emit(BatchSettleTimeoutUpdated { new_settle_timeout_ms }); +} + +public(package) fun emit_batch_size_updated(new_batch_size: u16) { + event::emit(BatchSizeUpdated { new_batch_size }); +} diff --git a/sources/mint_burn_adapters/xmn_mint_cap_adapter.move b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move new file mode 100644 index 0000000..41849e6 --- /dev/null +++ b/sources/mint_burn_adapters/xmn_mint_cap_adapter.move @@ -0,0 +1,230 @@ +module bridge_safe::xmn_mint_cap_adapter; + +use bridge_safe::bridge::Bridge; +use bridge_safe::bridge_roles::BridgeCap; +use bridge_safe::events; +use bridge_safe::safe::BridgeSafe; +use bridge_safe::utils; +use sui::clock::Clock; +use sui::coin::Coin; +use sui::deny_list::DenyList; +use sui::dynamic_object_field as dof; +use treasury::treasury::{MintCap, Treasury as XmnTreasury}; + +public struct CapKey has copy, drop, store { + token_type: vector, +} + +const EMintBurnCapNotFound: u64 = 20; +const EMintBurnCapAlreadyRegistered: u64 = 21; + +// === Public API === + +public fun deposit( + safe: &mut BridgeSafe, + coin_in: Coin, + recipient: vector, + clock: &Clock, + xmn_treasury: &mut XmnTreasury, + deny_list: &DenyList, + ctx: &mut TxContext, +) { + safe.assert_is_compatible(); + assert!(has_cap(safe.uid()), EMintBurnCapNotFound); + + let (key, amount, batch_nonce, dep_nonce) = safe.deposit_validate_and_record( + &coin_in, + recipient, + true, + clock, + ctx, + ); + + burn(safe.uid(), xmn_treasury, deny_list, coin_in, ctx); + + events::emit_deposit_v1( + batch_nonce, + dep_nonce, + ctx.sender(), + recipient, + amount, + key, + ); +} + +public fun execute_transfer( + bridge: &mut Bridge, + safe: &mut BridgeSafe, + recipients: vector
, + amounts: vector, + deposit_nonces: vector, + batch_nonce_mvx: u64, + signatures: vector>, + is_batch_complete: bool, + xmn_treasury: &mut XmnTreasury, + deny_list: &DenyList, + clock: &Clock, + ctx: &mut TxContext, +) { + bridge.assert_bridge_is_compatible(); + safe.assert_is_compatible(); + bridge.pre_execute_transfer( + batch_nonce_mvx, + &recipients, + &amounts, + &deposit_nonces, + &signatures, + clock, + ctx, + ); + + let len = recipients.length(); + let mut i = 0; + while (i < len) { + let success = transfer( + safe, + bridge.bridge_cap(), + *recipients.borrow(i), + *amounts.borrow(i), + xmn_treasury, + deny_list, + ctx, + ); + bridge.record_transfer_result(batch_nonce_mvx, success); + i = i + 1; + }; + + bridge.finalize_batch(batch_nonce_mvx, len, is_batch_complete, clock); +} + +// === Admin Management === + +public fun whitelist_token( + safe: &mut BridgeSafe, + minimum_amount: u64, + maximum_amount: u64, + cap: MintCap, + treasury_id: ID, + ctx: &TxContext, +) { + safe.assert_is_compatible(); + assert!(!has_cap(safe.uid()), EMintBurnCapAlreadyRegistered); + safe.whitelist_token_internal( + minimum_amount, + maximum_amount, + false, + option::some(treasury_id), + true, + false, + ctx, + ); + register(safe.uid_mut(), cap); +} + +/// Remove a mint-burn token from the whitelist and deregister its MintCap in one atomic operation. +#[allow(lint(self_transfer))] +public fun remove_token_from_whitelist(safe: &mut BridgeSafe, ctx: &mut TxContext) { + safe.assert_is_compatible(); + safe.checkOwnerRole(ctx); + assert!(has_cap(safe.uid()), EMintBurnCapNotFound); + deregister(safe.uid_mut(), ctx.sender()); + let key = utils::type_name_bytes(); + safe.unwhitelist_token(key); +} + +// === Internal helpers === + +public(package) fun transfer( + safe: &mut BridgeSafe, + _bridge_cap: &BridgeCap, + receiver: address, + amount: u64, + xmn_treasury: &mut XmnTreasury, + deny_list: &DenyList, + ctx: &mut TxContext, +): bool { + if (!safe.has_token_config()) { return false }; + if (!safe.get_token_is_mint_burn()) { return false }; + if (safe.get_stored_coin_balance() < amount) { return false }; + if (!has_cap(safe.uid())) { return false }; + + mint(safe.uid(), xmn_treasury, deny_list, amount, receiver, ctx); + safe.subtract_token_balance(amount); + + true +} + +fun cap_key(): CapKey { + CapKey { token_type: utils::type_name_bytes() } +} + +public(package) fun register(id: &mut UID, cap: MintCap) { + dof::add(id, cap_key(), cap); +} + +#[allow(lint(self_transfer))] +public(package) fun deregister(id: &mut UID, recipient: address) { + let cap = dof::remove>(id, cap_key()); + transfer::public_transfer(cap, recipient); +} + +public(package) fun has_cap(id: &UID): bool { + dof::exists_with_type>(id, cap_key()) +} + +public(package) fun burn( + id: &UID, + xmn_treasury: &mut XmnTreasury, + deny_list: &DenyList, + coin_in: Coin, + ctx: &TxContext, +) { + let cap = dof::borrow(id, cap_key()); + xmn_treasury.burn(cap, deny_list, coin_in, ctx); +} + +public(package) fun mint( + id: &UID, + xmn_treasury: &mut XmnTreasury, + deny_list: &DenyList, + amount: u64, + receiver: address, + ctx: &mut TxContext, +) { + let cap = dof::borrow(id, cap_key()); + xmn_treasury.mint(cap, deny_list, amount, receiver, ctx); +} + +#[test_only] +public fun execute_transfer_for_testing( + bridge: &mut Bridge, + safe: &mut BridgeSafe, + recipients: vector
, + amounts: vector, + batch_nonce_mvx: u64, + is_batch_complete: bool, + xmn_treasury: &mut XmnTreasury, + deny_list: &DenyList, + clock: &Clock, + ctx: &mut TxContext, +) { + bridge.pre_execute_transfer_for_testing(batch_nonce_mvx, clock); + + let len = recipients.length(); + let mut i = 0; + while (i < len) { + let success = transfer( + safe, + bridge.bridge_cap(), + *recipients.borrow(i), + *amounts.borrow(i), + xmn_treasury, + deny_list, + ctx, + ); + bridge.record_transfer_result(batch_nonce_mvx, success); + i = i + 1; + }; + + bridge.finalize_batch(batch_nonce_mvx, len, is_batch_complete, clock); +} diff --git a/sources/pausable.move b/sources/pausable.move index 97fcf98..aa94b3f 100644 --- a/sources/pausable.move +++ b/sources/pausable.move @@ -1,5 +1,5 @@ /// Pausable Module - Emergency Stop Functionality -/// +/// /// This module provides pausable functionality for emergency stops. module bridge_safe::pausable; @@ -17,14 +17,14 @@ public fun new(): Pause { Pause { paused: false } } -public fun pause(p: &mut Pause) { +public(package) fun pause(p: &mut Pause) { if (!p.paused) { p.paused = true; events::emit_pause(true); } } -public fun unpause(p: &mut Pause) { +public(package) fun unpause(p: &mut Pause) { if (p.paused) { p.paused = false; events::emit_pause(false); diff --git a/sources/safe.move b/sources/safe.move index ddfb71b..1f0fb0f 100644 --- a/sources/safe.move +++ b/sources/safe.move @@ -1,4 +1,4 @@ -/// Safe Module - Token Management and Batch Processing +/// Safe Module - Token Management ani Batch Processing /// /// This module manages token deposits, batching, and secure transfers. /// It handles whitelisting, token limits, and coordinates with the bridge module. @@ -12,11 +12,11 @@ use bridge_safe::pausable::{Self, Pause}; use bridge_safe::upgrade_service_bridge; use bridge_safe::utils; use locked_token::bridge_token::BRIDGE_TOKEN; -use locked_token::treasury; -use shared_structs::shared_structs::{Self, TokenConfig, Batch, Deposit}; +use locked_token::treasury::{Self as lkt}; +use bridge_safe::shared_structs::{Self, TokenConfig, Batch, Deposit}; use std::u64::{min, max}; use sui::bag::{Self, Bag}; -use sui::clock::{Self, Clock}; +use sui::clock::Clock; use sui::coin::{Self, Coin}; use sui::event; use sui::table::{Self, Table}; @@ -56,6 +56,8 @@ const EInvalidTokenLimits: u64 = 15; const EMigrationStarted: u64 = 16; const EMigrationNotStarted: u64 = 17; const ENotPendingVersion: u64 = 18; +const ENotNativeToken: u64 = 19; +const EIncompatibleTokenFlags: u64 = 22; const MAX_U64: u64 = 18446744073709551615; const DEFAULT_BATCH_TIMEOUT_MS: u64 = 5 * 1000; // 5 seconds @@ -75,17 +77,28 @@ public struct BridgeSafe has key { batches: Table, batch_deposits: Table>, coin_storage: Bag, - from_coin_cap: treasury::FromCoinCap, + from_coin_cap: lkt::FromCoinCap, compatible_versions: VecSet, } public struct SAFE has drop {} +fun init(witness: SAFE, ctx: &mut TxContext) { + let (upgrade_service, _witness) = upgrade_service_bridge::new( + witness, + ctx.sender(), + ctx, + ); + + // Share the upgrade service object + transfer::public_share_object(upgrade_service); +} + #[allow(lint(self_transfer))] -public fun initialize(from_coin_cap: treasury::FromCoinCap, ctx: &mut TxContext) { - let deployer = tx_context::sender(ctx); +public fun initialize(from_coin_cap: lkt::FromCoinCap, ctx: &mut TxContext) { + let deployer = ctx.sender(); let w = bridge_roles::grant_witness(); - let (bridge_cap) = bridge_roles::publish_caps(w, ctx); + let (bridge_cap) = w.publish_caps(ctx); let safe = BridgeSafe { id: object::new(ctx), @@ -109,251 +122,322 @@ public fun initialize(from_coin_cap: treasury::FromCoinCap, ctx: & transfer::share_object(safe); } -fun init(witness: SAFE, ctx: &mut TxContext) { - let (upgrade_service, _witness) = upgrade_service_bridge::new( - witness, - ctx.sender(), +/// Deposit function for native tokens: coin is stored in the safe's coin_storage bag. +public fun deposit( + safe: &mut BridgeSafe, + coin_in: Coin, + recipient: vector, + clock: &Clock, + ctx: &mut TxContext, +) { + assert_is_compatible(safe); + let (key, amount, batch_nonce, dep_nonce) = deposit_validate_and_record( + safe, + &coin_in, + recipient, + false, + clock, ctx, ); - // Share the upgrade service object - transfer::public_share_object(upgrade_service); -} + if (safe.coin_storage.contains(key)) { + safe.coin_storage.borrow_mut, Coin>(key).join(coin_in); + } else { + safe.coin_storage.add(key, coin_in); + }; -fun borrow_token_cfg_mut(safe: &mut BridgeSafe, key: vector): &mut TokenConfig { - table::borrow_mut(&mut safe.token_cfg, key) + events::emit_deposit_v1( + batch_nonce, + dep_nonce, + ctx.sender(), + recipient, + amount, + key, + ); } -public fun whitelist_token( +/// Transfer function for native tokens: splits coin from the safe's bag and sends to receiver. +/// Only the bridge role can call this function. +public(package) fun transfer( safe: &mut BridgeSafe, - minimum_amount: u64, - maximum_amount: u64, - is_native: bool, - is_locked: bool, + _bridge_cap: &bridge_roles::BridgeCap, + receiver: address, + amount: u64, + treasury: &mut lkt::Treasury, ctx: &mut TxContext, -) { - safe.roles.owner_role().assert_sender_is_active_role(ctx); - +): bool { let key = utils::type_name_bytes(); - let exists = table::contains(&safe.token_cfg, key); - assert!(minimum_amount > 0, EZeroAmount); - assert!(minimum_amount <= maximum_amount, EInvalidTokenLimits); + if (!safe.token_cfg.contains(key)) { + return false + }; - if (exists) { - let cfg = table::borrow(&safe.token_cfg, key); - let is_currently_whitelisted = shared_structs::token_config_whitelisted(cfg); - assert!(!is_currently_whitelisted, ETokenAlreadyExists); - - let cfg_mut = borrow_token_cfg_mut(safe, key); - shared_structs::set_token_config_whitelisted(cfg_mut, true); - shared_structs::set_token_config_is_native(cfg_mut, is_native); - shared_structs::set_token_config_min_limit(cfg_mut, minimum_amount); - shared_structs::set_token_config_max_limit(cfg_mut, maximum_amount); - shared_structs::set_token_config_is_locked(cfg_mut, is_locked); + let (is_mint_burn, current_balance, is_locked) = { + let cfg_ref = safe.token_cfg.borrow(key); + (cfg_ref.token_config_is_mint_burn(), cfg_ref.token_config_total_balance(), cfg_ref.get_token_config_is_locked()) + }; + + if (is_mint_burn) { + return false + }; + + if (current_balance < amount) { + return false + }; + + if (!safe.coin_storage.contains(key)) { + return false + }; + + if (!is_locked) { + let stored_coin = safe.coin_storage.borrow_mut, Coin>(key); + let coin_value = stored_coin.value(); + if (coin_value < amount) { + return false + }; + + let coin_to_transfer = stored_coin.split(amount, ctx); + + if (stored_coin.value() == 0) { + let empty_coin = safe.coin_storage.remove, Coin>(key); + empty_coin.destroy_zero(); + }; + + transfer::public_transfer(coin_to_transfer, receiver); + } else { - let cfg = shared_structs::create_token_config( - true, - is_native, - minimum_amount, - maximum_amount, - is_locked, + let stored_bt_coin = safe.coin_storage.borrow_mut, Coin>(key); + + let coin_value = stored_bt_coin.value(); + if (coin_value < amount) { + return false + }; + + let coin_bt = stored_bt_coin.split(amount, ctx); + if (stored_bt_coin.value() == 0) { + let empty_coin = safe.coin_storage.remove, Coin>( + key, + ); + empty_coin.destroy_zero(); + }; + lkt::transfer_from_coin( + treasury, + receiver, + &safe.from_coin_cap, + coin_bt, + ctx, ); - table::add(&mut safe.token_cfg, key, cfg); }; - events::emit_token_whitelisted( - key, - minimum_amount, - maximum_amount, - is_native, - false, - is_locked, - ); -} - -public fun remove_token_from_whitelist(safe: &mut BridgeSafe, ctx: &mut TxContext) { - safe.roles.owner_role().assert_sender_is_active_role(ctx); - let key = utils::type_name_bytes(); - let cfg = borrow_token_cfg_mut(safe, key); - shared_structs::set_token_config_whitelisted(cfg, false); + let cfg_mut = borrow_token_cfg_mut(safe, key); + cfg_mut.subtract_from_token_config_total_balance(amount); - events::emit_token_removed_from_whitelist(key); + true } public fun is_token_whitelisted(safe: &BridgeSafe): bool { let key = utils::type_name_bytes(); - if (!table::contains(&safe.token_cfg, key)) { + if (!safe.token_cfg.contains(key)) { return false }; - let cfg = table::borrow(&safe.token_cfg, key); - shared_structs::token_config_whitelisted(cfg) + let cfg = safe.token_cfg.borrow(key); + cfg.token_config_whitelisted() } -public fun set_batch_timeout_ms(safe: &mut BridgeSafe, new_timeout_ms: u64, ctx: &mut TxContext) { - safe.roles.owner_role().assert_sender_is_active_role(ctx); - assert!(new_timeout_ms <= safe.batch_settle_timeout_ms, EBatchBlockLimitExceedsSettle); - safe.batch_timeout_ms = new_timeout_ms; +public fun get_token_min_limit(safe: &BridgeSafe): u64 { + let key = utils::type_name_bytes(); + let cfg = safe.token_cfg.borrow(key); + cfg.token_config_min_limit() } -public fun set_batch_settle_timeout_ms( - safe: &mut BridgeSafe, - new_timeout_ms: u64, - clock: &Clock, - ctx: &mut TxContext, -) { - pausable::assert_paused(&safe.pause); - safe.roles.owner_role().assert_sender_is_active_role(ctx); - assert!(new_timeout_ms >= safe.batch_timeout_ms, EBatchSettleLimitBelowBlock); - assert!(!is_any_batch_in_progress_internal(safe, clock), EBatchInProgress); - safe.batch_settle_timeout_ms = new_timeout_ms; +public fun get_token_max_limit(safe: &BridgeSafe): u64 { + let key = utils::type_name_bytes(); + let cfg = safe.token_cfg.borrow(key); + cfg.token_config_max_limit() } -public fun set_batch_size(safe: &mut BridgeSafe, new_size: u16, ctx: &mut TxContext) { - safe.roles.owner_role().assert_sender_is_active_role(ctx); - assert!(new_size > 0, EBatchSizeZero); - assert!(new_size <= 100, EBatchSizeTooLarge); - safe.batch_size = new_size; +public fun get_token_is_mint_burn(safe: &BridgeSafe): bool { + let key = utils::type_name_bytes(); + let cfg = safe.token_cfg.borrow(key); + cfg.token_config_is_mint_burn() } -public fun set_token_min_limit(safe: &mut BridgeSafe, amount: u64, ctx: &mut TxContext) { - safe.roles.owner_role().assert_sender_is_active_role(ctx); - +public fun get_token_is_native(safe: &BridgeSafe): bool { let key = utils::type_name_bytes(); - let cfg = borrow_token_cfg_mut(safe, key); - let old_max = shared_structs::token_config_max_limit(cfg); + let cfg = safe.token_cfg.borrow(key); + cfg.token_config_is_native() +} - assert!(amount > 0, EZeroAmount); - assert!(amount <= old_max, EInvalidTokenLimits); +public fun get_batch(safe: &BridgeSafe, batch_nonce: u64, clock: &Clock): (Batch, bool) { + assert!(batch_nonce > 0, EBatchNotFound); + let batch_index = batch_nonce - 1; - shared_structs::set_token_config_min_limit(cfg, amount); + if (!safe.batches.contains(batch_index)) { + let empty_batch = shared_structs::create_batch(0, 0); + return (empty_batch, false) + }; - events::emit_token_limits_updated(key, amount, old_max); + let batch = *safe.batches.borrow(batch_index); + let is_final = is_batch_final_internal(safe, &batch, clock); + (batch, is_final) } -public fun get_token_min_limit(safe: &BridgeSafe): u64 { - let key = utils::type_name_bytes(); - let cfg = table::borrow(&safe.token_cfg, key); - shared_structs::token_config_min_limit(cfg) -} +public fun get_deposits( + safe: &BridgeSafe, + batch_nonce: u64, + clock: &Clock, +): (vector, bool) { + assert!(batch_nonce > 0, EBatchNotFound); + let batch_index = batch_nonce - 1; + let deposits = if (safe.batch_deposits.contains(batch_index)) { + *safe.batch_deposits.borrow(batch_index) + } else { + vector[] + }; + if (!safe.batches.contains(batch_index)) { + return (deposits, false) + }; -public(package) fun roles_mut(safe: &mut BridgeSafe): &mut Roles { - &mut safe.roles + let batch = safe.batches.borrow(batch_index); + let is_final = is_batch_final_internal(safe, batch, clock); + (deposits, is_final) } -public fun set_token_max_limit(safe: &mut BridgeSafe, amount: u64, ctx: &mut TxContext) { - safe.roles.owner_role().assert_sender_is_active_role(ctx); +public fun is_any_batch_in_progress(safe: &BridgeSafe, clock: &Clock): bool { + is_any_batch_in_progress_internal(safe, clock) +} - let key = utils::type_name_bytes(); - let cfg = borrow_token_cfg_mut(safe, key); - let old_min = shared_structs::token_config_min_limit(cfg); +public fun get_bridge_addr(safe: &BridgeSafe): address { + safe.bridge_addr +} - assert!(amount >= old_min, EInvalidTokenLimits); - shared_structs::set_token_config_max_limit(cfg, amount); +/// Get the current owner address +public fun get_owner(safe: &BridgeSafe): address { + safe.roles.owner() +} - events::emit_token_limits_updated(key, old_min, amount); +/// Get the pending owner address (if any) +public fun get_pending_owner(safe: &BridgeSafe): Option
{ + safe.roles.pending_owner() } -public fun get_token_max_limit(safe: &BridgeSafe): u64 { - let key = utils::type_name_bytes(); - let cfg = table::borrow(&safe.token_cfg, key); - shared_structs::token_config_max_limit(cfg) +public fun get_batch_size(safe: &BridgeSafe): u16 { + safe.batch_size } -public fun get_token_is_mint_burn(safe: &BridgeSafe): bool { - let key = utils::type_name_bytes(); - let cfg = table::borrow(&safe.token_cfg, key); - shared_structs::token_config_is_mint_burn(cfg) +public fun get_batch_timeout_ms(safe: &BridgeSafe): u64 { + safe.batch_timeout_ms } -public fun get_token_is_native(safe: &BridgeSafe): bool { - let key = utils::type_name_bytes(); - let cfg = table::borrow(&safe.token_cfg, key); - shared_structs::token_config_is_native(cfg) +public fun get_batch_settle_timeout_ms(safe: &BridgeSafe): u64 { + safe.batch_settle_timeout_ms } -public fun set_token_is_native(safe: &mut BridgeSafe, is_native: bool, ctx: &mut TxContext) { - safe.roles.owner_role().assert_sender_is_active_role(ctx); +public fun get_batches_count(safe: &BridgeSafe): u64 { + safe.batches_count +} - let key = utils::type_name_bytes(); - let cfg = borrow_token_cfg_mut(safe, key); - shared_structs::set_token_config_is_native(cfg, is_native); +public fun get_deposits_count(safe: &BridgeSafe): u64 { + safe.deposits_count +} - events::emit_token_is_native_updated(key, is_native); +public fun get_pause(safe: &BridgeSafe): &Pause { + &safe.pause } -public fun set_token_is_locked(safe: &mut BridgeSafe, is_locked: bool, ctx: &mut TxContext) { - safe.roles.owner_role().assert_sender_is_active_role(ctx); +public(package) fun get_pause_mut(safe: &mut BridgeSafe): &mut Pause { + &mut safe.pause +} - let key = utils::type_name_bytes(); - let cfg = borrow_token_cfg_mut(safe, key); - shared_structs::set_token_config_is_locked(cfg, is_locked); +public fun get_batch_nonce(batch: &Batch): u64 { + batch.batch_nonce() +} - events::emit_token_is_locked_updated(key, is_locked); +public fun get_batch_deposits_count(batch: &Batch): u16 { + batch.batch_deposits_count() } -public fun set_token_is_mint_burn( - safe: &mut BridgeSafe, - is_mint_burn: bool, - ctx: &mut TxContext, -) { - safe.roles.owner_role().assert_sender_is_active_role(ctx); +public fun get_stored_coin_balance(safe: &mut BridgeSafe): u64 { + let key = utils::type_name_bytes(); + if (!safe.token_cfg.contains(key)) { + return 0 + }; + let cfg_ref = safe.token_cfg.borrow(key); + cfg_ref.token_config_total_balance() +} +public fun get_coin_storage_balance(safe: &BridgeSafe): u64 { let key = utils::type_name_bytes(); - let cfg = borrow_token_cfg_mut(safe, key); - shared_structs::set_token_config_is_mint_burn(cfg, is_mint_burn); + if (!safe.coin_storage.contains(key)) { + return 0 + }; + let stored_coin = safe.coin_storage.borrow, Coin>(key); + stored_coin.value() +} - events::emit_token_is_mint_burn_updated(key, is_mint_burn); +// === Admin Management === + +public fun pause_contract(safe: &mut BridgeSafe, ctx: &mut TxContext) { + assert_is_compatible(safe); + safe.roles.owner_role().assert_sender_is_active_role(ctx); + safe.pause.pause(); } -public fun set_bridge_addr(safe: &mut BridgeSafe, new_bridge_addr: address, ctx: &TxContext) { +public fun unpause_contract(safe: &mut BridgeSafe, ctx: &mut TxContext) { + assert_is_compatible(safe); safe.roles.owner_role().assert_sender_is_active_role(ctx); + safe.pause.unpause(); +} - let previous_bridge = safe.bridge_addr; - safe.bridge_addr = new_bridge_addr; - events::emit_bridge_transferred(previous_bridge, new_bridge_addr); +public fun transfer_ownership(safe: &mut BridgeSafe, new_owner: address, ctx: &TxContext) { + assert_is_compatible(safe); + safe.roles_mut().owner_role_mut().begin_role_transfer(new_owner, ctx) +} + +public fun accept_ownership(safe: &mut BridgeSafe, ctx: &TxContext) { + assert_is_compatible(safe); + safe.roles_mut().owner_role_mut().accept_role(ctx) } public fun init_supply(safe: &mut BridgeSafe, coin_in: Coin, ctx: &mut TxContext) { + assert_is_compatible(safe); safe.roles.owner_role().assert_sender_is_active_role(ctx); let key = utils::type_name_bytes(); - assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); - let cfg_ref = table::borrow(&safe.token_cfg, key); - assert!(shared_structs::token_config_whitelisted(cfg_ref), ETokenNotWhitelisted); - - assert!(shared_structs::token_config_is_native(cfg_ref), EInsufficientBalance); + assert_token_is_whitelisted(safe, key); + let cfg_ref = safe.token_cfg.borrow(key); + assert!(cfg_ref.token_config_is_native(), ENotNativeToken); let amount = coin::value(&coin_in); let cfg_mut = borrow_token_cfg_mut(safe, key); - shared_structs::add_to_token_config_total_balance(cfg_mut, amount); + cfg_mut.add_to_token_config_total_balance(amount); - if (bag::contains(&safe.coin_storage, key)) { - let existing_coin = bag::borrow_mut, Coin>(&mut safe.coin_storage, key); - coin::join(existing_coin, coin_in); + if (safe.coin_storage.contains(key)) { + let existing_coin = safe.coin_storage.borrow_mut, Coin>(key); + existing_coin.join(coin_in); } else { - bag::add(&mut safe.coin_storage, key, coin_in); + safe.coin_storage.add(key, coin_in); }; } #[allow(lint(self_transfer))] public fun sync_supply(safe: &mut BridgeSafe, mut coin_in: Coin, ctx: &mut TxContext) { + assert_is_compatible(safe); safe.roles.owner_role().assert_sender_is_active_role(ctx); let key = utils::type_name_bytes(); - assert!(table::contains(&safe.token_cfg, key), ETokenNotWhitelisted); - let cfg_ref = table::borrow(&safe.token_cfg, key); - assert!(shared_structs::token_config_whitelisted(cfg_ref), ETokenNotWhitelisted); - assert!(shared_structs::token_config_is_native(cfg_ref), EInsufficientBalance); + assert_token_is_whitelisted(safe, key); + let cfg_ref = safe.token_cfg.borrow(key); + assert!(cfg_ref.token_config_is_native(), ENotNativeToken); - let expected_balance = shared_structs::token_config_total_balance(cfg_ref); + let expected_balance = cfg_ref.token_config_total_balance(); - let actual_balance = if (bag::contains(&safe.coin_storage, key)) { - let stored_coin = bag::borrow, Coin>(&safe.coin_storage, key); - coin::value(stored_coin) + let actual_balance = if (safe.coin_storage.contains(key)) { + let stored_coin = safe.coin_storage.borrow, Coin>(key); + stored_coin.value() } else { 0 }; @@ -361,324 +445,174 @@ public fun sync_supply(safe: &mut BridgeSafe, mut coin_in: Coin, ctx: &mut assert!(expected_balance > actual_balance, EInsufficientBalance); let deficit = expected_balance - actual_balance; - assert!(coin::value(&coin_in) >= deficit, EInsufficientBalance); + assert!(coin_in.value() >= deficit, EInsufficientBalance); - let top_up_coin = coin::split(&mut coin_in, deficit, ctx); + let top_up_coin = coin_in.split(deficit, ctx); - if (bag::contains(&safe.coin_storage, key)) { - let existing_coin = bag::borrow_mut, Coin>(&mut safe.coin_storage, key); - coin::join(existing_coin, top_up_coin); + if (safe.coin_storage.contains(key)) { + let existing_coin = safe.coin_storage.borrow_mut, Coin>(key); + existing_coin.join(top_up_coin); } else { - bag::add(&mut safe.coin_storage, key, top_up_coin); + safe.coin_storage.add(key, top_up_coin); }; - if (coin::value(&coin_in) == 0) { - coin::destroy_zero(coin_in); + if (coin_in.value() == 0) { + coin_in.destroy_zero(); } else { - transfer::public_transfer(coin_in, tx_context::sender(ctx)); + transfer::public_transfer(coin_in, ctx.sender()); }; } -/// Deposit function: Users send coins FROM their wallet TO the bridge safe contract -/// The coins are stored in the contract's coin_storage for later transfer -public fun deposit( +public fun whitelist_token( safe: &mut BridgeSafe, - coin_in: Coin, - recipient: vector, - clock: &Clock, + minimum_amount: u64, + maximum_amount: u64, + is_locked: bool, ctx: &mut TxContext, ) { - pausable::assert_not_paused(&safe.pause); - - assert!(vector::length(&recipient) == 32, EInvalidRecipient); - - let key = utils::type_name_bytes(); - let cfg_ref = table::borrow(&safe.token_cfg, key); - assert!(shared_structs::token_config_whitelisted(cfg_ref), ETokenNotWhitelisted); - - let amount = coin::value(&coin_in); - assert!(amount > 0, EZeroAmount); - assert!(amount >= shared_structs::token_config_min_limit(cfg_ref), EAmountBelowMinimum); - assert!(amount <= shared_structs::token_config_max_limit(cfg_ref), EAmountAboveMaximum); - - if (should_create_new_batch_internal(safe, clock)) { - create_new_batch_internal(safe, clock, ctx); - }; - let batch_index = safe.batches_count - 1; - let batch = table::borrow_mut(&mut safe.batches, batch_index); - - assert!(safe.deposits_count < MAX_U64, EOverflow); - let dep_nonce = safe.deposits_count + 1; - let dep = shared_structs::create_deposit( - dep_nonce, - key, - amount, - tx_context::sender(ctx), - recipient, - ); - if (!table::contains(&safe.batch_deposits, batch_index)) { - table::add(&mut safe.batch_deposits, batch_index, vector::empty()); - }; - let vec_ref = table::borrow_mut(&mut safe.batch_deposits, batch_index); - vector::push_back(vec_ref, dep); - - safe.deposits_count = dep_nonce; - shared_structs::increment_batch_deposits(batch); - shared_structs::set_batch_last_updated_timestamp_ms(batch, clock::timestamp_ms(clock)); - - let batch_nonce = shared_structs::batch_nonce(batch); - - let cfg = borrow_token_cfg_mut(safe, key); - shared_structs::add_to_token_config_total_balance(cfg, amount); - - if (bag::contains(&safe.coin_storage, key)) { - let existing_coin = bag::borrow_mut, Coin>(&mut safe.coin_storage, key); - coin::join(existing_coin, coin_in); - } else { - bag::add(&mut safe.coin_storage, key, coin_in); - }; - - events::emit_deposit_v1( - batch_nonce, - dep_nonce, - tx_context::sender(ctx), - recipient, - amount, - key, + assert_is_compatible(safe); + whitelist_token_internal( + safe, + minimum_amount, + maximum_amount, + true, + option::none(), + false, + is_locked, + ctx, ); } -public(package) fun checkOwnerRole(safe: &BridgeSafe, ctx: &TxContext) { +/// Removes a native (non-mint-burn) token from the whitelist. +/// For mint-burn tokens, use the adapter's remove_token_from_whitelist instead. +public fun remove_token_from_whitelist(safe: &mut BridgeSafe, ctx: &mut TxContext) { + assert_is_compatible(safe); safe.roles.owner_role().assert_sender_is_active_role(ctx); + let key = utils::type_name_bytes(); + let cfg_ref = safe.token_cfg.borrow(key); + assert!(!cfg_ref.token_config_is_mint_burn(), EIncompatibleTokenFlags); + unwhitelist_token(safe, key); } -public fun get_batch(safe: &BridgeSafe, batch_nonce: u64, clock: &Clock): (Batch, bool) { - assert!(batch_nonce > 0, EBatchNotFound); - let batch_index = batch_nonce - 1; - - if (!table::contains(&safe.batches, batch_index)) { - let empty_batch = shared_structs::create_batch(0, 0); - return (empty_batch, false) - }; - - let batch = *table::borrow(&safe.batches, batch_index); - let is_final = is_batch_final_internal(safe, &batch, clock); - (batch, is_final) -} - -public fun get_deposits( - safe: &BridgeSafe, - batch_nonce: u64, - clock: &Clock, -): (vector, bool) { - assert!(batch_nonce > 0, EBatchNotFound); - let batch_index = batch_nonce - 1; - let deposits = if (table::contains(&safe.batch_deposits, batch_index)) { - *table::borrow(&safe.batch_deposits, batch_index) - } else { - vector::empty() - }; - if (!table::contains(&safe.batches, batch_index)) { - return (deposits, false) - }; - - let batch = table::borrow(&safe.batches, batch_index); - let is_final = is_batch_final_internal(safe, batch, clock); - (deposits, is_final) -} - -public fun is_any_batch_in_progress(safe: &BridgeSafe, clock: &Clock): bool { - is_any_batch_in_progress_internal(safe, clock) -} - -fun create_new_batch_internal(safe: &mut BridgeSafe, clock: &Clock, _ctx: &mut TxContext) { - assert!(safe.batches_count < MAX_U64, EOverflow); - let nonce = safe.batches_count + 1; - let batch = shared_structs::create_batch(nonce, clock::timestamp_ms(clock)); - table::add(&mut safe.batches, safe.batches_count, batch); - safe.batches_count = nonce; -} - -fun should_create_new_batch_internal(safe: &BridgeSafe, clock: &Clock): bool { - if (safe.batches_count == 0) { return true }; - let last_index = safe.batches_count - 1; - let batch = table::borrow(&safe.batches, last_index); - is_batch_progress_over_internal(safe, shared_structs::batch_deposits_count(batch), shared_structs::batch_timestamp_ms(batch), clock) || (shared_structs::batch_deposits_count(batch) >= safe.batch_size) -} - -fun is_batch_progress_over_internal( - safe: &BridgeSafe, - dep_count: u16, - timestamp_ms: u64, - clock: &Clock, -): bool { - if (dep_count == 0) { return false }; - (timestamp_ms + safe.batch_timeout_ms) <= clock::timestamp_ms(clock) -} - -fun is_batch_final_internal(safe: &BridgeSafe, batch: &Batch, clock: &Clock): bool { - (shared_structs::batch_last_updated_timestamp_ms(batch) + safe.batch_settle_timeout_ms) <= clock::timestamp_ms(clock) -} - -fun is_any_batch_in_progress_internal(safe: &BridgeSafe, clock: &Clock): bool { - if (safe.batches_count == 0) { return false }; - let last_index = safe.batches_count - 1; - if (!should_create_new_batch_internal(safe, clock)) { return true }; - let batch = table::borrow(&safe.batches, last_index); - !is_batch_final_internal(safe, batch, clock) -} - -public fun get_bridge_addr(safe: &BridgeSafe): address { - safe.bridge_addr -} - -/// Get the current owner address -public fun get_owner(safe: &BridgeSafe): address { - bridge_roles::owner(&safe.roles) -} - -/// Get the pending owner address (if any) -public fun get_pending_owner(safe: &BridgeSafe): Option
{ - bridge_roles::pending_owner(&safe.roles) -} - -public fun get_batch_size(safe: &BridgeSafe): u16 { - safe.batch_size -} - -public fun get_batch_timeout_ms(safe: &BridgeSafe): u64 { - safe.batch_timeout_ms -} - -public fun get_batch_settle_timeout_ms(safe: &BridgeSafe): u64 { - safe.batch_settle_timeout_ms -} - -public fun get_batches_count(safe: &BridgeSafe): u64 { - safe.batches_count -} - -public fun get_deposits_count(safe: &BridgeSafe): u64 { - safe.deposits_count -} - -public fun get_pause(safe: &BridgeSafe): &Pause { - &safe.pause -} - -public fun get_pause_mut(safe: &mut BridgeSafe): &mut Pause { - &mut safe.pause +/// Package-internal: marks a token as not whitelisted without the mint-burn guard. +/// Used by the adapter which handles MintCap cleanup separately. +public(package) fun unwhitelist_token(safe: &mut BridgeSafe, key: vector) { + let cfg = borrow_token_cfg_mut(safe, key); + cfg.set_token_config_whitelisted(false); + events::emit_token_removed_from_whitelist(key); } -public fun get_batch_nonce(batch: &Batch): u64 { - shared_structs::batch_nonce(batch) +public fun set_bridge_addr(safe: &mut BridgeSafe, new_bridge_addr: address, ctx: &TxContext) { + assert_is_compatible(safe); + safe.roles.owner_role().assert_sender_is_active_role(ctx); + + let previous_bridge = safe.bridge_addr; + safe.bridge_addr = new_bridge_addr; + events::emit_bridge_transferred(previous_bridge, new_bridge_addr); } -public fun get_batch_deposits_count(batch: &Batch): u16 { - shared_structs::batch_deposits_count(batch) +public fun set_batch_timeout_ms(safe: &mut BridgeSafe, new_timeout_ms: u64, ctx: &mut TxContext) { + assert_is_compatible(safe); + safe.roles.owner_role().assert_sender_is_active_role(ctx); + assert!(new_timeout_ms <= safe.batch_settle_timeout_ms, EBatchBlockLimitExceedsSettle); + safe.batch_timeout_ms = new_timeout_ms; + events::emit_batch_timeout_updated(new_timeout_ms); } -/// Transfer function: Bridge sends coins FROM the bridge safe contract TO recipient -/// Only the bridge role can call this function -/// The coins are taken from the contract's storage and sent to recipient -public(package) fun transfer( +public fun set_batch_settle_timeout_ms( safe: &mut BridgeSafe, - _bridge_cap: &bridge_roles::BridgeCap, - receiver: address, - amount: u64, - treasury: &mut treasury::Treasury, + new_timeout_ms: u64, + clock: &Clock, ctx: &mut TxContext, -): bool { - let key = utils::type_name_bytes(); +) { + assert_is_compatible(safe); + safe.pause.assert_paused(); + safe.roles.owner_role().assert_sender_is_active_role(ctx); + assert!(new_timeout_ms >= safe.batch_timeout_ms, EBatchSettleLimitBelowBlock); + assert!(!is_any_batch_in_progress_internal(safe, clock), EBatchInProgress); + safe.batch_settle_timeout_ms = new_timeout_ms; + events::emit_batch_settle_timeout_updated(new_timeout_ms); +} - if (!table::contains(&safe.token_cfg, key)) { - return false - }; +public fun set_batch_size(safe: &mut BridgeSafe, new_size: u16, ctx: &mut TxContext) { + assert_is_compatible(safe); + safe.roles.owner_role().assert_sender_is_active_role(ctx); + assert!(new_size > 0, EBatchSizeZero); + assert!(new_size <= 100, EBatchSizeTooLarge); + safe.batch_size = new_size; + events::emit_batch_size_updated(new_size); +} - let cfg_ref = table::borrow(&safe.token_cfg, key); +public fun set_token_min_limit(safe: &mut BridgeSafe, amount: u64, ctx: &mut TxContext) { + assert_is_compatible(safe); + safe.roles.owner_role().assert_sender_is_active_role(ctx); - let current_balance = shared_structs::token_config_total_balance(cfg_ref); - if (current_balance < amount) { - return false - }; + let key = utils::type_name_bytes(); + let cfg = borrow_token_cfg_mut(safe, key); + let old_max = cfg.token_config_max_limit(); - if (!bag::contains(&safe.coin_storage, key)) { - return false - }; + assert!(amount > 0, EZeroAmount); + assert!(amount <= old_max, EInvalidTokenLimits); - if (!shared_structs::get_token_config_is_locked(cfg_ref)) { - let stored_coin = bag::borrow_mut, Coin>(&mut safe.coin_storage, key); - let coin_value = coin::value(stored_coin); - if (coin_value < amount) { - return false - }; + cfg.set_token_config_min_limit(amount); - let coin_to_transfer = coin::split(stored_coin, amount, ctx); + events::emit_token_limits_updated(key, amount, old_max); +} - if (coin::value(stored_coin) == 0) { - let empty_coin = bag::remove, Coin>(&mut safe.coin_storage, key); - coin::destroy_zero(empty_coin); - }; - transfer::public_transfer(coin_to_transfer, receiver); - } else { - let stored_bt_coin = bag::borrow_mut< - vector, - Coin, - >( - &mut safe.coin_storage, - key, - ); - let coin_bt = coin::split(stored_bt_coin, amount, ctx); +public fun set_token_max_limit(safe: &mut BridgeSafe, amount: u64, ctx: &mut TxContext) { + assert_is_compatible(safe); + safe.roles.owner_role().assert_sender_is_active_role(ctx); - treasury::transfer_from_coin( - treasury, - receiver, - &safe.from_coin_cap, - coin_bt, - ctx, - ); - }; + let key = utils::type_name_bytes(); + let cfg = borrow_token_cfg_mut(safe, key); + let old_min = cfg.token_config_min_limit(); - let cfg_mut = borrow_token_cfg_mut(safe, key); - shared_structs::subtract_from_token_config_total_balance(cfg_mut, amount); + assert!(amount >= old_min, EInvalidTokenLimits); + cfg.set_token_config_max_limit(amount); - true + events::emit_token_limits_updated(key, old_min, amount); } -public fun get_stored_coin_balance(safe: &mut BridgeSafe): u64 { - let key = utils::type_name_bytes(); - if (!table::contains(&safe.token_cfg, key)) { - return 0 - }; - let cfg_ref = table::borrow(&safe.token_cfg, key); - shared_structs::token_config_total_balance(cfg_ref) -} +public fun set_token_is_native(safe: &mut BridgeSafe, is_native: bool, ctx: &mut TxContext) { + assert_is_compatible(safe); + safe.roles.owner_role().assert_sender_is_active_role(ctx); -public fun get_coin_storage_balance(safe: &BridgeSafe): u64 { let key = utils::type_name_bytes(); - if (!bag::contains(&safe.coin_storage, key)) { - return 0 - }; - let stored_coin = bag::borrow, Coin>(&safe.coin_storage, key); - coin::value(stored_coin) + let cfg = borrow_token_cfg_mut(safe, key); + assert!(!(is_native && cfg.token_config_is_mint_burn()), EIncompatibleTokenFlags); + cfg.set_token_config_is_native(is_native); + + events::emit_token_is_native_updated(key, is_native); } -public fun pause_contract(safe: &mut BridgeSafe, ctx: &mut TxContext) { +public fun set_token_is_mint_burn( + safe: &mut BridgeSafe, + is_mint_burn: bool, + ctx: &mut TxContext, +) { + assert_is_compatible(safe); safe.roles.owner_role().assert_sender_is_active_role(ctx); - pausable::pause(&mut safe.pause); + + let key = utils::type_name_bytes(); + let cfg = borrow_token_cfg_mut(safe, key); + assert!(!(is_mint_burn && cfg.token_config_is_native()), EIncompatibleTokenFlags); + cfg.set_token_config_is_mint_burn(is_mint_burn); + + events::emit_token_is_mint_burn_updated(key, is_mint_burn); } -public fun unpause_contract(safe: &mut BridgeSafe, ctx: &mut TxContext) { +public fun set_token_is_locked(safe: &mut BridgeSafe, is_locked: bool, ctx: &mut TxContext) { safe.roles.owner_role().assert_sender_is_active_role(ctx); - pausable::unpause(&mut safe.pause); -} -public fun transfer_ownership(safe: &mut BridgeSafe, new_owner: address, ctx: &TxContext) { - safe.roles_mut().owner_role_mut().begin_role_transfer(new_owner, ctx) -} + let key = utils::type_name_bytes(); + let cfg = borrow_token_cfg_mut(safe, key); + assert!( + !(is_locked && shared_structs::token_config_is_mint_burn(cfg)), + EIncompatibleTokenFlags, + ); + shared_structs::set_token_config_is_locked(cfg, is_locked); -public fun accept_ownership(safe: &mut BridgeSafe, ctx: &TxContext) { - safe.roles_mut().owner_role_mut().accept_role(ctx) + events::emit_token_is_locked_updated(key, is_locked); } // === Upgrade Management === @@ -769,14 +703,239 @@ public fun is_migration_in_progress(safe: &BridgeSafe): bool { safe.compatible_versions.length() > 1 } -/// [Package private] Asserts that the Safe object -/// is compatible with the package's version. +// === Asserts === + public(package) fun assert_is_compatible(safe: &BridgeSafe) { bridge_version_control::assert_object_version_is_compatible_with_package(safe.compatible_versions); } +public(package) fun assert_token_is_whitelisted(safe: &BridgeSafe, key: vector) { + assert!(safe.token_cfg.contains(key), ETokenNotWhitelisted); + let cfg = safe.token_cfg.borrow(key); + 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!(!cfg.token_config_whitelisted(), ETokenAlreadyExists); +} + +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!(cfg.token_config_is_mint_burn(), EIncompatibleTokenFlags); +} + +/// ==== Internal logic helpers ==== + +public(package) fun whitelist_token_internal( + safe: &mut BridgeSafe, + minimum_amount: u64, + maximum_amount: u64, + 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); + + let key = utils::type_name_bytes(); + let exists = safe.token_cfg.contains(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, + is_locked, + ); + + events::emit_token_whitelisted( + key, + minimum_amount, + maximum_amount, + is_native, + is_mint_burn, + is_locked, + ); +} + +/// 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) { + 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!(cfg_ref.token_config_whitelisted(), ETokenNotWhitelisted); + assert!(cfg_ref.token_config_is_mint_burn() == expect_mint_burn, EIncompatibleTokenFlags); + + 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); + + if (should_create_new_batch_internal(safe, clock)) { + create_new_batch_internal(safe, clock, ctx); + }; + + let batch_index = safe.batches_count - 1; + let batch = safe.batches.borrow_mut(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, + ctx.sender(), + recipient, + ); + + if (!safe.batch_deposits.contains(batch_index)) { + safe.batch_deposits.add(batch_index, vector[]); + }; + let vec_ref = safe.batch_deposits.borrow_mut(batch_index); + vec_ref.push_back(dep); + + safe.deposits_count = dep_nonce; + batch.increment_batch_deposits(); + batch.set_batch_last_updated_timestamp_ms(clock.timestamp_ms()); + + let batch_nonce = batch.batch_nonce(); + + let cfg = borrow_token_cfg_mut(safe, key); + cfg.add_to_token_config_total_balance(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()); + 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 = safe.batches.borrow(last_index); + 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( + 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() +} + +fun is_batch_final_internal(safe: &BridgeSafe, batch: &Batch, clock: &Clock): bool { + (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 { + 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 = safe.batches.borrow(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 { + 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 = safe.token_cfg.borrow_mut(key); + cfg.subtract_from_token_config_total_balance(amount); +} + +fun borrow_token_cfg_mut(safe: &mut BridgeSafe, key: vector): &mut TokenConfig { + safe.token_cfg.borrow_mut(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. +/// 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_v1( + batch_nonce, + dep_nonce, + ctx.sender(), + recipient, + amount, + key, + ); +} + #[test_only] -public fun init_for_testing(from_cap: treasury::FromCoinCap, ctx: &mut TxContext) { +public fun init_for_testing(from_cap: lkt::FromCoinCap, ctx: &mut TxContext) { initialize(from_cap, ctx); } @@ -789,6 +948,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); } - diff --git a/sources/shared_structs.move b/sources/shared_structs.move index 0214507..ef0d7c4 100644 --- a/sources/shared_structs.move +++ b/sources/shared_structs.move @@ -1,4 +1,10 @@ -module shared_structs::shared_structs; +module bridge_safe::shared_structs; + +use sui::table::Table; + +const EUnderflow: u64 = 0; +const EOverflow: u64 = 1; +const MAX_U64: u64 = 18446744073709551615; public enum DepositStatus has copy, drop, store { None, @@ -35,6 +41,7 @@ public struct TokenConfig has copy, drop, store { min_limit: u64, max_limit: u64, total_balance: u64, + treasury_id: Option, is_locked: bool, } @@ -93,11 +100,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; } @@ -133,29 +140,31 @@ 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; -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) { +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; } @@ -184,7 +193,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; } @@ -192,20 +201,81 @@ public(package) fun set_batch_last_updated_timestamp_ms(batch: &mut Batch, times batch.last_updated_timestamp_ms = timestamp_ms; } +public(package) fun upsert_token_config( + config: &mut Table, TokenConfig>, + key: vector, + whitelisted: bool, + is_native: bool, + min_limit: u64, + max_limit: u64, + treasury_id: Option, + is_mint_burn: bool, + is_locked: bool +) { + if (config.contains(key)) { + let cfg = config.borrow_mut(key); + 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, + is_mint_burn, + min_limit, + max_limit, + treasury_id, + is_locked, + ); + config.add(key, cfg); +} + +public(package) fun set_token_config( + config: &mut TokenConfig, + whitelisted: bool, + is_native: bool, + min_limit: u64, + 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); + 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); + 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_locked: bool, ): TokenConfig { TokenConfig { whitelisted, is_native, - is_mint_burn: false, + is_mint_burn, min_limit, max_limit, total_balance: 0, + treasury_id, is_locked, } } diff --git a/sources/upgrade_manager.move b/sources/upgrade_manager.move index 488b44f..4d498c6 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::Bridge; use bridge_safe::bridge_version_control; +use bridge_safe::safe::BridgeSafe; use sui::event; // === Events === @@ -24,21 +24,17 @@ 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); - + 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(), }); } @@ -49,12 +45,12 @@ 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); - 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(), previous_versions: vector[bridge_version_control::current_version() - 1], // Previous version @@ -62,19 +58,15 @@ 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); - +public fun abort_system_migration(safe: &mut BridgeSafe, bridge: &mut Bridge, ctx: &mut TxContext) { + 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 5c8eee9..eb17b84 100644 --- a/sources/utils.move +++ b/sources/utils.move @@ -1,15 +1,13 @@ /// Utils Module - Utility Functions -/// +/// /// This module provides common utility functions used across the bridge system. 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.into_string().into_bytes() } diff --git a/sources/version_control.move b/sources/version_control.move index e1cdd6d..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. @@ -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/audit_fixes_tests.move b/tests/audit_fixes_tests.move new file mode 100644 index 0000000..f98fd44 --- /dev/null +++ b/tests/audit_fixes_tests.move @@ -0,0 +1,242 @@ +#[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 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 {} + +const ADMIN: address = @0xa11ce; + +const INITIAL_QUORUM: u64 = 3; +const MIN_AMOUNT: u64 = 100; +const MAX_AMOUNT: u64 = 1_000_000; +const DEFAULT_LOKED: bool = false; + +const PK1: vector = b"12345678901234567890123456789012"; +const PK2: vector = b"abcdefghijklmnopqrstuvwxyz123456"; +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()); + }; + + 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, + DEFAULT_LOKED, + 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 +} + +// === 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); +} + +// === 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, + DEFAULT_LOKED, + 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, + DEFAULT_LOKED, + 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); +} + +// === 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/bridge_comprehensive_tests.move b/tests/bridge_comprehensive_tests.move index 11f58e5..f36bfda 100644 --- a/tests/bridge_comprehensive_tests.move +++ b/tests/bridge_comprehensive_tests.move @@ -61,8 +61,7 @@ fun test_initialize_bridge_success() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -145,8 +144,7 @@ fun test_set_quorum_success() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -192,8 +190,7 @@ fun test_set_quorum_not_admin() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -237,8 +234,7 @@ fun test_add_relayer_success() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -286,8 +282,7 @@ fun test_remove_relayer_success() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -341,8 +336,7 @@ fun test_remove_relayer_below_quorum() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -386,8 +380,7 @@ fun test_pause_unpause_contract() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -442,8 +435,7 @@ fun test_getter_functions() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -514,8 +506,7 @@ fun test_set_batch_settle_timeout_success() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -575,8 +566,7 @@ fun test_set_batch_settle_timeout_not_admin() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -629,8 +619,7 @@ fun test_execute_transfer_invalid_signature_length() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -655,7 +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 = scenario.take_shared>(); + let mut treasury = ts::take_shared>(&scenario); let clock = clock::create_for_testing(ts::ctx(&mut scenario)); let recipients = vector[USER]; @@ -680,8 +669,8 @@ fun test_execute_transfer_invalid_signature_length() { ); ts::return_shared(bridge); - ts::return_shared(treasury); ts::return_shared(safe); + ts::return_shared(treasury); clock::destroy_for_testing(clock); }; @@ -702,8 +691,7 @@ fun test_execute_transfer_insufficient_signatures() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -728,7 +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 = scenario.take_shared>(); + let mut treasury = ts::take_shared>(&scenario); let clock = clock::create_for_testing(ts::ctx(&mut scenario)); let recipients = vector[USER]; @@ -786,8 +774,7 @@ fun test_add_relayer_invalid_public_key_length() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -833,8 +820,7 @@ fun test_add_relayer_not_admin() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -879,8 +865,7 @@ fun test_remove_relayer_not_admin() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -925,8 +910,7 @@ fun test_pause_contract_not_admin() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -971,8 +955,7 @@ fun test_unpause_contract_not_admin() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -1037,23 +1020,22 @@ 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"; 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]; - - br::init_for_testing(scenario.ctx()); - + scenario.next_tx(ADMIN); { let mut treasury = scenario.take_shared>(); @@ -1072,57 +1054,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 = 0; - while (i < 64) { + + 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, @@ -1130,12 +1125,12 @@ fun test_validate_quorum_reaches_signature_verification() { &recipients, &amounts, &signatures, - &deposit_nonces + &deposit_nonces, ); - + ts::return_shared(bridge); }; - + ts::end(scenario); } @@ -1143,22 +1138,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, @@ -1166,12 +1167,12 @@ fun test_validate_quorum_insufficient_signatures() { &recipients, &amounts, &signatures, - &deposit_nonces + &deposit_nonces, ); - + ts::return_shared(bridge); }; - + ts::end(scenario); } @@ -1179,24 +1180,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, @@ -1204,12 +1205,12 @@ fun test_validate_quorum_invalid_signature_length() { &recipients, &amounts, &signatures, - &deposit_nonces + &deposit_nonces, ); - + ts::return_shared(bridge); }; - + ts::end(scenario); } @@ -1217,24 +1218,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, @@ -1242,11 +1243,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 53f461a..ab1c65b 100644 --- a/tests/deposit_transfer_tests.move +++ b/tests/deposit_transfer_tests.move @@ -4,15 +4,22 @@ 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 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; 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; @@ -59,8 +66,7 @@ fun test_deposit_basic() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -122,8 +128,7 @@ fun test_deposit_multiple_same_batch() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -195,8 +200,7 @@ fun test_deposit_triggers_new_batch() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -267,8 +271,7 @@ fun test_deposit_invalid_recipient() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -321,8 +324,7 @@ fun test_deposit_zero_amount() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -356,8 +358,7 @@ fun test_deposit_amount_below_minimum() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -391,8 +392,7 @@ fun test_deposit_amount_above_maximum() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -426,8 +426,7 @@ fun test_deposit_when_paused() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -463,8 +462,7 @@ fun test_transfer_basic() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -477,8 +475,8 @@ 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); + let mut treasury = ts::take_shared>(&scenario); // Verify initial balance assert!(safe::get_stored_coin_balance(&mut safe) == 100000, 0); @@ -500,8 +498,8 @@ 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(safe); ts::return_shared(treasury); + ts::return_shared(safe); ts::return_to_address(ADMIN, bridge_cap); }; @@ -528,8 +526,7 @@ fun test_transfer_exact_balance() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -543,8 +540,8 @@ 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); + let mut treasury = ts::take_shared>(&scenario); // Transfer entire balance let success = safe::transfer( @@ -563,8 +560,8 @@ fun test_transfer_exact_balance() { assert!(bag_balance == 0, 10); assert!(bag_balance == safe::get_stored_coin_balance(&mut safe), 11); - ts::return_shared(safe); ts::return_shared(treasury); + ts::return_shared(safe); ts::return_to_address(ADMIN, bridge_cap); }; @@ -578,8 +575,8 @@ 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); + let mut treasury = ts::take_shared>(&scenario); // Try to transfer non-whitelisted token - should return false let success = safe::transfer( @@ -597,8 +594,8 @@ 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(safe); ts::return_shared(treasury); + ts::return_shared(safe); ts::return_to_address(ADMIN, bridge_cap); }; @@ -618,8 +615,7 @@ fun test_transfer_token_removed_from_whitelist() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -634,8 +630,8 @@ 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); + 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( @@ -653,8 +649,8 @@ 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(safe); ts::return_shared(treasury); + ts::return_shared(safe); ts::return_to_address(ADMIN, bridge_cap); }; @@ -673,8 +669,7 @@ fun test_transfer_insufficient_balance() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -688,8 +683,8 @@ 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); + let mut treasury = ts::take_shared>(&scenario); // Try to transfer more than balance - should return false let success = safe::transfer( @@ -708,8 +703,8 @@ fun test_transfer_insufficient_balance() { assert!(bag_balance == 1000, 10); assert!(bag_balance == safe::get_stored_coin_balance(&mut safe), 11); - ts::return_shared(safe); ts::return_shared(treasury); + ts::return_shared(safe); ts::return_to_address(ADMIN, bridge_cap); }; @@ -729,8 +724,7 @@ fun test_transfer_no_coin_storage() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -740,8 +734,8 @@ 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); + let mut treasury = ts::take_shared>(&scenario); // Try to transfer when no coins stored - should return false let success = safe::transfer( @@ -759,8 +753,8 @@ 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(safe); ts::return_shared(treasury); + ts::return_shared(safe); ts::return_to_address(ADMIN, bridge_cap); }; @@ -779,8 +773,7 @@ fun test_transfer_multiple_partial() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -793,8 +786,8 @@ 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); + let mut treasury = ts::take_shared>(&scenario); // Multiple transfers let success1 = safe::transfer( @@ -831,8 +824,8 @@ fun test_transfer_multiple_partial() { assert!(bag_balance == 40000, 10); assert!(bag_balance == safe::get_stored_coin_balance(&mut safe), 11); - ts::return_shared(safe); ts::return_shared(treasury); + ts::return_shared(safe); ts::return_to_address(ADMIN, bridge_cap); }; @@ -852,7 +845,6 @@ fun test_deposit_then_transfer_integration() { MIN_AMOUNT, MAX_AMOUNT, false, - false, // is_locked ts::ctx(&mut scenario), ); @@ -876,8 +868,8 @@ 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 mut treasury = ts::take_shared>(&scenario); let success = safe::transfer( &mut safe, @@ -895,8 +887,847 @@ 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); + }; + + 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_internal( + &mut safe, + MIN_AMOUNT, + MAX_AMOUNT, + false, + option::some(object::id_from_address(@0x1234)), + true, + false, + 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); + br::init_for_testing(s.ctx()); + 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); + + 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); + { + let from_cap_db = s.take_from_address>(ADMIN); + safe::init_for_testing(from_cap_db, 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, + false, + 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 = xmn_mint_cap_adapter::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_internal( + &mut safe, + MIN_AMOUNT, + MAX_AMOUNT, + false, + option::some(object::id(&treasury)), + true, + false, + ts::ctx(&mut scenario), + ); + + let coin = coin::mint_for_testing( + DEPOSIT_AMOUNT, + ts::ctx(&mut scenario), + ); + + // No MintCap registered — expect EMintBurnCapNotFound + xmn_mint_cap_adapter::deposit( + &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); +} + +#[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, + 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); + }; + + 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, + false, + 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, + false, + 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, + false, + + 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); }; diff --git a/tests/events_tests.move b/tests/events_tests.move index 9a6a670..13831be 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 @@ -121,17 +121,17 @@ fun test_emit_token_whitelisted() { 10000, // max_limit true, // is_native false, // is_mint_burn - true // is_locked + false, // is_locked ); }; - + 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 @@ -141,17 +141,17 @@ fun test_emit_token_whitelisted_all_false() { 5000, // max_limit false, // is_native false, // is_mint_burn - false // is_locked + false, // is_locked ); }; - + 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 @@ -161,66 +161,66 @@ fun test_emit_token_whitelisted_mint_burn() { 1000000, // max_limit false, // is_native true, // is_mint_burn - false // is_locked + false, // is_locked ); }; - + 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); } @@ -230,7 +230,6 @@ fun test_emit_token_is_locked_updated() { scenario.next_tx(ADMIN); { - // Test emitting a token is locked updated event - true events::emit_token_is_locked_updated(b"LOCKED_TOKEN", true); }; @@ -243,7 +242,6 @@ fun test_emit_token_is_locked_updated_false() { scenario.next_tx(ADMIN); { - // Test emitting a token is locked updated event - false events::emit_token_is_locked_updated(b"UNLOCKED_TOKEN", false); }; @@ -253,49 +251,49 @@ fun test_emit_token_is_locked_updated_false() { #[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 @@ -303,17 +301,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 @@ -321,83 +319,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); } @@ -414,7 +411,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", ); }; @@ -424,17 +421,17 @@ 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); 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, 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 63de240..0055688 100644 --- a/tests/safe_edge_case_tests.move +++ b/tests/safe_edge_case_tests.move @@ -95,8 +95,7 @@ fun test_multiple_token_whitelist() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -105,8 +104,7 @@ fun test_multiple_token_whitelist() { &mut safe, MIN_AMOUNT * 2, MAX_AMOUNT * 2, - false, // not native - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -137,8 +135,7 @@ fun test_token_limit_updates() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -192,8 +189,7 @@ fun test_init_supply_zero_amount() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -212,9 +208,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::EInsufficientBalance)] fun test_init_supply_non_native_token() { let mut scenario = setup(); scenario.next_tx(ADMIN); @@ -227,7 +222,6 @@ fun test_init_supply_non_native_token() { MIN_AMOUNT, MAX_AMOUNT, false, - false, // is_locked ts::ctx(&mut scenario), ); @@ -257,8 +251,7 @@ fun test_init_supply_removed_token() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); diff --git a/tests/safe_internal_tests.move b/tests/safe_internal_tests.move index 65079ee..a841821 100644 --- a/tests/safe_internal_tests.move +++ b/tests/safe_internal_tests.move @@ -1,14 +1,14 @@ #[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 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}; -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 {} @@ -85,8 +85,7 @@ fun test_whitelist_token() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, // is_native - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -113,8 +112,7 @@ fun test_whitelist_token_already_exists() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -123,8 +121,7 @@ fun test_whitelist_token_already_exists() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -148,8 +145,7 @@ fun test_whitelist_token_not_admin() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -170,8 +166,7 @@ fun test_remove_token_from_whitelist() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -317,7 +312,6 @@ fun test_set_batch_size() { safe::set_batch_size( &mut safe, - new_size, ts::ctx(&mut scenario), ); @@ -363,8 +357,7 @@ fun test_set_token_min_limit() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -395,8 +388,7 @@ fun test_set_token_max_limit() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -447,8 +439,7 @@ fun test_init_supply() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, // is_native = true - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -481,8 +472,7 @@ fun test_init_supply_multiple_times() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, // is_native = true - false, // is_locked + false, ts::ctx(&mut scenario), ); @@ -831,12 +821,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); }; @@ -850,15 +840,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); }; @@ -880,12 +870,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); }; @@ -900,9 +890,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); }; @@ -916,15 +906,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); }; @@ -932,11 +922,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); }; @@ -1003,11 +993,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); }; @@ -1015,22 +1005,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); }; @@ -1038,9 +1028,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); }; @@ -1058,7 +1048,6 @@ fun test_sync_supply() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, false, ts::ctx(&mut scenario), ); @@ -1101,7 +1090,6 @@ fun test_sync_supply_exact_amount() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, false, ts::ctx(&mut scenario), ); @@ -1133,7 +1121,6 @@ fun test_sync_supply_no_existing_bag_entry() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, false, ts::ctx(&mut scenario), ); @@ -1166,7 +1153,6 @@ fun test_sync_supply_not_owner() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, false, ts::ctx(&mut scenario), ); @@ -1212,7 +1198,6 @@ fun test_sync_supply_no_deficit() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, false, ts::ctx(&mut scenario), ); @@ -1240,7 +1225,6 @@ fun test_sync_supply_insufficient_coin() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, false, ts::ctx(&mut scenario), ); @@ -1270,7 +1254,6 @@ fun test_sync_supply_not_native() { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - false, false, ts::ctx(&mut scenario), ); @@ -1305,9 +1288,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/security_tests.move b/tests/security_tests.move index 42ca210..c34190b 100644 --- a/tests/security_tests.move +++ b/tests/security_tests.move @@ -54,8 +54,7 @@ fun setup(): Scenario { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, - false, // is_locked + false, ts::ctx(&mut s), ); diff --git a/tests/shared_structs_tests.move b/tests/shared_structs_tests.move index 8a254e5..765a346 100644 --- a/tests/shared_structs_tests.move +++ b/tests/shared_structs_tests.move @@ -1,39 +1,41 @@ #[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; #[test] fun test_token_config_is_mint_burn() { let mut config = shared_structs::create_token_config( - true, - false, - 100, - 1000, - false + true, + false, + 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); - + 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] 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); } @@ -41,19 +43,21 @@ 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, - false + true, + false, + 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); } @@ -62,13 +66,15 @@ 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, - false + true, + false, + false, + 100, + 1000, + option::none(), + false, ); - + shared_structs::subtract_from_token_config_total_balance(&mut config, 1); } @@ -76,33 +82,37 @@ 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, - false + true, + false, + 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, - false + true, + false, + 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); } @@ -111,15 +121,17 @@ 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, - false + true, + false, + 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); } @@ -127,71 +139,53 @@ 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, - false + true, + false, + 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, - false + true, + false, + 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); } -#[test] -fun test_set_token_config_is_locked() { - let mut config = shared_structs::create_token_config( - true, - false, - 100, - 1000, - 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[ 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); } @@ -200,13 +194,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); } @@ -227,53 +223,53 @@ 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, - false + true, + false, + 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_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); - - shared_structs::set_batch_deposits_count(&mut batch, 10); + + config.set_token_config_is_native(true); + config.set_token_config_is_mint_burn(true); + config.add_to_token_config_total_balance(500); + + 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); + + 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); - + 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 +} diff --git a/tests/upgrade_tests.move b/tests/upgrade_tests.move index f0c3831..0059b64 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 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}; @@ -50,7 +50,6 @@ fun setup(): Scenario { &mut safe, MIN_AMOUNT, MAX_AMOUNT, - true, false, s.ctx(), ); @@ -74,79 +73,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); }