Skip to content

test: diagnose transient batch fund failure on Tempo#21

Open
g4titanx wants to merge 2 commits intomasterfrom
fix/batch-fund-wrong-address-diagnosis
Open

test: diagnose transient batch fund failure on Tempo#21
g4titanx wants to merge 2 commits intomasterfrom
fix/batch-fund-wrong-address-diagnosis

Conversation

@g4titanx
Copy link
Copy Markdown
Member

@g4titanx g4titanx commented Mar 31, 2026

On Tempo testnet, escrow funding intermittently fails with "Contract is not funded" despite the batch transaction succeeding. The node logs show:

token:  0x20C0000000000000000000000000000000000000
escrow: 0x7e9798a62b42d97fb05b9e092a9a2117fa3fb995
amount: 100000000, reward: 723471
→ "Contract is not funded"

Failing TX: 0xedf034...ac94ae

Root Cause

The batch transaction has 3 steps:

Step Operation Target Has Code?
1 CREATE escrow 0x7e97...b995 Yes (9177 bytes)
2 approve PathUSD 0xD69B...D2Aa No
3 fund (0x49364cd4) 0xD69B...D2Aa No

The escrow deploys correctly at 0x7e97..., but the approve and fund calls target 0xD69B... — an address with zero code. In the EVM, a CALL to a codeless address succeeds silently (returns empty data, near-zero gas). So the batch doesn't revert, but fund() never executes. The escrow remains unfunded.

The obfuscation is not the issue — the obfuscated fund selector 0x49364cd4 is present in the deployed dispatcher and routes correctly. The original selector 0xa65e2cfd is absent as expected post-obfuscation.

The bug is upstream in the batch construction code (nomad_enclave), which pre-computes the escrow address for post-deploy calls and gets it wrong. 0xD69B... does not match any CREATE nonce (0–200) from the sender 0xA790...BBaD, ruling out a simple off-by-one.

Cross-TX Verification

Comparing a successful batch against the failing one confirms the root cause:

TX CREATE target approve target fund target Result
0x8bff4e... (success) 0x7720...5c9e 0x7720...5c9e 0x7720...5c9e All same — works
0xedf034... (failure) 0x7e97...b995 0xD69B...D2Aa 0xD69B...D2Aa Mismatch — silent no-op

When the batch targets the correct address, it works. When wrong, silent no-op.

What We Tested (29 tests)

H1 — Bytecode & selector verification (3 tests)

  • Compiled creation code is non-trivial (>8KB)
  • fund(uint256,uint256) selector 0xa65e2cfd exists in deployed bytecode
  • Raw low-level call with correct selector works; wrong selector reverts
  • Result: RULED OUT — contract bytecode and dispatch are correct

H2 — msg.sender context in batch (3 tests)

  • Factory deploy + auto-fund works when factory is msg.sender
  • Factory deploy then separate fund() works
  • EOA cannot fund a factory-deployed escrow (OnlyDeployer)
  • Result: PLAUSIBLE general risk — but not the cause here since calls never reach the escrow

H3 — Lenient batcher swallows reverts (2 tests)

  • Fund revert (no allowance) inside lenient batcher is silently swallowed
  • OnlyDeployer revert inside lenient batcher is silently swallowed
  • Result: CONFIRMED MECHANISM — this is how the silent failure propagates

H4 — Batch reverts after fund (2 tests)

  • Strict batcher: later revert rolls back fund() state
  • Strict batcher: msg.sender = batcher, not deployer → OnlyDeployer revert
  • Result: PLAUSIBLE general risk

H5 — No-op token (1 test)

  • Token that returns true but never moves balances → funded flag set but zero balance
  • This would cause payout failure, NOT "not funded" → different error
  • Result: RULED OUT

H6 — Non-compliant token (2 tests)

  • Token returning no data → ABI decode revert at deploy time
  • Reverting token → deploy fails
  • Result: RULED OUT

H7 — staticcall (eth_call) (1 test)

  • staticcall to fund() reverts (SSTORE forbidden) — state never persists
  • Result: RULED OUT

H8 — Batch targets wrong address (ROOT CAUSE) (3 tests, using real on-chain values)

  • testCallToEmptyAddressSucceeds: Calling 0x49364cd4(723471, 100000000) on 0xD69B... returns success with empty data — the EVM behavior that enables silent failure
  • testExactTempoFailure_WrongAddressBatch: Full reproduction — deploy escrow, approve + fund to wrong address, escrow stays unfunded
  • testObfuscatedFundWorksAtCorrectAddress: Same amounts, same token — fund() works when called at the right address
  • Result: ROOT CAUSE CONFIRMED

Baseline & edge cases (9 tests)

  • Direct fund, constructor auto-fund, storage slot verification
  • Full flow (separate txs), full flow via strict/lenient batcher
  • Insufficient allowance, approval to wrong address

On-chain constants used

All H8 tests use verified values from the failing TX:

  • Token: 0x20C0000000000000000000000000000000000000 (PathUSD)
  • Escrow: 0x7e9798a62b42D97fb05b9e092a9A2117FA3fB995
  • Wrong target: 0xD69B8fC5D21819A713fDE3e051C97e1Cb09BD2Aa
  • Sender: 0xA79045285379f02ad505D7338523843D3A73BBaD
  • Fund params: reward=723471, amount=100000000
  • Obfuscated selector: 0x49364cd4

g4titanx and others added 2 commits March 31, 2026 11:45
The Tempo batch TX (0xedf034...ac94ae) deploys the escrow at
0x7e97...b995 but sends the subsequent approve + fund calls to
0xd69b...d2aa, which has no code. EVM CALL to a codeless address
succeeds silently, so the batch doesn't revert but the escrow
is never funded.

Root cause is upstream in the batch construction code which
pre-computes the wrong target address for post-deploy calls.
The obfuscated fund() selector (0x49364cd4) and dispatch logic
are correct — confirmed via eth_call on the live escrow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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