From 8700ae8779d9c61ac572494dc150c0182c3bc59a Mon Sep 17 00:00:00 2001 From: hackercf <140406910+hackercf@users.noreply.github.com> Date: Wed, 18 Mar 2026 19:28:34 +0530 Subject: [PATCH 01/14] fix: replace inverted assert with explicit conditional in socialize-bad-debt (L-1) The old `asserts!` pattern abused the error branch to execute the slash-total-staked-lp-tokens call. Replace with an explicit `if`/`try!` conditional for clarity and proper error propagation. --- contracts/liquidator-v1.clar | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/liquidator-v1.clar b/contracts/liquidator-v1.clar index 347e772..d426103 100644 --- a/contracts/liquidator-v1.clar +++ b/contracts/liquidator-v1.clar @@ -549,7 +549,7 @@ updated-total-borrowed-amount: updated-total-borrowed-amount, burned-staking-lp-tokens: burned-staking-lp-tokens }) - (asserts! (<= burned-staking-lp-tokens u0) (contract-call? .staking-v1 slash-total-staked-lp-tokens burned-staking-lp-tokens)) + (if (> burned-staking-lp-tokens u0) (try! (contract-call? .staking-v1 slash-total-staked-lp-tokens burned-staking-lp-tokens)) true) SUCCESS )) ) From 89117d5d48274d4c97fc4eac1369de83c56c081f Mon Sep 17 00:00:00 2001 From: hackercf <140406910+hackercf@users.noreply.github.com> Date: Wed, 25 Mar 2026 17:00:31 +0530 Subject: [PATCH 02/14] fix: use safe-sub for denominator in calculate-repayment-info (L-3) Prevents potential underflow when the liquidation discount times collateral LTV exceeds the scaling factor. --- contracts/liquidator-v1.clar | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/liquidator-v1.clar b/contracts/liquidator-v1.clar index d426103..2bc463f 100644 --- a/contracts/liquidator-v1.clar +++ b/contracts/liquidator-v1.clar @@ -369,7 +369,7 @@ ) (let ( - (denominator (- + (denominator (contract-call? .math-v1 safe-sub SCALING-FACTOR (try! (safe-div (* (+ SCALING-FACTOR liquidation-discount) collateral-liquid-ltv) SCALING-FACTOR)) )) From 293779b8c59c151419c0a7e42593537df39ea3d6 Mon Sep 17 00:00:00 2001 From: hackercf <140406910+hackercf@users.noreply.github.com> Date: Wed, 25 Mar 2026 17:00:53 +0530 Subject: [PATCH 03/14] fix: absorb rounding dust in borrower interest split (L-4) Compute staked-part as the remainder (interest - lp - protocol) instead of a third independent division, so rounding dust doesn't leak. --- contracts/borrower-v1.clar | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/borrower-v1.clar b/contracts/borrower-v1.clar index 718d96e..c75af64 100644 --- a/contracts/borrower-v1.clar +++ b/contracts/borrower-v1.clar @@ -97,7 +97,7 @@ (lp-open-interest-without-principal (contract-call? .math-v1 safe-sub (get lp-open-interest interest-params) total-borrowed-amount)) (lp-part (contract-call? .math-v1 safe-div (* interest-part lp-open-interest-without-principal) open-interest-without-principal)) (protocol-part (contract-call? .math-v1 safe-div (* interest-part (get protocol-open-interest interest-params)) open-interest-without-principal)) - (staked-part (contract-call? .math-v1 safe-div (* interest-part (get staked-open-interest interest-params)) open-interest-without-principal)) + (staked-part (contract-call? .math-v1 safe-sub interest-part (+ lp-part protocol-part))) (asset-params (contract-call? .state-v1 get-lp-params)) (staked-lp-tokens (contract-call? .math-v1 convert-to-shares asset-params staked-part false)) (total-user-debt-shares (unwrap! (contract-call? .math-v1 sub (get debt-shares position) shares) ERR-NOT-ENOUGH-SHARES)) From 0d535e771dd6d815349486362d155263c0e160d4 Mon Sep 17 00:00:00 2001 From: hackercf <140406910+hackercf@users.noreply.github.com> Date: Wed, 25 Mar 2026 17:01:12 +0530 Subject: [PATCH 04/14] fix: eliminate double division precision loss in get-rt-by-block (L-6) The old formula divided by one-12 twice, losing precision. Simplified to a single division by seconds-in-year since rate is already 12-fixed. --- contracts/modules/linear-kinked-ir-v1.clar | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/modules/linear-kinked-ir-v1.clar b/contracts/modules/linear-kinked-ir-v1.clar index da8bcef..dc9ee0b 100644 --- a/contracts/modules/linear-kinked-ir-v1.clar +++ b/contracts/modules/linear-kinked-ir-v1.clar @@ -181,7 +181,7 @@ ;; rate in 12-fixed ;; n-blocks (define-read-only (get-rt-by-block (rate uint) (elapsed-block-time uint)) - (/ (* rate (/ (* elapsed-block-time one-12) seconds-in-year)) one-12) + (/ (* rate elapsed-block-time) seconds-in-year) ) ;; taylor series expansion to the 6th degree to estimate e^x From eb30da8156fae354537a518895bbb48037bf2690 Mon Sep 17 00:00:00 2001 From: hackercf <140406910+hackercf@users.noreply.github.com> Date: Wed, 25 Mar 2026 17:01:50 +0530 Subject: [PATCH 05/14] fix: cap taylor-6 input at 2x to prevent series divergence (L-7) The 6th-degree Taylor expansion of e^x diverges for large inputs. Cap rt at MAX-TAYLOR-INPUT (2x in 12-fixed) and return an explicit error instead of silently producing wrong results. --- contracts/modules/linear-kinked-ir-v1.clar | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/contracts/modules/linear-kinked-ir-v1.clar b/contracts/modules/linear-kinked-ir-v1.clar index dc9ee0b..7387780 100644 --- a/contracts/modules/linear-kinked-ir-v1.clar +++ b/contracts/modules/linear-kinked-ir-v1.clar @@ -10,6 +10,7 @@ (define-constant ERR-NOT-INITIALIZED (err u70002)) (define-constant ERR-NOT-GOVERNANCE (err u70003)) (define-constant ERR-INVALID-UTILIZATION-KINK (err u70004)) +(define-constant ERR-TAYLOR-INPUT-TOO-LARGE (err u70006)) ;; CONSTANTS (define-constant one-8 u100000000) @@ -20,6 +21,7 @@ (define-constant fact_5 u120000000000000) (define-constant fact_6 u720000000000000) (define-constant seconds-in-year u31536000) +(define-constant MAX-TAYLOR-INPUT u2000000000000) (define-constant STACKS_BLOCK_TIME (contract-call? .constants-v1 get-stacks-block-time )) ;; DATA-VARS @@ -126,7 +128,10 @@ (define-read-only (compounded-interest (current-interest-rate uint) (elapsed-block-time uint)) (begin (asserts! (var-get is-initialized) ERR-NOT-INITIALIZED) - (ok (taylor-6 (get-rt-by-block current-interest-rate elapsed-block-time))) + (let ((rt (get-rt-by-block current-interest-rate elapsed-block-time))) + (asserts! (<= rt MAX-TAYLOR-INPUT) ERR-TAYLOR-INPUT-TOO-LARGE) + (ok (taylor-6 rt)) + ) )) (define-read-only (get-ir-params) From b593dae6dd387903350c438b5ff770ddeaa92da8 Mon Sep 17 00:00:00 2001 From: hackercf <140406910+hackercf@users.noreply.github.com> Date: Wed, 25 Mar 2026 17:02:08 +0530 Subject: [PATCH 06/14] fix: use safe-sub for slash subtraction in staking (L-10) Prevents potential underflow when slashing staked LP tokens if rounding causes active-staked-lp-tokens-to-slash to exceed the total. --- contracts/staking-v1.clar | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/staking-v1.clar b/contracts/staking-v1.clar index 4f069c7..bdb78ad 100644 --- a/contracts/staking-v1.clar +++ b/contracts/staking-v1.clar @@ -208,7 +208,7 @@ (active-staked-lp-tokens-to-slash (- lp-tokens withdrawal-lp-tokens-to-slash)) ) (try! (contract-call? .state-v1 is-allowed-contract contract-caller)) - (var-set total-lp-tokens-staked (- (var-get total-lp-tokens-staked) active-staked-lp-tokens-to-slash)) + (var-set total-lp-tokens-staked (contract-call? .math-v1 safe-sub (var-get total-lp-tokens-staked) active-staked-lp-tokens-to-slash)) (if (is-eq withdrawal-lp-tokens withdrawal-lp-tokens-to-slash) (var-set unfinalized-withdrawals { lp-tokens: u0, From a16d132cfcfec21f2fd9fbfa9f05c911936697aa Mon Sep 17 00:00:00 2001 From: hackercf <140406910+hackercf@users.noreply.github.com> Date: Wed, 25 Mar 2026 17:02:48 +0530 Subject: [PATCH 07/14] fix: replace unwrap-panic with proper error handling in withdrawal-caps (L-11) get-time-now now returns (response uint uint) with ERR-BLOCK-INFO instead of panicking. All callers updated to propagate via try!. --- contracts/modules/withdrawal-caps-v1.clar | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/contracts/modules/withdrawal-caps-v1.clar b/contracts/modules/withdrawal-caps-v1.clar index 91cc3a7..36d686d 100644 --- a/contracts/modules/withdrawal-caps-v1.clar +++ b/contracts/modules/withdrawal-caps-v1.clar @@ -20,6 +20,7 @@ (define-constant ERR-INVALID-CAP-FACTOR (err u120005)) (define-constant ERR-NOT-AUTHORIZED (err u120006)) (define-constant ERR-SYNC-FAILED (err u120007)) +(define-constant ERR-BLOCK-INFO (err u120008)) ;; VARIABLES @@ -67,7 +68,7 @@ ;; PRIVATE FUNCTIONS (define-private (get-time-now) - (unwrap-panic (get-stacks-block-info? time (- stacks-block-height u1))) + (ok (unwrap! (get-stacks-block-info? time (- stacks-block-height u1)) ERR-BLOCK-INFO)) ) (define-private (refill-bucket-amount (last-updated-at uint) (time-now uint) (max-bucket uint) (current-bucket uint) (inflow uint)) @@ -94,7 +95,7 @@ (define-private (sync-lp-bucket (inflow uint)) (let ( - (time-now (get-time-now)) + (time-now (try! (get-time-now))) (last-ts (var-get last-lp-bucket-update)) (total-liquidity (unwrap! (contract-call? .mock-usdc get-balance .state-v1) ERR-FAILED-TO-GET-BALANCE)) (max-lp-bucket (/ (* total-liquidity (var-get lp-cap-factor)) SCALING-FACTOR)) @@ -120,7 +121,7 @@ (define-private (sync-debt-bucket (inflow uint)) (let ( - (time-now (get-time-now)) + (time-now (try! (get-time-now))) (last-ts (var-get last-debt-bucket-update)) (total-liquidity (contract-call? .state-v1 get-borrowable-balance)) (max-debt-bucket (/ (* total-liquidity (var-get debt-cap-factor)) SCALING-FACTOR)) @@ -145,7 +146,7 @@ (define-private (sync-collateral-bucket (collateral ) (inflow uint)) (let ( - (time-now (get-time-now)) + (time-now (try! (get-time-now))) (collateral-token (contract-of collateral)) (last-ts (default-to u0 (map-get? last-collateral-bucket-update collateral-token))) (total-liquidity (unwrap! (contract-call? collateral get-balance .state-v1) ERR-FAILED-TO-GET-BALANCE)) From 628d57a5f94f759822b2ffce32a28c6dc04c1810 Mon Sep 17 00:00:00 2001 From: hackercf <140406910+hackercf@users.noreply.github.com> Date: Wed, 25 Mar 2026 17:03:20 +0530 Subject: [PATCH 08/14] fix: absorb rounding dust in liquidator interest split (L-18) Compute staked-part as remainder (interest - lp - protocol) in both execute-liquidation and socialize-bad-debt paths, matching the L-4 fix in borrower-v1. --- contracts/liquidator-v1.clar | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/liquidator-v1.clar b/contracts/liquidator-v1.clar index 2bc463f..e8350e8 100644 --- a/contracts/liquidator-v1.clar +++ b/contracts/liquidator-v1.clar @@ -283,7 +283,7 @@ ;; calculate liquidity provider and protocol debt repaid (lp-part (contract-call? .math-v1 safe-div (* interest-part lp-open-interest-without-principal) open-interest-without-principal)) (protocol-part (contract-call? .math-v1 safe-div (* interest-part (get protocol-open-interest open-interest-info)) open-interest-without-principal)) - (staked-part (contract-call? .math-v1 safe-div (* interest-part (get staked-open-interest open-interest-info)) open-interest-without-principal)) + (staked-part (contract-call? .math-v1 safe-sub interest-part (+ lp-part protocol-part))) (asset-params (contract-call? .state-v1 get-lp-params)) (staked-lp-tokens (contract-call? .math-v1 convert-to-shares asset-params staked-part false)) (updated-borrowed-amount (contract-call? .math-v1 safe-sub user-borrowed-amount principal-part)) @@ -532,9 +532,10 @@ (interest-part (get interest-part interest-portion)) (open-interest-without-principal (contract-call? .math-v1 safe-sub total-open-interest total-borrowed-amount)) (lp-open-interest-without-principal (contract-call? .math-v1 safe-sub lp-open-interest-val total-borrowed-amount)) - (lp-part (+ principal-part (contract-call? .math-v1 safe-div (* interest-part lp-open-interest-without-principal) open-interest-without-principal))) + (lp-interest-part (contract-call? .math-v1 safe-div (* interest-part lp-open-interest-without-principal) open-interest-without-principal)) (protocol-part (contract-call? .math-v1 safe-div (* interest-part protocol-open-interest-val) open-interest-without-principal)) - (staked-part (contract-call? .math-v1 safe-div (* interest-part staked-open-interest-val) open-interest-without-principal)) + (staked-part (contract-call? .math-v1 safe-sub interest-part (+ lp-interest-part protocol-part))) + (lp-part (+ principal-part lp-interest-part)) (updated-total-borrowed-amount (contract-call? .math-v1 safe-sub total-borrowed-amount principal-part)) (staked-lp-tokens (contract-call? .staking-v1 get-total-staked-lp-tokens)) (burned-staking-lp-tokens (try! (contract-call? .state-v1 socialize-user-bad-debt user remaining-debt lp-part staked-part protocol-part updated-total-borrowed-amount .staking-v1 staked-lp-tokens))) From 661422998f6c2f35447acf039458590f94305cc3 Mon Sep 17 00:00:00 2001 From: hackercf <140406910+hackercf@users.noreply.github.com> Date: Wed, 25 Mar 2026 17:03:40 +0530 Subject: [PATCH 09/14] fix: safe subtraction in withdrawal cap decay calculation (L-23) Guard against underflow when time-now <= last-updated-at by returning elapsed=0 instead of subtracting. --- contracts/modules/withdrawal-caps-v1.clar | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/modules/withdrawal-caps-v1.clar b/contracts/modules/withdrawal-caps-v1.clar index 36d686d..556a623 100644 --- a/contracts/modules/withdrawal-caps-v1.clar +++ b/contracts/modules/withdrawal-caps-v1.clar @@ -85,7 +85,7 @@ (let ( (extra-bucket-amount (- current-bucket max-bucket)) (decay-window (get-decay-time-window)) - (elapsed (if (is-eq last-updated-at u0) decay-window (- time-now last-updated-at))) + (elapsed (if (is-eq last-updated-at u0) decay-window (if (> time-now last-updated-at) (- time-now last-updated-at) u0))) (decayed-amount (if (>= elapsed decay-window) extra-bucket-amount (/ (* extra-bucket-amount elapsed) decay-window))) (new-bucket (- current-bucket decayed-amount)) ) From 97f1b0d5c630d0d71a59c3264a47c0960cf654f9 Mon Sep 17 00:00:00 2001 From: hackercf <140406910+hackercf@users.noreply.github.com> Date: Wed, 25 Mar 2026 17:04:44 +0530 Subject: [PATCH 10/14] fix: add authorization check on claim-rewards (L-30) Prevent third-party callers from claiming rewards on behalf of another user. Only the user themselves can claim (or caller == user when on-behalf-of is none). --- contracts/lp-incentives-v2.clar | 2 ++ tests/lp-incentives.test.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/contracts/lp-incentives-v2.clar b/contracts/lp-incentives-v2.clar index 8cfad79..807d011 100644 --- a/contracts/lp-incentives-v2.clar +++ b/contracts/lp-incentives-v2.clar @@ -51,6 +51,7 @@ (define-constant ERR-USER-REWARDS-CLAIMED (err u100009)) (define-constant ERR-REWARDS-NOT-CLAIMED (err u100010)) (define-constant ERR-INVALID-SNAPSHOT-TIME (err u100011)) +(define-constant ERR-NOT-AUTHORIZED (err u100012)) ;; Read-only functions (define-read-only (get-epoch-details) @@ -121,6 +122,7 @@ (define-public (claim-rewards (on-behalf-of (optional principal))) (let ( (user (default-to contract-caller on-behalf-of)) + (auth-check (asserts! (or (is-eq contract-caller user) (is-none on-behalf-of)) ERR-NOT-AUTHORIZED)) (rewards (unwrap! (map-get? user-rewards user) ERR-NO-USER-REWARDS)) (reward-amount (get earned-rewards rewards)) ) diff --git a/tests/lp-incentives.test.ts b/tests/lp-incentives.test.ts index d3a8770..ac68f84 100644 --- a/tests/lp-incentives.test.ts +++ b/tests/lp-incentives.test.ts @@ -316,8 +316,8 @@ describe("LP incentives tests", () => { expectUserStxBalance(user2, 0n); expectUnclaimedUserCount(1n); - // claim user2 rewards through user1 - claimRewards(user1, user2); + // claim user2 rewards (L-30: must be claimed by user themselves) + claimRewards(user2); expectUserStxBalance(user1, 60n); expectUserStxBalance(user2, 40n); expectUnclaimedUserCount(0n); @@ -414,9 +414,9 @@ describe("LP incentives tests", () => { // claim user rewards claimRewards(user1); - claimRewards(user1, user2); + claimRewards(user2); claimRewards(user3); - claimRewards(user1, user4); + claimRewards(user4); claimRewards(user5); claimRewards(user6); @@ -552,9 +552,9 @@ describe("LP incentives tests", () => { // claim user rewards claimRewards(user1); - claimRewards(user1, user2); + claimRewards(user2); claimRewards(user3); - claimRewards(user1, user4); + claimRewards(user4); claimRewards(user5); claimRewards(user6); From b01a8532abd9470490ceec87e8dbacedc8911601 Mon Sep 17 00:00:00 2001 From: hackercf <140406910+hackercf@users.noreply.github.com> Date: Wed, 25 Mar 2026 17:05:21 +0530 Subject: [PATCH 11/14] fix: remove compounding rounding bias in taylor series mul (L-34) The half-up rounding in mul() compounds through 6 taylor terms, systematically inflating the result. Switch to truncation to match standard fixed-point convention. --- contracts/modules/linear-kinked-ir-v1.clar | 2 +- tests/modules/linear-kinked-ir.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/modules/linear-kinked-ir-v1.clar b/contracts/modules/linear-kinked-ir-v1.clar index 7387780..7616083 100644 --- a/contracts/modules/linear-kinked-ir-v1.clar +++ b/contracts/modules/linear-kinked-ir-v1.clar @@ -176,7 +176,7 @@ )) (define-private (mul (x uint) (y uint)) - (/ (+ (* x y) (/ one-12 u2)) one-12) + (/ (* x y) one-12) ) (define-private (div (x uint) (y uint)) diff --git a/tests/modules/linear-kinked-ir.test.ts b/tests/modules/linear-kinked-ir.test.ts index 4924f8b..f54ea9c 100644 --- a/tests/modules/linear-kinked-ir.test.ts +++ b/tests/modules/linear-kinked-ir.test.ts @@ -420,7 +420,7 @@ describe("linear kinked interest rate module tests", () => { // (((1 + (0.15/365/24/60/60))^(30000)) - 1) * 100 = 0.01427042449% // ((1 + (0.15/365/24/60/60))^(30000)) * 10^8 = 10,00,14,270.4244869736 - expect(IR.result).toBeOk(Cl.uint(1000142704245)); + expect(IR.result).toBeOk(Cl.uint(1000142704244)); }); it("calculate interest accrual with 10000 elapsed blocks", () => { From 237a4aa734dfb8babc78cf9873ca8805b7c8c17a Mon Sep 17 00:00:00 2001 From: hackercf <140406910+hackercf@users.noreply.github.com> Date: Wed, 25 Mar 2026 17:06:39 +0530 Subject: [PATCH 12/14] fix: require 6-block time-lock between borrowing and liquidation (L-36) Prevent same-block borrow-then-liquidate attacks by enforcing a minimum 6-block gap (~1 minute) between a user's borrow and any liquidation attempt on their position. --- contracts/liquidator-v1.clar | 10 ++++-- tests/borrower_flow.test.ts | 10 ++++++ tests/liquidation.test.ts | 18 ++++++++-- tests/poc-slash-underflow.test.ts | 6 ++++ tests/staking.test.ts | 60 +++++++++++++++++++++++++++++++ 5 files changed, 99 insertions(+), 5 deletions(-) diff --git a/contracts/liquidator-v1.clar b/contracts/liquidator-v1.clar index e8350e8..81c4560 100644 --- a/contracts/liquidator-v1.clar +++ b/contracts/liquidator-v1.clar @@ -25,8 +25,9 @@ (define-constant ERR-INVALID-ORACLE-PRICE (err u30008)) (define-constant ERR-MISSING-MARKET-PRICE (err u30009)) (define-constant ERR-NON-ZERO-REPAY-AMOUNT (err u30010)) +(define-constant ERR-LIQUIDATION-NOT-ALLOWED (err u30011)) -;; PUBLIC FUNCTIONS +;; PUBLIC FUNCTIONS (define-public (batch-liquidate (pyth-price-feed-data (optional (buff 8192))) (collateral ) (batch (list 20 (optional { user: principal, liquidator-repay-amount: uint, @@ -251,7 +252,10 @@ (define-private (execute-liquidation (user principal) (collateral ) (liquidator-repay-amount uint) (min-collateral-expected uint)) (let ( - (collateral-token (contract-of collateral)) + (collateral-token (contract-of collateral)) + ;; L-36: enforce minimum 6-block gap between borrowing and liquidation + (user-position-data (contract-call? .state-v1 get-borrow-repay-params user)) + (position-for-block-check (unwrap! (get user-position user-position-data) ERR-NO-POSITION)) ;; get liquidation info for the user (liquidation-res (try! (get-liquidation-info user collateral liquidator-repay-amount none none none none))) (liquidation-info (get liquidation-info liquidation-res)) @@ -294,6 +298,8 @@ (get collaterals position-data) )) ) + ;; L-36: require at least 6 blocks (~1 min) between borrowing and liquidation + (asserts! (>= stacks-block-height (+ (get borrowed-block position-for-block-check) u6)) ERR-LIQUIDATION-NOT-ALLOWED) (try! (ensure-non-zero-repay-amount liquidator-repay-amount collateral-price)) ;; update state (try! (contract-call? .state-v1 update-liquidate-collateral-state collateral { diff --git a/tests/borrower_flow.test.ts b/tests/borrower_flow.test.ts index 20c5bad..f479c95 100644 --- a/tests/borrower_flow.test.ts +++ b/tests/borrower_flow.test.ts @@ -403,6 +403,11 @@ describe("Borrower User flow tests", () => { ); expect(depositorBalance.result.value.value).toBe(5000n); + simnet.mineEmptyBlocks(6); + // refresh prices after mining blocks + await set_price("mock-usdc", 1n, deployer); + await set_price("mock-btc", 10n, deployer); + await set_price("mock-eth", 65n, deployer); let liquidate = simnet.callPublicFn( "liquidator-v1", "liquidate-collateral", @@ -449,6 +454,11 @@ describe("Borrower User flow tests", () => { ); expect(userDebtShares.result.value.data["debt-shares"].value).toEqual(796n); + simnet.mineEmptyBlocks(6); + // refresh prices after mining blocks + await set_price("mock-usdc", 1n, deployer); + await set_price("mock-btc", 10n, deployer); + await set_price("mock-eth", 65n, deployer); liquidate = simnet.callPublicFn( "liquidator-v1", "liquidate-collateral", diff --git a/tests/liquidation.test.ts b/tests/liquidation.test.ts index 699a416..080d5c0 100644 --- a/tests/liquidation.test.ts +++ b/tests/liquidation.test.ts @@ -369,6 +369,7 @@ describe("liquidation tests", () => { ); mint_token("mock-usdc", 18181818181, depositor); + simnet.mineEmptyBlocks(6); let liquidate = simnet.callPublicFn( "liquidator-v1", "liquidate-collateral", @@ -457,6 +458,7 @@ describe("liquidation tests", () => { borrower1 ).result.value.value; + simnet.mineEmptyBlocks(6); let liquidate = simnet.callPublicFn( "liquidator-v1", "liquidate-collateral", @@ -552,7 +554,7 @@ describe("liquidation tests", () => { let length = block.length; block.forEach((txn, index) => { if (index == length - 1) { - expect(txn.result).toBeErr(Cl.uint(113)); // ERR-LIQUIDATION-NOT-ALLOWED + expect(txn.result).toBeErr(Cl.uint(30011)); // ERR-LIQUIDATION-TIME-LOCK } else { expect(txn.result).toBeOk(Cl.bool(true)); } @@ -627,6 +629,7 @@ describe("liquidation tests", () => { mint_token("mock-usdc", 17777777777, depositor); + simnet.mineEmptyBlocks(6); let liquidate = simnet.callPublicFn( "liquidator-v1", "liquidate-collateral", @@ -759,6 +762,7 @@ describe("liquidation tests", () => { ); mint_token("mock-usdc", 18181818181, depositor); + simnet.mineEmptyBlocks(6); let liquidate = simnet.callPublicFn( "liquidator-v1", "liquidate-collateral", @@ -841,6 +845,7 @@ describe("liquidation tests", () => { ); mint_token("mock-usdc", 39000013190, depositor); + simnet.mineEmptyBlocks(6); let liquidate = simnet.callPublicFn( "liquidator-v1", "liquidate-collateral", @@ -978,6 +983,7 @@ describe("liquidation tests", () => { liquidateData2, ...new Array(18).fill(Cl.none()), ]; + simnet.mineEmptyBlocks(6); let liquidate = simnet.callPublicFn( "liquidator-v1", "batch-liquidate", @@ -1077,6 +1083,7 @@ describe("liquidation tests", () => { ); mint_token("mock-usdc", 30000000000000, depositor); + simnet.mineEmptyBlocks(6); let liquidate = simnet.callPublicFn( "liquidator-v1", "liquidate-collateral", @@ -1098,7 +1105,7 @@ describe("liquidation tests", () => { deployer ); expect(accounthealthRes.result.value.data["position-health"]).toEqual( - Cl.uint(100001972n) + Cl.uint(100001973n) ); }); @@ -1152,6 +1159,7 @@ describe("liquidation tests", () => { ); mint_token("mock-usdc", 18181818181, depositor); + simnet.mineEmptyBlocks(6); let liquidate = simnet.callPublicFn( "liquidator-v1", "liquidate-collateral", @@ -1173,7 +1181,7 @@ describe("liquidation tests", () => { deployer ); expect(accounthealthRes.result.value.data["position-health"]).toEqual( - Cl.uint(100000002n) + Cl.uint(100000000n) ); }); @@ -1227,6 +1235,7 @@ describe("liquidation tests", () => { ); mint_token("mock-usdc", 18181818181, depositor); + simnet.mineEmptyBlocks(6); let liquidate = simnet.callPublicFn( "liquidator-v1", "liquidate-collateral", @@ -1302,6 +1311,7 @@ describe("liquidation tests", () => { ); mint_token("mock-usdc", 18181818181, depositor); + simnet.mineEmptyBlocks(6); let liquidate = simnet.callPublicFn( "liquidator-v1", "liquidate-collateral", @@ -1377,6 +1387,7 @@ describe("liquidation tests", () => { ); mint_token("mock-usdc", 18181818181, depositor); + simnet.mineEmptyBlocks(6); let liquidate = simnet.callPublicFn( "liquidator-v1", "liquidate-collateral", @@ -1492,6 +1503,7 @@ describe("liquidation tests", () => { ); state_set_governance_contract(deployer); + simnet.mineEmptyBlocks(6); let liquidate = simnet.callPublicFn( "mock-liquidator-with-flash-loan", "liquidate-collateral", diff --git a/tests/poc-slash-underflow.test.ts b/tests/poc-slash-underflow.test.ts index baeabba..51128db 100644 --- a/tests/poc-slash-underflow.test.ts +++ b/tests/poc-slash-underflow.test.ts @@ -177,6 +177,12 @@ describe("PoC: slash-total-staked-lp-tokens underflow", () => { // ──────────────────────────────────────────────────────────── mint_token("mock-usdc", 20_000_000_000, liquidator); + // Advance past the 6-block time-lock between borrowing and liquidation + simnet.mineEmptyBlocks(6); + // refresh prices after mining blocks + await set_price("mock-usdc", 1n, deployer); + await set_price("mock-btc", 94n, deployer); + // After fix, liquidation should succeed (bad debt socialized correctly) const result = simnet.callPublicFn( "liquidator-v1", diff --git a/tests/staking.test.ts b/tests/staking.test.ts index 80d8fa7..4a52436 100644 --- a/tests/staking.test.ts +++ b/tests/staking.test.ts @@ -649,6 +649,11 @@ describe("staking tests", () => { ); expect(depositorBalance.result.value.value).toBe(5000n); + simnet.mineEmptyBlocks(6); + // refresh prices after mining blocks + await set_price("mock-usdc", 1n, deployer); + await set_price("mock-btc", 10n, deployer); + await set_price("mock-eth", 65n, deployer); let liquidate = simnet.callPublicFn( "liquidator-v1", "liquidate-collateral", @@ -705,6 +710,11 @@ describe("staking tests", () => { ); expect(depositToReserve.result).toBeOk(Cl.bool(true)); + simnet.mineEmptyBlocks(6); + // refresh prices after mining blocks + await set_price("mock-usdc", 1n, deployer); + await set_price("mock-btc", 10n, deployer); + await set_price("mock-eth", 65n, deployer); liquidate = simnet.callPublicFn( "liquidator-v1", "liquidate-collateral", @@ -903,6 +913,11 @@ describe("staking tests", () => { ); expect(depositorBalance.result.value.value).toBe(5000n); + simnet.mineEmptyBlocks(6); + // refresh prices after mining blocks + await set_price("mock-usdc", 1n, deployer); + await set_price("mock-btc", 10n, deployer); + await set_price("mock-eth", 65n, deployer); let liquidate = simnet.callPublicFn( "liquidator-v1", "liquidate-collateral", @@ -972,6 +987,11 @@ describe("staking tests", () => { ); expect(totalLpSupply.result).toBeOk(Cl.uint(2000)); + simnet.mineEmptyBlocks(6); + // refresh prices after mining blocks + await set_price("mock-usdc", 1n, deployer); + await set_price("mock-btc", 10n, deployer); + await set_price("mock-eth", 65n, deployer); liquidate = simnet.callPublicFn( "liquidator-v1", "liquidate-collateral", @@ -1195,6 +1215,11 @@ describe("staking tests", () => { ); expect(depositorBalance.result.value.value).toBe(5000n); + simnet.mineEmptyBlocks(6); + // refresh prices after mining blocks + await set_price("mock-usdc", 1n, deployer); + await set_price("mock-btc", 10n, deployer); + await set_price("mock-eth", 65n, deployer); let liquidate = simnet.callPublicFn( "liquidator-v1", "liquidate-collateral", @@ -1264,6 +1289,11 @@ describe("staking tests", () => { ); expect(totalLpSupply.result).toBeOk(Cl.uint(2000)); + simnet.mineEmptyBlocks(6); + // refresh prices after mining blocks + await set_price("mock-usdc", 1n, deployer); + await set_price("mock-btc", 10n, deployer); + await set_price("mock-eth", 65n, deployer); liquidate = simnet.callPublicFn( "liquidator-v1", "liquidate-collateral", @@ -1449,6 +1479,11 @@ describe("staking tests", () => { mint_token("mock-usdc", 5000, depositor1); // first liquidation + simnet.mineEmptyBlocks(6); + // refresh prices after mining blocks + await set_price("mock-usdc", 1n, deployer); + await set_price("mock-btc", 10n, deployer); + await set_price("mock-eth", 65n, deployer); let liquidate = simnet.callPublicFn( "liquidator-v1", "liquidate-collateral", @@ -1474,6 +1509,11 @@ describe("staking tests", () => { expect(depositToReserve.result).toBeOk(Cl.bool(true)); // second liquidation triggers full wipe-out + simnet.mineEmptyBlocks(6); + // refresh prices after mining blocks + await set_price("mock-usdc", 1n, deployer); + await set_price("mock-btc", 10n, deployer); + await set_price("mock-eth", 65n, deployer); liquidate = simnet.callPublicFn( "liquidator-v1", "liquidate-collateral", @@ -1580,6 +1620,11 @@ describe("staking tests", () => { mint_token("mock-usdc", 5000, depositor1); // first liquidation + simnet.mineEmptyBlocks(6); + // refresh prices after mining blocks + await set_price("mock-usdc", 1n, deployer); + await set_price("mock-btc", 10n, deployer); + await set_price("mock-eth", 65n, deployer); let liquidate = simnet.callPublicFn( "liquidator-v1", "liquidate-collateral", @@ -1605,6 +1650,11 @@ describe("staking tests", () => { expect(depositToReserve.result).toBeOk(Cl.bool(true)); // second liquidation triggers full wipe-out + simnet.mineEmptyBlocks(6); + // refresh prices after mining blocks + await set_price("mock-usdc", 1n, deployer); + await set_price("mock-btc", 10n, deployer); + await set_price("mock-eth", 65n, deployer); liquidate = simnet.callPublicFn( "liquidator-v1", "liquidate-collateral", @@ -1769,6 +1819,11 @@ describe("staking tests", () => { ); expect(depositorBalance.result.value.value).toBe(5000n); + simnet.mineEmptyBlocks(6); + // refresh prices after mining blocks + await set_price("mock-usdc", 1n, deployer); + await set_price("mock-btc", 10n, deployer); + await set_price("mock-eth", 65n, deployer); let liquidate = simnet.callPublicFn( "liquidator-v1", "liquidate-collateral", @@ -1846,6 +1901,11 @@ describe("staking tests", () => { ); expect(stakingStatus.result).toStrictEqual(Cl.bool(true)); + simnet.mineEmptyBlocks(6); + // refresh prices after mining blocks + await set_price("mock-usdc", 1n, deployer); + await set_price("mock-btc", 10n, deployer); + await set_price("mock-eth", 65n, deployer); liquidate = simnet.callPublicFn( "liquidator-v1", "liquidate-collateral", From 345584cc4c6a4717c3d42e2699410ac4acd3f8f3 Mon Sep 17 00:00:00 2001 From: hackercf <140406910+hackercf@users.noreply.github.com> Date: Wed, 25 Mar 2026 17:06:57 +0530 Subject: [PATCH 13/14] fix: widen post-liquidation health buffer from 0.5% to 2.0% (M-5) The 0.5% buffer was too tight for multi-collateral positions where rounding across multiple collateral types could push the post-liquidation health ratio above the threshold, causing the assert to revert. --- contracts/liquidator-v1.clar | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/liquidator-v1.clar b/contracts/liquidator-v1.clar index 81c4560..4f77d3c 100644 --- a/contracts/liquidator-v1.clar +++ b/contracts/liquidator-v1.clar @@ -10,8 +10,8 @@ (define-constant PRICE-SCALING-FACTOR (contract-call? .constants-v2 get-price-scaling-factor)) ;; Must have the same precision as SCALING-FACTOR (define-constant MINIMUM_HEALTH_RATIO u100000000) -;; Liquidation buffer of 0.50% -(define-constant LIQUIDATION-BUFFER u500000) +;; Liquidation buffer of 2.00% +(define-constant LIQUIDATION-BUFFER u2000000) ;; ERROR VALUES (define-constant ERR-DIVIDE-BY-ZERO (err u30000)) From 4f9d4228cf8a84e71fd0f32530fdce48ba25976a Mon Sep 17 00:00:00 2001 From: hackercf <140406910+hackercf@users.noreply.github.com> Date: Wed, 25 Mar 2026 17:07:35 +0530 Subject: [PATCH 14/14] fix: add upper bounds on IR slope and base parameters (M-16) Cap ir-slope-1, ir-slope-2, and base-ir at MAX-IR-PARAM to prevent governance from setting values that would cause arithmetic overflow in the interest rate calculation, bricking accrual. --- contracts/modules/linear-kinked-ir-v1.clar | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contracts/modules/linear-kinked-ir-v1.clar b/contracts/modules/linear-kinked-ir-v1.clar index 7616083..cd865c7 100644 --- a/contracts/modules/linear-kinked-ir-v1.clar +++ b/contracts/modules/linear-kinked-ir-v1.clar @@ -10,6 +10,7 @@ (define-constant ERR-NOT-INITIALIZED (err u70002)) (define-constant ERR-NOT-GOVERNANCE (err u70003)) (define-constant ERR-INVALID-UTILIZATION-KINK (err u70004)) +(define-constant ERR-INVALID-IR-PARAMS (err u70005)) (define-constant ERR-TAYLOR-INPUT-TOO-LARGE (err u70006)) ;; CONSTANTS @@ -21,6 +22,7 @@ (define-constant fact_5 u120000000000000) (define-constant fact_6 u720000000000000) (define-constant seconds-in-year u31536000) +(define-constant MAX-IR-PARAM u100000000000000) (define-constant MAX-TAYLOR-INPUT u2000000000000) (define-constant STACKS_BLOCK_TIME (contract-call? .constants-v1 get-stacks-block-time )) @@ -49,6 +51,9 @@ ) (asserts! (< utilization-kink-val one-12) ERR-INVALID-UTILIZATION-KINK) + (asserts! (<= ir-slope-1-val MAX-IR-PARAM) ERR-INVALID-IR-PARAMS) + (asserts! (<= ir-slope-2-val MAX-IR-PARAM) ERR-INVALID-IR-PARAMS) + (asserts! (<= base-ir-val MAX-IR-PARAM) ERR-INVALID-IR-PARAMS) (print { old-ir-slope-1: (var-get ir-slope-1), new-ir-slope-1: ir-slope-1-val,