fix(xrp): gate withdrawal on destination-tag plumbing; reserve minimum; honest trust docs

Red review of 2dd8da86 found the real blocker: XRP outbound has no DestinationTag
(internal/txassembler/xrp.go builds Payment without one; Swap/API have no tag field),
so a payout to an exchange's pooled deposit address is credited to nobody and is
irreversible. Set XRP isWithdrawalEnabled:false on both nets until the tag is plumbed
end-to-end + X-addresses are rejected — deposit-only, matching the BTC precedent.

Also from the review:
- per-XRP minUSD:30 floor — a fresh per-swap deposit address must hold the >=10-XRP
  AccountRoot base reserve or it never confirms (token-denominated min is the robust
  follow-up; USD floor is the interim proxy).
- Correct the depositcheck + deposit_watcher trust-model docstrings: depositcheck.Check
  is the SOLE runtime gate that advances a swap to a release-pool payout — the single
  upstream RPC is load-bearing/trusted, not 'secondary verification'. Documents the
  systemic multi-RPC-quorum hardening follow-up so operators point only at trusted RPCs.

Inbound checkXRP is unchanged (red verified it solid). cmd/bridge + depositcheck green.
This commit is contained in:
2026-06-26 08:59:17 -07:00
parent 2dd8da86db
commit 517aced181
4 changed files with 49 additions and 20 deletions
+9 -3
View File
@@ -31,9 +31,15 @@ import (
// The MPC ceremony driver (Phase 4.6) is what picks up
// bridge_transfer_pending swaps from here.
//
// Trust model: depositcheck queries public source-chain RPCs. The
// result is best-effort. BridgeVM's own auditor (when deployed) is
// the authority for slashing; this layer is for UI / SDK polling.
// Trust model (IMPORTANT — load-bearing): this watcher is the sole
// automated forward producer of bridge_transfer_pending for deposit-address
// swaps, and that state directly drives a release-pool payout. So the single
// upstream RPC that depositcheck.Check consults IS trusted at runtime: an RPC
// that lies about a balance triggers a payout for a deposit that never landed.
// There is no runtime BridgeVM gate today (--resync-swaps is a one-shot boot
// reconciliation). Hardening this to a multi-RPC quorum / on-chain proof is a
// platform-wide follow-up; until then, operators MUST point depositcheck at
// trusted endpoints only.
// DepositChecker is the 1-method interface the watcher needs. Pulls
// the dependency to an interface so tests can swap in a fake without
+14 -5
View File
@@ -270,16 +270,20 @@ tokens:
isWithdrawalEnabled: false
# XRP settles on secp256k1 (same curve as the live ETH/BTC pipeline) — no
# mpcd FROST/Ed25519 gap. Inbound detection via rippled account_info
# (internal/depositcheck); outbound via the XRP release pool + assembler +
# rippled submit. Operator note: pre-fund each XRP release wallet with
# >= 10 XRP (AccountRoot base reserve) + per-payment fee before payouts.
# mpcd FROST/Ed25519 gap. Inbound detection via rippled account_info, last
# validated ledger (internal/depositcheck). Deposit-only for now, matching
# the BTC precedent: WITHDRAWAL IS GATED until the destination tag is plumbed
# end-to-end (API -> Swap -> XRPSpec -> Payment) and X-addresses are rejected
# at create time — a tag-less payout to an exchange's pooled deposit address
# is credited to nobody and is irreversible (red finding, c603f579 review).
# A fresh per-swap deposit address must also hold the >= 10-XRP AccountRoot
# base reserve, so the per-XRP minimum below stays above it.
- asset: XRP
name: XRP
decimals: 6
network: XRP_MAINNET
isDepositEnabled: true
isWithdrawalEnabled: true
isWithdrawalEnabled: false
limits:
minUSD: 10
@@ -288,3 +292,8 @@ limits:
USDT: {minUSD: 10, maxUSD: 100000}
USDC: {minUSD: 10, maxUSD: 100000}
DAI: {minUSD: 10, maxUSD: 100000}
# XRP per-swap deposit addresses must hold the >= 10-XRP AccountRoot base
# reserve to exist on-ledger; a sub-reserve deposit never confirms. Keep the
# minimum above the reserve (token-denominated minimum is the robust
# follow-up; this USD floor is the interim proxy).
XRP: {minUSD: 30, maxUSD: 100000}
+16 -6
View File
@@ -280,18 +280,28 @@ tokens:
# XRP settles on secp256k1 — the SAME curve as the live ETH/BTC pipeline, so
# it has no mpcd FROST/Ed25519 gap (unlike SOL/TON). Inbound detection
# (rippled account_info) is implemented in internal/depositcheck; the
# r-address is derived locally from the ecdsa_pub_key mpcd already returns.
# Outbound is wired (XRP release pool + assembler + rippled submit).
# Operator note: pre-fund each XRP release wallet with >= 10 XRP (the
# AccountRoot base reserve) + per-payment fee before enabling payouts.
# (rippled account_info, last validated ledger) is implemented in
# internal/depositcheck; the r-address derives locally from the ecdsa_pub_key
# mpcd already returns. Deposit-only for now, matching the BTC precedent:
# WITHDRAWAL IS GATED until the destination tag is plumbed end-to-end
# (API -> Swap -> XRPSpec -> Payment) and X-addresses are rejected at create
# time — a tag-less payout to an exchange's pooled deposit address is
# credited to nobody and is irreversible (red finding, c603f579 review).
# A fresh per-swap deposit address must also hold the >= 10-XRP AccountRoot
# base reserve, so the per-XRP minimum below stays above it.
- asset: XRP
name: XRP
decimals: 6
network: XRP_TESTNET
isDepositEnabled: true
isWithdrawalEnabled: true
isWithdrawalEnabled: false
limits:
minUSD: 1
maxUSD: 100000
per:
# XRP per-swap deposit addresses must hold the >= 10-XRP AccountRoot base
# reserve to exist on-ledger; a sub-reserve deposit is rejected network-side
# and never confirms. Keep the minimum above the reserve (a token-denominated
# minimum is the robust follow-up; this USD floor is the interim proxy).
XRP: {minUSD: 30, maxUSD: 100000}
+10 -6
View File
@@ -7,12 +7,16 @@
// and returns whether the balance at the address meets or exceeds the
// required amount.
//
// Trust model: each upstream RPC / REST endpoint is treated as
// best-effort transport. The authoritative "swap can advance to
// signing" decision is made by b-chain (BridgeVM); this package is
// only useful for ops diagnostics, latency monitoring, or as a
// secondary verification layer. Production swap-state advancement
// belongs to BridgeVM's own watcher, not to anything in here.
// Trust model (IMPORTANT — load-bearing): the deposit watcher
// (cmd/bridge/deposit_watcher.go) uses Check to advance a swap from
// user_deposit_pending to bridge_transfer_pending, which drives a
// release-pool payout. So the single upstream RPC per network IS
// trusted at runtime — an RPC that lies about a balance triggers a
// payout for a deposit that never landed. This is identical across
// every chain family here (one hardcoded endpoint, no quorum, no
// confirmation count). Hardening to a multi-RPC quorum / on-chain
// proof is a platform-wide follow-up; until then, only point this at
// trusted endpoints (RPCURLOverrides) in production.
//
// Returns from `Check`:
// - (true, nil) — confirmed deposit