Skip to content

feat(swapper): state-override gas estimation, replace tenderly - #12515

Merged
kaladinlight merged 12 commits into
developfrom
feat/evm-state-override-gas-estimation
Aug 3, 2026
Merged

feat(swapper): state-override gas estimation, replace tenderly#12515
kaladinlight merged 12 commits into
developfrom
feat/evm-state-override-gas-estimation

Conversation

@kaladinlight

@kaladinlight kaladinlight commented Aug 3, 2026

Copy link
Copy Markdown
Member

Description

Pre-approval ERC-20 sells revert eth_estimateGas, so executable quotes could never carry a real gas limit until the user approved — ArbitrumBridge, ButterSwap, THORChain/MAYAChain EVM, and BOB Gateway hard-failed pre-approval quotes outright, and Relay/Across/deBridge/Bebop failed whenever their provider omitted gas. This PR makes the estimation seat approval-aware and removes Tenderly from the swapper package.

State-override estimation (utils/evm/stateOverride.ts): the quote arm of getEvmNetworkFeeCryptoBaseUnit now reads the seller's actual allowance/balance first and, only when insufficient, re-estimates under a minimal node-level stateOverride (allowance/balance slot patched) on our own RPCs. Sufficient state estimates plainly as before — overrides only apply when they have to. All simulation work is bounded by a single global 3s timeout. All 35 EVM chains probed with negative controls: 34 support eth_estimateGas overrides (Scroll's l2geth cannot; only Relay/Across route Scroll and both normally supply provider gas).

Slot discovery handles arbitrary tokens: known-table guess first, then eth_createAccessList-assisted discovery — the node reports which slots balanceOf/allowance touch, and the mapping slot number is recovered by matching them against locally computed hashes (any slot number 0–255, both Solidity keccak(key‖slot) and Vyper keccak(slot‖key) orders, one RPC call; supported on 23/32 chains, with a raw-slot fallback for ERC-7201 namespaced storage) — then a batched candidate sweep across both layouts as last resort. Every result is validated by a sentinel probe (an eth_call under the override must echo the written value) before caching per chain+token. The sentinel fits uint96-packed storage, so UNI/COMP-style governance tokens discover correctly, and the override writes max(sellAmount, sentinel) rather than maxUint256 to keep flag bits and packed neighbors clear (USDC blacklist bit, packed balances). Genuinely un-overridable tokens (computed balances: stETH, aTokens, AMPL; shifted-packed: Katana AUSD) fail discovery cleanly, are negative-cached for 60s, and degrade to plain estimation — for funded/approved sellers nothing changes.

spenderAddress single-sourced from trade contexts: every EVM swapper threads the estimation spender through stepDataArgs from the same expression its context uses for allowanceContract ('' = no approval, same sentinel convention). This surfaced and fixed two latent spender mismatches: THORChain longtail pulls tokens through the aggregator token transfer proxy (not the aggregator the tx targets), and the Portals rate arm used the raw order target instead of the context's cross-chain-aware derivation. Simulating the spender the user will actually approve means a stale spender fails loudly at quote time instead of green-lighting a swap that breaks after approval.

Estimated gas limits buffered by default (1.2): previously split per swapper (ButterSwap/deBridge/BOB/Portals buffered, the rest raw). eth_estimateGas returns the bare minimum that succeeds and these quotes execute post-approval against moved state — an OOG revert burns the full limit while an oversized one refunds, and the displayed fee still prices the raw estimate, so the margin is effectively free. Provider-supplied limits are never buffered.

Tenderly removed from the swapper: the three rate-arm callers (Relay, Portals, NearIntents) now use the same native estimation, which also removes both Monad special-cases Tenderly required. Swapper Tenderly module, config, and public-api env plumbing deleted. The walletConnectToDapps Tenderly integration is separate (asset-change decoding has no estimateGas equivalent) and untouched.

Portals pre-approval quotes (validate: false fallback): Portals validates orders by simulating with the sender's live state, so unapproved senders failed at their API before our estimation ran. On validation failure the order is refetched without validation and our estimation supplies the gas limit. Probed extensively: the unvalidated response carries byte-identical calldata and the same minOutputAmount (fill protection unchanged) — and its outputAmount is the minOut, so the fallback quote displays the guaranteed worst-case fill and can never overstate. Validated orders are unchanged (sim-refined output, expected-slippage errors, padded gas).

public-api rate inputs are now well-formed (supportsEIP1559: false for EVM sell chains): GetEvmTradeRateInput pins the field and swappers discriminate EVM from Solana inputs on its presence — the rates endpoint omitted it, so Bebop routed EVM pairs to its Solana implementation and errored ("Bebop Solana quote not executable: undefined").

Deliberately out of scope (tracked separately): populating approvalTx/approvalTxs on the public-api wire, and USDT approve-to-zero reset detection.

Issue (if applicable)

closes #

Risk

High Risk PRs Require 2 approvals

Medium. Quote-time gas limits for previously-failing pre-approval token sells are now produced by state-override estimation — the same estimation engine over patched pre-state; the only systematic delta is the unrealized allowance-decrement refund (a few k gas conservative, absorbed by the buffer). Provider-supplied gas limits are still priced as-is everywhere they exist. Estimated limits across all swappers now carry the 1.2 margin (previously only four swappers did). No transaction construction, signing, or broadcast logic is touched — gas limits and displayed fees only.

What protocols, transaction types, wallets or contract interactions might be affected by this PR?

EVM swap quotes/rates across all swappers (gas limit + network fee display). No on-chain transaction shape changes.

Testing

