feat(ai-explainer): address labels + Maple proposal unwrap#231
Merged
Conversation
Before: the LLM saw decoded address arguments as raw hex (e.g. `address[]:
('0xac21...',)`), so a Maple addFarms alert could only say "registers farm
type 1 at 0xac21..." despite the farm contract being verified on Etherscan.
After: every address-typed argument (scalar `address` and `address[]`
elements) gets annotated with its on-chain ContractName, so the LLM can
write "registers MorphoFarm (type 1) to FarmRegistry." Same benefit for
Safe queued-tx alerts since both flows share the same explainer.
Implementation:
- New `get_contract_label(chain_id, address)` in `utils/source_context.py`
reuses the existing per-process Etherscan source cache; falls through to
EIP-1967 impl name when the proxy itself is named one of the generic
wrappers (TransparentUpgradeableProxy, ERC1967Proxy, etc.).
- `_collect_address_labels()` + `_annotate_address()` in `ai_explainer.py`
collect labels for every address arg (skipping each call's own target,
zero addresses, and malformed hex), then `_format_decoded_calls` renders
`address[]:` as a per-element bullet list with `(ContractName)` suffix.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Maple's GovernorTimelock `ProposalScheduled` event only carries a uint256 `proposalId` and an opaque hash — no targets, no data. As a result our timelock alerts for MAPLE had no AI summary section, leaving recipients with just "Proposal: 13" and no idea what the proposal actually does. The full payload is recoverable from the originating transaction: the DAO multisig calls Safe `execTransaction(...)` wrapping `scheduleProposals(address[] targets, bytes[] data)` into the GovernorTimelock. We can decode that one layer down and feed the targets+data directly into `explain_batch_transaction`. Implementation: - New `utils/safe_tx.py::unwrap_safe_exec_transaction(input_hex)` returning the inner `(target, value, data, operation)` for any Safe-wrapped tx. - `_maple_proposal_calls(event, chain_id)` in `timelock_alerts.py` fetches the source tx, unwraps the Safe layer if present, decodes `scheduleProposals`, and returns a list shaped for the batch explainer. - Maple branch in `_get_ai_explanation` routes through it before the generic path. Bails cleanly to `None` on tx fetch errors, unknown signatures (e.g. proposeRoleUpdates), or shape mismatches. - Added the three relevant selectors (Maple `scheduleProposals` + `proposeRoleUpdates`, Safe `execTransaction`) to `KNOWN_SELECTORS` so decoding is offline and doesn't depend on Sourcify. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Recent alerts on simpler calls were collapsing to single-sentence TLDRs
("Adds farm. LOW.") because the prompt only enforced a ≤25-word cap and a
trailing risk tag — nothing required the middle impact/magnitude beat that
the original "good example" demonstrated.
Make the structure explicit: 2-4 short sentences, ≤40 words total, with a
[what changed] · [magnitude or impact] · [risk tag] formula. Add a second
bad example showing the too-terse failure mode alongside the existing
preamble+run-on bad example. Update REFINE_TASK item 2 to flag
single-sentence TLDRs for revision.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tenderly frequently misreports legitimate governance calls as reverting — wrong msg.sender, missing storage overrides, edge cases in their state diff engine. When that "FAILED" verdict reached the LLM via the Simulation Results section, the model would write "this tx will revert on execution" or escalate risk on the strength of a false negative. Treat sim.success == False the same as no sim available: log a warning locally so the failure is still visible in logs, but omit the section from the prompt. The LLM then works from calldata + source context + on-chain state — same path as `skip_simulation=True`. Single-call path: short-circuit right after the existing log line. Batch path: prefer a successful sim from the list; if every inner call failed, drop the section entirely. Co-Authored-By: Claude Opus 4.7 <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.
Summary
Two small enrichments to the AI transaction explainer, both fixing real "the alert says less than it could" gaps from recent observations.
1. Address arguments → contract names
The LLM previously saw decoded address arguments as raw hex, so an InfiniFi
addFarms(uint256, address[])alert read:After: every
address/address[]arg is annotated with its on-chainContractName(reusing the existing Etherscan source cache and following EIP-1967 when the proxy itself is named generically), so it can read:Same benefit for Safe queued-tx alerts since both flows share the same explainer.
2. Maple
ProposalScheduled→ actual proposal callsMaple's GovernorTimelock only emits
proposalId+ opaque hash, so Maple alerts had no AI summary section at all — recipients sawProposal: 13with no description.The calldata is recoverable from the originating tx: the DAO multisig calls Safe
execTransaction(...)wrappingscheduleProposals(address[] targets, bytes[] data)into the GovernorTimelock. We unwrap one Safe layer, decodescheduleProposals, and feed the targets+data intoexplain_batch_transaction. Bails cleanly toNonefor tx-fetch errors, unknown signatures (e.g.proposeRoleUpdates), and shape mismatches.Snapshot was ruled out as an enrichment source —
mapledao.ethhas had no votes since MIP-020 (Jan 2026), and on-chain proposals continue to fire under prior MIPs, so it's not a reliable lookup.Test plan
uv run pytest tests/test_ai_explainer.py tests/test_source_context.py tests/test_timelock_alerts.py -quv run ruff format .uv run ruff check .ProposalScheduledevent — should produce an AI summary instead of bareProposal: <id>🤖 Generated with Claude Code