Security overview
Every transaction flow in this API - staking, unstaking, stock trades and bridging - ends in an EIP-7702 batch that your user signs. This page lists the protections built into those batches today, and the ones being added with the Batch Verifier, so you know what the API guarantees and what is still yours to check.
Built into every batch today
Intents are single-use, short-lived and server-held
The build call returns a userOpHash to sign and an intentId; the user
operation itself stays on the Restake side under that intentId. On submit you
return only the signatures, so a signed hash cannot be replayed against a
different chain, amount or target than the one that was quoted.
An intent can be submitted once and expires shortly after it is built. A
replayed or late submit is a 400 - rebuild and re-sign rather than retrying.
The delegate is pinned
The EIP-7702 authorization points the user's address at a contract's code, so
it is the most sensitive of the two signatures. Restake always delegates to the
same account implementation on every chain. The signing guides
(Privy, Privy native, Wallet Core)
include an assertExpectedDelegate check that refuses any other address.
Keep the delegate check
If a bundle ever fails the delegate check, stop and contact us. Do not update the constant to match the bundle.
Exact approvals, never unlimited
When a batch needs an ERC-20 allowance - the base token for a stake, the pay token for a stock trade, USDC for a bridge deposit - it grants exactly the amount the next call spends:
Code
Because the approve and the call that consumes it are in the same atomic batch,
the allowance is used up in the same transaction. If the main call fails, the
whole batch reverts and the approve never takes effect. The API never asks a
user to sign an infinite (type(uint256).max) approval.
A stock trade skips the approve entirely when the wallet's existing allowance already covers the trade.
Allowances are revoked on unstake
Staking approves the pool contract to pull the user's base token - usually a stablecoin such as USDC. When the user unstakes, the batch ends with a revoke of that allowance:
Code
So a user who exits a pool keeps no standing approval to it - neither a leftover from their own deposit, nor an older allowance granted elsewhere (for example an infinite approval from another app).
- The revoke is added only when the wallet still grants the pool an allowance, read on-chain while the batch is built. With nothing to revoke, the batch has no extra call and costs no extra gas.
- If the allowance cannot be read at build time, the revoke is included anyway: a zero approve is harmless, a forgotten allowance is not.
You will see the revoke in bundle.calls of an unstake build response, as the
call right after the withdrawal.
Funds only go back to the user
Deposits, withdrawals, swaps and bridge transfers are built with the user's own address as the receiver, owner and refund address. Bridge deposits carry no destination message, so nothing executes on the destination chain besides the transfer itself.
Minimum-out protection
Stock trades are built with a minimum return taken from the quote, and a swap that returns less than that reverts the whole batch - including its approve. Bridge deposits pin the minimum output amount the same way, and Across refunds a deposit that is not filled before its fill deadline.
DeadlineGuard
A signed user operation stays valid until it is executed. Without an expiry, a signed batch could in principle be held and submitted much later, at a price or state the user never agreed to. DeadlineGuard closes that gap on-chain.
It is a tiny contract with one function:
Code
It is placed as call 0 of every signed batch:
Code
Because the deadline sits inside the calldata the user signs, it cannot be stripped or extended without invalidating the signature. After the deadline, the first call reverts and the whole batch reverts with it - no approval, no transfer, no fee.
| Property | Value |
|---|---|
| Address | 0x4d707032A01Ec803b39cf984d04e24F06185B7f2, the same on every chain (CREATE2) |
| Chains | Base, BSC |
| Maximum batch lifetime | 600 seconds from verification (maxBatchTtl) |
| Upgradeable | No - immutable and admin-less, because it is a call target inside users' signed batches |
| Gas cost | One cheap view call per batch |
The Batch Verifier rejects any batch whose call 0 is not
DeadlineGuard.requireBefore(d) with now < d ≤ now + maxBatchTtl, and
returns deadline alongside the verified hash so your UI can show the user
how long the signature is good for.
Rollout
DeadlineGuard is live on Base and BSC: every batch built for those chains carries it as call 0, with a deadline that matches the build response's intent expiry (under 10 minutes). Batches on other chains don't carry it yet and rely on the server-side intent expiry alone.
Your side of the checklist
The API does its part; these stay with you:
- Keep the delegate check from the signing guides, and never update the expected address to match a bundle.
- Check the 7702 nonce: when
bundle.authorizationis present, itsnoncemust equaleth_getTransactionCount(user)on the batch's chain. When it isnull, the wallet's code there must already be0xef0100 ‖ delegate. - Show the user what they sign: the amount, token and pool or stock come from your UI, not from the build response.
- Derive
minOutand fee limits independently of the API when you adopt the Batch Verifier - the check is only as strong as the intent you pass it. - Keep credentials server-side: a client secret must never reach a browser or a mobile app. See Authentication.
