All articles
Smart Contracts·March 20, 2026·7 min read

Solidity Permit2 Gasless Approvals for an EVM Sniping Bot

Build a router with permit2 smart contract integration so your EVM sniping bot swaps in one tx — no separate approve, a full block of latency and gas cut.

A sniping bot that needs a separate approve() transaction before every buy is already too slow. That approval sits in its own block, the mempool sees it, and by the time your swap lands the price you modeled is gone. Permit2 fixes this by letting your router pull tokens on a signature you generate off-chain, so the entire "authorize plus swap" sequence collapses into a single transaction. Below is how to wire a router contract that consumes Permit2 signatures directly, the gas and latency math that makes it worth the effort, and the failure modes that will burn you if you copy a reference implementation without understanding it.

Why the extra approve tx actually hurts

The classic ERC-20 flow is two transactions: approve(spender, amount), then the swap. For a bot chasing a new pool or a liquidity add, that first tx is dead weight. It costs ~46k gas on a cold SLOAD/SSTORE, it advertises your intent to every searcher watching the mempool, and it forces a block boundary between authorization and execution. On a chain like Ethereum mainnet or Base, one block is 2 seconds of exposure where someone can front-run your obvious follow-up swap.

Infinite approvals (type(uint256).max) skip the repeat cost but trade it for a standing liability: any contract you approved keeps that allowance forever, and one bad approval target drains the wallet. Permit2 gives you the single-tx path without the permanent allowance surface. You approve Permit2 once per token (the canonical contract at 0x000000000022D473030F116dDEE9F6B43aC78BA3), and after that every swap is authorized by a fresh signed message with its own amount, nonce, and deadline.

Two flavors: AllowanceTransfer vs SignatureTransfer

Permit2 exposes two subsystems and picking the wrong one is the most common mistake I see.

  • AllowanceTransfer stores an on-chain allowance keyed by (owner, token, spender) with a packed amount, expiration, and nonce. You sign a PermitSingle, submit it once, and subsequent transfers within the expiration window need no new signature. Good for a bot that swaps the same token repeatedly.
  • SignatureTransfer stores nothing between calls. Each transfer carries a PermitTransferFrom with a unique unordered nonce and a deadline, and the signature is the authorization. Good for one-shot snipes where you never want a lingering allowance.

For a sniping router I default to SignatureTransfer. No residual allowance, no expiration bookkeeping, and the nonce is a 256-bit unordered value so you never fight nonce ordering under concurrency. If your bot fires ten snipes in parallel from the same key, unordered nonces are the difference between all ten landing and nine reverting.

The router contract

The core is a function that takes the permit struct plus the owner's signature and calls permitTransferFrom to pull funds into the router, then executes the swap. Keep the router as the spender in the signed message — Permit2 checks msg.sender == permit.spender implicitly through the transfer details.

import {ISignatureTransfer} from "permit2/interfaces/ISignatureTransfer.sol";

contract SnipeRouter {
    ISignatureTransfer public constant PERMIT2 =
        ISignatureTransfer(0x000000000022D473030F116dDEE9F6B43aC78BA3);

    function snipe(
        ISignatureTransfer.PermitTransferFrom calldata permit,
        bytes calldata signature,
        address pool,
        bytes calldata swapData
    ) external {
        // Pull tokens from the signer straight into this router.
        PERMIT2.permitTransferFrom(
            permit,
            ISignatureTransfer.SignatureTransferDetails({
                to: address(this),
                requestedAmount: permit.permitted.amount
            }),
            msg.sender,          // owner == the bot's signing key
            signature
        );

        // tokens are now in the router; execute the pre-computed swap
        (bool ok, ) = pool.call(swapData);
        require(ok, "swap failed");
    }
}

Note that owner is msg.sender here because the bot both signs and submits. If you're building a relayer where a separate account submits on behalf of a signer, owner is the signer's address and the submitter pays gas — that's the gasless meta-transaction pattern, and it's why Permit2 is often described as "gasless approvals" even though someone still pays for the transfer.

Building the EIP-712 signature off-chain

