SatGuard Protocol — Insurance Pool Security Audit

Mosas2000/SatGuard-Protocol · Commit: 8995aa7 (2026-01-31) · Audited: February 21, 2026

2
Critical
2
High
3
Medium
2
Low
1
Info

Overview

SatGuard Protocol is a Bitcoin-backed micro-insurance system on Stacks. Users create insurance pools with configurable coverage types, contribute funds, submit claims, and vote on claim payouts using contribution-weighted governance. The contract manages pool lifecycle (create → contribute → claim → vote → payout → close → withdraw).

The contract is 439 lines of Clarity across a single file (insurance-pool-full.clar) with 5 data maps, 2 data variables, 8 public functions, and 5 read-only functions.

Priority Score

MetricWeightScoreWeighted
Financial risk33 — DeFi insurance, intended to hold/transfer pooled funds9
Deployment likelihood21 — Has README but no tests or CI2
Code complexity22 — 439 lines, 5 maps, voting + claims4
User exposure1.50 — 0 stars/forks0
Novelty1.52 — Insurance is a new audit category3
Total18 / 10 = 1.8 ✓

Findings Summary

IDSeverityTitle
C-01CRITICALNo Token Transfers — Insurance Pool is Pure Bookkeeping
C-02CRITICALWithdrawal Share Calculation is an Identity Function
H-01HIGHClaimant Can Vote on Own Claim
H-02HIGHNo Access Control on Claim Processing
M-01MEDIUMPool Closure Ignores Pending Claims
M-02MEDIUMNo Claim Amount Cap Per Contributor
M-03MEDIUMInteger Division in Approval Threshold
L-01LOWNo Claim Expiry or Voting Deadline
L-02LOWNo Double-Claim Prevention Per Contributor
I-01INFOContributor Count Only Increments

Detailed Findings

C-01 No Token Transfers — Insurance Pool is Pure Bookkeeping

Location: contribute-to-pool, process-claim-payout, withdraw-from-pool

Description: The entire insurance protocol never calls stx-transfer? or any token transfer function. When users "contribute" to a pool, only map entries are updated — no STX moves. When claims are "paid", the claimant receives nothing. When users "withdraw", no tokens are returned. The contract is a pure accounting system with no economic reality.

;; contribute-to-pool — updates maps but never transfers STX
(map-set contributors
  { pool-id: pool-id, contributor: tx-sender }
  { amount: (+ (get amount existing-contribution) amount), contributed-at: block-height })

;; process-claim-payout — marks claim as paid but sends nothing
(map-set claims { claim-id: claim-id }
  (merge claim { status: CLAIM-STATUS-PAID }))
(map-set pools { pool-id: pool-id }
  (merge pool { total-funds: (- (get total-funds pool) (get amount claim)) }))

Impact: The insurance protocol provides zero financial protection. Users believe they are contributing to and protected by an insurance pool, but no funds are ever collected or distributed. This is the most fundamental flaw — the entire contract's purpose is unfulfilled.

Recommendation: Add (try! (stx-transfer? amount tx-sender (as-contract tx-sender))) in contribute-to-pool, (try! (as-contract (stx-transfer? (get amount claim) tx-sender (get claimant claim)))) in process-claim-payout, and corresponding transfer in withdraw-from-pool.

C-02 Withdrawal Share Calculation is an Identity Function

Location: withdraw-from-pool, line ~420

Description: The "proportional share" calculation divides by the same value it multiplies by, making it a no-op:

