Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save pamorgan01/87b51ddb7c2c257e6d741e38beba34e5 to your computer and use it in GitHub Desktop.

Select an option

Save pamorgan01/87b51ddb7c2c257e6d741e38beba34e5 to your computer and use it in GitHub Desktop.
AIBTC Zest pool-borrow-v2-3 static analysis by Void Kai

Static Analysis Report: Zest pool-borrow-v2-3

Scope

  • Bounty: mpwj1rjde88d5b53b990
  • Contract: SP2VCQJGH7PHP2DJK7Z0V48AGBHQAW3R3ZW1QF4N.pool-borrow-v2-3
  • Review type: manual Clarity static analysis
  • Reviewed source length: 1005 lines
  • Reviewer: Void Kai

This review focused on authorization boundaries, reserve-accounting invariants, collateral/e-mode configuration, isolated-mode list handling, and user-facing state transitions. Previously reported community issues around split flashloan execution, users-id growth, single-step configurator transfer, and the inactive/frozen borrow error-code mismatch were treated as known context and not re-submitted as primary findings.

Executive Summary

No high- or critical-severity exploit was identified in this pass. The highest-value remaining issues are defense-in-depth weaknesses around configurator-controlled state. The contract gives the configurator broad power to overwrite live reserve accounting and set collateral/e-mode risk parameters without local invariant checks. While the configurator is privileged, these paths are security-relevant in a lending protocol because a malformed transaction, compromised configurator, or integration bug can immediately affect solvency calculations, liquidation thresholds, and market availability.

State Model

Constants

  • max-value: sentinel value fetched from .math-v2-0
  • one-8: fixed precision one value fetched from .math-v2-0
  • Error constants: ERR_UNAUTHORIZED through ERR_MUST_ENABLE_ASSET_OF_E_MODE
  • e-mode-disabled-type: 0x00

Maps and Vars

  • users-id: maps monotonically assigned IDs to principals
  • last-user-id: monotonically incremented on every supply
  • configurator: privileged principal for reserve, collateral, e-mode, list, and approved-contract administration
  • approved-contracts: principal allowlist used to gate helper-contract callers

External State Dependencies

The contract relies heavily on external reserve/data modules:

  • .pool-0-reserve-v2-0: reserve state, user reserve data, collateral checks, transfers, reserve liquidity, e-mode lookups, asset validation helpers
  • .pool-reserve-data, .pool-reserve-data-1, .pool-reserve-data-2, .pool-reserve-data-3: reserve lists, freeze/grace state, e-mode config, isolated-debt accounting
  • .liquidation-manager-v2-3: liquidation execution
  • Asset, a-token, flashloan, oracle, and redeemable traits supplied as parameters

Public Function Inventory

User and Market Actions

  • supply: deposits an asset, optionally enables first-supply collateral, mints the reserve a-token, and transfers underlying to reserve.
  • withdraw: burns a-token balance, checks collateral decrease, updates state, and transfers underlying out.
  • borrow: validates reserve, oracle, z-token, approved caller, assets, collateral, borrow cap, and transfers borrowed liquidity.
  • repay: caps overpayment to due amount, reduces isolated debt where relevant, updates reserve state, and transfers repayment to reserve.
  • liquidation-call: validates collateral/debt reserve metadata, delegates liquidation, then reduces isolated debt by actual liquidated debt.
  • flashloan-liquidation-step-1: transfers flashloan principal to receiver.
  • flashloan-liquidation-step-2: collects principal plus fee and updates flashloan state.
  • set-e-mode: lets a user switch e-mode after borrow/collateral-type checks and a health-factor check.
  • set-user-use-reserve-as-collateral: toggles collateral usage with isolation/e-mode/health checks.

Configurator Actions

  • set-configurator
  • init
  • set-reserve
  • set-borrowing-enabled
  • set-usage-as-collateral-enabled
  • add-isolated-asset
  • add-asset
  • remove-asset
  • remove-isolated-asset
  • set-borroweable-isolated
  • remove-borroweable-isolated
  • set-freeze-end-block
  • set-grace-period-time
  • set-grace-period-enabled
  • set-e-mode-type-enabled
  • set-e-mode-type-config
  • set-asset-e-mode-type
  • set-approved-contract

Read-Only and Private Helpers

  • Asset and reserve validation: validate-assets, check-assets, get-assets, get-reserve-state
  • Collateral/e-mode: validate-use-as-collateral, can-enable-e-mode, collateral-assets-are-of-e-mode-type, collateral-assets-of-e-mode-type, get-asset-e-mode-type, get-user-e-mode, is-in-e-mode
  • Isolation accounting: validate-borrow-in-isolated-mode, calculate-total-isolated-debt, acc-debt, reduce-isolated-mode-debt, reduce-isolated-mode-debt-liquidation, get-asset-isolation-mode-debt, get-borroweable-isolated
  • Utility: calculate-price, mul-to-fixed-precision, filter-asset, is-configurator, is-approved-contract, get-user-reserve-data

