Fireblocks Withdrawals
A Fireblocks custody client needs collateral to leave CRX to a Fireblocks-whitelisted address — signed through the client's own console, with no rejected signature and no expired deadline. This page walks through that handshake.
The release mechanism is the gasless WithdrawIntent — the same one in Fund the Account (~6 min). What changes for a custody client is three things: how the signing happens (through a policy engine, not a browser key), how long the signature stays valid (long enough for a human to approve it), and what must be whitelisted (two places, not one). CRX interoperates with the client's custody; it does not rebuild it.
NoteThe client's custody controls stay with the client. Address cooldowns, 4-eyes approval, and velocity limits live in the Fireblocks Transaction Authorization Policy (TAP) — the rule engine that releases a signing request only after the client's approvers clear it. CRX enforces none of them and duplicates none of them. The CRX on-chain route is the mirror of the address already approved, not a second copy of the client's policy.
Vault or Safe signing?
A Fireblocks client connects in one of two shapes, and the two sign differently. The contract and the relayer verify both. The table below shows the two topologies side by side:
| Raw MPC vault | Fireblocks-fronted Gnosis Safe | |
|---|---|---|
party address is | the vault's EVM address (EOA-style) | the Safe contract address |
| Signature type | standard ECDSA, 65 bytes | ERC-1271 isValidSignature |
| On-chain verify | ECDSA branch | 1271 branch |
| Relayer verify | ECDSA recovery | ERC-1271 eth_call to the Safe |
| Status | works | works |
The contract verifies both, as does the relayer. For a Safe-custodied client, the relayer recovers no usable address from ECDSA, then calls the Safe's isValidSignature(bytes32,bytes) over the same EIP-712 digest and accepts on the magic value 0x1626ba7e. This needs no configuration; pick the topology in Fireblocks and the stack follows.
NoteA Safe must already be deployed on-chain. ERC-6492 — a signature from a counterfactual Safe not yet on-chain — is out of scope. A Fireblocks client's Safe is always deployed before it funds a position, leaving this case absent in practice. Fund first, sign second.
How is the intent signed?
Request eth_signTypedData_v4 over the WithdrawIntent through the connected Fireblocks provider. The fields and the domain are byte-identical to the on-chain typehash — the signed struct is the same one the contract verifies. Build the intent and request the signature as follows:
// WithdrawIntent — field order is the on-chain typehash, exactly.
const intent = {
party, // your vault or Safe address
token, // USDC
amount, // native units (USDC is 18 decimals here), as a bigint
recipient, // a destination whitelisted in BOTH places (see below)
nonce, // per-party, single-use
deadline, // unix seconds; 24h out unless overridden
};
const signature = await fireblocksProvider.request({
method: "eth_signTypedData_v4",
params: [
party,
JSON.stringify({
domain: { name: "CRX", version: "1", chainId: 84532, verifyingContract: CRX },
types: WITHDRAW_INTENT_TYPES,
primaryType: "WithdrawIntent",
message: intent,
}),
],
});Two connection paths reach the console; pick the one the ops team runs:
- Fireblocks Web3 provider — the EIP-1193 provider from the Fireblocks Web3 SDK, wired as the wallet the dApp talks to. The signing request lands in the console for policy approval.
- WalletConnect to the Fireblocks console — pair the CRX app to the Fireblocks workspace over WalletConnect; the
eth_signTypedData_v4request appears in the console as a pending message signature.
Either way, the request does not return a signature instantly: it waits in the TAP until an approver clears it. That latency is exactly why the deadline is long.
Why is the deadline 24 hours?
Custody approval is not instant, and a signature that expires before the approvers clear it is dead on arrival. A browser key signs in one second; a Fireblocks TAP can route a typed-message request to a human and take minutes to hours. A Safe collects owner signatures asynchronously. The old 1-hour window could expire after sign but before custody approved it — the signature lands valid but stale, the contract reverts Expired, and the re-sign falls into the same trap.
The standard signing window is 24 hours, configurable per deployment. The withdraw UI surfaces the chosen expiry: "Valid until {time}. Approve it in the custody console before then."
WarningApprove before the window closes. The intent carries itsdeadlinein the signature. If 24h passes before the approvers clear it, the relayer and contract both reject it asExpiredand the process starts over with a fresh nonce. Approve the pending request in the Fireblocks console well inside the window.
The window is deliberately bounded rather than infinite. An unbounded deadline is a standing signed cheque; 24h clears realistic approval latency while keeping the replay window short. The nonce still makes every signature single-use.
Why are there two whitelists?
Neither side trusts the other to vet a destination alone. A withdrawal to address R must be approved in two places — and both must say yes before any collateral moves. The table below shows the two whitelists and what each enforces:
| Whitelist | Owner | What it enforces |
|---|---|---|
| Fireblocks TAP | the client | The client's cooldown, 4-eyes, and velocity policy. CRX never sees it. |
| On-chain withdraw route | CRX operator | The party → recipient mapping the contract checks before release. |
This is defence in depth, not redundancy. The Fireblocks policy decides whether the client's treasury may ever send to R. The CRX on-chain route is the operator's mirror of that decision — CRX cannot hold the client's WHITELIST_ADMIN role, leaving the address approved in Fireblocks still in need of CRX to record it on-chain before the gasless path will release to it.
WarningThe recipient must also be a Fireblocks-internal address. The CRX route gate proves the recipient passed the CRX whitelist, not that it lives inside the client's custody. A withdrawal to an address that is not one of the client's own Fireblocks deposit addresses leaves the funds outside custody the moment they land. Withdraw to an address still held.
How is a destination added?
The client requests the destination from the UI, an operator mirrors it on-chain, and Re-check unlocks it. The on-chain route is never self-approved — that key belongs to CRX.
- In the withdraw panel, type the destination and click Request withdrawal address. The UI posts the candidate route to
POST /collateral/withdraw-route/request. The relayer records it durably, notifies the CRX operator, and returns a request id:
await fetch(`${RELAYER}/collateral/withdraw-route/request`, {
method: "POST",
headers: { "Content-Type": "application/json", "X-CRX-Address": party },
body: JSON.stringify({ party, recipient, label: "Acme treasury" }),
}).then((r) => r.json());
// → { request_id: "req_…", status: "pending", created_at: 1749470000 }- The CRX operator reviews the request and adds the route on-chain through the existing
POST /admin/withdraw-route(Bearer-gated, operator-only). The client does not call this. - Click Re-check in the panel. It polls
isWithdrawRoute(party, recipient)and unlocks the withdraw the moment the route lands.
The request is idempotent on (party, recipient): re-requesting an open route returns the same id, not a duplicate. Once both whitelists hold, the destination is available to withdraw to until either side removes it.
A worked withdrawal, end to end
The following steps walk through one withdrawal of free USDC from a Fireblocks-custodied account to a treasury address, front to back:
- Whitelist in Fireblocks. Add the destination to the TAP, clear its cooldown and approvals. This is the client's policy, on the client's side.
- Request the on-chain route. In the withdraw panel, Request withdrawal address for the same destination. The response carries a
request_id; statuspending. - Operator mirrors it. The CRX operator reviews and adds the on-chain route. The wait is typically minutes.
- Re-check. Click Re-check; the panel confirms the route is approved and enables the withdraw.
- Sign the intent. Enter the amount, click withdraw. The app builds the
WithdrawIntentwith adeadline24h out and requestseth_signTypedData_v4through the Fireblocks provider. - Approve in custody. The signing request appears in the Fireblocks console. The approvers clear it inside the 24h window.
- Relayer releases. The signed intent posts to
POST /collateral/withdraw. The relayer verifies the signature — ECDSA for a vault, ERC-1271 for a Safe — checks the route and deadline, pays the gas, and the contract releases the free balance to the recipient. The party signs; it never sends a transaction. - Confirm on-chain. Read
generalBalance(party, token)— it falls by the amount — and check the recipient on the explorer. See Read the Data (~4 min).
A repeat withdrawal to the same destination skips steps 1–4 because both whitelists already hold. Sign, approve, and release.
What the handshake delivers, and the branch
- A custody-native signing path.
eth_signTypedData_v4through Fireblocks, verified ECDSA or ERC-1271, whichever topology runs. - A deadline that outlasts approval. 24 hours, surfaced in the UI, bounded by the nonce.
- A real route channel. Request → operator approves → Re-check unlocks, mirroring the address whitelisted in Fireblocks.
The everyday taker view of the same withdrawal is Withdraw (~1 min). The gasless mechanism and the on-chain alternatives are in Fund the Account (~6 min).
Glossary
| Term | Meaning |
|---|---|
| Fireblocks | Institutional custody platform. Keys live in MPC; every transaction and message-signing passes a policy engine (TAP) before it is released. |
| MPC vault | A Fireblocks account whose private key is split across parties via multi-party computation. Its EVM address behaves like an EOA and produces standard ECDSA signatures. |
| TAP | Transaction Authorization Policy — Fireblocks' rule engine. Routes a signing or transfer request to approvers and applies cooldowns and limits before release. |
| ERC-1271 | Standard letting a contract validate a signature via isValidSignature(hash, sig), returning the magic value 0x1626ba7e. How a Gnosis Safe "signs". |
| EOA | Externally-owned account — a normal address controlled by one private key, verified by ECDSA recovery. |
| WithdrawIntent | The CRX EIP-712 struct (party, token, amount, recipient, nonce, deadline). The party signs it; the relayer pays gas to submit it; the contract verifies and releases. |
| Gasless withdraw | CRX pays the gas to submit the signed intent. No Fireblocks Gas Station is needed and no native token is held here. |