# Zest pool-borrow v2-3 static-analysis report

Contract: `SP2VCQJGH7PHP2DJK7Z0V48AGBHQAW3R3ZW1QF4N.pool-borrow-v2-3`

Source reviewed: `zest.clar`, 1005 lines

Source SHA-256: `F4B9A764C6F486A6732B7110BFCEFFB9353A96AC55EA8526C01FCA3B586A479C`

Scope note: this report reviews the supplied `pool-borrow-v2-3` contract. Many accounting operations are delegated to `.pool-0-reserve-v2-0`, `.pool-reserve-data`, `.pool-reserve-data-1`, `.pool-reserve-data-2`, `.pool-reserve-data-3`, `.liquidation-manager-v2-3`, token traits, and oracle traits. Where this contract delegates an invariant, I call that out.

Responsible disclosure: no high or critical issue was identified in this static review. Findings below are medium, low, or informational, so public submission is appropriate under the bounty rules.

## 1. State model

### Local constants

| Constant | Lines | Purpose |
|---|---:|---|
| Trait imports | 1-6 | FT, mintable FT, a-token, flash-loan, oracle, and redeemable token interfaces. |
| `max-value` | 8 | Delegated sentinel from `.math-v2-0`, used for max withdraw/repay. |
| `one-8` | 9 | Fixed precision unit from `.math-v2-0`. |
| Error constants | 11-42 | Domain errors for authorization, collateral, caps, e-mode, liquidation, and configuration failures. |
| `e-mode-disabled-type` | 45 | `0x00`, the disabled e-mode value. |

### Local stores

| Store | Line | Type | Mutators | Authority |
|---|---:|---|---|---|
| `users-id` | 47 | `map uint -> principal` | `supply` inserts current `last-user-id` | Approved call path with `tx-sender == owner` |
| `last-user-id` | 49 | `uint`, starts at `u0` | `supply` increments on every supply | Approved call path with `tx-sender == owner` |
| `configurator` | 623 | `principal`, starts at deployer | `set-configurator` | Current configurator |
| `approved-contracts` | 992 | `map principal -> bool` | `set-approved-contract` | Configurator |

### Delegated state

| External contract | Role |
|---|---|
| `.pool-0-reserve-v2-0` | Main reserve/accounting helper: reserve state, liquidity, user assets, collateral flags, user global data, caps, e-mode reads, transfers to/from reserve, flashloan fee config. |
| `.pool-reserve-data` | Direct reserve-state writes in `set-reserve`. |
| `.pool-reserve-data-1` | Freeze/grace-period settings. |
| `.pool-reserve-data-2` | E-mode user/type/asset settings. |
| `.pool-reserve-data-3` | Isolation-mode debt accounting. |
| `.liquidation-manager-v2-3` | Liquidation calculation/execution. |
| Token/oracle traits | Asset transfers, a-token mint/burn/cumulate, oracle prices. |

## 2. Function inventory

