Skip to content

feat(psbt): allow UnrestrictedRelayer to bypass change<min_input rule#49

Open
karim-en wants to merge 1 commit into
omni-mainfrom
relax-inputs-check
Open

feat(psbt): allow UnrestrictedRelayer to bypass change<min_input rule#49
karim-en wants to merge 1 commit into
omni-mainfrom
relax-inputs-check

Conversation

@karim-en
Copy link
Copy Markdown
Collaborator

Summary

Lets accounts with the UnrestrictedRelayer role submit withdraw PSBTs where a change output is >= min(inputs). The standard change < smallest_input rule still applies to everyone else.

Motivation

The off-chain consolidation/anchor-fill selector in bridge-sdk-rs#281 intentionally produces withdraws that consume many small UTXOs (fillers) with a single large anchor input, emitting one sizable change output. That's the shape the contract rule blocks: with min_input = filler_size, the rule would force the change into 100+ sub-min_change_amount pieces — no valid split exists, so consolidation withdraws are unbuildable today.

The relaxation only affects a UTXO-management invariant. All value-flow checks remain enforced for every caller:

  • min_change_amount and max_change_amount
  • change_amounts.all(|v| v < max_change_amount) when input_num > change_num (still caps any single change piece)
  • min_btc_gas_fee / max_btc_gas_fee
  • actual_received_amount window (max - min_change_amount .. max)

A misbehaving role holder can fragment / waste UTXOs (each input requires a Chain Signatures MPC sign), not steal funds. This matches the trust model the role already implies — UnrestrictedRelayer already gates the bridge's #[trusted_relayer] methods.

Changes

contracts/satoshi-bridge/src/psbt.rs

  • Add signer_is_unrestricted = acl_has_role(UnrestrictedRelayer, signer) check, hoisted out of the per-output loop (one ACL read per call instead of one per change output).
  • Bypass change < min_input_amount when the signer has UnrestrictedRelayer.
  • Update error message to reflect both branches.
  • Replace the cloned iterator + two passes for min and sum with a single fold over vutxos.

contracts/satoshi-bridge/tests/setup/context.rs

  • Add bridge_acl_revoke_role helper (mirrors bridge_acl_grant_role).

contracts/satoshi-bridge/tests/test_satoshi_bridge.rs

  • Extend test_utxo_passive_management to revoke alice's role, assert the rule still fires with the new message, then re-grant the role and assert the same PSBT succeeds.

Why signer_account_id()

In both code paths that reach check_withdraw_psbt:

  • ft_on_transfercheck_withdraw_psbt: signer is the end user (NEAR forwards the original tx signer across the nBTC → bridge promise chain). predecessor is the nBTC contract, so it would be wrong here.
  • withdraw_rbfcheck_withdraw_psbt: signer is the caller, which is already UnrestrictedRelayer-gated via #[trusted_relayer].

Behavior note

The min/sum refactor changes the empty-vutxos case from panic (via .min().unwrap()) to (u128::MAX, 0). The empty case isn't reachable in practice and is still caught downstream by the actual_received_amounts.len() == 1 check, gas_fee >= min_btc_gas_fee, and overflow-checks = true on the total_input - total_output subtraction.

Test plan

  • cargo test --features bitcoin -p satoshi-bridge --test test_satoshi_bridge — 15/15 passing, including test_utxo_passive_management exercising both branches.
  • make clippy-bitcoin clean.
  • Verify the anchor-fill SDK can now submit consolidation withdraws end-to-end against this contract.

@karim-en karim-en requested review from frolvanya and olga24912 May 16, 2026 20:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants