- 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.
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.
max-value: sentinel value fetched from.math-v2-0one-8: fixed precision one value fetched from.math-v2-0- Error constants:
ERR_UNAUTHORIZEDthroughERR_MUST_ENABLE_ASSET_OF_E_MODE e-mode-disabled-type:0x00
users-id: maps monotonically assigned IDs to principalslast-user-id: monotonically incremented on everysupplyconfigurator: privileged principal for reserve, collateral, e-mode, list, and approved-contract administrationapproved-contracts: principal allowlist used to gate helper-contract callers
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
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.
set-configuratorinitset-reserveset-borrowing-enabledset-usage-as-collateral-enabledadd-isolated-assetadd-assetremove-assetremove-isolated-assetset-borroweable-isolatedremove-borroweable-isolatedset-freeze-end-blockset-grace-period-timeset-grace-period-enabledset-e-mode-type-enabledset-e-mode-type-configset-asset-e-mode-typeset-approved-contract
- 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
| 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 |
| 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 |
- 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-indexcurrent-liquidity-ratetotal-borrows-stabletotal-borrows-variablecurrent-variable-borrow-ratecurrent-stable-borrow-ratecurrent-average-stable-borrow-ratelast-variable-borrow-cumulative-indexlast-updated-blockaccrued-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.
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.
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.
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-heightand monotonicity where expected. - Validate
a-token-address, oracle, decimals, caps, and collateral parameters.
- 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-thresholdltv <= 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.
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.
Add explicit validation before writing collateral or e-mode config:
- For reserve collateral:
- if
enabled = false, requirebase-ltv-as-collateral = 0,liquidation-threshold = 0, andliquidation-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.
- if
- For e-mode:
- require
ltv <= liquidation-threshold <= one-8. - consider rejecting configs for disabled e-mode types or forcing explicit enablement order.
- require
- 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-isolatedfilters all matching entries, so a duplicate-filled list can be cleaned up, but only if the configurator realizes the duplication and calls removal.
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.
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
mapmembership index if list membership is frequently updated.
- 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.
This is an operational availability risk rather than an unprivileged exploit. A compromised or mistaken configurator can disrupt market behavior for longer than intended.
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.
- 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.
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.
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.
tx-senderis used for user-level ownership checks, whilecontract-calleris used for approved helper gating. This separation is appropriate for wrapper-mediated flows.try!is used consistently around fallible external calls.unwrap-panicappears insupplyfor 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.
- Split or constrain
set-reserveso live accounting fields cannot be overwritten through a generic admin tuple. - Add local collateral/e-mode parameter invariants before writing risk config.
- Add duplicate and capacity guards for borrowable-isolated list updates.
- Add cap/timelock/monitoring safeguards around freeze and grace-period configuration.
- Document or expose canonical asset-list construction for approved wrappers and frontends.
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.