EIP-712 Signing

CRE Connect authorizes every Operation with an EIP-712 typed-data signature. The Smart Account verifies the signature on-chain before any transaction in the Operation runs. With EIP-712, wallet UIs and key-management systems can show a human-readable approval prompt instead of an opaque hash.

Domain

The EIP-712 domain is constant across CRE Connect:

Field
ValueSource
nameCLLSmartAccounttransact/types constant EIP712DomainName.
version1transact/types constant EIP712DomainVersion.
chainIdThe numeric chain ID, derived from the CCIP chain selector passed to the SDK.Resolved with GetChainIDFromSelector(selector). EVM-only.
verifyingContractThe Smart Account address (Operation.Account).Built by SmartAccountEIP712Domain(chainId, account).

Because verifyingContract is the Smart Account itself, two wallets on the same chain produce different domain hashes. A signature for wallet A is unusable on wallet B even if both contracts speak the same protocol. This is what makes the EIP-712 binding per-account.

Typed-data schema

The primary type is Operation. It references one dependent type, Transaction:

Operation {
    id          uint256
    account     address
    deadline    uint256
    transactions Transaction[]
}

Transaction {
    to    address
    value uint256
    data  bytes
}

These are the same fields documented in Operations. EIP-712 hashing follows the standard rules:

  • bytes and string fields are hashed with keccak256 before being included.
  • Dynamic arrays are hashed by concatenating the hashes of each element and keccak256-ing the result.
  • The struct hash is keccak256(typeHash ‖ encoded fields).
  • The final digest is keccak256("\x19\x01" ‖ domainSeparator ‖ structHash).

The SDK builds this digest with go-ethereum's apitypes.TypedDataAndHash, so the bytes are bit-for-bit compatible with any EIP-712 implementation in Solidity, Ethers, viem, or rust-ethereum.

Hashing pipeline (in code)

Most application code does not call the Handler directly. Use the high-level entry points on the unified client:

  • client.Transact.HashOperation(op, chainSelector): returns the 32-byte digest without signing. Use this for draft operations and external approval workflows.
  • client.Transact.SignOperation(ctx, op, signer, chainSelector): returns the 32-byte digest and the signature.
  • client.Transact.ExecuteOperation(ctx, channelID, signer, op, chainSelector): signs and submits in one call.
  • client.Transact.ExecuteTransactions(ctx, channelID, signer, executorAccount, txs, deadline, chainSelector): convenience wrapper that builds the Operation for you.

Draft operations use the same digest. In the draft flow, CRE Connect stores the unsigned operation first, then your application sends the digest and signature when it finalizes the draft.

The lower-level handler is exposed in transact/eip712 if you need the digest without signing or want to drive the pipeline yourself:

  • eip712.Handler.HashOperation(op, chainSelector): returns the 32-byte digest, no signing.
  • eip712.Handler.SignOperation(ctx, op, signer, chainSelector): computes the digest and asks the supplied Signer to sign it.

Signers

A signer.Signer is anything that produces a 65-byte ECDSA signature over a 32-byte hash. The interface lives in github.com/smartcontractkit/crec-sdk/transact/signer:

import "github.com/smartcontractkit/crec-sdk/transact/signer"

type Signer interface {
    Sign(ctx context.Context, hash []byte) ([]byte, error)
}

Some signer adapters (for example, Fireblocks) also implement signer.TypedDataSigner, which lets the adapter render a typed-data prompt to a human approver:

type TypedDataSigner interface {
    SignTypedData(ctx context.Context, typedData *TypedData) ([]byte, error)
}

The SDK ships five built-in adapters and supports any custom implementation:

AdapterPackageNotes
Local (ECDSA)transact/signer/localIn-process key. Recommended for local development only.
AWS KMStransact/signer/kmsKMS-managed ECC_SECG_P256K1 key.
HashiCorp Vaulttransact/signer/vaultVault Transit secrets engine, ECDSA secp256k1 key.
Fireblockstransact/signer/fireblocksImplements both Signer and TypedDataSigner.
Privytransact/signer/privyPrivy wallet via REST API.
Customany packageAny type implementing the Signer interface.

See Signers for runnable setup guides.

Authorization vs execution

EIP-712 signing only authorizes the Operation. It does not pay gas, and it does not put the Operation on-chain by itself. Execution still goes through the Chainlink DON, which signs and broadcasts the underlying transaction on your Smart Account's behalf (see Account Abstraction & Gas Sponsorship).

This separation has two practical consequences:

  1. The signing key never holds gas. A KMS- or Vault-managed key with no on-chain presence is sufficient.
  2. The on-chain tx.origin is the DON's writer, not the user. Authorization is anchored on the EIP-712-recovered signer address compared against the wallet's allow-list, not on the message sender. See Smart Accounts for the contract-level model.

Get the latest Chainlink content straight to your inbox.