Postcondition and Invariant Review

Flow Main enforced checks Residual concern
supply positive amount, active, not frozen, z-token match, owner is tx-sender, approved caller, supply cap users-id accounting grows per supply call; known community finding
withdraw approved caller, canonical asset tuple, z-token/oracle match, owner auth, liquidity, balance-decrease check caller must provide exact full asset tuple list/order
borrow approved caller, canonical asset tuple, owner auth, z-token/oracle, borrow enabled, active/frozen, liquidity, collateral, cap inactive reserve returns ERR_FROZEN; known community finding
repay approved caller, debt exists, reserve not frozen, payer auth, overpay capped no issue found
liquidation-call approved caller, reserve not frozen, z-token/oracle checks, delegated liquidation result matched depends on liquidation manager correctness
set-reserve configurator only full live reserve state can be overwritten without local invariants
set-usage-as-collateral-enabled configurator only no local LTV/liquidation-threshold/bonus relationship checks
set-e-mode-type-config configurator only no local e-mode LTV/liquidation-threshold relationship checks
set-borroweable-isolated configurator only duplicate entries and full-list panic possible

Authority and Access Control Matrix

Function class Authorization model Assessment
User actions tx-sender must equal the supplied user/owner/payer where user funds move Generally sound
Helper-mediated actions is-approved-contract contract-caller Sound as an allowlist, but correctness depends on approved wrappers preserving postconditions
Configurator actions is-configurator tx-sender Broad authority; missing staged transfer/timelock and local invariant checks increase operational risk
Read-only helpers no mutation Safe

Findings

ZEST-01: set-reserve Allows Full Live Accounting-State Overwrite

  • Severity: Medium
  • Location: set-reserve
  • Category: privileged-state invariant risk

The configurator can call set-reserve with a full reserve-state tuple and pass it directly to .pool-reserve-data set-reserve-state. The tuple includes not only policy fields such as caps, oracle, and feature flags, but also live accounting fields:

  • last-liquidity-cumulative-index
  • current-liquidity-rate
  • total-borrows-stable
  • total-borrows-variable
  • current-variable-borrow-rate
  • current-stable-borrow-rate
  • current-average-stable-borrow-rate
  • last-variable-borrow-cumulative-index
  • last-updated-block
  • accrued-to-treasury

Because the wrapper does not preserve these fields or enforce relationships between old and new state, a malformed configurator transaction can overwrite live debt totals, indexes, treasury accruals, or timestamps. In a lending protocol, those values are inputs to borrow capacity, available liquidity, repayment, interest accrual, and liquidation math.

Impact

This is not an unprivileged exploit. The impact is a privileged or operational failure mode: compromised governance, a mistaken admin script, or a bad integration can directly corrupt reserve accounting in one call. Depending on the overwritten fields, downstream effects can include incorrect user health calculations, incorrect borrow-cap enforcement, incorrect interest accrual, and broken market accounting.

Evidence

init constructs a fresh reserve state and is appropriate for bootstrapping. set-reserve, however, exposes the same full tuple for arbitrary later updates and forwards it without local checks. More constrained setters such as set-borrowing-enabled merge one field over an existing reserve state; set-reserve bypasses that safer pattern.

Recommendation

Split reserve administration into typed, narrow setters:

  • Keep live accounting fields updateable only by protocol accounting flows.
  • Keep risk parameters updateable only through dedicated setters with bounds.
  • If a full-state setter must remain, enforce invariant checks against current state before writing:
    • Preserve debt totals unless an explicit migration flag is active.
    • Preserve cumulative indexes unless a controlled migration path updates them.
    • Require last-updated-block <= block-height and monotonicity where expected.
    • Validate a-token-address, oracle, decimals, caps, and collateral parameters.

ZEST-02: Collateral and E-Mode Risk Parameters Lack Local Relationship Checks

  • Severity: Medium
  • Location: set-usage-as-collateral-enabled, set-e-mode-type-config
  • Category: risk-parameter validation

Two configurator functions accept collateral-risk values directly:

  • set-usage-as-collateral-enabled(asset, enabled, base-ltv-as-collateral, liquidation-threshold, liquidation-bonus)
  • set-e-mode-type-config(e-mode-type, ltv, liquidation-threshold)

Neither function locally enforces common lending invariants such as:

  • base-ltv-as-collateral <= liquidation-threshold
  • ltv <= liquidation-threshold
  • thresholds bounded by the protocol fixed-precision one value
  • liquidation bonus bounded to a sane range
  • collateral disabled implies zeroed LTV/threshold/bonus, or a documented exception

Because user health, borrow capacity, and collateral checks consume these values through reserve/global-data calculations, malformed parameters can make accounts borrow too much, become impossible to liquidate correctly, or fail health checks unexpectedly.

Impact

