# Static Analysis: Zest Pool-Borrow v2-3 **Contract:** `SP2VCQJGH7PHP2DJK7Z0V48AGBHQAW3R3ZW1QF4N.pool-borrow-v2-3` **Audit date:** 2026-06-09 **Scope:** Deployed source returned by the Hiro mainnet contract-source API. ## Executive summary `pool-borrow-v2-3` is the public orchestration layer for supply, withdrawal, borrowing, repayment, liquidation, flashloan liquidation, isolation mode, e-mode, and reserve configuration. It delegates custody, position accounting, interest indexes, and much of the risk calculation to the `pool-0-reserve-v2-0` and `pool-reserve-data-*` contracts. The primary finding is an isolation-debt accounting asymmetry: borrowing adds principal-only amounts to the isolation ledger, while repayment and liquidation subtract interest-inclusive amounts. This can make tracked isolation debt fall faster than principal debt and permit later borrowing beyond the intended debt ceiling. The split flashloan flow also has no local in-flight binding between the transfer-out and repayment steps. No high or critical issue was identified. The review found three medium, three low, and two informational findings. | Severity | Count | | --- | ---: | | Critical | 0 | | High | 0 | | Medium | 3 | | Low | 3 | | Informational | 2 | ## 1. State model ### Local persistent state | Store | Line | Initial value / shape | Writers | Authority | | --- | ---: | --- | --- | --- | | `users-id` map | 47 | sequential ID to supplier principal | `supply` | Any approved helper on a valid supply | | `last-user-id` | 49 | `u0` | `supply` | Any approved helper on a valid supply | | `configurator` | 623 | deployer | `set-configurator` | Current configurator by `tx-sender` | | `approved-contracts` map | 992 | principal to bool | `set-approved-contract` | Configurator | ### Constants | Lines | Constants | Purpose | | --- | --- | --- | | 8-9 | `max-value`, `one-8` | Sentinel and fixed-point unit | | 11-42 | `ERR_*` | Authorization, risk, liquidity, asset, mode, and validation errors | | 45 | `e-mode-disabled-type` | Disabled e-mode identifier | ### External state model | External contract(s) | State / capability | | --- | --- | | `pool-0-reserve-v2-0` | Reserve state, user positions, collateral flags, liquidity, borrow/repay/liquidation updates, asset lists | | `pool-reserve-data`, `-1`, `-2`, `-3` | Reserve state, freeze/grace settings, e-mode, isolation debt | | Supplied FT / a-token contracts | User balances, mint/burn, transfers | | Caller-supplied oracle contracts | Asset prices | | Caller-supplied approved helper contracts | Public user-flow entrypoints and flashloan coordination | ## 2. Function inventory ### Read-only and private functions | Group | Functions | Behavior | | --- | --- | --- | | Supplier index | `get-user`, `get-last-user-id` | Reads append-only supplier-call index | | Collateral / asset validation | `validate-use-as-collateral`, `validate-assets`, `check-assets` | Enforces supplied asset tuple consistency | | Isolation accounting | `reduce-isolated-mode-debt-liquidation`, `reduce-isolated-mode-debt`, `validate-borrow-in-isolated-mode`, `calculate-total-isolated-debt`, `acc-debt`, `calculate-price`, `get-asset-isolation-mode-debt` | Checks debt ceiling and mutates isolation debt | | e-mode | `can-enable-e-mode`, collateral-type fold helpers, `set-user-e-mode`, `calculate-user-global-data`, e-mode getters | Validates and stores user e-mode | | Reserve/config getters | `get-assets`, `get-reserve-state`, `get-user-reserve-data`, `get-borroweable-isolated`, `filter-asset`, `is-configurator`, `is-approved-contract` | Delegated state queries and helper checks | ### Public user and liquidation functions | Function | Lines | Authority / preconditions | Mutations / transfers | | --- | ---: | --- | --- | | `supply` | 59-123 | Owner is `tx-sender`; approved helper; active/unfrozen reserve; caps | Mints a-token, transfers asset to reserve, updates supplier index and collateral state | | `withdraw` | 149-197 | Owner is `tx-sender`; approved helper; valid assets/oracle; healthy after decrease | Burns a-token and sends underlying | | `borrow` | 199-283 | Owner is `tx-sender`; approved helper; active/unfrozen/borrow-enabled; liquidity, cap, health, isolation/e-mode checks | Sends underlying; updates borrow and isolation state | | `repay` | 423-472 | Payer is `tx-sender`; approved helper; valid reserve | Transfers repayment and reduces user/isolation debt | | `liquidation-call` | 474-523 | Approved helper; valid reserve/oracle/assets | Executes reserve liquidation and reduces isolation debt | | `flashloan-liquidation-step-1` | 554-577 | Approved helper; enabled/active/unfrozen reserve | Sends flashloan amount to receiver | | `flashloan-liquidation-step-2` | 579-621 | Approved helper; enabled/active/unfrozen reserve; local liquidity check | Pulls principal + fee from receiver and updates reserve | | `set-e-mode` | 635-668 | User is `tx-sender`; approved helper | Changes user e-mode after asset/debt/health checks | | `set-user-use-reserve-as-collateral` | 713-775 | User is `tx-sender`; approved helper; valid reserve/oracle/assets | Updates collateral-use flag | ### Public configuration functions All functions below authorize the current configurator by `tx-sender`. | Functions | Lines | Mutation | | --- | ---: | --- | | `set-configurator` | 625-628 | Replaces configurator in one step | | `init`, `set-reserve`, `set-borrowing-enabled` | 786-870 | Creates or replaces reserve state | | `set-usage-as-collateral-enabled` | 881-899 | Collateral enablement and risk parameters | | Isolation setters | 901-944 | Isolated assets and borrowable-isolated list | | Freeze/grace setters | 946-964 | Freeze and grace state | | e-mode setters | 967-988 | e-mode enablement/config and asset type | | `set-approved-contract` | 994-998 | Approved helper allowlist | ## 3. Post-condition coverage matrix | Public function | Token movement | Recommended caller post-conditions | | --- | --- | --- | | `supply` | Underlying from owner to reserve; a-token to owner | Cap underlying sent; require expected a-token receipt | | `withdraw` | a-token burned; underlying sent to owner | Cap a-token burn; require minimum underlying | | `borrow` | Underlying from reserve to owner | Require exact/minimum underlying receipt | | `repay` | Underlying from payer to reserve | Cap underlying sent; bind payer and beneficiary | | `liquidation-call` | Debt asset from liquidator; collateral asset to receiver | Cap debt sent; require minimum collateral and bind borrower/assets | | Flashloan steps | Asset out to receiver, then principal + fee into vault | Bind receiver/asset/amount across an atomic wrapper; require final pool balance and fee | | e-mode / collateral toggles | None directly | Bind exact user, asset list, oracle, and requested mode | | Configuration functions | None directly | Bind exact contract/function/arguments; use governance delay and review | ## 4. Authority / access-control matrix | Capability | Authorized principal | Check | | --- | --- | --- | | User supply, withdraw, borrow, repay, mode changes | Original `tx-sender` plus approved immediate helper | Owner/payer/user equality and `is-approved-contract(contract-caller)` | | Liquidation and flashloan steps | Any approved helper | Approved-contract map | | Reserve, risk, oracle, asset, e-mode, isolation, helper configuration | Current configurator | `is-configurator(tx-sender)` | | Replace configurator | Current configurator | Single-step `tx-sender` check | | Direct unapproved calls to most user flows | Rejected | Approved helper requirement | Approved helper contracts are therefore a broad and important trust boundary. The flashloan steps rely on that boundary rather than locally binding an outstanding loan to its repayment. ## 5. Clarity best-practice review | Check | Result | | --- | --- | | `tx-sender` vs `contract-caller` | User ownership and configurator authority intentionally use `tx-sender`; immediate helper must be approved. This exposes users/configurator authority to a malicious approved intermediary | | Panic unwraps | `supply`, isolation-list append/filter, and other paths use `unwrap-panic`; see ZEST-06 | | Arithmetic / invariants | Fixed-point helpers and cap checks exist. Isolation debt mixes principal-only increments with interest-inclusive decrements; see ZEST-01 | | `as-contract` | No local `as-contract` asset escalation found in the reviewed source | | Trait conformance | FT, a-token, oracle, redeemable, and flashloan traits are used; supplied tuples are validated against reserve state | | Borrow / repay / liquidation | Liquidity, cap, health, isolation, e-mode, and reserve checks exist; isolation accounting drift remains | | Flashloan atomicity | Split entrypoints have no local outstanding-loan state or same-call binding; see ZEST-02 | ## 6. Findings | ID | Severity | Function | Line | Finding | Recommended fix | | --- | --- | --- | ---: | --- | --- | | ZEST-01 | Medium | Isolation borrow / repay / liquidation | 305-360, 423-523 | **Isolation debt can be undercounted because increments are principal-only while decrements include interest.** Borrow stores `amount-to-be-borrowed`; repayment/liquidation pass interest-inclusive paid amounts to `reduce-isolated-mode-debt`. As interest accrues, a partial or full payment can reduce the isolation ledger by more than the principal it removes, freeing debt-ceiling capacity early. | Track scaled principal consistently and derive current debt from indexes, or update isolation debt using the exact principal component removed. Add an invariant that tracked isolation debt equals aggregate outstanding isolated principal/value. | | ZEST-02 | Medium | Flashloan liquidation steps | 554-621 | **The split flashloan flow has no local in-flight binding.** Step 1 transfers assets out; step 2 is independently callable by any approved helper and does not prove a matching step 1, receiver, amount, or same transaction. Correctness depends entirely on approved-helper implementation. | Expose one atomic entrypoint that transfers, calls a receiver callback, and enforces repayment before return. Otherwise store and consume a unique in-flight loan record bound to caller/receiver/asset/amount. | | ZEST-03 | Medium | `set-reserve` and risk setters | 830-899, 967-988 | **Configurator can replace live accounting and set risk relationships without local invariants.** `set-reserve` accepts cumulative indexes, total borrows, rates, caps, oracle, and treasury fields wholesale. Collateral/e-mode setters do not locally enforce `LTV <= liquidation-threshold`, bounded bonus, or enabled type consistency. A typo or compromised configurator can corrupt accounting or make positions unsafe. | Split accounting from policy setters; make live accounting fields immutable to configuration; validate all risk relationships and bounds in the authoritative data contracts. | | ZEST-04 | Low | `set-configurator` | 625-628 | **Configurator transfer is immediate and single-step.** A typo or malicious intermediary called by the configurator can permanently obtain broad reserve and helper authority. | Use propose/accept with a timelock and cancellation path; require `contract-caller` as appropriate. | | ZEST-05 | Low | `set-borroweable-isolated`, `remove-borroweable-isolated` | 924-944 | **Borrowable-isolated list management permits duplicates and wraps downstream removal errors.** Repeated additions consume the bounded list and can panic at capacity. Removal returns `(ok (contract-call? ...))`, producing a nested response that tooling may interpret as success even if the underlying update fails. | Reject duplicates, return a structured capacity error, and propagate the downstream response directly with `try!`. | | ZEST-06 | Low | `supply`, isolation list helpers | 91-100, 930, 944 | **Reachable `unwrap-panic` sites turn validation/capacity failures into opaque aborts.** State remains atomic, but integrations receive less actionable failures. | Replace with `unwrap!` and dedicated errors. | | ZEST-07 | Informational | `supply` supplier index | 84-85 | **Every supply call appends another user entry, including repeat suppliers.** The index represents calls rather than unique suppliers and grows without bound. | Track first-seen users or explicitly rename/document the map as a supply-event index. | | ZEST-08 | Informational | `borrow` | 221 | The inactive-reserve assertion returns `ERR_FROZEN` instead of `ERR_INACTIVE`, preventing clients from distinguishing policy states. | Return `ERR_INACTIVE` for the `is-active` assertion. | ### ZEST-01 impact detail At borrow time, the isolation ledger increases by the raw principal amount: `total-asset-isolated-debt = amount-to-be-borrowed + prior`. The source even notes that this "only adds principal, not interest." At repay time, the `payback-amount` passed into `reduce-isolated-mode-debt` can include accrued interest. Liquidation follows the same reduction helper. Example: if 100 units of principal accrue to 110 units and a user repays 55, the isolation ledger decreases by 55 even though only 50 principal units were removed. Repeating this behavior makes the ledger understate outstanding principal and weakens the debt ceiling. ### ZEST-02 impact detail Step 1 sends `amount` to an arbitrary receiver and returns. Step 2 later pulls `amount + fee` from a supplied receiver, but no local state proves that a corresponding step 1 occurred or that the arguments match. An approved helper can orphan step 1, invoke step 2 independently, or mix mismatched calls. This does not grant an arbitrary public caller access because the helper must be approved, so it is rated medium rather than high. ## Positive security properties - User flows validate owner/payer identity and approved immediate helpers. - Asset, a-token, and oracle principals are cross-checked against reserve state. - Supply and borrow caps, available liquidity, active/frozen state, collateral health, isolation mode, and e-mode restrictions are enforced. - Withdrawal checks the resulting position before reducing collateral. - Repayment supports full-debt sentinel behavior and updates isolation state. - Configuration is consistently restricted to the configurator. - No high or critical finding was identified, so private responsible disclosure was not required before publication. ## Suggested tests 1. Accrue interest on isolated debt, partially repay and liquidate it, and assert the isolation ledger never falls below aggregate remaining principal/value. 2. Invoke each flashloan step independently and with mismatched arguments; then verify a proposed atomic wrapper rejects every orphan/mismatch. 3. Fuzz reserve and e-mode configuration relationships and reject invalid accounting/risk tuples. 4. Add the same borrowable-isolated asset repeatedly and verify duplicate and capacity behavior. 5. Force all `unwrap-panic` paths and verify structured errors. 6. Supply repeatedly from one account and confirm the intended supplier-index semantics.