DEX swaps reverting with Too little received: stale quotes, slippage bounds and deadlines
Our treasury rebalancing service swapped on a DEX and about a quarter of swaps reverted on volatile days with Too little received, wasting gas. Quotes were computed seconds before submission and transactions waited in the mempool. Fresh quotes, simulation and a deadline fixed it without loosening slippage.
Our treasury service converts a portion of incoming tokens to stablecoins on a decentralised exchange on a schedule. On calm days, swaps succeeded. On volatile days, roughly one in four transactions reverted, each still costing gas. The revert reason in the transaction trace:
Error: execution reverted: "Too little received"
Some transactions sat pending for 40 to 90 seconds before being included, and then reverted.
Reproducing it
The flow:
const quote = await quoter.quoteExactInputSingle.staticCall(params); // expected output
const minOut = quote.amountOut * 995n / 1000n; // 0.5% slippage
const tx = await router.exactInputSingle({ ...params, amountOutMinimum: minOut });
Between the quote and the transaction's execution in a block, the pool price moved more than 0.5 percent on volatile days, so the router's check amountOut >= amountOutMinimum failed and the transaction reverted.
Two aggravating factors: the service paid a fixed, low priority fee, so transactions waited several blocks in busy periods; and there was no deadline, so a transaction could execute minutes after its quote if it eventually got included.
What we ruled out
- Insufficient allowance or balance. Those revert with different reasons and happened zero times.
- A broken router address or pool. Calm-day swaps through the same route succeeded.
Root cause
A swap's minimum output is computed from a quote at one moment and enforced at execution time, which can be many seconds later. The longer the gap and the more volatile the pool, the more likely the bound fails. Our service maximised the gap with low fees and no deadline. The revert itself is the slippage protection doing its job; the bug was in how we prepared and sent transactions.
What is happening under the hood
DEX routers protect swappers with two parameters:
- Minimum output (
amountOutMinimumor similar): the swap reverts if it would return less. - Deadline: the swap reverts if executed after a timestamp.
Loosening the minimum output to avoid reverts is the wrong reflex. Wide slippage tolerance lets the transaction execute at bad prices and makes it an attractive target for sandwich attacks, where others trade around your transaction in the same block to capture the slack you allowed. Tight bounds plus fast, fresh submission is the safer combination.
Transactions in the public mempool are visible before inclusion. For larger swaps, private transaction submission services exist that send transactions directly to block builders, which reduces exposure.
Environment
| Component | Version / value |
|---|---|
| Chain | Ethereum mainnet |
| DEX | Uniswap v3 via SwapRouter02 |
| Client | Node.js 22, ethers 6 |
| Fees | Fixed low priority fee |
| Swap size | Equivalent of a few thousand USD per swap |
Minimal reproduction
On a testnet fork, quote a swap, advance the fork by several blocks while moving the pool price with another swap, then send the original transaction with the old minimum: it reverts with the same reason.
The fix
1. Quote, simulate, send immediately. The service fetches a fresh quote, builds the transaction, simulates it with eth_call against the latest block, and sends it right away:
async function swap(params) {
const quote = await quoter.quoteExactInputSingle.staticCall(params);
const minOut = quote.amountOut * (10_000n - SLIPPAGE_BPS) / 10_000n;
const deadline = BigInt(Math.floor(Date.now() / 1000) + 60);
const call = router.interface.encodeFunctionData("multicall(uint256,bytes[])", [
deadline,
[router.interface.encodeFunctionData("exactInputSingle", [{ ...params, amountOutMinimum: minOut }])],
]);
await provider.call({ to: ROUTER, from: wallet.address, data: call }); // throws on revert
const fee = await feePolicy.current(); // see below
return wallet.sendTransaction({ to: ROUTER, data: call, ...fee });
}
2. A deadline of 60 seconds. If the transaction is not included quickly, it reverts cheaply rather than executing at an unexpected time, and the service re-quotes.
3. Dynamic fees. Priority fee and max fee are set from recent blocks (eth_feeHistory), with a cap. Paying a reasonable priority fee shortened time to inclusion from tens of seconds to usually the next block or two.
4. Smaller slices on volatile days. When recent volatility of the pool is high, the service splits a conversion into several smaller swaps spaced over minutes, keeping slippage tolerance tight.
5. Private submission for larger swaps. Swaps above a size threshold go through a private transaction relay instead of the public mempool.
Alternatives and trade-offs
- Wider slippage. Fewer reverts, worse prices and more exposure to sandwiching. Not acceptable for treasury operations.
- Aggregators. Route across many pools and often provide better quotes and built-in protections; you depend on the aggregator's contracts and API.
- Limit-order style protocols where you sign an order and solvers fill it. Different trust and execution model, no reverted gas for failed attempts.
How to test the fix
Fork tests simulate price movement between quote and execution and check that the service either succeeds within bounds or re-quotes after deadline expiry. Over a week on mainnet with small amounts, reverts fell to 2 percent, all on simulated-success transactions that lost a race in the same block.
Confirming in production
- Revert rate fell from about 25 percent on volatile days to under 3 percent.
- Average execution price relative to the quote improved, since transactions are included faster.
- Gas wasted on reverts dropped by more than 90 percent.
Regression guard
- Metrics: time from quote to inclusion, revert rate, realised slippage per swap.
- Alert if revert rate exceeds 10 percent over an hour.
- Fork tests in CI for the swap module.
Security implications
The treasury wallet's private key lives in a hardware-backed signer with a policy that only allows calls to the approved router and token contracts, with per-transaction and daily limits. Token approvals are for exact amounts, not unlimited. Tight slippage and deadlines are also security controls against value extraction by third parties. This post covers execution mechanics, not investment decisions.
Prevention checklist
- Quote and simulate immediately before sending.
- Always set a short deadline.
- Keep slippage tolerance tight; fix latency instead of widening it.
- Use dynamic fees so transactions are included promptly.
- Use exact approvals and restricted signing policies for automated wallets.
References
- Uniswap v3 swap router and amountOutMinimum — https://docs.uniswap.org/contracts/v3/guides/swaps/single-swaps
- Uniswap v3 Quoter — https://docs.uniswap.org/contracts/v3/reference/periphery/lens/QuoterV2
- Ethereum JSON-RPC eth_call and eth_feeHistory — https://ethereum.org/en/developers/docs/apis/json-rpc/
- ethers v6 documentation — https://docs.ethers.org/v6/
Add a label
Embed this post
<iframe src="https://hibug.me/dex-swap-reverted-slippage-deadline" width="700" height="420" frameborder="0" loading="lazy"></iframe>
I build and run the platforms other engineers ship on: CI/CD pipelines, Kubernetes clusters, GPU inference services, LLM features in production, and the cloud bills that come with all of it. I have also spent years writing and operating automated trading systems, where a rounding bug or a missed websocket message is not a log line but a position you did not intend to hold. Each post here starts from a real failure: the alert, the exact error, what we ruled out, the root cause, and how we proved the fix. Same path every time: Investigate, Explain, Fix, Verify. No hype, no price predictions, just engineering.
Related Stories
Why paper trading results do not survive live execution
Comments
0No comments yet. Be the first to comment!
Sign In to post a comment.