test(anvil): add tempo mainnet canary replays - #16793
Conversation
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.
✅ Changelog foundThe deterministic check will validate the changed entry. |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
mattsse
left a comment
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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() => { |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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.
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
transfercalls 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.eapprovein 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 pinnedtemporevision 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, sponsoredtransferWithMemo, and allowance pulls throughtransferFrom), 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_ROLEbecoming a const. And Anvil's pool rejected every non-AA mainnet sender withInsufficient 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 firsteth_getAccountInfoprobe succeeded or fork-db fell back toeth_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.xyzendpoint and honourTEMPO_MAINNET_RPC_URL, which the test workflow now receives from the existing repository secret.🤖 Generated with Claude Code