Skip to content

feat(custody): Fireblocks BYOK - #2455

Open
cutbow7 wants to merge 13 commits into
mainfrom
codex/fireblocks-byok-minimal
Open

cutbow7 wants to merge 13 commits into
mainfrom
codex/fireblocks-byok-minimal

Conversation

@cutbow7

@cutbow7 cutbow7 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Add beta-only Fireblocks BYOK so organizations can connect their own Fireblocks workspace and send SOL or USDC payments through SDP.

Connection and payments

  • Generate a dedicated RSA key on SDP servers and provide a downloadable CSR for registration in Fireblocks.
  • Accept the resulting API user ID, discover named vaults, and let users select their vaults.
  • Keep private keys encrypted and server-side, with tenant isolation and credential rotation support.
  • Submit native Fireblocks TRANSFER requests. Fireblocks handles approval, signing, network fees, and broadcasting.
  • Track transaction status and verify the returned vault, asset, recipient, and amounts before marking a payment confirmed.
  • Preserve existing managed Fireblocks and Privy behavior.

Minimal integration scope

  • Check destination whitelisting and transfer-policy eligibility without modifying Fireblocks configuration.
  • Show dismissible warnings that users can permanently suppress in their browser.
  • Exclude whitelist proposals, policy proposals, and automatic contact imports.
  • Enable Fireblocks BYOK only on beta and experimental release channels; managed Fireblocks remains available on stable.

Validation

  • 686 distinct tests passed across API, dashboard, provider, and live-test-runner safety suites.
  • API and dashboard TypeScript checks passed.
  • Module boundaries, migration compatibility, secret-reference serialization, API playground drift, and changed-file lint checks passed.
  • Security review covered tenant isolation, credential handling, release gates, submission idempotency, and settlement reconciliation. Fixed a managed-wallet routing regression and incomplete settlement validation.

Operational notes

  • Organizations must register the CSR and configure permissions, whitelisted destinations, approval rules, and designated signers in Fireblocks.
  • The source vault must hold SOL for transaction fees.
  • Ambiguous submission outcomes are reconciled without automatically resubmitting payments.
  • Live Fireblocks calls and deployed GCP secret infrastructure were not exercised in this review.

See docs/ops/fireblocks-byok.md for setup instructions and the detailed security review.


Security / SDLC §7

Risk tier: Critical — credential handling and fund movement.

Proposed ADR 0007 exception: retain native Fireblocks TRANSFER

I propose retaining Fireblocks native TRANSFER and obtaining explicit, named security-owner approval for the exception described below.

SDP checks the initiating principal’s live authorization, wallet scope, organization/project eligibility, and custody connection before submitting a transfer. Fireblocks subsequently controls approval, signing, and broadcasting. Our current integration has no execution callback that rechecks SDP authorization immediately before Fireblocks signs.

Consequently, a transfer submitted while access was valid can still execute after the initiating SDP key is revoked, the member loses access, the organization is suspended, or the connection is deactivated. Revocation prevents new SDP submissions, but does not withdraw an instruction already accepted by Fireblocks. An ambiguous submission outcome must also be treated as potentially accepted.

This differs from ADR 0007’s requirement to recheck authorization when an asynchronous signing request executes.

Proposed security boundary

Treat an accepted or potentially accepted Fireblocks transfer as an externally committed instruction:

  • SDP authorization must be valid when the transfer is submitted.
  • Fireblocks approval policies and designated signers govern subsequent execution.
  • Revoking SDP access does not guarantee cancellation of previously submitted transfers.
  • Incident response must include reviewing and rejecting/cancelling pending transfers in Fireblocks. Cancellation can race execution.

Automatic cancellation on SDP revocation is not currently implemented. Adding it would be a useful mitigation, but would not guarantee that revocation wins a race with signing.

Threat model and controls

Relevant threats include cross-tenant credential or vault substitution, credential disclosure, duplicate payments after ambiguous responses, incorrect settlement attribution, and execution after SDP authorization is revoked.