(let
  ((share (/ (* withdrawal-amount (get total-funds pool))
             (get total-funds pool))))  ;; = withdrawal-amount * X / X = withdrawal-amount

The formula withdrawal-amount * total-funds / total-funds always equals withdrawal-amount (ignoring integer truncation edge cases). This means if a pool has had claims paid out (reducing total-funds), withdrawing users get back their full original contribution amount rather than a proportional share of the remaining funds. The pool would underflow when the last contributors withdraw.

Impact: After claims deplete pool funds, early withdrawers take more than their fair share. The last contributor's withdrawal will revert with an underflow, permanently locking their share. If total-funds reaches 0 from claims, the division by zero aborts all withdrawals.

Recommendation: Track original total contributions separately. The share formula should be: (/ (* withdrawal-amount remaining-funds) original-total-contributions).

H-01 Claimant Can Vote on Own Claim

Location: vote-on-claim

Description: The voting function only checks that the voter is a pool contributor and hasn't voted before. It does not prevent the claimant from voting on their own claim. Since voting power equals contribution amount, a large contributor can submit a claim and immediately vote to approve it with significant weight.

;; No check for: (asserts! (not (is-eq tx-sender (get claimant claim))) err-unauthorized)
(asserts! (> voting-power u0) err-unauthorized)
(asserts! (is-none existing-vote) err-unauthorized)

Impact: A contributor who holds >60% of pool funds can unilaterally approve their own claim for max-coverage, draining the pool. Even with less than 60%, self-voting gives an unfair advantage in the governance process.

Recommendation: Add (asserts! (not (is-eq tx-sender (get claimant claim))) err-unauthorized) to prevent self-voting on claims.

H-02 No Access Control on Claim Processing

Location: process-claim-payout

Description: Anyone can call process-claim-payout — there is no restriction to pool creators, contributors, or any governance role. An external party could prematurely process a claim as soon as the minimum vote threshold is met, before all voters have participated.

Impact: A claimant (or colluding party) can call process-claim-payout immediately after gathering just enough votes, preventing additional negative votes from being cast. This creates a race condition favoring claim approval.

Recommendation: Restrict to pool creator or implement a voting deadline after which processing is allowed.

M-01 Pool Closure Ignores Pending Claims

Location: close-pool

Description: The pool creator can close a pool at any time without checking for pending claims. Since process-claim-payout only checks claim status (not pool status), pending claims can still be processed after closure. However, since submit-claim checks pool status, new claims are blocked. The inconsistency creates confusion about pool lifecycle.

Impact: A malicious pool creator could close the pool, trigger withdrawals, and drain funds before pending claims are processed.

Recommendation: Either block closure while claims are pending, or check pool status in process-claim-payout.

M-02 No Claim Amount Cap Per Contributor

Location: submit-claim

Description: A contributor who deposited 1 STX can submit a claim for max-coverage (potentially thousands of STX). The only check is that the contributor has some contribution, not that the claim is proportional to their stake.

;; Only checks contributor exists, not proportionality
(asserts! (> (get amount contribution) u0) err-unauthorized)
(asserts! (<= amount (get max-coverage pool)) err-invalid-amount)

Impact: A minimum contributor can claim the maximum coverage amount, extracting far more value than they put in.

Recommendation: Cap claims at a multiple of the contributor's own contribution, or require minimum contribution thresholds for different claim tiers.

M-03 Integer Division in Approval Threshold

Location: process-claim-payout

Description: The approval threshold uses integer division: (/ (* total-funds u60) u100). For small pool sizes, this truncates significantly. For example, with total-funds = 1, the threshold becomes 0, meaning any positive vote approves the claim.

Impact: Claims in very small pools can be approved with less than the intended 60% threshold. A pool with 1 STX total can have claims approved with zero votes-for.

Recommendation: Use (>= (* (get votes-for claim) u100) (* (get total-funds pool) u60)) to avoid division truncation.

L-01 No Claim Expiry or Voting Deadline

Location: submit-claim, vote-on-claim

Description: Claims remain in PENDING status indefinitely. There is no deadline for voting or automatic expiry. A claim that never reaches quorum stays pending forever, potentially blocking pool closure or creating governance gridlock.

Recommendation: Add a voting-deadline field (e.g., submitted-at + 1000 blocks) and allow automatic rejection after expiry.

L-02 No Double-Claim Prevention Per Contributor

Location: submit-claim

Description: A contributor can submit unlimited claims against the same pool. While each claim requires separate voting, a malicious contributor could spam claims to exhaust voter attention or overwhelm governance.

Recommendation: Track active claims per contributor and limit concurrent pending claims.

I-01 Contributor Count Only Increments

Location: withdraw-from-pool

Description: The contributor-count is decremented during withdrawals but is never checked for underflow. More importantly, it only increments on first contribution — if the same contributor withdraws and the pool somehow re-opens, the count is not restored on re-contribution since the map entry was deleted.

Recommendation: Minor issue given pool lifecycle, but consider making contributor-count a derived value or removing it from core logic.

Architecture Assessment

The contract implements a complete insurance pool lifecycle — pool creation, contribution, claims, voting, payouts, closure, and withdrawal. The governance model (contribution-weighted voting with 60% approval and 50% quorum) is reasonable in design.

However, the contract suffers from the same fundamental flaw seen across many Clarity projects: no actual token transfers. Every financial operation is pure bookkeeping. This pattern — tracking balances in maps without moving real tokens — makes the contract a simulation rather than a functioning protocol.

The withdrawal share calculation bug (C-02) further demonstrates that the financial math was not tested, as the identity function x * y / y = x would be immediately apparent in any integration test with claim payouts.

Positive Observations