Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save tinyopsstudio/f748bc8a135a21c57e84e088f44c2e60 to your computer and use it in GitHub Desktop.

Select an option

Save tinyopsstudio/f748bc8a135a21c57e84e088f44c2e60 to your computer and use it in GitHub Desktop.
Static analysis: Zest pool-borrow v2-3

Static Analysis Report: Zest Pool Borrow v2-3

Prepared for AIBTC bounty mpwj1rjde88d5b53b990.

  • Contract: SP2VCQJGH7PHP2DJK7Z0V48AGBHQAW3R3ZW1QF4N.pool-borrow-v2-3
  • Source reviewed: https://api.hiro.so/v2/contracts/source/SP2VCQJGH7PHP2DJK7Z0V48AGBHQAW3R3ZW1QF4N/pool-borrow-v2-3
  • Review type: static analysis only
  • Prepared by: TinyOps Studio LLC
  • Date: 2026-06-02

No high or critical severity finding was classified in this static review. The flashloan finding is kept at medium because every affected entrypoint first requires an approved contract-caller.

Top 3 Findings

  1. Medium: the flashloan liquidation flow is split across two public steps, does not call the flashloan-script parameter, and relies on approved callers to enforce atomic repayment.
  2. Medium: collateral and e-mode risk-parameter setters do not locally cap LTV/liquidation thresholds to fixed-point maximums.
  3. Low: add-isolated-asset and set-borroweable-isolated can place assets into isolated-mode configuration without local duplicate/existence checks.

State Model

Local Stores

Line Store Meaning Mutated by
47 users-id Map from local incrementing user ID to principal supply
49 last-user-id Next user ID counter supply
623 configurator Principal allowed to configure protocol/reserve settings set-configurator
992 approved-contracts Allowlist for helper/router contracts permitted to call user-facing pool entrypoints set-approved-contract

External State Dependencies

Contract State / behavior used
.pool-0-reserve-v2-0 Reserve state, user reserve data, global health calculations, collateral toggles, supply/borrow accounting, reserve transfers, flashloan accounting
.pool-reserve-data, .pool-reserve-data-1, .pool-reserve-data-2, .pool-reserve-data-3 Reserve state writes, grace-period/freeze settings, e-mode config, isolated debt accounting
.liquidation-manager-v2-3 Liquidation execution
.math-v2-0 Fixed-point constants
Token traits Underlying asset, a-token, redeemable token, oracle, and flash-loan trait calls

Function Inventory

Read-Only Functions

Function Lines Authority Preconditions State mutations External calls
get-user, get-last-user-id 51-57 Open None None None
validate-use-as-collateral 125-147 Open helper Valid reserve inputs None Reserve isolation checks
mul-to-fixed-precision 411-415 Open helper Valid decimals/price inputs None Reserve math
get-asset-isolation-mode-debt 418-421 Open None None Pool-reserve-data-3
get-assets, validate-assets, check-assets 525-552 Open validation Caller-provided list must exactly correspond to registry list None Reserve data, reserve state
is-configurator 630-633 Open None None None
E-mode/read helpers 670-711, 777-784 Open/helpers Valid user/asset inputs None Reserve data
get-reserve-state, get-user-reserve-data, get-borroweable-isolated, filter-asset, is-approved-contract 872-879, 941-944, 1001-1004 Open Reserve/asset inputs must be valid where delegated None Reserve data / local map

Public User Functions

