# 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](/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), [Privy native](/privy-native), [Wallet Core](/wallet-core))
include an `assertExpectedDelegate` check that refuses any other address.

<Callout type="caution" title="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.
</Callout>

### 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:

```
[0] DeadlineGuard.requireBefore(d)   Base and BSC - see DeadlineGuard below
[1] token.approve(spender, amount)   exact amount of this operation
[2] main call                        deposit / swap / bridge deposit
…
```

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:

```
[0] DeadlineGuard.requireBefore(d)              Base and BSC
[1] pool.withdraw / redeem(...)                 main call
[2] baseToken.approve(poolContract, 0)          revoke
```

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:

```solidity
function requireBefore(uint256 deadline) external view;
// reverts when block.timestamp >= deadline
```

It is placed as **call 0** of every signed batch:

```
[0] DeadlineGuard.requireBefore(deadline)   required
[1] token.approve(spender, 0)               optional, USDT-style reset
[2] token.approve(spender, amount)          optional, exact amount only
[3] main call                               deposit / redeem / swap / bridge
[4] token.approve(spender, 0)               revoke, when an allowance would remain
[5] USDC.transfer(feeRecipient, fee)        optional platform fee, always last
```

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](/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.

<Callout type="info" title="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.
</Callout>

## 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.authorization` is present, its `nonce`
  must equal `eth_getTransactionCount(user)` on the batch's chain. When it is
  `null`, the wallet's code there must already be `0xef0100 ‖ delegate`.
- **Show the user what they sign**: the amount, token and pool or stock come
  from your UI, not from the build response.
- **Derive `minOut` and 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](/authentication).
