witness: pre-sign approval guard with USD-based caps#316
Open
CoderZhi wants to merge 4 commits into
Open
Conversation
…SD caps Introduces an ApprovalGuard that gates witness signing on configurable USD limits, with Lark-based admin approval for transfers above the single-tx cap. Disabled by default; opt-in per cashier via config. - util/coingecko: cached price source for USD conversion - util/lark_card: interactive approval card sender + HMAC signature verify - witness/approval: guard core, decision lock with DB-fallback after restart - witness/approval_server: HTTP callback handler with replay protection - witness/recorder: SignedAmountSince, MarkTransferAwaitingApproval, ApproveTransferToReady, RejectTransfer - types: new TransferAwaitingApproval and TransferRejected statuses - tokencashierbase: integrate guard before signing; fail closed on evaluation errors Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Make the per-token coingeckoID config field optional. At witness startup,
unset ids are resolved via GET /coins/{platform}/contract/{token1}. Bridged
or otherwise unindexed tokens can still be handled by setting coingeckoID
explicitly as an override. Existing configs are unchanged.
- util/coingecko: ResolveIDByContract + ErrCoinGeckoIDNotFound sentinel
- cmd/witness/main.go:
- chain → CoinGecko platform map + native-gas id map (for zero-addr token)
- coingeckoResolver (in-process cache, one HTTP call per unique address)
- tokenMetasFor* consult resolver when CoinGeckoID is empty
- newPriceFeed + startPriceFeedRunner split so cashier wiring can run
between cache construction and runner start (price feed now uses the
union of explicit + auto-resolved ids)
- buildApprovalGuard error message clarifies that coingeckoID is now
an override, not a requirement
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previously the admin-approval flow flipped a row from `approval` straight back to `ready`, so the next signing tick re-ran guard.Check and the single-tx limit pushed the row back into `approval` again — the admin decision was silently undone. Introduce TransferApproved (8 chars, fits varchar(10)). ApproveTransfer now sets `approved`; the cashier loop skips guard.Check for that status and treats it like `ready` for signing/confirm. Rolling-window total keeps counting only post-sign statuses (pending/confirmed/settled), so the admin override does not leak into the window cap. Also drop the lingering MFA wording from comments in types.go and solrecorder.go — the actual flow is Lark interactive cards, not MFA. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
End-to-end coverage for the approval flow: tick 1 holds an over-limit transfer; flipping the status to `approved` (as Recorder.ApproveTransfer does in SQL) lets tick 2 sign it without re-running guard.Check. Guards against future regressions in the approval ↔ approved transition path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ApprovalGuardthat gates witness signing on a per-cashier rolling-window USD cap and a per-transfer USD cap, with Lark-based admin approve/reject for transfers above the single-tx cap.approval/priceFeedconfig blocks and per-cashierwindowValueLimit/singleTxValueLimit. Existing chain configs are unchanged.UPDATE ... WHERE status='approval'provides atomicity.cb.Cashieris used only for routing; SQLWHEREalways uses the guard's bound cashier key.Changes
witness/approval.go— guard core,Check,RequestApproval,Approve/Reject, nonce/replay protectionwitness/approval_server.go— HTTP callback handler with HMAC-SHA256 signature verify and replay protectionutil/lark_card.go— interactive approval card sender + signature verifierutil/coingecko.go— cached CoinGecko price source with refresh loop and max-agewitness/recorder.go— newSignedAmountSince,MarkTransferAwaitingApproval,ApproveTransferToReady,RejectTransferwitness/types.go—TransferAwaitingApproval,TransferRejectedstatuseswitness/tokencashierbase.go— integrate guard before signingcmd/witness/main.go— wire config, price feed, and approval serverTest plan
go test ./witness-service/...(72 tests across 18 packages pass)go build ./witness-service/...approvalstate, verify admin click still resolves it via DB-fallback path🤖 Generated with Claude Code