Function Lines Authority Preconditions / asserts State mutations External calls / transfers
supply 59-123 Approved contract-caller; owner == tx-sender Amount > 0; reserve active and not frozen; correct a-token; supply cap users-id, last-user-id; reserve/user state; a-token mint Transfers asset to reserve
withdraw 149-197 Approved contract-caller; owner == tx-sender Valid assets; correct a-token/oracle; enough balance/liquidity; balance decrease allowed Reserve/user state; a-token burn Transfers asset to user
borrow 199-283 Approved contract-caller; owner == tx-sender Valid assets; borrowing enabled; reserve active/not frozen; collateral and cap checks; e-mode/isolation checks Borrow state; isolated debt if applicable Transfers borrowed asset to user
repay 423-472 Approved contract-caller; payer == tx-sender Borrow balance > 0; reserve not frozen; amount > 0 Repay state; isolated debt if applicable Transfers repayment to reserve
liquidation-call 474-522 Approved contract-caller Valid reserve/oracle/a-token inputs; reserves not frozen; assets validated Liquidation manager state; isolated debt reduction Delegates liquidation to manager
flashloan-liquidation-step-1 554-577 Approved contract-caller Enough liquidity; nonzero fees; flashloan enabled; reserve active/not frozen None in this contract Transfers asset amount to receiver
flashloan-liquidation-step-2 579-621 Approved contract-caller Enough liquidity snapshot; nonzero fees; flashloan enabled; reserve active/not frozen Flashloan accounting Transfers amount plus fee from receiver to pool vault
set-e-mode 635-668 Approved contract-caller; user == tx-sender Valid assets; borrow/collateral compatibility; post-change health factor ok User e-mode Reserve data
set-user-use-reserve-as-collateral 713-775 Approved contract-caller; who == tx-sender Valid assets; active/not frozen; collateral enabled; correct a-token/oracle; nonzero balance; isolation/e-mode checks User reserve collateral flag Reserve data

Public Configuration Functions

| Function | Lines | Authority | State mutations | | --- | --- | --- | | set-configurator | 625-628 | Current configurator | Local configurator | | init, set-reserve, set-borrowing-enabled, set-usage-as-collateral-enabled | 786-899 | Configurator | Reserve state | | add-isolated-asset, remove-isolated-asset, set-borroweable-isolated, remove-borroweable-isolated | 901-939 | Configurator | Isolation-mode reserve state/list | | add-asset, remove-asset | 909-917 | Configurator | Global asset list | | Freeze/grace/e-mode config setters | 946-988 | Configurator | Reserve/e-mode config | | set-approved-contract | 994-998 | Configurator | Local approved caller map |

Post-Condition Coverage Matrix

Public function Token movements Suggested caller post-conditions
supply User transfers asset to reserve; receives a-token mint Bound asset spend and require expected a-token balance/mint
withdraw User burns a-token; reserve transfers underlying asset to user Bound a-token burn and require underlying received
borrow Reserve transfers borrowed asset to user Require borrowed asset received by owner; ensure no unexpected asset transfers from user
repay Payer transfers repayment asset to reserve Bound repayment spend to expected amount
liquidation-call Liquidator pays debt and receives collateral or a-token through manager Use post-conditions around both debt asset spent and collateral/a-token received
flashloan-liquidation-step-1 Reserve transfers flashloan principal to receiver Approved wrapper should combine this with repayment in the same transaction and bound receiver/principal
flashloan-liquidation-step-2 Receiver transfers principal plus fee to pool vault Bound amount plus fee and verify reserve accounting
User e-mode/collateral toggles No direct token movement None; health-factor checks are state-based
Config setters No direct token movement Governance monitoring; no token post-conditions

Authority and Access-Control Matrix

Area Authority holder Functions Notes
Configurator rotation Current configurator set-configurator Single-step rotation, no delay
Approved caller allowlist Configurator set-approved-contract All user-facing actions require approved contract-caller
User asset actions Approved contract plus user tx-sender Supply, withdraw, borrow, repay, e-mode/collateral toggles Direct users rely on an approved router/helper surface
Liquidations and flashloan steps Approved contract liquidation-call, flashloan-liquidation-step-1, flashloan-liquidation-step-2 Flashloan atomicity is delegated to approved caller discipline
Reserve and risk configuration Configurator init, set-reserve, collateral/borrow/e-mode/isolation/freeze/grace setters Broad authority; local validation is partial