The signature has to match Permit2's domain separator exactly. The domain is {name: "Permit2", chainId, verifyingContract: PERMIT2_ADDRESS} — no version field, which trips up people who assume the standard four-field EIP-712 domain. Here's the ethers.js v6 side:

const permit = {
  permitted: { token: WETH, amount: amountIn },
  nonce: randomNonce(),        // unordered, 256-bit
  deadline: Math.floor(Date.now() / 1000) + 30,
};

const { domain, types, values } =
  SignatureTransfer.getPermitData(permit, PERMIT2_ADDRESS, chainId, {
    to: routerAddress,
    requestedAmount: amountIn,
  });

const signature = await wallet.signTypedData(domain, types, values);

The witness variant (permitWitnessTransferFrom) lets you bind extra data — pool address, min output, target block — into the signed hash so a relayer can't swap your tokens into a different pool. If a third party ever submits your signatures, use the witness. It's a few extra lines and it closes the biggest hole in the naive design.

Gas and latency, concretely

On Base, dropping the standalone approve saves roughly 46k gas on the cold path plus a full ~2s block. The Permit2 permitTransferFrom itself costs about 30–35k gas (signature recovery via ecrecover is ~3k, the rest is the transfer and nonce bookkeeping), so you're net ahead on gas and you've removed a block of latency. For a bot where the difference between block N and block N+1 decides whether you snipe the launch, the latency win dwarfs the gas.

The nonce design matters for throughput. SignatureTransfer nonces live in a bitmap: the top 248 bits pick a word, the bottom 8 bits pick a bit within it. If you want to avoid an SLOAD collision hotspot when firing many snipes at once, spread your nonces across different words rather than incrementing sequentially. This is the same bitmap-slot thinking that shows up when you design PDA seeds for multi-vault trading programs on Solana — you're partitioning a keyspace so concurrent writers don't serialize on the same slot.

Gotchas that will cost you a snipe

  • Deadline too tight. A 30-second deadline is fine when you submit immediately. If your tx sits in the mempool during congestion and the deadline passes, Permit2 reverts and you've spent gas for nothing. Size the deadline to your worst-case inclusion time, not your best case.
  • Nonce reuse reverts, silently in your logs. Reusing a spent nonce reverts with InvalidNonce. Generate nonces from a source you track, and don't assume a failed tx frees the nonce — SignatureTransfer marks it spent only on success, but AllowanceTransfer increments regardless.
  • Fee-on-transfer tokens. requestedAmount is what Permit2 attempts to transfer, not what lands. For tokens that tax transfers, measure the router's balance before and after and swap the delta, or your swap math is wrong.
  • The one-time Permit2 approval is still an approval. You must approve(PERMIT2, max) on the token once. That's a standing infinite allowance to the Permit2 contract. It's a far smaller trust surface than approving arbitrary routers, but it's not zero — Permit2's security is the thing keeping your wallet safe, so treat the initial approval as a real decision.

Any contract that pulls user funds on a signature deserves a real review before it holds size, and the signature-verification path is exactly where subtle bugs hide — a contract audit focused on the permit and transfer logic is cheap insurance next to a drained hot wallet. The witness binding, the deadline handling, and the nonce accounting are the three places I'd want a second set of eyes.

Where this fits in a bot stack

The router is one piece. You still need fast signature generation in the hot path, a mempool or block-builder feed to trigger the snipe, and swap calldata pre-computed so swapData is ready the instant your trigger fires. The same "collapse two round-trips into one atomic action" instinct drives multi-leg design elsewhere — it's why Address Lookup Tables for multi-leg arb transactions exist on Solana and why zero-copy account layouts for on-chain orderbook state matter when every microsecond of deserialization counts. Latency is a systems problem, not a single-contract problem.

If you're building the on-chain side of an EVM bot and want the router, the signature tooling, and the swap path built and hardened together, our smart contract development team does exactly this kind of Permit2 integration work.

Need a bot like this built?

We design, build and run trading bots on Solana, Hyperliquid and Polymarket.

Start a project
#Permit2#Solidity#EVM Bots#Gas Optimization#Smart Contracts