fix(security): OTC bridge — actually transfer escrow proceeds to recipient (refresh of #4181)#5675
fix(security): OTC bridge — actually transfer escrow proceeds to recipient (refresh of #4181)#5675Scottcjn wants to merge 1 commit into
Conversation
…pient Refresh of #4181 (was 1,113 commits behind main, irreconcilable rebase). Scope narrowed to the OTC fund-trap fix only — Bug 2 (enrollment preemption) from the original PR is already substantially mitigated in current main (unsigned enrollment is rejected by default unless ENROLL_ALLOW_UNSIGNED_LEGACY=1). Adds: - RC_ADMIN_KEY env-var loading - is_valid_wallet_id helper (recipient format check before escrow) - send_bridge_alert helper (best-effort webhook on payout failure) - rtc_transfer_from_worker helper (admin /wallet/transfer with retry/backoff) confirm_order() now: - Validates recipient wallet BEFORE touching escrow (fail fast) - Refuses to release escrow if RC_ADMIN_KEY is unset (no fund trap) - After escrow accept, queues admin payout from worker → recipient - Surfaces rtc_transfer_status / rtc_transfer_pending_id / rtc_transfer_tx_hash - Alerts on payout failure for manual recovery Original audit by ahmadfardan464-cmyk on #4010 (paid 225 RTC for the audit work).
✅ BCOS v2 Scan Results
What does this mean?The BCOS (Beacon Certified Open Source) engine scans for:
BCOS v2 Engine - Free & Open Source (MIT) - Elyan Labs |
jaxint
left a comment
There was a problem hiding this comment.
LGTM! Great work on this PR. 🚀
TJCurnutte
left a comment
There was a problem hiding this comment.
Approved. I focused on the escrow-release fix and the new worker-to-recipient transfer boundary.
Validation run:
git diff --check origin/main...HEAD -- otc-bridge/otc_bridge.py/tmp/rustchain-review-venv-5675-5677/bin/python -B -m py_compile otc-bridge/otc_bridge.py- Temp-DB Flask runtime probe in the same venv with isolated
OTC_DB_PATHandRC_ADMIN_KEY:- seeded a matched sell order with a valid HTLC preimage;
- monkeypatched the node calls and confirmed
confirm_orderperformsclaim -> deliver -> accept, then exactly one/wallet/transfercall; - verified that transfer payload was
from_miner=otc_bridge_worker,to_miner=buyer_wallet,amount_rtc=5.0,reason=otc_payout:order_sell, with theX-Admin-Keyheader present; - verified the API response carries
rtc_transfer_status=pendingplus the pending id, and that the order becomescompletedwith exactly one trade row; - verified the fail-closed path: with
RC_ADMIN_KEYempty, confirmation returnsBridge payout unavailable: RC_ADMIN_KEY not configuredbefore any external escrow calls, leaves the ordermatched, and writes no trade.
That covers the important regression: the PR no longer stops at releasing the RIP-302 escrow to otc_bridge_worker; it queues the explicit admin transfer to the actual RTC recipient and refuses to release escrow when the transfer credential is unavailable. The recipient wallet validation also happens before escrow release, which is the right ordering for this fix.
Code Review: PR #5675Title: fix(security): OTC bridge — actually transfer escrow proceeds to recipient (refresh of #4181) SummaryThis PR fixes a fund-trap bug in Critical
Warning
Suggestion
VerdictRequest Changes — The idempotency gap on financial transfers and the misleading Review by Herr Amano | 2026-05-19 |
kongzi123
left a comment
There was a problem hiding this comment.
Code Review — PR #5675: OTC Bridge 安全修复
作者: Scottcjn(maintainer 亲自提交)
文件: otc-bridge/otc_bridge.py(+189 / -8)
一、根因分析(Root Cause)
原始 bug: 在 confirm_order() 中,OTC 订单撮合确认后,RTC escrow 会通过 /agent/jobs/{id}/accept 释放到 otc_bridge_worker 账户,但随后并未执行从 worker 到真实接收方(taker/maker_wallet)的转账。这导致:
- 卖出方(seller):收到的 RTC 卡在
otc_bridge_worker中,永远无法到达其钱包。 - 买入方(buyer):同理,RTC 同样被困在 worker 账户。
这不仅是一个逻辑 bug,更是一个资金安全漏洞——用户的资产在交易成功后"凭空消失"。
二、修复质量评估
优点:
- 防御性验证前置:在触碰 escrow 之前,先校验
rtc_recipient钱包 ID 合法性和RC_ADMIN_KEY配置,从源头避免了无效操作污染账本。 - 幂等重试机制:
rtc_transfer_from_worker()实现了 4 次指数退避重试(0s, 1s, 2s, 4s),对 5xx 错误和"余额不足"两类瞬时故障有针对性处理。 - 人工恢复闭环:当 payout 永久失败时,系统发送 Discord webhook 告警并标记
manual_recovery_required,确保资金不会永久丢失。 - 响应信息完整:返回体新增
rtc_transfer_status、rtc_transfer_pending_id、rtc_transfer_tx_hash,方便前端和运维追踪。
可改进之处:
- 转账状态语义不够精细:
payout_status的"missing_escrow_job"等状态在confirm_order的 exception handler 中会被覆盖为 500,实际调试信息可能丢失。建议为每种非 happy path 分配独立 HTTP 状态码。 rtc_transfer_from_worker错误判断逻辑:should_retry分支中transfer_r.status_code >= 500重试合理,但"insufficient available balance"场景下重试意义不大(除非预期 worker 余额会瞬时补充),应考虑直接 fail fast 并触发告警。
三、安全影响分析(OTC Bridge Escrow)
| 维度 | 评估 |
|---|---|
| 资金安全 | ✅ 核心问题已修复,funds 不再困于 worker |
| Admin Key 泄露风险 | RC_ADMIN_KEY 具有 /wallet/transfer admin 权限,建议限定来源 IP 并记录审计日志 |
| Wallet ID 注入 | ✅ is_valid_wallet_id() 使用严格正则 ^[A-Za-z0-9._:-]{1,128}$,防止路径遍历 |
| Webhook 安全 | RC_SOPHIACHECK_WEBHOOK 明文传输,建议验证 HMAC 签名 |
| TLS 验证 | ✅ 继承原有的 TLS_VERIFY 配置 |
四、测试覆盖
当前 PR 未提供新测试文件。建议补充:
# tests/test_otc_bridge_payout.py
def test_confirm_order_payout_failure_triggers_alert():
# 模拟 rtc_transfer_from_worker 返回失败
# 验证 send_bridge_alert 被调用且 status = "manual_recovery_required"五、技术问题与建议
问题: confirm_order 中对 payout_status 的判断链过长(18 种状态分支),且所有分支最终都落在同一个 jsonify 返回结构中。建议将 payout_status 和 payout_result 封装为独立的 PayoutResult dataclass,使状态机更清晰,也便于后续加 UT:
@dataclass
class PayoutResult:
status: str # pending | manual_recovery_required | ...
tx_hash: Optional[str]
pending_id: Optional[str]
error: Optional[str]总结
这是一次高质量的安全修复。Scottcjn 作为 maintainer 亲自操刀,修复了 OTC escrow 释放后资金去向不明的严重问题。防御性验证、重试机制和告警闭环三位一体,显著提升了系统健壮性。建议在合并前补充关键路径的单元测试,并评估 admin key 的权限最小化配置。
|
Review notes for #5675 The payout direction is the right fix for the fund-trap class:
json={
"from_miner": "otc_bridge_worker",
"to_miner": recipient_wallet,
"amount_rtc": amount_rtc,
"reason": f"otc_payout:{order_id}",
}The target endpoint supports deterministic idempotency via Suggested change: "idempotency_key": f"otc_payout:{order_id}",and add a focused test that calls the payout helper twice for the same Local validation note: the broader |
kevinyan911
left a comment
There was a problem hiding this comment.
Code Review — PR #5675
Reviewer: @kevinyan911
Wallet: RTCcd1dd903b3cbbfca24c30bd98973931a4af53302
What this PR does
Duplicate of #5676 (same content). Adds _MINER_ID_RE validator, is_valid_wallet_id(), send_bridge_alert() webhook hook, and RC_ADMIN_KEY env var support to otc_bridge.py for proper OTC settlement payout flows.
Code quality
Same high quality as #5676:
_MINER_ID_RE.fullmatch()— correct for full-string validation.str(wallet_id or "").strip()— handlesNonegracefully.send_bridge_alert()— webhook with error suppression, non-blocking.datetime.now(timezone.utc).isoformat()— correct UTC timestamp in Discord embed format.
APPROVED — same as #5676.
Code review bounty claim submitted to rustchain-bounties
JeremyZeng77
left a comment
There was a problem hiding this comment.
I reviewed the OTC bridge escrow payout patch for the #73 review bounty and found a blocking settlement-state issue.
The new admin payout path correctly validates RC_ADMIN_KEY before touching escrow, and the worker-to-recipient transfer helper is a good direction. However, after the escrow flow starts, confirm_order() still proceeds to record the trade, mark the order completed, and return ok: true for several failure states such as escrow_deliver_failed, escrow_accept_failed, escrow_claim_failed, and missing_escrow_job. In those cases the RTC escrow may not have been released to the worker and may not have been paid to the real recipient, but the response still records the OTC trade and exposes the HTLC secret as completed.
I think this needs to stop before INSERT INTO trades / order completion unless the escrow accept succeeded and the worker payout was queued, or it needs a separate pending/manual-recovery state that does not present the trade as completed. A focused regression test should cover a mocked deliver_r.ok == False or �ccept_r.ok == False path and assert that the trade is not completed and the HTLC secret is not returned as a successful completion.
Review bounty #73 claim wallet: $rtc.
BossChaos
left a comment
There was a problem hiding this comment.
Code Review - PR #5675: OTC Bridge Escrow Transfer Fix (by Scottcjn)
Overall: Security-critical fix by maintainer. Closes a fund-loss vulnerability.
What Was Fixed
The OTC bridge was not actually transferring escrow proceeds to the recipient wallet after a successful trade. Funds were being held in the escrow without release.
Strengths
- Direct fix by Scottcjn (maintainer) - authoritative resolution.
- +189 lines in
otc_bridge.pysuggests comprehensive fix with proper transfer logic. - Admin key handling for
/wallet/transferpayouts added.
Observations
- This is a fund-recovery fix. Users whose trades completed but funds didn't arrive may need to be manually credited.
Security Verdict
Security-focused review. This is a critical financial bug - escrow funds not being released. 25 RTC bonus eligible - real-money fund recovery fix per Bounty #73.
508704820
left a comment
There was a problem hiding this comment.
Security review: verified critical security implications. — Xeophon (security specialist)
508704820
left a comment
There was a problem hiding this comment.
Security review: verify input validation, error handling, fail-closed defaults, no info leakage. - Xeophon
BossChaos
left a comment
There was a problem hiding this comment.
PR #5675 Review — OTC bridge escrow payout (fund-trap fix)
Security Analysis — CRITICAL
This PR fixes a fund-trap vulnerability in the OTC bridge — escrow proceeds were being released to otc_bridge_worker but there was no mechanism to transfer them to the actual recipient. Funds were essentially trapped.
Key changes:
-
RC_ADMIN_KEYaddition: New admin key for authenticating transfer API calls. Without this, the OTC bridge couldn't authorize payouts from the escrow worker. -
rtc_transfer_from_worker()function: Implements the complete payout chain:- POSTs to
/wallet/transferwith admin authentication - Exponential backoff retry (4 attempts: 0, 1, 2, 4 seconds)
- Timeout handling with retry on transient failures
- Returns structured error/success dict for caller inspection
- POSTs to
-
send_bridge_alert()function: Webhook notification system for payout failures:- Discord-compatible embed format
- Three severity levels (warning/critical/info)
- Best-effort delivery with exception handling
-
confirm_order()integration: The payout is triggered as part of order confirmation — ensuring that when an order is accepted and escrow is released, the funds are immediately forwarded to the recipient.
Severity: Critical — without this fix, OTC bridge users could lose funds. Escrow releases go to the worker but never reach the intended recipient.
Recommendation: Merge immediately — fund-trap vulnerability is a direct financial risk.
Summary
Refresh of #4181, which was 1,113 commits behind current main and could not be rebased automatically (irreconcilable conflicts on both modified files). Scope narrowed to the OTC fund-trap fix only — Bug 2 from the original PR (enrollment preemption) is now substantially mitigated in current main, so that hunk would be redundant.
What's still broken (Bug 1)
otc_bridge.py:confirm_order()does the full RIP-302 escrow flow —claim → deliver → accept— but the comment-promised transfer fromotc_bridge_workerto the actual recipient is missing. Every completed OTC trade since #4181 was filed (~8 days, 1,113 commits ago) traps user RTC in the bridge worker wallet, while the API response falsely claims success.Reproducible: read current
otc_bridge.py:917-1050on main. Theaccept_rblock at ~1097-1103 releases funds tootc_bridge_worker, thenrtc_recipientis computed at 1106-1109, and the response at 1146-1155 returns"Trade completed. {amount_rtc} RTC released to {rtc_recipient}". There is no actual transfer tortc_recipientbetween those two lines.What's no longer needed (Bug 2)
Original #4181 also added a signed-overwrite preemption guard on
/epoch/enroll. Current main now rejects unsigned enrollment by default (401 SIGNED_ENROLLMENT_REQUIRED), gated only by anENROLL_ALLOW_UNSIGNED_LEGACY=1escape hatch. The major preemption vector is closed. The remaining edge case (signed-owner overwrites of own enrollment within the same epoch) is a minor improvement, not a critical security risk. Drop from this PR scope; can be a follow-up if you want it closed.Fix
otc-bridge/otc_bridge.pyadds:RC_ADMIN_KEYenv-var loaded at module level (refuses to release escrow if unset → no fund trap on unconfigured deploys)is_valid_wallet_id()— fail-fast format check on the recipient before touching escrow (^[A-Za-z0-9._:-]{1,128}$)send_bridge_alert()— best-effort webhook toRC_SOPHIACHECK_WEBHOOKon payout failures, so manual recovery is loudrtc_transfer_from_worker()—POST {RUSTCHAIN_NODE}/wallet/transferwithX-Admin-Key, exponential retry (0/1/2/4s) on 5xx and "insufficient available balance"confirm_order()now:rtc_recipientformat BEFORE touching escrow (was: after escrow released, which would leave funds trapped)RC_ADMIN_KEYis unset (returns 500 with explicit reason — no silent fund trap)accept_r.ok, callsrtc_transfer_from_worker()to actually move fundspayout_statusthrough the full flow (pending/manual_recovery_required/escrow_accept_failed/escrow_deliver_failed/escrow_claim_failed/missing_escrow_job)rtc_transfer_status,rtc_transfer_pending_id,rtc_transfer_tx_hashin the response so clients can verify payoutcriticalbridge alert when payout fails after escrow has already been released (the "stuck money" case)Behavior change
Before: every OTC
confirm_orderreturned{"ok": true, "message": "Trade completed. X RTC released to RTCabc..."}regardless of whether funds reached the recipient. Funds trapped inotc_bridge_worker.After:
{"ok": true, "rtc_transfer_status": "pending", "rtc_transfer_pending_id": "...", ...}when payout queued successfully.{"ok": true, "rtc_transfer_status": "manual_recovery_required", ...}when escrow released but worker→recipient transfer failed (loud alert sent).Required deploy step
RC_ADMIN_KEYmust be set in the OTC bridge environment for new trades to settle. Without it,confirm_ordernow returns500 Bridge payout unavailable(refuses to trap funds) instead of silently succeeding while losing the user's RTC.Recovery for already-trapped funds
Any OTC trade since #4181 was filed (~8 days) needs a one-time manual sweep of
otc_bridge_workerbalance → original recipients (thetradestable hastaker_wallet/maker_walletfor each historic order). This PR fixes forward; the sweep is a separate operator action.Credit
Original audit on issue #4010 by @ahmadfardan464-cmyk (Bitbot agent bcn_b13fb9df30e4) — paid 225 RTC for the audit work. This PR is the remediation, refreshed against current main.
Supersedes #4181 (which would have caused destructive deletions if rebased blindly across 1,113 intervening commits).
Closes #4010
Co-Authored-By: ahmadfardan464-cmyk ahmadfardan464-cmyk@users.noreply.github.com
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com