Clarity Best-Practice Review

  • tx-sender is used to bind user actions to owner, payer, or who, while contract-caller is used as the approved helper/router gate. This is a deliberate pattern but makes the allowlist critical.
  • unwrap-panic appears in user-facing or configuration-adjacent paths at lines 91, 929, and 944. Prefer explicit try!/unwrap! errors for predictable failures.
  • No as-contract principal escalation was observed in this contract; most privileged writes are delegated by direct contract calls.
  • Oracle trait references are validated against reserve state before withdraw/borrow/liquidation flows. The caller-provided full asset list is also validated by order and length.
  • Configuration functions often delegate validation to reserve-data contracts. Where local constants/errors indicate intended bounds, local assertions would make failures easier to audit.

Findings Table

ID Severity Function Line Finding Recommended fix
ZEST-01 Medium flashloan-liquidation-step-1, flashloan-liquidation-step-2 554-621 The flashloan liquidation flow is split into two public functions. Step 1 transfers principal to receiver, step 2 separately pulls principal plus fee back, and the flashloan-script parameter is never called in either function. Because both steps are gated by is-approved-contract contract-caller, this is not directly callable by arbitrary users, but any approved wrapper must enforce atomic step1/liquidation/step2 sequencing perfectly. Replace the split public pattern with one public function that calls the receiver/script callback and verifies repayment before returning, or make step 1 private/internal to a trusted orchestrator. At minimum, remove the unused flashloan-script parameter or use it in the same transaction.
ZEST-02 Medium set-usage-as-collateral-enabled, set-e-mode-type-config 881-899, 974-981 Risk-parameter setters accept base-ltv-as-collateral, liquidation-threshold, liquidation-bonus, e-mode ltv, and e-mode liquidation-threshold without local bounds. If downstream reserve-data contracts do not enforce caps, configurator mistakes can set unsafe ratios or impossible health-factor behavior. Assert fixed-point ratios are within documented ranges, for example ltv <= liquidation-threshold <= one-8, and bound liquidation bonus to a protocol maximum before writing reserve/e-mode config.
ZEST-03 Low add-isolated-asset, set-borroweable-isolated 901-906, 924-931 Isolation configuration can add assets without local existence and duplicate checks. set-borroweable-isolated appends to the allowlist and uses unwrap-panic on max length; it does not check whether the asset is already present or whether it is an initialized reserve. Require asset reserve existence, reject duplicates before append, and return an explicit error when the list is full.
ZEST-04 Low supply, set-borroweable-isolated, filter-asset 91, 929, 944 unwrap-panic is used where explicit errors would be safer. In supply, a validation helper is unwrapped with panic; in isolation-list updates, list-bound failures panic instead of returning a protocol error. Replace with try! or unwrap! and a defined error such as ERR_PANIC or a more specific new error.
ZEST-05 Low borrow 221 The inactive-reserve check uses ERR_FROZEN instead of ERR_INACTIVE. This does not change acceptance logic, but it makes monitoring and integrator handling misleading. Return ERR_INACTIVE when is-active is false.
ZEST-06 Informational supply 84-85 Every supply call inserts the owner into users-id and increments last-user-id, even for repeat suppliers. The map therefore records duplicate owner principals over time and does not represent unique users. If the map is intended for unique-user indexing, add a reverse lookup or only insert first-time suppliers. Otherwise document that it is an append-only supply-event index.

Additional Notes

  • validate-assets is strict: it requires caller-provided assets to match the reserve asset list by length and order. This is good for preventing mismatched oracle/a-token inputs, but frontends must keep the asset list fresh.
  • repay intentionally allows a payer to repay on behalf of a different borrower while requiring payer == tx-sender.
  • Configuration authority is intentionally broad. The findings above focus on missing local validation and operator-footgun risk rather than assuming the configurator is malicious.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment