Explore the factory hooks
As already discussed in the whitepaper Vesu comes with a already implemented extension which we call the factory extension or default extension.
In this article we explore this contract into more detail, to learn how to write our own extension contract and get some inspiration.
The interface we need to implement defines the functions
- singleton
- price
- interest_rate
- rate_accumulator
- before_modify_position
- after_modify_position
- before_transfer_position
- after_transfer_position
- before_liquidate_position
- after_liquidate_position
Let's see how the default extension implements it.
singleton
Returns the address of the singleton contract
fn singleton(self: @ContractState) -> ContractAddress {
self.singleton.read()
}
price
Must return the price for a given asset in a given pool.
Vesu delegates the implementation of oracle price feeds to a lending pool’s extension.
The default extension integrates with the Pragma oracle which offers robust price feeds for a variety of assets on Starknet.
fn price(
self: @ContractState,
pool_id: felt252,
asset: ContractAddress
) -> AssetPrice
{
let (value, is_valid) = self.pragma_oracle.price(pool_id, asset);
AssetPrice { value, is_valid }
}
interest_rate
Returns the current interest rate for a given asset in a given pool, given it's utilization.
The default's extension interest rate model is implemented as here on Github.
fn interest_rate(
self: @ContractState,
pool_id: felt252,
asset: ContractAddress,
utilization: u256,
last_updated: u64,
last_full_utilization_rate: u256,
) -> u256 {
let (interest_rate, _) = self
.interest_rate_model
.interest_rate(pool_id, asset, utilization, last_updated, last_full_utilization_rate);
interest_rate
}
rate_accumulator
Returns the current rate accumulator for a given asset in a given pool, given it's utilization.
The default's extension rate accumulator is implemented as here on Github.
fn rate_accumulator(
self: @ContractState,
pool_id: felt252,
asset: ContractAddress,
utilization: u256,
last_updated: u64,
last_rate_accumulator: u256,
last_full_utilization_rate: u256,
) -> (u256, u256) {
self
.interest_rate_model
.rate_accumulator(
pool_id, asset, utilization, last_updated, last_rate_accumulator, last_full_utilization_rate
)
}
Hooks
before_modify_position
The default extension hasn't implemented a particular before_modify_position
hook, it just returns the user's values (collateral, debt).
fn before_modify_position(
ref self: ContractState,
context: Context,
collateral: Amount,
debt: Amount,
data: Span<felt252>,
caller: ContractAddress
) -> (Amount, Amount) {
assert!(get_caller_address() == self.singleton.read(), "caller-not-singleton");
(collateral, debt)
}
after_modify_position
after_modify_position
hook is implemented as defined here.
after_modify_position
does a few emergency checks, and if none is active, it returns true.
If true
collateral and debt amounts are allowed to be modified in any way.
fn after_modify_position(
ref self: ComponentState<TContractState>,
mut context: Context,
collateral_delta: i257,
collateral_shares_delta: i257,
debt_delta: i257,
nominal_debt_delta: i257,
data: Span<felt252>,
caller: ContractAddress
) -> bool {
self.update_pair(ref context, collateral_shares_delta, nominal_debt_delta);
let shutdown_mode = self.update_shutdown_status(ref context);
// check invariants for collateral and debt amounts
// check shutdown mode against shutdown types
//
true
}
before_transfer_position
Transfer position callback. Called by the Singleton contract before transferring collateral / debt between position.
The function returns
collateral
- amount of collateral to be transferreddebt
- amount of debt to be transferred
In the implementation of the default extension the amounts are returned unchanged.
fn before_transfer_position(
ref self: ComponentState<TContractState>,
from_context: Context,
to_context: Context,
collateral: UnsignedAmount,
debt: UnsignedAmount,
data: Span<felt252>,
caller: ContractAddress
) -> (UnsignedAmount, UnsignedAmount) {
if from_context.debt_asset == Zeroable::zero() && from_context.user == get_contract_address() {
ISingletonDispatcher { contract_address: self.get_contract().singleton() }
.modify_delegation(from_context.pool_id, caller, true);
}
(collateral, debt)
}
after_transfer_position
Implements logic to execute after a transfer of collateral or debt from one position to another. Revokes the caller's delegate to modify the position owned by the extension itself.
Returns
bool
- true if it was successful, false otherwise
fn after_transfer_position(
ref self: ComponentState<TContractState>,
mut from_context: Context,
mut to_context: Context,
collateral_delta: u256,
collateral_shares_delta: u256,
debt_delta: u256,
nominal_debt_delta: u256,
data: Span<felt252>,
caller: ContractAddress
) -> bool {
// skip shutdown mode evaluation and updating the pairs collateral shares and nominal debt balances
// if the pairs are the same
let (from_shutdown_mode, to_shutdown_mode) = if (from_context.pool_id == to_context.pool_id
&& from_context.collateral_asset == to_context.collateral_asset
&& from_context.debt_asset == to_context.debt_asset) {
let from_shutdown_mode = self.update_shutdown_status(ref from_context);
(from_shutdown_mode, from_shutdown_mode)
} else {
// either the collateral asset or the debt asset has to match (also enforced by the singleton)
assert!(
from_context.collateral_asset == to_context.collateral_asset
|| from_context.debt_asset == to_context.debt_asset,
"asset-mismatch"
);
self
.update_pair(
ref from_context, i257_new(collateral_shares_delta, true), i257_new(nominal_debt_delta, true)
);
self
.update_pair(
ref to_context, i257_new(collateral_shares_delta, false), i257_new(nominal_debt_delta, false)
);
(self.update_shutdown_status(ref from_context), self.update_shutdown_status(ref to_context))
};
// if shutdown mode has been triggered then the 'from' position should have no debt and only
// transfers within the same pairing are allowed
if from_shutdown_mode != ShutdownMode::None || to_shutdown_mode != ShutdownMode::None {
assert!(from_context.position.nominal_debt == 0, "shutdown-non-zero-debt");
assert!(
from_context.collateral_asset == to_context.collateral_asset
&& from_context.debt_asset == to_context.debt_asset,
"shutdown-pair-mismatch"
);
}
// mint vTokens if collateral shares are transferred to the corresponding vToken pairing
if to_context.debt_asset == Zeroable::zero() && to_context.user == get_contract_address() {
assert!(from_context.collateral_asset == to_context.collateral_asset, "v-token-to-asset-mismatch");
let mut tokenization = self.get_contract_mut();
tokenization
.mint_or_burn_v_token(
to_context.pool_id,
to_context.collateral_asset,
caller,
i257_new(collateral_shares_delta, false)
);
}
// burn vTokens if collateral shares are transferred from the corresponding vToken pairing
if from_context.debt_asset == Zeroable::zero() && from_context.user == get_contract_address() {
assert!(from_context.collateral_asset == to_context.collateral_asset, "v-token-from-asset-mismatch");
ISingletonDispatcher { contract_address: self.get_contract().singleton() }
.modify_delegation(from_context.pool_id, caller, false);
let mut tokenization = self.get_contract_mut();
tokenization
.mint_or_burn_v_token(
to_context.pool_id, to_context.collateral_asset, caller, i257_new(collateral_shares_delta, true)
);
}
true
}
before_liquidate_position
Implements logic to execute before a position gets liquidated. See implementation here
Liquidations are only allowed in normal and recovery mode.
// don't allow for liquidations if the pool is not in normal or recovery mode
let shutdown_mode = self.update_shutdown_status(ref context);
assert!(
(shutdown_mode == ShutdownMode::None || shutdown_mode == ShutdownMode::Recovery)
&& context.collateral_asset_price.is_valid
&& context.debt_asset_price.is_valid,
"emergency-mode"
);
The liquidator has to be specify how much debt to repay and the minimum amount of collateral to receive in exchange. The value of the collateral is discounted by the liquidation factor in comparison to the current price (according to the oracle).
// apply liquidation factor to debt value to get the collateral amount to release
let collateral_value_to_receive = debt_to_repay
* context.debt_asset_price.value
/ context.debt_asset_config.scale;
let mut collateral_to_receive = (collateral_value_to_receive * SCALE / context.collateral_asset_price.value)
* context.collateral_asset_config.scale
/ liquidation_factor;
In an event where there's not enough collateral to cover the debt, the liquidation will result in bad debt. The bad debt is attributed to the pool and distributed amongst the lenders of the corresponding collateral asset. The liquidator receives all the collateral but only has to repay the proportioned debt value.
// account for bad debt if there isn't enough collateral to cover the debt
let mut bad_debt = 0;
if collateral_value < debt_value {
// limit the bad debt by the outstanding collateral and debt values (in usd)
if collateral_value < debt_to_repay * context.debt_asset_price.value / context.debt_asset_config.scale {
bad_debt = (debt_value - collateral_value)
* context.debt_asset_config.scale
/ context.debt_asset_price.value;
debt_to_repay = debt;
} else {
// derive the bad debt proportionally to the debt repaid
bad_debt = debt_to_repay * (debt_value - collateral_value) / collateral_value;
debt_to_repay = debt_to_repay + bad_debt;
}
}
after_liquidate_position
Implements logic to execute after a position gets liquidated.
fn after_liquidate_position(
ref self: ComponentState<TContractState>,
mut context: Context,
collateral_delta: i257,
collateral_shares_delta: i257,
debt_delta: i257,
nominal_debt_delta: i257,
bad_debt: u256,
data: Span<felt252>,
caller: ContractAddress
) -> bool {
self.update_pair(ref context, collateral_shares_delta, nominal_debt_delta);
self.update_shutdown_status(ref context);
true
}