Skip to content

Commit fdbe262

Browse files
Glory MatthewGlory Matthew
authored andcommitted
Security audit: fix all remaining findings before mainnet deployment
H-01: Fee now transferred to treasury instead of burned - Mint total-owed to receiver; burn only amount; transfer fee to treasury - Supply invariant updated: supply-after == supply-before + fee - New set-treasury / get-treasury admin functions added H-03: All receivers now query fee dynamically from flashstack-core - example-arbitrage-receiver, collateral-swap-receiver, leverage-loop-receiver, liquidation-receiver, yield-optimization-receiver all updated - Eliminates desync when admin changes fee rate L-01: Minimum fee of 1 sat enforced (prevents free flash loans on tiny amounts) - raw-fee computed, then floored to 1 if zero L-02: set-fee now returns ERR-INVALID-AMOUNT (104) on out-of-range fee - Was incorrectly returning ERR-UNAUTHORIZED (102) M-02: block-height is correct for Clarity 2 epoch 2.5 - stacks-block-height not available until epoch 3.0; no change needed M-03: DEX price setters in example-arbitrage-receiver restricted to CONTRACT-OWNER I-02: Deleted incomplete flashstack-core-v2.clar from repo All 86 tests passing
1 parent 987259a commit fdbe262

10 files changed

Lines changed: 84 additions & 71 deletions

contracts/collateral-swap-receiver.clar

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,15 @@
1212
(define-constant ERR-SWAP-FAILED (err u501))
1313
(define-constant ERR-INSUFFICIENT-COLLATERAL (err u502))
1414
(define-constant ERR-REPAYMENT-FAILED (err u503))
15+
(define-constant ERR-FEE-FETCH-FAILED (err u504))
1516

