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
3 changes: 2 additions & 1 deletion common/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ pub enum MarketEvent {
#[event_version("1.0.0")]
SupplyWithdrawn {
account_id: AccountId,
borrow_asset_amount: BorrowAssetAmount,
borrow_asset_amount_to_account: BorrowAssetAmount,
borrow_asset_amount_to_fees: BorrowAssetAmount,
},
#[event_version("1.0.0")]
CollateralDeposited {
Expand Down
38 changes: 19 additions & 19 deletions common/src/fee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,35 +50,35 @@ impl<T: AssetClass> TimeBasedFee<T> {
pub enum TimeBasedFeeFunction {
Fixed,
Linear,
Logarithmic,
}

impl<T: AssetClass> TimeBasedFee<T> {
pub fn of(&self, amount: FungibleAssetAmount<T>, time: u64) -> Option<FungibleAssetAmount<T>> {
pub fn of(
&self,
amount: FungibleAssetAmount<T>,
duration: u64,
) -> Option<FungibleAssetAmount<T>> {
let base_fee = self.fee.of(amount)?;

if self.duration.0 == 0 {
return Some(0.into());
}

match self.behavior {
TimeBasedFeeFunction::Fixed => Some(base_fee),
TimeBasedFeeFunction::Linear => (Decimal::from(time) / self.duration.0
* base_fee.as_u128())
.to_u128_ceil()
.map(FungibleAssetAmount::new),
TimeBasedFeeFunction::Logarithmic => Some(
// TODO: Seems jank.
#[allow(
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
clippy::cast_precision_loss
)]
(((base_fee.as_u128() as f64 * f64::log2((1 + time - self.duration.0) as f64))
/ f64::log2((1 + time) as f64))
.ceil() as u128)
.into(),
),
TimeBasedFeeFunction::Fixed => {
if duration >= self.duration.0 {
Some(0.into())
} else {
Some(base_fee)
}
}
TimeBasedFeeFunction::Linear => {
(Decimal::from(self.duration.0.saturating_sub(duration))
/ Decimal::from(self.duration.0)
* base_fee.as_u128())
.to_u128_ceil()
.map(FungibleAssetAmount::new)
}
}
}
}
5 changes: 3 additions & 2 deletions common/src/market/configuration.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use near_sdk::{json_types::U64, near};
use near_sdk::{json_types::U64, near, AccountId};

use crate::{
asset::{
Expand Down Expand Up @@ -33,8 +33,9 @@ pub struct MarketConfiguration {
pub maximum_borrow_duration_ms: Option<U64>,
pub minimum_borrow_amount: BorrowAssetAmount,
pub maximum_borrow_amount: BorrowAssetAmount,
pub supply_withdrawal_fee: TimeBasedFee<CollateralAsset>,
pub supply_withdrawal_fee: TimeBasedFee<BorrowAsset>,
pub yield_weights: YieldWeights,
pub protocol_account_id: AccountId,
/// How far below market rate to accept liquidation? This is effectively the liquidator's spread.
///
/// For example, if a 100USDC borrow is (under)collateralized with $110 of
Expand Down
22 changes: 18 additions & 4 deletions common/src/market/impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ use crate::{
withdrawal_queue::{error::WithdrawalQueueLockError, WithdrawalQueue},
};

use super::WithdrawalResolution;

#[derive(BorshStorageKey)]
#[near]
enum StorageKey {
Expand Down Expand Up @@ -240,7 +242,7 @@ impl Market {
/// - If the withdrawal queue is empty.
pub fn try_lock_next_withdrawal_request(
&mut self,
) -> Result<Option<(AccountId, BorrowAssetAmount)>, WithdrawalQueueLockError> {
) -> Result<Option<WithdrawalResolution>, WithdrawalQueueLockError> {
let (account_id, requested_amount) = self.withdrawal_queue.try_lock()?;

let Some((amount, mut supply_position)) = self
Expand All @@ -266,12 +268,24 @@ impl Market {
return Ok(None);
};

supply_position.record_withdrawal(amount);
let resolution = supply_position.record_withdrawal(amount, env::block_timestamp_ms());

Ok(Some(resolution))
}

pub fn record_borrow_asset_protocol_yield(&mut self, amount: BorrowAssetAmount) {
let mut yield_record = self
.static_yield
.get(&self.configuration.protocol_account_id)
.unwrap_or_default();

yield_record.borrow_asset.join(amount);

Ok(Some((account_id, amount)))
self.static_yield
.insert(&self.configuration.protocol_account_id, &yield_record);
}

pub(crate) fn record_borrow_asset_yield_distribution(&mut self, mut amount: BorrowAssetAmount) {
pub fn record_borrow_asset_yield_distribution(&mut self, mut amount: BorrowAssetAmount) {
// Sanity.
if amount.is_zero() {
return;
Expand Down
8 changes: 8 additions & 0 deletions common/src/market/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,11 @@ pub enum Nep141MarketDepositMessage {
pub struct LiquidateMsg {
pub account_id: AccountId,
}

#[derive(Clone, Debug)]
#[near(serializers = [json, borsh])]
pub struct WithdrawalResolution {
pub account_id: AccountId,
pub amount_to_account: BorrowAssetAmount,
pub amount_to_fees: BorrowAssetAmount,
}
52 changes: 43 additions & 9 deletions common/src/supply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,28 @@ use std::{
ops::{Deref, DerefMut},
};

use near_sdk::{env, near, AccountId};
use near_sdk::{env, json_types::U64, near, AccountId};

use crate::{
accumulator::{AccumulationRecord, Accumulator},
asset::{BorrowAsset, BorrowAssetAmount},
event::MarketEvent,
market::Market,
market::{Market, WithdrawalResolution},
number::Decimal,
};

#[derive(Debug, Clone, PartialEq, Eq)]
#[near(serializers = [json, borsh])]
pub struct SupplyPosition {
pub started_at_block_timestamp_ms: Option<U64>,
borrow_asset_deposit: BorrowAssetAmount,
pub borrow_asset_yield: Accumulator<BorrowAsset>,
}

impl SupplyPosition {
pub fn new(current_snapshot_index: u32) -> Self {
Self {
started_at_block_timestamp_ms: None,
borrow_asset_deposit: 0.into(),
// We start at next log index so that the supply starts
// accumulating yield from the _next_ log (since they were not
Expand All @@ -35,6 +37,10 @@ impl SupplyPosition {
self.borrow_asset_deposit
}

pub fn get_started_at_block_timestamp_ms(&self) -> Option<u64> {
self.started_at_block_timestamp_ms.map(u64::from)
}

pub fn exists(&self) -> bool {
!self.borrow_asset_deposit.is_zero() || !self.borrow_asset_yield.get_total().is_zero()
}
Expand All @@ -43,7 +49,11 @@ impl SupplyPosition {
pub(crate) fn increase_borrow_asset_deposit(
&mut self,
amount: BorrowAssetAmount,
block_timestamp_ms: u64,
) -> Option<()> {
if self.started_at_block_timestamp_ms.is_none() || self.borrow_asset_deposit.is_zero() {
self.started_at_block_timestamp_ms = Some(block_timestamp_ms.into());
}
self.borrow_asset_deposit.join(amount)
}

Expand All @@ -52,6 +62,8 @@ impl SupplyPosition {
&mut self,
amount: BorrowAssetAmount,
) -> Option<BorrowAssetAmount> {
// No need to reset the timer; it is a permanent indication of the
// initial supply event.
self.borrow_asset_deposit.split(amount)
}
}
Expand Down Expand Up @@ -247,11 +259,14 @@ impl<M: BorrowMut<Market>> LinkedSupplyPositionMut<M> {
}
}

pub fn record_withdrawal(&mut self, amount: BorrowAssetAmount) -> BorrowAssetAmount {
pub fn record_withdrawal(
&mut self,
mut amount: BorrowAssetAmount,
block_timestamp_ms: u64,
) -> WithdrawalResolution {
self.accumulate_yield();

let withdrawn = self
.position
self.position
.decrease_borrow_asset_deposit(amount)
.unwrap_or_else(|| env::panic_str("Supply position borrow asset underflow"));

Expand All @@ -263,20 +278,39 @@ impl<M: BorrowMut<Market>> LinkedSupplyPositionMut<M> {

self.market.borrow_mut().snapshot();

let started_at_block_timestamp_ms =
self.0.position.started_at_block_timestamp_ms.unwrap().0;
let supply_duration = block_timestamp_ms.saturating_sub(started_at_block_timestamp_ms);

let amount_to_fees = self
.market
.borrow()
.configuration
.supply_withdrawal_fee
.of(amount, supply_duration)
.unwrap();

amount.split(amount_to_fees).unwrap();

MarketEvent::SupplyWithdrawn {
account_id: self.account_id.clone(),
borrow_asset_amount: amount,
borrow_asset_amount_to_account: amount,
borrow_asset_amount_to_fees: amount_to_fees,
}
.emit();

withdrawn
WithdrawalResolution {
account_id: self.account_id.clone(),
amount_to_account: amount,
amount_to_fees,
}
}

pub fn record_deposit(&mut self, amount: BorrowAssetAmount) {
pub fn record_deposit(&mut self, amount: BorrowAssetAmount, block_timestamp_ms: u64) {
self.accumulate_yield();

self.position
.increase_borrow_asset_deposit(amount)
.increase_borrow_asset_deposit(amount, block_timestamp_ms)
.unwrap_or_else(|| env::panic_str("Supply position borrow asset overflow"));

self.market
Expand Down
19 changes: 14 additions & 5 deletions contract/market/src/impl_helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use near_sdk::{
use templar_common::{
asset::{BorrowAssetAmount, CollateralAssetAmount},
market::PricePair,
market::WithdrawalResolution,
oracle::pyth::OracleResponse,
snapshot::Snapshot,
};
Expand All @@ -15,7 +16,7 @@ use crate::{Contract, ContractExt};
impl Contract {
pub fn execute_supply(&mut self, account_id: AccountId, amount: BorrowAssetAmount) {
let mut supply_position = self.get_or_create_linked_supply_position_mut(account_id);
supply_position.record_deposit(amount);
supply_position.record_deposit(amount, env::block_timestamp_ms());
}

pub fn execute_collateralize(&mut self, account_id: AccountId, amount: CollateralAssetAmount) {
Expand Down Expand Up @@ -237,7 +238,7 @@ impl Contract {
}

#[private]
pub fn after_execute_next_withdrawal(&mut self, account: AccountId, amount: BorrowAssetAmount) {
pub fn after_execute_next_withdrawal(&mut self, withdrawal_resolution: WithdrawalResolution) {
// TODO: Is this check even necessary in a #[private] function?
require!(env::promise_results_count() == 1);

Expand All @@ -256,9 +257,11 @@ impl Contract {
// head of the queue cannot change while transfers are
// in-flight. This should be maintained by the queue itself.
require!(
popped_account == account,
popped_account == withdrawal_resolution.account_id,
"Invariant violation: Queue shifted while locked/in-flight.",
);

self.record_borrow_asset_protocol_yield(withdrawal_resolution.amount_to_fees);
}
PromiseResult::Failed => {
// Withdrawal failed: unlock the queue so they can try again.
Expand All @@ -270,8 +273,14 @@ impl Contract {

env::log_str("The withdrawal request cannot be fulfilled at this time. Please try again later.");
self.withdrawal_queue.unlock();
if let Some(mut supply_position) = self.get_linked_supply_position_mut(account) {
supply_position.record_deposit(amount);

if let Some(mut supply_position) =
self.get_linked_supply_position_mut(withdrawal_resolution.account_id.clone())
{
let timestamp = env::block_timestamp_ms();
let mut amount = withdrawal_resolution.amount_to_account;
amount.join(withdrawal_resolution.amount_to_fees);
supply_position.record_deposit(amount, timestamp);
}
}
}
Expand Down
11 changes: 7 additions & 4 deletions contract/market/src/impl_market_external.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ impl MarketExternalInterface for Contract {
}

fn execute_next_supply_withdrawal_request(&mut self) -> PromiseOrValue<()> {
let Some((account_id, amount)) = self
let Some(withdrawal_resolution) = self
.try_lock_next_withdrawal_request()
.unwrap_or_else(|e| env::panic_str(&e.to_string()))
else {
Expand All @@ -190,10 +190,13 @@ impl MarketExternalInterface for Contract {
PromiseOrValue::Promise(
self.configuration
.borrow_asset
.transfer(account_id.clone(), amount)
.transfer(
withdrawal_resolution.account_id.clone(),
withdrawal_resolution.amount_to_account,
)
.then(
Self::ext(env::current_account_id())
.after_execute_next_withdrawal(account_id.clone(), amount),
.after_execute_next_withdrawal(withdrawal_resolution),
),
)
}
Expand All @@ -217,7 +220,7 @@ impl MarketExternalInterface for Contract {
// 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);
supply_position.record_deposit(total_yield, env::block_timestamp_ms());
}
}
}
Expand Down
Loading