Controls include tenant-scoped encrypted credentials, human-only connection management, beta gating, live authorization before submission, Fireblocks approval policies, durable external transaction IDs, no automatic resubmission after uncertainty, exact settlement verification, and sanitized logging.

The revocation race remains an explicit residual risk requiring approval.

Dependency assessment and AI use

This change adds zod@4.6.5 to the custody package for provider-response validation and @solana/keychain-core@1.4.0 / @solana/keychain-fireblocks@1.4.0 as API test dependencies. These versions already exist in the workspace dependency graph; the dependency and lockfile changes remain part of the security review.

Codex assisted implementation, testing, and review remediation. Automated checks and AI-assisted review do not constitute independent human review or security approval.

Approval required before merge

Status: proposed; not yet security-approved.

Requesting named security-owner approval of both:

  1. The ADR 0007 exception accepting the execution semantics above.
  2. The fireblocks-native-transfer allowlist entry in scripts/check-value-movement.mjs.

Record the reviewer, approval date, approval-comment link, and explicitly accepted exposure in the ADR. Until that approval is recorded, the security review item remains open.

If this boundary is not acceptable, native sends should remain disabled until we implement a flow that can enforce fresh SDP authorization before signing.

@vercel

vercel Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
sdp-docs Ready Ready Preview Oct 21, 2026 7:12pm UTC
sdp-web Ready Ready Preview Oct 21, 2026 7:12pm UTC

Request Review

@greptile-apps

greptile-apps Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Critical impact] The PR reaches customer credentials and real payment execution through Fireblocks.

The two remaining repository requirements should be satisfied before merging.

Findings

  1. P2 RPC client bypasses shared scope ▶
  2. P2 Runner duplicates shared token identifier ▶

Summary

Adds project-scoped Fireblocks BYOK setup and native SOL/USDC payments.

  • Organizations can connect Fireblocks vaults to their SDP projects.
  • Connected Fireblocks vaults can send tracked SOL and USDC payments through SDP.
  • The payment page guides users through Fireblocks checks and shows transfer status.

Reviews (9) · Last reviewed commit: "fix(web): preserve payment polling after..." · Reviewed by Greptile

Comment thread apps/sdp-web/messages/fr/dashboard-custody.json Outdated
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit 01502f7.

Comment thread scripts/fireblocks-live.mjs Dismissed
Comment thread scripts/fireblocks-live.mjs Fixed
Comment thread scripts/fireblocks-live.mjs Fixed
Comment thread scripts/fireblocks-live.mjs Fixed
Comment thread packages/sdp-custody/src/provisioning/fireblocks-transfers.ts Outdated
Comment thread apps/sdp-api/src/services/payments/fireblocks-transfer.ts Outdated
Comment thread apps/sdp-api/src/routes/payments/mappers.ts Outdated

