BchainPay logoBchainPay
EngineeringEVMSecurityMEVInfrastructure

MEV, Frontrunning, and Sandwich Attacks: Protecting Crypto Payments

How MEV bots exploit pending EVM payment transactions, which flows are actually at risk, and the receiveWithAuthorization, private RPC, and slippage strategies that stop them.

By Cipher · Founding engineer, BchainPay10 min read
Illustration for MEV, Frontrunning, and Sandwich Attacks: Protecting Crypto Payments

MEV is extensively covered in the DeFi literature — AMM sandwich attacks, backrunning liquidations, priority-fee auctions between competing arbitrageurs. Payment gateways inherit some of that exposure but not most of it. The risk surface is narrower and more specific, which means you can close the real threats without reinventing the full Flashbots stack.

This post maps exactly which payment actions create extractable value, explains the mechanics for each, and covers the concrete countermeasures that apply.

Which payment actions actually have MEV exposure#

Not all on-chain payment operations are equal. Here is the honest breakdown:

Payment action MEV surface Severity
Plain ERC-20 transfer() to deposit address None — no sandwich surface Safe
Native coin send None Safe
transferWithAuthorization relay (EIP-3009) Relay griefing via calldata copying Medium
receiveWithAuthorization relay (EIP-3009) Materially reduced — recipient-bound Low
permit + downstream transferFrom (EIP-2612) Allowance race only if spender is callable Low–Medium
Treasury settlement swap via DEX Classic sandwich attack High
On-chain invoice with manipulable price feed Price manipulation Medium–High

The biggest operational risk for most merchants is the settlement swap row. The authorization-relay rows are more about operational disruption than value extraction. Pure transfers — by far the most common pattern — have no meaningful sandwich surface.

Threat 1: Settlement swap sandwiching#

If your treasury auto-sweep or real-time settlement converts received tokens via a DEX (Uniswap V3, Curve, PancakeSwap), that swap is visible in the public mempool before it executes. A searcher can:

  1. Buy the output token ahead of your swap (frontrun), pushing the price against you.
  2. Let your swap move the pool further in their favour.
  3. Sell immediately after (backrun), pocketing the spread.

The attack is only profitable when your swap size moves the pool's price noticeably. For stablecoin pairs on deep Uniswap V3 pools (USDC/USDT 0.01 % fee tier), the breakeven sandwich size is roughly $50k. For thinner pools or volatile pairs, it can be as low as $5k.

A concrete example: you sweep 80,000 USDC and route through a shallow USDC/USDT pool with $400k TVL. A 2 % price impact is realistic. A searcher who frontruns buys USDT before your swap, captures ~1.5 % spread, and nets ~$1,200 on your single sweep. At scale, this compounds.

Threat 2: transferWithAuthorization relay griefing#

A gasless relay (see the EIP-3009 post) submits a signed authorization on behalf of the payer. The signature commits to from, to, value, validAfter, validBefore, and a one-time nonce. A bot that sees this in the public mempool cannot change any of those parameters — the payment still arrives at the intended recipient.

What the bot can do is copy the exact calldata and submit it with a higher tip, landing before the legitimate relay. Your relay transaction then fails with "authorization already used" because the EIP-3009 nonce is consumed. You paid no funds from the merchant, but you burned gas on a failed transaction and now need to handle the retry.

This is griefing / relay competition, not classical value extraction. It is more relevant when there are competing relayers than when BchainPay is the only party submitting. The mitigation is architectural, covered below.

permit is a different story. An EIP-2612 permit call sets an allowance for a specific spender address embedded in the signature. A bot that copies the permit calldata just confirms an allowance that was going to happen anyway — it doesn't get to redirect the funds unless the downstream contract that calls transferFrom is callable by arbitrary parties. For standard payment flows where the spender is BchainPay's own contract, this creates no additional risk beyond the relay griefing already described.

Defense 1: Use receiveWithAuthorization where possible#

EIP-3009 defines two relay functions:

  • transferWithAuthorization(from, to, value, ...): callable by any msg.sender
  • receiveWithAuthorization(from, to, value, ...): requires msg.sender == to

The second form means only the intended recipient can invoke the authorization. A bot in the public mempool cannot front-run it because they are not the to address and the call will revert.

If your payment contract is the recipient — for example, a merchant escrow that receives and records incoming USDC — switching to receiveWithAuthorization eliminates relay griefing entirely at the protocol level, regardless of which RPC endpoint you use.

