Skip to content

fix(ai-explainer): address auditor findings from PR #226#227

Merged
spalen0 merged 1 commit into
mainfrom
ai-explainer-impl-diff-fixes
May 20, 2026
Merged

fix(ai-explainer): address auditor findings from PR #226#227
spalen0 merged 1 commit into
mainfrom
ai-explainer-impl-diff-fixes

Conversation

@spalen0
Copy link
Copy Markdown
Collaborator

@spalen0 spalen0 commented May 20, 2026

Follow-up to #226. The auditor's review landed after #226 was already merged, so these fixes need a separate PR.

Three findings — all valid:

P2.1 — Short-circuit `detect_proxy_upgrade` before decoding

Previously every call invoked `decode_calldata` before checking whether the selector was even an upgrade selector. For non-upgrade calls (which is most timelock alerts), an unknown selector miss could wait on the 30s Sourcify timeout for no reason.

Fix: selector membership check comes first; unknown selectors return None immediately.

Regression test patches `fetch_json` to raise and confirms no network call for a non-upgrade selector.

P2.2 — Show the proxy address in ProxyAdmin alerts

When the new ProxyAdmin path is detected, the tx `target` shown in the Telegram message is the ProxyAdmin — but the proxy actually being upgraded sits inside the calldata. Recipients couldn't tell which contract was changing without decoding the calldata themselves.

Fix: added a `🅿️ Proxy:` line when `upgrade.proxy_address != target`.

P2.3 — Recognize OZ storage-gap consumption as safe

Most serious finding. `_storage_layout` did naive slot-by-slot comparison and flagged the canonical OZ gap pattern as UNSAFE:

uint256 a; uint256[50] __gap;  →  uint256 a; uint256 b; uint256[49] __gap;

Slot 1 changed from `__gap` to `b`, so the original code flagged it. But this is the OpenZeppelin upgrade-safety primitive — almost every upgradeable contract has a trailing gap reserved exactly for this purpose. Without recognizing it, the tool would have produced false high-risk warnings on routine upgrades and recipients would have learned to ignore the warning exactly when it actually mattered.

Fix: detach trailing `gap` arrays before comparison, then validate that any new vars inserted before the gap correspond to a matching reduction in gap size. Still unsafe and flagged: gap underflow, no shrink while vars were inserted, gap removed without consumption.

Six new tests cover: consume-one, consume-many, underflow, no-shrink, full-consume, gap-removed-without-consumption.

Test plan

  • `uv run ruff check .` passes
  • `uv run pytest tests/test_impl_diff.py tests/test_proxy_upgrade.py` — 35 pass
  • Full suite still passes locally
  • Watch the next proxy-upgrade alert after merge to confirm the proxy address line renders and gap consumption no longer flags safe upgrades

🤖 Generated with Claude Code

Three correctness/perf fixes that landed on the original branch after PR #226
was already merged, so they need a follow-up PR.

1. Short-circuit detect_proxy_upgrade before decoding calldata.

   Previously every call invoked decode_calldata before checking the selector,
   meaning unknown selectors in non-upgrade calls could wait on the 30s
   Sourcify timeout. Now the selector is matched against the upgrade set
   first; unknown selectors return None immediately.

   Regression test: `test_non_upgrade_short_circuits_before_decode` patches
   fetch_json to raise and confirms no network call for a non-upgrade selector.

2. Show the proxy address in ProxyAdmin upgrade alerts.

   For OZ ProxyAdmin-routed upgrades, the tx `target` is the ProxyAdmin
   contract — the actual proxy being upgraded sits in the calldata. The
   Telegram alert previously showed only the ProxyAdmin and the old→new impl
   pair, leaving recipients unable to tell which proxy is changing. Added a
   `🅿️ Proxy:` line when `upgrade.proxy_address != target`.

3. Recognize OZ trailing-storage-gap consumption as safe.

   `_storage_layout` did naive slot-by-slot comparison, which flagged the
   canonical OZ pattern `uint256 a; uint256[50] __gap;` →
   `uint256 a; uint256 b; uint256[49] __gap;` as UNSAFE because slot 1
   changed from __gap to b. This is the intended safe upgrade pattern and
   would have produced false high-risk warnings on most upgradeable
   contracts — alert fatigue eroding signal value.

   Now: detach trailing `<name>gap` arrays before comparison, then validate
   that any new vars inserted before the gap correspond to a matching
   reduction in gap size. Still unsafe: gap underflow, gap kept same size
   while vars inserted (slot shift), gap removed without consumption.

   Six new tests cover: consume-one, consume-many, underflow, no-shrink,
   full-consume, gap-removed-without-consumption.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@spalen0 spalen0 merged commit 77d6b92 into main May 20, 2026
2 checks passed
@spalen0 spalen0 deleted the ai-explainer-impl-diff-fixes branch May 20, 2026 12:31
spalen0 added a commit that referenced this pull request May 20, 2026
Expands the README's "Proxy Upgrade Detection" section to cover the new
implementation-diff feature shipped in #226 and #227:

- All three recognized upgrade selectors (incl. OZ ProxyAdmin's
  upgradeAndCall) and the ProxyUpgrade dataclass returned.
- Selector short-circuit before calldata decoding (no Sourcify lookup
  for non-upgrade calls).
- Structural diff between old/new impl: function add/remove/changed
  visibility, storage layout safety with OZ trailing-gap consumption,
  EIP-7201 namespaced storage detection.
- Default-internal state vars now captured (visibility no longer
  required); function locals excluded via brace-depth tracking.

Updates the example assembled prompt to show the new "--- Proxy Upgrade
---" block content (with Old/New impl labels and function/state diff)
and adds utils/impl_diff.py to the module-structure listing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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