Fireblocks Withdrawals
You custody in Fireblocks. You want collateral to leave CRX to a Fireblocks-whitelisted address, signed through your own console, with no rejected signature and no expired deadline. This page is that handshake.
The release mechanism is the gasless WithdrawIntent — the same one in Fund Your Account (~6 min). What changes for a custody client is three things: how you sign (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). We interoperate with your custody; we do not rebuild it.
NoteYour custody controls stay yours. Address cooldowns, 4-eyes approval, and velocity limits live in your Fireblocks Transaction Authorization Policy (TAP) — the rule engine that releases a signing request only after your approvers clear it. We enforce none of them and duplicate none of them. Our on-chain route is the mirror of the address you already approved, not a second copy of your policy.
Which signing topology do you run?
One of two. A Fireblocks client connects in one of two shapes, and they sign differently. Both reach the contract; only one needed a relayer fix, now shipped.
| 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, and so does the relayer. For a Safe-custodied client, the relayer recovers no usable address from ECDSA, then calls your Safe's isValidSignature(bytes32,bytes) over the same EIP-712 digest and accepts on the magic value 0x1626ba7e. You do not configure this; pick your 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, so this never bites in practice. Fund first, sign second.
How do you sign the intent?
Request eth_signTypedData_v4 over our WithdrawIntent through your connected Fireblocks provider. The fields and the domain are byte-identical to the on-chain typehash — you sign the same struct the contract verifies.
// 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 by default
};
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 your console, pick the one your 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 your console for policy approval.
- WalletConnect to the Fireblocks console — pair the CRX app to your 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 your TAP until an approver clears it. That latency is exactly why the deadline is long.
Why is the deadline 24 hours?
Because custody approval is not instant, and a signature that expires before your 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 you clicked sign but before custody approved it — the signature lands valid but stale, the contract reverts Expired, and you re-sign into the same trap.
The default signing window is now 24 hours, configurable per deployment. The withdraw UI surfaces the chosen expiry: "Valid until {time}. Approve it in your custody console before then."
WarningApprove before the window closes. The intent carries itsdeadlinein the signature. If 24h passes before your approvers clear it, the relayer and contract both reject it asExpiredand you start over with a fresh nonce. Approve the pending request in your Fireblocks console well inside the window.
The window is bounded, not infinite, on purpose. 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?
Because 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.
| Whitelist | Owner | What it enforces |
|---|---|---|
| Fireblocks TAP | you | Your cooldown, your 4-eyes, your velocity policy. We never see it. |
| On-chain withdraw route | CRX operator | The party → recipient mapping the contract checks before release. |
This is defence in depth, not redundancy. Your Fireblocks policy decides whether your treasury may ever send to R. Our on-chain route is the operator's mirror of that decision — we cannot hold your WHITELIST_ADMIN role, so the address you approved in Fireblocks still needs us to record it on-chain before the gasless path will release to it.
WarningThe recipient must also be a Fireblocks-internal address. Our route gate proves the recipient passed our whitelist, not that it lives inside your custody. If you withdraw to an address that is not one of your own Fireblocks deposit addresses, the funds leave your custody the moment they land. Withdraw to an address you still hold.
How do you add a destination?
Request it from the UI; an operator mirrors it on-chain; Re-check unlocks it. You never self-approve the on-chain route — that key is ours.
- 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 our 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 }- Our operator reviews the request and adds the route on-chain through the existing
POST /admin/withdraw-route(Bearer-gated, operator-only). You do 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 yours to withdraw to until either side removes it.
A worked withdrawal, end to end
Front to back, one withdrawal of free USDC from a Fireblocks-custodied account to a treasury address:
- Whitelist in Fireblocks. Add the destination to your TAP, clear its cooldown and approvals. This is your policy, on your side.
- Request the on-chain route. In the withdraw panel, Request withdrawal address for the same destination. You get a
request_id; statuspending. - Operator mirrors it. Our operator reviews and adds the on-chain route. You wait — 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 your Fireblocks provider. - Approve in custody. The signing request appears in your Fireblocks console. Your 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 your free balance to the recipient. You sign; you never send a transaction. - Confirm on-chain. Read
generalBalance(party, token)— it falls by the amount — and check the recipient on the explorer. See Read Your Data (~4 min).
A repeat withdrawal to the same destination skips steps 1–4: both whitelists already hold. You sign, approve, release.
What you have now, and the branch
- A custody-native signing path.
eth_signTypedData_v4through Fireblocks, verified ECDSA or ERC-1271, whichever topology you run. - 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 you 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 Your 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 | Our 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 | We pay the gas to submit your signed intent. You need no Fireblocks Gas Station and hold no native token here. |