diff --git a/.github/workflows/rust-wasm.yml b/.github/workflows/rust-wasm.yml
index 5fcbe7ad..1b21c263 100644
--- a/.github/workflows/rust-wasm.yml
+++ b/.github/workflows/rust-wasm.yml
@@ -6,6 +6,7 @@ on:
- "contracts/**"
- "Cargo.toml"
- "Cargo.lock"
+ - "scripts/check_bridge_abi.sh"
- ".github/workflows/rust-wasm.yml"
push:
branches:
@@ -14,6 +15,7 @@ on:
- "contracts/**"
- "Cargo.toml"
- "Cargo.lock"
+ - "scripts/check_bridge_abi.sh"
- ".github/workflows/rust-wasm.yml"
jobs:
@@ -177,3 +179,17 @@ jobs:
name: testnet-deployment
path: deployment.json
retention-days: 7
+
+ bridge-abi:
+ name: Bridge ABI compatibility
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ # Issue #1305: the bridge-compat contract must keep its ABI shape and
+ # event signatures stable across vault upgrades. The checker is pure
+ # text analysis, so it runs without a Rust toolchain � and therefore
+ # gates even while the workspace does not compile.
+ - name: Verify bridge-compat ABI against snapshot
+ run: bash scripts/check_bridge_abi.sh
diff --git a/contracts/bridge-compat/abi_snapshot.json b/contracts/bridge-compat/abi_snapshot.json
new file mode 100644
index 00000000..7e2f9138
--- /dev/null
+++ b/contracts/bridge-compat/abi_snapshot.json
@@ -0,0 +1,198 @@
+{
+ "bridge": {
+ "functions": [
+ "pub fn admin(env: Env) -> Option
",
+ "pub fn confirm_transfer( env: Env, transfer_id: u64, ) -> Result<(), BridgeError>",
+ "pub fn default_provider(env: Env) -> Option",
+ "pub fn estimate_transfer( env: Env, amount: i128, dest_chain: u32, provider_id: Option, ) -> Result",
+ "pub fn fail_transfer( env: Env, transfer_id: u64, ) -> Result<(), BridgeError>",
+ "pub fn initialize(env: Env, admin: Address, token: Address) -> Result<(), BridgeError>",
+ "pub fn provider(env: Env, provider_id: u32) -> Option",
+ "pub fn provider_count(env: Env) -> u32",
+ "pub fn register_provider( env: Env, name: String, kind: BridgeProviderKind, endpoint: Address, fee_bps: i128, max_transfer: i128, supported_chains: Vec, ) -> Result",
+ "pub fn set_default_provider( env: Env, provider_id: u32, ) -> Result<(), BridgeError>",
+ "pub fn set_provider_enabled( env: Env, provider_id: u32, enabled: bool, ) -> Result<(), BridgeError>",
+ "pub fn set_provider_fee( env: Env, provider_id: u32, fee_bps: i128, ) -> Result<(), BridgeError>",
+ "pub fn set_transfer_limits( env: Env, limits: TransferLimits, ) -> Result<(), BridgeError>",
+ "pub fn token(env: Env) -> Option",
+ "pub fn transfer(env: Env, transfer_id: u64) -> Option",
+ "pub fn transfer_limits(env: Env) -> TransferLimits",
+ "pub fn transfer_out( env: Env, sender: Address, recipient: Bytes, amount: i128, dest_chain: u32, provider_id: Option, ) -> Result",
+ "pub fn user_transfer_count(env: Env, user: Address) -> i128"
+ ],
+ "events": [
+ "brgadd",
+ "brgdef",
+ "brgdone",
+ "brgfail",
+ "brgout",
+ "brgtog"
+ ]
+ },
+ "vault": {
+ "functions": [
+ "pub fn accept_admin(env: Env, proposal_id: u32) -> Result<(), VaultError>",
+ "pub fn accrue_korean_debt_yield(env: Env) -> Result",
+ "pub fn accrue_yield(env: Env, amount: i128) -> Result<(), VaultError>",
+ "pub fn activate_strategy_registration(env: Env, strategy: Address) -> Result<(), VaultError>",
+ "pub fn add_shipment( env: Env, shipment_id: u64, status: ShipmentStatus, ) -> Result<(), VaultError>",
+ "pub fn admin(env: Env) -> Option",
+ "pub fn admin_param_change_interval(env: Env) -> u64",
+ "pub fn admin_proposal(env: Env, proposal_id: u32) -> Option",
+ "pub fn balance(env: Env, user: Address) -> i128",
+ "pub fn balance_at(env: Env, user: Address, checkpoint_id: u32) -> i128",
+ "pub fn batch_deposit( env: Env, relayer: Address, entries: Vec, ) -> Result",
+ "pub fn benji_strategy(env: Env) -> Address",
+ "pub fn calculate_assets(env: Env, shares: i128) -> Result",
+ "pub fn calculate_shares(env: Env, assets: i128) -> Result",
+ "pub fn cancel_admin_rotation(env: Env, proposal_id: u32) -> Result<(), VaultError>",
+ "pub fn cancel_emergency_action(env: Env, proposal_id: u32) -> Result<(), VaultError>",
+ "pub fn cancel_fee_bps_change(env: Env) -> Result<(), VaultError>",
+ "pub fn cancel_fee_curve_change(env: Env) -> Result<(), VaultError>",
+ "pub fn cancel_price_oracle_change(env: Env) -> Result<(), VaultError>",
+ "pub fn cancel_treasury_change(env: Env) -> Result<(), VaultError>",
+ "pub fn claim_all_fees(env: Env) -> Result<(), VaultError>",
+ "pub fn claim_fees(env: Env) -> Result<(), VaultError>",
+ "pub fn configure_korean_strategy(env: Env, strategy: Address)",
+ "pub fn confirm_emergency_action( env: Env, confirmer: Address, proposal_id: u32, ) -> Result<(), VaultError>",
+ "pub fn contract_version(env: Env) -> String",
+ "pub fn create_checkpoint(env: Env) -> u32",
+ "pub fn create_strategy_proposal(env: Env, proposer: Address, strategy: Address) -> u32",
+ "pub fn deposit(env: Env, user: Address, amount: i128) -> Result",
+ "pub fn diagnostics(env: Env) -> Result",
+ "pub fn diagnostics_enabled(env: Env) -> bool",
+ "pub fn divest(env: Env, amount: i128)",
+ "pub fn effective_fee_bps(env: Env) -> i128",
+ "pub fn emergency_approver_primary(env: Env) -> Option",
+ "pub fn emergency_approver_secondary(env: Env) -> Option",
+ "pub fn emergency_dispute_window(env: Env) -> u64",
+ "pub fn emergency_proposal(env: Env, proposal_id: u32) -> Option",
+ "pub fn execute_fee_bps_change(env: Env) -> Result<(), VaultError>",
+ "pub fn execute_fee_curve_change(env: Env) -> Result<(), VaultError>",
+ "pub fn execute_price_oracle_change(env: Env) -> Result<(), VaultError>",
+ "pub fn execute_strategy_proposal(env: Env, proposal_id: u32) -> Result<(), VaultError>",
+ "pub fn execute_treasury_change(env: Env) -> Result<(), VaultError>",
+ "pub fn execute_withdrawal(env: Env, user: Address) -> Result",
+ "pub fn fee_bps(env: Env) -> i128",
+ "pub fn fee_curve(env: Env) -> fee_curve::FeeCurve",
+ "pub fn finalize_governance_migration(env: Env)",
+ "pub fn gasless_deposit( env: Env, relayer: Address, user: Address, amount: i128, ) -> Result",
+ "pub fn governance_signers(env: Env) -> Option>",
+ "pub fn governance_threshold(env: Env) -> u32",
+ "pub fn idle_total_assets(env: Env) -> i128",
+ "pub fn initialize(env: Env, admin: Address, token: Address) -> Result<(), VaultError>",
+ "pub fn invest(env: Env, amount: i128) -> Result<(), VaultError>",
+ "pub fn is_gasless_relayer(env: Env, relayer: Address) -> bool",
+ "pub fn is_oracle_enabled(env: Env) -> bool",
+ "pub fn is_paused(env: Env) -> bool",
+ "pub fn is_performance_fee_enabled(env: Env) -> bool",
+ "pub fn is_relayer(env: Env, relayer: Address) -> bool",
+ "pub fn is_strategy_whitelisted(env: Env, strategy: Address) -> bool",
+ "pub fn korean_strategy(env: Env) -> Address",
+ "pub fn large_withdrawal_threshold(env: Env) -> i128",
+ "pub fn max_batch_size(env: Env) -> u32",
+ "pub fn max_conc_bps(env: Env) -> i128",
+ "pub fn max_deploy_bps(env: Env) -> i128",
+ "pub fn max_vault_tvl(env: Env) -> i128",
+ "pub fn metadata(env: Env) -> ContractMetadata",
+ "pub fn migrate_storage(env: Env, target_version: u32) -> Result<(), VaultError>",
+ "pub fn min_deposit(env: Env) -> i128",
+ "pub fn min_liquidity_buffer(env: Env) -> i128",
+ "pub fn oracle_heartbeat(env: Env) -> u64",
+ "pub fn pause(env: Env, reason: PauseReason)",
+ "pub fn pause_reason(env: Env) -> Option",
+ "pub fn pause_with_role( env: Env, caller: Address, reason: PauseReason, ) -> Result<(), VaultError>",
+ "pub fn pauser(env: Env) -> Option",
+ "pub fn pending_admin(env: Env) -> Option",
+ "pub fn pending_fee_bps_change(env: Env) -> Option",
+ "pub fn pending_fee_curve_change(env: Env) -> Option",
+ "pub fn pending_price_oracle_change(env: Env) -> Option",
+ "pub fn pending_treasury_change(env: Env) -> Option",
+ "pub fn per_user_cap(env: Env) -> i128",
+ "pub fn performance_fee_bps(env: Env) -> i128",
+ "pub fn performance_incentive_pool(env: Env) -> Option",
+ "pub fn price_oracle(env: Env) -> Option",
+ "pub fn process_withdrawal_queue(env: Env, max_entries: u32) -> u32",
+ "pub fn propose_admin(env: Env, new_admin: Address) -> u32",
+ "pub fn propose_emergency_action( env: Env, initiator: Address, kind: emergency::EmergencyActionKind, pause_reason_code: u32, divest_amount: Option, wasm_hash: Option>, ) -> Result",
+ "pub fn queue_fee_bps_change(env: Env, new_bps: i128) -> Result",
+ "pub fn queue_fee_curve_change(env: Env, curve: fee_curve::FeeCurve) -> Result",
+ "pub fn queue_price_oracle_change(env: Env, oracle: Address) -> Result",
+ "pub fn queue_treasury_change(env: Env, treasury: Address) -> Result",
+ "pub fn rebalance( env: Env, from_strategy: Address, to_strategy: Address, amount: i128, min_divest_value: i128, min_invest_value: i128, ) -> Result<(), VaultError>",
+ "pub fn record_strategy_heartbeat(env: Env, strategy: Address) -> Result<(), VaultError>",
+ "pub fn register_strategy(env: Env, strategy: Address) -> Result<(), VaultError>",
+ "pub fn report_benji_yield(env: Env, strategy: Address, amount: i128) -> Result<(), VaultError>",
+ "pub fn require_governance_threshold( env: Env, approvals: Vec, ) -> Result<(), VaultError>",
+ "pub fn retire_strategy(env: Env, strategy: Address) -> Result<(), VaultError>",
+ "pub fn sensitive_timelock_delay(env: Env) -> u64",
+ "pub fn set_admin_param_change_interval(env: Env, seconds: u64) -> Result<(), VaultError>",
+ "pub fn set_dao_threshold(env: Env, threshold: i128) -> Result<(), VaultError>",
+ "pub fn set_diagnostics_enabled(env: Env, enabled: bool) -> Result<(), VaultError>",
+ "pub fn set_emergency_approvers(env: Env, primary: Address, secondary: Address)",
+ "pub fn set_emergency_dispute_window(env: Env, seconds: u64)",
+ "pub fn set_gasless_relayer(env: Env, relayer: Address, approved: bool)",
+ "pub fn set_governance_signers( env: Env, signers: Vec, threshold: u32, migration_deadline: u64, ) -> Result<(), VaultError>",
+ "pub fn set_large_withdrawal_threshold(env: Env, threshold: i128) -> Result<(), VaultError>",
+ "pub fn set_max_batch_size(env: Env, size: u32) -> Result<(), VaultError>",
+ "pub fn set_max_conc_bps(env: Env, bps: i128) -> Result<(), VaultError>",
+ "pub fn set_max_deployed_bps(env: Env, bps: i128) -> Result<(), VaultError>",
+ "pub fn set_max_vault_tvl(env: Env, tvl: i128) -> Result<(), VaultError>",
+ "pub fn set_min_deposit(env: Env, new_min: i128) -> Result<(), VaultError>",
+ "pub fn set_min_liquidity_buffer(env: Env, new_buffer: i128) -> Result<(), VaultError>",
+ "pub fn set_oracle_enabled(env: Env, enabled: bool) -> Result<(), VaultError>",
+ "pub fn set_oracle_heartbeat(env: Env, seconds: u64) -> Result<(), VaultError>",
+ "pub fn set_pauser(env: Env, pauser: Option) -> Result<(), VaultError>",
+ "pub fn set_per_user_cap(env: Env, cap: i128) -> Result<(), VaultError>",
+ "pub fn set_performance_fee_bps(env: Env, bps: i128) -> Result<(), VaultError>",
+ "pub fn set_performance_fee_enabled( env: Env, enabled: bool, ) -> Result<(), VaultError>",
+ "pub fn set_performance_incentive_pool( env: Env, pool: Address, ) -> Result<(), VaultError>",
+ "pub fn set_relayer(env: Env, relayer: Address, approved: bool)",
+ "pub fn set_sensitive_timelock_delay(env: Env, seconds: u64) -> Result<(), VaultError>",
+ "pub fn set_strategy(env: Env, strategy: Address) -> Result<(), VaultError>",
+ "pub fn set_strategy_cap(env: Env, strategy: Address, cap: i128)",
+ "pub fn set_strategy_heartbeat(env: Env, seconds: u64)",
+ "pub fn set_strategy_risk_threshold( env: Env, strategy: Address, threshold: i128, ) -> Result<(), VaultError>",
+ "pub fn set_strategy_switch_cooldown(env: Env, seconds: u64) -> Result<(), VaultError>",
+ "pub fn set_stress_limits( env: Env, concentration_bps: i128, deployed_bps: i128, ) -> Result<(), VaultError>",
+ "pub fn set_stress_mode(env: Env, enabled: bool) -> Result<(), VaultError>",
+ "pub fn set_treasury_claim_quota(env: Env, epoch_duration: u64, max_claim_amount: i128)",
+ "pub fn set_withdrawal_cooldown(env: Env, seconds: u64) -> Result<(), VaultError>",
+ "pub fn share_price(env: Env) -> i128",
+ "pub fn shipment_ids_by_status( env: Env, status: ShipmentStatus, cursor: Option, page_size: u32, ) -> Result",
+ "pub fn simulate_emergency_unwind( env: Env, estimated_slippage_bps: i128, estimated_fee_bps: i128, ) -> emergency::EmergencyUnwindResult",
+ "pub fn snapshot_user_balance(env: Env, user: Address)",
+ "pub fn storage_key_registry(env: Env) -> storage_registry::ValidateRegistryResult",
+ "pub fn storage_version(env: Env) -> u32",
+ "pub fn strategy(env: Env) -> Option",
+ "pub fn strategy_cap(env: Env, strategy: Address) -> i128",
+ "pub fn strategy_heartbeat(env: Env) -> u64",
+ "pub fn strategy_last_heartbeat(env: Env, strategy: Address) -> Option",
+ "pub fn strategy_registration_state(env: Env, strategy: Address) -> Option",
+ "pub fn strategy_risk_threshold(env: Env, strategy: Address) -> i128",
+ "pub fn strategy_switch_cooldown(env: Env) -> u64",
+ "pub fn strategy_switch_cooldown_remaining(env: Env) -> u64",
+ "pub fn strategy_watermark(env: Env, strategy: Address) -> i128",
+ "pub fn stress_mode(env: Env) -> bool",
+ "pub fn token(env: Env) -> Address",
+ "pub fn total_assets(env: Env) -> Result",
+ "pub fn total_assets_at(env: Env, checkpoint_id: u32) -> i128",
+ "pub fn total_shares(env: Env) -> i128",
+ "pub fn total_shares_at(env: Env, checkpoint_id: u32) -> i128",
+ "pub fn treasury(env: Env) -> Option",
+ "pub fn treasury_balance(env: Env) -> i128",
+ "pub fn treasury_rollover_excess(env: Env) -> i128",
+ "pub fn unpause(env: Env)",
+ "pub fn unpause_with_role(env: Env, caller: Address) -> Result<(), VaultError>",
+ "pub fn update_shipment_status( env: Env, shipment_id: u64, new_status: ShipmentStatus, ) -> Result<(), VaultError>",
+ "pub fn upgrade(env: Env, new_wasm_hash: BytesN<32>)",
+ "pub fn user_deposit(env: Env, user: Address) -> i128",
+ "pub fn utilization_bps(env: Env) -> i128",
+ "pub fn vote_on_proposal( env: Env, voter: Address, proposal_id: u32, support: bool, weight: i128, ) -> Result<(), VaultError>",
+ "pub fn whitelist_strategy( env: Env, strategy: Address, approved: bool, ) -> Result<(), VaultError>",
+ "pub fn withdraw(env: Env, user: Address, shares: i128) -> Result",
+ "pub fn withdrawal_cooldown(env: Env) -> u64",
+ "pub fn withdrawal_queue_length(env: Env) -> u64"
+ ]
+ }
+}
diff --git a/scripts/check_bridge_abi.sh b/scripts/check_bridge_abi.sh
new file mode 100644
index 00000000..a956fca7
--- /dev/null
+++ b/scripts/check_bridge_abi.sh
@@ -0,0 +1,126 @@
+#!/bin/bash
+# Bridge-compat ABI shape check (issue #1305).
+#
+# Verifies that the bridge-compat contract's ABI (exported functions + event
+# topics) still matches the approved snapshot, and that the vault entry
+# points the bridge integration relies on are still present after upgrades.
+#
+# Pure text analysis: runs without compiling, so it also gates while the
+# workspace does not build. Regenerate the snapshot after an intentional
+# change with: bash scripts/check_bridge_abi.sh --update
+#
+# Exit status: 0 when the surface matches, 1 on drift (with a diff).
+
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+BRIDGE_SRC="$ROOT/contracts/bridge-compat/src"
+VAULT_SRC="$ROOT/contracts/vault/src"
+SNAPSHOT="$ROOT/contracts/bridge-compat/abi_snapshot.json"
+
+# Test-only sources never contribute to a contract ABI.
+is_test_source() {
+ case "$1" in
+ */tests/*|*/test.rs|*_tests.rs|*/fuzz_*.rs|*/deposit_withdraw_props.rs|*/benji_strategy.rs)
+ return 0 ;;
+ *) return 1 ;;
+ esac
+}
+
+# Emit normalized `name(params) -> Ret` signatures for every `pub fn` inside a
+# #[contractimpl] block found in the given files.
+#
+# The impl block ends at the first column-0 `}`: inside an impl, no other
+# top-level item can start, so that brace necessarily closes it. Brace
+# counting is deliberately avoided — string literals and comments routinely
+# contain unbalanced braces.
+extract_functions() {
+ for file in "$@"; do
+ is_test_source "$file" && continue
+ awk '
+ /#\[contractimpl\]/ { want_impl = 1; next }
+ want_impl && /^[[:space:]]*impl[[:space:]]/ { in_impl = 1; want_impl = 0; next }
+ in_impl && /^\}/ { in_impl = 0; next }
+ in_impl && /^[[:space:]]*pub[[:space:]]+fn[[:space:]]/ {
+ sig = $0
+ while (sig !~ /\{/) {
+ if ((getline nextline) <= 0) break
+ sig = sig " " nextline
+ }
+ sub(/\{.*$/, "", sig)
+ gsub(/[[:space:]]+/, " ", sig)
+ gsub(/^ | $/, "", sig)
+ print sig
+ }
+ ' "$file"
+ done | sort -u
+}
+
+# Emit every symbol_short! topic used by non-test sources.
+extract_events() {
+ for file in "$@"; do
+ is_test_source "$file" && continue
+ grep -o 'symbol_short!("[^"]*")' "$file" 2>/dev/null || true
+ done | sed 's/symbol_short!("//; s/")//' | sort -u
+}
+
+json_array() {
+ # json_array < each line on stdin > prints "a", "b", ...
+ local first=1
+ while IFS= read -r line; do
+ [ -z "$line" ] && continue
+ if [ "$first" -eq 1 ]; then first=0; else printf ",\n"; fi
+ # identifiers/topics contain no quotes; escape backslashes defensively
+ printf ' "%s"' "$(printf '%s' "$line" | sed 's/\\/\\\\/g')"
+ done
+}
+
+generate_snapshot() {
+ shopt -s nullglob
+ local bridge_sources=("$BRIDGE_SRC"/*.rs)
+ local vault_sources=("$VAULT_SRC"/*.rs)
+ bridge_fns=$(extract_functions "${bridge_sources[@]}")
+ bridge_events=$(extract_events "${bridge_sources[@]}")
+ vault_fns=$(extract_functions "${vault_sources[@]}")
+ {
+ printf '{\n'
+ printf ' "bridge": {\n'
+ printf ' "functions": [\n'
+ printf '%s\n' "$bridge_fns" | json_array
+ printf '\n ],\n'
+ printf ' "events": [\n'
+ printf '%s\n' "$bridge_events" | json_array
+ printf '\n ]\n'
+ printf ' },\n'
+ printf ' "vault": {\n'
+ printf ' "functions": [\n'
+ printf '%s\n' "$vault_fns" | json_array
+ printf '\n ]\n'
+ printf ' }\n'
+ printf '}\n'
+ }
+}
+
+if [ "${1:-}" = "--update" ]; then
+ generate_snapshot > "$SNAPSHOT"
+ echo "snapshot written to $SNAPSHOT"
+ exit 0
+fi
+
+if [ ! -f "$SNAPSHOT" ]; then
+ echo "error: snapshot $SNAPSHOT does not exist; run with --update to create it" >&2
+ exit 1
+fi
+
+tmp_current="$(mktemp)"
+trap 'rm -f "$tmp_current"' EXIT
+generate_snapshot > "$tmp_current"
+
+if diff -u "$SNAPSHOT" "$tmp_current"; then
+ echo "bridge ABI matches snapshot"
+else
+ echo ""
+ echo "error: bridge-compat ABI surface drifted from contracts/bridge-compat/abi_snapshot.json" >&2
+ echo "If the change is intentional, regenerate with: bash scripts/check_bridge_abi.sh --update" >&2
+ exit 1
+fi