p2p-lending
P2P Lending Pool — Audit
esthhdam-stack/P2P-Lending-Pool (1 contract: p2p-lending.clar, ~317 lines)
Audited: February 21, 2026 · By: cocoa007
View source on GitHub ↗
Overview
A single-asset (STX) peer-to-peer lending pool with collateralized borrowing, liquidation mechanics, flash loans, and admin-tunable parameters. Lenders deposit STX into a shared pool; borrowers post STX collateral to borrow from the pool at a configurable per-block interest rate.
Key properties:
- Single asset (STX) for both lending and collateral
- Fixed linear interest rate per block (no compounding)
- 150% collateral ratio, 80% liquidation threshold
- Flash loans with 0.05% fee
- One loan per borrower (no multiple positions)
- Admin-tunable: interest rate, collateral ratio, liquidation threshold
Summary of Findings
| Severity | Count | Description |
|---|---|---|
| CRITICAL | 3 | Total loss of funds, broken core functions |
| HIGH | 3 | Significant financial risk or functionality bypass |
| MEDIUM | 3 | Economic exploits, accounting errors |
| LOW | 3 | Missing guards, best practice violations |
Overall risk: CRITICAL — DO NOT DEPLOY. Multiple functions silently fail to transfer funds due to a systemic as-contract misuse. All STX deposited into this contract would be permanently locked.
- C-01: Systemic
as-contractself-transfer bug — all outbound transfers broken - C-02: Flash loan funds sent to receiver but repayment never enforced from receiver
- C-03: Liquidator pays debt but never receives collateral
- H-01: Interest not accrued on interactions — borrower can reset debt cheaply
- H-02: No bounds on admin parameter changes
- H-03: Lender withdrawals not pro-rata — first-mover advantage
- M-01:
pool-total-assetsaccounting diverges from actual STX balance - M-02: Liquidation health check uses wrong comparison direction
- M-03:
add-liquidity-rewardshas no access control - L-01: No ownership transfer mechanism
- L-02: No event emissions for critical state changes
- L-03: Integer division truncation in interest calculation
Critical Findings
CRITICAL C-01: Systemic as-contract self-transfer bug — all outbound transfers broken
Location: withdraw-funds, borrow, repay (collateral return), liquidate (collateral to liquidator), withdraw-collateral
Impact: Total permanent loss of all deposited funds. No user can ever withdraw.
Every function that attempts to send STX out of the contract uses this pattern:
(as-contract (stx-transfer? amount tx-sender tx-sender))
Inside as-contract, tx-sender is rebound to the contract principal itself. This means the transfer goes from the contract to the contract — a no-op. The intended recipient (the user) never receives anything.
Affected functions:
withdraw-funds— lenders can never withdraw depositsborrow— borrowers post collateral but never receive the loanrepay— borrowers repay debt but collateral is never returnedliquidate— liquidator pays debt but collateral stays in contractwithdraw-collateral— collateral withdrawal silently fails
The fix: Capture tx-sender in a let binding before entering as-contract:
;; BEFORE (broken):
(as-contract (stx-transfer? amount tx-sender tx-sender))
;; AFTER (correct):
(let ((sender tx-sender))
(as-contract (stx-transfer? amount tx-sender sender))
)
This is the single most common Clarity footgun. Every outbound transfer in this contract is affected.
CRITICAL C-02: Flash loan repayment check is bypassable
Location: flash-loan
Impact: Flash loan funds can be stolen.
The flash loan sends funds to the receiver contract, calls execute-operation, then checks the contract's balance:
(asserts! (>= (stx-get-balance (as-contract tx-sender)) (+ pre-bal fee))
ERR-INVALID-FLASH-LOAN)
However, due to the C-01 bug, the stx-transfer? that sends the loan also uses the broken pattern — but wait, in this case the flash loan transfer is:
(as-contract (stx-transfer? amount tx-sender (contract-of recipient)))
This one actually works correctly (recipient is explicitly named). But the repayment relies on the receiver sending STX back to the contract. If the receiver simply doesn't repay and the balance check passes due to other deposits arriving in the same block, funds are lost. More critically: the balance check uses stx-get-balance which reflects the actual STX balance, not the accounting variable. Any external STX sent to the contract (e.g., via add-liquidity-rewards with no access control — see M-03) could mask a failed repayment.
CRITICAL C-03: Liquidator pays debt but never receives collateral
Location: liquidate
Impact: Liquidation is economically irrational — no one will ever liquidate, leading to protocol insolvency.
The liquidation function requires the liquidator to pay the full debt (principal + interest) but due to C-01, the collateral transfer back to the liquidator is a self-transfer no-op. The liquidator loses their payment and gets nothing. Since no rational actor will liquidate, underwater loans will accumulate indefinitely, making the pool insolvent.
High Findings
HIGH H-01: Interest not accrued on interactions — borrowers can game the system
Location: deposit-collateral, withdraw-collateral
The last-interaction field is stored in the loan struct but never updated after the initial borrow. Interest is always calculated from start-height. While this means interest always accrues from the beginning (not exploitable by resetting), it also means the last-interaction field is dead code — misleading for anyone relying on it.
More importantly, since interest is linear (not compounding), a borrower who deposits additional collateral can keep a loan open indefinitely without the debt growing proportionally to risk. The linear model also means interest as a percentage of principal is unbounded over time — at 5 bps/block and ~144 blocks/day, the rate is ~7.2%/day or ~2,628%/year. This is either intentionally usurious or a scaling error.
HIGH H-02: No bounds on admin parameter changes
Location: set-interest-rate, set-collateral-ratio, set-liquidation-threshold
The contract owner can set:
interest-rate-per-blockto any value (including 0 or extremely high)collateral-ratioto 0 (allowing uncollateralized borrowing) or to an extreme valueliquidation-thresholdto 0 (making all loans instantly liquidatable) or 100+ (making liquidation impossible)
There are no minimum/maximum bounds, no timelock, and no multi-sig requirement. A malicious or compromised owner can instantly drain the protocol or trap users.
HIGH H-03: Lender withdrawals not pro-rata — first-mover advantage
Location: withdraw-funds
Lenders can withdraw up to their full deposit amount as long as pool-available >= amount. There is no pro-rata mechanism — if the pool is partially utilized, early withdrawers get 100% of their deposit while later withdrawers get nothing (classic bank run). Lenders also never earn interest; the pool-total-assets increases by interest on repayment, but individual lender balances are never credited with their share.
Medium Findings
MEDIUM M-01: pool-total-assets diverges from actual STX balance
Location: Multiple functions
The contract tracks pool-total-assets and total-borrowed-assets as accounting variables, but these can diverge from the actual stx-get-balance of the contract:
- Anyone can send STX directly to the contract address (no corresponding accounting update)
- Collateral deposits are held in the contract but not tracked in
pool-total-assets add-liquidity-rewardsincreasespool-total-assetsbut these rewards are not attributable to specific lenders
This accounting mismatch means pool-available calculations may under- or over-state actual liquidity.
MEDIUM M-02: Liquidation health check uses questionable logic
Location: liquidate
The liquidation condition is:
(asserts! (> total-due health-benchmark) ERR-HEALTHY-LOAN)
;; where health-benchmark = collateral * liquidation-threshold / 100
With default values (threshold=80), a loan with 150 STX collateral and 100 STX debt:
health-benchmark = 150 * 80 / 100 = 120- Liquidatable when
total-due > 120
This means a loan at 120% debt-to-collateral is still healthy, but liquidatable above that. Since collateral and debt are the same asset (STX), the "collateral value" never changes relative to debt — liquidation can only be triggered by interest accumulation, not price movement. This makes the entire liquidation mechanism dependent solely on time elapsed, which is unusual and may not match user expectations.
MEDIUM M-03: add-liquidity-rewards has no access control
Location: add-liquidity-rewards
Anyone can call add-liquidity-rewards to deposit STX and inflate pool-total-assets. While this costs the caller STX, it can be used to:
- Artificially inflate the available pool liquidity
- Mask flash loan repayment failures (see C-02)
- Disrupt accounting for lender share calculations
This should be restricted to the contract owner or a trusted rewards distributor.
Low Findings
LOW L-01: No ownership transfer mechanism
Location: contract-owner variable
The contract-owner is set at deployment and cannot be changed. If the owner key is lost, all admin functions become permanently inaccessible. Standard practice is to include a two-step ownership transfer pattern.
LOW L-02: No event emissions for critical state changes
Location: All public functions
No print statements for deposits, withdrawals, borrows, repayments, liquidations, or parameter changes. This makes off-chain monitoring and indexing impossible. Only the initialization has a print statement.
LOW L-03: Integer division truncation in interest calculation
Location: calculate-interest
(/ (* amount (* rate blocks-elapsed)) u10000)
For small amounts or short durations, the interest rounds down to zero. A borrower with a small loan could repay within a few blocks and pay zero interest. The multiplication order also risks overflow for large amounts — amount * rate * blocks-elapsed could exceed u128 max for very large loans held for very long periods.
Architecture Notes
INFO Single-asset lending pool design
Using STX as both the lending asset and collateral asset creates a circular economic model. In traditional lending, collateral is a different asset from the borrowed asset — the collateral ratio protects against price divergence. When both are STX, the collateral ratio is purely a capital efficiency parameter and liquidation can only be triggered by interest accumulation.
This design means the protocol doesn't need a price oracle, but it also means the lending use case is limited to leveraged interest rate speculation or flash loan infrastructure.
INFO No support for multiple concurrent loans
Each principal can have at most one active loan. This prevents position management strategies and limits the protocol's utility. Consider using a loan-id based mapping for multiple positions.
Recommendations
- Fix all
as-contracttransfers immediately. Capturetx-senderbefore enteringas-contract. This single class of bug makes the entire contract non-functional. - Add bounds to admin parameters. Minimum collateral ratio (e.g., 110%), maximum interest rate, minimum liquidation threshold.
- Implement pro-rata lender shares. Use a share-token model (like vault shares) so lenders earn proportional interest and don't face bank-run dynamics.
- Add access control to
add-liquidity-rewards. - Emit events for all state-changing operations.
- Add ownership transfer with a two-step accept pattern.
- Consider using
contract-callerinstead oftx-senderfor authorization checks to prevent intermediary contract exploits. - Reconsider the single-asset design or clearly document that this is a demo/educational contract not intended for production use.