// Prefer receiveWithAuthorization when your contract is the recipient
const tx = await usdcContract.receiveWithAuthorization(
  from,
  to,             // msg.sender must equal this
  value,
  validAfter,
  validBefore,
  nonce,
  v, r, s,
);

When the merchant uses a plain EOA as the deposit address (the most common BchainPay pattern), receiveWithAuthorization is not available — EOAs cannot be msg.sender in a contract call. In that case, the relay uses transferWithAuthorization, and private routing becomes the primary defense.

Defense 2: Private mempool routing#

Routing relay transactions through a private RPC means the calldata never enters the public P2P gossip layer. A bot that cannot see the transaction cannot race it.

For straightforward relay submissions, switching the provider is a one-line change:

// Public broadcast (exposes calldata to MEV bots)
const publicProvider  = new ethers.JsonRpcProvider(
  'https://eth-mainnet.g.alchemy.com/v2/<key>',
);
 
// Private broadcast (Flashbots Protect — no auth required on mainnet)
const privateProvider = new ethers.JsonRpcProvider('https://rpc.flashbots.net');
 
// Same tx, different provider — no other code changes needed for basic protection
const tx = await wallet.connect(privateProvider).sendTransaction(txRequest);

For more control — expiry semantics, bundle atomicity — use the Flashbots private-transaction API directly. This is distinct from the simple provider swap: it uses a separate endpoint and an additional signed request:

import { FlashbotsBundleProvider } from '@flashbots/ethers-provider-bundle';
 
const authSigner = new ethers.Wallet(process.env.FLASHBOTS_AUTH_KEY!);
const fbProvider = await FlashbotsBundleProvider.create(
  publicProvider,
  authSigner,
  'https://relay.flashbots.net',
  'mainnet',
);
 
// maxBlockNumber causes the tx to expire rather than fall back to public mempool
const response = await fbProvider.sendPrivateTransaction(
  { transaction: txRequest, signer: wallet },
  { maxBlockNumber: currentBlock + 3 },
);

Other private-relay options for Ethereum mainnet:

  • MEV Blocker: https://rpc.mevblocker.io — distributes to multiple non-MEV builders
  • bloXroute BDN: https://api.blxrbdn.com — API key required, higher throughput

Chain coverage caveat. Flashbots Protect and MEV Blocker cover Ethereum mainnet and some L2s. On Arbitrum, the sequencer provides native first-come ordering with sub-second block times, which significantly reduces the relay griefing window. On Polygon PoS, builders vary; test your private RPC endpoint before relying on it in production. On BSC, the block time is 3 seconds but private mempool support is patchier — elevated priority fees are a more reliable mitigation there.

Defense 3: Hard slippage caps on settlement swaps#

Private routing solves the visibility problem for relay transactions, but settlement swaps need an additional defense: a hard output floor.

const swapParams = {
  tokenIn:           USDC_ADDRESS,
  tokenOut:          USDT_ADDRESS,
  fee:               100,                          // Uniswap V3 0.01 % stable pool
  recipient:         treasuryAddress,
  amountIn:          sweepAmount,
  amountOutMinimum:  (sweepAmount * 9980n) / 10000n,  // 0.2 % max slippage
  sqrtPriceLimitX96: 0n,
};
 
await uniswapRouter.exactInputSingle(swapParams);

Recommended thresholds:

  • Stablecoin pairs (USDC/USDT, USDC/DAI): 0.2–0.5 % — pools are deep, normal price movement is tiny, tighter = safer
  • Volatile pairs (USDC/ETH, USDC/WBTC): 1–2 % — more natural price movement, too tight = frequent reverts

A successful sandwich requires the price impact of your transaction to exceed your slippage tolerance. Set the floor correctly and the attack becomes uneconomical.

Slippage caps and private routing are complementary, not interchangeable. Private routing prevents visibility before inclusion; slippage caps bound damage if a transaction somehow escapes to the public mempool (provider fallback, chain reorg, misconfiguration).

Defense 4: Avoid manipulable on-chain price feeds in payment logic#

If your payment contract computes a dynamic fee or validates an amount against an on-chain price, use a robust external oracle rather than an AMM spot price or a short TWAP.

AMM spot price reads are the most exploitable: a single swap in the same block can move the reported price significantly, making the oracle output stale before your payment transaction executes in the same block. Uniswap V3 TWAPs are much better, but short windows (under 30 minutes) are still sandwichable with enough capital.

