From c51603d7063ec8c7165186d6d3a81e975ef5b1bd Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Fri, 5 Jun 2026 11:54:02 -0400 Subject: [PATCH 1/4] NONEVM-5258: billing for Sui => Sui --- contracts/ccip/ccip/sources/fee_quoter.move | 112 +++++- .../ccip/tests/sui_fee_billing_tests.move | 319 ++++++++++++++++++ contracts/ccip/mock_ccip_v2/fee_quoter.move | 112 +++++- sui.nix | 2 +- 4 files changed, 534 insertions(+), 11 deletions(-) create mode 100644 contracts/ccip/ccip/tests/sui_fee_billing_tests.move diff --git a/contracts/ccip/ccip/sources/fee_quoter.move b/contracts/ccip/ccip/sources/fee_quoter.move index 97dafcd4..ad99792d 100644 --- a/contracts/ccip/ccip/sources/fee_quoter.move +++ b/contracts/ccip/ccip/sources/fee_quoter.move @@ -71,6 +71,24 @@ const SVM_TOKEN_TRANSFER_DATA_OVERHEAD: u64 = + 32 // per-chain token billing config, not always included in the token lookup table + 32; // OffRamp pool signer PDA, not included in the token lookup table; +/// The maximum number of receiver object IDs that can be passed in SuiExtraArgsV1. +const SUI_EXTRA_ARGS_MAX_RECEIVER_OBJECT_IDS: u64 = 64; + +/// Number of overhead accounts needed for message execution on SUI. +/// This is the message.receiver. +const SUI_MESSAGING_ACCOUNTS_OVERHEAD: u64 = 1; + +/// The size of each SUI account address in bytes. +const SUI_ACCOUNT_BYTE_SIZE: u64 = 32; + +/// The expected static payload size of a token transfer on SUI. +const SUI_TOKEN_TRANSFER_DATA_OVERHEAD: u64 = + (4 + 32) // source_pool + + 32 // dest_token_address + + 4 // dest_gas_amount + + 4 // extra_data length + + 32; // amount + const MAX_U64: u256 = 18446744073709551615; const MAX_U160: u256 = 1461501637330902918203684832716283019655932542975; const VAL_1E5: u256 = 100_000; @@ -241,6 +259,8 @@ const EInvalidSvmAccountLength: u64 = 35; const ETokenAmountMismatch: u64 = 36; const EInvalidOwnerCap: u64 = 37; const EInvalidFunction: u64 = 38; +const ETooManySuiExtraArgsReceiverObjectIds: u64 = 39; +const EInvalidSuiReceiverObjectIdLength: u64 = 40; public fun type_and_version(): String { string::utf8(b"FeeQuoter 1.6.1") @@ -708,7 +728,18 @@ public fun get_validated_fee( ) { resolve_generic_gas_limit(dest_chain_config, extra_args) } else if (chain_family_selector == CHAIN_FAMILY_SELECTOR_SUI) { - resolve_sui_gas_limit(dest_chain_config, extra_args) + let (gas, overhead) = resolve_sui_gas_limit( + dest_chain_config, + state, + dest_chain_selector, + extra_args, + receiver, + data_len, + tokens_len, + local_token_addresses, + ); + svm_payload_overhead = overhead; + gas } else if (chain_family_selector == CHAIN_FAMILY_SELECTOR_SVM) { let (gas, overhead) = resolve_svm_gas_limit( dest_chain_config, @@ -866,19 +897,90 @@ fun resolve_generic_gas_limit(dest_chain_config: &DestChainConfig, extra_args: v gas_limit } -fun resolve_sui_gas_limit(dest_chain_config: &DestChainConfig, extra_args: vector): u256 { +/// Returns `(gas_limit, sui_payload_overhead_bytes)`. +/// `sui_payload_overhead_bytes` is the SUI-specific payload size counted in maxDataBytes validation +/// but not already included in `data_len` or per-token `dest_bytes_overhead` billing. +fun resolve_sui_gas_limit( + dest_chain_config: &DestChainConfig, + state: &FeeQuoterState, + dest_chain_selector: u64, + extra_args: vector, + receiver: vector, + data_len: u64, + tokens_len: u64, + local_token_addresses: vector
, +): (u256, u64) { let ( gas_limit, allow_out_of_order_execution, - _token_receiver, - _receiver_object_ids, + token_receiver, + receiver_object_ids, ) = decode_sui_extra_args(extra_args); assert!(gas_limit <= (dest_chain_config.max_per_msg_gas_limit as u64), EMessageGasLimitTooHigh); assert!( !dest_chain_config.enforce_out_of_order || allow_out_of_order_execution, EExtraArgOutOfOrderExecutionMustBeTrue, ); - gas_limit as u256 + + let receiver_object_ids_length = receiver_object_ids.length(); + assert!( + receiver_object_ids_length <= SUI_EXTRA_ARGS_MAX_RECEIVER_OBJECT_IDS, + ETooManySuiExtraArgsReceiverObjectIds, + ); + + let mut i = 0; + while (i < receiver_object_ids_length) { + assert!(receiver_object_ids[i].length() == 32, EInvalidSuiReceiverObjectIdLength); + i = i + 1; + }; + + let mut sui_payload_overhead = tokens_len * SUI_TOKEN_TRANSFER_DATA_OVERHEAD; + + let receiver_uint = eth_abi::decode_u256_value(receiver); + if (receiver_uint == 0) { + assert!(receiver_object_ids_length == 0, ETooManySuiExtraArgsReceiverObjectIds); + } else { + sui_payload_overhead = + sui_payload_overhead + ( + receiver_object_ids_length + SUI_MESSAGING_ACCOUNTS_OVERHEAD + ) * SUI_ACCOUNT_BYTE_SIZE; + }; + + if (tokens_len > 0) { + assert!( + token_receiver.length() == 32 + && eth_abi::decode_u256_value(token_receiver) != 0, + EInvalidTokenReceiver, + ); + }; + + let mut sui_expanded_data_length = data_len + sui_payload_overhead; + + let mut i = 0; + while (i < tokens_len) { + let local_token_address = local_token_addresses[i]; + let destBytesOverhead = get_token_transfer_fee_config_internal( + state, + dest_chain_selector, + local_token_address, + ).dest_bytes_overhead; + + if (destBytesOverhead > 0) { + sui_expanded_data_length = sui_expanded_data_length + (destBytesOverhead as u64); + } else { + sui_expanded_data_length = + sui_expanded_data_length + (CCIP_LOCK_OR_BURN_V1_RET_BYTES as u64); + }; + + i = i + 1; + }; + + assert!( + sui_expanded_data_length <= (dest_chain_config.max_data_bytes as u64), + EMessageTooLarge, + ); + + (gas_limit as u256, sui_payload_overhead) } fun check_svm_writable_bitmap(account_is_writable_bitmap: u64, accounts_length: u64) { diff --git a/contracts/ccip/ccip/tests/sui_fee_billing_tests.move b/contracts/ccip/ccip/tests/sui_fee_billing_tests.move new file mode 100644 index 00000000..fe4a709c --- /dev/null +++ b/contracts/ccip/ccip/tests/sui_fee_billing_tests.move @@ -0,0 +1,319 @@ +#[allow(implicit_const_copy)] +#[test_only] +module ccip::sui_fee_billing_tests; + +use ccip::client; +use ccip::fee_quoter; +use ccip::ownable::OwnerCap; +use ccip::state_object::{Self, CCIPObjectRef}; +use ccip::upgrade_registry; +use sui::clock; +use sui::test_scenario; + +const CHAIN_FAMILY_SELECTOR_SUI: vector = x"c4e05953"; +const ONE_E_18: u256 = 1_000_000_000_000_000_000; +const FEE_TOKEN: address = + @0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b; +const TOKEN_2: address = @0x000000000000000000000000F4030086522a5bEEa4988F8cA5B36dbC97BeE88c; +const SUI_RECEIVER: vector = + x"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; +const TOKEN_RECEIVER: vector = + x"aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344"; +const OBJ1: vector = + x"0101010101010101010101010101010101010101010101010101010101010101"; +const OBJ2: vector = + x"0202020202020202020202020202020202020202020202020202020202020202"; +const OBJ3: vector = + x"0303030303030303030303030303030303030303030303030303030303030303"; +const OBJ4: vector = + x"0404040404040404040404040404040404040404040404040404040404040404"; +const OBJ5: vector = + x"0505050505050505050505050505050505050505050505050505050505050505"; +const OBJ6: vector = + x"0606060606060606060606060606060606060606060606060606060606060606"; +const OBJ7: vector = + x"0707070707070707070707070707070707070707070707070707070707070707"; +const OBJ8: vector = + x"0808080808080808080808080808080808080808080808080808080808080808"; +const OBJ9: vector = + x"0909090909090909090909090909090909090909090909090909090909090909"; +const OBJ10: vector = + x"0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a"; +const ADMIN: address = @0x1; + +fun setup(): (test_scenario::Scenario, OwnerCap, CCIPObjectRef) { + let mut scenario = test_scenario::begin(ADMIN); + let ctx = scenario.ctx(); + state_object::test_init(ctx); + scenario.next_tx(ADMIN); + let owner_cap = scenario.take_from_sender(); + let ref = scenario.take_shared(); + (scenario, owner_cap, ref) +} + +fun init_fee_quoter(ref: &mut CCIPObjectRef, cap: &OwnerCap, ctx: &mut TxContext) { + upgrade_registry::initialize(ref, cap, ctx); + fee_quoter::initialize( + ref, + cap, + 200 * ONE_E_18, + FEE_TOKEN, + 1000, + vector[FEE_TOKEN, TOKEN_2], + ctx, + ); +} + +fun setup_sui_dest(ref: &mut CCIPObjectRef, cap: &OwnerCap, ctx: &mut TxContext) { + fee_quoter::apply_dest_chain_config_updates( + ref, + cap, + 100, + true, + 10, + 30_000, + 3_000_000, + 300_000, + 16, + 40, + 300, + 100, + 16, + 600, + CHAIN_FAMILY_SELECTOR_SUI, + false, + 50, + 90_000, + 200_000, + ONE_E_18 as u64, + 1_000_000, + 50, + ctx, + ); +} + +fun setup_prices( + ref: &mut CCIPObjectRef, + fq_cap: &fee_quoter::FeeQuoterCap, + clock: &clock::Clock, + ctx: &mut TxContext, +) { + fee_quoter::update_prices( + ref, + fq_cap, + clock, + vector[FEE_TOKEN, TOKEN_2], + vector[150_000_000_000 * ONE_E_18, 150_000_000_000 * ONE_E_18], + vector[100], + vector[7_500_000_000_000], + ctx, + ); +} + +fun cleanup(scenario: test_scenario::Scenario, cap: OwnerCap, ref: CCIPObjectRef) { + test_scenario::return_to_sender(&scenario, cap); + test_scenario::return_shared(ref); + test_scenario::end(scenario); +} + +fun many_object_ids(count: u64): vector> { + let mut ids = vector[]; + let mut i = 0; + while (i < count) { + ids.push_back(x"2234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdea"); + i = i + 1; + }; + ids +} + +#[test] +public fun test_sui_fee_increases_with_receiver_object_id_count() { + let (mut scenario, cap, mut ref) = setup(); + let ctx = scenario.ctx(); + init_fee_quoter(&mut ref, &cap, ctx); + let fq_cap = fee_quoter::new_fee_quoter_cap(&ref, &cap, ctx); + let mut clock = clock::create_for_testing(ctx); + clock::increment_for_testing(&mut clock, 20000); + setup_prices(&mut ref, &fq_cap, &clock, ctx); + setup_sui_dest(&mut ref, &cap, ctx); + fee_quoter::apply_premium_multiplier_wei_per_eth_updates( + &mut ref, + &cap, + vector[FEE_TOKEN], + vector[ONE_E_18 as u64], + ctx, + ); + + let fee_0_ids = fee_quoter::get_validated_fee( + &ref, + &clock, + 100, + SUI_RECEIVER, + b"test_payload", + vector[], + vector[], + FEE_TOKEN, + client::encode_sui_extra_args_v1(200_000, false, TOKEN_RECEIVER, vector[]), + ); + let fee_10_ids = fee_quoter::get_validated_fee( + &ref, + &clock, + 100, + SUI_RECEIVER, + b"test_payload", + vector[], + vector[], + FEE_TOKEN, + client::encode_sui_extra_args_v1( + 200_000, + false, + TOKEN_RECEIVER, + vector[OBJ1, OBJ2, OBJ3, OBJ4, OBJ5, OBJ6, OBJ7, OBJ8, OBJ9, OBJ10], + ), + ); + + assert!(fee_0_ids > 0, 1); + assert!(fee_10_ids > fee_0_ids, 2); + + fee_quoter::destroy_fee_quoter_cap(&ref, &cap, fq_cap); + clock::destroy_for_testing(clock); + cleanup(scenario, cap, ref); +} + +#[test] +#[expected_failure(abort_code = fee_quoter::ETooManySuiExtraArgsReceiverObjectIds)] +public fun test_sui_fee_reverts_when_too_many_receiver_object_ids() { + let (mut scenario, cap, mut ref) = setup(); + let ctx = scenario.ctx(); + init_fee_quoter(&mut ref, &cap, ctx); + let fq_cap = fee_quoter::new_fee_quoter_cap(&ref, &cap, ctx); + let mut clock = clock::create_for_testing(ctx); + clock::increment_for_testing(&mut clock, 20000); + setup_prices(&mut ref, &fq_cap, &clock, ctx); + setup_sui_dest(&mut ref, &cap, ctx); + fee_quoter::apply_premium_multiplier_wei_per_eth_updates( + &mut ref, + &cap, + vector[FEE_TOKEN], + vector[ONE_E_18 as u64], + ctx, + ); + + fee_quoter::get_validated_fee( + &ref, + &clock, + 100, + SUI_RECEIVER, + b"test_payload", + vector[], + vector[], + FEE_TOKEN, + client::encode_sui_extra_args_v1(200_000, false, TOKEN_RECEIVER, many_object_ids(65)), + ); + + fee_quoter::destroy_fee_quoter_cap(&ref, &cap, fq_cap); + clock::destroy_for_testing(clock); + cleanup(scenario, cap, ref); +} + +#[test] +#[expected_failure(abort_code = fee_quoter::ETooManySuiExtraArgsReceiverObjectIds)] +public fun test_sui_fee_reverts_zero_receiver_with_object_ids() { + let (mut scenario, cap, mut ref) = setup(); + let ctx = scenario.ctx(); + init_fee_quoter(&mut ref, &cap, ctx); + let fq_cap = fee_quoter::new_fee_quoter_cap(&ref, &cap, ctx); + let mut clock = clock::create_for_testing(ctx); + clock::increment_for_testing(&mut clock, 20000); + setup_prices(&mut ref, &fq_cap, &clock, ctx); + setup_sui_dest(&mut ref, &cap, ctx); + fee_quoter::apply_premium_multiplier_wei_per_eth_updates( + &mut ref, + &cap, + vector[FEE_TOKEN], + vector[ONE_E_18 as u64], + ctx, + ); + + let zero_receiver = x"0000000000000000000000000000000000000000000000000000000000000000"; + fee_quoter::get_validated_fee( + &ref, + &clock, + 100, + zero_receiver, + b"test_payload", + vector[], + vector[], + FEE_TOKEN, + client::encode_sui_extra_args_v1(200_000, false, TOKEN_RECEIVER, vector[OBJ1]), + ); + + fee_quoter::destroy_fee_quoter_cap(&ref, &cap, fq_cap); + clock::destroy_for_testing(clock); + cleanup(scenario, cap, ref); +} + +#[test] +#[expected_failure(abort_code = fee_quoter::EMessageTooLarge)] +public fun test_sui_expanded_payload_respects_max_data_bytes() { + let (mut scenario, cap, mut ref) = setup(); + let ctx = scenario.ctx(); + init_fee_quoter(&mut ref, &cap, ctx); + let fq_cap = fee_quoter::new_fee_quoter_cap(&ref, &cap, ctx); + let mut clock = clock::create_for_testing(ctx); + clock::increment_for_testing(&mut clock, 20000); + setup_prices(&mut ref, &fq_cap, &clock, ctx); + + // Configure with a low max_data_bytes (512) so object IDs push it over the limit + fee_quoter::apply_dest_chain_config_updates( + &mut ref, + &cap, + 100, + true, + 10, + 512, + 3_000_000, + 300_000, + 16, + 40, + 300, + 100, + 16, + 600, + CHAIN_FAMILY_SELECTOR_SUI, + false, + 50, + 90_000, + 200_000, + ONE_E_18 as u64, + 1_000_000, + 50, + ctx, + ); + + fee_quoter::apply_premium_multiplier_wei_per_eth_updates( + &mut ref, + &cap, + vector[FEE_TOKEN], + vector[ONE_E_18 as u64], + ctx, + ); + + // 10 object IDs = (10 + 1) * 32 = 352 bytes overhead + 12 bytes data = 364. Should pass. + // But 15 IDs = (15 + 1) * 32 = 512 + 12 = 524 > 512. Should fail. + fee_quoter::get_validated_fee( + &ref, + &clock, + 100, + SUI_RECEIVER, + b"test_payload", + vector[], + vector[], + FEE_TOKEN, + client::encode_sui_extra_args_v1(200_000, false, TOKEN_RECEIVER, many_object_ids(15)), + ); + + fee_quoter::destroy_fee_quoter_cap(&ref, &cap, fq_cap); + clock::destroy_for_testing(clock); + cleanup(scenario, cap, ref); +} diff --git a/contracts/ccip/mock_ccip_v2/fee_quoter.move b/contracts/ccip/mock_ccip_v2/fee_quoter.move index 1d2e550f..5f6850ea 100644 --- a/contracts/ccip/mock_ccip_v2/fee_quoter.move +++ b/contracts/ccip/mock_ccip_v2/fee_quoter.move @@ -71,6 +71,24 @@ const SVM_TOKEN_TRANSFER_DATA_OVERHEAD: u64 = + 32 // per-chain token billing config, not always included in the token lookup table + 32; // OffRamp pool signer PDA, not included in the token lookup table; +/// The maximum number of receiver object IDs that can be passed in SuiExtraArgsV1. +const SUI_EXTRA_ARGS_MAX_RECEIVER_OBJECT_IDS: u64 = 64; + +/// Number of overhead accounts needed for message execution on SUI. +/// This is the message.receiver. +const SUI_MESSAGING_ACCOUNTS_OVERHEAD: u64 = 1; + +/// The size of each SUI account address in bytes. +const SUI_ACCOUNT_BYTE_SIZE: u64 = 32; + +/// The expected static payload size of a token transfer on SUI. +const SUI_TOKEN_TRANSFER_DATA_OVERHEAD: u64 = + (4 + 32) // source_pool + + 32 // dest_token_address + + 4 // dest_gas_amount + + 4 // extra_data length + + 32; // amount + const MAX_U64: u256 = 18446744073709551615; const MAX_U160: u256 = 1461501637330902918203684832716283019655932542975; const VAL_1E5: u256 = 100_000; @@ -241,6 +259,8 @@ const EInvalidSvmAccountLength: u64 = 35; const ETokenAmountMismatch: u64 = 36; const EInvalidOwnerCap: u64 = 37; const EInvalidFunction: u64 = 38; +const ETooManySuiExtraArgsReceiverObjectIds: u64 = 39; +const EInvalidSuiReceiverObjectIdLength: u64 = 40; public fun type_and_version(): String { string::utf8(b"FeeQuoter 1.6.2") @@ -709,7 +729,18 @@ public fun get_validated_fee( ) { resolve_generic_gas_limit(dest_chain_config, extra_args) } else if (chain_family_selector == CHAIN_FAMILY_SELECTOR_SUI) { - resolve_sui_gas_limit(dest_chain_config, extra_args) + let (gas, overhead) = resolve_sui_gas_limit( + dest_chain_config, + state, + dest_chain_selector, + extra_args, + receiver, + data_len, + tokens_len, + local_token_addresses, + ); + svm_payload_overhead = overhead; + gas } else if (chain_family_selector == CHAIN_FAMILY_SELECTOR_SVM) { let (gas, overhead) = resolve_svm_gas_limit( dest_chain_config, @@ -867,19 +898,90 @@ fun resolve_generic_gas_limit(dest_chain_config: &DestChainConfig, extra_args: v gas_limit } -fun resolve_sui_gas_limit(dest_chain_config: &DestChainConfig, extra_args: vector): u256 { +/// Returns `(gas_limit, sui_payload_overhead_bytes)`. +/// `sui_payload_overhead_bytes` is the SUI-specific payload size counted in maxDataBytes validation +/// but not already included in `data_len` or per-token `dest_bytes_overhead` billing. +fun resolve_sui_gas_limit( + dest_chain_config: &DestChainConfig, + state: &FeeQuoterState, + dest_chain_selector: u64, + extra_args: vector, + receiver: vector, + data_len: u64, + tokens_len: u64, + local_token_addresses: vector
, +): (u256, u64) { let ( gas_limit, allow_out_of_order_execution, - _token_receiver, - _receiver_object_ids, + token_receiver, + receiver_object_ids, ) = decode_sui_extra_args(extra_args); assert!(gas_limit <= (dest_chain_config.max_per_msg_gas_limit as u64), EMessageGasLimitTooHigh); assert!( !dest_chain_config.enforce_out_of_order || allow_out_of_order_execution, EExtraArgOutOfOrderExecutionMustBeTrue, ); - gas_limit as u256 + + let receiver_object_ids_length = receiver_object_ids.length(); + assert!( + receiver_object_ids_length <= SUI_EXTRA_ARGS_MAX_RECEIVER_OBJECT_IDS, + ETooManySuiExtraArgsReceiverObjectIds, + ); + + let mut i = 0; + while (i < receiver_object_ids_length) { + assert!(receiver_object_ids[i].length() == 32, EInvalidSuiReceiverObjectIdLength); + i = i + 1; + }; + + let mut sui_payload_overhead = tokens_len * SUI_TOKEN_TRANSFER_DATA_OVERHEAD; + + let receiver_uint = eth_abi::decode_u256_value(receiver); + if (receiver_uint == 0) { + assert!(receiver_object_ids_length == 0, ETooManySuiExtraArgsReceiverObjectIds); + } else { + sui_payload_overhead = + sui_payload_overhead + ( + receiver_object_ids_length + SUI_MESSAGING_ACCOUNTS_OVERHEAD + ) * SUI_ACCOUNT_BYTE_SIZE; + }; + + if (tokens_len > 0) { + assert!( + token_receiver.length() == 32 + && eth_abi::decode_u256_value(token_receiver) != 0, + EInvalidTokenReceiver, + ); + }; + + let mut sui_expanded_data_length = data_len + sui_payload_overhead; + + let mut i = 0; + while (i < tokens_len) { + let local_token_address = local_token_addresses[i]; + let destBytesOverhead = get_token_transfer_fee_config_internal( + state, + dest_chain_selector, + local_token_address, + ).dest_bytes_overhead; + + if (destBytesOverhead > 0) { + sui_expanded_data_length = sui_expanded_data_length + (destBytesOverhead as u64); + } else { + sui_expanded_data_length = + sui_expanded_data_length + (CCIP_LOCK_OR_BURN_V1_RET_BYTES as u64); + }; + + i = i + 1; + }; + + assert!( + sui_expanded_data_length <= (dest_chain_config.max_data_bytes as u64), + EMessageTooLarge, + ); + + (gas_limit as u256, sui_payload_overhead) } fun check_svm_writable_bitmap(account_is_writable_bitmap: u64, accounts_length: u64) { diff --git a/sui.nix b/sui.nix index e5c1d263..8ee0225b 100644 --- a/sui.nix +++ b/sui.nix @@ -9,7 +9,7 @@ stdenv.mkDerivation rec { src = if stdenv.hostPlatform.isDarwin then pkgs.fetchzip { url = "https://github.com/MystenLabs/sui/releases/download/mainnet-v${version}/sui-mainnet-v${version}-macos-arm64.tgz"; # Assume is a M1 Mac - sha256 = "sha256-rtONzChS4tJmqCXWtW6fceb10xNQyZUwcFJMMTT+ZAE="; # Should be replaced when bumping versions + sha256 = "sha256-fSIt7V78vLJQfWKvzwJn6A+ER0JjvWHTDw7hdgdPSUc="; # Should be replaced when bumping versions stripRoot = false; } else if stdenv.isLinux then From a3a7555c3ec29790966b39af0e2cf0ac4a2e888c Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Fri, 5 Jun 2026 12:31:59 -0400 Subject: [PATCH 2/4] fix tests --- .../ccip/tests/state_object_upgrade_test.move | 22 ++++++----------- .../tests/token_admin_registry_tests.move | 4 ++-- .../mcms/mcms/sources/mcms_deployer.move | 24 +++++++++++++++++++ contracts/scripts/test.sh | 7 +++--- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/contracts/ccip/ccip/tests/state_object_upgrade_test.move b/contracts/ccip/ccip/tests/state_object_upgrade_test.move index 2ba44449..39e1849a 100644 --- a/contracts/ccip/ccip/tests/state_object_upgrade_test.move +++ b/contracts/ccip/ccip/tests/state_object_upgrade_test.move @@ -11,6 +11,7 @@ use sui::test_scenario::{Self as ts, Scenario}; const ADMIN: address = @0xA; const PACKAGE_OWNER: address = @0xB; +const TEST_PACKAGE: address = @0xCCCC; fun init_mcms_and_ccip(ctx: &mut TxContext) { mcms_account::test_init(ctx); @@ -62,16 +63,14 @@ fun test_upgrade_flow_updates_state_correctly() { ts::next_tx(&mut scenario, PACKAGE_OWNER); let mut deployer_state = ts::take_shared(&scenario); - let registry = ts::take_shared(&scenario); let ctx = ts::ctx(&mut scenario); - let upgrade_cap = package::test_publish(@ccip.to_id(), ctx); + let upgrade_cap = package::test_publish(TEST_PACKAGE.to_id(), ctx); let old_package_address = upgrade_cap.package().to_address(); let cap_id = object::id(&upgrade_cap); - mcms_deployer::register_upgrade_cap( + mcms_deployer::test_register_upgrade_cap( &mut deployer_state, - ®istry, upgrade_cap, ctx, ); @@ -80,7 +79,6 @@ fun test_upgrade_flow_updates_state_correctly() { assert!(mcms_deployer::has_upgrade_cap(&deployer_state, old_package_address), 0); ts::return_shared(deployer_state); - ts::return_shared(registry); // Perform upgrade: authorize -> upgrade -> commit ts::next_tx(&mut scenario, ADMIN); @@ -165,21 +163,18 @@ fun test_cannot_authorize_upgrade_with_old_package_address_after_upgrade() { { ts::next_tx(&mut scenario, PACKAGE_OWNER); let mut deployer_state = ts::take_shared(&scenario); - let registry = ts::take_shared(&scenario); let ctx = ts::ctx(&mut scenario); - let upgrade_cap = package::test_publish(@ccip.to_id(), ctx); + let upgrade_cap = package::test_publish(TEST_PACKAGE.to_id(), ctx); old_package_address = upgrade_cap.package().to_address(); - mcms_deployer::register_upgrade_cap( + mcms_deployer::test_register_upgrade_cap( &mut deployer_state, - ®istry, upgrade_cap, ctx, ); ts::return_shared(deployer_state); - ts::return_shared(registry); }; // Perform one upgrade @@ -247,21 +242,18 @@ fun test_multiple_upgrades_chain_correctly() { { ts::next_tx(&mut scenario, PACKAGE_OWNER); let mut deployer_state = ts::take_shared(&scenario); - let registry = ts::take_shared(&scenario); let ctx = ts::ctx(&mut scenario); - let upgrade_cap = package::test_publish(@ccip.to_id(), ctx); + let upgrade_cap = package::test_publish(TEST_PACKAGE.to_id(), ctx); current_package_address = upgrade_cap.package().to_address(); - mcms_deployer::register_upgrade_cap( + mcms_deployer::test_register_upgrade_cap( &mut deployer_state, - ®istry, upgrade_cap, ctx, ); ts::return_shared(deployer_state); - ts::return_shared(registry); }; // Perform 3 consecutive upgrades diff --git a/contracts/ccip/ccip/tests/token_admin_registry_tests.move b/contracts/ccip/ccip/tests/token_admin_registry_tests.move index f32238c8..1b5d4174 100644 --- a/contracts/ccip/ccip/tests/token_admin_registry_tests.move +++ b/contracts/ccip/ccip/tests/token_admin_registry_tests.move @@ -378,7 +378,7 @@ public fun test_register() { local_token, ); assert!( - token_type == ascii::string(b"5ef4b483da6644c84aa78eae4f51a9bfb1fb4554d5134ac98892e931fcbdd6bf::token_admin_registry_tests::TOKEN_ADMIN_REGISTRY_TESTS"), + token_type == type_name::into_string(type_name::with_defining_ids()), ); assert!(type_proof == type_name::into_string(type_name::with_defining_ids())); @@ -416,7 +416,7 @@ public fun test_register() { local_token, ); assert!( - token_type == ascii::string(b"5ef4b483da6644c84aa78eae4f51a9bfb1fb4554d5134ac98892e931fcbdd6bf::token_admin_registry_tests::TOKEN_ADMIN_REGISTRY_TESTS"), + token_type == type_name::into_string(type_name::with_defining_ids()), ); // Since TypeProof and TypeProof2 have the same package ID, the type proof should remain as TypeProof assert!(type_proof == type_name::into_string(type_name::with_defining_ids())); diff --git a/contracts/mcms/mcms/sources/mcms_deployer.move b/contracts/mcms/mcms/sources/mcms_deployer.move index 99c8771c..afcdfb1b 100644 --- a/contracts/mcms/mcms/sources/mcms_deployer.move +++ b/contracts/mcms/mcms/sources/mcms_deployer.move @@ -168,3 +168,27 @@ public fun has_upgrade_cap(state: &DeployerState, package_address: address): boo public fun test_init(ctx: &mut TxContext) { init(MCMS_DEPLOYER {}, ctx); } + +#[test_only] +/// Register an upgrade cap without requiring MCMS registry check. +/// Needed because in tests @self resolves to 0x0, which Sui >= 1.73 +/// rejects in `authorize_upgrade` (uses 0x0 as a sentinel for in-progress upgrades). +public fun test_register_upgrade_cap( + state: &mut DeployerState, + upgrade_cap: UpgradeCap, + ctx: &mut TxContext, +) { + let package_address = upgrade_cap.package().to_address(); + let version = upgrade_cap.version(); + let policy = upgrade_cap.policy(); + + state.cap_to_package.add(object::id(&upgrade_cap), package_address); + state.upgrade_caps.add(package_address, upgrade_cap); + + event::emit(UpgradeCapRegistered { + prev_owner: ctx.sender(), + package_address, + version, + policy, + }); +} diff --git a/contracts/scripts/test.sh b/contracts/scripts/test.sh index e07f5c41..768fedee 100755 --- a/contracts/scripts/test.sh +++ b/contracts/scripts/test.sh @@ -19,10 +19,11 @@ PACKAGES=( ) # Sui ≥1.66 uses on-chain-like gas metering in unit tests with a ~1M default budget. -# Some tests (e.g. MCMS multi-step flows) exceed that and fail as "Test timed out". -SUI_TEST_GAS_LIMIT="${SUI_TEST_GAS_LIMIT:-500000000}" +# Some tests (e.g. MCMS multi-step flows, BCS deserialization of large extra_args) exceed that. +SUI_TEST_GAS_LIMIT="${SUI_TEST_GAS_LIMIT:-2000000000}" +SUI_BUILD_ENV="${SUI_BUILD_ENV:-testnet}" # run tests for pkg in "${PACKAGES[@]}"; do - sui move test --path "$pkg" --gas-limit "$SUI_TEST_GAS_LIMIT" + sui move test --path "$pkg" --build-env "$SUI_BUILD_ENV" --gas-limit "$SUI_TEST_GAS_LIMIT" done From c31212673fb191d8145ebd804e06a5e69d92e932 Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Fri, 5 Jun 2026 12:41:56 -0400 Subject: [PATCH 3/4] fi --- contracts/ccip/ccip/sources/fee_quoter.move | 1 + contracts/ccip/mock_ccip_v2/fee_quoter.move | 1 + 2 files changed, 2 insertions(+) diff --git a/contracts/ccip/ccip/sources/fee_quoter.move b/contracts/ccip/ccip/sources/fee_quoter.move index ad99792d..da9fc178 100644 --- a/contracts/ccip/ccip/sources/fee_quoter.move +++ b/contracts/ccip/ccip/sources/fee_quoter.move @@ -936,6 +936,7 @@ fun resolve_sui_gas_limit( let mut sui_payload_overhead = tokens_len * SUI_TOKEN_TRANSFER_DATA_OVERHEAD; + assert!(receiver.length() == 32, EInvalid32BytesAddress); let receiver_uint = eth_abi::decode_u256_value(receiver); if (receiver_uint == 0) { assert!(receiver_object_ids_length == 0, ETooManySuiExtraArgsReceiverObjectIds); diff --git a/contracts/ccip/mock_ccip_v2/fee_quoter.move b/contracts/ccip/mock_ccip_v2/fee_quoter.move index 5f6850ea..6e36697e 100644 --- a/contracts/ccip/mock_ccip_v2/fee_quoter.move +++ b/contracts/ccip/mock_ccip_v2/fee_quoter.move @@ -937,6 +937,7 @@ fun resolve_sui_gas_limit( let mut sui_payload_overhead = tokens_len * SUI_TOKEN_TRANSFER_DATA_OVERHEAD; + assert!(receiver.length() == 32, EInvalid32BytesAddress); let receiver_uint = eth_abi::decode_u256_value(receiver); if (receiver_uint == 0) { assert!(receiver_object_ids_length == 0, ETooManySuiExtraArgsReceiverObjectIds); From d5d6240bab71fa6c6d0e90f983bfa57096ea1fd5 Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Fri, 5 Jun 2026 16:17:00 -0400 Subject: [PATCH 4/4] fix --- contracts/ccip/ccip/sources/fee_quoter.move | 7 ++++--- contracts/ccip/mock_ccip_v2/fee_quoter.move | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/contracts/ccip/ccip/sources/fee_quoter.move b/contracts/ccip/ccip/sources/fee_quoter.move index da9fc178..8f9de2ff 100644 --- a/contracts/ccip/ccip/sources/fee_quoter.move +++ b/contracts/ccip/ccip/sources/fee_quoter.move @@ -81,12 +81,13 @@ const SUI_MESSAGING_ACCOUNTS_OVERHEAD: u64 = 1; /// The size of each SUI account address in bytes. const SUI_ACCOUNT_BYTE_SIZE: u64 = 32; -/// The expected static payload size of a token transfer on SUI. +/// The expected static payload size of a token transfer when BCS encoded on SUI. +/// TokenPool extra_data contents are dynamic and billed via dest_bytes_overhead. const SUI_TOKEN_TRANSFER_DATA_OVERHEAD: u64 = - (4 + 32) // source_pool + (1 + 32) // source_pool_address: ULEB128 length + 32 bytes + 32 // dest_token_address + 4 // dest_gas_amount - + 4 // extra_data length + + 1 // extra_data: ULEB128 length for empty vector + 32; // amount const MAX_U64: u256 = 18446744073709551615; diff --git a/contracts/ccip/mock_ccip_v2/fee_quoter.move b/contracts/ccip/mock_ccip_v2/fee_quoter.move index 6e36697e..e5ae88f8 100644 --- a/contracts/ccip/mock_ccip_v2/fee_quoter.move +++ b/contracts/ccip/mock_ccip_v2/fee_quoter.move @@ -81,12 +81,13 @@ const SUI_MESSAGING_ACCOUNTS_OVERHEAD: u64 = 1; /// The size of each SUI account address in bytes. const SUI_ACCOUNT_BYTE_SIZE: u64 = 32; -/// The expected static payload size of a token transfer on SUI. +/// The expected static payload size of a token transfer when BCS encoded on SUI. +/// TokenPool extra_data contents are dynamic and billed via dest_bytes_overhead. const SUI_TOKEN_TRANSFER_DATA_OVERHEAD: u64 = - (4 + 32) // source_pool + (1 + 32) // source_pool_address: ULEB128 length + 32 bytes + 32 // dest_token_address + 4 // dest_gas_amount - + 4 // extra_data length + + 1 // extra_data: ULEB128 length for empty vector + 32; // amount const MAX_U64: u256 = 18446744073709551615;