feat: ERC20 allowance overrides + user-facing erc20_overrides API for RunNodeImmediately#620
Conversation
…#619) Co-authored-by: Will Zimmerman <will@avaprotocol.org>
…ing (AvaProtocol#618) Co-authored-by: Will Zimmerman <will@avaprotocol.org>
|
@Antrikshgwal, thanks for the PR, let us review this. |
There was a problem hiding this comment.
Pull request overview
Adds a user-facing ERC20 state override mechanism to RunNodeImmediately simulations so callers can pre-seed ERC20 balanceOf and allowance storage values (via proto + REST), enabling isolated contract-write simulations (e.g., Uniswap) to bypass funding/approval preconditions without affecting real execution paths.
Changes:
- Extend gRPC + REST/OpenAPI request models with
erc20_overrides/erc20Overridesand wire them intoRunNodeImmediately. - Implement
SimulationStateMap.ApplyUserERC20Overrideplus parsing/slot math to seed ERC20 storage overrides in simulation state. - Update/introduce unit + integration-style tests and refresh Tenderly override documentation.
Reviewed changes
Copilot reviewed 8 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| protobuf/avs.proto | Adds ERC20StateOverride message + erc20_overrides field on RunNodeWithInputsReq. |
| protobuf/avs.pb.go | Regenerated Go bindings to include the new message/field. |
| api/openapi.yaml | Adds ERC20StateOverride schema and erc20Overrides to RunNodeRequest. |
| aggregator/rest/handlers_nodes.go | Maps REST erc20Overrides into proto request for RunNodeImmediately. |
| aggregator/rest/generated/types.gen.go | Regenerated REST types for the new schema/field. |
| core/taskengine/run_node_immediately.go | Threads overrides into VM simulation state and rejects them in non-simulated execution. |
| core/taskengine/simulation_state.go | Adds parsing + state seeding for ERC20 balance/allowance slot overrides. |
| core/taskengine/simulation_state_override_test.go | Adds unit tests for parsing, slot math, and validation. |
| core/taskengine/vm_runner_contract_write_uniswap_test.go | Updates Uniswap simulation test to seed overrides and assert allowance/balance preconditions are removed. |
| core/taskengine/TENDERLY_STATE_OVERRIDES.md | Updates documentation to reflect shipped SimulationStateMap + user-facing overrides API. |
Files not reviewed (1)
- protobuf/avs.pb.go: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Seed at every common balance slot so the test USDC's actual layout is covered. | ||
| for _, slot := range commonBalanceSlots { | ||
| s := uint64(slot) | ||
| err := vm.simulationState.ApplyUserERC20Override( | ||
| usdc, owner, swapRouter02, | ||
| "0x38d7ea4c68000", // 1,000,000 USDC (6 decimals) | ||
| "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", // max allowance | ||
| &s, &s, | ||
| ) | ||
| require.NoError(t, err) | ||
| } |
| func parseUint256(value string) (*big.Int, error) { | ||
| v := strings.TrimSpace(value) | ||
| if v == "" { | ||
| return nil, fmt.Errorf("empty value") | ||
| } | ||
| if strings.HasPrefix(v, "0x") || strings.HasPrefix(v, "0X") { | ||
| n, ok := new(big.Int).SetString(v[2:], 16) | ||
| if !ok { | ||
| return nil, fmt.Errorf("invalid hex value %q", value) | ||
| } | ||
| return n, nil | ||
| } | ||
| n, ok := new(big.Int).SetString(v, 10) | ||
| if !ok { | ||
| return nil, fmt.Errorf("invalid decimal value %q", value) | ||
| } | ||
| return n, nil | ||
| } |
| slot := defaultERC20BalanceSlot | ||
| if balanceSlot != nil { | ||
| slot = int64(*balanceSlot) | ||
| } |
| slot := defaultERC20AllowanceSlot | ||
| if allowanceSlot != nil { | ||
| slot = int64(*allowanceSlot) | ||
| } |
| // Default storage-slot indices used when a user-supplied ERC20 override does | ||
| // not specify them. These match the most common standard-ERC20 layout; tokens | ||
| // with non-standard layouts (e.g. USDC FiatToken at 9/10) must set the slots | ||
| // explicitly. See TENDERLY_STATE_OVERRIDES.md for a reference table. | ||
| const ( |
|
Hi @Antrikshgwal, Because this PR touches the protobuf files — it adds a new ERC20StateOverride message and the erc20_overrides field to RunNodeWithInputsReq in
That's the test that actually proves the feature works. The pure-helper tests you already have are good, but they don't exercise the protocol surface this PR adds. |
…-index backfill aa-wallet-toolkit.sh exec'd scripts/aa-wallet-toolkit.go, which was deleted in AvaProtocol#538 — the wrapper has been broken (and unused) ever since. Remove it and its config/README mention. The (owner, factory, salt) → address backfill migration has run on all chains, so remove the standalone script, the /ava backfill-wallet-salt-index subcommand, and the BackfillWalletSaltIndex/RedactRPCURL core. Live StoreWallet/MarkWalletStale/ListWallets behavior and its tests are kept; only the migration-only tests are dropped. yaml.v3 drops to indirect.
The Option C Phase 1-4 narrative described a finished one-time cleanup and no longer reflects current state. Keep the still-accurate per-chain config exception (userops_withdraw_test.go), promoted to its own heading.
…way/worker, add operator template (AvaProtocol#621) Co-authored-by: Claude <noreply@anthropic.com>
|
Hi @Antrikshgwal — one heads-up before you tackle Will's checklist: the Where the test config lives now Those simulation E2E tests load their config via # 1. Create the fixture from the template
cp config/test.example.yaml config/test.yaml
# 2. Fill in (in config/test.yaml):
# eth_rpc_url — a Sepolia RPC endpoint (your own Infura/Alchemy/etc.)
# tenderly_account / tenderly_project / tenderly_access_key — your Tenderly creds
# 3. Set the funded owner EOA (env, or a .env file in the repo root)
export OWNER_EOA=0x... # a Sepolia EOA with testnet funds
1. Confirm the existing simulation tests run green (not skipped) go test -v ./core/taskengine/ \
-run 'TestContractWriteNode_UniswapV3Quote|TestExecuteTask_UniswapApprovalAndSwap_DifferentApprovalAmounts_Sepolia|TestContractWriteTenderlySimulation'Look for 2. The new E2E test that drives Use
proving the overrides flow from the request → proto field → REST/handler mapping → Tenderly simulation, rather than calling Generated by Claude Code |
Hi @chrisli30, Im done with the changes, but can't find the Tenderly access token, on their website, Could you please tell me where can i find it ? |
Addresses review feedback on AvaProtocol#620. - parseUint256: reject negative values and anything exceeding uint256 max, so an override can never format into an invalid EVM storage word. - erc20SlotOrDefault: bounds-check the caller-supplied *uint64 slot before the int64 conversion, preventing a >MaxInt64 index from overflowing into a bogus storage slot. - Add a real end-to-end test as a subtest of TestRunNodeImmediatelyRPC (ERC20Overrides_UniswapSwap): builds a RunNodeWithInputsReq with populated erc20_overrides and calls the real RunNodeImmediatelyRPC on a Uniswap exactInputSingle node, asserting the sim no longer reverts on the allowance/balance precondition — exercising the full request → proto → RunNodeImmediately → SimulationStateMap → Tenderly path (not the helper directly). Skips cleanly without OWNER_EOA. - Seed balance and allowance at their OWN slots (balance 0/9, allowance 3/10) instead of reusing balance slots for allowance; keep the documented 0/3 defaults and add unit cases for the new validation. - Docs: note the value/slot bounds and the explicit-slot escape hatch for OpenZeppelin (allowance slot 1) and USDC (9/10) layouts.
1abe216 to
5e0c377
Compare
Implementation + the new RPC-level E2E test (TestRunNodeImmediatelyRPC/ERC20Overrides_UniswapSwap) are in. I set up config/test.yaml locally, but the config validator requires controller_private_key to match the testnet paymaster's verifyingSigner (0x82F2Dd9a…), which is an Ava Protocol secret I don't have — so I can't run the Tenderly E2E suite green locally. Could you run TestContractWriteNode_UniswapV3Quote, TestExecuteTask_UniswapApprovalAndSwap_DifferentApprovalAmounts_Sepolia, TestContractWriteTenderlySimulation, and the new override test on your configured environment? Happy to iterate on anything they surface. |
Addresses review feedback on #620. - parseUint256: reject negative values and anything exceeding uint256 max, so an override can never format into an invalid EVM storage word. - erc20SlotOrDefault: bounds-check the caller-supplied *uint64 slot before the int64 conversion, preventing a >MaxInt64 index from overflowing into a bogus storage slot. - Add a real end-to-end test as a subtest of TestRunNodeImmediatelyRPC (ERC20Overrides_UniswapSwap): builds a RunNodeWithInputsReq with populated erc20_overrides and calls the real RunNodeImmediatelyRPC on a Uniswap exactInputSingle node, asserting the sim no longer reverts on the allowance/balance precondition — exercising the full request → proto → RunNodeImmediately → SimulationStateMap → Tenderly path (not the helper directly). Skips cleanly without OWNER_EOA. - Seed balance and allowance at their OWN slots (balance 0/9, allowance 3/10) instead of reusing balance slots for allowance; keep the documented 0/3 defaults and add unit cases for the new validation. - Docs: note the value/slot bounds and the explicit-slot escape hatch for OpenZeppelin (allowance slot 1) and USDC (9/10) layouts.
|
Hi @Antrikshgwal — thanks again for this work. We picked it up and got it over the line in #624, which folds in your two commits (authorship preserved) on top of the latest staging. The gap that was blocking green CI: your wiring was correct all the way from the request → proto → What we added on top of your commits:
CI is green — the full core/taskengine suite passes in the unit-test workflow. We'll land it via #624 → staging. Appreciate the contribution! 🙏 |
|
Closing this PR since @Antrikshgwal's commits are included in the #624 now. |
Thanks for picking this up and the clear writeup, that tempVM boundary in RunNodeWithInputs is a sneaky one. Seeding simulationState on the parent VM is moot if the per-node tempVM starts empty, and my helper-only tests sailed right past it. Exactly the blind spot that you flagged, lesson taken on testing at the real protocol surface. One small clarification on the slots: I wasn't defaulting/guessing — the overrides seeded across the common ERC20 layouts (balance 0/9, allowance 3/10) to cover both standard and USDC FiatToken. But I agree requiring an explicit slot is the cleaner contract — fail loudly beats silently seeding the wrong slot for an unknown token. Appreciate you preserving authorship and landing it in #624 🙏 Happy to take on more — point me at it. |
Fixes #413
Summary
Completes the remaining work on #413. The
SimulationStateMapinfra and thebalance-override path landed in #517; this PR adds the allowance path and the
user-facing
erc20_overridesAPI, so a caller testing an isolatedRunNodeImmediatelycontract write (e.g. a Uniswap swap) can seed token balanceand approval up front instead of hitting
ERC20: transfer amount exceeds allowance/balance.Changes
protobuf/avs.proto): newERC20StateOverridemessage andrepeated ERC20StateOverride erc20_overrides = 5onRunNodeWithInputsReq.Regenerated
avs.pb.gowith the pinned toolchain (protoc v29.2 /protoc-gen-go v1.36.6) so the diff is limited to the new message + field.
api/openapi.yaml,aggregator/rest/...):ERC20StateOverrideschema +
erc20OverridesonRunNodeRequest; regenerated types; mapping fromthe generated type to proto in the
RunNodehandler.core/taskengine/simulation_state.go,run_node_immediately.go):ApplyUserERC20Override— pure, absolute-value seeding ofbalanceOf/allowancestorage slots (defaults: balance slot 0, allowance slot 3),plus a
parseUint256hex/decimal helper.SimulationStateMapand are rejected in real-execution mode — they only ever affect
simulations, never deployed workflows or live wallets.
and validation (
simulation_state_override_test.go); the Uniswap test nowseeds overrides through the new path and skips cleanly without Tenderly config.
TENDERLY_STATE_OVERRIDES.mdrefreshed to document the shippedSimulationStateMapdesign and the newerc20_overridesAPI.Notes for reviewers
= 5rather than the= 10in theoriginal issue sketch (1/3/4 are used, 2 is reserved).
method (
QuoterV2.quoteExactInputSingle) reverts by design to return thequote. The test now asserts the allowance/balance revert is gone (the actual
fix) and tolerates QuoterV2's intentional revert, rather than asserting a
misleading success.
erc20_overridesare simulation-only and explicit opt-in;passing them with
isSimulated:falsereturns a clear error.