EIP-712 Binding & Terms
What actually binds a trade?
Two signatures over one Terms object. The maker signs the Terms when it prices your request; you sign the same Terms when you accept. Both signatures are EIP-712 — typed, domain-bound, verifiable on-chain. When both exist and the agreed quote is anchored, the contract accepts the bind. No session, no password, no login. The two signatures are the authorization.
This is why the trade path takes no login. Authority comes from the signatures, not from a token. The /auth/* calls exist only for inbox and firm reads, never to trade.
What is in the Terms object?
The Terms is the whole agreement, signed once by each side. The fields, in signing order:
| Field | Meaning |
|---|---|
maId | id of the Master Agreement this position lives in |
pair | the currency pair, as a hashed label |
longIsParty1 | which side is long — a flag, not an address |
notional | size, in quote units |
lockedRate | the forward rate you are binding |
imLongBps / imShortBps | initial margin per side, as a % of notional |
thresholdBps / mtaBps / tolBps | the VM threshold, minimum transfer, and settlement tolerance |
fixingTime / settlementTime / calendarId | the FX calendar terms |
sigDeadline | the moment after which the signatures are no longer valid |
quoteRef / nonce | bind this Terms to the exact anchored quote, once |
The Terms carries no party addresses. Side is the longIsParty1 flag; the addresses are arguments to the bind call, not fields in the signed struct.
Why is there a sigDeadline?
So a signed quote cannot be replayed later. sigDeadline is the timestamp past which the contract rejects the Terms. A maker prices a rate that is honest now, not next week. The deadline closes the window. Sign and bind inside it, or request a fresh quote.
A signature with no deadline is a blank cheque against a moving market.
How are the ids derived?
Deterministically, from the agreement's identity. The Master Agreement id is the hash of the core contract address, the chain id, the two parties, the agreement number, and the chosen CSA. The position id derives from the Terms inside it. Two consequences follow:
- The id binds the chain. Because the chain id is part of the hash, a signature for one chain cannot be replayed on a fork. After a fork, the venue redeploys fresh rather than reusing ids.
- The id binds the contract. The core address is in the hash, so a signature is only valid against the contract it was meant for.
You do not compute the id by hand on the trade path. The relayer returns the canonical Terms in the bundle; you sign exactly what it returns. The contract recomputes the id and checks it against your signature.
What is the EIP-712 domain?
The domain is { name: "CRX", version: "1", chainId, verifyingContract }, where verifyingContract is the deployed core address and chainId is 14601 on testnet. Sign the Terms under this domain. A signature under any other domain will not verify.
const sig = await wallet.signTypedData({
domain: { name: "CRX", version: "1", chainId: 14601, verifyingContract: CRX },
types: { Terms: [/* fields, in the order above */] },
primaryType: "Terms",
message: terms,
});
What can go wrong at bind time?
Two branches, both clean:
- It binds. Both signatures verify, the Terms match the anchor, and the deadline has not passed. The position opens; allocate your margin and read it back.
- It reverts. The signature is over different Terms than were anchored, the
sigDeadlinepassed, or a party is not approved. Nothing partial happens — re-request a quote and sign the fresh Terms.
Bind exactly the Terms the relayer anchored. The contract verifies the agreement, not your intent.
Next: Request a Quote (~5 min) — the concrete calls that produce the Terms you sign.