Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/rust-wasm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
- "contracts/**"
- "Cargo.toml"
- "Cargo.lock"
- "scripts/check_bridge_abi.sh"
- ".github/workflows/rust-wasm.yml"
push:
branches:
Expand All @@ -14,6 +15,7 @@ on:
- "contracts/**"
- "Cargo.toml"
- "Cargo.lock"
- "scripts/check_bridge_abi.sh"
- ".github/workflows/rust-wasm.yml"

jobs:
Expand Down Expand Up @@ -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
198 changes: 198 additions & 0 deletions contracts/bridge-compat/abi_snapshot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
{
"bridge": {
"functions": [
"pub fn admin(env: Env) -> Option<Address>",
"pub fn confirm_transfer( env: Env, transfer_id: u64, ) -> Result<(), BridgeError>",
"pub fn default_provider(env: Env) -> Option<u32>",
"pub fn estimate_transfer( env: Env, amount: i128, dest_chain: u32, provider_id: Option<u32>, ) -> Result<TransferEstimate, BridgeError>",
"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<BridgeProvider>",
"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<u32>, ) -> Result<u32, BridgeError>",
"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<Address>",
"pub fn transfer(env: Env, transfer_id: u64) -> Option<BridgeTransfer>",
"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<u32>, ) -> Result<u64, BridgeError>",
"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<i128, VaultError>",
"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<Address>",
"pub fn admin_param_change_interval(env: Env) -> u64",
"pub fn admin_proposal(env: Env, proposal_id: u32) -> Option<admin::AdminProposal>",
"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<DepositEntry>, ) -> Result<BatchDepositResult, VaultError>",
"pub fn benji_strategy(env: Env) -> Address",
"pub fn calculate_assets(env: Env, shares: i128) -> Result<i128, VaultError>",
"pub fn calculate_shares(env: Env, assets: i128) -> Result<i128, VaultError>",
"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<i128, VaultError>",
"pub fn diagnostics(env: Env) -> Result<telemetry::VaultDiagnostics, VaultError>",
"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<Address>",
"pub fn emergency_approver_secondary(env: Env) -> Option<Address>",
"pub fn emergency_dispute_window(env: Env) -> u64",
"pub fn emergency_proposal(env: Env, proposal_id: u32) -> Option<emergency::EmergencyProposal>",
"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<i128, VaultError>",
"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<i128, VaultError>",
"pub fn governance_signers(env: Env) -> Option<Vec<Address>>",
"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<PauseReason>",
"pub fn pause_with_role( env: Env, caller: Address, reason: PauseReason, ) -> Result<(), VaultError>",
"pub fn pauser(env: Env) -> Option<Address>",
"pub fn pending_admin(env: Env) -> Option<Address>",
"pub fn pending_fee_bps_change(env: Env) -> Option<timelock::PendingI128Change>",
"pub fn pending_fee_curve_change(env: Env) -> Option<fee_curve::PendingFeeCurveChange>",
"pub fn pending_price_oracle_change(env: Env) -> Option<timelock::PendingAddressChange>",
"pub fn pending_treasury_change(env: Env) -> Option<timelock::PendingAddressChange>",
"pub fn per_user_cap(env: Env) -> i128",
"pub fn performance_fee_bps(env: Env) -> i128",
"pub fn performance_incentive_pool(env: Env) -> Option<Address>",
"pub fn price_oracle(env: Env) -> Option<Address>",
"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<i128>, wasm_hash: Option<BytesN<32>>, ) -> Result<u32, VaultError>",
"pub fn queue_fee_bps_change(env: Env, new_bps: i128) -> Result<u64, VaultError>",
"pub fn queue_fee_curve_change(env: Env, curve: fee_curve::FeeCurve) -> Result<u64, VaultError>",
"pub fn queue_price_oracle_change(env: Env, oracle: Address) -> Result<u64, VaultError>",
"pub fn queue_treasury_change(env: Env, treasury: Address) -> Result<u64, VaultError>",
"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<Address>, ) -> 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<Address>, 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<Address>) -> 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<u64>, page_size: u32, ) -> Result<ShipmentPage, VaultError>",
"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<Address>",
"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<u64>",
"pub fn strategy_registration_state(env: Env, strategy: Address) -> Option<u32>",
"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<i128, VaultError>",
"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<Address>",
"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<i128, VaultError>",
"pub fn withdrawal_cooldown(env: Env) -> u64",
"pub fn withdrawal_queue_length(env: Env) -> u64"
]
}
}
126 changes: 126 additions & 0 deletions scripts/check_bridge_abi.sh
Original file line number Diff line number Diff line change
@@ -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
Loading