Skip to content

feat(ai-explainer): address labels + Maple proposal unwrap#231

Merged
spalen0 merged 5 commits into
mainfrom
feat/ai-explainer-address-labels-and-maple-unwrap
May 21, 2026
Merged

feat(ai-explainer): address labels + Maple proposal unwrap#231
spalen0 merged 5 commits into
mainfrom
feat/ai-explainer-address-labels-and-maple-unwrap

Conversation

@spalen0
Copy link
Copy Markdown
Collaborator

@spalen0 spalen0 commented May 20, 2026

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:

Registers farm type 1 at 0xac21... to FarmRegistry. Expands protocol surface. MEDIUM.

After: every address/address[] arg is annotated with its on-chain ContractName (reusing the existing Etherscan source cache and following EIP-1967 when the proxy itself is named generically), so it can read:

Registers MorphoFarm (type 1) to FarmRegistry. Expands protocol surface. MEDIUM.

Same benefit for Safe queued-tx alerts since both flows share the same explainer.

2. Maple ProposalScheduled → actual proposal calls

Maple's GovernorTimelock only emits proposalId + opaque hash, so Maple alerts had no AI summary section at all — recipients saw Proposal: 13 with no description.

The calldata is recoverable from the originating tx: the DAO multisig calls Safe execTransaction(...) wrapping scheduleProposals(address[] targets, bytes[] data) into the GovernorTimelock. We unwrap one Safe layer, decode scheduleProposals, and feed the targets+data into explain_batch_transaction. Bails cleanly to None for tx-fetch errors, unknown signatures (e.g. proposeRoleUpdates), and shape mismatches.

Snapshot was ruled out as an enrichment source — mapledao.eth has 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 -q
  • uv run ruff format .
  • uv run ruff check .
  • Full suite: 276 passing (1 pre-existing telegram failure unrelated, broken on main)
  • Live e2e on next Maple ProposalScheduled event — should produce an AI summary instead of bare Proposal: <id>
  • Live e2e on next InfiniFi addFarms (or similar address-arg-heavy) tx — AI summary should name the contract being passed in

🤖 Generated with Claude Code

spalen0 and others added 4 commits May 20, 2026 23:12
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>
@spalen0 spalen0 marked this pull request as ready for review May 21, 2026 09:24
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>
@spalen0 spalen0 merged commit 9ae76f5 into main May 21, 2026
2 checks passed
@spalen0 spalen0 deleted the feat/ai-explainer-address-labels-and-maple-unwrap branch May 21, 2026 09:32
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