Skip to content

Commit

Permalink
feat: implement debt limit (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
xJonathanLEI committed Apr 11, 2023
1 parent ce32e78 commit f13ff97
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 0 deletions.
7 changes: 7 additions & 0 deletions src/zklend/Market.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,13 @@ func set_treasury{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_pt
return External.set_treasury(new_treasury);
}

@external
func set_debt_limit{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
token: felt, limit: felt
) {
return External.set_debt_limit(token, limit);
}

@external
func transfer_ownership{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
new_owner: felt
Expand Down
3 changes: 3 additions & 0 deletions src/zklend/interfaces/IMarket.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ namespace IMarket {
func set_treasury(new_treasury: felt) {
}

func set_debt_limit(token: felt, limit: felt) {
}

func transfer_ownership(new_owner: felt) {
}

Expand Down
4 changes: 4 additions & 0 deletions src/zklend/internals/Market/events.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ func AccumulatorsSync(token: felt, lending_accumulator: felt, debt_accumulator:
func InterestRatesSync(token: felt, lending_rate: felt, borrowing_rate: felt) {
}

@event
func DebtLimitUpdate(token: felt, limit: felt) {
}

@event
func Deposit(user: felt, token: felt, face_amount: felt) {
}
Expand Down
42 changes: 42 additions & 0 deletions src/zklend/internals/Market/functions.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ from zklend.internals.Market.events import (
TreasuryUpdate,
AccumulatorsSync,
InterestRatesSync,
DebtLimitUpdate,
Deposit,
Withdrawal,
Borrowing,
Expand Down Expand Up @@ -292,6 +293,7 @@ namespace External {
raw_total_debt=0,
flash_loan_fee=flash_loan_fee,
liquidation_bonus=liquidation_bonus,
debt_limit=0,
);
reserves.write(token, new_reserve);

Expand Down Expand Up @@ -335,6 +337,18 @@ namespace External {
return ();
}

func set_debt_limit{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
token: felt, limit: felt
) {
Ownable.assert_only_owner();

Internal.assert_reserve_exists(token);

reserves.write_debt_limit(token, limit);
DebtLimitUpdate.emit(token, limit);
return ();
}

func transfer_ownership{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
new_owner: felt
) {
Expand Down Expand Up @@ -642,6 +656,9 @@ namespace Internal {
abs_delta_raw_total_debt=scaled_down_amount,
);

// Enforces token debt limit
Internal.assert_debt_limit_satisfied(token=token);

Borrowing.emit(caller, token, scaled_down_amount, amount);

// It's easier to post-check collateralization factor
Expand Down Expand Up @@ -1473,6 +1490,31 @@ namespace Internal {
return ();
}

// Checks if the debt limit is satisfied
func assert_debt_limit_satisfied{
syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr
}(token: felt) {
alloc_locals;

let (debt_limit) = reserves.read_debt_limit(token=token);
if (debt_limit == 0) {
// 0 means no limit
return ();
} else {
let (raw_total_debt) = reserves.read_raw_total_debt(token);
local raw_total_debt = raw_total_debt;

let (debt_accumulator) = View.get_debt_accumulator(token);
let scaled_debt = SafeDecimalMath.mul(raw_total_debt, debt_accumulator);

with_attr error_message("Market: debt limit exceeded") {
assert_le_felt(scaled_debt, debt_limit);
}

return ();
}
}

// This function is called to distribute excessive reserve assets to depositors. Such extra
// balance can come from a variety of sources, including direct transfer of tokens into this
// contract. However, in practice, this function is only called right after a flash loan,
Expand Down
33 changes: 33 additions & 0 deletions src/zklend/internals/Market/storage.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ namespace reserves {
let (__storage_var_temp13) = storage_read(address=storage_addr + 13);
// liquidation_bonus
let (__storage_var_temp14) = storage_read(address=storage_addr + 14);
// debt_limit
let (__storage_var_temp15) = storage_read(address=storage_addr + 15);

tempvar syscall_ptr = syscall_ptr;
tempvar pedersen_ptr = pedersen_ptr;
Expand Down Expand Up @@ -100,6 +102,8 @@ namespace reserves {
tempvar __storage_var_temp13: felt = __storage_var_temp13;
// liquidation_bonus
tempvar __storage_var_temp14: felt = __storage_var_temp14;
// debt_limit
tempvar __storage_var_temp15: felt = __storage_var_temp15;

return ([cast(&__storage_var_temp0, Structs.ReserveData*)],);
}
Expand Down Expand Up @@ -139,6 +143,8 @@ namespace reserves {
storage_write(address=storage_addr + 13, value=[cast(&value, felt) + 13]);
// liquidation_bonus
storage_write(address=storage_addr + 14, value=[cast(&value, felt) + 14]);
// debt_limit
storage_write(address=storage_addr + 15, value=[cast(&value, felt) + 15]);

return ();
}
Expand Down Expand Up @@ -239,6 +245,22 @@ namespace reserves {
return (flash_loan_fee=__storage_var_temp13);
}

func read_debt_limit{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
token: felt
) -> (debt_limit: felt) {
let (storage_addr) = addr(token);

// debt_limit
let (__storage_var_temp15) = storage_read(address=storage_addr + 15);

// TODO: check if we really need these
tempvar syscall_ptr = syscall_ptr;
tempvar pedersen_ptr = pedersen_ptr;
tempvar range_check_ptr = range_check_ptr;

return (debt_limit=__storage_var_temp15);
}

func read_interest_rate_model_and_raw_total_debt{
syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr
}(token: felt) -> (interest_rate_model: felt, raw_total_debt: felt) {
Expand Down Expand Up @@ -433,6 +455,17 @@ namespace reserves {
return ();
}

func write_debt_limit{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
token: felt, value: felt
) {
let (storage_addr) = addr(token);

// debt_limit
storage_write(address=storage_addr + 15, value=value);

return ();
}

func write_accumulators{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
token: felt, timestamp: felt, lending_accumulator, debt_accumulator
) {
Expand Down
1 change: 1 addition & 0 deletions src/zklend/internals/Market/structs.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ namespace Structs {
raw_total_debt: felt,
flash_loan_fee: felt,
liquidation_bonus: felt,
debt_limit: felt,
}
}
121 changes: 121 additions & 0 deletions tests/Market_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,127 @@ async def test_borrow_token(setup: Setup):
assert reserve_data.current_borrowing_rate == 51125 * 10**21


@pytest.mark.asyncio
async def test_cannot_exceed_debt_limit(setup: Setup):
# Bob deposits enough TST_B for Alice to borrow
await setup.bob.execute(
[
Call(
setup.token_b.contract_address,
get_selector_from_name("approve"),
[
setup.market.contract_address, # spender
*Uint256.from_int(10_000 * 10**18), # amount
],
),
Call(
setup.market.contract_address,
get_selector_from_name("deposit"),
[
setup.token_b.contract_address, # token
10_000 * 10**18, # amount
],
),
Call(
setup.market.contract_address,
get_selector_from_name("enable_collateral"),
[
setup.token_b.contract_address, # token
],
),
]
)
await setup.alice.execute(
[
Call(
setup.token_a.contract_address,
get_selector_from_name("approve"),
[
setup.market.contract_address, # spender
*Uint256.from_int(1_000 * 10**18), # amount
],
),
Call(
setup.market.contract_address,
get_selector_from_name("deposit"),
[
setup.token_a.contract_address, # token
100 * 10**18, # amount
],
),
Call(
setup.market.contract_address,
get_selector_from_name("enable_collateral"),
[
setup.token_a.contract_address, # token
],
),
]
)

# Debt limit set to 10 TST_B
await setup.alice.execute(
[
Call(
setup.market.contract_address,
get_selector_from_name("set_debt_limit"),
[
setup.token_b.contract_address, # token
10 * 10**18, # limit
],
),
]
)

# Alice can't borrow 11 TST_B
await assert_reverted_with(
setup.alice.execute(
[
Call(
setup.market.contract_address,
get_selector_from_name("borrow"),
[
setup.token_b.contract_address, # token
11 * 10**18, # amount
],
)
]
),
"Market: debt limit exceeded",
)

# Borrowing 10 TST_B is allowed (exactly at limit)
await setup.alice.execute(
[
Call(
setup.market.contract_address,
get_selector_from_name("borrow"),
[
setup.token_b.contract_address, # token
10 * 10**18, # amount
],
)
]
)

# Borrowing anything more is disallowed
await assert_reverted_with(
setup.bob.execute(
[
Call(
setup.market.contract_address,
get_selector_from_name("borrow"),
[
setup.token_b.contract_address, # token
1 * 10**18, # amount
],
)
]
),
"Market: debt limit exceeded",
)


@pytest.mark.asyncio
async def test_rate_changes_on_deposit(setup_with_loan: Setup):
setup = setup_with_loan
Expand Down

0 comments on commit f13ff97

Please sign in to comment.