Target: Zest Protocol V2, Immunefi $100K Critical, no KYC, permanent BB program (not contest) LEARN-MODE date: 2026-05-19 (Cycle 159) Predecessor: Cycle 158 pre-check (PROCEED-WITH-CAUTION score 0.40) Cross-target lessons applied: modular-account-v2-cycle141 + base-azul-cycle150 + k2-soroban-cycle154
Zest V2 is heavily-audited (4 reports, 3 firms, Oct-Dec 2025 = ~50 findings, almost all RESOLVED). Post-audit drift exists: 3 market.clar evolutions between Dec 20 2025 (Upgrade V2 signoff) and Feb 26 2026 (final deployment). We identified 2 post-audit drift CANDIDATES, manually traced both, and both are correctly defended. The protocol team has rigorous defensive design.
Verdict: WALK CLEAN with banked Clarity-language Skill v0.1 + lessons file (this file). No submission.
Coverage advantage assessment confirmed: WEAK on Clarity-specific (no prior Skill), MEDIUM on Aave-V3-lending invariants (familiar via K2/MA V2). Net: useful research target for v0.2 Skill building, not productive for first-pass BB submission.
| # | Firm | Date | Type | Findings (raw) | Status |
|---|---|---|---|---|---|
| 1 | Clarity Alliance | 2025-10-23 | Initial (v2 ~Oct) | 2C + 11H + 17M = ~30 | All RESOLVED by Dec 2025 follow-ups |
| 2 | Clarity Alliance | 2025-12-03 | Upgrade audit | 1C + 4H + 7M + 7L = ~19 | All RESOLVED |
| 3 | Greybeard Security | 2025-12-04 | Independent | NOT FETCHED (Drive PDF requires Google Drive auth) | Unknown |
| 4 | Clarity Alliance | 2025-12-20 | Upgrade V2 | 1C + 7M = 8 | 7 RESOLVED, 1 ACKNOWLEDGED |
Total accounted findings: ~57 (excluding Greybeard). ACK risk: 1 finding only (M-07 Upgrade V2: "Full Vault Asset Debt Socialization Bricks Vault" - documented OOS by design).
| Surface | Coverage saturation |
|---|---|
| FlashLoan invariants | HIGH (5+ findings in Oct + Dec) |
| Vault share/asset rounding | HIGH (multiple findings) |
| Liquidation flow + bad debt | HIGH (multiple) |
| Pyth + DIA oracle integration | HIGH (M-15 confidence interval, M-16 freshness) |
| Governance + DAO | MEDIUM (M-06 proposals never expire, M-07 multisig config) |
| Disabled collateral / disabled debt | HIGH (H-09 + H-03 + extensive coverage) |
| Efficiency groups (egroups) | HIGH (specific to Zest V2 design) |
| SIP-010 fungible token compliance | HIGH (M-01 covered) |
| Post-Clarity-4 post-conditions usage | LOW (L-03 only) |
| Cross-contract callback ordering | UNKNOWN |
| Post-audit drift on Feb 2026 commits | UNKNOWN (CYCLE 159 SCOPE) |
The novel-surface zones are: post-Clarity-4 post-conditions usage + post-audit drift on Feb 2026 commits.
c5fd388 2026-01-21 Update market, update scope market.clar -> v0-0-market.clar (+7/-8)
650bc3f 2026-01-28 Update with latest deployments (file changes not detailed)
581f28b 2026-02-02 Update to latest v0-1-market.clar -> v0-2-market.clar (+4)
4ae1d6e 2026-02-04 Update to latest deployment v0-2-market.clar -> v0-3-market.clar (+53/-79)
9d73cea 2026-02-26 Latest deployment v0-3-market.clar -> v0-4-market.clar (+30/-3)
fe18f82 2026-05-12 logo cosmetic
e033a61 2026-05-12 clean logo cosmetic
Material code changes: - Jan 28 -> Feb 4 (v0-0 -> v0-3): 4 commits, net ~140 lines changed in market.clar - Feb 26 (v0-3 -> v0-4): final commit, 33 lines added to liquidation flow
The post-audit window for material risk: 21 January 2026 -> 26 February 2026 (5 weeks), then frozen until present (May 19 2026, 83 days frozen).
Code location: mainnet/contracts/market/v0-4-market.clar lines 1107-1169
Pattern: v0-3 introduced a conditional branch in collateral-remove:
(has-debt (> (len (get debt position)) u0))
...
(if has-debt
;; HAS DEBT: full price resolution + accrual + health check
...
;; NO DEBT: Skip price resolution entirely
(let ((result (try! (contract-call? .v0-market-vault collateral-remove ...))))
(ok result)))
Hypothesis: if (get debt position) could be empty while actual debt exists (e.g. in a disabled-debt asset), the NO-DEBT branch would skip health checks and let the user withdraw all collateral.
Rebuttal (manual code trace): in v0-market-vault.clar line 325, get-position returns:
(is-debt (lookup-debt user-id mask MAX-U128))
The debt-lookup uses MAX-U128 (all debt assets), NOT the enabled-mask. So (get debt position) returns the FULL debt list including disabled-debt assets. The has-debt check is correct.
Verdict: REBUTTAL HOLDS. The protocol team learned from Oct H-09 + Dec H-03 (disabled-debt accounting bugs) and correctly preserved the distinction in v0-3+. No exploit.
Hypothesis: iter-lookup-debt (v0-market-vault.clar line 218) calls get-debt which uses unwrap-panic on the debt map (line 229). If a user fully repays a debt and the user's mask still has the bit set, the next get-position call panics, DoS'ing collateral-remove and other dependent functions.
Rebuttal (manual code trace): debt-remove-scaled (v0-market-vault.clar) properly updates the mask on full repay:
(nmask (if (is-eq remaining u0)
(mask-update mask asset-id false false) ;; debt, remove
mask))
When remaining = 0, the mask bit for that debt asset is cleared. The next mask-to-list-debt iteration won't include the deleted asset-id. So get-debt unwrap-panic is never called on a deleted entry.
Verdict: REBUTTAL HOLDS. The mask + debt-map consistency invariant is maintained correctly.
Applied 4 prior lessons matrices to Zest V2 surface map:
| Pattern from | Match strength | Zest V2 location | Status |
|---|---|---|---|
| MA V2 module-pair-asymmetry-check | MEDIUM | v0-X-market.clar versions (v0-0..v0-4 coexist?) | NEEDS MANUAL CHECK in Cycle 160 if pursued |
| K2 oracle cascade first-match | STRONG | Pyth + DIA in market.clar; resolve-callcode dispatch | COVERED BY CLARITY ALLIANCE M-15, M-16 |
| Base Azul module-pair STRICT-GATE vs SILENT-SKIP | MEDIUM | No direct module-pair pattern; closest is vault-stx vs vault-sbtc vs vault-stx adapters | LOW-PRIORITY |
| K2 swap adapter asymmetry | WEAK | Zest has no DEX swap adapter (lending only, no aggregator router) | NOT APPLICABLE |
| K2 admin role asymmetry | MEDIUM | DAO + impl auth pattern in v0-market-vault | COVERED in Dec 2025 H-02 (DAO implementation cannot be updated) |
| Verus missing-backing-check on cross-chain | WEAK | No cross-chain in Zest core | NOT APPLICABLE |
| K2 64-reserve bitmap boundary | STRONG | Zest uses ITER-UINT-64 list pattern (64-bit position mask, max 64 assets) | NEEDS Clarinet boundary test, candidate for Cycle 160 |
When a protocol refactors a function after an audit to add a fast-path branch (e.g. "NO DEBT: skip price feeds"), the question is always: can the predicate that enables the fast-path be reached while the full-path safety check would have caught something?
In Zest V2's case, the team correctly preserved the disabled-debt accounting (passing MAX-U128 to lookup-debt) so the fast-path predicate is sound. But the GENERAL PATTERN remains a high-yield audit hook for ANY post-audit codebase. Bank as cross-target Skill v0.5 candidate.
Clarity's unwrap-panic on map-get? is structurally dangerous if any path can delete the map entry while another path still references it via stale state. The defense is to update ALL stale state (masks, lists, parallel maps) atomically with the deletion.
Zest V2 implements this correctly (mask updated on full repay). But the pattern repeats often in Clarity codebases. Bank as /clarity-audit-primitives Skill v0.2 audit hook.
When a Clarity (or any chain) lending protocol uses a bitmap for asset enablement, the distinction between "enabled-only" and "all-position" reads matters at every callsite. Zest V2's get-position uses ENABLED-mask for collateral and MAX-U128 for debt - intentional asymmetry. Audit hook: confirm every position-read declares which mask it uses and why.
Zest V2 had 4 published audits in 2 months (Oct 23 -> Dec 20 2025). This high audit-cadence is BOTH a saturation signal AND a signal of active engineering momentum. The Feb 2026 post-audit drift is a natural continuation: they kept iterating. Audit cadence > 1 per quarter implies the team is responsive but also the codebase is moving fast - which means the post-audit drift window is materially relevant.
Bank: when audit-cadence > 1/quarter, weight post-audit drift WINDOW as the primary attack surface, not the audited code itself.
After Cycle 159, the /clarity-audit-primitives Skill should evolve to include:
These bank from Cycle 159 manual code-trace work even though no submission was generated.
/clarity-audit-primitives v0.1)Cycle 159 WALK CLEAN with banked Clarity Skill + lessons file (this file). No submission. Recommended follow-up: Cycle 160 boundary testing on the 3 untested surfaces (64-mask boundary, cross-version coexistence, post-conditions absence) - probability of finding novel bug: low but not zero. Coverage advantage on these surfaces is LOW (we'd need Clarinet installed) so EV is mixed.