Lending Helper / Router Contract โ Independent Security Audit
borrow-helper-v2-1-7 is a thin helper/router contract for the Zest Protocol lending platform on Stacks mainnet. It wraps the core pool-borrow-v2-4 contract, adding:
write-feed) before price-sensitive operationssupply-all function for batch multi-asset supplyThe contract holds no state and no funds. All token transfers and access control are delegated to downstream contracts. This is an inherently low-risk architecture โ the helper itself cannot lose funds. The primary risk surface is incorrect event data misleading off-chain systems.
| Function | Delegates To | Extra Logic |
|---|---|---|
supply | pool-borrow-v2-4.supply | Claims incentive rewards first |
supply-all | supply (ร10 assets) | Batch convenience wrapper |
borrow | pool-borrow-v2-4.borrow | Pyth feed update |
repay | pool-borrow-v2-4.repay | Event logging |
withdraw | pool-borrow-v2-4.withdraw | Pyth feed + incentive claim |
claim-rewards | incentives.claim-rewards | Rewards contract validation |
liquidation-call | pool-borrow-v2-4.liquidation-call | Pyth feed update |
set-e-mode | pool-borrow-v2-4.set-e-mode | Pyth feed update |
set-user-use-reserve-as-collateral | pool-borrow-v2-4.set-user-use-reserve-as-collateral | Pyth feed update |
tx-sender == contract-caller check on every public function โ prevents smart-contract-mediated calls and reentrancy vectorsas-contract usage โ despite being pre-Clarity 4, the contract never assumes contract identity for token operationsis-rewards-contract checks the incentives trait against a stored whitelist, preventing malicious incentives contractsliquidation-call โ print statementcollateral-reserve-state field in the liquidation event log queries debt-asset-principal instead of collateral-asset-principal. This emits the debt reserve state twice and never emits the actual collateral reserve state.;; BUG: uses debt-asset-principal instead of collateral-asset-principal
collateral-reserve-state: (try! (contract-call? .pool-0-reserve-v2-0
get-reserve-state debt-asset-principal)),
;; ^^^^^^^^^^^^^^^^^^^
;; Should be: collateral-asset-principal
collateral-reserve-state: (try! (contract-call? .pool-0-reserve-v2-0
get-reserve-state collateral-asset-principal)),supply, borrow, withdraw, claim-rewards, set-user-use-reserve-as-collateral, set-e-modeowner (or who/user) parameter without verifying it equals tx-sender. The contract checks (is-eq tx-sender contract-caller) but not (is-eq tx-sender owner). Whether this is exploitable depends entirely on the downstream pool-borrow-v2-4 access control.pool-borrow-v2-4 does not independently verify ownership:
supply: Caller pays tokens, credit goes to arbitrary owner โ likely intentional (supply on behalf of)borrow: Could potentially borrow against someone else's collateralwithdraw: Could potentially withdraw someone else's supplied assetsclaim-rewards: Could claim someone else's rewards(asserts! (is-eq tx-sender owner) ERR_UNAUTHORIZED) to functions where owner must equal caller, or document explicitly that pool-borrow-v2-4 enforces this. The "supply on behalf of" pattern should use a separate function.borrow, liquidation-call, withdrawdeadline parameter:
(asserts! (< stacks-block-height deadline) (err u9999))liquidation-callpool-borrow-v2-4.liquidation-call and returns (ok u0). All other functions return (ok true). The inconsistency means callers cannot distinguish between different liquidation outcomes.(ok true) for consistency.supply-allsupply-all function hardcodes 10 specific token contract addresses (ststx, aeusdc, wstx, diko, usdh, susdt, usda, sbtc, ststxbtc, alex) and their corresponding z-token LP contracts. Adding or removing supported assets requires deploying a new helper version.as-contract and holds no assets, the practical impact is zero. Clarity 4's as-contract? with explicit allowances would be relevant if the contract ever needed to act with its own authority.as-contract patterns.This contract's security is almost entirely dependent on downstream contracts. The helper is a pass-through โ it cannot independently lose funds. Key trust boundaries:
pool-borrow-v2-4 โ must enforce ownership checks on borrow, withdraw, collateral toggle, and e-mode changes. All financial logic lives here.pool-0-reserve-v2-0 โ must correctly track reserve and user state. Used only for read-only event data in this contract.pyth-oracle-v4) โ price feed integrity depends on Wormhole guardian signatures. The helper correctly passes through caller-provided bytes for on-chain verification.rewards-data whitelist before use. Trust is in the whitelist management.This is a well-structured, low-risk helper contract. It follows the common Aave-style "helper" pattern โ a thin routing layer that bundles oracle updates, incentive claims, and event emission around core protocol calls. The one material finding (M-01) is a copy-paste bug in the liquidation event log that could mislead off-chain systems. The architectural decision to keep the helper stateless and custody-free is sound and limits the blast radius of any bugs in this layer.