Skip to content

Commit

Permalink
feat: oracle adpater for pricing kSTRK token (#88)
Browse files Browse the repository at this point in the history
  • Loading branch information
zklend-tech authored Dec 23, 2024
1 parent 787f344 commit c123549
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 2 deletions.
9 changes: 9 additions & 0 deletions src/interfaces.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -293,8 +293,17 @@ trait IChainlinkOracle<TContractState> {
fn decimals(self: @TContractState) -> u8;
}

#[starknet::interface]
trait IKstrkPool<TContractState> {
fn get_staked_token(self: @TContractState) -> ContractAddress;

fn get_total_stake(self: @TContractState) -> u128;
}

#[starknet::interface]
trait IERC20<TContractState> {
fn totalSupply(self: @TContractState) -> u256;

fn decimals(self: @TContractState) -> felt252;

fn balanceOf(self: @TContractState, user: ContractAddress) -> u256;
Expand Down
2 changes: 2 additions & 0 deletions src/oracles.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ mod dual_oracle_adapter;
mod pragma_oracle_adapter;

mod chainlink_oracle_adapter;

mod kstrk_oracle_adapter;
70 changes: 70 additions & 0 deletions src/oracles/kstrk_oracle_adapter.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
mod errors {
const PRICE_OVERFLOW: felt252 = 'KSTRK_PRICE_OVERFLOW';
}

/// An oracle implementation that prices the `kSTRK` token by multiplying the `STRK` price from
/// another "upstream" adapter by the `kSTRK/STRK` exchange rate.
#[starknet::contract]
mod KstrkOracleAdapter {
use starknet::{ContractAddress, get_block_timestamp};

// Hack to simulate the `crate` keyword
use super::super::super as crate;

use crate::interfaces::{
IERC20Dispatcher, IERC20DispatcherTrait, IKstrkPoolDispatcher, IKstrkPoolDispatcherTrait,
IPriceOracleSource, IPriceOracleSourceDispatcher, IPriceOracleSourceDispatcherTrait,
PriceWithUpdateTime
};
use crate::libraries::safe_decimal_math;

use super::errors;

#[storage]
struct Storage {
strk_upstream: ContractAddress,
kstrk_pool: ContractAddress,
}

#[constructor]
fn constructor(
ref self: ContractState, strk_upstream: ContractAddress, kstrk_pool: ContractAddress
) {
self.strk_upstream.write(strk_upstream);
self.kstrk_pool.write(kstrk_pool);
}

#[abi(embed_v0)]
impl IPriceOracleSourceImpl of IPriceOracleSource<ContractState> {
fn get_price(self: @ContractState) -> felt252 {
get_data(self).price
}

fn get_price_with_time(self: @ContractState) -> PriceWithUpdateTime {
get_data(self)
}
}

fn get_data(self: @ContractState) -> PriceWithUpdateTime {
// There is no need to scale the prices as all `IPriceOracleSource` implementations are
// guaranteed to return at target decimals.
let strk_price = IPriceOracleSourceDispatcher {
contract_address: self.strk_upstream.read()
}
.get_price_with_time();

let strk_price_u256: u256 = strk_price.price.into();

let kstrk_pool = IKstrkPoolDispatcher { contract_address: self.kstrk_pool.read() };
let kstrk_token = IERC20Dispatcher { contract_address: kstrk_pool.get_staked_token() };

let pool_size: u256 = kstrk_pool.get_total_stake().into();
let total_supply = kstrk_token.totalSupply();

let kstrk_price: felt252 = (strk_price_u256 * pool_size / total_supply)
.try_into()
.expect(errors::PRICE_OVERFLOW);

PriceWithUpdateTime { price: kstrk_price, update_time: get_block_timestamp().into() }
}
}
31 changes: 29 additions & 2 deletions tests/deploy.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ use zklend::irms::default_interest_rate_model::DefaultInterestRateModel;
use zklend::market::Market;
use zklend::oracles::chainlink_oracle_adapter::ChainlinkOracleAdapter;
use zklend::oracles::dual_oracle_adapter::DualOracleAdapter;
use zklend::oracles::kstrk_oracle_adapter::KstrkOracleAdapter;
use zklend::oracles::pragma_oracle_adapter::PragmaOracleAdapter;
use zklend::z_token::ZToken;

use tests::mock;
use tests::mock::{
IAccountDispatcher, IERC20Dispatcher, IFlashLoanHandlerDispatcher,
IMockChainlinkOracleDispatcher, IMockMarketDispatcher, IMockPragmaOracleDispatcher,
IMockPriceOracleDispatcher
IMockChainlinkOracleDispatcher, IMockKstrkPoolDispatcher, IMockMarketDispatcher,
IMockPragmaOracleDispatcher, IMockPriceOracleDispatcher
};

fn deploy_account(salt: felt252) -> IAccountDispatcher {
Expand Down Expand Up @@ -106,6 +107,18 @@ fn deploy_mock_market() -> IMockMarketDispatcher {
IMockMarketDispatcher { contract_address }
}

fn deploy_mock_kstrk_pool() -> IMockKstrkPoolDispatcher {
let (contract_address, _) = deploy_syscall(
mock::mock_kstrk_pool::MockKstrkPool::TEST_CLASS_HASH.try_into().unwrap(),
0,
Default::default().span(),
false
)
.unwrap();

IMockKstrkPoolDispatcher { contract_address }
}

fn deploy_flash_loan_handler() -> IFlashLoanHandlerDispatcher {
let (contract_address, _) = deploy_syscall(
mock::flash_loan_handler::FlashLoanHandler::TEST_CLASS_HASH.try_into().unwrap(),
Expand All @@ -132,6 +145,20 @@ fn deploy_dual_oracle_adapter(
IPriceOracleSourceDispatcher { contract_address }
}

fn deploy_kstrk_oracle_adapter(
strk_upstream: ContractAddress, kstrk_pool: ContractAddress
) -> IPriceOracleSourceDispatcher {
let (contract_address, _) = deploy_syscall(
KstrkOracleAdapter::TEST_CLASS_HASH.try_into().unwrap(),
0,
array![strk_upstream.into(), kstrk_pool.into()].span(),
false
)
.unwrap();

IPriceOracleSourceDispatcher { contract_address }
}

fn deploy_chainlink_oracle_adapter(
oracle: ContractAddress, timeout: felt252
) -> IPriceOracleSourceDispatcher {
Expand Down
52 changes: 52 additions & 0 deletions tests/kstrk_oracle_adapter.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use starknet::contract_address_const;

use test::test_utils::assert_eq;

use zklend::interfaces::IPriceOracleSourceDispatcherTrait;

use tests::deploy;
use tests::mock::{IMockKstrkPoolDispatcherTrait, IMockPragmaOracleDispatcherTrait};

#[test]
#[available_gas(30000000)]
fn test_price() {
let mock_pragma_oracle = deploy::deploy_mock_pragma_oracle();

// STRK price: 0.5
mock_pragma_oracle.set_price('STRK/USD', 0_50000000, 8, 100, 5);

// kSTRK total supply: 100
let mock_kstrk_token = deploy::deploy_erc20(
'kSTRK', 'kSTRK', 18, 100_000000000000000000, contract_address_const::<1>()
);

let mock_kstrk_pool = deploy::deploy_mock_kstrk_pool();
mock_kstrk_pool.set_staked_token(mock_kstrk_token.contract_address);

let pragma_oracle_adpater = deploy::deploy_pragma_oracle_adapter(
mock_pragma_oracle.contract_address, 'STRK/USD', 500
);
let kstrk_oracle_adapter = deploy::deploy_kstrk_oracle_adapter(
pragma_oracle_adpater.contract_address, mock_kstrk_pool.contract_address
);

// Pool size: 100 STRK
// Exchange rate: 1 kSTRK = 1 STRK
// Price: 0.5
mock_kstrk_pool.set_total_stake(100_000000000000000000);
assert(kstrk_oracle_adapter.get_price() == 0_50000000, 'FAILED');

// Pool size: 120 STRK
// Exchange rate: 1 kSTRK = 1.2 STRK
// Price: 0.6
mock_kstrk_pool.set_total_stake(120_000000000000000000);
assert(kstrk_oracle_adapter.get_price() == 0_60000000, 'FAILED');

// NOTE: this scenario should be impossible but is tested anyway
//
// Pool size: 80 STRK
// Exchange rate: 1 kSTRK = 0.8 STRK
// Price: 0.4
mock_kstrk_pool.set_total_stake(80_000000000000000000);
assert(kstrk_oracle_adapter.get_price() == 0_40000000, 'FAILED');
}
3 changes: 3 additions & 0 deletions tests/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ mod chainlink_oracle_adapter;
#[cfg(test)]
mod pragma_oracle_adapter;

#[cfg(test)]
mod kstrk_oracle_adapter;

#[cfg(test)]
mod z_token;

Expand Down
13 changes: 13 additions & 0 deletions tests/mock.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ mod mock_chainlink_oracle;

mod mock_pragma_oracle;

mod mock_kstrk_pool;

mod flash_loan_handler;

mod erc20;
Expand Down Expand Up @@ -213,6 +215,17 @@ trait IMockMarket<TContractState> {
fn burn_all_z_token(ref self: TContractState, z_token: ContractAddress, user: ContractAddress);
}

#[starknet::interface]
trait IMockKstrkPool<TContractState> {
fn get_staked_token(self: @TContractState) -> ContractAddress;

fn get_total_stake(self: @TContractState) -> u128;

fn set_staked_token(ref self: TContractState, staked_token: ContractAddress);

fn set_total_stake(ref self: TContractState, total_stake: u128);
}

#[starknet::interface]
trait IMockPriceOracle<TContractState> {
//
Expand Down
33 changes: 33 additions & 0 deletions tests/mock/mock_kstrk_pool.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use super::IMockKstrkPool;

#[starknet::contract]
mod MockKstrkPool {
use starknet::ContractAddress;

use super::IMockKstrkPool;

#[storage]
struct Storage {
staked_token: ContractAddress,
total_stake: u128,
}

#[abi(embed_v0)]
impl IMockKstrkPoolImpl of IMockKstrkPool<ContractState> {
fn get_staked_token(self: @ContractState) -> ContractAddress {
self.staked_token.read()
}

fn get_total_stake(self: @ContractState) -> u128 {
self.total_stake.read()
}

fn set_staked_token(ref self: ContractState, staked_token: ContractAddress) {
self.staked_token.write(staked_token)
}

fn set_total_stake(ref self: ContractState, total_stake: u128) {
self.total_stake.write(total_stake)
}
}
}

0 comments on commit c123549

Please sign in to comment.