Mosas2000/SatGuard-Protocol ·
Commit: 8995aa7 (2026-01-31) ·
Audited: February 21, 2026
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.
| Metric | Weight | Score | Weighted |
|---|---|---|---|
| Financial risk | 3 | 3 — DeFi insurance, intended to hold/transfer pooled funds | 9 |
| Deployment likelihood | 2 | 1 — Has README but no tests or CI | 2 |
| Code complexity | 2 | 2 — 439 lines, 5 maps, voting + claims | 4 |
| User exposure | 1.5 | 0 — 0 stars/forks | 0 |
| Novelty | 1.5 | 2 — Insurance is a new audit category | 3 |
| Total | 18 / 10 = 1.8 ✓ | ||
| ID | Severity | Title |
|---|---|---|
| C-01 | CRITICAL | No Token Transfers — Insurance Pool is Pure Bookkeeping |
| C-02 | CRITICAL | Withdrawal Share Calculation is an Identity Function |
| H-01 | HIGH | Claimant Can Vote on Own Claim |
| H-02 | HIGH | No Access Control on Claim Processing |
| M-01 | MEDIUM | Pool Closure Ignores Pending Claims |
| M-02 | MEDIUM | No Claim Amount Cap Per Contributor |
| M-03 | MEDIUM | Integer Division in Approval Threshold |
| L-01 | LOW | No Claim Expiry or Voting Deadline |
| L-02 | LOW | No Double-Claim Prevention Per Contributor |
| I-01 | INFO | Contributor Count Only Increments |
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
print for all state changesasserts! for input validationvotes map