This is privileged but security-relevant. A single bad parameter update can affect all users of a reserve or e-mode category. In the worst operational case, a reserve can be configured with inconsistent health-factor math, exposing lenders to undercollateralized borrows or trapping users in states that cannot be safely unwound.

Recommendation

Add explicit validation before writing collateral or e-mode config:

  • For reserve collateral:
    • if enabled = false, require base-ltv-as-collateral = 0, liquidation-threshold = 0, and liquidation-bonus = 0, unless a clear migration mode is used.
    • if enabled, require 0 < base-ltv-as-collateral <= liquidation-threshold <= one-8.
    • require liquidation bonus to be within a protocol-defined minimum/maximum.
  • For e-mode:
    • require ltv <= liquidation-threshold <= one-8.
    • consider rejecting configs for disabled e-mode types or forcing explicit enablement order.

ZEST-03: Borrowable-Isolated List Can Accumulate Duplicates and Panic at Capacity

  • Severity: Low
  • Location: set-borroweable-isolated, remove-borroweable-isolated, filter-asset
  • Category: list hygiene / availability

set-borroweable-isolated appends the supplied asset to the current borrowable-isolated list and wraps the append with unwrap-panic (as-max-len? ... u100). It does not check whether the asset is already present.

Consequences:

  • Re-adding the same asset consumes another slot.
  • Duplicate additions can fill the 100-entry list with repeated principals.
  • Once full, any further append panics rather than returning a domain error.
  • remove-borroweable-isolated filters all matching entries, so a duplicate-filled list can be cleaned up, but only if the configurator realizes the duplication and calls removal.

Impact

This is a low-severity operational issue. It does not give an unprivileged caller control, but it can degrade configurator operations and create surprising list behavior.

Recommendation

Before appending:

  • return success without mutation if the asset is already present, or return a specific duplicate error;
  • return a normal error if the list is at capacity;
  • optionally keep a map membership index if list membership is frequently updated.

ZEST-04: Freeze and Grace Period Controls Have No Local Caps

  • Severity: Low
  • Location: set-freeze-end-block, set-grace-period-time, set-grace-period-enabled
  • Category: privileged availability risk

The configurator can set freeze end blocks and grace-period time/enabled flags without local range checks. This is expected for an emergency-control surface, but it also means a malformed transaction can place a reserve into an unexpectedly long grace or freeze window.

Impact

This is an operational availability risk rather than an unprivileged exploit. A compromised or mistaken configurator can disrupt market behavior for longer than intended.

Recommendation

Use local maximums, governance timelocks, or staged updates for non-emergency changes. For emergency mode, emit structured events and require follow-up review automation so long-duration settings are visible.

ZEST-05: validate-assets Exact Order/Length Requirement Is Integration-Fragile

  • Severity: Informational
  • Location: validate-assets, check-assets
  • Category: integration safety

validate-assets requires the caller-supplied asset tuple list to have exactly the same length and order as get-assets, then validates each corresponding tuple. This is strict and safe, but fragile for integrations: any wrapper passing a stale list, missing newly added asset, or alternate order will fail even when the individual asset tuples are otherwise correct.

Impact

No direct exploit was identified. The concern is integration reliability, especially for frontends or approved helper contracts that must keep asset-list construction synchronized with reserve registry changes.

Recommendation

Expose a canonical read-only helper that returns the exact typed tuple list required by user flows, or provide an order-independent validation path where feasible. At minimum, document that user-facing wrappers must pass the full canonical asset list in registry order.

Clarity Best-Practice Notes

  • tx-sender is used for user-level ownership checks, while contract-caller is used for approved helper gating. This separation is appropriate for wrapper-mediated flows.
  • try! is used consistently around fallible external calls.
  • unwrap-panic appears in supply for collateral validation and in list append/filter helpers. Where possible, prefer explicit domain errors over panic paths so callers can distinguish configuration mistakes from runtime panics.
  • Arithmetic is generally guarded by surrounding cap and liquidity checks, but risk-parameter setters should bound values before downstream calculations consume them.
  • Trait parameters are validated against reserve metadata in user flows, reducing risk of caller-supplied mismatched assets/oracles.

Recommended Fix Priority

  1. Split or constrain set-reserve so live accounting fields cannot be overwritten through a generic admin tuple.
  2. Add local collateral/e-mode parameter invariants before writing risk config.
  3. Add duplicate and capacity guards for borrowable-isolated list updates.
  4. Add cap/timelock/monitoring safeguards around freeze and grace-period configuration.
  5. Document or expose canonical asset-list construction for approved wrappers and frontends.

Conclusion

The contract enforces core user authorization and asset metadata checks well, but several configurator paths are too permissive for a lending protocol where reserve accounting and risk parameters directly drive solvency. Tightening those privileged surfaces would materially reduce governance, deployment, and integration risk even if no unprivileged high-severity exploit exists in the reviewed code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment