# Granite Finance v0-4 market static-analysis report

Contract: `SP1A27KFY4XERQCCRCARCYD1CC5N7M6688BSYADJ7.v0-4-market`

Source reviewed: `granite.clar`, 1662 lines

Source SHA-256: `3B173DEF1BAAD24A25CB1E908BA4A6C6A7A1118C5BF280D7ABB2EE8CC3556740`

Scope note: this report reviews the supplied `v0-4-market` contract. Core reserve, position, collateral, scaled-debt, vault, asset-status, and egroup state is delegated to `.v0-market-vault`, `.v0-market-registry`, vault contracts, Pyth, DIA, and the stSTX helper. Where this contract relies on those external invariants, 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 |
|---|---:|---|
| Asset ids | 17-29 | Paired underlying and zToken ids: STX/zSTX, sBTC/zsBTC, stSTX/zstSTX, USDC/zUSDC, USDH/zUSDH, stSTXbtc/zstSTXbtc. `ztokens` marks vault-share collateral ids. |
| Precision units | 32-33, 49 | `BPS`, `INDEX-PRECISION`, and `STSTX-RATIO-DECIMALS`. |
| Oracle tags | 36-46 | Oracle type and callcode identifiers for Pyth, DIA, stSTX ratio, and vault-token price conversions. |
| Bitmap/debt packing | 52-55 | `MAX-U64`, `DEBT-MASK`, `DEBT-OFFSET`, and `ITER-UINT-64` support mask expansion and debt/collateral flag extraction. |
| Liquidation sentinels | 58-59 | Max liquidation amount and global liquidation grace-period id. |
| Wrapper principal | 62 | `.wstx` wrapper used by STX vault deposit flow. |
| Error constants | 67-91 | Domain errors for auth, amounts, collateral/debt enablement, vault routing, oracle failures, liquidation, slippage, egroup, and same-block borrow protection. |

### Local stores

| Store | Line | Type | Mutators | Authority |
|---|---:|---|---|---|
| `pause-liquidation` | 98 | `bool`, starts `false` | `set-pause-liquidation` | DAO executor only via `check-dao-auth` |
| `max-confidence-ratio` | 103 | `uint`, starts `u1000` | `set-max-confidence-ratio` | DAO executor only |
| `liquidation-grace-periods` | 110 | `map uint -> uint` | `set-pause-liquidation`, `set-liquidation-grace-period` | DAO executor only |
| `index-cache` | 113-115 | `map {timestamp, aid} -> {index, lindex}` | `accrue-and-cache` during user/liquidation flows | Open user paths, populated from delegated vault accrual |
| `last-update` | 118-120 | `map {type, ident} -> uint` | `price-resolve` | Open paths that resolve prices after feed validation |

### Delegated state and dependencies

| External contract | Role |
|---|---|
| `.v0-market-vault` | Position registry, collateral balances, scaled debt balances, add/remove debt and collateral. |
| `.v0-market-registry` | Asset status, asset addresses, oracles, egroup configs, enabled bitmap, and safe user masks. |
| `.v0-vault-*` | Underlying vaults for deposit, redeem, accrue, system borrow, system repay, socialized debt, and index reads. |
| Pyth storage v4 | Pyth price feed source. |
| DIA oracle | DIA price source. |
| `.ststx-token` | stSTX ratio source used by `call-ststx-ratio`. |
| FT traits and `.wstx` | Token movements, STX wrapping, zToken mint/redeem flows. |

## 2. Function inventory

| Function | Lines | Authority | Preconditions / asserts | State mutations | External calls / transfers |
|---|---:|---|---|---|---|
| `get-pause-liquidation` | 926 | Open read | None | None | Reads `pause-liquidation` |
| `get-liquidation-grace-end` | 928-929 | Open read | None | None | Reads global grace map entry |
| `get-liquidation-grace-period-asset` | 931-933 | Open read | None | None | Reads asset grace map entry |
| `get-max-confidence-ratio` | 936-937 | Open read | None | None | Reads `max-confidence-ratio` |
| `oracle-last-update` | 939-941 | Open read | None | None | Reads `last-update` |
| `get-cached-indexes` | 944-949 | Open read | Cache must exist for current block and asset | None | Reads `index-cache` |
| `set-pause-liquidation` | 953-976 | DAO executor | `tx-sender == .dao-executor` | Sets pause flag; on unpause after pause sets global grace end | None |
| `set-liquidation-grace-period` | 978-993 | DAO executor | `tx-sender == .dao-executor` | Sets asset/global grace end for supplied id | None |
| `set-max-confidence-ratio` | 995-1010 | DAO executor | `ratio <= BPS` | Sets oracle confidence threshold | None |
| `call-ststx-ratio` | 1015-1017 | Open | stSTX helper must return ratio | None | Calls `.ststx-token get-ststx-per-stx` |
| `collateral-add` | 1020-1104 | Direct user only | Asset collateral-enabled; `contract-caller == tx-sender`; valid current/future egroup; if existing debt, future borrow capacity must not decrease | Delegated collateral add | Optional Pyth feed writes, registry reads, vault accruals, `.v0-market-vault collateral-add` |
| `collateral-remove` | 1107-1169 | Open for caller's own account | Amount > 0; if debt exists, position must be currently healthy and post-removal healthy | Delegated collateral remove | Optional Pyth feed writes, registry/oracle/vault reads, `.v0-market-vault collateral-remove` and token transfer through vault |
| `supply-collateral-add` | 1175-1207 | Direct user only | Amount > 0; `contract-caller == tx-sender`; asset/vault route must be known | Delegated vault deposit then collateral add | FT transfer to market, `as-contract?` vault deposit, internal `collateral-add` |
| `collateral-remove-redeem` | 1211-1234 | Open for caller's own account | Input FT must be a zToken; collateral removal must pass checks; redeem min-underlying must pass in vault | Delegated collateral remove and vault redeem | Internal `collateral-remove`, vault redeem to receiver |
| `borrow` | 1238-1314 | Open for caller's own account | Amount > 0; debt enabled; current position healthy; future egroup borrowing allowed; post-borrow healthy | Delegated borrow-index/debt add and vault borrow | Optional Pyth feed writes, accrual, registry/oracle reads, vault system borrow, `.v0-market-vault debt-add-scaled` |
| `repay` | 1316-1378 | Direct payer only | `contract-caller == tx-sender`; amount > 0; repay removes positive scaled debt | Delegated debt removal | Vault system repay transfers payer funds; `.v0-market-vault debt-remove-scaled` |
| `liquidate` | 1382-1585 | Direct liquidator only | Price feeds write; not same block as borrow; position at/above partial liquidation LTV; liquidation not paused; positive repay/collateral; slippage met | Delegated debt removal, collateral removal, possible socialized debt | Vault system repay, vault collateral transfer, bad-debt socialization across debt assets |
| `liquidate-multi` | 1593-1599 | Open direct callers through mapped `liquidate` | Each mapped call enforces liquidation checks; batch has no price-feed input | None local beyond mapped calls | Maps `call-liquidate`, returns per-position ok/err results |
| `liquidate-redeem` | 1604-1661 | Direct liquidator through internal `liquidate` | Collateral must be zToken; liquidation and redeem slippage checks pass | Delegated liquidation then vault redeem | Internal `liquidate` sends zTokens to market; vault redeem sends underlying to receiver |

## 3. Post-condition coverage matrix

| User call | Token movements | Suggested caller post-conditions |
|---|---|---|
| `supply-collateral-add` | Underlying FT transfers from user to market, then market deposits into matching vault and zTokens are minted/credited as collateral for the user. STX route uses `.wstx` plus `as-contract? ((with-stx amount))`; other tokens use `as-contract? ((with-ft ft-address "*" amount))`. | Assert no more than `amount` of underlying leaves caller; assert at least `min-shares` zTokens are minted/credited to caller or their collateral account; for non-STX tokens assert receiver is expected vault/market principal. |
| `collateral-remove-redeem` | zToken collateral is removed from caller account to `current-contract`, then redeemed for underlying to supplied receiver or caller. | Assert zToken decrease is no more than `amount`; assert underlying received by intended receiver is at least `min-underlying`; assert no unexpected transfer of unrelated FT assets. |
| `borrow` | Matching vault sends borrowed FT to receiver or caller; market adds scaled debt to caller account. | Assert borrowed asset transfer to intended receiver is exactly or at least the requested safe amount expected by UI; assert no transfer of collateral assets out of caller; assert receiver principal matches UI selection. |
| `repay` | Payer transfers up to requested amount, capped to outstanding debt, into the debt vault; scaled debt decreases for `on-behalf-of` account or payer. | Assert no more than requested amount leaves payer; assert transfer recipient is the expected vault/market path; when repaying for another account, assert no unrelated asset movement from payer. |
| `liquidate` | Liquidator repays debt asset; borrower debt is reduced; collateral asset is transferred to liquidator or supplied receiver; possible bad debt is socialized if collateral is exhausted. | Assert no more than intended debt asset leaves liquidator; assert at least `min-collateral-expected` collateral reaches intended receiver; assert collateral asset id matches UI; assert price-feed update post-conditions if caller supplies Pyth updates. |
| `liquidate-redeem` | Liquidator repays debt; zToken collateral is seized to market; market redeems zToken for underlying to receiver. | Assert no more than intended debt leaves liquidator; assert at least `min-underlying` underlying reaches receiver; assert no zTokens remain unexpectedly in market if full redeem succeeds. |

## 4. Authority / access-control matrix

| Surface | Authority / trust boundary | Notes |
|---|---|---|
| DAO operations | `check-dao-auth` requires `tx-sender == .dao-executor` at line 185. | Controls global liquidation pause, per-asset grace periods, and Pyth confidence threshold. |
| Registry operations | Delegated to `.v0-market-registry`. | Asset enablement, asset status, egroup params, oracles, decimals, and enabled bitmaps are trusted inputs to this contract. |
| Vault operations | Delegated to `.v0-vault-*` and `.v0-market-vault`. | Interest indexes, system borrow/repay, debt socialization, collateral accounting, and vault asset custody are external invariants. |
| Oracle feeds | Pyth and DIA external contracts plus local `last-update` freshness monotonicity. | User paths can submit up to 3 Pyth price-update buffers before price resolution. DIA values are read directly. |
| Direct user checks | `collateral-add`, `supply-collateral-add`, `repay`, and `liquidate` require `contract-caller == tx-sender`. | Prevents helper contracts from acting as users in those flows, but reduces router/account-abstraction composability. |
| Caller-owned account model | `borrow`, `collateral-remove`, `collateral-remove-redeem`, and `liquidate-redeem` use `contract-caller` as the account. | A wrapper contract can only affect its own market account, not the end user's account, unless state is intentionally held by the wrapper. |
| Liquidation controls | Manual pause plus global/asset grace periods checked by `is-liquidation-paused`. | Pause applies per debt asset in `liquidate`; unpause can add global grace. |

## 5. Clarity best-practice review

| Topic | Result |
|---|---|
| `tx-sender` vs `contract-caller` | No direct arbitrary-user authorization bug found. Direct-user assertions are intentional in several paths. The model is inconsistent for integrators because `borrow` is open for `contract-caller` accounts while other user paths require `contract-caller == tx-sender`. |
| `unwrap-panic` | Present in user-reachable paths for accrual, oracle/list handling, cached indexes, debt/collateral processing, and bad-debt list rebuilding. See GRA-03. |
| Arithmetic overflow / underflow | Most arithmetic is bounded by Clarity checked arithmetic, but risk parameters from egroup/registry can trigger aborts if invalid. Liquidation denominator ordering is not locally asserted. See GRA-01. |
| `as-contract?` usage | Used in `supply-collateral-add` to deposit market-held assets into vaults. This is expected for custody handoff, but callers should use post-conditions because funds route through the market contract. |
| Trait conformance | Token traits are checked mainly via `contract-of` mapped to registry asset ids. This prevents callers from choosing arbitrary FT contracts for known assets. |
| Oracle staleness | Staleness and monotonic update checks exist, but future timestamps are treated as zero-age and can move `last-update` ahead. See GRA-02. |
| Liquidation invariants | Same-block borrow liquidation is blocked; healthy positions cannot be liquidated; slippage is caller-controlled. Batch liquidation intentionally returns per-position ok/err and omits price-feed updates. |

## 6. Findings table

| ID | Severity | Function | Line | Finding | Recommended fix |
|---|---|---|---:|---|---|
| GRA-01 | Medium | `calc-liq-factor`, `liquidate` | 703-704, 1416-1441 | Liquidation math assumes `LTV-LIQ-FULL > LTV-LIQ-PARTIAL`, but this contract does not assert that egroup invariant before subtracting the denominator. A bad registry/egroup config can cause liquidation to abort exactly when the market needs liquidation. | Enforce `ltv-liq-full > ltv-liq-partial` in the registry setter and also assert it in `liquidate` before `calc-liquidation-params`; add bounds for penalty min/max and curve exponent. |
| GRA-02 | Medium | `oracle-timestamp-fresh`, `price-resolve` | 365-392 | Future-dated oracle timestamps are accepted as fresh by setting `delta` to zero, then `last-update` can be advanced to that future time. Later otherwise-valid feed values with timestamps below that future value are rejected by the monotonic check. | Reject future timestamps or allow only a small configured skew, e.g. assert `ts <= stacks-block-time + max-skew`; only write `last-update` after bounded-skew validation. |
| GRA-03 | Low | Multiple user paths | 267, 290, 327, 414, 491, 565, 770, 814, 864, 1482, 1522, 1540 | `unwrap-panic` is reachable from user and liquidation flows. Many failures are unlikely if registry/vault invariants hold, but oracle, cache, list, or config failures produce panics rather than typed domain errors. This reduces diagnosability and integration safety. | Replace with `try!`/`unwrap!` and explicit errors, especially around oracle resolution, cached index reads, asset lookup, and list append paths. |
| GRA-04 | Low | `set-max-confidence-ratio` | 995-1010 | DAO can set `max-confidence-ratio` to zero. With zero, any Pyth price with nonzero confidence fails `check-confidence`, effectively bricking Pyth-priced asset operations until reset. | Enforce a nonzero lower bound and consider a sane minimum/maximum range per asset risk policy. |
| GRA-05 | Low | `calc-liq-factor-exp` | 708-713 | Liquidation curve exponents are interpreted coarsely: `exp == BPS` is linear, `exp > BPS` truncates to an integer exponent via `/ exp BPS`, and any `exp < BPS` becomes square-root behavior. If governance expects decimal bps exponents, the effective curve can differ materially from configuration intent. | Document allowed exponent values or implement fixed-point exponent handling; validate egroup curve exponents to the supported set. |
| GRA-06 | Informational | `liquidate-multi` | 1590-1599 | Batch liquidation does not accept price-feed updates and returns a list of per-position responses instead of reverting the whole batch. This is documented in comments, but callers need to pre-update feeds and parse per-item results. | Keep the comment in user-facing docs; consider a batch variant with shared feed updates or an all-or-nothing mode for liquidator tooling. |

## Finding details

### GRA-01: Liquidation LTV ordering is trusted to registry configuration

Severity: Medium

`calc-liq-factor` subtracts `ltv-liq-partial` from both `ltv-curr` and `ltv-liq-full` at lines 703-704. The public `liquidate` path asserts `current-ltv >= ltv-liq-partial` at line 1435, so the numerator is guarded. It does not assert `ltv-liq-full > ltv-liq-partial`, so an invalid egroup can make the denominator zero or underflow.

This is not an attacker-controlled input during a normal liquidation; the values come from the registry/egroup trust boundary. The impact is still material because liquidation availability depends on these parameters. A single bad egroup update can halt liquidation for affected masks.

Recommended fix: enforce ordering and sane ranges at the egroup registry setter. Also add a local guard in `liquidate` before `calc-liquidation-params` so this contract fails with a typed configuration error instead of arithmetic aborting.

### GRA-02: Future-dated oracle timestamps can advance `last-update`

Severity: Medium

`oracle-timestamp-fresh` treats any timestamp greater than `stacks-block-time` as having `delta = u0` at lines 365-368. If that timestamp is also greater than the previous value, `price-resolve` writes it to `last-update` at lines 390-392. After that, feeds with normal timestamps below the future value fail `(>= ts prev)`.

The oracle contracts are trusted dependencies, so this is not a direct permissionless price attack. It is a robustness risk at the oracle boundary: a future-dated value, whether from oracle behavior or bad feed data, can deny later valid updates until time catches up.

Recommended fix: reject future timestamps or permit only a small explicit skew. The monotonic write should happen only after the bounded-skew check succeeds.

### GRA-03: `unwrap-panic` remains in user and liquidation paths

Severity: Low

Several paths reachable from public functions panic on internal errors: accrual cache population at lines 267 and 290, DIA key decoding at line 327, price-list append at line 414, `get-assets` price resolution at line 491, debt notional accrual at line 565, liquidation asset lookup at line 770, disabled collateral price resolution at line 814, cached liquidation indexes at lines 864, 1482, and 1522, and bad-debt list reconstruction at line 1540.

Most of these should be unreachable under valid registry/vault/list invariants. The remaining issue is failure mode quality: user transactions and liquidator tooling get a panic instead of a typed error, making it harder to distinguish stale oracle, missing cache, bad asset, and list-capacity failures.

Recommended fix: replace panics with typed `unwrap!`/`try!` results and domain errors. Prioritize price resolution and liquidation paths because liquidators need reliable error classification.

### GRA-04: `max-confidence-ratio` allows a zero configuration

Severity: Low

`set-max-confidence-ratio` only checks `ratio <= BPS` at line 998. If DAO sets the ratio to zero, `check-confidence` at line 306 requires `confidence <= 0` for any positive price. Most real Pyth feeds have nonzero confidence intervals, so Pyth-priced operations can fail until the setting is restored.

Recommended fix: require `ratio > 0` and ideally set a policy lower bound that still allows expected feed confidence under normal volatility.

### GRA-05: Liquidation curve exponent semantics are coarse

Severity: Low

`calc-liq-factor-exp` supports three effective modes: linear when `exp == BPS`, integer powers when `exp > BPS`, and square root for any `exp < BPS`. Since `exp > BPS` uses integer division by `BPS`, values like 1.5x BPS are treated as 1x for the exponent term. Values below BPS all collapse to square root.

This is mainly a configuration semantics issue. If risk governance expects basis-point precision for the curve exponent, the executed curve can differ from the intended liquidation shape.

Recommended fix: document and validate the supported exponent set, or use a fixed-point approximation with explicit tests over expected parameter values.

### GRA-06: Batch liquidation requires external feed freshness management

Severity: Informational

The comment at lines 1590-1592 says price feeds are not supported in batch and failed liquidations return per-position errors. This is not a bug by itself, but it matters operationally: liquidators using `liquidate-multi` need to update Pyth feeds separately and parse the returned response list instead of assuming all-or-nothing execution.

Recommended fix: keep this behavior explicit in public liquidator docs and SDKs. Consider a batch function variant that accepts shared feed updates or reverts on the first failure for callers that prefer atomic batches.

## Non-findings and positive observations

- Asset inputs are tied back to registry entries using `contract-of`, reducing arbitrary trait substitution risk.
- Liquidation includes same-block borrow protection and caller slippage protection.
- `collateral-remove` without a direct `tx-sender` check affects only `contract-caller`'s own account, so a wrapper cannot remove an external user's market collateral unless that wrapper is intentionally the account owner.
- Repay supports paying on behalf of another account but requires the payer to be the direct transaction caller.
- Disabled collateral can still be priced for liquidation/removal paths, which avoids relying only on the enabled asset list.
