Zest Protocol v0-3-market

Lending/Borrowing Market Controller β€” Independent Security Audit

Contract
SP1A27KFY4XERQCCRCARCYD1CC5N7M6688BSYADJ7.v0-3-market
Source
On-chain (Hiro Explorer)
Clarity Version
Clarity 4 β€” uses as-contract?, current-contract, stacks-block-time
Lines of Code
~1,635
Audit Date
February 24, 2026
Auditor
cocoa007
Confidence
🟑 MEDIUM β€” multi-contract system; can't verify market-vault or vault internals
Priority Score
2.7 / 3.0 (Financial=3, Deploy=3, Complexity=3, Exposure=2, Novelty=2)

Findings Summary

0Critical
0High
2Medium
2Low
3Info

Overview

v0-3-market is the central controller for Zest Protocol's lending/borrowing market on Stacks mainnet. It orchestrates six underlying vault contracts (STX, sBTC, stSTX, USDC, USDH, stSTXbtc), manages oracle price feeds (Pyth and DIA), and implements a sophisticated graduated liquidation mechanism with configurable curve exponents and grace periods.

Architecture highlights:

Overall assessment: This is one of the most well-engineered Clarity contracts I've audited. The code demonstrates deep understanding of DeFi lending mechanics, proper Clarity 4 idioms, and careful attention to edge cases (disabled collateral pricing, bad debt socialization, oracle staleness). No critical or high severity findings. The medium findings relate to edge-case behavior in the graduated liquidation math and bad debt socialization trigger conditions.

Findings

M-01: Simplified Liquidation Curve Exponent

MEDIUM
Location: calc-liq-factor-exp (line ~710)
Description:

The graduated liquidation system supports a configurable curve exponent to control how aggressively liquidation percentage scales. However, the implementation only handles three cases:

If the DAO configures a curve exponent of, say, 0.25 (2500 BPS) or 0.75 (7500 BPS), the actual behavior is always √x (0.5 power). This means:

(define-private (calc-liq-factor-exp (factor uint) (exp uint))
  (if (is-eq exp BPS) 
    factor
    (if (> exp BPS) 
        (/ (pow factor (/ exp BPS)) (pow BPS (- (/ exp BPS) u1)))
        (sqrti (* factor BPS))))) ;; assume factor^0.5 β€” ALWAYS 0.5 regardless of exp
Impact: Misconfigured liquidation aggressiveness for sub-linear exponents. If DAO only uses exponents of 0.5 or β‰₯1.0, no impact. Actual risk depends on DAO parameter governance.
Recommendation: Document the supported exponent values (0.5, 1.0, or integer multiples of BPS). Alternatively, implement a piecewise approximation for common fractional exponents (0.25, 0.75) using nested square roots.

M-02: Bad Debt Socialization May Not Trigger for Multi-Collateral Positions

MEDIUM
Location: liquidate β€” bad debt socialization check (line ~1520)
Description:

Bad debt socialization only triggers when the borrower's position has exactly one collateral type remaining AND that collateral is fully seized:

(no-collateral-left (and
  (is-eq (len (get collateral pos-full)) u1)
  (is-eq coll-removed u0)))

For multi-collateral positions where collateral is depleted across multiple liquidation calls targeting different collateral types, the trigger depends on whether v0-market-vault removes zero-balance entries from the collateral list:

Impact: If v0-market-vault retains zero-balance collateral entries, bad debt from multi-collateral positions accumulates without socialization. This creates a growing gap between actual vault assets and tracked debt, eventually causing depositor losses on withdrawal.
Recommendation: Either (a) verify that v0-market-vault.collateral-remove removes entries when balance hits zero, or (b) change the check to sum all collateral amounts rather than checking list length: (is-eq total-coll-remaining u0).

L-01: liquidate-multi Does Not Support Inline Price Feeds

LOW
Location: call-liquidate / liquidate-multi (line ~755)
Description:

The batch liquidation helper liquidate-multi hardcodes none for the price-feeds parameter. Liquidators must update Pyth price feeds in a separate transaction before calling liquidate-multi. If the price feed update and batch liquidation land in different blocks, the prices may become stale, causing the batch to fail.

(define-private (call-liquidate (position { ... }))
  (liquidate ...
             none   ;; collateral-receiver defaults to liquidator
             none)) ;; price-feeds not supported in batch
Impact: Batch liquidations may fail if oracle prices become stale between the price update transaction and the batch call. Individual liquidate() with inline feeds is unaffected.
Recommendation: Accept an optional price-feeds parameter at the liquidate-multi level and pass it to the first call-liquidate invocation (subsequent calls in the same block can use cached prices).