@keeganthomp keeganthomp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for fireblocks-transfer.ts:223 (ADR 0007 re-admission), :259 (429s wedge payments), :321 (swallowed settlement mismatch) and handlers.ts:953 (refusals return 200), plus the missing SDLC §7 tier, threat-model, dependency and AI-use lines; Greptile's eight items are still open.

});
if (!claimed) return currentRow(repo, row);
try {
await assertMoneyStartAdmitted(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ADR 0007 requires pending-approval signing to re-run decideMovement at execution. This admits once before the POST, so a key revoked or org suspended while Fireblocks awaits approval still pays out. Cancel open Fireblocks transactions on revocation, or amend ADR 0007 with security sign-off, which the value-movement allowlist entry needs too.

Comment thread apps/sdp-api/src/services/payments/fireblocks-transfer.ts Outdated
Comment thread apps/sdp-api/src/services/payments/fireblocks-transfer.ts Outdated
Comment thread apps/sdp-api/src/routes/payments/transfers/handlers.ts Outdated
Comment thread apps/sdp-api/src/services/payments/fireblocks-transfer.ts Outdated
Comment thread pnpm-lock.yaml Outdated
Comment thread packages/sdp-custody/src/provisioning/fireblocks-transfers.ts Outdated
Comment thread apps/sdp-api/src/services/custody/fireblocks-policy.service.ts Outdated
Comment thread apps/sdp-api/src/routes/internal-fireblocks/index.ts
Comment thread scripts/fireblocks-live.mjs Dismissed
Comment thread scripts/fireblocks-live.mjs Dismissed
Comment thread scripts/fireblocks-live.mjs Dismissed
Comment thread apps/sdp-api/src/routes/internal-custody/index.ts Outdated
Comment thread apps/sdp-api/src/services/domain/signing/custody-runtime-target.ts Outdated
Comment thread apps/sdp-web/src/app/dashboard/[projectId]/custody/connections/secret-field.tsx Outdated
Comment thread apps/sdp-api/src/services/payments/fireblocks-transfer.ts Outdated
@cutbow7

cutbow7 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor Author

human translation:

I propose we explicitly document and allow a race condition where someone can make a Fireblocks proposal to TRANSFER from within SDP, then, before the TRANSFER is approved in Fireblocks, you revoke the key that made the proposal in SDP... at this point, SDP doesn't do anything to attempt to prevent the transfer. I think that's okay and we should allow that behavior and document it.

Detailed AI Slop for the Curious of Mind:

Following up on the ADR 0007 re-admission concern: I propose retaining native Fireblocks TRANSFER and approving an explicit security exception, subject to named security-owner review.

The remaining behavior is:

  1. SDP verifies live authorization and submits the transfer.
  2. SDP access may subsequently be revoked while Fireblocks approval is pending.
  3. Fireblocks can still approve, sign, and execute that previously submitted transfer.

We have documented this limitation and implemented safeguards around submission, tenant isolation, idempotency, and settlement verification. We have not eliminated the revocation race or implemented automatic cancellation on SDP revocation.

The proposed boundary treats an accepted—or potentially accepted—Fireblocks request as an externally committed instruction. Revocation blocks new SDP submissions; incident response must separately address pending transfers in Fireblocks. Automatic cancellation could reduce exposure, but can race execution and would not guarantee compliance with the existing pre-sign authorization requirement.

Requesting explicit, named security-owner approval for both the ADR 0007 exception and the fireblocks-native-transfer value-movement allowlist entry. The approval should acknowledge the residual exposure and be recorded in the ADR with the reviewer, date, and comment link.

This comment proposes a decision; it does not record security approval. Please keep the security thread open until that decision is made. If the exception is declined, we should disable native sends until an enforceable pre-sign authorization flow is available.

@Windscar

Windscar commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

I propose we explicitly document and allow a race condition where someone can make a Fireblocks proposal to TRANSFER from within SDP, then, before the TRANSFER is approved in Fireblocks, you revoke the key that made the proposal in SDP... at this point, SDP doesn't do anything to attempt to prevent the transfer. I think that's okay and we should allow that behavior and document it.

Yeah I think this is a reasonable exception

@Windscar Windscar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work. The transfer recovery and settlement paths look sound, but credential rotation can leave imported vault wallets unusable.

Reviewed: Submission idempotency, reconciliation, provider responses, credential and database state transitions, tenant scope, release gates, dashboard flows, and relevant tests.

Required for approval:

  • Verify every attached Fireblocks vault before credential rotation or rollback; details inline.

Non-blocking:

  • Limit Fireblocks Pay choices to SOL and USDC; the wizard currently offers tokens the API rejects.

Comment thread apps/sdp-api/src/services/provider-credential-lifecycle.service.ts Outdated

@Windscar Windscar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work. One recovery path still needs to handle credentials that become unavailable after Fireblocks accepts a transfer.

Reviewed: Prior review threads and fixes; BYOK credential lifecycle, payment admission and idempotency, settlement reconciliation, dashboard flows, and relevant tests.

Required for approval:

  • Keep accepted Fireblocks transfers reconcilable after connection deactivation or a failed credential check; details inline.

Comment thread apps/sdp-api/src/services/domain/signing/custody-runtime-target.ts Outdated
Windscar
Windscar previously approved these changes Oct 21, 2026

@Windscar Windscar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. The reconciliation gap is fixed.

Reviewed: Connection and failed-check recovery, submission gating, intent binding, and the new dashboard and cron tests.

Comment on lines +222 to +228
await assertMoneyStartAdmitted(
env,
{ organizationId: row.organization_id, projectId: row.project_id },
{ surface: "http", movement: "payments.transfer", subjectId: row.id },
{ custodyWalletId: row.custody_wallet_id }
);
const result = await createFireblocksTransfer(runtime, prepared.access, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Disabled custody can still send

submitFireblocksTransfer uses credentials loaded by prepareFireblocksTransfer, before asset discovery, payment writes, and audit writes. If the wallet becomes inactive, the connection fails its health check, or credentials are retired during that work, assertMoneyStartAdmitted does not catch it. Its query checks organization and caller access, not custody state. The server can therefore submit a new payment after custody access was disabled.

Resolve custody access again immediately before the POST and check it against the saved intent. This happens before Fireblocks accepts the instruction, so it is separate from the documented post-acceptance exception.

How this was verified: The POST uses captured credentials, while its final access query does not read wallet, connection, or credential state.

Knowledge Base Used: Custody and transaction controls

Comment thread scripts/fireblocks-live.mjs Outdated
Comment on lines +221 to +224
assert(
wallet.status === "active" && wallet.isRuntimeExecutionAllowed === true,
"The Fireblocks connection must be active and enabled"
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Status recovery requires send access

The runner requires an active, execution-enabled wallet even in status mode. After a connection fails its health check or is deactivated, this assertion stops the runner before its transfer GET. Operators then cannot use the documented recovery command, although the API supports checking accepted transfers through retained credentials.

Apply this execution check only to modes that start new work.

Generate dedicated organization/project RSA credentials and a downloadable CSR. Guide API-user registration, discover named vaults, and import selected wallets with encrypted BYOK credentials and tenant isolation. Support credential rotation, rollback, deactivation and secret cleanup.

Use native Fireblocks TRANSFER for direct SOL and USDC payments. Resolve USDC by exact mint/network, persist one submission attempt with a stable external transaction ID, and reconcile approval/broadcast status without automatic resubmission. Show provider status, substatus and fee responsibility in the dashboard. Other signing paths retain solana-keychain RAW signing.

Validate custody, policy, idempotency, concurrent submission, uncertain outcomes, tenant boundaries, status tracking, dashboard and live-test runner behavior. API/web typechecks, module boundaries, migration compatibility and secret serialization checks pass. Verify the real Fireblocks asset catalogue and local mainnet UI without submitting a native payment.

Deploy migrations 0127 and 0128 before the BYOK runtime. Native payments require TRANSFER and destination-policy permission plus SOL for fees in the source vault. RAW permission is only required for separate custom signing and the optional signer check.
Validate provider boundaries and preserve credential identity through setup and
rotation. Return definitive payment refusals, log reconciliation failures, bound
unacknowledged recovery, and keep durable provider deduplication.

Remove unused proposal schema and privileged policy reads, repair setup pending
state and React Doctor findings, and harden the opt-in live runner.

Retain native TRANSFER with a proposed ADR 0007 exception that still requires
named security approval; no claim of approval or atomic revocation is made.
Preserve selected vault readiness after failed pagination and explicitly bind
credential rotation requests to their provider. Reuse shared input styling,
centralize transfer status mapping, and remove unchecked assertions and
nested UI branching.

Add regression tests for pagination failures and rotation provider mismatches,
and verify live-runner configuration and receipt data boundaries.
…tion

Keep COMPLETED create responses and detailed responses without a transaction
signature processing until a detailed read verifies the payment intent,
destination and exact settled amount. Make settlement verification an explicit
input to the shared status mapping and cover eventual recovery without another
transaction POST.

Make the CSR retry regression test control action completion so it cannot race
the parent transition when the next registration form mounts.
…ment

Verify each attached vault through solana-keychain before rotation or rollback,
compare its address with the stored wallet, and recheck the locked wallet set
before committing. Preserve the current credential on failure and keep transient
provider failures retryable. Each distinct vault requires one address GET,
including the reused primary result; no Solana RPC calls are added.

Limit both Pay layouts to SOL and canonical network USDC for Fireblocks BYOK,
using the same supported-mint predicate as the API. Clear unsupported selections
when switching wallets and reject stale selections before review.

Validation: 224 API tests; 3,586 dashboard tests; all 28 workspace typecheck tasks;
119 changed-file Biome checks; module boundaries, value movement, SecretRef
serialization, and React Doctor with no new findings. No live Fireblocks calls.
Keep main's recurring-payment expiry migration at 0152 and move the unchanged
Fireblocks credential-constraint migration to unused number 0154. The CSR table
does not depend on these constraints; the validation migration remains later.

Document how existing local PR databases can rename the migration-ledger entry
without replaying already-applied constraint changes or replacing payment data.
…vation

Separate submission health checks from intent-bound recovery access. Before
reading the retained active credential, verify the original connection,
provider account, vault and source address. New payments still require an
active healthy connection, and revoked credentials remain unavailable.

Cover dashboard and cron settlement after deactivation and failed checks,
blocked new submissions, mismatched intents, revoked credentials, and no
further provider reads after settlement. No extra RPC or provider requests:
each admitted poll still issues one Fireblocks GET and never resubmits.

Validation: 95 focused API tests, API typecheck, Biome, module boundaries,
value-movement and SecretRef serialization checks.
Resolve fresh custody access against the durable intent before the provider POST, fail definitive local refusals, and recover status without requiring an operational wallet. Cover custody transitions and receipt-bound read-only recovery.
Keep pending native payments refreshing while allowing ramps to use their existing live poller. Extract the refresh effect so the rebased transaction component stays within React Doctor complexity limits.

Ramp reads use one polling path instead of both the ramp poller and a redundant 15-second page refresh. Native payment polling remains one visible-page refresh every 15 seconds. Cover settlement, missing transfer details, ramp exclusion, hidden tabs, and unmount cleanup.

/** Recheck the exact approved message after slow custody signing; never silently re-sign it. */
export async function signWithFreshBlockhash<T>(input: {
rpc: SolanaRpc;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 RPC client bypasses shared scope

signWithFreshBlockhash takes an RPC client from its callers. The “RPC and fee payment” section of the code standards requires new code to read that client from the open cluster scope rather than pass it between functions.

Create the client with createClusterRpc() inside this helper and remove the rpc argument from both callers. This repository requirement must be satisfied before merging.

File Used: docs/contributing/code-standards.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

// biome-ignore lint/security/noSecrets: Public Solana mainnet genesis hash.
const MAINNET_GENESIS = "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d";
// biome-ignore lint/security/noSecrets: Public wrapped SOL mint address.
const SOL_MINT = "So11111111111111111111111111111111111111112";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Runner duplicates shared token identifier

fireblocks-live.mjs adds a hardcoded SOL_MINT. The TypeScript section of the code standards says token mints must come from @sdp/types/well-known-tokens and explicitly requires scripts to import shared values.

Use that shared value instead of keeping another copy. This repository requirement must be satisfied before merging.

File Used: docs/contributing/code-standards.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

This branch was successfully deployed

2 active deployments
Preview – sdp-web — 01502f7e Deployed Oct 21, 2026 by vercel[bot]
Preview – sdp-docs — 01502f7e Deployed Oct 21, 2026 by vercel[bot]
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.

4 participants