EIP-712 schema and session keys

The typed data traders sign, and the scoped key that avoids re-signing.

5 min read · pre-launch draft
Documentation contents

Authorisation happens once. The wallet signs a gas-free Session struct naming a temporary key generated in the browser, its scope, the wallet's revocation epoch and an expiry. The relayer verifies that signature, stores the grant, and from then on accepts typed intents signed by the temporary key — so placing an order raises no wallet prompt. On a deployed venue the relayer carries the grant with every order and PrismPerpSettlement verifies it again, so a delegated signature is exactly as good on-chain as at the relayer.

typescript
// Signed once per session, by the wallet.
const types = {
  Session: [
    { name: "trader",     type: "address" },
    { name: "sessionKey", type: "address" },
    { name: "scope",      type: "uint32"  },  // 1 open | 2 close | 4 cancel | 8 paper collateral
    { name: "epoch",      type: "uint64"  },  // settlement.sessionEpoch(trader); bumping it revokes every key
    { name: "issuedAt",   type: "uint64"  },
    { name: "expiresAt",  type: "uint64"  },  // 12 hours; the relayer and the contract refuse more than 7 days
  ],
} as const;

Every order, whoever signs it, is the same Order struct the settlement contract verifies. The signing domain is below; the relayer publishes the chain and contract it verifies against in the snapshot, and the client signs against that rather than its own configuration.

typescript
const domain = {
  name: "PrismPerp",
  version: "1",
  chainId: 46630,
  verifyingContract: SETTLEMENT_ADDRESS,
} as const;

const types = {
  Order: [
    { name: "trader",      type: "address" },
    { name: "market",      type: "uint8"   },
    { name: "isLong",      type: "bool"    },
    { name: "collateral",  type: "address" },
    { name: "size",        type: "uint256" },
    { name: "margin",      type: "uint256" },
    { name: "limitPrice",  type: "int256"  },
    { name: "leverage",    type: "uint64"  },
    { name: "nonce",       type: "uint64"  },
    { name: "deadline",    type: "uint64"  },
  ],
} as const;

const signature = await walletClient.signTypedData({
  domain, types, primaryType: "Order", message: order,
});
  • Session keys are scoped to trading. They cannot withdraw on-chain collateral, transfer tokens or change vault parameters; the one collateral scope they hold covers paper balances only, and on-chain every deposit and withdrawal is the wallet's own transaction.
  • Every key carries an expiry and can be ended from the wallet menu, which revokes it at the relayer and deletes it from the browser. On a deployed venue revokeAllSessions() ends every key on-chain as well. No funds move.
  • Closing a position, cancelling an intent, moving paper collateral and revoking a session are each their own typed struct — ClosePosition, CancelIntent, PaperCollateral, RevokeSession — accepted once, by hash, inside a ten-minute deadline.
  • Order nonces are single-use per trader, not sequential. The relayer rejects a reuse before it reaches the chain, and a refused order does not consume its nonce.
  • The deadline field bounds how long a signed intent stays matchable. A resting limit order that reaches it expires and its reserved margin is released.
  • The collateral token is a field of Order, so which asset a signature spends is the trader's decision: change it and the signature recovers to somebody else.
  • npm run test:eip712 proves the wallet and a deployed contract hash every one of these structs identically.