L-02: No Deadline Protection on Lending Operations

LOW
Location: borrow, repay, collateral-add, collateral-remove
Description:

None of the lending operations accept a deadline parameter. If a transaction is delayed in the mempool, it may execute at a significantly different interest index than when the user signed it. For borrow, a delayed transaction results in more scaled debt than expected. For repay, a delayed transaction may not fully repay the intended amount due to accrued interest.

Impact: Low β€” Stacks mempool delays are typically short, and the interest rate impact per block is minimal. Users can set min-out / min-shares on deposit/redeem operations for slippage protection, but no equivalent exists for borrow/repay index drift.
Recommendation: Consider adding an optional max-block-time parameter to borrow and repay for users who want transaction freshness guarantees.

I-01: Unused Parameters in vault-system-repay

INFO
Location: vault-system-repay (line ~169)
Description:

The vault-system-repay private function accepts ft (<ft-trait>) and ft-address (principal) parameters but never uses them β€” vault routing is purely based on aid. These parameters are vestigial and add confusion without functional impact.

(define-private (vault-system-repay (aid uint) (amount uint) (ft <ft-trait>) (ft-address principal))
  (if (is-eq aid STX) (contract-call? .v0-vault-stx system-repay amount)
  ;; ... ft and ft-address never referenced
Impact: None β€” dead code. The vault contracts know their own token type.
Recommendation: Remove unused parameters for code clarity, or document why they're retained (e.g., future extensibility).

I-02: contract-caller == tx-sender Restriction Limits Composability

INFO
Location: collateral-add, borrow, repay, liquidate
Description:

Multiple functions assert (is-eq contract-caller tx-sender), which prevents smart contract wallets, routers, or automation contracts from calling the market on behalf of users. This is an intentional security measure that eliminates an entire class of proxy attacks, but it means:

Impact: Informational β€” security/composability tradeoff. The protocol provides composite operations (supply-collateral-add, collateral-remove-redeem, liquidate-redeem) that reduce the need for external routers.
Recommendation: Document this restriction. Consider a future allowlist for approved proxy contracts if composability demand arises.

I-03: Exemplary Clarity 4 Asset Safety

INFO
Location: supply-collateral-add (line ~1190)
Description:

The supply-collateral-add function demonstrates best-practice Clarity 4 usage with as-contract? and explicit asset allowances:

;; For wSTX: explicitly allows only STX movement
(as-contract? ((with-stx amount))
  (try! (vault-deposit asset-id amount min-shares account)))

;; For other tokens: explicitly allows only the specific FT
(as-contract? ((with-ft ft-address "*" amount))
  (try! (vault-deposit asset-id amount min-shares account)))

This ensures the contract can only move the exact token type and amount needed, even when operating under its own authority. This is strictly safer than pre-Clarity 4 as-contract which grants blanket asset access.

Impact: Positive β€” significantly reduces the blast radius if any vault contract or trait implementation were malicious.

Positive Observations

Architecture

                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚   v0-3-market     β”‚  ← This contract
                    β”‚  (Controller)     β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                             β”‚
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
          β–Ό          β–Ό       β–Ό       β–Ό          β–Ό          β–Ό
     v0-vault-   v0-vault- v0-vault- v0-vault- v0-vault- v0-vault-
       stx        sbtc     ststx     usdc      usdh     ststxbtc
          β”‚          β”‚       β”‚       β”‚          β”‚          β”‚
          β–Ό          β–Ό       β–Ό       β–Ό          β–Ό          β–Ό
        wSTX       sBTC    stSTX    USDC      USDH    stSTXbtc

   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ v0-assetsβ”‚  β”‚v0-egroup β”‚  β”‚v0-market-β”‚  β”‚  Oracles  β”‚
   β”‚(registry)β”‚  β”‚  (risk)  β”‚  β”‚  vault   β”‚  β”‚Pyth / DIA β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Risk Assessment

CategoryAssessment
Funds at riskYes β€” manages deposits, borrows, and liquidations across 6 asset vaults
Oracle dependencyPyth (primary) + DIA (secondary) with confidence + staleness checks
Admin privilegesDAO executor can pause liquidations, set grace periods, configure oracle confidence
ComposabilityLimited β€” contract-caller == tx-sender blocks proxy contracts
Clarity versionClarity 4 βœ“ β€” uses as-contract? with explicit asset allowances
Multi-contract riskDepends on 6 vault contracts + asset registry + egroup resolver + market-vault