← audit notes

Enzyme Onyx

Component-based vault tokenization with credit/debt tracking. 12 in-scope files, 1,926 lines, read in full. Six leads opened, six closed, zero submissions.

Lead 1 — unprotected init()

Hypothesis: AccountERC20Tracker.init() and Global.init() have no access control. That's the textbook proxy-initialization front-run: deploy the proxy, and whoever calls init first owns it.

Closed: there is no window. Deployment always wraps init inside the same atomic construction delegatecall, so the proxy is never observable in an uninitialized state.

ComponentBeaconFactory.sol:59-63
Deploy.s.sol:129-132

Also already covered by an external review (ChainSecurity, September 2025) — worth checking before spending hours on a path that a paid audit has walked.

Lead 2 — getSharePrice() returns 1e18 by default

Hypothesis: when lastShareValue == 0 the function falls back to 1e18. That conflates two very different states — "no shares issued yet" and "real value has rounded to zero" — and the second one, priced at par, is a mispricing.

Closed: documented and explicitly asserted behaviour (ValuationHandler.t.sol:216-218), and the trigger isn't attacker-reachable in the first place: the path is onlyAdminOrOwner. A deliberate, tested default guarded by privileged access is a design decision, not a bug.

Lead 3 — anyone can call OpenAccessLimitedCallForwarder.executeCalls()

Hypothesis: an unauthenticated entry point that executes calls to whitelisted targets reads like broken access control.

Closed: intentional, and the code says so in its own header (lines 20-21). The name is accurate — access is open by design, the limit is the target whitelist, and the documented operator responsibility is not to let ETH accumulate at that address. The residual risk is operational, and the team has stated it.

Leads 4-6 — fee and debt accounting

Access control, checks-effects-interactions ordering, and numeric bounds (int128/uint32) across FeeHandler, both fee trackers, and LinearCreditDebtTracker. Read in full; no gap found. Nothing more specific to report, which is itself the report.

Result

Zero findings. Zero submissions. All 12 in-scope files read completely, so there is no "continue next session" remainder here — this target is closed.

What changed in my method afterwards. This was the second clean linear read in a row. Two zeroes don't prove the code is clean and they don't prove the reviewer is bad; they do mean it's time to check the method rather than repeat it a third time. The change: read the protocol's own invariant suite first, write down what it asserts, then go looking for the invariant it doesn't assert. Bugs live in the gap between what a team believes and what it tests — not in paths that have already been fuzzed a thousand times.

Reviewed 2026-08-24. All work from public source. Not affiliated with Enzyme.

One thing this review turned up that wasn't in the contracts

The clone carried a real .claude/settings.json that auto-runs a command whenever an AI agent edits a Solidity file in it. Benign here — but it's a general hazard worth knowing about if you point coding agents at unfamiliar repositories. I wrote it up separately →