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
2 changes: 1 addition & 1 deletion common/src/market/external.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ pub trait MarketExternalInterface {
) -> Option<WithdrawalRequestStatus>;
fn get_supply_withdrawal_queue_status(&self) -> WithdrawalQueueStatus;

fn harvest_yield(&mut self);
fn harvest_yield(&mut self, compounding: Option<bool>);

fn get_last_yield_rate(&self) -> Decimal;

Expand Down
8 changes: 7 additions & 1 deletion contract/market/src/impl_market_external.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,16 @@ impl MarketExternalInterface for Contract {
self.withdrawal_queue.get_status()
}

fn harvest_yield(&mut self) {
fn harvest_yield(&mut self, compounding: Option<bool>) {
let predecessor = env::predecessor_account_id();
if let Some(mut supply_position) = self.get_linked_supply_position_mut(predecessor) {
supply_position.accumulate_yield();
if compounding.unwrap_or(false) {
// Compound yield by withdrawing it and recording it as an immediate deposit.
let total_yield = supply_position.inner().borrow_asset_yield.get_total();
supply_position.record_yield_withdrawal(total_yield);
supply_position.record_deposit(total_yield);
}
}
}

Expand Down
103 changes: 103 additions & 0 deletions contract/market/tests/compounding_yield.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use std::{sync::atomic::Ordering, time::Duration};

use rstest::rstest;
use templar_common::{dec, fee::Fee, interest_rate_strategy::InterestRateStrategy};
use test_utils::*;

#[rstest]
#[case(1_000_000, InterestRateStrategy::linear(dec!("1000000"), dec!("1000000")).unwrap(), true)]
#[case(1_000_000, InterestRateStrategy::linear(dec!("1000000"), dec!("1000000")).unwrap(), false)]
#[tokio::test]
async fn compounding_yield(
#[case] principal: u128,
#[case] strategy: InterestRateStrategy,
#[case] compounding: bool,
) {
let SetupEverything {
c,
supply_user,
supply_user_2,
borrow_user,
..
} = setup_everything(|c| {
c.borrow_origination_fee = Fee::zero();
c.borrow_interest_rate_strategy = strategy.clone();
})
.await;

c.supply(&supply_user, principal * 5).await;
c.supply(&supply_user_2, principal * 5).await;
c.collateralize(&borrow_user, principal * 2).await;

c.borrow(&borrow_user, principal).await;

println!("Sleeping...");
let mut iters = 0;
let done = std::sync::atomic::AtomicBool::new(false);
tokio::join!(
async {
while !done.load(Ordering::Relaxed) {
c.harvest_yield(&supply_user_2, compounding).await;
iters += 1;
}
},
async {
while !done.load(Ordering::Relaxed) {
let position = c.get_borrow_position(borrow_user.id()).await.unwrap();
c.repay(
&borrow_user,
position.get_total_borrow_asset_liability().as_u128() * 120 / 100,
)
.await;
c.borrow(&borrow_user, principal).await;
}
},
async {
tokio::time::sleep(Duration::from_secs(20)).await;
done.store(true, Ordering::Relaxed);
}
);
println!("Done sleeping!");

c.harvest_yield(&supply_user, false).await;

let (supply_position_1_after, supply_position_2_after) = tokio::join!(
async { c.get_supply_position(supply_user.id()).await.unwrap() },
async { c.get_supply_position(supply_user_2.id()).await.unwrap() },
);

let supply_yield_1 = supply_position_1_after.get_borrow_asset_deposit().as_u128()
+ supply_position_1_after
.borrow_asset_yield
.get_total()
.as_u128()
+ supply_position_1_after
.borrow_asset_yield
.pending_estimate
.as_u128()
- principal * 5;
let supply_yield_2 = supply_position_2_after.get_borrow_asset_deposit().as_u128()
+ supply_position_2_after
.borrow_asset_yield
.get_total()
.as_u128()
+ supply_position_2_after
.borrow_asset_yield
.pending_estimate
.as_u128()
- principal * 5;

println!("supply 1 yield: {supply_yield_1:#?}");
println!("supply 2 yield: {supply_yield_2:#?}");
println!("iterations: {iters}");

if compounding {
// Supply user 2 will be rounded DOWN each iteration.
// Ensure that it is compounding, so each iteration should add (much) more
// than 1.
assert!(supply_yield_2 > supply_yield_1 + iters);
} else {
assert!(supply_yield_1 >= supply_yield_2);
assert!(supply_yield_1 < supply_yield_2 + iters + 1);
}
}
2 changes: 1 addition & 1 deletion contract/market/tests/happy_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ async fn test_happy(#[case] native_asset_case: NativeAssetCase) {
async {
// Withdraw yield.
{
c.harvest_yield(&supply_user).await;
c.harvest_yield(&supply_user, false).await;
let supply_position = c.get_supply_position(supply_user.id()).await.unwrap();
assert_eq!(supply_position.borrow_asset_yield.get_total().as_u128(), 80);

Expand Down
2 changes: 1 addition & 1 deletion contract/market/tests/harvest_yield_gas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ async fn harvest_yield_gas(#[case] iterations: usize) {
c.repay(&borrow_user, 1100).await;
}

let r = c.harvest_yield(&supply_user).await;
let r = c.harvest_yield(&supply_user, true).await;

println!("Total gas burnt ({iterations}): {}", r.total_gas_burnt);
}
10 changes: 7 additions & 3 deletions contract/market/tests/interest_rate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ async fn interest_rate(#[case] principal: u128, #[case] strategy: InterestRateSt
while !done.load(Ordering::Relaxed) {
tokio::join!(
c.apply_interest(&borrow_user_2),
c.harvest_yield(&supply_user_2),
// No compounding so we get apples-to-apples comparison.
// Technically it should be optimal to harvest (and
// compound) occasionally throughout the duration of
// the supply.
c.harvest_yield(&supply_user_2, false),
);
tokio::time::sleep(Duration::from_secs(1)).await;
iters += 1;
Expand Down Expand Up @@ -189,11 +193,11 @@ async fn interest_rate(#[case] principal: u128, #[case] strategy: InterestRateSt

let (supply_position_1, supply_position_2) = tokio::join!(
async {
c.harvest_yield(&supply_user).await;
c.harvest_yield(&supply_user, false).await;
c.get_supply_position(supply_user.id()).await.unwrap()
},
async {
c.harvest_yield(&supply_user_2).await;
c.harvest_yield(&supply_user_2, false).await;
c.get_supply_position(supply_user_2.id()).await.unwrap()
},
);
Expand Down
2 changes: 1 addition & 1 deletion contract/market/tests/liquidation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ async fn successful_liquidation_good_debt_under_mcr(

tokio::join!(
async {
c.harvest_yield(&supply_user).await;
c.harvest_yield(&supply_user, false).await;
let supply_position = c.get_supply_position(supply_user.id()).await.unwrap();
assert_eq!(
supply_position.borrow_asset_yield.get_total().as_u128(),
Expand Down
10 changes: 8 additions & 2 deletions test-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -461,11 +461,17 @@ impl TestController {
.unwrap()
}

pub async fn harvest_yield(&self, supply_user: &Account) -> ExecutionSuccess {
pub async fn harvest_yield(
&self,
supply_user: &Account,
compounding: bool,
) -> ExecutionSuccess {
println!("{} harvesting yield...", supply_user.id());
supply_user
.call(self.contract.id(), "harvest_yield")
.args_json(json!({}))
.args_json(json!({
"compounding": compounding,
}))
.max_gas()
.transact()
.await
Expand Down