Skip to content

test(anvil): add tempo mainnet canary replays - #16793

Open
mattsse wants to merge 6 commits into
masterfrom
mattsse/anvil-tempo-canary-tests
Open

test(anvil): add tempo mainnet canary replays#16793
mattsse wants to merge 6 commits into
masterfrom
mattsse/anvil-tempo-canary-tests

Conversation

@mattsse

@mattsse mattsse commented Sep 10, 2026

Copy link
Copy Markdown
Member

T11 activated on Tempo mainnet at block 38891821 and, through the strict ABI decoding it introduced for precompile calls, started rejecting calldata with trailing bytes. Relay's solver appends the 32-byte request id to its USDC.e transfer calls and every fill failed until Relay dropped the suffix, and end users of a frontend that appends an ERC-8021 attribution suffix lost every USDC.e approve in the same way. Scanning the blocks between activation and 20:50 UTC finds 64 failed transactions, 44 of them these two families, against 13 failures in the ~1.8 hours before activation. Replaying those transactions on a Tempo fork with T11 forced would have shown both regressions before the fork went live, so this adds canary tests that do exactly that for whatever hardfork the pinned tempo revision considers newest.

Each test forks mainnet at the parent of a pinned block, forces TempoHardfork::latest(), re-submits the block's raw transactions at the block's timestamp and asserts the local receipts reproduce mainnet status and gas. Each canary pins one transaction, linked to the explorer next to its block, and asserts that the replayed block holds it. The pinned transactions come from the services that dominate mainnet traffic: Relay's solver (USDC.e and pathUSD transfers) and its ERC20 router (permit2TransferAndMulticall), the Tempo AA payout senders (transfer, sponsored transferWithMemo, and allowance pulls through transferFrom), an ERC-4337 v0.7 bundler and an ERC-7821 relayer for EIP-7702 accounts. The two trailing-bytes incidents are pinned as regression tests that replay the failed mainnet transaction under T10, T11 and the latest hardfork: T10 accepts it, T11 reproduces the mainnet failure with identical gas, and T12 accepts it again. A Relay fill that mainnet executed under T10 with the request id still attached is pinned as well, matching mainnet exactly under T10 and succeeding under the latest hardfork with the 96 gas T11 added to precompile calldata. Running the two regression tests against master's tempo revision, which predates tempoxyz/tempo#7598, fails both at the latest-hardfork step with the mainnet failure gas, which is the signal this PR wants before the next activation. Gas is compared exactly, so the pinned blocks are post-T11 ones; T11 raised the per-word calldata cost of precompile calls, which is precisely the kind of drift the module docs describe how to relax and re-pin when a future hardfork changes gas again.

Two things had to change for the replays to work. The Tempo dependencies move to tempoxyz/tempo@731535b, the merge of tempoxyz/tempo#7598, so that T12 accepts trailing bytes; this pulls alloy-core 1.7.3 and a small fix for ISSUER_ROLE becoming a const. And Anvil's pool rejected every non-AA mainnet sender with Insufficient funds for gas * price + value: on Tempo gas is paid in a fee token for every transaction type, forked mainnet accounts hold no native balance, and whether the pool saw that depended on whether the first eth_getAccountInfo probe succeeded or fork-db fell back to eth_getBalance, which Tempo answers with a placeholder value. The pool now validates the fee token balance for all transactions on Tempo, mirroring the node, with a deterministic test for a sender that holds fee tokens but no native balance.

The tests default to the public https://rpc.tempo.xyz endpoint and honour TEMPO_MAINNET_RPC_URL, which the test workflow now receives from the existing repository secret.

🤖 Generated with Claude Code

Picks up tempoxyz/tempo#7598, which makes T12 accept trailing ABI bytes in
precompile calls again, and adapts to the TIP20 role hashes becoming consts.
Tempo pays gas in a fee token for every transaction type, but the pool still
required non-AA senders to hold the native gas cost. Forked mainnet senders
hold no native balance, so replaying their transactions failed with
insufficient funds whenever fork-db fetched the account through
eth_getAccountInfo instead of the placeholder eth_getBalance answer.

The fee token balance check now covers all transactions on Tempo, resolving
the fee token of non-AA senders from the fee manager like the node does.
Each canary forks Tempo mainnet at the parent of a pinned block, forces the
newest hardfork the pinned tempo revision knows, replays the block's raw
transactions at the original timestamp and requires the local receipts to
reproduce mainnet status and gas. The pinned blocks cover Relay's solver and
router, Tempo AA payout senders, an ERC-4337 bundler and an ERC-7821 relayer.

The two T11 regressions, Relay's request id suffix and ERC-8021 attribution
suffixes on TIP20 calls, are pinned as replays of the failed mainnet
transactions under T10, T11 and the latest hardfork.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Changelog found

The deterministic check will validate the changed entry.

@socket-security

socket-security Bot commented Sep 10, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatedalloy-json-abi@​1.7.2 ⏵ 1.7.310010093100100
Updatedalloy-primitives@​1.7.2 ⏵ 1.7.310010093100100
Updatedalloy-sol-macro-expander@​1.7.2 ⏵ 1.7.310010093100100
Updatedalloy-sol-macro-input@​1.7.2 ⏵ 1.7.39910093100100
Updatedalloy-sol-types@​1.7.2 ⏵ 1.7.310010093100100

View full report

@mattsse mattsse left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Besides the canaries and the dependency bump, this PR fixes an existing Anvil bug in Tempo mode that the replays exposed; the inline comments explain it.

let fee_token =
tempo_tx.fee_token.unwrap_or(foundry_evm::core::tempo::PATH_USD_ADDRESS);
// Tempo charges gas in a fee token for every transaction type, never in the native token,
// so the fee token balance replaces the native balance check.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Existing bug, surfaced by the replays: on Tempo every transaction type pays gas in a fee token, but the pool only ran this fee token check for AA transactions and required legacy/EIP-1559 senders to hold gas * price + value in the native token further down. Forked mainnet senders hold no native balance (the node answers eth_getBalance with a placeholder value but eth_getAccountInfo with the real zero), so replaying their transactions failed with Insufficient funds for gas * price + value, and whether it failed depended on which of the two methods fork-db ended up using. The check now runs for all transactions and resolves a non-AA sender's fee token from the fee manager, falling back to pathUSD, the same way the node's validator does.

// Tempo AA transactions pay gas with fee tokens, not ETH.
// Fee token balance is validated in validate_pool_transaction (async).
}
_ if self.is_tempo() => {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Second half of the same fix: this arm keeps the native gas * price + value requirement from applying to non-AA transactions in Tempo mode, since the fee token balance was already validated in validate_pool_transaction. The AA arm above is unchanged.

/// native balance, which is what every forked mainnet sender looks like, must get its EIP-1559
/// transaction pooled and mined.
#[tokio::test(flavor = "multi_thread")]
async fn test_tempo_eip1559_sender_without_native_balance_pays_gas_in_fee_token() {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Deterministic regression test for the pool fix, independent of the mainnet forks: before this change a sender holding pathUSD but no native balance was rejected with Insufficient funds for gas * price + value. It now gets the fee token error while it has no tokens and mines once dealt some.

Each canary now names the mainnet transaction it is about, links the block
and the receipt on the Tempo explorer, and asserts the replayed block holds
it. Adds an allowance pull through a TIP20 transferFrom as a case.
Replays a fill mainnet executed under T10 with exact parity under T10 and
under the latest hardfork with the 96 gas T11 added to precompile calldata.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant