fix(exit-certificate): implement ERC-20 balance storage patching for SC-locked exits (F-01)#1622
Conversation
…for SC-locked ERC-20 exits (F-01)
`ensureERC20Balance` was a stub that always returned an error without
patching Anvil storage. This implements two-layout detection via
`hardhat_setStorageAt`:
1. OZ v4 non-upgradeable: `_balances` at mapping slot 0
2. OZ v5 upgradeable: `_balances` inside the namespaced `ERC20Storage`
struct at `ERC20StorageLocation` (0x52c63...ce00)
After each attempt the function reads `balanceOf` to verify the patch
succeeded before trying the next layout. Wrapped tokens deployed by
AgglayerBridge (wTTK) use OZ v5 upgradeable storage, so the slot-0
attempt is a no-op and the second candidate fixes the balance.
Also adds two helper scripts:
- `scripts/bridge_l1_to_l2.sh`: bridges ETH/ERC-20 from L1→L2 in a
running Kurtosis `aggkit` enclave
- `scripts/reproduce_sc_locked.sh`: end-to-end reproducer for F-01
(deploys a TokenHolder SC on L2, transfers wTTK to it, then drives
the exit-certificate tool through steps 0→G to confirm the fix)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Claude finished @joanestebanr's task in 2m 6s —— View job Code Review Complete ✓🚀 What's NewThis PR implements proper ERC-20 balance storage patching for smart-contract-locked exits in Step G. The 🐛 Bug Fixes
📋 Config UpdatesNone
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e71c290e54
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return "0x" + hex.EncodeToString(crypto.Keccak256(preimage)) | ||
| } | ||
|
|
||
| // Try OZ v4 (slot 0) first, then OZ v5 upgradeable (namespaced storage). | ||
| candidates := []string{ | ||
| erc20BalanceSlot(common.Hash{}), // OZ v4: _balances at slot 0 | ||
| erc20BalanceSlot(erc20NamespacedStorageLocation), // OZ v5 upgradeable | ||
| } | ||
|
|
||
| for _, slotHex := range candidates { | ||
| if _, err := singleRPC(ctx, rpcURL, "hardhat_setStorageAt", | ||
| []any{tokenAddr.Hex(), slotHex, valueHex}, defaultRetries); err != nil { | ||
| return fmt.Errorf("set ERC-20 balance storage slot: %w", err) |
There was a problem hiding this comment.
Normalize slot index before calling hardhat_setStorageAt
hardhat_setStorageAt expects the storage position as an RPC quantity, but erc20BalanceSlot always returns a zero-padded 32-byte hex string. When the computed hash starts with 0, some nodes reject it as an invalid quantity, and this function returns immediately from the first candidate instead of reaching the fallback layout, so ERC-20 balance patching fails intermittently for affected accounts/tokens.
Useful? React with 👍 / 👎.
|



🔄 Changes Summary
Fix F-01:
ensureERC20Balancestorage patching for SC-locked ERC-20 exitsensureERC20Balancein Step G, which was a stub that always returned an error without actually patching Anvil storage._balances[account]viahardhat_setStorageAtusing a two-layout detection strategy, verifyingbalanceOfafter each attempt:_balancesmapping at storage slot 0_balancesinside the namespacedERC20Storagestruct atERC20StorageLocation = 0x52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00erc20NamespacedStorageLocationconstant documenting the OZ v5 storage namespace derivation.Refactor: remove
lbtFileconfig optionlbtFilewas an escape hatch to skip Step 0 by providing a pre-generated LBT. Step 0 now always runs, so the field is removed.RunStepCWithEntriesback intoRunStepC(simpler API, no config dependency).resolveOrGenerateLBT,loadWrappedTokensFromLBT,runSingleC,runSingleB,runSingleF,runSingleGaccordingly.Kurtosis script improvements
0xe25f5B65E4976025f670e52b790a9746F27A3DB6) inconfiguration_based_on_kurtosis.shso the exit address is stable across runs without requiring Foundry at script runtime. The private key and an encrypted keystore (password:test) are written totmp/on first execution.lbtFileconfig field removed. Any existing config files using it will have the field silently ignored (unknown JSON fields are not errors, but Step 0 will now always run).📋 Config Updates
lbtFileremoved fromConfigandrawConfig. Step 0 is no longer skippable via config.✅ Testing
ok github.com/agglayer/aggkit/tools/exit_certificate)tools/exit_certificate/scripts/reproduce_sc_locked.shagainst a live KurtosisaggkitenclaveTokenHoldersmart contract on L2, transfers wrapped ERC-20 tokens to it, then drives the exit-certificate tool from steps 0→GERC20InsufficientBalance(0xe450d38c) when replaying the SC-lockedBridgeExitNewLocalExitRoot🐞 Issues
ensureERC20Balancealways errors for SC-locked ERC-20 exits🔗 Related PRs
📝 Notes
TokenWrapped(wTTK) deployed byAgglayerBridgeuses OZ v5 upgradeable storage, so the slot-0 attempt is a no-op. The second candidate (namespaced storage) is the one that matches and patches the balance correctly.