Engineering

  • RPC support matrix: all 35 EVM chains probed for eth_call/eth_estimateGas state overrides with negative controls (silent param-ignoring caught): 34 PASS, Scroll FAIL (node software; all public Scroll RPCs checked).
  • Slot machinery: live-verified against every storage-layout family — known tables (USDT/eth 5/2, USDC Circle 10/9, USDT/arb 52/51), standard (SHIB, LINK, MKR), uint96-packed (UNI 4/3, COMP 1/0), Vyper (CRV, 3CRV 3/4), non-candidate slots (PAXG allowance 3) — discovery + estimation-under-override returned sane transfer gas for all; computed-balance tokens (stETH, AMPL) correctly fail discovery and degrade.
  • Chain-level validation: real discovery + override estimation run live on 33/34 EVM chains (each with a real stablecoin/major token, unfunded unapproved owner) — all pass except Scroll (known). Blast/Katana initially failed on rebasing/shifted-packed native tokens; both pass with normal tokens.
  • Public-RPC burst tests: worst-case sweep bursts (19 concurrent probes, retries off) against all 20 long-tail public primaries — zero rate limiting; bob/mantle drop requests above ~14 concurrent, hence the batched sweep (6 concurrent).
  • E2E via public-api (dead sender: zero balance, zero allowance): all 10 enabled swappers produce pre-approval quotes — THORChain token sell 79601 gas + approval.isRequired, ButterSwap ~1.5M, Relay 76k, Chainflip 63078, NearIntents 63078, MAYAChain 45488, Portals ~430k (via fallback), Bebop/0x provider gas preserved unchanged, CoW gasless. Zero estimation failures remain. Bebop EVM rates verified returning real amounts post input fix, with Solana sells still routing to the Solana implementation. Exotic-token pre-approval quotes through the full API flow: Portals UNI/CRV/COMP/PAXG and NearIntents UNI all pass with estimated gas limits (a dead sender can't estimate plainly, so passing proves the override path end to end).
  • Portals validate semantics: 10-route + slippage sweep confirming identical tx/minOut and worst-case-honest display (unvalidated outputAmount === minOutputAmount, drift tracks slippage 1:1).
  • pnpm type-check clean; full swapper test suite passing (unit tests mock getMinimalStateOverride since the override path reads live chain state).

Operations

  • Quote an unapproved ERC-20 sell (fresh wallet, no allowance) on THORChain/ButterSwap via the API or web — quote should return with a real network fee and approval requirement instead of failing.
  • Verify normal approved-sender swaps across a few EVM swappers display sane network fees (unchanged behavior).

Screenshots (if applicable)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved EVM network-fee estimation using temporary balance and allowance data.
    • Added more reliable rate and quote fallbacks when estimation is unavailable or times out.
    • EVM rate requests now account for non-EIP-1559 networks.
  • Bug Fixes

    • Improved spender and allowance handling across swap routes.
    • Increased gas-limit safety margins for more accurate fee estimates.
  • Chores

    • Removed obsolete Tenderly configuration and simulation support.

@kaladinlight
kaladinlight requested a review from a team as a code owner August 3, 2026 17:25
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change replaces Tenderly-based EVM fee simulation with local state-override estimation. Swapper flows now pass sell asset, amount, and spender data. Tenderly configuration and utilities are removed. Portals adds unvalidated-order fallback handling.

Changes

EVM fee estimation migration

Layer / File(s) Summary
State-override estimation core
packages/swapper/src/utils/evm/*
Adds balance and allowance overrides, storage-slot discovery, caching, timeout handling, and gas estimation integration.
Swapper integrations
packages/swapper/src/swappers/{AcrossSwapper,ArbitrumBridgeSwapper,BebopSwapper,BobGatewaySwapper,ButterSwap,ChainflipSwapper,DebridgeSwapper}/*, packages/swapper/src/utils/thorchain/*
Passes sell asset, sell amount, and spender data into EVM fee estimation.
Fallback and configuration flows
packages/swapper/src/swappers/{NearIntentsSwapper,PortalsSwapper,RelaySwapper}/*, packages/public-api/*, packages/swapper/src/types.ts
Replaces Tenderly simulation, removes Tenderly settings, and adds Portals fallback handling and EVM rate dispatch changes.
Integration validation
packages/swapper/src/swappers/{ButterSwap,ThorchainSwapper}/**/*.test.ts, packages/swapper/src/utils/test-data/*
Mocks state-override generation and updates gas-limit expectations and asset-map typing.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SwapperStepData
  participant FeeEstimator
  participant StateOverride
  participant EvmRpc
  SwapperStepData->>FeeEstimator: Provide transaction, sell asset, amount, and spender
  FeeEstimator->>StateOverride: Build minimal balance and allowance override
  StateOverride->>EvmRpc: Read balances and probe token storage slots
  StateOverride-->>FeeEstimator: Return state override or undefined
  FeeEstimator->>EvmRpc: Estimate gas with override
  EvmRpc-->>FeeEstimator: Return gas estimate
  FeeEstimator-->>SwapperStepData: Return buffered network fee data
Loading

Possibly related PRs

Suggested reviewers: 0xapotheosis

Poem

A rabbit maps the spender trail,
Then estimates gas with care.
State overrides fill missing funds,
While Tenderly leaves the lair.
Portals retries orders,
And clean fee data fills the air.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: state-override gas estimation and replacement of Tenderly in the swapper package.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/evm-state-override-gas-estimation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
packages/swapper/src/utils/evm/index.ts (1)

5-6: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

withTimeout is now public API under a generic name.

The barrel re-exports withTimeout from stateOverride. The name does not describe the EVM state-override context, and it is broad enough to collide with an existing timeout helper elsewhere in the monorepo. Either keep it module-internal, or rename it to reflect scope.

#!/bin/bash
# Check for existing withTimeout helpers and for out-of-module consumers.
rg -nP --type=ts -C2 '\bwithTimeout\b'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swapper/src/utils/evm/index.ts` around lines 5 - 6, Update the
state-override module and its barrel export so the generic withTimeout helper is
not exposed as public API: either keep it internal by removing its export, or
rename it to an EVM state-override-specific symbol and update all references and
exports accordingly. Use repository-wide search to ensure no consumers remain on
the ambiguous name.
packages/swapper/src/utils/evm/stateOverride.ts (3)

283-289: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Validate data before asserting it to Hex.

data as Hex on line 286 accepts any string. Callers pass provider-supplied calldata. If the value is not 0x-prefixed hex, estimateGas fails with an opaque Viem error instead of a clear message. Use isHex from viem as a guard, or type data as Hex in EstimateGasWithStateOverrideArgs and convert at the call sites.

As per coding guidelines: "NEVER use type assertions without proper validation in TypeScript".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swapper/src/utils/evm/stateOverride.ts` around lines 283 - 289,
Validate the provider-supplied data before the `estimateGas` call in the
surrounding state-override flow, rather than unconditionally asserting `data as
Hex`. Use Viem’s `isHex` guard (or make `EstimateGasWithStateOverrideArgs.data`
a validated `Hex` at all call sites) and reject invalid non-0x-prefixed calldata
with a clear error before invoking `client.estimateGas`.

Source: Coding guidelines


174-187: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the explicit return type and guard the sellAmountCryptoBaseUnit conversion.

Two points in this block:

  • needsNativeBalance on line 183 has no explicit return type. Add : Promise<boolean>.
  • BigInt(sellAmountCryptoBaseUnit) on line 176 throws SyntaxError for any non-integer string, for example a decimal amount from a provider response. Validate the input and return an explicit error instead.

As per coding guidelines: "ALWAYS use explicit types for function parameters and return values in TypeScript" and "ALWAYS validate inputs before processing with clear validation error messages and use early returns for validation failures".

♻️ Proposed change
   const valueBigInt = BigInt(value || '0')
-  const sellAmount = BigInt(sellAmountCryptoBaseUnit)
+  if (!/^\d+$/.test(sellAmountCryptoBaseUnit)) {
+    throw new Error(
+      `getMinimalStateOverride: sellAmountCryptoBaseUnit must be an integer base unit string, got '${sellAmountCryptoBaseUnit}'`,
+    )
+  }
+  const sellAmount = BigInt(sellAmountCryptoBaseUnit)
-  const needsNativeBalance = async () => {
+  const needsNativeBalance = async (): Promise<boolean> => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swapper/src/utils/evm/stateOverride.ts` around lines 174 - 187,
Update the state-override setup around sellAmountCryptoBaseUnit to validate that
the input is a valid integer string before converting it with BigInt, returning
a clear validation error through the surrounding function’s existing error path
when invalid. Add the explicit : Promise<boolean> return type to
needsNativeBalance while preserving its current balance-check behavior.

Source: Coding guidelines


118-153: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Cache discovery failures as well as successes.

discoveredSlots stores only found slots. When discovery fails, the next estimate for the same chain, token, and kind repeats the guess probe plus all remaining candidate probes. On an RPC that does not support stateOverride, every probe returns false, so each quote pays 8 RPC calls and then throws. The PR notes Scroll has no override support, so that chain hits this path for every token quote.

Consider a negative cache entry with a short TTL, and separately detect the "state override unsupported" case so the thrown error text is actionable.

♻️ Sketch
-const discoveredSlots = new Map<string, number>()
+// `null` records a failed discovery so repeat estimates skip the probe fan out
+const discoveredSlots = new Map<string, number | null>()
   const cached = discoveredSlots.get(cacheKey)
-  if (cached !== undefined) return cached
+  if (cached === null) throw new Error(`Unable to locate ${kind} storage slot for token ${tokenAddress}`)
+  if (cached !== undefined) return cached
   if (found === undefined) {
+    discoveredSlots.set(cacheKey, null)
     throw new Error(`Unable to locate ${kind} storage slot for token ${tokenAddress}`)
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swapper/src/utils/evm/stateOverride.ts` around lines 118 - 153,
Update discoverSlot and discoveredSlots to cache failed discoveries with a short
expiration, preventing repeated guess and candidate probes for the same chain,
token, and kind. Separately identify when probes indicate stateOverride is
unsupported, and throw an actionable error describing that limitation; preserve
normal slot-not-found errors for other failures and avoid caching expired
negative entries.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/swapper/src/swappers/BebopSwapper/endpoints.ts`:
- Around line 28-37: Add and use isBebopSolanaTradeQuoteInput and
isBebopSolanaTradeRateInput in getTradeQuote and getTradeRate to validate both
input.chainId and sellAsset.chainId before route-specific casts. When either
guard detects a mismatch, return Err(InvalidInput); otherwise preserve the
existing Solana and EVM handler routing.

In
`@packages/swapper/src/swappers/NearIntentsSwapper/utils/getNearIntentsStepData.ts`:
- Around line 83-95: Update the rate estimation fallback in the EVM branch of
getNearIntentsStepData so the catch path returns undefined instead of '0'.
Preserve the existing estimate call and NearIntentsRateStepData construction
while matching the file’s established undefined sentinel for unavailable fee
estimates.

In
`@packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts`:
- Around line 108-131: Restrict the validate:false retry in getPortalsTradeQuote
to errors explicitly identified as sender allowance or balance-state failures;
do not retry for other PortalsError values or network/axios failures. For all
non-matching failures, return the original error unchanged, preserving the
existing fallback error mapping only when the sender-state predicate matches.

In `@packages/swapper/src/utils/evm/getEvmNetworkFeeCryptoBaseUnit.ts`:
- Around line 60-66: Update the override argument construction in
getEvmNetworkFeeCryptoBaseUnit to pass spenderAddress through unchanged instead
of defaulting it to to. Preserve the existing caller opt-in behavior so
balance-only state overrides do not trigger allowance lookup or slot discovery.
- Around line 55-80: Update the state-override estimation block in
getEvmNetworkFeeCryptoBaseUnit so timeout, getMinimalStateOverride, and
estimateGasWithStateOverride rejections are caught and treated as an unavailable
override estimate, allowing execution to continue to the existing plain on-chain
gas estimation fallback. Preserve the current overriddenGasLimit success path
and only change the rejection handling; if rejection propagation is intentional
instead, revise the nearby fallback comment to match that behavior.

---

Nitpick comments:
In `@packages/swapper/src/utils/evm/index.ts`:
- Around line 5-6: Update the state-override module and its barrel export so the
generic withTimeout helper is not exposed as public API: either keep it internal
by removing its export, or rename it to an EVM state-override-specific symbol
and update all references and exports accordingly. Use repository-wide search to
ensure no consumers remain on the ambiguous name.

In `@packages/swapper/src/utils/evm/stateOverride.ts`:
- Around line 283-289: Validate the provider-supplied data before the
`estimateGas` call in the surrounding state-override flow, rather than
unconditionally asserting `data as Hex`. Use Viem’s `isHex` guard (or make
`EstimateGasWithStateOverrideArgs.data` a validated `Hex` at all call sites) and
reject invalid non-0x-prefixed calldata with a clear error before invoking
`client.estimateGas`.
- Around line 174-187: Update the state-override setup around
sellAmountCryptoBaseUnit to validate that the input is a valid integer string
before converting it with BigInt, returning a clear validation error through the
surrounding function’s existing error path when invalid. Add the explicit :
Promise<boolean> return type to needsNativeBalance while preserving its current
balance-check behavior.
- Around line 118-153: Update discoverSlot and discoveredSlots to cache failed
discoveries with a short expiration, preventing repeated guess and candidate
probes for the same chain, token, and kind. Separately identify when probes
indicate stateOverride is unsupported, and throw an actionable error describing
that limitation; preserve normal slot-not-found errors for other failures and
avoid caching expired negative entries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c48ca597-58c2-4b0d-8505-cdf188e13a85

📥 Commits

Reviewing files that changed from the base of the PR and between ad3b3f0 and 54a335c.

📒 Files selected for processing (28)
  • packages/public-api/.env.example
  • packages/public-api/src/config.ts
  • packages/public-api/src/env.ts
  • packages/swapper/src/swappers/AcrossSwapper/utils/getAcrossStepData.ts
  • packages/swapper/src/swappers/AcrossSwapper/utils/getAcrossTradeContext.ts
  • packages/swapper/src/swappers/ArbitrumBridgeSwapper/utils/getArbitrumBridgeStepData.ts
  • packages/swapper/src/swappers/BebopSwapper/endpoints.ts
  • packages/swapper/src/swappers/BebopSwapper/utils/getBebopStepData.ts
  • packages/swapper/src/swappers/BebopSwapper/utils/getBebopTradeContext.ts
  • packages/swapper/src/swappers/BobGatewaySwapper/utils/getBobGatewayStepData.ts
  • packages/swapper/src/swappers/ButterSwap/utils/getButterSwapStepData.ts
  • packages/swapper/src/swappers/ChainflipSwapper/utils/getChainflipStepData.ts
  • packages/swapper/src/swappers/DebridgeSwapper/utils/getDebridgeStepData.ts
  • packages/swapper/src/swappers/NearIntentsSwapper/utils/getNearIntentsStepData.ts
  • packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts
  • packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeRate/getPortalsTradeRate.ts
  • packages/swapper/src/swappers/PortalsSwapper/utils/fetchPortalsTradeOrder.ts
  • packages/swapper/src/swappers/PortalsSwapper/utils/getPortalsStepData.ts
  • packages/swapper/src/swappers/RelaySwapper/utils/getRelayStepData.ts
  • packages/swapper/src/types.ts
  • packages/swapper/src/utils/evm/getEvmNetworkFeeCryptoBaseUnit.ts
  • packages/swapper/src/utils/evm/index.ts
  • packages/swapper/src/utils/evm/stateOverride.ts
  • packages/swapper/src/utils/evm/storageSlots.ts
  • packages/swapper/src/utils/tenderly/index.ts
  • packages/swapper/src/utils/tenderly/simulate.ts
  • packages/swapper/src/utils/tenderly/types.ts
  • packages/swapper/src/utils/thorchain/getThorStepData.ts
💤 Files with no reviewable changes (7)
  • packages/public-api/.env.example
  • packages/swapper/src/utils/tenderly/index.ts
  • packages/public-api/src/config.ts
  • packages/swapper/src/types.ts
  • packages/swapper/src/utils/tenderly/simulate.ts
  • packages/swapper/src/utils/tenderly/types.ts
  • packages/public-api/src/env.ts

Comment thread packages/swapper/src/swappers/BebopSwapper/endpoints.ts Outdated
Comment thread packages/swapper/src/utils/evm/getEvmNetworkFeeCryptoBaseUnit.ts
Comment thread packages/swapper/src/utils/evm/getEvmNetworkFeeCryptoBaseUnit.ts
@kaladinlight
kaladinlight force-pushed the feat/evm-state-override-gas-estimation branch 5 times, most recently from d4d5f8f to 0afe910 Compare August 3, 2026 18:24

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
packages/swapper/src/swappers/ButterSwap/utils/getButterSwapStepData.test.ts (1)

73-73: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Use valid spender fixtures in EVM tests.

spenderAddress: '' is not a valid EVM address. Because getMinimalStateOverride is mocked to return undefined, these tests do not validate spender propagation into the approval-aware estimation path. Use a valid spender fixture in the EVM cases at Lines 73, 89, 108, 136, and 157.

The production fee helper forwards spenderAddress to getMinimalStateOverride in packages/swapper/src/utils/evm/getEvmNetworkFeeCryptoBaseUnit.ts, Lines 60-73.

Also applies to: 89-89, 108-108, 136-136, 157-157

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swapper/src/swappers/ButterSwap/utils/getButterSwapStepData.test.ts`
at line 73, Replace the empty spenderAddress fixtures in the EVM test cases of
getButterSwapStepData with a valid EVM address, using the same fixture
consistently at the identified cases. Ensure the tests exercise spender
propagation through getMinimalStateOverride and the approval-aware estimation
path.
packages/swapper/src/swappers/ButterSwap/swapperApi/getTradeQuote.test.ts (1)

35-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a type-safe mock pattern in both test files.

Both mocks widen the original module to object and rely on inferred callback types.

  • packages/swapper/src/swappers/ButterSwap/swapperApi/getTradeQuote.test.ts#L35-L37: use the stateOverride module type and explicit factory types.
  • packages/swapper/src/swappers/ButterSwap/utils/getButterSwapStepData.test.ts#L14-L16: apply the same typed mock pattern.

As per coding guidelines, TypeScript parameters and return values require explicit types, and object shapes require explicit types.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swapper/src/swappers/ButterSwap/swapperApi/getTradeQuote.test.ts`
around lines 35 - 37, Update the vi.mock factories in
packages/swapper/src/swappers/ButterSwap/swapperApi/getTradeQuote.test.ts:35-37
and
packages/swapper/src/swappers/ButterSwap/utils/getButterSwapStepData.test.ts:14-16
to use the stateOverride module type instead of object, with explicit factory
parameter and return types; preserve the existing getMinimalStateOverride mock
behavior.

Source: Coding guidelines

packages/swapper/src/swappers/ButterSwap/utils/getButterSwapTradeContext.ts (1)

142-172: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Deduplicate the route.contract ?? '' computation.

allowanceContract (line 150) and spenderAddress (line 169) derive from the same expression route.contract ?? '', computed independently. If a future edit changes one without the other, the approval target and the gas-estimation spender diverge. getDebridgeTradeContext.ts avoids this by computing the value once and reusing it for both fields.

♻️ Proposed fix to compute the address once
+  const allowanceContract = route.contract ?? ''
+
   return Ok({
     tradeCommon: {
       id: route.hash,
       rate,
       swapperName: SwapperName.ButterSwap,
       affiliateBps,
       isStreaming: false,
       slippageTolerancePercentageDecimal,
     },
     stepCommon: {
       rate,
       buyAmountBeforeFeesCryptoBaseUnit: buyAmountAfterFeesCryptoBaseUnit,
       buyAmountAfterFeesCryptoBaseUnit,
       sellAmountIncludingProtocolFeesCryptoBaseUnit,
       source: SwapperName.ButterSwap,
       buyAsset,
       sellAsset,
-      allowanceContract: route.contract ?? '',
+      allowanceContract,
       estimatedExecutionTimeMs: route.timeEstimated * 1000,
       ...
     },
     protocolFees: undefined,
     route,
     stepDataArgs: {
       route,
       feeAsset,
       sellAmountCryptoBaseUnit: sellAmountIncludingProtocolFeesCryptoBaseUnit,
       sellAsset,
-      spenderAddress: route.contract ?? '',
+      spenderAddress: allowanceContract,
       deps,
     },
   })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swapper/src/swappers/ButterSwap/utils/getButterSwapTradeContext.ts`
around lines 142 - 172, In getButterSwapTradeContext, compute route.contract ??
'' once in a local variable before constructing the trade context, then reuse
that variable for both stepCommon.allowanceContract and
stepDataArgs.spenderAddress so the approval target and spender remain
consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/swapper/src/swappers/ButterSwap/swapperApi/getTradeQuote.test.ts`:
- Around line 35-37: Update the vi.mock factories in
packages/swapper/src/swappers/ButterSwap/swapperApi/getTradeQuote.test.ts:35-37
and
packages/swapper/src/swappers/ButterSwap/utils/getButterSwapStepData.test.ts:14-16
to use the stateOverride module type instead of object, with explicit factory
parameter and return types; preserve the existing getMinimalStateOverride mock
behavior.

In
`@packages/swapper/src/swappers/ButterSwap/utils/getButterSwapStepData.test.ts`:
- Line 73: Replace the empty spenderAddress fixtures in the EVM test cases of
getButterSwapStepData with a valid EVM address, using the same fixture
consistently at the identified cases. Ensure the tests exercise spender
propagation through getMinimalStateOverride and the approval-aware estimation
path.

In `@packages/swapper/src/swappers/ButterSwap/utils/getButterSwapTradeContext.ts`:
- Around line 142-172: In getButterSwapTradeContext, compute route.contract ??
'' once in a local variable before constructing the trade context, then reuse
that variable for both stepCommon.allowanceContract and
stepDataArgs.spenderAddress so the approval target and spender remain
consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7a95f69a-7cc1-40a2-96e3-adb91140ca74

📥 Commits

Reviewing files that changed from the base of the PR and between 54a335c and 1698645.

📒 Files selected for processing (38)
  • packages/public-api/.env.example
  • packages/public-api/src/config.ts
  • packages/public-api/src/env.ts
  • packages/public-api/src/routes/rates/getRates.ts
  • packages/swapper/src/swappers/AcrossSwapper/utils/getAcrossStepData.ts
  • packages/swapper/src/swappers/AcrossSwapper/utils/getAcrossTradeContext.ts
  • packages/swapper/src/swappers/ArbitrumBridgeSwapper/utils/getArbitrumBridgeStepData.ts
  • packages/swapper/src/swappers/ArbitrumBridgeSwapper/utils/getArbitrumBridgeTradeContext.ts
  • packages/swapper/src/swappers/BebopSwapper/utils/getBebopStepData.ts
  • packages/swapper/src/swappers/BebopSwapper/utils/getBebopTradeContext.ts
  • packages/swapper/src/swappers/BobGatewaySwapper/utils/getBobGatewayStepData.ts
  • packages/swapper/src/swappers/BobGatewaySwapper/utils/getBobGatewayTradeContext.ts
  • packages/swapper/src/swappers/ButterSwap/swapperApi/getTradeQuote.test.ts
  • packages/swapper/src/swappers/ButterSwap/utils/getButterSwapStepData.test.ts
  • packages/swapper/src/swappers/ButterSwap/utils/getButterSwapStepData.ts
  • packages/swapper/src/swappers/ButterSwap/utils/getButterSwapTradeContext.ts
  • packages/swapper/src/swappers/ChainflipSwapper/utils/getChainflipStepData.ts
  • packages/swapper/src/swappers/DebridgeSwapper/utils/getDebridgeStepData.ts
  • packages/swapper/src/swappers/DebridgeSwapper/utils/getDebridgeTradeContext.ts
  • packages/swapper/src/swappers/NearIntentsSwapper/utils/getNearIntentsStepData.ts
  • packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts
  • packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeRate/getPortalsTradeRate.ts
  • packages/swapper/src/swappers/PortalsSwapper/utils/fetchPortalsTradeOrder.ts
  • packages/swapper/src/swappers/PortalsSwapper/utils/getPortalsStepData.ts
  • packages/swapper/src/swappers/PortalsSwapper/utils/getPortalsTradeContext.ts
  • packages/swapper/src/swappers/RelaySwapper/getTradeQuote/getTradeQuote.ts
  • packages/swapper/src/swappers/RelaySwapper/getTradeRate/getTradeRate.ts
  • packages/swapper/src/swappers/RelaySwapper/utils/getRelayStepData.ts
  • packages/swapper/src/swappers/RelaySwapper/utils/getRelayTradeContext.ts
  • packages/swapper/src/types.ts
  • packages/swapper/src/utils/evm/getEvmNetworkFeeCryptoBaseUnit.ts
  • packages/swapper/src/utils/evm/index.ts
  • packages/swapper/src/utils/evm/stateOverride.ts
  • packages/swapper/src/utils/evm/storageSlots.ts
  • packages/swapper/src/utils/tenderly/index.ts
  • packages/swapper/src/utils/tenderly/simulate.ts
  • packages/swapper/src/utils/tenderly/types.ts
  • packages/swapper/src/utils/thorchain/getThorStepData.ts
💤 Files with no reviewable changes (7)
  • packages/swapper/src/utils/tenderly/index.ts
  • packages/swapper/src/utils/tenderly/simulate.ts
  • packages/swapper/src/types.ts
  • packages/public-api/src/config.ts
  • packages/swapper/src/utils/tenderly/types.ts
  • packages/public-api/.env.example
  • packages/public-api/src/env.ts
🚧 Files skipped from review as they are similar to previous changes (16)
  • packages/swapper/src/utils/evm/storageSlots.ts
  • packages/swapper/src/swappers/AcrossSwapper/utils/getAcrossTradeContext.ts
  • packages/swapper/src/swappers/ChainflipSwapper/utils/getChainflipStepData.ts
  • packages/swapper/src/swappers/BebopSwapper/utils/getBebopStepData.ts
  • packages/swapper/src/swappers/BobGatewaySwapper/utils/getBobGatewayStepData.ts
  • packages/swapper/src/utils/thorchain/getThorStepData.ts
  • packages/swapper/src/swappers/BebopSwapper/utils/getBebopTradeContext.ts
  • packages/swapper/src/swappers/AcrossSwapper/utils/getAcrossStepData.ts
  • packages/swapper/src/swappers/NearIntentsSwapper/utils/getNearIntentsStepData.ts
  • packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts
  • packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeRate/getPortalsTradeRate.ts
  • packages/swapper/src/utils/evm/stateOverride.ts
  • packages/swapper/src/utils/evm/index.ts
  • packages/swapper/src/utils/evm/getEvmNetworkFeeCryptoBaseUnit.ts
  • packages/swapper/src/swappers/PortalsSwapper/utils/fetchPortalsTradeOrder.ts
  • packages/swapper/src/swappers/DebridgeSwapper/utils/getDebridgeStepData.ts

@kaladinlight
kaladinlight force-pushed the feat/evm-state-override-gas-estimation branch from 1698645 to ce3d879 Compare August 3, 2026 19:40

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/swapper/src/swappers/ThorchainSwapper/getTradeQuote/getTradeQuote.test.ts (2)

40-45: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert that quote estimation calls getMinimalStateOverride.

Returning undefined prevents live chain access, but the test does not prove that Thorchain estimation invokes the helper. Add an assertion for the call and its sell-state and spender inputs. This also detects an incorrect mock path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/swapper/src/swappers/ThorchainSwapper/getTradeQuote/getTradeQuote.test.ts`
around lines 40 - 45, Update the test around the mocked getMinimalStateOverride
in getTradeQuote.test.ts to assert that quote estimation invokes it with the
expected sell-state and spender arguments. Keep the undefined mock result so
estimation remains isolated from live chain state, and ensure the assertion
verifies the intended mock path.

42-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add explicit types to the mock factory.

importOriginal and the async factory return type are inferred. importOriginal<object>() also hides the module shape. Define a module-shaped type and annotate the callback parameter and return value. Verify the Vitest mock-factory signature when adding these annotations.

As per coding guidelines, TypeScript functions must use explicit parameter and return types, and object shapes must use interfaces or type aliases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/swapper/src/swappers/ThorchainSwapper/getTradeQuote/getTradeQuote.test.ts`
around lines 42 - 45, Update the vi.mock factory for stateOverride to use an
explicit module-shaped type alias or interface, annotate the importOriginal
callback parameter with the appropriate Vitest type, and annotate the async
factory return type. Replace importOriginal<object>() with the defined module
shape while preserving the existing mocked getMinimalStateOverride behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@packages/swapper/src/swappers/ThorchainSwapper/getTradeQuote/getTradeQuote.test.ts`:
- Around line 40-45: Update the test around the mocked getMinimalStateOverride
in getTradeQuote.test.ts to assert that quote estimation invokes it with the
expected sell-state and spender arguments. Keep the undefined mock result so
estimation remains isolated from live chain state, and ensure the assertion
verifies the intended mock path.
- Around line 42-45: Update the vi.mock factory for stateOverride to use an
explicit module-shaped type alias or interface, annotate the importOriginal
callback parameter with the appropriate Vitest type, and annotate the async
factory return type. Replace importOriginal<object>() with the defined module
shape while preserving the existing mocked getMinimalStateOverride behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9cd6875c-ac15-43cd-9342-288fafd33f6d

📥 Commits

Reviewing files that changed from the base of the PR and between 1698645 and ce3d879.

📒 Files selected for processing (6)
  • packages/swapper/src/swappers/BobGatewaySwapper/utils/getBobGatewayStepData.ts
  • packages/swapper/src/swappers/ButterSwap/utils/getButterSwapStepData.ts
  • packages/swapper/src/swappers/DebridgeSwapper/utils/getDebridgeStepData.ts
  • packages/swapper/src/swappers/PortalsSwapper/utils/getPortalsStepData.ts
  • packages/swapper/src/swappers/ThorchainSwapper/getTradeQuote/getTradeQuote.test.ts
  • packages/swapper/src/utils/evm/getEvmNetworkFeeCryptoBaseUnit.ts
💤 Files with no reviewable changes (3)
  • packages/swapper/src/swappers/BobGatewaySwapper/utils/getBobGatewayStepData.ts
  • packages/swapper/src/swappers/PortalsSwapper/utils/getPortalsStepData.ts
  • packages/swapper/src/swappers/DebridgeSwapper/utils/getDebridgeStepData.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/swapper/src/utils/evm/getEvmNetworkFeeCryptoBaseUnit.ts
  • packages/swapper/src/swappers/ButterSwap/utils/getButterSwapStepData.ts

kaladinlight and others added 4 commits August 3, 2026 13:57
Pre-approval token sells revert eth_estimateGas, so executable quotes could
never carry a real gas limit before the user approved. The estimation seat now
reads the seller's actual allowance/balance first and, only when insufficient,
re-estimates under a minimal node-level stateOverride (allowance/balance slot
patched) on our own RPCs. Storage slots are validated against the known-slot
tables via an eth_call under the override, with candidate probing and per-token
caching for unknown layouts.

Rate paths that simulated through Tenderly (Relay, Portals, NearIntents) now
use the same native estimation, which also removes the Monad special-casing
Tenderly required. The swapper Tenderly module and its config are gone; the
walletConnectToDapps integration is separate and untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Portals validates orders by simulating with the sender's live state, so an
unapproved sender failed the quote outright. On validation failure the order
is refetched without validation - the response carries identical calldata and
the same minOutputAmount but no gas limit, which the state-override estimation
supplies (buffered). Validated orders are unchanged: sim-refined output amount,
expected-slippage errors, and Portals' padded gas limit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The estimation spender was sourced three different ways (context-threaded,
re-derived, inline tx.to) plus an implicit tx.to default in the seat that
caused needless allowance reads for no-approval swappers. Every context now
threads spenderAddress (one name, replacing bebop's approvalTarget) from its
allowanceContract derivation, and the seat does nothing implicit.

Also fixes the thor longtail spender: swapIn pulls tokens through the
aggregator token transfer proxy, not the aggregator itself, so the override
patched the wrong allowance slot and longtail pre-approval estimation still
reverted. Portals' rate arm had the same latent mismatch (target vs the
context's cross-chain-aware derivation).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GetEvmTradeRateInput pins supportsEIP1559: false and swappers discriminate
evm from solana inputs on its presence - the rates endpoint omitted it, so
bebop routed evm pairs to its solana implementation and errored with
"Bebop Solana quote not executable: undefined".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kaladinlight
kaladinlight force-pushed the feat/evm-state-override-gas-estimation branch from ce3d879 to 231aac1 Compare August 3, 2026 19:57
Estimated limits now carry a uniform 1.2 safety margin instead of the
previous per-swapper split (butter/debridge/bob/portals buffered, the rest
raw). eth_estimateGas returns the bare minimum that succeeds and quotes
execute post-approval against moved state - an OOG revert burns the full
limit while an oversized one refunds, and the displayed fee still prices
the raw estimate, so the margin costs nothing. Provider-supplied limits
remain priced and carried as-is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kaladinlight
kaladinlight force-pushed the feat/evm-state-override-gas-estimation branch from 231aac1 to b06cd38 Compare August 3, 2026 20:01

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
packages/swapper/src/swappers/NearIntentsSwapper/utils/getNearIntentsStepData.ts (1)

83-90: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Rate failure still returns '0' instead of the file's undefined sentinel.

Every other rate path in this file uses networkFeeCryptoBaseUnit: undefined to signal that no fee estimate is available. See lines 209, 230, 251, 268, and the outer catch on line 297. This EVM branch returns '0', which a consumer reads as a free transaction, not as a failed estimate.

🐛 Proposed fix
             try {
               return await estimate()
             } catch {
-              return '0'
+              return undefined
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/swapper/src/swappers/NearIntentsSwapper/utils/getNearIntentsStepData.ts`
around lines 83 - 90, Update the rate branch in getNearIntentsStepData so a
failed estimate() returns the file’s undefined sentinel instead of the string
'0'. Preserve the successful estimate result and align this catch with the
existing networkFeeCryptoBaseUnit: undefined paths.
packages/swapper/src/utils/evm/getEvmNetworkFeeCryptoBaseUnit.ts (1)

55-79: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Override-estimation failures still bypass the on-chain fallback.

withTimeout rejects on failure. Three reachable paths reject here:

  • The internal 3s timeout fires.
  • getMinimalStateOverride throws, for example Unable to locate ${kind} storage slot for token ... from discoverSlot.
  • estimateGasWithStateOverride throws because the RPC rejects stateOverride (the PR notes Scroll).

Each rejection propagates out of getEvmNetworkFeeCryptoBaseUnit, so lines 83-84 never run. The comment on lines 81-82 states that the plain on-chain estimation always sets a gas limit. Callers such as getPortalsStepData, getBebopStepData, getArbitrumBridgeStepData, and getThorStepData map the rejection to makeNetworkFeeEstimationFailedErr, so the quote fails instead of falling back.

🛠️ Proposed fix to preserve the fallback
         return estimateGasWithStateOverride({ ...overrideArgs, to, data, stateOverride })
       })(),
-    )
+      // Override estimation is best effort - fall through to plain on-chain estimation
+    ).catch(() => undefined)

If rejection is intentional for executable quotes, update the comment on lines 81-82 to describe that.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swapper/src/utils/evm/getEvmNetworkFeeCryptoBaseUnit.ts` around
lines 55 - 79, Handle rejections from the state-override estimation block in
getEvmNetworkFeeCryptoBaseUnit so timeout, getMinimalStateOverride, or
estimateGasWithStateOverride failures do not propagate. Catch the failure,
continue to the existing plain on-chain gas estimation fallback, and preserve
the overridden result path when it succeeds.
🧹 Nitpick comments (2)
packages/swapper/src/utils/test-data/cryptoMarketDataById.ts (1)

18-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the inline market-data shape into a named type.

Define a named interface or type alias for { price: string }, then use it in Record<AssetId, ...>.

As per coding guidelines: “ALWAYS use explicit types for object shapes using interfaces or type aliases in TypeScript.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swapper/src/utils/test-data/cryptoMarketDataById.ts` at line 18,
Define a named interface or type alias for the market-data object shape
containing price: string, then update marketDataByAssetIdUsd to use that named
type as the Record value type instead of the inline object shape.

Source: Coding guidelines

packages/swapper/src/utils/evm/getEvmNetworkFeeCryptoBaseUnit.ts (1)

58-73: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Redundant timeout nesting doubles the worst-case budget.

estimateGasWithStateOverride already wraps its work in withTimeout (see packages/swapper/src/utils/evm/stateOverride.ts lines 267-288). The two timers are independent, so slot discovery can consume the outer 3s and the inner estimate can then start a fresh 3s. Rates run inside the UI's 10s bulk budget per the comment in stateOverride.ts line 16. Consider calling getMinimalStateOverride and client.estimateGas under a single timeout, or drop the outer withTimeout here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swapper/src/utils/evm/getEvmNetworkFeeCryptoBaseUnit.ts` around
lines 58 - 73, Remove the outer withTimeout wrapper around the async block in
getEvmNetworkFeeCryptoBaseUnit, since estimateGasWithStateOverride already
enforces its own timeout. Preserve the existing overrideArgs construction,
getMinimalStateOverride early return, and estimateGasWithStateOverride
invocation without introducing a second timeout.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/swapper/src/utils/evm/stateOverride.ts`:
- Around line 133-150: Update discoverSlot so failed slot discovery is cached
before throwing, including cases where every probeSlot result is false. Add or
reuse a negative-cache map keyed by cacheKey with a short expiry, check it
before probing, and record the failure in the found === undefined branch to
prevent repeated discovery attempts while allowing retries after expiration.

---

Duplicate comments:
In
`@packages/swapper/src/swappers/NearIntentsSwapper/utils/getNearIntentsStepData.ts`:
- Around line 83-90: Update the rate branch in getNearIntentsStepData so a
failed estimate() returns the file’s undefined sentinel instead of the string
'0'. Preserve the successful estimate result and align this catch with the
existing networkFeeCryptoBaseUnit: undefined paths.

In `@packages/swapper/src/utils/evm/getEvmNetworkFeeCryptoBaseUnit.ts`:
- Around line 55-79: Handle rejections from the state-override estimation block
in getEvmNetworkFeeCryptoBaseUnit so timeout, getMinimalStateOverride, or
estimateGasWithStateOverride failures do not propagate. Catch the failure,
continue to the existing plain on-chain gas estimation fallback, and preserve
the overridden result path when it succeeds.

---

Nitpick comments:
In `@packages/swapper/src/utils/evm/getEvmNetworkFeeCryptoBaseUnit.ts`:
- Around line 58-73: Remove the outer withTimeout wrapper around the async block
in getEvmNetworkFeeCryptoBaseUnit, since estimateGasWithStateOverride already
enforces its own timeout. Preserve the existing overrideArgs construction,
getMinimalStateOverride early return, and estimateGasWithStateOverride
invocation without introducing a second timeout.

In `@packages/swapper/src/utils/test-data/cryptoMarketDataById.ts`:
- Line 18: Define a named interface or type alias for the market-data object
shape containing price: string, then update marketDataByAssetIdUsd to use that
named type as the Record value type instead of the inline object shape.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5cdb06e4-cb7a-436b-a885-d62f4d34116f

📥 Commits

Reviewing files that changed from the base of the PR and between ce3d879 and 231aac1.

📒 Files selected for processing (40)
  • packages/public-api/.env.example
  • packages/public-api/src/config.ts
  • packages/public-api/src/env.ts
  • packages/public-api/src/routes/rates/getRates.ts
  • packages/swapper/src/swappers/AcrossSwapper/utils/getAcrossStepData.ts
  • packages/swapper/src/swappers/AcrossSwapper/utils/getAcrossTradeContext.ts
  • packages/swapper/src/swappers/ArbitrumBridgeSwapper/utils/getArbitrumBridgeStepData.ts
  • packages/swapper/src/swappers/ArbitrumBridgeSwapper/utils/getArbitrumBridgeTradeContext.ts
  • packages/swapper/src/swappers/BebopSwapper/utils/getBebopStepData.ts
  • packages/swapper/src/swappers/BebopSwapper/utils/getBebopTradeContext.ts
  • packages/swapper/src/swappers/BobGatewaySwapper/utils/getBobGatewayStepData.ts
  • packages/swapper/src/swappers/BobGatewaySwapper/utils/getBobGatewayTradeContext.ts
  • packages/swapper/src/swappers/ButterSwap/swapperApi/getTradeQuote.test.ts
  • packages/swapper/src/swappers/ButterSwap/utils/getButterSwapStepData.test.ts
  • packages/swapper/src/swappers/ButterSwap/utils/getButterSwapStepData.ts
  • packages/swapper/src/swappers/ButterSwap/utils/getButterSwapTradeContext.ts
  • packages/swapper/src/swappers/ChainflipSwapper/utils/getChainflipStepData.ts
  • packages/swapper/src/swappers/DebridgeSwapper/utils/getDebridgeStepData.ts
  • packages/swapper/src/swappers/DebridgeSwapper/utils/getDebridgeTradeContext.ts
  • packages/swapper/src/swappers/NearIntentsSwapper/utils/getNearIntentsStepData.ts
  • packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts
  • packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeRate/getPortalsTradeRate.ts
  • packages/swapper/src/swappers/PortalsSwapper/utils/fetchPortalsTradeOrder.ts
  • packages/swapper/src/swappers/PortalsSwapper/utils/getPortalsStepData.ts
  • packages/swapper/src/swappers/PortalsSwapper/utils/getPortalsTradeContext.ts
  • packages/swapper/src/swappers/RelaySwapper/getTradeQuote/getTradeQuote.ts
  • packages/swapper/src/swappers/RelaySwapper/getTradeRate/getTradeRate.ts
  • packages/swapper/src/swappers/RelaySwapper/utils/getRelayStepData.ts
  • packages/swapper/src/swappers/RelaySwapper/utils/getRelayTradeContext.ts
  • packages/swapper/src/swappers/ThorchainSwapper/getTradeQuote/getTradeQuote.test.ts
  • packages/swapper/src/types.ts
  • packages/swapper/src/utils/evm/getEvmNetworkFeeCryptoBaseUnit.ts
  • packages/swapper/src/utils/evm/index.ts
  • packages/swapper/src/utils/evm/stateOverride.ts
  • packages/swapper/src/utils/evm/storageSlots.ts
  • packages/swapper/src/utils/tenderly/index.ts
  • packages/swapper/src/utils/tenderly/simulate.ts
  • packages/swapper/src/utils/tenderly/types.ts
  • packages/swapper/src/utils/test-data/cryptoMarketDataById.ts
  • packages/swapper/src/utils/thorchain/getThorStepData.ts
💤 Files with no reviewable changes (7)
  • packages/swapper/src/types.ts
  • packages/public-api/.env.example
  • packages/swapper/src/utils/tenderly/types.ts
  • packages/swapper/src/utils/tenderly/index.ts
  • packages/public-api/src/config.ts
  • packages/public-api/src/env.ts
  • packages/swapper/src/utils/tenderly/simulate.ts
🚧 Files skipped from review as they are similar to previous changes (29)
  • packages/swapper/src/swappers/RelaySwapper/getTradeRate/getTradeRate.ts
  • packages/swapper/src/utils/evm/storageSlots.ts
  • packages/swapper/src/swappers/DebridgeSwapper/utils/getDebridgeTradeContext.ts
  • packages/swapper/src/swappers/RelaySwapper/utils/getRelayTradeContext.ts
  • packages/swapper/src/swappers/AcrossSwapper/utils/getAcrossTradeContext.ts
  • packages/swapper/src/swappers/BobGatewaySwapper/utils/getBobGatewayTradeContext.ts
  • packages/swapper/src/swappers/ButterSwap/utils/getButterSwapStepData.ts
  • packages/swapper/src/swappers/RelaySwapper/getTradeQuote/getTradeQuote.ts
  • packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts
  • packages/public-api/src/routes/rates/getRates.ts
  • packages/swapper/src/utils/evm/index.ts
  • packages/swapper/src/swappers/BebopSwapper/utils/getBebopStepData.ts
  • packages/swapper/src/swappers/BobGatewaySwapper/utils/getBobGatewayStepData.ts
  • packages/swapper/src/utils/thorchain/getThorStepData.ts
  • packages/swapper/src/swappers/DebridgeSwapper/utils/getDebridgeStepData.ts
  • packages/swapper/src/swappers/ThorchainSwapper/getTradeQuote/getTradeQuote.test.ts
  • packages/swapper/src/swappers/ButterSwap/utils/getButterSwapTradeContext.ts
  • packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeRate/getPortalsTradeRate.ts
  • packages/swapper/src/swappers/AcrossSwapper/utils/getAcrossStepData.ts
  • packages/swapper/src/swappers/PortalsSwapper/utils/getPortalsTradeContext.ts
  • packages/swapper/src/swappers/ButterSwap/swapperApi/getTradeQuote.test.ts
  • packages/swapper/src/swappers/ArbitrumBridgeSwapper/utils/getArbitrumBridgeTradeContext.ts
  • packages/swapper/src/swappers/BebopSwapper/utils/getBebopTradeContext.ts
  • packages/swapper/src/swappers/PortalsSwapper/utils/getPortalsStepData.ts
  • packages/swapper/src/swappers/ArbitrumBridgeSwapper/utils/getArbitrumBridgeStepData.ts
  • packages/swapper/src/swappers/PortalsSwapper/utils/fetchPortalsTradeOrder.ts
  • packages/swapper/src/swappers/RelaySwapper/utils/getRelayStepData.ts
  • packages/swapper/src/swappers/ButterSwap/utils/getButterSwapStepData.test.ts
  • packages/swapper/src/swappers/ChainflipSwapper/utils/getChainflipStepData.ts

Comment thread packages/swapper/src/utils/evm/stateOverride.ts Outdated
kaladinlight and others added 4 commits August 3, 2026 14:05
…gative cache

- Override-path failures (timeout, undiscoverable slot, rpc without
  stateOverride support) now fall through to plain estimation instead of
  rejecting the quote outright - sufficient-state sellers survive transient
  read failures, insufficient ones fail identically just later
- NearIntents evm rate failure returns undefined like the file's other
  namespaces, not '0' masquerading as a free transaction
- Failed slot discoveries are cached with a 60s ttl so unknown layouts and
  unsupported rpcs don't pay the full candidate sweep on every rate refresh

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… uint96)

- probe with a sub-uint96 sentinel so packed-storage tokens (UNI/COMP) read
  it back intact, and write the trade-sized value instead of maxUint256 to
  keep flag bits and packed neighbors clear
- support the vyper mapping hash order (Curve ecosystem)
- discover arbitrary layouts via eth_createAccessList preimage matching
  (supported on 23/32 EVM chains), with a widened dual-layout candidate
  sweep as fallback
- discover balance and allowance slots in parallel

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- omit the access-list account so gas-charging nodes (plume-style) accept
  unfunded owners - node defaults to the zero address, which never affects
  the slots a read touches (fixes a 61s sweep observed on plume)
- raw-slot fallback for namespaced storage (ERC-7201) - when no slot
  number is recoverable, sentinel-probe the touched slots directly and
  cache per owner
- batch the candidate sweep (6 concurrent) - public rpcs on long-tail
  chains drop requests above ~14 concurrent, and an early hit now skips
  the remaining batches

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/swapper/src/utils/evm/stateOverride.ts (2)

40-45: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Bound the discovery caches.

The three maps never evict entries. discoveredSlots and failedSlotDiscoveries are bounded by chain and token count, but rawDiscoveredSlots embeds the owner and the spender in its key, so its key space grows with every distinct trader. failedSlotDiscoveries also keeps expired keys forever, because the TTL check on Line 220 never deletes them. In the long-lived packages/public-api process this memory is retained for the process lifetime.

Add a size cap with oldest-entry eviction, and delete expired failure entries when you read them.

♻️ Sketch of a bounded cache
+const MAX_CACHED_SLOTS = 2_000
+
+const setBoundedCacheEntry = <TValue>(cache: Map<string, TValue>, key: string, value: TValue): void => {
+  if (cache.size >= MAX_CACHED_SLOTS) {
+    const oldestKey = cache.keys().next().value
+    if (oldestKey !== undefined) cache.delete(oldestKey)
+  }
+  cache.set(key, value)
+}

Then use setBoundedCacheEntry for the writes on lines 267, 272, and 276, and call failedSlotDiscoveries.delete(cacheKey) when the TTL has elapsed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swapper/src/utils/evm/stateOverride.ts` around lines 40 - 45, Bound
all three discovery maps with oldest-entry eviction: add or reuse a
setBoundedCacheEntry helper and use it for writes to discoveredSlots,
rawDiscoveredSlots, and failedSlotDiscoveries in the relevant discovery flow.
Preserve the existing cache keys and capacity policy, and in the
failedSlotDiscoveries TTL-read path delete the cacheKey when its entry has
expired before continuing discovery.

224-264: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Deduplicate concurrent discovery for the same key.

The caches are written only after discovery completes. Concurrent requests for the same chain, token, and kind therefore run the full sequence in parallel: the known-slot guess, the access list, up to 8 raw probes, and the batched sweep. Rate refreshes and the following quote overlap in practice, so the probe cost multiplies by the number of in-flight callers.

Store the in-flight promise in a map keyed by cacheKey and return it to later callers, then delete the entry when it settles.

Also add an explicit return type to probeLocator on Line 224.

♻️ Proposed changes
-  const probeLocator = (locator: SlotLocator) =>
+  const probeLocator = (locator: SlotLocator): Promise<boolean> =>
     probeStorageSlot({ ...args, storageSlot: getStorageSlotFromLocator({ ...args, ...locator }) })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swapper/src/utils/evm/stateOverride.ts` around lines 224 - 264,
Deduplicate concurrent slot discovery by adding an in-flight promise map keyed
by cacheKey around the discovery flow that defines found, returning the existing
promise for callers with the same key and deleting the map entry when it
settles. Preserve the existing cache behavior and probe sequence. Add an
explicit promise/boolean return type to probeLocator.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/swapper/src/swappers/NearIntentsSwapper/utils/getNearIntentsStepData.ts`:
- Around line 79-92: Update the rate-path call to getEvmNetworkFeeCryptoBaseUnit
so walletless estimation uses a placeholder sender distinct from depositAddress
instead of falling back to depositAddress. Preserve the existing from value when
a wallet is connected, and ensure the placeholder is used only for the
walletless case while retaining the balance override flow.

---

Nitpick comments:
In `@packages/swapper/src/utils/evm/stateOverride.ts`:
- Around line 40-45: Bound all three discovery maps with oldest-entry eviction:
add or reuse a setBoundedCacheEntry helper and use it for writes to
discoveredSlots, rawDiscoveredSlots, and failedSlotDiscoveries in the relevant
discovery flow. Preserve the existing cache keys and capacity policy, and in the
failedSlotDiscoveries TTL-read path delete the cacheKey when its entry has
expired before continuing discovery.
- Around line 224-264: Deduplicate concurrent slot discovery by adding an
in-flight promise map keyed by cacheKey around the discovery flow that defines
found, returning the existing promise for callers with the same key and deleting
the map entry when it settles. Preserve the existing cache behavior and probe
sequence. Add an explicit promise/boolean return type to probeLocator.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7134d6fa-9a60-4e72-959d-9f9257a6edc4

📥 Commits

Reviewing files that changed from the base of the PR and between 231aac1 and 80ab42a.

📒 Files selected for processing (9)
  • packages/swapper/src/swappers/BobGatewaySwapper/utils/getBobGatewayStepData.ts
  • packages/swapper/src/swappers/ButterSwap/utils/getButterSwapStepData.ts
  • packages/swapper/src/swappers/DebridgeSwapper/utils/getDebridgeStepData.ts
  • packages/swapper/src/swappers/NearIntentsSwapper/utils/getNearIntentsStepData.ts
  • packages/swapper/src/swappers/PortalsSwapper/utils/getPortalsStepData.ts
  • packages/swapper/src/swappers/ThorchainSwapper/getTradeQuote/getTradeQuote.test.ts
  • packages/swapper/src/utils/evm/getEvmNetworkFeeCryptoBaseUnit.ts
  • packages/swapper/src/utils/evm/stateOverride.ts
  • packages/swapper/src/utils/evm/storageSlots.ts
💤 Files with no reviewable changes (3)
  • packages/swapper/src/swappers/PortalsSwapper/utils/getPortalsStepData.ts
  • packages/swapper/src/swappers/BobGatewaySwapper/utils/getBobGatewayStepData.ts
  • packages/swapper/src/swappers/DebridgeSwapper/utils/getDebridgeStepData.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/swapper/src/swappers/ButterSwap/utils/getButterSwapStepData.ts
  • packages/swapper/src/swappers/ThorchainSwapper/getTradeQuote/getTradeQuote.test.ts
  • packages/swapper/src/utils/evm/getEvmNetworkFeeCryptoBaseUnit.ts

kaladinlight and others added 3 commits August 3, 2026 15:19
…er sender

A depositAddress fallback priced a self-transfer, which touches one
balance slot instead of two and estimates low

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kaladinlight
kaladinlight enabled auto-merge (squash) August 3, 2026 21:29
@kaladinlight
kaladinlight merged commit ed5a6bf into develop Aug 3, 2026
4 checks passed
@kaladinlight
kaladinlight deleted the feat/evm-state-override-gas-estimation branch August 3, 2026 21:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant