Skip to content

fix(llm): decode Yearn Executor execute(bytes) scripts - #443

Merged
spalen0 merged 2 commits into
mainfrom
fix/llm-executor-script
Oct 23, 2026
Merged

spalen0 merged 2 commits into
mainfrom
fix/llm-executor-script

Conversation

@spalen0

@spalen0 spalen0 commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

Problem

yChad nonce 2299 was rated HIGH by our AI explainer (gist), while Govlens rated it MEDIUM (gist). The main driver: the governance Executor's execute(bytes) _script is a packed script (target(160) | size(96) header + calldata per call, per the verified Executor.vy), not ABI calldata. try_decode_inner_calldata can't read it, so it went to the prompt as raw hex. The model sliced it one hex digit off, reported a nonexistent selector 0x4759be10, and treated the six calls as "unresolved downstream effects". That was a main stated reason for HIGH.

In fact the script makes six accept_management() calls: the Executor accepts management of five reward distributors and the Funding Distributor.

Changes

  • utils/calldata/wrappers.py: split_packed_script mirrors Executor.execute (requires exact consumption, a non-zero target, and ≤64 calls). execute(bytes) is registered as an executing wrapper, so proxy-upgrade detection and protocol context see the inner calls.
  • utils/calldata/decoder.py: DecodedCall.script_calls holds each inner call, decoded against its own target's ABI when the chain is known. Without a chain id, only selectors resolvable offline are decoded.
  • utils/llm/ai_explainer.py / utils/llm/report.py: the prompt and the gist Call Flow list each script call with its target. Script targets feed labels, address links, references and risk anchors.
  • System prompt: never derive selectors or targets by slicing raw hex. Undecoded payloads are stated as undecoded.
  • known_selectors.py: Executor execute / set_operator and set_management / accept_management.

Rendered prompt for the nonce 2299 script:

Call 1: execute(bytes)
    bytes _script: ↳ Executor script, 6 calls; each is made by the Executor, not the sender
      Script call 1 → 0x95547eDe56cF74B73dd78a37f547127dffDA6113: accept_management()
      ...
      Script call 6 → 0xbCc932e4750C3E465A7E54A06A34F9EdF8f6116b (Funding Distributor): accept_management()

Not in scope

  • Govlens also used off-chain governance context: the on-chain proposal that bundled these management transfers failed (602 of 1,306 YFI in favour). We still don't fetch proposal status. That would be a follow-up in protocol_context.py.
  • The simulation prompt still caps state changes and events at 10.

Testing

  • New tests/test_executor_script.py (14 tests): split / malformed scripts, unwrap, decoder with ABI and offline paths, prompt and report rendering, label and reference collection. Uses the real nonce 2299 script.
  • Full suite: 1939 passed, 4 skipped. ruff check / ruff format clean.
  • Not run end-to-end against the live tx (no API keys in this environment).

🤖 Generated with Claude Code

spalen0 and others added 2 commits October 23, 2026 08:18
The Executor's `_script` is packed (32-byte `target | size` header per call,
then calldata), not ABI-encoded, so it reached the prompt as raw hex. On yChad
nonce 2299 the model sliced it one hex digit off, reported a nonexistent
selector 0x4759be10 instead of six accept_management() calls, and rated the
batch HIGH for "unresolved downstream calls".

- wrappers: split_packed_script mirrors Executor.execute; execute(bytes) is
  registered as an executing wrapper (proxy-upgrade detection, protocol context)
- decoder: DecodedCall.script_calls holds each inner call, decoded against its
  own target's ABI (offline-only when no chain id)
- prompt + report: render each script call with its target; script targets
  feed labels, address links and risk anchors
- system prompt: never derive selectors/targets by slicing raw hex
- known selectors: Executor execute/set_operator and set/accept_management

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 1-3 byte script call is forwarded unchanged and can reach a different
fallback path than an empty call; render it as an undecoded short payload
in both the prompt and the report, matching _decode_status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@spalen0
spalen0 marked this pull request as ready for review October 23, 2026 09:27
@spalen0
spalen0 merged commit 3cda5f0 into main Oct 23, 2026
3 checks passed
@spalen0
spalen0 deleted the fix/llm-executor-script branch October 23, 2026 09:28
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