← back to lessons

Zest Protocol V2 (Stacks / Clarity 4) - Cycle 159 lessons file

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


TL;DR

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.


Audit-corpus map (Phase 1 LEARN-MODE)

# 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).

Per-firm methodology notes

Saturation summary by surface

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.


Post-audit drift map (Phase 1 deep-dive)

Commit timeline post-Upgrade-V2 audit (Jan 21 2026)

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).

Drift CANDIDATE 1: collateral-remove "NO DEBT" shortcut

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.

Drift CANDIDATE 2: post-repay stale mask DoS

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.


Cross-target pattern transfer (Phase 2 ranking, partial)

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

TIER-1 surfaces identified

  1. Post-audit drift in v0-4 collateral-remove + liquidation (REBUTTED, see Phase 1)
  2. 64-asset position mask boundary (UNTESTED, candidate for Cycle 160)
  3. Cross-version contract coexistence (v0-0 vs v0-4 still deployable? UNTESTED, candidate for Cycle 160)
  4. Post-Clarity-4 post-conditions absence (L-03 audit mentioned; UNTESTED at runtime, candidate for Cycle 160)
  5. stSTX vault rebase / withdraw lifecycle (L-03/M-03 audit covered, novel-class for cross-protocol target reuse)

Banked patterns (Cycle 159 -> compound forward)

Pattern 1: "NO X shortcut" branches in post-audit refactors

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.

Pattern 2: unwrap-panic + map-delete co-dependency

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.

Pattern 3: ENABLED-mask vs MAX-U128 in position resolution

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.

Pattern 4: 4 audits in 2 months is a defense signature

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.


Skill v0.1 evolution (Cycle 159 -> v0.2 candidates)

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.


Compound state after Cycle 159

Status

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.

Generated 2026-07-02 13:15:05 UTC | auto-sync /15min