| Function | Lines | Authority | Preconditions / asserts | State mutations | External calls / transfers |
|---|---:|---|---|---|---|
| `get-user` | 51-53 | Open read | None | None | Reads `users-id` |
| `get-last-user-id` | 55-57 | Open read | None | None | Reads `last-user-id` |
| `supply` | 59-123 | `tx-sender == owner` and approved `contract-caller` | Amount > 0; reserve active/not frozen; supplied LP matches reserve a-token; supply cap; collateral eligibility | Inserts `users-id`, increments `last-user-id`; delegated reserve/a-token state | LP `cumulate-balance`, reserve deposit update, LP mint, transfer asset to reserve |
| `validate-use-as-collateral` | 125-147 | Open read | None local | None | Reads isolated-type status from reserve helper |
| `withdraw` | 149-197 | `tx-sender == owner` and approved `contract-caller` | Valid asset list; LP/oracle match; amount > 0; enough a-token balance/liquidity; balance decrease allowed | Delegated collateral/user index cleanup and reserve state | LP burn, reserve transfer to user |
| `borrow` | 199-283 | `tx-sender == owner` and approved `contract-caller` | Valid assets; LP/oracle match; borrowing enabled; active/not frozen; amount > 0; liquidity; isolation/e-mode checks; collateral/cap checks | Delegated borrow state | Reserve transfer to user |
| `reduce-isolated-mode-debt-liquidation` | 285-303 | Private, still checks approved caller | User must be in isolation mode to reduce isolated debt | Delegated isolated debt | Calls `reduce-isolated-mode-debt` |
| `reduce-isolated-mode-debt` | 305-329 | Private | Reserve reads must succeed | Delegated isolated debt | `.pool-reserve-data-3 set-asset-isolation-mode-debt` |
| `validate-borrow-in-isolated-mode` | 331-363 | Private | Borrowed asset borrowable isolated; debt ceiling if nonzero | Delegated isolated debt increase | Reserve/debt helpers and oracle-derived amount |
| `calculate-total-isolated-debt` / `acc-debt` | 365-397 | Private | Asset/oracle reads must succeed | None | Reserve and oracle reads |
| `calculate-price` / `mul-to-fixed-precision` | 399-415 | Private/open read | Oracle price must read | None | Oracle and reserve math helper |
| `get-asset-isolation-mode-debt` | 418-421 | Open read | None | None | `.pool-reserve-data-3` read |
| `repay` | 423-472 | `tx-sender == payer` and approved `contract-caller` | Existing debt; reserve not frozen; amount > 0 | Delegated repay state and isolated debt reduction | Transfer payer asset to reserve |
| `liquidation-call` | 474-522 | Approved `contract-caller` | Valid reserves; both not frozen; collateral LP/oracles match; valid asset list | Delegated liquidation and isolated debt reduction | `.liquidation-manager-v2-3 liquidation-call` |
| `get-assets` / `validate-assets` / `check-assets` | 525-552 | Open read | Supplied list length/order must match registered assets; LP/oracles match each reserve | None | Reserve reads |
| `flashloan-liquidation-step-1` | 554-577 | Approved `contract-caller` | Liquidity available; nonzero fee and protocol fee; flashloan enabled; active/not frozen | None local | Reserve transfer to arbitrary receiver |
| `flashloan-liquidation-step-2` | 579-621 | Approved `contract-caller` | Liquidity available; nonzero fee and protocol fee; flashloan enabled; active/not frozen | Delegated flashloan accounting | Transfers amount+fee from receiver to `.pool-vault`; update flashloan state |
| `set-configurator` | 625-628 | Current configurator | Caller is configurator | Sets `configurator` | None |
| `is-configurator` | 630-633 | Open read | None | None | Reads `configurator` |
| `set-e-mode` | 635-668 | `tx-sender == user` and approved `contract-caller` | Valid assets; e-mode borrow/collateral checks; post-change health factor | Delegated user e-mode | `.pool-reserve-data-2 set-user-e-mode` |
| `can-enable-e-mode` | 670-671 | Open read | None local | None | Reserve helper |
| `set-user-use-reserve-as-collateral` | 713-775 | `tx-sender == who` and approved `contract-caller` | Valid assets; active/not frozen; collateral enabled; LP/oracle match; e-mode/isolation checks; balance decrease check when disabling | Delegated user reserve data | Reserve data writes |
| `get-asset-e-mode-type`, `get-user-e-mode`, `is-in-e-mode` | 777-784 | Open read | None local | None | Reserve helper |
| `init` | 786-828 | Configurator | Caller configurator | Delegated reserve initialization | `.pool-0-reserve-v2-0 set-reserve` |
| `set-reserve` | 830-865 | Configurator | Caller configurator | Delegated full reserve-state replacement | `.pool-reserve-data set-reserve-state` |
| `set-borrowing-enabled` | 867-870 | Configurator | Caller configurator | Delegated reserve mutation | `.pool-0-reserve-v2-0 set-reserve` |
| `get-reserve-state`, `get-user-reserve-data`, `get-borroweable-isolated` | 872-879 | Open read | None local | None | Reserve helper |
| `set-usage-as-collateral-enabled` | 881-899 | Configurator | Caller configurator | Delegated reserve risk params | `.pool-0-reserve-v2-0 set-reserve` |
| `add-isolated-asset` | 901-907 | Configurator | Caller configurator | Delegated isolated flag and debt ceiling | Reserve helper |
| `add-asset`, `remove-asset`, `remove-isolated-asset` | 909-922 | Configurator | Caller configurator | Delegated asset lists | Reserve helper |
| `set-borroweable-isolated`, `remove-borroweable-isolated`, `filter-asset` | 924-944 | Configurator/open read helper | Caller configurator for public mutators | Delegated borrowable isolated list | Reserve helper; `unwrap-panic` on list max |
| Freeze/grace/e-mode config setters | 946-989 | Configurator | Caller configurator | Delegated risk/feature config | Data helper contracts |
| `set-approved-contract` / `is-approved-contract` | 994-1004 | Configurator/open read | Caller configurator for setter | Updates approved caller map | None |

## 3. Post-condition coverage matrix

| Public function | Token movements | Caller post-conditions |
|---|---|---|
| `supply` | User asset moves from owner to reserve; a-token/LP is minted to owner | Limit asset debit to `amount`; require a-token/LP mint at least `amount`; no other token debit. |
| `withdraw` | a-token/LP is burned; reserve asset moves to owner | Limit a-token burn to redeemed amount; require asset credit to owner at least expected amount. |
| `borrow` | Reserve asset moves to owner; variable debt state changes in reserve helper | Require borrowed asset credit to owner equal to requested amount; no unexpected debits. |
| `repay` | Payer asset moves to reserve; debt decreases for `on-behalf-of` | Limit payer asset debit to calculated payback; if using max sentinel, compute expected cap off-chain. |
| `liquidation-call` | Delegated to liquidation manager; liquidator pays debt asset and receives collateral or a-token | Liquidator should cap debt-asset debit and require minimum collateral/a-token credit based on quote. |
| `flashloan-liquidation-step-1` | Reserve transfers flashloan amount to receiver | Approved helper should enforce amount/receiver exactly and pair with repayment in the same top-level transaction design. |
| `flashloan-liquidation-step-2` | Receiver transfers amount+fee to pool vault; reserve updates fees | Receiver/helper should cap repayment debit and verify expected reserve/vault recipient. |
| User config functions | No direct token movement | No token post-conditions; verify user, asset list, and target state. |
| Configurator functions | No direct token movement except delegated state effects | No token post-conditions; governance should verify exact risk values and contract principals. |

## 4. Authority / access-control matrix

| Role / gate | Mechanism | Scope |
|---|---|---|
| User-owner operations | `tx-sender == owner/who/payer/user` | Supply, withdraw, borrow, repay, e-mode, collateral toggles require the transaction sender to be the affected user or payer. |
| Approved helper contracts | `is-approved-contract contract-caller` | Most user-facing operations also require an approved caller contract, preventing direct calls unless the caller principal is approved. |
| Configurator | `configurator` data var | Can set itself, configure reserves, risk params, e-mode, isolated assets, approved contracts, freeze/grace values. |
| Liquidation manager | Hardcoded `.liquidation-manager-v2-3` call | Liquidation execution is delegated after local reserve/oracle/list validation. |
| Reserve/data helpers | Hardcoded pool data contracts | Accounting, balances, caps, e-mode, isolation debt, and transfers are delegated heavily. |

## 5. Clarity best-practice review

| Topic | Result |
|---|---|
| `tx-sender` vs `contract-caller` | User identity is guarded with `tx-sender`, while routing is guarded with approved `contract-caller`. This is consistent with helper-contract UX, but any approved helper becomes part of the trusted surface. |
| `unwrap-panic` / `unwrap-err-panic` | `unwrap-panic` appears in `supply` at line 91 and in isolated borrowable list helpers at lines 930 and 944. Prefer typed errors in user-facing or configuration paths. |
| Arithmetic overflow / interest math | Core interest math is mostly delegated, but this contract computes cap totals, debt ceiling totals, flashloan fees, and collateral totals with unchecked arithmetic that reverts on overflow. Revert risk is acceptable but should be considered in cap/fee bounds. |
| `as-contract` usage / principal escalation | No direct `as-contract` usage found in this contract. Principal-sensitive transfers are delegated to reserve/token contracts. |
| Trait conformance gaps | User-supplied token/oracle traits are validated against reserve state with `contract-of`, which is good. LP/a-token correctness still depends on reserve configuration. |
| Borrow / repay / liquidation invariants | Borrow, repay, and liquidation rely on reserve helpers for full accounting. Local validation covers major gates but does not independently verify post-liquidation health or vault repayment atomicity. |

## 6. Findings table

| ID | Severity | Function | Line | Finding | Recommended fix |
|---|---|---|---:|---|---|
| ZEST-01 | Medium | `flashloan-liquidation-step-1`, `flashloan-liquidation-step-2` | 554-621 | Flashloan execution is split into two public steps with no local state binding step 1 to step 2 and no same-transaction repayment enforcement in this contract. An approved helper must enforce atomicity externally. | Move flashloan flow into one function/callback that transfers out, invokes receiver, verifies repayment, and updates state atomically, or store a nonce/liability and restrict step 2 to the matching receiver/asset/amount. |
| ZEST-02 | Medium | Configurator risk setters | 830-899, 946-989 | Configurator can replace full reserve state and set LTV, liquidation threshold, liquidation bonus, e-mode LTV/threshold, freeze/grace values, and reserve caps without local bounds. | Add local invariant checks for risk params, especially `ltv <= liquidation-threshold`, sane liquidation bonus, caps, e-mode bounds, and nonzero valid oracle/a-token addresses. |
| ZEST-03 | Medium | `set-approved-contract` plus user functions | 994-1004, 59-775 | Approved helper contracts are a broad trust boundary. Any approved contract can route user operations and flashloan steps when `tx-sender` constraints are satisfied; flashloan steps do not require `tx-sender` to be receiver. | Keep approved contracts minimal, audited, and preferably immutable/timelocked; add narrower approvals per operation or per helper role. |
| ZEST-04 | Low | `supply`, `set-borroweable-isolated`, `filter-asset` | 91, 930, 944 | `unwrap-panic` is used in a public supply path and config/list helpers, producing panics rather than typed domain errors. | Replace with `try!` or `unwrap!` and explicit errors such as `ERR_PANIC` or specific list/collateral errors. |
| ZEST-05 | Low | `set-borroweable-isolated` | 924-931 | The borrowable isolated list appends without checking duplicates, so repeated additions can waste list capacity and eventually trigger the `unwrap-panic` max-list path. | Assert the asset is not already present before appending. |
| ZEST-06 | Informational | `supply` | 84-85 | `users-id`/`last-user-id` record every supply action, not unique users. Repeat suppliers receive multiple IDs; this is indexing semantics, not accounting security. | Rename to indicate event/deposit index or track a separate user-to-id map if unique user indexing is desired. |

