Skip to content

Signing (EIP-7702)

After receiving a quote, the user signs two things off-chain — both gasless, with no on-chain approval and no token allowance:

  1. The order — an EIP-712 typed-data signature (signTypedData) over the SweepOrder. This becomes the bytes signature passed to the on-chain sweep().
  2. An EIP-7702 authorization — delegates the user's EOA to the per-chain SweepDelegate implementation so the EOA can run the settlement as if it were a smart account.

The client then submits { quoteId, signature, authorization } to POST /intents. A relayer (Kryard) broadcasts the EIP-7702 type-4 ("set-code") transaction and pays the gas.

EIP-7702-only. The on-chain SweepRouter / Permit2 contracts still exist but are not used by the product — Permit2 needs an on-chain approve, which costs gas. The two off-chain signatures below are the only signing path.

Step 1 — Sign the order (EIP-712)

The SweepOrder is signed as EIP-712 typed data, not a blind hash — so hardware wallets render every field readably. The on-chain delegate verifies it as:

solidity
ECDSA.recover(toTypedDataHash(domainSeparator(), structHash(order)), sig) == address(this)

Because sweep() runs with address(this) == the user's EOA == order.owner, the recovered signer must be the user. Build the typed-data payload exactly as the on-chain SweepDelegate defines its EIP-712 scheme:

typescript
import type { SweepOrderWire } from "./types"; // the order from POST /quote

type Hex = `0x${string}`;

/**
 * EIP-712 type definition for the SweepOrder — all 14 fields, in the order that
 * matches the on-chain SWEEP_ORDER_TYPEHASH.
 */
export const SWEEP_ORDER_TYPES = {
  SweepOrder: [
    { name: "owner", type: "address" },
    { name: "tokenIn", type: "address" },
    { name: "amountIn", type: "uint256" },
    { name: "feeAmount", type: "uint256" },
    { name: "builder", type: "address" },
    { name: "builderFee", type: "uint256" },
    { name: "tokenOut", type: "address" },
    { name: "minAmountOut", type: "uint256" },
    { name: "destination", type: "address" },
    { name: "swapTarget", type: "address" },
    { name: "swapCalldata", type: "bytes" },
    { name: "deadline", type: "uint256" },
    { name: "nonce", type: "uint256" },
    { name: "destChainId", type: "uint256" },
  ],
} as const;

/**
 * Build the full EIP-712 typed-data payload the EOA signs via
 * `walletClient.signTypedData(...)`. Mirrors the on-chain SweepDelegate scheme:
 *
 *   domain = { name: "Sweepster", version: "1",
 *              chainId: <source chain id>,
 *              verifyingContract: order.owner }
 *   primaryType = "SweepOrder"
 *
 * `verifyingContract` is the delegated EOA (== order.owner) and `chainId` is the
 * SOURCE chain id, because `sweep` runs as the user's EOA on the source chain.
 */
export function buildDelegateTypedData(order: SweepOrderWire, sourceChainId: number) {
  return {
    domain: {
      name: "Sweepster",
      version: "1",
      chainId: BigInt(sourceChainId),
      verifyingContract: order.owner as Hex,
    },
    types: SWEEP_ORDER_TYPES,
    primaryType: "SweepOrder",
    message: {
      owner: order.owner,
      tokenIn: order.tokenIn,
      amountIn: BigInt(order.amountIn),
      feeAmount: BigInt(order.feeAmount),
      builder: order.builder,
      builderFee: BigInt(order.builderFee),
      tokenOut: order.tokenOut,
      minAmountOut: BigInt(order.minAmountOut),
      destination: order.destination,
      swapTarget: order.swapTarget,
      swapCalldata: order.swapCalldata,
      deadline: BigInt(order.deadline),
      nonce: BigInt(order.nonce),
      destChainId: BigInt(order.destChainId),
    },
  } as const;
}

Field order is critical. The SWEEP_ORDER_TYPES field list mirrors the on-chain SWEEP_ORDER_TYPEHASH exactly. Any SweepOrder field change must be reflected in both the contract typehash and this type definition, or the recovered signer will not match.

Sign the typed data

Sign the typed data with signTypedData — viem computes the EIP-712 digest (keccak256(0x1901 ‖ domainSeparator ‖ structHash)) that the on-chain toTypedDataHash recovers against.

typescript
import { createWalletClient, custom } from "viem";
import { base } from "viem/chains";

const walletClient = createWalletClient({
  chain: base,
  transport: custom(window.ethereum),
});

const signature = await walletClient.signTypedData({
  account: order.owner as Hex,
  ...buildDelegateTypedData(order, 8453), // 8453 = source chain (Base)
});

With wagmi (React):

typescript
import { useSignTypedData } from "wagmi";

const { signTypedDataAsync } = useSignTypedData();

const signature = await signTypedDataAsync(
  buildDelegateTypedData(order, sourceChainId),
);

Step 2 — Sign the EIP-7702 authorization

The authorization delegates the user's EOA to the per-chain SweepDelegate implementation contract. The contractAddress comes from the quote (quote.settlementAddress, preferred) or a configured per-chain fallback.

typescript
const signedAuth = await walletClient.signAuthorization({
  account: walletClient.account,
  contractAddress: sweepDelegateAddress, // the per-chain SweepDelegate impl
  chainId,                               // the source chain id
});

// Serialize for the orchestrator (POST /intents `authorization`):
const authorization = {
  address: signedAuth.address,   // the SweepDelegate impl
  chainId: signedAuth.chainId,
  nonce: signedAuth.nonce,
  r: signedAuth.r,
  s: signedAuth.s,
  yParity: signedAuth.yParity ?? 0,
};

The authorization is required. POST /intents returns 422 if authorization is absent — there is no fallback. EIP-7702 signing requires viem >= 2.x and a Pectra-capable chain + wallet; all supported chains qualify.

What the user is authorizing

The EIP-712 order signature binds every field of the SweepOrder. By signing, the user authorizes Sweepster to:

  1. Pull exactly amountIn of tokenIn from their wallet (self-pull, via the 7702 delegation)
  2. Swap and skim feeAmount (protocol fee) + builderFee
  3. Deliver minAmountOut or more of tokenOut to destination
  4. Execute via swapTarget using swapCalldata
  5. Expire at deadline

The relayer cannot change any of these without invalidating the signature.

Displaying the order to the user

Before signing, display the key order fields clearly:

You will send:    1,000 USDC (Base)
Fee:                  8 USDC (protocol: 5, builder: 3)
You will receive: ≥ 992 USDC (Arbitrum)
To:               0xYourDestination
Expires:          2026-06-13T14:30:00Z

A robust integration also re-validates the returned order against the user's original request (owner, token, amount, destination, chain, fees, slippage floor) before signing — to prevent blind-signing an order the orchestrator may have altered.

Next step

Submit the signature + authorization →

Non-custodial · Atomic · Audit + bug bounty planned before mainnet value.