# Signing Rules

What Aurea checks before it signs for your tenant: which messages a wallet may sign, approvals for the exact amount with nothing left over, swaps signed from the route Aurea quoted, and each transaction simulated before it leaves.

## Overview

| Rule | What it does in `enforce` |
| --- | --- |
| Messages | A wallet signs a `personal_sign` only from one of your enabled dApps or under the rules of the app's own flows, typed data only for the EIP-712 domains allowed, and a raw digest only under a rule for it. |
| Exact approvals | A token approval is for the operation's exact amount, to a spender on the allowlist; an allowance left over after a custodial swap is revoked. |
| Swaps built on the server | A swap is signed from the route Aurea kept with its quote, never from the transaction the client sends. |
| Transaction simulation | Each transaction is decoded and simulated before it is signed; one that cannot be, or whose outcome is not what was asked, is refused. |

- **Today the message rules and swaps built on the server are in `log` for every tenant** — checked and kept, never refused. Enforcing them, exact approvals and the transaction simulation are on request [Coming soon, upon request](https://docs.aureahub.com/docs/feature-availability.md); until then your wallets sign exactly as before.
- What the rules said about an operation is in [Get an Operation](https://docs.aureahub.com/docs/operation-get.md): `signingChecks` — a refused check included, when the call had an operation — and `simulations`. One check by its id, the `details.checkId` of a refusal: [Get a Signing Check](https://docs.aureahub.com/docs/operation-signing-check.md).

## Off, Log, Enforce

| Mode | What your calls meet |
| --- | --- |
| off | **Exact approvals and transaction simulation, unless your tenant asks for them.** Nothing is checked, nothing is kept, every answer is as before. |
| log | **The message rules and swaps built on the server, for every tenant today.** Each check is made and kept — what `enforce` would refuse or change, recorded beside the signing — and **nothing is refused or changed**: every answer is the one it would be without the rule. |
| enforce | The rule applies: the refusals below, before anything is signed or sent. A tenant on the institutional security profile is in `enforce` for every rule by its profile. |

Each rule's mode is set for your tenant separately, on request.

## Messages

- **A dApp's `personal_sign`** ([Sign Personal Message](https://docs.aureahub.com/docs/dapps-sign.md)): send the origin of the dApp that asks in `dappOrigin` — `https://app.example.com`. In `enforce` the message is signed only when that origin is one of your tenant's active dApps ([List DApps](https://docs.aureahub.com/docs/dapps-list.md)) with the dApps switched on for your tenant. Origins compare scheme, host and port: the path is ignored, and another subdomain is another origin.
- **Without `dappOrigin`**, a `personal_sign` is signed only when it is one of the messages of the app's own flows that Aurea knows; a message that names a dApp is never judged by those. Send `dappOrigin` from your dApp browser now: until the rule is on it changes nothing.
- **Typed data (EIP-712)** — [Sign EIP-712 Permit](https://docs.aureahub.com/docs/swap-sign-permit.md), and an MPC wallet's signing ceremony — is signed only for a domain Aurea builds itself or one allowed for your tenant.
- **A raw digest** an MPC wallet's [signing ceremony](https://docs.aureahub.com/docs/mpc-sign-start.md) is started for — not a transaction or a permit Aurea prepared, nor a message it checked — is signed only under a rule set for your tenant on request.

## Exact Approvals

- **[Approve Token](https://docs.aureahub.com/docs/swap-approve.md) needs `amount`** — the swap's amount in the token's smallest unit. Without it, or for the largest possible amount (unlimited): `400` `APPROVAL_AMOUNT_REQUIRED`. `"0"` is accepted: it revokes. Send the amount now: until the rule is on, it changes nothing.
- **The spender** must be on the allowlist of the contracts Aurea approves — the swap router and the gasless executor it uses, and the contracts of its own registry: `403` `APPROVAL_SPENDER_NOT_ALLOWED` otherwise. The same holds for a permit's spender ([Sign EIP-712 Permit](https://docs.aureahub.com/docs/swap-sign-permit.md)).
- **A token that takes a new allowance only from zero**, with one still left for that spender: a custodial wallet's is reset to 0 by Aurea first; a client-side or MPC wallet answers `409` `APPROVAL_RESET_REQUIRED` — approve `"0"`, then the amount.
- **After a custodial swap**, an allowance the router left unused is revoked once the swap is mined. [Execute Swap](https://docs.aureahub.com/docs/swap-execute.md) waits for the swap's receipt and then the revoke's within **one budget of 60 seconds** — never longer for the two together. A receipt not seen in time never fails the swap, which is out: the call answers as it would, and the operation's check (`signingChecks`, subject `revoke`, verdict `failed`) says why — `SWAP_RECEIPT_TIMEOUT` (no revoke was sent: what is left is unknown) or `REVOKE_RECEIPT_TIMEOUT` (the revoke was sent and may still be mined: `details.revokeSent` `true`); any other failure, `REVOKE_FAILED`.
- **A client-side or MPC wallet's swap** is not revoked: the platform holds no key to do it. In `log` and `enforce`, once a swap of an ERC-20 sent with [Broadcast Swap Tx](https://docs.aureahub.com/docs/swap-broadcast.md) is mined — waited for up to 60 seconds beside the call, which answers as before —, Aurea reads the allowance it left to the router and keeps it as a check of the operation (point `client_broadcast`, subject `revoke`, `details.residueAfter`, `details.revoked` `false`): verdict `would_change` with code `RESIDUE_LEFT` when some is left — revoke it yourself with [Approve Token](https://docs.aureahub.com/docs/swap-approve.md) for `"0"` —, `unchanged` when none, `failed` with `SWAP_RECEIPT_TIMEOUT` or `RESIDUE_UNREAD` when it could not be read. Never refused.

## Swaps Built on the Server

- [Get Quote](https://docs.aureahub.com/docs/swap-quote.md) keeps the quote's route. [Execute Swap](https://docs.aureahub.com/docs/swap-execute.md) and [Execute Gasless](https://docs.aureahub.com/docs/swap-gasless.md) on an EVM chain then sign **that route** — its call data and value — whatever transaction the client sends; a gasless swap's permit is checked against the route's target, amount and token.
- **Execute a quote you asked for**, for the same wallet, while it is valid: otherwise `409` `SWAP_QUOTE_UNKNOWN`, with `details.reasons` — `quote_unknown`, `other_user`, `other_wallet`, `quote_expired`, `chain_differs`, `route_not_kept`. Ask for a new quote.
- A quote whose router is not on the allowlist of the routers Aurea signs for: `403` `SWAP_ROUTER_NOT_ALLOWED`.
- **Solana swaps too.** The quote keeps the serialized transaction the swap router answered. [Execute Solana Swap](https://docs.aureahub.com/docs/swap-execute-solana.md) then signs **exactly that transaction** for a custodial wallet — or hands its message to a client-side wallet to sign, where your tenant has client-side Solana swaps — whatever the client sends. Refused before any other check: no such quote, or one another wallet pays (`409` `SWAP_QUOTE_UNKNOWN`, with the reason `payer_differs` besides those above); a route that calls a program outside the Solana allowlist — the System, Token, Token-2022, associated token account, compute budget and memo programs, and the swap router's own program — `403` `SWAP_ROUTER_NOT_ALLOWED`. In `log` the client's transaction is signed as before, and the check keeps how it differs and whether the route's programs are all allowed.

## Transaction Simulation

- Each transaction Aurea is about to sign or send is **decoded** — what it calls, with what — and **simulated** on the platform's node of its chain, and the outcome is compared with what was asked: the amount, the asset and the recipient, an approval's spender. Each simulation is kept, with the operation where it is recorded ([Get an Operation](https://docs.aureahub.com/docs/operation-get.md), `simulations`).
- **In `enforce`** the signing waits for it: a transaction that cannot be decoded and simulated answers `409` `TX_NOT_SIMULATED`, one whose outcome diverges from what was asked `409` `TX_SIMULATION_DIVERGES` — with `details.reasons` and `details.simulationId` (the entry of `simulations`) — and a recorded operation ends `FAILED` `not_sent`. One that matches goes on to your policy.
- **Solana**: a custodial wallet's send — SOL, or a token with its recipient's token account created on the way — and a custodial Solana swap are simulated on the platform's Solana node (`method` `simulateTransaction`) and judged like an EVM transaction: in `enforce`, `409` `TX_NOT_SIMULATED` or `TX_SIMULATION_DIVERGES` (a send of more than the wallet holds, for example: `reverted`). Its outcome reads system and token transfers, the balance changes, and a token account's delegate as an approval of `kind` `spl`. A client-side wallet's Solana swap is simulated too — the transaction it is handed (on a tenant with the institutional security profile) and the one it signed, at [Broadcast Solana Swap](https://docs.aureahub.com/docs/swap-broadcast-solana.md) — and the result kept with its operation: never waited for or refused, in every mode.
- **Never refused for it**, only recorded: a message signature, a raw digest of an MPC wallet (left to your policy), and a transaction the gasless relayer signs.
- A dApp's transaction has nothing precise asked of it: only a revert diverges.

## Refusals

In `enforce`, before anything is signed or sent; each carries `details.code`, and the money routes answer it as it is:

| Status | details.code | message |
| --- | --- | --- |
| 403 | PERSONAL_SIGN_NOT_ALLOWED | `This message is not one the tenant allows its wallets to sign (a personal_sign comes from one of the tenant's enabled dApps, or is one of the explicit rules): nothing was signed.` |
| 403 | MESSAGE_DOMAIN_NOT_ALLOWED | `The typed data's EIP-712 domain is not one the tenant allows: nothing was signed.` |
| 403 | RAW_SIGN_NOT_ALLOWED | `A raw digest is signed only by an explicit rule of the tenant: nothing was signed.` |
| 400 | APPROVAL_AMOUNT_REQUIRED | `An approval names the exact amount of the operation it is for: without one it would be unlimited, which the tenant does not sign.` |
| 403 | APPROVAL_SPENDER_NOT_ALLOWED | `The spender of this approval is not on the allowlist of the contracts the Hub approves: nothing was signed.` |
| 409 | APPROVAL_RESET_REQUIRED | `This token takes a new allowance only from zero, and one is left for this spender: approve an amount of 0 first, then the operation's amount.` |
| 409 | SWAP_QUOTE_UNKNOWN | `This swap names no quote the Hub issued to this wallet that is still valid: ask for a quote, then execute it.` |
| 403 | SWAP_ROUTER_NOT_ALLOWED | `The quote's router is not on the allowlist of the routers the Hub signs for: nothing was signed.` |
| 409 | TX_NOT_SIMULATED | `This transaction could not be decoded and simulated, which the tenant requires before it is signed: nothing was signed or sent.` |
| 409 | TX_SIMULATION_DIVERGES | `The simulated outcome of this transaction diverges from what was asked: nothing was signed or sent.` |

```json
{
  "statusCode": 403,
  "error": "ForbiddenError",
  "message": "This message is not one the tenant allows its wallets to sign (a personal_sign comes from one of the tenant's enabled dApps, or is one of the explicit rules): nothing was signed.",
  "details": {
    "code": "PERSONAL_SIGN_NOT_ALLOWED",
    "reasons": ["dapp_not_enabled"],
    "checkId": "b81f2c64-9d3e-4a57-8c10-5e7a2f9d4b36"
  }
}
```

- The message refusals' `details.reasons`: `no_dapp_origin`, `dapp_not_enabled`, `dapps_not_enabled`, `dapp_origin_unreadable`, `unreadable`, `domain_not_allowed`, `no_raw_rule`, and those of the app's own flows.
- **`details.checkId`**, on every refusal of the message rules, the exact approvals and the swaps built on the server: the check Aurea kept. Read it with [Get a Signing Check](https://docs.aureahub.com/docs/operation-signing-check.md) — its `operationId` says which operation it belongs to, or `null` when the call had none yet — and quote it when you ask your Aurea contact about it. When the call had an operation, the refused check is also in that operation's `signingChecks`.
- The simulation's `details.reasons` are those of `simulations` ([Get an Operation](https://docs.aureahub.com/docs/operation-get.md)).

---

Web version: https://docs.aureahub.com/#guide-signing-rules
