Skip to content

Commit

Permalink
Configurable Fee receivers (#47)
Browse files Browse the repository at this point in the history
* team / dao fee split

* change claim rewards and deposit function

* configurable fee receivers

* minor nits + extra testing

* version bump
  • Loading branch information
0xripleys authored Jan 13, 2025
1 parent 2512902 commit c034587
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 12 deletions.
98 changes: 89 additions & 9 deletions contracts/suilend/sources/lending_market.move
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ module suilend::lending_market {
use suilend::liquidity_mining::{Self};
use sui::package;
use sui::sui::SUI;
use sui::dynamic_field::{Self};

// === Errors ===
const EIncorrectVersion: u64 = 1;
Expand All @@ -32,9 +33,10 @@ module suilend::lending_market {
const ERewardPeriodNotOver: u64 = 5;
const ECannotClaimReward: u64 = 6;
const EInvalidObligationId: u64 = 7;
const EInvalidFeeReceivers: u64 = 8;

// === Constants ===
const CURRENT_VERSION: u64 = 6;
const CURRENT_VERSION: u64 = 7;
const U64_MAX: u64 = 18_446_744_073_709_551_615;

// === One time Witness ===
Expand All @@ -54,7 +56,7 @@ module suilend::lending_market {

// window duration is in seconds
rate_limiter: RateLimiter,
fee_receiver: address,
fee_receiver: address, // deprecated

/// unused
bad_debt_usd: Decimal,
Expand All @@ -72,6 +74,15 @@ module suilend::lending_market {
obligation_id: ID
}

// === Dynamic Fields ===
public struct FeeReceiversKey has copy, drop, store { }

public struct FeeReceivers has store {
receivers: vector<address>,
weights: vector<u64>,
total_weight: u64,
}

// cTokens redemptions and borrows are rate limited to mitigate exploits. however,
// on a liquidation we don't want to rate limit redemptions because we don't want liquidators to
// get stuck holding cTokens. So the liquidate function issues this exemption
Expand Down Expand Up @@ -168,7 +179,7 @@ module suilend::lending_market {
LendingMarketOwnerCap<P>,
LendingMarket<P>
) {
let lending_market = LendingMarket<P> {
let mut lending_market = LendingMarket<P> {
id: object::new(ctx),
version: CURRENT_VERSION,
reserves: vector::empty(),
Expand All @@ -178,12 +189,19 @@ module suilend::lending_market {
bad_debt_usd: decimal::from(0),
bad_debt_limit_usd: decimal::from(0),
};

let owner_cap = LendingMarketOwnerCap<P> {
id: object::new(ctx),
lending_market_id: object::id(&lending_market)
};

set_fee_receivers(
&owner_cap,
&mut lending_market,
vector[tx_context::sender(ctx)],
vector[100]
);

(owner_cap, lending_market)
}

Expand Down Expand Up @@ -689,8 +707,8 @@ module suilend::lending_market {
);
};

let deposit_reserve = vector::borrow_mut(&mut lending_market.reserves, deposit_reserve_id);
let expected_ctokens = {
let deposit_reserve = vector::borrow(&lending_market.reserves, deposit_reserve_id);
assert!(reserve::coin_type(deposit_reserve) == type_name::get<RewardType>(), EWrongType);

floor(
Expand All @@ -702,7 +720,7 @@ module suilend::lending_market {
};

if (expected_ctokens == 0) {
transfer::public_transfer(rewards, lending_market.fee_receiver);
reserve::join_fees<P, RewardType>(deposit_reserve, coin::into_balance(rewards));
}
else {
let ctokens = deposit_liquidity_and_mint_ctokens<P, RewardType>(
Expand Down Expand Up @@ -1038,6 +1056,34 @@ module suilend::lending_market {
lending_market.rate_limiter = rate_limiter::new(config, clock::timestamp_ms(clock) / 1000);
}

public fun set_fee_receivers<P>(
_: &LendingMarketOwnerCap<P>,
lending_market: &mut LendingMarket<P>,
receivers: vector<address>,
weights: vector<u64>,
) {
assert!(lending_market.version == CURRENT_VERSION, EIncorrectVersion);

assert!(vector::length(&receivers) == vector::length(&weights), EInvalidFeeReceivers);
assert!(vector::length(&receivers) > 0, EInvalidFeeReceivers);

let total_weight = vector::fold!(weights, 0, |acc, weight| acc + weight);
assert!(total_weight > 0, EInvalidFeeReceivers);

if (dynamic_field::exists_(&lending_market.id, FeeReceiversKey {})) {
let FeeReceivers { .. } = dynamic_field::remove<FeeReceiversKey, FeeReceivers>(
&mut lending_market.id,
FeeReceiversKey {}
);
};

dynamic_field::add(
&mut lending_market.id,
FeeReceiversKey {},
FeeReceivers { receivers, weights, total_weight }
);
}

entry fun claim_fees<P, T>(
lending_market: &mut LendingMarket<P>,
reserve_array_index: u64,
Expand All @@ -1047,10 +1093,44 @@ module suilend::lending_market {

let reserve = vector::borrow_mut(&mut lending_market.reserves, reserve_array_index);
assert!(reserve::coin_type(reserve) == type_name::get<T>(), EWrongType);
let (ctoken_fees, fees) = reserve::claim_fees<P, T>(reserve);

transfer::public_transfer(coin::from_balance(ctoken_fees, ctx), lending_market.fee_receiver);
transfer::public_transfer(coin::from_balance(fees, ctx), lending_market.fee_receiver);
let (mut ctoken_fees, mut fees) = reserve::claim_fees<P, T>(reserve);
let total_ctoken_fees = balance::value(&ctoken_fees);
let total_fees = balance::value(&fees);

let fee_receivers: &FeeReceivers = dynamic_field::borrow(&lending_market.id, FeeReceiversKey {});
let num_fee_receivers = vector::length(&fee_receivers.weights);

num_fee_receivers.do!(|i| {
let fee_amount = (total_fees as u128) * (fee_receivers.weights[i] as u128) / (fee_receivers.total_weight as u128);
let fee = if (i == num_fee_receivers - 1) {
balance::withdraw_all(&mut fees)
} else {
balance::split(&mut fees, fee_amount as u64)
};

if (balance::value(&fee) > 0) {
transfer::public_transfer(coin::from_balance(fee, ctx), fee_receivers.receivers[i]);
} else {
balance::destroy_zero(fee);
};

let ctoken_fee_amount = (total_ctoken_fees as u128) * (fee_receivers.weights[i] as u128) / (fee_receivers.total_weight as u128);
let ctoken_fee = if (i == num_fee_receivers - 1) {
balance::withdraw_all(&mut ctoken_fees)
} else {
balance::split(&mut ctoken_fees, ctoken_fee_amount as u64)
};

if (balance::value(&ctoken_fee) > 0) {
transfer::public_transfer(coin::from_balance(ctoken_fee, ctx), fee_receivers.receivers[i]);
} else {
balance::destroy_zero(ctoken_fee);
};
});

balance::destroy_zero(fees);
balance::destroy_zero(ctoken_fees);
}

public fun new_obligation_owner_cap<P>(
Expand Down
7 changes: 6 additions & 1 deletion contracts/suilend/sources/reserve.move
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,11 @@ module suilend::reserve {
(protocol_fee_amount, liquidator_bonus_amount)
}

public(package) fun join_fees<P, T>(reserve: &mut Reserve<P>, fees: Balance<T>) {
let balances: &mut Balances<P, T> = dynamic_field::borrow_mut(&mut reserve.id, BalanceKey {});
balance::join(&mut balances.fees, fees);
}

public(package) fun update_reserve_config<P>(
reserve: &mut Reserve<P>,
config: ReserveConfig,
Expand Down Expand Up @@ -910,7 +915,7 @@ module suilend::reserve {
}

// === Private Functions ===
fun log_reserve_data<P>(reserve: &Reserve<P>){
fun log_reserve_data<P>(reserve: &Reserve<P>) {
let available_amount_decimal = decimal::from(reserve.available_amount);
let supply_amount = total_supply(reserve);
let cur_util = calculate_utilization_rate(reserve);
Expand Down
14 changes: 12 additions & 2 deletions contracts/suilend/tests/lending_market_tests.move
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,13 @@ module suilend::lending_market_tests {

test_scenario::next_tx(&mut scenario, owner);

lending_market::set_fee_receivers<LENDING_MARKET>(
&owner_cap,
&mut lending_market,
vector[tx_context::sender(test_scenario::ctx(&mut scenario)), @0x27],
vector[1, 2]
);

lending_market::claim_fees<LENDING_MARKET, TEST_SUI>(
&mut lending_market,
*bag::borrow(&type_to_index, type_name::get<TEST_SUI>()),
Expand All @@ -548,9 +555,12 @@ module suilend::lending_market_tests {

test_scenario::next_tx(&mut scenario, owner);

let fees: Coin<TEST_SUI> = test_scenario::take_from_address(&scenario, lending_market::fee_receiver(&lending_market));
assert!(coin::value(&fees) == 1_000_000, 0);
let fees: Coin<TEST_SUI> = test_scenario::take_from_address(&scenario, @0x26);
assert!(coin::value(&fees) == 1_000_000 / 3, 0);
test_utils::destroy(fees);

let fees: Coin<TEST_SUI> = test_scenario::take_from_address(&scenario, @0x27);
assert!(coin::value(&fees) == 2 * 1_000_000 / 3 + 1, 0);
test_utils::destroy(fees);

test_utils::destroy(sui);
Expand Down

0 comments on commit c034587

Please sign in to comment.