# Security Model

The question every 1CT integration has to answer for its users is: *what can this key actually do?*

## What the signing key cannot do

The 1CT key is not a wallet delegate. It is a trading-intent signer:

- **It cannot move funds.** There is no transfer, withdrawal, or approval action. The full set of
  operations it can authorize is the [14 trading actions](/gasless/actions) — all of which act on the
  trader's own positions inside the protocol.
- **It cannot exceed its granted permissions.** Authorization carries a per-action bitmask.
- **It cannot outlive revocation.** Revocation is an onchain write and takes effect immediately.
- **It never touches the main wallet key.** The main wallet signs one authorization transaction and
  is not used again.

## Defence layers

| Mechanism | What it prevents |
| :--- | :--- |
| **Permission bitmask** | A key granted "open and close" cannot remove margin. Enforced onchain, per action. |
| **Time-window nonce** | Nonces are millisecond timestamps, must strictly increase, and are only valid from 2 days in the past to 5 minutes in the future. Old signatures cannot be replayed. |
| **Per-intent deadline** | Every intent expires. The reference client defaults to 5 minutes. |
| **Execution fee** | A small per-action fee charged onchain makes spamming the endpoint uneconomical. |
| **Relayer separation** | The relayer holds a restricted role that only lets it *forward* signed intents. It cannot forge a signature or alter one. |
| **Per-intent isolation** | One failing intent in a batch does not affect the others. |
| **Pre-flight simulation** | Each intent is simulated before it goes onchain; intents that would certainly fail never consume gas. |

## Key handling

How the key is stored is the integrator's responsibility, and it is the part most worth getting
right.

### Browser keys

The pattern used by the LeverUp frontend:

```
① User signs a fixed message with their main wallet
② privateKey = keccak256(signature)          ← deterministic derivation
③ Encrypt the private key with AES-GCM
   - the CryptoKey is generated with extractable: false, so scripts cannot export it
   - 12-byte random IV
④ Store { owner, address, cryptoKey, encryptedPk, iv } in IndexedDB
```

```ts
import { keccak256 } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'

const signature = await signMessageAsync({ message: ONE_CLICK_WALLET_SIGN_MESSAGE })
const privateKey = keccak256(signature)
const agent = privateKeyToAccount(privateKey)
// agent.address is what you authorize onchain
```

Two properties fall out of this:

- **Deterministic recovery.** A user who switches device or clears storage re-signs the same message
  with the same wallet and recovers the same 1CT address — the onchain authorization stays valid.
- **Non-exportable at rest.** Generating the wrapping `CryptoKey` with `extractable: false` means a
  script that gains page access still cannot read out the private key.

::: warning Write an honest signing prompt
The message the user signs is the only thing standing between them and a phishing site that asks for
the same signature. State the domain, state the connected address, and state explicitly that the
signature does not authorize any transfer or trade by itself. Keep those guarantees if you adapt
this flow.
:::

### Hosted agents

If your backend holds the agent key, standard key-management applies — HSM or KMS, no key material
in source control or logs, rotation via `authorizeAgent` with a new address.

Grant the narrowest permission set that works. A copy-trading service that only mirrors entries and
exits should not be able to remove margin:

```ts
const permissions = (1n << 0n) | (1n << 1n)   // MARKET_OPEN + MARKET_CLOSE only
```

See [Actions Reference](/gasless/actions#permission-bits) for the bit layout.

## Revocation

```solidity
function revokeAgent(address agent) external;
function revokeAgentByName(bytes32 name) external;
function revokeAllAgents() external;
```

Effective as soon as the transaction confirms — no expiry to wait out.

::: warning Order matters when resetting
Revoke onchain and **wait for the receipt** before deleting the local key. Doing it the other way
around leaves an authorization live for a key the user can no longer control.
:::

## Nonce and deadline

```ts
const nonce = BigInt(Date.now())                          // milliseconds, uint64
const deadline = Math.floor(Date.now() / 1000) + 300      // seconds, uint48
```

| Constraint | Enforced by |
| :--- | :--- |
| `nonce > lastNonce[trader, signer]` | Contract |
| `nonce` not older than 2 days | Contract and backend |
| `nonce` not more than 5 minutes in the future | Contract and backend |
| `deadline` in the future | Contract and backend |

Nonce sequences are tracked per `(trader, signer)` pair, so several agents for the same trader do not
collide. Read the current value with `getLastNonce(trader, signer)`.

::: warning High-frequency clients
Two intents signed in the same millisecond share a nonce and the second is skipped. Maintain a
monotonic counter instead of calling `Date.now()` twice:

```ts
let last = 0n
const nextNonce = () => (last = BigInt(Math.max(Date.now(), Number(last) + 1)))
```
:::

Also note `nonce` is a `uint64` millisecond timestamp — larger than `Number.MAX_SAFE_INTEGER`. Send
it as a **string** in JSON.