1617
;; Main flash loan execution
1718
(define-public (execute-flash (amount uint) (borrower principal))
1819
(let (
19-
(fee (/ (* amount u5) u10000)) ;; 0.05% FlashStack fee
20+
;; H-03 fix: query fee dynamically from core contract
21+
(fee-bp (unwrap! (contract-call? .flashstack-core get-fee-basis-points) ERR-FEE-FETCH-FAILED))
22+
(raw-fee (/ (* amount fee-bp) u10000))
23+
(fee (if (> raw-fee u0) raw-fee u1))
2024
(total-owed (+ amount fee))
2125
)
2226
;; Verify this is called by FlashStack

contracts/example-arbitrage-receiver.clar

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
;; Error Codes
88
(define-constant ERR-ARBITRAGE-FAILED (err u200))
99
(define-constant ERR-INSUFFICIENT-PROFIT (err u201))
10+
(define-constant ERR-FEE-FETCH-FAILED (err u202))
1011

1112
;; Simulated DEX prices for demo
1213
(define-data-var dex-a-price uint u1000000)
@@ -16,7 +17,10 @@
1617
;; Receiver gets amount + fee, must return amount + fee
1718
(define-public (execute-flash (amount uint) (borrower principal))
1819
(let (
19-
(fee (/ (* amount u5) u10000))
20+
;; H-03 fix: query fee dynamically from core contract
21+
(fee-bp (unwrap! (contract-call? .flashstack-core get-fee-basis-points) ERR-FEE-FETCH-FAILED))
22+
(raw-fee (/ (* amount fee-bp) u10000))
23+
(fee (if (> raw-fee u0) raw-fee u1))
2024
(total-owed (+ amount fee))
2125
)
2226
;; In production: Use the sBTC for arbitrage
@@ -57,16 +61,20 @@
5761
)
5862
)
5963

60-
;; Admin functions for testing
64+
;; Admin-only price setters (M-03 fix: restricted to contract deployer)
65+
(define-constant CONTRACT-OWNER tx-sender)
66+
6167
(define-public (set-dex-a-price (price uint))
6268
(begin
69+
(asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-ARBITRAGE-FAILED)
6370
(asserts! (> price u0) ERR-ARBITRAGE-FAILED)
6471
(ok (var-set dex-a-price price))
6572
)
6673
)
6774

6875
(define-public (set-dex-b-price (price uint))
6976
(begin
77+
(asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-ARBITRAGE-FAILED)
7078
(asserts! (> price u0) ERR-ARBITRAGE-FAILED)
7179
(ok (var-set dex-b-price price))
7280
)

contracts/flashstack-core-v2.clar

Lines changed: 0 additions & 48 deletions
This file was deleted.

contracts/flashstack-core.clar

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,12 @@
3939
;; Whitelist for approved receiver contracts
4040
(define-map approved-receivers principal bool)
4141

42-
;; Per-block volume tracking
42+
;; Per-block volume tracking (uses block-height - M-02 fix)
4343
(define-map block-loan-volume uint uint)
4444

45+
;; Treasury address for collected fees (H-01 fix)
46+
(define-data-var treasury principal tx-sender)
47+
4548
;; Collateral ratio: 300% = 3x leverage max
4649
(define-constant MIN-COLLATERAL-RATIO u300)
4750

@@ -82,7 +85,10 @@
8285
(locked-stx (get-stx-locked borrower))
8386
(min-required (/ (* amount MIN-COLLATERAL-RATIO) u100))
8487
(receiver-principal (contract-of receiver))
85-
(fee (/ (* amount (var-get flash-fee-basis-points)) u10000))
88+
;; L-01 fix: enforce minimum fee of 1 sat to prevent free flash loans
89+
(raw-fee (/ (* amount (var-get flash-fee-basis-points)) u10000))
90+
;; L-01 fix: enforce minimum fee of 1 sat to prevent free flash loans on tiny amounts
91+
(fee (if (> raw-fee u0) raw-fee u1))
8692
(total-owed (+ amount fee))
8793
)
8894
;; ===== SECURITY CHECKS =====
@@ -99,7 +105,7 @@
99105
;; Circuit breaker: Check single loan limit
100106
(asserts! (<= amount (var-get max-single-loan)) ERR-LOAN-TOO-LARGE)
101107

102-
;; Circuit breaker: Check block volume limit
108+
;; Circuit breaker: Check block volume limit (M-02: use block-height)
103109
(let (
104110
(current-block-volume (default-to u0 (map-get? block-loan-volume block-height)))
105111
(new-block-volume (+ current-block-volume amount))
@@ -124,15 +130,27 @@
124130
;; Execute callback
125131
(match (contract-call? receiver execute-flash amount borrower)
126132
success (begin
127-
;; Burn the returned tokens to complete the cycle
128-
(try! (as-contract (contract-call? .sbtc-token burn total-owed tx-sender)))
129-
130-
;; Verify supply invariant: total supply must equal pre-mint level,
131-
;; proving the exact minted amount was burned (not satisfied via pre-existing tokens)
133+
;; H-01 fix: Burn only the principal (amount), not the fee.
134+
;; The receiver returns total-owed to this contract; we burn
135+
;; only amount and transfer the fee to treasury.
136+
(try! (as-contract (contract-call? .sbtc-token burn amount tx-sender)))
137+
138+
;; Transfer fee to treasury (H-01: protocol earns the fee)
139+
(try! (as-contract (contract-call? .sbtc-token transfer
140+
fee
141+
tx-sender
142+
(var-get treasury)
143+
(some 0x464c415348)
144+
)))
145+
146+
;; Verify supply invariant: supply must have decreased by exactly amount
147+
;; (amount burned + fee still exists in treasury, so supply-after = supply-before + fee)
148+
;; The invariant we enforce: supply-after == supply-before + fee
149+
;; This proves amount was burned and fee was not destroyed.
132150
(let (
133151
(supply-after (unwrap! (contract-call? .sbtc-token get-total-supply) ERR-REPAY-FAILED))
134152
)
135-
(asserts! (is-eq supply-after supply-before) ERR-REPAY-FAILED)
153+
(asserts! (is-eq supply-after (+ supply-before fee)) ERR-REPAY-FAILED)
136154

137155
(var-set total-flash-mints (+ (var-get total-flash-mints) u1))
138156
(var-set total-volume (+ (var-get total-volume) amount))
@@ -204,11 +222,23 @@
204222
(define-public (set-fee (new-fee-bp uint))
205223
(begin
206224
(asserts! (is-eq contract-caller (var-get admin)) ERR-UNAUTHORIZED)
207-
(asserts! (<= new-fee-bp u100) ERR-UNAUTHORIZED)
225+
;; L-02 fix: use ERR-INVALID-AMOUNT for fee validation (not ERR-UNAUTHORIZED)
226+
(asserts! (<= new-fee-bp u100) ERR-INVALID-AMOUNT)
208227
(ok (var-set flash-fee-basis-points new-fee-bp))
209228
)
210229
)
211230

231+
(define-public (set-treasury (new-treasury principal))
232+
(begin
233+
(asserts! (is-eq contract-caller (var-get admin)) ERR-UNAUTHORIZED)
234+
(ok (var-set treasury new-treasury))
235+
)
236+
)
237+
238+
(define-read-only (get-treasury)
239+
(ok (var-get treasury))
240+
)
241+
212242
(define-public (set-admin (new-admin principal))
213243
(begin
214244
(asserts! (is-eq contract-caller (var-get admin)) ERR-UNAUTHORIZED)

contracts/leverage-loop-receiver.clar

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,18 @@
1414
(define-constant ERR-DEPOSIT-FAILED (err u503))
1515
(define-constant ERR-BORROW-FAILED (err u504))
1616
(define-constant ERR-REPAYMENT-FAILED (err u505))
17+
(define-constant ERR-FEE-FETCH-FAILED (err u506))
1718

1819
;; Maximum leverage allowed (in basis points, 30000 = 3x)
1920
(define-constant MAX-LEVERAGE-BP u30000)
2021

2122
;; Main flash loan execution for leverage
2223
(define-public (execute-flash (amount uint) (borrower principal))
2324
(let (
24-
(fee (/ (* amount u5) u10000)) ;; 0.05% FlashStack fee
25+
;; H-03 fix: query fee dynamically from core contract
26+
(fee-bp (unwrap! (contract-call? .flashstack-core get-fee-basis-points) ERR-FEE-FETCH-FAILED))
27+
(raw-fee (/ (* amount fee-bp) u10000))
28+
(fee (if (> raw-fee u0) raw-fee u1))
2529
(total-owed (+ amount fee))
2630
)
2731
;; Verify this is called by FlashStack

contracts/liquidation-receiver.clar

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,18 @@
1313
(define-constant ERR-INSUFFICIENT-PROFIT (err u502))
1414
(define-constant ERR-REPAYMENT-FAILED (err u503))
1515
(define-constant ERR-MOCK-ERROR (err u999))
16+
(define-constant ERR-FEE-FETCH-FAILED (err u504))
1617

1718
;; Example liquidation parameters
1819
(define-data-var liquidation-bonus-bp uint u1000) ;; 10% liquidation bonus
1920

2021
;; Main flash loan execution
2122
(define-public (execute-flash (amount uint) (borrower principal))
2223
(let (
23-
(fee (/ (* amount u5) u10000)) ;; 0.05% FlashStack fee
24+
;; H-03 fix: query fee dynamically from core contract
25+
(fee-bp (unwrap! (contract-call? .flashstack-core get-fee-basis-points) ERR-FEE-FETCH-FAILED))
26+
(raw-fee (/ (* amount fee-bp) u10000))
27+
(fee (if (> raw-fee u0) raw-fee u1))
2428
(total-owed (+ amount fee))
2529
(liquidation-bonus (/ (* amount (var-get liquidation-bonus-bp)) u10000))
2630
(expected-profit (- liquidation-bonus fee))

contracts/yield-optimization-receiver.clar

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,15 @@
1313
(define-constant ERR-COMPOUND-FAILED (err u502))
1414
(define-constant ERR-REPAYMENT-FAILED (err u503))
1515
(define-constant ERR-STRATEGY-FAILED (err u504))
16+
(define-constant ERR-FEE-FETCH-FAILED (err u505))
1617

1718
;; Main flash loan execution for yield optimization
1819
(define-public (execute-flash (amount uint) (borrower principal))
1920
(let (
20-
(fee (/ (* amount u5) u10000)) ;; 0.05% FlashStack fee
21+
;; H-03 fix: query fee dynamically from core contract
22+
(fee-bp (unwrap! (contract-call? .flashstack-core get-fee-basis-points) ERR-FEE-FETCH-FAILED))
23+
(raw-fee (/ (* amount fee-bp) u10000))
24+
(fee (if (> raw-fee u0) raw-fee u1))
2125
(total-owed (+ amount fee))
2226
)
2327
;; Verify this is called by FlashStack

tests/flashstack-comprehensive.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ describe("FlashStack Core - Comprehensive Test Suite", () => {
175175
[Cl.uint(101)],
176176
deployer
177177
);
178-
expect(result).toBeErr(Cl.uint(102)); // ERR-UNAUTHORIZED
178+
expect(result).toBeErr(Cl.uint(104)); // ERR-INVALID-AMOUNT (L-02 fix)
179179
});
180180

181181
it("can set fee to maximum (1%)", () => {

tests/flashstack-core_test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,8 @@ describe("FlashStack Core Tests", () => {
159159
[Cl.uint(101)], // Above 1% max (100 basis points)
160160
deployer
161161
);
162-
// Contract returns ERR-UNAUTHORIZED (102) for invalid fee
163-
expect(result).toBeErr(Cl.uint(102));
162+
// L-02 fix: now returns ERR-INVALID-AMOUNT (104) for invalid fee, not ERR-UNAUTHORIZED
163+
expect(result).toBeErr(Cl.uint(104));
164164
});
165165

166166
it("gets protocol statistics", () => {

tests/flashstack-edge-cases.test.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,11 @@ describe("FlashStack - Edge Cases & Integration", () => {
106106
expect(stats["total-fees-collected"].value).toBe(50000n); // 5bp fee
107107
});
108108

109-
it("maintains zero sBTC supply after flash mint (mint-burn cycle)", () => {
109+
it("supply increases by exactly fee after flash mint (H-01: fee kept in treasury)", () => {
110+
const amount = 100000000; // 1 sBTC
111+
const feeBp = 5n;
112+
const expectedFee = (BigInt(amount) * feeBp) / 10000n; // 5000 sats
113+
110114
const { result: supplyBefore } = simnet.callReadOnlyFn(
111115
"sbtc-token",
112116
"get-total-supply",
@@ -117,7 +121,7 @@ describe("FlashStack - Edge Cases & Integration", () => {
117121
simnet.callPublicFn(
118122
"flashstack-core",
119123
"flash-mint",
120-
[Cl.uint(100000000), Cl.principal(`${deployer}.test-receiver`)],
124+
[Cl.uint(amount), Cl.principal(`${deployer}.test-receiver`)],
121125
wallet1
122126
);
123127

@@ -128,8 +132,11 @@ describe("FlashStack - Edge Cases & Integration", () => {
128132
deployer
129133
);
130134

131-
// Supply should be identical — minted tokens were burned
132-
expect(supplyAfter).toBeOk(supplyBefore.value);
135+
// H-01 fix: only principal is burned; fee remains in treasury.
136+
// supply-after = supply-before + fee
137+
const beforeVal = (supplyBefore as any).value.value;
138+
const afterVal = (supplyAfter as any).value.value;
139+
expect(afterVal - beforeVal).toBe(expectedFee);
133140
});
134141
});
135142

0 commit comments

Comments
 (0)