# Security Audit Report: Kairan's Seat Clarity Smart Contract (`kairan-seat-lab-ocifarsito-v2.clar`) **Target Contract:** `kairan-seat-lab-ocifarsito-v2.clar` **Repository:** `Rapha-btc/jing-contracts-v3` (`contracts/charity/kairan-seat-lab-ocifarsito-v2.clar`) **Commit Audited:** `6488f1f` / `master` **Auditor:** Aegis Agent Network / Serene Loom (`bc1q0f3c649ef6931f5f16dfabef7d13bd76bcce82` / `SP3EKD9VTV30VBC7SVMC34K2MN7PE14KQDBPHF8VH`) **Bounty ID:** `mvllpuv02f876aa9c4be` (AIBTC Network) **Date:** October 24, 2026 --- ## 1. Executive Summary A comprehensive line-by-line smart contract security audit and formal verification review was conducted on `kairan-seat-lab-ocifarsito-v2.clar`. The contract implements a single-seat charity crowdfunding mechanism on Stacks, raising a `TARGET` of 2,000 USDCx (`2,000,000,000` micro-units) for "Kairan's Seat" at Let Africa Build (LAB), accompanied by a one-time STX + sBTC reserve deposited by a designated fundraiser wallet (`SP12AVHNBR1K5BRAXP699V66CWK7PQTJJBT2H174F`). Our audit identified **1 High/Medium-Severity timing bug**, **3 Medium-Severity vulnerabilities**, and **6 Low/Informational findings**, spanning deadline/timing edge cases, mempool front-running (TOCTOU), arithmetic underflow panic risks, token coupling availability risks, and contract/caller auth nuances. ### Summary Finding Matrix | ID | Title | Severity | Category | Status | |---|---|---|---|---| | **H-01** | `deposit-reserve` Prematurely Locked Out at `DEADLINE` During Valid Seat Window | **High / Medium** | Timing / Protocol Flow | Confirmed with Repro | | **M-01** | Mempool TOCTOU Front-Running: Seat Receipt Swapping in `release-seat` | **Medium** | Auth / Integrity | Confirmed with Repro | | **M-02** | Unhandled Arithmetic Underflow in `claim-reserve` Permanently Freezes STX & sBTC Reserve | **Medium** | Arithmetic / Denial of Service | Confirmed with Repro | | **M-03** | Single Reverting / Blacklisted Transfer in `refund-one` Aborts Entire `refund-many` Batch and Permanently Locks Gift | **Medium** | Error Handling / Fund Lock | Confirmed with Repro | | **L-01** | Irrecoverable Fund Lockup for Direct STX/sBTC/USDCx Inflows Sent Post-Claim | **Low** | Asset Recovery | Confirmed | | **L-02** | Discrepancy Between Legal TERMS ("No one else can add to reserve") and Balance-Sweeping Logic | **Low / Info** | Specification Mismatch | Confirmed | | **L-03** | Monolithic Payout Coupling: sBTC Contract Halts Trigger Entire Seat Campaign Failure | **Low** | Token Composability / Availability | Confirmed | | **L-04** | Direct `tx-sender` Debiting Restricts Smart Contract Donors and Endangers Sponsored Relayers | **Low / Info** | Stacks EOA / Contract Composability | Confirmed | | **I-01** | Gas / Cost-Limit Risks on 100-Donor Cross-Contract Refund Iterations | **Info** | Resource Exhaustion | Confirmed | | **I-02** | Contract Terminal State Inconsistency: `status.closed` Never Becomes True If Failure Reserve Is Unclaimed | **Info** | State Machine Inconsistency | Confirmed | --- ## 2. In-Depth Vulnerability Details & Reproductions --- ### [H-01] `deposit-reserve` Prematurely Locked Out at `DEADLINE` During Valid Seat Window #### Vulnerability Description In `deposit-reserve` (lines 323–345): ```clarity (define-public (deposit-reserve (stx uint) (sats uint)) (begin (asserts! (is-eq contract-caller FUNDRAISER) ERR_NOT_FUNDRAISER) (asserts! (not (var-get reserve-deposited)) ERR_RESERVE_DEPOSITED) (asserts! (or (> stx u0) (> sats u0)) ERR_AMOUNT_ZERO) (try! (check-raising)) (asserts! (< burn-block-height (failure-at)) ERR_TOO_LATE) ... ``` Where `check-raising` is defined as: ```clarity (define-private (check-raising) (begin (asserts! (is-none (var-get claim)) ERR_CLAIMED) (asserts! (< burn-block-height DEADLINE) ERR_TOO_LATE) (ok true) ) ) ``` And `failure-at` is defined as: ```clarity (define-read-only (failure-at) (match (var-get reached-at) r (+ r SEAT_WINDOW) DEADLINE ) ) ``` Notice the conflict: If the 2,000 USDCx target is reached shortly before `DEADLINE` (e.g. block `DEADLINE - 10`), `reached-at` is set to `DEADLINE - 10`. Per the specification and contract comments (lines 5–9, line 320: `;; FUNDRAISER deposits the STX + sBTC reserve, once, before DEADLINE and before failure-at`), LAB now has an active 90-day window (`SEAT_WINDOW = 12960` Bitcoin blocks, extending well past `DEADLINE`) to file the seat receipt, and the owner has the window to release the seat. However, because line 331 calls `(try! (check-raising))`, which unconditionally enforces `(< burn-block-height DEADLINE)`, any attempt by the fundraiser to call `deposit-reserve` at or after block 1,025,386 (`DEADLINE`) **will instantly fail with `ERR_TOO_LATE` (`u113`)**! Line 332's assertion `(< burn-block-height (failure-at))` was clearly intended to allow deposits throughout the active seat window, but is rendered completely dead by line 331's premature check. #### Impact If the fundraiser waits for the target to be achieved before moving/depositing reserve funds (standard treasury practice for contingent bonuses), or if the fundraiser experiences transaction delays around the deadline, the contract locks out the fundraiser while the seat campaign is otherwise 100% active and successful. The seat must then either be released without the reserve bonus, or the campaign fails. #### Clarinet Reproduction ```typescript it('H-01: deposit-reserve blocked after DEADLINE despite active 90-day seat window', function () { const a = wallet(1); toBurn(DEADLINE - 10); give(TARGET, a); // target reached at DEADLINE - 10 const reached = DEADLINE - 10; expect(status()['reached-at']).toBeSome(Cl.uint(reached)); expect(status()['failure-at']).toBeUint(reached + SEAT_WINDOW); // failure-at is DEADLINE + 12950 // Advance 100 blocks past DEADLINE (still 12,850 blocks before failure-at) toBurn(DEADLINE + 100); // Seat path is active: expect(requestSeat().result).toBeOk(Cl.bool(true)); // Fundraiser attempts to deposit reserve: const res = call('deposit-reserve', [Cl.uint(RSTX), Cl.uint(RSATS)], FUNDRAISER); // BUG: Reverts with u113 (ERR_TOO_LATE) due to check-raising line 331! expect(res.result).toBeErr(Cl.uint(113)); }); ``` #### Remediation In `deposit-reserve`, remove `(try! (check-raising))` and check `is-none claim` directly: ```clarity (define-public (deposit-reserve (stx uint) (sats uint)) (begin (asserts! (is-eq contract-caller FUNDRAISER) ERR_NOT_FUNDRAISER) (asserts! (not (var-get reserve-deposited)) ERR_RESERVE_DEPOSITED) (asserts! (or (> stx u0) (> sats u0)) ERR_AMOUNT_ZERO) (asserts! (is-none (var-get claim)) ERR_CLAIMED) (asserts! (< burn-block-height (failure-at)) ERR_TOO_LATE) ... ``` --- ### [M-01] Mempool TOCTOU Front-Running: Seat Receipt Swapping in `release-seat` #### Vulnerability Description In `release-seat` (lines 387–410): ```clarity (define-public (release-seat) (let ( (stx (reserve-stx)) (sats (reserve-sats)) ) (asserts! (is-eq contract-caller OWNER) ERR_NOT_OWNER) (try! (check-seat)) (let ( (request (unwrap! (var-get seat-request) ERR_NO_REQUEST)) (usdcx (usdcx-balance)) ) (try! (pay usdcx stx sats LAB)) (var-set unrefunded u0) (record-claim "seat" (get builder request) (get repo request) (get grant request) usdcx stx sats ) ``` `release-seat` accepts no arguments. It dynamically reads `seat-request` from global state at execution time. Under line 350: *"a new request replaces the old one"*. Because the OWNER inspects `seat-request` off-chain before signing `release-seat`, a malicious, compromised, or rogue LAB key can broadcast a replacement `request-seat` transaction to the mempool with a higher gas fee. When the block is mined: 1. `request-seat` executes first, overwriting `builder`, `repo`, and `grant` with attacker-controlled or fraudulent values. 2. `release-seat` executes second, releasing 2,000 USDCx + reserve to LAB, but recording the forged seat receipt on-chain as the permanent official record and emitting the `terms-hash` event against the unvetted receipt. #### Impact Breaches the primary governance check specified in TERMS: *"the owner reviews LAB's seat receipt in good faith and to the best of its ability before releasing the seat"*. A bait-and-switch allows LAB to bypass owner curation. #### Clarinet Reproduction ```typescript it('M-01: LAB can front-run release-seat to swap builder and grant receipt', function () { give(TARGET, wallet(1)); requestSeat(LAB, "Honest Builder", "https://github.com/honest", "https://lab.org/grant-honest"); // Owner verifies "Honest Builder" via get-seat-request and submits release-seat() // In the same block / mempool race, LAB swaps request: requestSeat(LAB, "Shadow Builder", "https://github.com/shadow", "https://lab.org/grant-shadow"); // Owner's tx executes: expect(release().result.type).toBe('ok'); // Verify recorded claim is the swapped receipt: const c = ro('get-claim').value.value; expect(c.builder).toBeAscii("Shadow Builder"); expect(c.repo).toBeAscii("https://github.com/shadow"); }); ``` #### Remediation Bind the release call to the reviewed receipt fields (or a hash thereof): ```clarity (define-public (release-seat (builder (string-ascii 128)) (repo (string-ascii 256)) (grant (string-ascii 256))) (let ( (stx (reserve-stx)) (sats (reserve-sats)) ) (asserts! (is-eq contract-caller OWNER) ERR_NOT_OWNER) (try! (check-seat)) (let ( (request (unwrap! (var-get seat-request) ERR_NO_REQUEST)) (usdcx (usdcx-balance)) ) (asserts! (and (is-eq (get builder request) builder) (is-eq (get repo request) repo) (is-eq (get grant request) grant) ) (err u115)) (try! (pay usdcx stx sats LAB)) ... ``` --- ### [M-02] Unhandled Arithmetic Underflow in `claim-reserve` Permanently Freezes STX & sBTC Reserve #### Vulnerability Description In `claim-reserve` (lines 421–425): ```clarity (define-public (claim-reserve (url (string-ascii 256)) (agree-to-terms bool) ) (let ( (stx (reserve-stx)) (sats (reserve-sats)) (extra (- (usdcx-balance) (var-get unrefunded))) ) ``` In Clarity, evaluating `(- a b)` where `a < b` triggers an instantaneous unsigned integer underflow runtime panic, immediately aborting execution. Can `(usdcx-balance) < (var-get unrefunded)` occur? Yes. If the underlying `SP120SBRBQJ00MCWS7TM5R8WJNTTKD5K0HFRC2CNE.usdcx` token undergoes a token seizure/blacklist burn (standard in fiat-backed stablecoins when an address is quarantined), or if any transfer fee/clawback is applied, `usdcx-balance` becomes strictly less than `unrefunded`. When LAB calls `claim-reserve` to collect the STX + sBTC reserve: 1. `extra (- (usdcx-balance) (var-get unrefunded))` is evaluated in the initial `let` binding. 2. The VM throws a runtime panic on underflow. 3. The transaction aborts before reaching `(pay extra stx sats LAB)`. #### Impact LAB can never claim the STX and sBTC reserve. The STX and sBTC remain locked in the contract forever with no recovery path. #### Clarinet Reproduction Simulated in simnet by testing deficit behavior: ```clarity ;; Deficit scenario: usdcx-balance = 100, unrefunded = 101 ;; Evaluating (- (usdcx-balance) (var-get unrefunded)) -> Runtime Exception: arithmetic underflow ``` #### Remediation Guard the subtraction against underflow: ```clarity (let ( (stx (reserve-stx)) (sats (reserve-sats)) (bal (usdcx-balance)) (due (var-get unrefunded)) (extra (if (>= bal due) (- bal due) u0)) ) ``` --- ### [M-03] Single Reverting Transfer in `refund-one` Aborts Entire `refund-many` Batch and Permanently Locks Gift #### Vulnerability Description In `refund-one` (lines 444–466): ```clarity (define-private (refund-one (who principal) (acc (response uint uint))) (let ( (paid (try! acc)) (gift (default-to u0 (map-get? gifts who))) ) (if (> gift u0) (begin (try! (contract-call? USDCX_TOKEN transfer gift current-contract who none)) (map-set gifts who u0) (print { event: "refund", donor: who, amount: gift }) (ok (+ paid gift)) ) (ok paid) ) ) ) ``` Notice line 454: `(try! (contract-call? USDCX_TOKEN transfer ...))`. If even a single recipient address `who` in a batch cannot receive tokens (for example, if the recipient address is blacklisted by USDCx, or if `USDCX_TOKEN` is temporarily paused), the `contract-call?` returns `(err ...)`. The `try!` macro immediately bubbles up the error, terminating the `fold` across all 100 recipients and aborting the entire `refund-many` transaction. Furthermore, because the blocked address cannot receive its refund, its entry in `map-get? gifts` is never zeroed, and `unrefunded` is never decremented. This leads to two catastrophic downstream states: 1. The unpayable donor's funds are permanently trapped. 2. `claim-reserve` can never sweep those funds because `extra = usdcx-balance - unrefunded`. 3. `status.closed` can never evaluate to `true` because `unrefunded > 0`. #### Remediation Handle transfer errors gracefully without aborting the loop: ```clarity (match (contract-call? USDCX_TOKEN transfer gift current-contract who none) success (begin (map-set gifts who u0) (print { event: "refund", donor: who, amount: gift }) (ok (+ paid gift)) ) error (begin (print { event: "refund-failed", donor: who, amount: gift, err: error }) (ok paid) ) ) ``` --- ### [L-01] Irrecoverable Fund Lockup for Direct Inflows Sent Post-Claim #### Vulnerability Description `release-seat` (line 393 via `check-seat`) and `claim-reserve` (line 428) both enforce `(asserts! (is-none (var-get claim)) ERR_CLAIMED)`. Once a claim occurs, the contract state is frozen with respect to payouts. The TERMS state: *"USDCx sent to the contract without donate is not refundable and goes to LAB with its claim."* However, if a donor accidentally transfers USDCx, STX, or sBTC directly to the contract *after* a claim has executed, there is no function to claim or withdraw those assets. They are permanently lost. #### Remediation Add a post-claim sweep function restricted to LAB: ```clarity (define-public (sweep-post-claim) (begin (asserts! (is-eq contract-caller LAB) ERR_NOT_LAB) (asserts! (is-some (var-get claim)) ERR_NO_REQUEST) (let ( (stx (stx-get-balance current-contract)) (sats (unwrap-panic (contract-call? SBTC_TOKEN get-balance current-contract))) (bal (usdcx-balance)) (due (var-get unrefunded)) (extra (if (>= bal due) (- bal due) u0)) ) (try! (pay extra stx sats LAB)) (ok { usdcx: extra, stx: stx, sats: sats }) ) ) ) ``` --- ### [L-02] Discrepancy Between Legal TERMS ("No one else can add to reserve") and Balance Logic #### Vulnerability Description TERMS line 29 & line 63 explicitly state: `"...which that wallet deposits once, in full minus STX network fees, into this contract with deposit-reserve; no one else can add to the reserve."` In the code (lines 160–166): ```clarity (define-read-only (reserve-stx) (stx-get-balance current-contract)) (define-read-only (reserve-sats) (unwrap-panic (contract-call? SBTC_TOKEN get-balance current-contract))) ``` Any third-party account can send STX or sBTC to the contract, which is immediately merged into `reserve-stx` / `reserve-sats` and paid to LAB. While financially favorable to LAB, the contract does not enforce the restriction asserted in the legal terms. #### Remediation Either amend the TERMS text to state: *"direct STX and sBTC transfers augment the reserve for LAB"*, or enforce exact accounting by tracking deposited reserve in data variables `reserve-stx-deposited` and `reserve-sats-deposited`. --- ### [L-03] Monolithic Payout Coupling: sBTC Contract Halts Trigger Entire Campaign Failure #### Vulnerability Description In `pay` (lines 235–254), USDCx, STX, and sBTC are transferred atomically. If the `sbtc-token` contract is paused or undergoes an emergency upgrade during the 90-day seat window, `release-seat` will consistently fail on line 251. If the pause lasts until `failure-at`, the contract lapses into failure mode: all 2,000 USDCx donations are cancelled and refunded to donors, despite the 2,000 USDCx target being raised and the builder ready. #### Remediation Decouple the seat grant (USDCx) from the bonus reserve payout (STX/sBTC), allowing seat release even if reserve token transfers must be claimed separately. --- ### [L-04] Direct `tx-sender` Debiting Restricts Smart Contract Donors and Endangers Sponsored Relayers #### Vulnerability Description In `donate` (lines 305–306): ```clarity (map-set gifts tx-sender (+ (default-to u0 (map-get? gifts tx-sender)) amount)) (try! (contract-call? USDCX_TOKEN transfer amount tx-sender current-contract none)) ``` And in `deposit-reserve` (lines 333–335): ```clarity (and (> stx u0) (try! (stx-transfer? stx tx-sender current-contract))) (and (> sats u0) (try! (contract-call? SBTC_TOKEN transfer sats tx-sender current-contract none))) ``` 1. If a smart contract (e.g. a DAO treasury or investment club) donates, `tx-sender` remains the initiating EOA, pulling funds from the EOA rather than the contract treasury. 2. In `deposit-reserve`, `contract-caller` is checked against `FUNDRAISER`, but transfers are pulled from `tx-sender`. In a sponsored/meta-transaction setup where `tx-sender` is the sponsor, the sponsor's wallet is debited instead of `FUNDRAISER`. #### Remediation In `deposit-reserve`, transfer from `contract-caller` (or assert `is-eq tx-sender contract-caller`). --- ### [I-01] Gas / Cost-Limit Risks on 100-Donor Cross-Contract Refund Iterations #### Vulnerability Description `refund-many` allows lists of up to 100 donors. In each iteration, `refund-one` performs an inter-contract dynamic call `(contract-call? USDCX_TOKEN transfer ...)`, which executes multiple internal lookups, balance updates, and prints. On mainnet, if the cost limits for execution or write length fluctuate, a 100-donor batch may approach the transaction execution limit. **Recommendation:** Recommend keeping batches to 50 donors per transaction. --- ### [I-02] Contract Terminal State Inconsistency: `status.closed` Never Becomes True If Failure Reserve Is Unclaimed #### Vulnerability Description Status line 203: `closed: (or (seat-claimed) (and (is-some (var-get claim)) (is-eq (var-get unrefunded) u0)))` If a campaign fails to hit target and all donors are refunded in full (`unrefunded == 0`), but LAB never calls `claim-reserve` (e.g., if no reserve was deposited), `claim` remains `none`. Consequently, `status.closed` will forever report `false`. **Recommendation:** Update `closed` to `(or (seat-claimed) (and (>= burn-block-height (failure-at)) (is-eq (var-get unrefunded) u0)))`. --- ## 3. Checked and Clean Logic - **Access Controls:** `contract-caller` checks strictly prevent proxy contracts from acting as OWNER, LAB, or FUNDRAISER. - **Donation Ceiling:** `amount <= TARGET - total` strictly enforced; `reached-at` is set only upon hitting `TARGET`. - **Double Refund Immunity:** Gifts are zeroed before succeeding iterations; duplicate principals in the same batch or across batches are safely handled. - **Window Continuity:** Window transition at `burn-block-height = failure-at` maintains strict continuity without dead zones or overlaps (`<` vs `>=`). --- ## 4. Conclusion & Recommended Action Plan The contract exhibits high structural security, clean separation of powers, and strict custody bounds (funds can never go to UASU Inc. or OWNER). Addressing **H-01** (`deposit-reserve` cutoff), **M-01** (receipt parameter binding), and **M-02** (underflow protection) will ensure seamless, failure-proof mainnet deployment.