fix(api): repair persisted ERC-20 transfer recipients from before #2252 - #2259
Open
tony8713 wants to merge 1 commit into
Open
fix(api): repair persisted ERC-20 transfer recipients from before #2252#2259tony8713 wants to merge 1 commit into
tony8713 wants to merge 1 commit into
Conversation
#2252 fixed the decoder, but the decoded execution is persisted into proposalmetadataitems.execution at index time, so it only corrected proposals indexed after the deploy. The 140 ERC-20 transfers already written across 96 proposals in the 6 governor spaces still name the token contract as the payee, which is what ENS is still seeing on their proposal. Add a script that re-derives each recipient from the calldata already stored on the row and emits guarded UPDATE statements on stdout. Only _form.recipient is rewritten; to, data, value and the token metadata are untouched. Each statement is scoped to the live version of the row (upper_inf(block_range)) and guarded on the recipient it expects to find, so re-running is a no-op and a row that has since been re-indexed correctly is skipped. A transfer whose calldata is not exactly a selector plus two words is left alone rather than given an invented recipient. Uniswap Governor Bravo proposal 81 is the one such row: it supplies both a transfer(address,uint256) signature and a calldata that already carries the selector, so what the Governor executes is double-prefixed and is not a well-formed transfer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #2252, which fixed the decoder but deliberately left the already-written rows alone. ENS (@gregskril) is still seeing the wrong recipient on their proposal, because the wrong value is persisted.
Why the symptom outlived the fix
The indexer decodes the calldata once, at index time, and writes the result into
proposalmetadataitems.execution:apps/api/src/evm/protocols/openzeppelin/writers.ts:431-442apps/api/src/evm/protocols/governor-bravo/writers.ts:372-393So #2252 corrects proposals indexed after it deployed (api deployed from
cb368c0eon 2026-08-04 15:01 UTC), and nothing else. No governor proposal has been created since, so the fix has had no occasion to show itself, and every historical row still carries the pre-fix value.api.snapshot.boxtoday, for ENS proposal[Executable] Next Era of ENS DAO: Empowering the ENS Foundation:0xC18360...is the ENS token itself. The calldata says0x9C7dB6B1085ec4D07f75c0BD91AD3FcD368fA19E, the ENS DAO Safe (v1.4.1, 3-of-5).Scope
Measured against the live API: 623 proposals across the 6 governor spaces, 184
sendTokentransactions, of which 140 decode from an ERC-20transferand all 140 are wrong, spread over 96 proposals.0x408ED635…0xc0Da0293…0x323A7639…0x789fC990…The 43 native-ETH
sendTokenentries (data === '0x') were always correct, because there the call target genuinely is the recipient.What this does
repairErc20TransferRecipients.tsreads the affected rows from the public API (no credentials) and emits guarded SQL on stdout:Each statement rewrites exactly one JSON path:
to,data,value,saltand the token metadata are untouched. Only the displayed payee moves.upper_inf(block_range)leaves checkpoint's closed history alone; we are correcting a decode, not recording a new observation, so no new version is inserted.transferpayload has to be exactly a selector plus two 32-byte words.Currently 140 statements, 53 KB.
Verified against real data
Not reviewed by reading: the 96 real production rows were loaded into a local Postgres 16 and the generated SQL applied.
The one row deliberately left alone
Uniswap Governor Bravo proposal 81 stores a doubled selector:
That is not our bug.
getActions(81)on chain returns signaturetransfer(address,uint256)and a calldata that already carries the selector, and Governor Bravo'sexecuteprepends the sighash from the signature, so the payload the Governor itself executes is double-prefixed.governor-bravo/writers.tsfaithfully reproduces it. The proposal is malformed on chain; the storeddatais right. Its recipient and amount are both meaningless, and the strict length check leaves them as they are rather than inventing an address.Worth a separate change:
decodeExecutionshould probably reject atransferwhose calldata is not 68 bytes and fall back toraw, instead of rendering a fabricated recipient and a nonsense amount. Not in this PR.Execution was never at risk
Worth stating explicitly, since a wrong payee is alarming:
_formis display data.queue/executeread only top-levelto/value/data, all carried through verbatim from the on-chainProposalCreatedevent (packages/sx.js/src/clients/openzeppelin/ethereum-tx/index.ts:96-98, 122-124). The ENS proposal's storedto/data/valueare byte-identical to the on-chainpropose()args.The one place
_form.recipientbecomes a real transaction is the Safe batch export,apps/ui/src/helpers/safe/ build.ts:38-42, which re-encodestransfer(recipient, amount)and dropsdata. It is unreachable here:ProposalExecutionsList.vue:83only renders that button forstrategyType === 'ReadOnlyExecution', which is produced solely by the offchain plugin path, whose transactions come from the proposal author verbatim and never through this decoder. Governor proposals areOpenZeppelinTimelockController/GovernorBravoTimelock.Tests
Fixtures are the verbatim production rows: the ENS one, the malformed Uniswap 81 one, a native-ETH send, and an already-correct row. Repo-wide
bun run build,typecheck,lint,lint:unusedandtestare green.🤖 Generated with Claude Code