Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions contracts/liquidator-v1.clar
Original file line number Diff line number Diff line change
Expand Up @@ -492,13 +492,10 @@
))

(define-private (ensure-non-zero-repay-amount (amount uint) (collateral-price uint))
(if (<= collateral-price u0)
;; if collateral price is zero, we don't care about repay amount, anything is accepted
(if (> amount u0)
SUCCESS
;; if not zero, then amount must be > 0
(if (<= amount u0) ERR-NON-ZERO-REPAY-AMOUNT SUCCESS)
ERR-NON-ZERO-REPAY-AMOUNT
)

)

(define-private (socialize-bad-debt (bad-debt bool) (user principal))
Expand Down
4 changes: 2 additions & 2 deletions contracts/modules/linear-kinked-ir-utility.clar
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
(ir-slope-1 uint) (ir-slope-2 uint) (utilization-kink uint) (base-ir uint)
)
(let (
(elapsed-block-time (- time-now last-accrued-block-time))
(elapsed-block-time (if (> time-now last-accrued-block-time) (- time-now last-accrued-block-time) u0))
(premature-return (asserts!
(not (is-eq u0 elapsed-block-time))
(ok {
Expand Down Expand Up @@ -67,7 +67,7 @@
;; PRIVATE HELPER FUNCTIONS
;; total-assets and open-interest are fixed to u8 precision
(define-private (utilization-calc (total-assets uint) (open-interest uint))
(if (> (+ total-assets open-interest) u0) (/ (* open-interest one-12) total-assets) u0)
(if (> total-assets u0) (/ (* open-interest one-12) total-assets) u0)
)

(define-private (get-ir (total-assets uint) (open-interest uint) (ir-slope-1 uint) (ir-slope-2 uint) (utilization-kink uint) (base-ir uint))
Expand Down
4 changes: 2 additions & 2 deletions contracts/modules/linear-kinked-ir-v1.clar
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
(define-read-only (accrue-interest (last-accrued-block-time uint) (lp-open-interest uint) (staked-open-interest uint) (staking-reward-percentage uint) (protocol-open-interest uint) (protocol-reserve-percentage uint) (total-assets uint))
(let (
(time-now (+ (unwrap-panic (get-stacks-block-info? time (- stacks-block-height u1))) STACKS_BLOCK_TIME))
(elapsed-block-time (- time-now last-accrued-block-time))
(elapsed-block-time (if (> time-now last-accrued-block-time) (- time-now last-accrued-block-time) u0))
(premature-return (asserts!
(not (or (is-eq u0 elapsed-block-time) (not (contract-call? .state-v1 is-interest-accrual-enabled))))
(ok {
Expand Down Expand Up @@ -114,7 +114,7 @@

;; total-assets and open-interest are fixed to u8 precision
(define-read-only (utilization-calc (total-assets uint) (open-interest uint))
(if (> (+ total-assets open-interest) u0) (/ (* open-interest one-12) total-assets) u0)
(if (> total-assets u0) (/ (* open-interest one-12) total-assets) u0)
)

(define-read-only (get-ir (total-assets uint) (open-interest uint))
Expand Down
17 changes: 14 additions & 3 deletions contracts/modules/pyth-adapter-v1.clar
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@
(define-constant ERR-PYTH-PRICE-STALE (err u80002))
(define-constant ERR-INVALID-MAX-CONFIDENCE-RATIO (err u80003))
(define-constant ERR-PRICE-CONFIDENCE-LOW (err u80004))
(define-constant ERR-INVALID-PRICE (err u80005))
(define-constant ERR-INVALID-EXPONENT (err u80006))
(define-constant ERR-INVALID-TIME-DELTA (err u80007))

(define-constant STACKS_BLOCK_TIME (contract-call? .constants-v1 get-stacks-block-time ))
;; Minimum time delta of 1 minute.
(define-constant MINIMUM_TIME_DELTA u60)
;; Maximum time delta of 2 hours.
(define-constant MAXIMUM_TIME_DELTA u7200)
;; Maximum future timestamp tolerance of 60 seconds.
(define-constant FUTURE_TIMESTAMP_TOLERANCE u60)
;; Confidence ratio scaling factor = 100% confidence
(define-constant CONFIDENCE_SCALING_FACTOR u10000)
;; Price scaling factor decimals
Expand Down Expand Up @@ -44,6 +51,8 @@
(define-public (update-time-delta (delta uint))
(begin
(asserts! (is-eq (contract-call? .state-v1 get-governance) contract-caller) ERR-NOT-AUTHORIZED)
(asserts! (>= delta MINIMUM_TIME_DELTA) ERR-INVALID-TIME-DELTA)
(asserts! (<= delta MAXIMUM_TIME_DELTA) ERR-INVALID-TIME-DELTA)
(print {
event-type: "update-time-delta",
old-val: (var-get time-delta),
Expand Down Expand Up @@ -120,22 +129,24 @@
(price-conf (get conf pyth-record))

)
(asserts! (> price 0) ERR-INVALID-PRICE)
(asserts! (and (>= expo -18) (<= expo 18)) ERR-INVALID-EXPONENT)
(asserts! (is-valid timestamp) ERR-PYTH-PRICE-STALE)
(try! (check-confidence (to-uint price) price-conf max-confidence-ratio))
(ok (to-uint (convert-res price expo PRICE_DECIMALS)))
))

(define-private (check-confidence (price uint) (confidence uint) (max-confidence-ratio uint))
(if (or (is-eq u0 price) (<= confidence (/ (* price max-confidence-ratio) CONFIDENCE_SCALING_FACTOR)))
(if (<= confidence (/ (* price max-confidence-ratio) CONFIDENCE_SCALING_FACTOR))
(ok true)
ERR-PRICE-CONFIDENCE-LOW
)
)

(define-private (is-valid (timestamp uint))
(let ((block-timestamp (+ (unwrap-panic (get-stacks-block-info? time (- stacks-block-height u1))) STACKS_BLOCK_TIME)))
(if (>= timestamp block-timestamp)
true
(if (>= timestamp block-timestamp)
(<= timestamp (+ block-timestamp FUTURE_TIMESTAMP_TOLERANCE))
(> timestamp (- block-timestamp (var-get time-delta))))
)
)
Expand Down
4 changes: 4 additions & 0 deletions contracts/test/faucet.clar
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
)
)

(define-read-only (get-block-time)
(+ (unwrap-panic (get-stacks-block-info? time (- stacks-block-height u1))) u5)
)

(define-public (get-mock-eth (amount uint))
(let
(
Expand Down
10 changes: 10 additions & 0 deletions deployments/default.simnet-plan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,43 @@ genesis:
- name: deployer
address: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: faucet
address: STNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2CMMMAZ6
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_1
address: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_2
address: ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_3
address: ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_4
address: ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_5
address: ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_6
address: ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_7
address: ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ
balance: "100000000000000"
sbtc-balance: "1000000000"
- name: wallet_8
address: ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP
balance: "100000000000000"
sbtc-balance: "1000000000"
contracts:
- costs
- pox
Expand Down
2 changes: 1 addition & 1 deletion tests/borrower.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const btc_collateral_contract = contractPrincipalCV(deployer, "mock-btc");
describe("borrower tests", () => {
beforeEach(async () => {
init_pyth(deployer);
set_pyth_time_delta(100000, deployer);
set_pyth_time_delta(7200, deployer);
set_allowed_contracts(deployer);
set_asset_cap(deployer, 10000000000000n); // 100k USDC
initialize_ir(deployer);
Expand Down
7 changes: 6 additions & 1 deletion tests/borrower_flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const borrower1 = accounts.get("wallet_2")!;
describe("Borrower User flow tests", () => {
beforeEach(async () => {
init_pyth(deployer);
set_pyth_time_delta(100000, deployer);
set_pyth_time_delta(7200, deployer);
set_allowed_contracts(deployer);
set_asset_cap(deployer, 10000000000000n); // 100k USDC
initialize_ir(deployer);
Expand Down Expand Up @@ -63,6 +63,11 @@ describe("Borrower User flow tests", () => {
// accrue interest
simnet.mineEmptyBlocks(5);

// refresh prices after mining blocks
await set_price("mock-usdc", 1n, deployer);
await set_price("mock-btc", 10n, deployer);
await set_price("mock-eth", 1n, deployer);

// repay
mint_token("mock-usdc", 693, borrower1);
let repay = simnet.callPublicFn(
Expand Down
8 changes: 6 additions & 2 deletions tests/governance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ function execute_proposal_failed(response: any, error: any) {
describe("governance tests", () => {
beforeEach(async () => {
init_pyth(deployer);
await set_pyth_time_delta(100000000, deployer);
await set_pyth_time_delta(7200, deployer);
set_allowed_contracts(deployer);
set_asset_cap(deployer, 10000000000000n); // 100k USDC
initialize_ir(deployer);
Expand Down Expand Up @@ -205,6 +205,10 @@ describe("governance tests", () => {
expect(response.result.type).toBe(ClarityType.ResponseOk);
execute_proposal(response);

// refresh prices after mining blocks for governance proposals
await set_price("mock-usdc", 1n, deployer);
await set_price("mock-btc", 1n, deployer);

remove_collateral("mock-btc", 1000, deployer, governance_account);
});

Expand Down Expand Up @@ -1312,7 +1316,7 @@ describe("governance tests", () => {
[],
deployer
);
expect(response.result).toEqual(Cl.uint(100000000));
expect(response.result).toEqual(Cl.uint(7200));

response = simnet.callPublicFn(
"governance-v1",
Expand Down
48 changes: 45 additions & 3 deletions tests/liquidation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const flashLoanCallbackContract = Cl.contractPrincipal(
describe("liquidation tests", () => {
beforeEach(async () => {
init_pyth(deployer);
set_pyth_time_delta(100000, deployer);
set_pyth_time_delta(7200, deployer);
set_allowed_contracts(deployer);
set_asset_cap(deployer, 10000000000000n); // 100k USDC
initialize_ir(deployer);
Expand Down Expand Up @@ -987,7 +987,7 @@ describe("liquidation tests", () => {
expect(liquidate.result).toBeOk(Cl.bool(true));
});

it("should liquidate correctly a collateral with price 0", async () => {
it("should reject liquidation when collateral price is 0 (H-1 fix)", async () => {
mint_token("mock-usdc", 100000000000, depositor);
deposit(100000000000, depositor);

Expand All @@ -1006,6 +1006,9 @@ describe("liquidation tests", () => {

await set_price("mock-btc", 0n, deployer);

// With H-1 fix, zero price is rejected at pyth-adapter level.
// read-price returns ERR-INVALID-PRICE (u80005), which propagates unchanged via try! through
// account-health -> check-account-unhealthy -> get-liquidate-params -> get-liquidation-info.
let liquidate = simnet.callPublicFn(
"liquidator-v1",
"liquidate-collateral",
Expand All @@ -1018,7 +1021,7 @@ describe("liquidation tests", () => {
],
depositor
);
expect(liquidate.result).toBeOk(Cl.bool(true));
expect(liquidate.result).toBeErr(Cl.uint(80005)); // ERR-INVALID-PRICE (from pyth-adapter, propagated via try!)
});

it("test liquidation buffer", async () => {
Expand Down Expand Up @@ -1532,4 +1535,43 @@ describe("liquidation tests", () => {
// mock liquidator should have all the btc collateral
expectUserBTCBalance(flashLoanCallbackContract, 20000000000n, deployer);
});

it("zero repay amount rejected regardless of collateral price (H-1 defense-in-depth)", async () => {
mint_token("mock-usdc", 100000000000, depositor);
deposit(100000000000, depositor);

update_supported_collateral(
"mock-btc",
90000000,
95000000,
5000000,
8,
deployer
);
mint_token("mock-btc", 100000000000, borrower1);
add_collateral("mock-btc", 20000000000, deployer, borrower1);

borrow(18000000000, borrower1);

// Drop price to make position unhealthy
await set_price("mock-btc", 0n, deployer);

// Attempt liquidation with both zero repay amount and zero collateral price.
// The zero price is caught first at the oracle level (H-1 primary fix),
// before ensure-non-zero-repay-amount is reached.
let liquidate = simnet.callPublicFn(
"liquidator-v1",
"liquidate-collateral",
[
Cl.none(),
btc_collateral_contract,
Cl.principal(borrower1),
Cl.uint(0),
Cl.uint(0),
],
depositor
);
// Zero price is caught first at oracle level (H-1 primary fix)
expect(liquidate.result).toBeErr(Cl.uint(80005)); // ERR-INVALID-PRICE (from pyth-adapter, propagated via try!)
});
});
2 changes: 1 addition & 1 deletion tests/liquidity_provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const deployer = accounts.get("deployer")!;
describe("liquidity-provider tests", () => {
beforeEach(async () => {
init_pyth(deployer);
set_pyth_time_delta(100000, deployer);
set_pyth_time_delta(7200, deployer);
set_allowed_contracts(deployer);
set_asset_cap(deployer, 10000000000000n); // 100k USDC
initialize_ir(deployer);
Expand Down
2 changes: 1 addition & 1 deletion tests/liquidity_provider_flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const borrower1 = accounts.get("wallet_2")!;
describe("LP User flow tests", () => {
beforeEach(async () => {
init_pyth(deployer);
set_pyth_time_delta(100000, deployer);
set_pyth_time_delta(7200, deployer);
set_allowed_contracts(deployer);
set_asset_cap(deployer, 10000000000000n); // 100k USDC
initialize_ir(deployer);
Expand Down
15 changes: 15 additions & 0 deletions tests/modules/linear-kinked-ir.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,21 @@ describe("linear kinked interest rate module tests", () => {
);
});

it("utilization with zero total-assets returns 0 (H-2 div-by-zero guard)", () => {
const totalAssets = Cl.uint(0);
const openInterest = Cl.uint(1000);

const args = [totalAssets, openInterest];
const ur = simnet.callReadOnlyFn(
"linear-kinked-ir-v1",
"utilization-calc",
args,
address1
);

expect(ur.result).toStrictEqual(Cl.uint(0));
});

it("utilization should be 0 when 0 open interest", () => {
const totalAssets = Cl.uint(50000000000); // 500 * 10^8 = 500 usd
const openInterest = Cl.uint(0);
Expand Down
Loading