## Finding details

### ZEST-01: Flashloan flow is split and relies on external helper atomicity

Severity: Medium

Lines: 554-621

`flashloan-liquidation-step-1` checks liquidity, fees, flashloan enablement, active/frozen state, then transfers `amount` from the reserve to an arbitrary `receiver` and returns. `flashloan-liquidation-step-2` separately transfers `amount + amount-fee` back from `receiver` to `.pool-vault` and updates reserve flashloan state.

There is no local pending-loan record tying step 1 to step 2, no nonce, no matching receiver/asset/amount state, and no direct callback into the supplied `flashloan-script` in the reviewed code. The design may be safe if only a tightly audited helper contract can call both steps in one transaction, but this contract itself does not enforce the atomic flashloan invariant.

Recommended fix: consolidate the flow into an atomic flashloan function, or store a pending liability and require repayment with exact matching parameters before the transaction can complete.

### ZEST-02: Risk parameter setters lack local bounds

Severity: Medium

Lines: 830-899 and 946-989

The configurator can set the full reserve state, collateral parameters, e-mode parameters, freeze/grace settings, asset e-mode mapping, and reserve caps. This is expected governance power, but the contract does not locally enforce standard lending invariants such as:

- LTV <= liquidation threshold.
- Liquidation threshold <= fixed precision unit.
- Sane liquidation bonus upper bound.
- Nonzero and valid oracle/a-token principals.
- Debt ceiling/cap values within intended ranges.

If the configurator is a robust governance contract this may be handled upstream. Still, local checks reduce accidental bricking or unsafe risk configurations.

Recommended fix: add local validation helpers for reserve and e-mode risk params before delegating state writes.

### ZEST-03: Approved helper contracts are a broad trust boundary

Severity: Medium

Lines: 994-1004, and user paths 59-775

Most user functions require both a `tx-sender` identity check and an approved `contract-caller`. This is useful for routing through a frontend/helper contract, but it means any approved helper contract is security-critical. The flashloan steps are especially sensitive because they only check approved `contract-caller`, not that `tx-sender` equals the receiver or a specific liquidator.

Recommended fix: split approval by role or function, audit approved helpers as part of the core protocol, and consider making approved helper changes timelocked.

### ZEST-04: `unwrap-panic` in supply/config paths

Severity: Low

Lines: 91, 930, 944

The supply path unwraps `validate-use-as-collateral` with `unwrap-panic`. Config/list helpers also panic when list length conversion fails. Transaction atomicity prevents partial state changes, but panics reduce integrator diagnosability and can make boundary conditions harder to triage.

Recommended fix: use `unwrap!` with typed errors or propagate errors with `try!`.

### ZEST-05: Duplicate borrowable isolated assets can consume list capacity

Severity: Low

Lines: 924-931

`set-borroweable-isolated` appends `asset` to the borrowable isolated list without checking whether it is already present. Repeated configurator calls can add duplicates and eventually hit the max length, causing the `unwrap-panic` path.

Recommended fix: assert `asset` is not already in the list before appending.

### ZEST-06: `users-id` tracks supply events rather than unique users

Severity: Informational

Lines: 84-85

Every successful supply inserts the current `last-user-id` and increments it. A repeat supplier gets another ID. This does not affect lending balances, which are delegated elsewhere, but index consumers may misread it as a unique user registry.

Recommended fix: rename or document the semantics, or add a separate reverse map if unique user IDs are needed.

## Non-findings and positive observations

- Token/oracle/LP inputs are consistently checked against reserve configuration with `contract-of`.
- User-affecting functions use `tx-sender` checks so helpers cannot act for arbitrary users without a transaction from that user.
- Withdraw validates health impact through `check-balance-decrease-allowed`.
- Repay allows a payer to repay on behalf of another account but requires `tx-sender == payer`.
- Liquidation delegates complex calculations to a dedicated liquidation manager after local reserve/oracle validation.