Chainlink price feeds use a median from multiple independent data sources and are updated on a deviation threshold; they are not movable by a single DEX interaction. For payment-critical price reads, the choice is:

  • Chainlink / Pyth median oracle: safe for payment logic
  • Uniswap V3 TWAP (30 min+): acceptable for low-value flows
  • AMM spot price or short TWAP: avoid in any settlement logic

Monitoring: detecting sandwiches in production#

After each settlement swap, check the adjacent transactions in the same block for a sandwich pattern. This is a heuristic — sandwich legs aren't always literally adjacent by index — but it catches the most common searcher behaviour:

async function checkForSandwich(
  swapTxHash: string,
  poolAddress: string,
  provider:    ethers.Provider,
): Promise<boolean> {
  const receipt = await provider.getTransactionReceipt(swapTxHash);
  if (!receipt) return false;
 
  const block = await provider.getBlock(receipt.blockNumber, true);
  if (!block?.prefetchedTransactions) return false;
 
  const myIndex  = receipt.index;
  const allSwaps = block.prefetchedTransactions.filter(tx =>
    tx.to?.toLowerCase() === poolAddress.toLowerCase(),
  );
 
  const before = allSwaps.filter(tx => tx.index < myIndex);
  const after  = allSwaps.filter(tx => tx.index > myIndex);
 
  if (before.length > 0 && after.length > 0) {
    const sameSender = before.some(b =>
      after.some(a => a.from.toLowerCase() === b.from.toLowerCase()),
    );
    if (sameSender) {
      logger.warn(`sandwich detected block=${receipt.blockNumber} pool=${poolAddress} tx=${swapTxHash}`);
      return true;
    }
  }
  return false;
}

Feed this check into your ops pipeline. A pool accumulating sandwich incidents is a signal to either tighten slippage, route through a deeper pool, or batch small sweeps until the total size justifies private bundle submission.

How BchainPay handles this in production#

All transferWithAuthorization relays are broadcast exclusively via private RPC endpoints — Flashbots Protect for Ethereum mainnet, MEV Blocker as secondary. Public eth_sendRawTransaction is reserved for balance queries, eth_call, and receipt polling only.

Settlement swaps use 0.2 % slippage for stablecoin pairs and 1 % for volatile pairs. Sweep sizes are capped per transaction; batches that exceed the threshold are spread across multiple blocks rather than submitted as single large swaps that would make sandwiching worthwhile.

Merchant-facing payment intents use a deposit-address pattern where the payer sends directly to a derived address — no relay, no calldata, no MEV surface. The relay infrastructure only activates for sponsored gasless checkout flows.

The BchainPay payment-intent API abstracts all of this: create an intent, receive a deposit address, and get a webhook on confirmation. The MEV countermeasure layer is invisible to the integration.

Key takeaways#

  • Most payment transfers have zero sandwich surface. A plain ERC-20 transfer to a deposit address creates no arbitrage opportunity. MEV risk is concentrated in DEX settlement swaps and complex relay flows, not baseline transfers.

  • receiveWithAuthorization is the strongest relay fix. When your contract is the recipient, this single change eliminates relay griefing at the protocol level. Private routing is defense-in-depth, not the primary mitigation.

  • Swapping the RPC endpoint is the simplest private-routing fix. Point your relay signer at rpc.flashbots.net instead of a public node — no other code changes required for basic protection. Use the bundle API only when you need expiry semantics or atomic multi-tx bundles.

  • Slippage caps and private routing are complementary. Hard output floors bound sandwich damage if a swap ever escapes to the public mempool. A 0.2 % cap on stablecoin pairs makes the attack uneconomical.

  • Avoid AMM spot prices in payment logic. Use Chainlink or Pyth median oracles for any fee calculation or amount validation inside a payment contract. AMM spot reads are movable in the same block.

  • Monitor adjacent swaps after inclusion. Same-block, same-pool, same-sender pattern before and after your swap is a reliable sandwich heuristic. A per-pool incident counter gives early warning before losses compound.

  • Private mempool coverage is chain-specific. Flashbots Protect works well on Ethereum mainnet. Verify coverage on each L2 and sidechain you run on; short block times on Arbitrum reduce exposure even without private routing.


Try it yourself

Spin up a sandbox merchant in under 60 seconds.

One REST endpoint, signed webhooks, five chains. No credit card required.

Related reading