Orkid

Permit2 signing walkthrough

How to sign a PermitTransferFrom message for gasless execution.

Permit2 lets a user approve a token transfer with an off-chain signature. The PermitTransferFrom signature authorizes a specific spender (the TVMExecutor) to pull an exact amount of a specific token before a deadline, using a specific one-time nonce.

Step 1 — Approve the token to Permit2 (one-time)

Before the first swap of each token, the user must call:

IERC20(token).approve(PERMIT2, type(uint256).max)

Permit2 address on all chains: 0x000000000022D473030F116dDEE9F6B43aC78BA3

Step 2 — Get the correct TVMExecutor address

The spender in the signed permit must match the TVMExecutor on the target chain:

ChainTVMExecutor
Base0x60AB9E090abF9B6BcFbA16017eE18AEf5f2c2289
Ethereum0xCfc33b521190FcD414c8f81f479749c4dCE8f69b
Arbitrum0xf57235609bf99fb0b017e95b3bc33a606a541374
Polygon0xd633Ea84E6E2Db002C14C6fe3A064eE2cc4E258c

Step 3 — Find an unused nonce

Permit2 stores a nonce bitmap for each user. If you reuse a nonce, the transaction reverts.

The nonce layout is:

wordPos = uint248(nonce >> 8)
bitPos  = uint8(nonce)

Read nonceBitmap(owner, wordPos) from the Permit2 contract until you find a word with an unset bit, then construct (wordPos << 8) | bitPos.

Step 4 — Sign the Permit2 typed data

Domain

{
  "name": "Permit2",
  "chainId": 8453,
  "verifyingContract": "0x000000000022D473030F116dDEE9F6B43aC78BA3"
}

Types

{
  "PermitTransferFrom": [
    { "name": "permitted", "type": "TokenPermissions" },
    { "name": "spender", "type": "address" },
    { "name": "nonce", "type": "uint256" },
    { "name": "deadline", "type": "uint256" }
  ],
  "TokenPermissions": [
    { "name": "token", "type": "address" },
    { "name": "amount", "type": "uint256" }
  ]
}

Message

{
  "permitted": {
    "token": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
    "amount": "25000000"
  },
  "spender": "0x60AB9E090abF9B6BcFbA16017eE18AEf5f2c2289",
  "nonce": "123456...",
  "deadline": "1725561600"
}

The spender must be in the signed message but is not included in the JSON permit object sent to /api/v1/solve.

Step 5 — Call /api/v1/solve

Send the permit (without spender) and the 65-byte hex signature:

{
  "permit": {
    "permitted": {
      "token": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
      "amount": "25000000"
    },
    "nonce": "123456...",
    "deadline": "1725561600"
  },
  "signature": "0x..."
}

Using the SDK instead

The OrkidPermitSigner handles all of the above automatically:

import { OrkidPermitSigner } from '@orkid/sdk'
import { createWalletClient, http } from 'viem'
import { base } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'

const account = privateKeyToAccount('0x...')
const walletClient = createWalletClient({ account, chain: base, transport: http() })
const permitSigner = new OrkidPermitSigner(walletClient)

const signed = await permitSigner.prepareAndSign({
  fromToken: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
  fromDecimals: 6,
  amount: '25.0',
  chain: 'base',
})

// signed.permit and signed.signature are ready to send to /api/v1/solve

On this page