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:
| Chain | TVMExecutor |
|---|---|
| Base | 0x60AB9E090abF9B6BcFbA16017eE18AEf5f2c2289 |
| Ethereum | 0xCfc33b521190FcD414c8f81f479749c4dCE8f69b |
| Arbitrum | 0xf57235609bf99fb0b017e95b3bc33a606a541374 |
| Polygon | 0xd633Ea84E6E2Db002C14C6fe3A064eE2cc4E258c |
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
spendermust be in the signed message but is not included in the JSONpermitobject 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