-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' of https://github.com/paltalabs/defindex into fea…
…t/refactorModels
- Loading branch information
Showing
23 changed files
with
857 additions
and
51 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
[package] | ||
name = "fixed_apr_strategy" | ||
version = { workspace = true } | ||
authors = ["coderipper <[email protected]>"] | ||
license = { workspace = true } | ||
edition = { workspace = true } | ||
publish = false | ||
repository = { workspace = true } | ||
|
||
[lib] | ||
crate-type = ["cdylib"] | ||
|
||
[dependencies] | ||
soroban-sdk = { workspace = true } | ||
defindex-strategy-core = { workspace = true } | ||
|
||
[dev-dependencies] | ||
soroban-sdk = { workspace = true, features = ["testutils"] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
default: build | ||
|
||
all: test | ||
|
||
test: build | ||
cargo test | ||
|
||
build: | ||
cargo build --target wasm32-unknown-unknown --release | ||
soroban contract optimize --wasm ../../target/wasm32-unknown-unknown/release/fixed_apr_strategy.wasm | ||
@rm ../../target/wasm32-unknown-unknown/release/fixed_apr_strategy.wasm | ||
|
||
fmt: | ||
cargo fmt --all --check | ||
|
||
clean: | ||
cargo clean |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
use soroban_sdk::{Address, Env}; | ||
|
||
use crate::storage::{DataKey, INSTANCE_BUMP_AMOUNT, INSTANCE_LIFETIME_THRESHOLD}; | ||
use crate::StrategyError; | ||
|
||
pub fn read_balance(e: &Env, addr: Address) -> i128 { | ||
let key = DataKey::Balance(addr); | ||
if let Some(balance) = e.storage().persistent().get::<DataKey, i128>(&key) { | ||
e.storage() | ||
.persistent() | ||
.extend_ttl(&key, INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); | ||
balance | ||
} else { | ||
0 | ||
} | ||
} | ||
|
||
fn write_balance(e: &Env, addr: Address, amount: i128) { | ||
let key = DataKey::Balance(addr); | ||
e.storage().persistent().set(&key, &amount); | ||
e.storage() | ||
.persistent() | ||
.extend_ttl(&key, INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); | ||
} | ||
|
||
pub fn receive_balance(e: &Env, addr: Address, amount: i128) { | ||
let balance = read_balance(e, addr.clone()); | ||
|
||
let new_balance = balance.checked_add(amount) | ||
.expect("Integer overflow occurred while adding balance."); | ||
|
||
write_balance(e, addr, new_balance); | ||
} | ||
|
||
pub fn spend_balance(e: &Env, addr: Address, amount: i128) -> Result<(), StrategyError> { | ||
|
||
let balance = read_balance(e, addr.clone()); | ||
if balance < amount { | ||
return Err(StrategyError::InsufficientBalance); | ||
} | ||
write_balance(e, addr, balance - amount); | ||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
pub(crate) const MAX_BPS: i128 = 10_000; | ||
pub(crate) const SECONDS_PER_YEAR: i128 = 31_536_000; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
#![no_std] | ||
use constants::{MAX_BPS, SECONDS_PER_YEAR}; | ||
use soroban_sdk::{ | ||
contract, contractimpl, token::Client as TokenClient, Address, Env, IntoVal, String, Val, Vec | ||
}; | ||
|
||
mod balance; | ||
mod constants; | ||
mod storage; | ||
mod yield_balance; | ||
|
||
use balance::{read_balance, receive_balance, spend_balance}; | ||
use storage::{ | ||
extend_instance_ttl, get_underlying_asset, is_initialized, set_initialized, | ||
set_underlying_asset, set_apr, get_apr, set_last_harvest_time, get_last_harvest_time, | ||
}; | ||
|
||
pub use defindex_strategy_core::{DeFindexStrategyTrait, StrategyError, event}; | ||
use yield_balance::{read_yield, receive_yield, spend_yield}; | ||
|
||
pub fn check_nonnegative_amount(amount: i128) -> Result<(), StrategyError> { | ||
if amount < 0 { | ||
Err(StrategyError::NegativeNotAllowed) | ||
} else { | ||
Ok(()) | ||
} | ||
} | ||
|
||
fn check_initialized(e: &Env) -> Result<(), StrategyError> { | ||
if is_initialized(e) { | ||
Ok(()) | ||
} else { | ||
Err(StrategyError::NotInitialized) | ||
} | ||
} | ||
|
||
const STRATEGY_NAME: &str = "FixAprStrategy"; | ||
|
||
#[contract] | ||
struct FixAprStrategy; | ||
|
||
#[contractimpl] | ||
impl DeFindexStrategyTrait for FixAprStrategy { | ||
fn initialize( | ||
e: Env, | ||
asset: Address, | ||
init_args: Vec<Val>, | ||
) -> Result<(), StrategyError> { | ||
if is_initialized(&e) { | ||
return Err(StrategyError::AlreadyInitialized); | ||
} | ||
|
||
// Extract APR from `init_args`, assumed to be the first argument | ||
let apr_bps: u32 = init_args.get(0).ok_or(StrategyError::InvalidArgument)?.into_val(&e); | ||
let caller: Address = init_args.get(1).ok_or(StrategyError::InvalidArgument)?.into_val(&e); | ||
let amount: i128 = init_args.get(2).ok_or(StrategyError::InvalidArgument)?.into_val(&e); | ||
|
||
set_initialized(&e); | ||
set_underlying_asset(&e, &asset); | ||
set_apr(&e, apr_bps); | ||
|
||
// Should transfer tokens from the caller to the contract | ||
caller.require_auth(); | ||
TokenClient::new(&e, &asset).transfer(&caller, &e.current_contract_address(), &amount); | ||
|
||
event::emit_initialize(&e, String::from_str(&e, STRATEGY_NAME), asset); | ||
extend_instance_ttl(&e); | ||
Ok(()) | ||
} | ||
|
||
fn asset(e: Env) -> Result<Address, StrategyError> { | ||
check_initialized(&e)?; | ||
extend_instance_ttl(&e); | ||
Ok(get_underlying_asset(&e)) | ||
} | ||
|
||
fn deposit( | ||
e: Env, | ||
amount: i128, | ||
from: Address, | ||
) -> Result<(), StrategyError> { | ||
check_initialized(&e)?; | ||
check_nonnegative_amount(amount)?; | ||
extend_instance_ttl(&e); | ||
from.require_auth(); | ||
|
||
update_yield_balance(&e, &from); | ||
|
||
let contract_address = e.current_contract_address(); | ||
let underlying_asset = get_underlying_asset(&e); | ||
TokenClient::new(&e, &underlying_asset).transfer(&from, &contract_address, &amount); | ||
|
||
receive_balance(&e, from.clone(), amount); | ||
|
||
set_last_harvest_time(&e, e.ledger().timestamp(), from.clone()); | ||
event::emit_deposit(&e, String::from_str(&e, STRATEGY_NAME), amount, from); | ||
|
||
Ok(()) | ||
} | ||
|
||
fn harvest(e: Env, from: Address) -> Result<(), StrategyError> { | ||
check_initialized(&e)?; | ||
extend_instance_ttl(&e); | ||
|
||
let yield_balance = update_yield_balance(&e, &from); | ||
|
||
if yield_balance == 0 { | ||
return Ok(()); | ||
} | ||
|
||
// Transfer the reward tokens to the user's balance | ||
spend_yield(&e, from.clone(), yield_balance)?; | ||
receive_balance(&e, from.clone(), yield_balance); | ||
|
||
event::emit_harvest(&e, String::from_str(&e, STRATEGY_NAME), yield_balance, from); | ||
|
||
Ok(()) | ||
} | ||
|
||
fn withdraw( | ||
e: Env, | ||
amount: i128, | ||
from: Address, | ||
) -> Result<i128, StrategyError> { | ||
from.require_auth(); | ||
check_initialized(&e)?; | ||
check_nonnegative_amount(amount)?; | ||
extend_instance_ttl(&e); | ||
|
||
spend_balance(&e, from.clone(), amount)?; | ||
|
||
let contract_address = e.current_contract_address(); | ||
let underlying_asset = get_underlying_asset(&e); | ||
TokenClient::new(&e, &underlying_asset).transfer(&contract_address, &from, &amount); | ||
event::emit_withdraw(&e, String::from_str(&e, STRATEGY_NAME), amount, from); | ||
|
||
Ok(amount) | ||
} | ||
|
||
fn balance( | ||
e: Env, | ||
from: Address, | ||
) -> Result<i128, StrategyError> { | ||
check_initialized(&e)?; | ||
extend_instance_ttl(&e); | ||
Ok(read_balance(&e, from)) | ||
} | ||
} | ||
|
||
|
||
fn calculate_yield(user_balance: i128, apr: u32, time_elapsed: u64) -> i128 { | ||
// Calculate yield based on the APR, time elapsed, and user's balance | ||
let seconds_per_year = SECONDS_PER_YEAR; | ||
let apr_bps = apr as i128; | ||
let time_elapsed_i128 = time_elapsed as i128; | ||
|
||
(user_balance * apr_bps * time_elapsed_i128) / (seconds_per_year * MAX_BPS) | ||
} | ||
|
||
fn update_yield_balance(e: &Env, from: &Address) -> i128 { | ||
let apr = get_apr(e); | ||
let last_harvest = get_last_harvest_time(e, from.clone()); | ||
let time_elapsed = e.ledger().timestamp().saturating_sub(last_harvest); | ||
|
||
if time_elapsed == 0 { | ||
return 0; | ||
} | ||
|
||
let user_balance = read_balance(e, from.clone()); | ||
let reward_amount = calculate_yield(user_balance, apr, time_elapsed); | ||
|
||
if reward_amount == 0 { | ||
return 0; | ||
} | ||
|
||
receive_yield(e, from.clone(), reward_amount); | ||
set_last_harvest_time(e, e.ledger().timestamp(), from.clone()); | ||
read_yield(e, from.clone()) | ||
} | ||
|
||
mod test; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
use soroban_sdk::{contracttype, Address, Env}; | ||
|
||
#[derive(Clone)] | ||
#[contracttype] | ||
pub enum DataKey { | ||
Initialized, | ||
UnderlyingAsset, | ||
Balance(Address), | ||
YieldBalance(Address), | ||
Apr, | ||
LastHarvestTime(Address), | ||
} | ||
|
||
const DAY_IN_LEDGERS: u32 = 17280; | ||
pub const INSTANCE_BUMP_AMOUNT: u32 = 30 * DAY_IN_LEDGERS; | ||
pub const INSTANCE_LIFETIME_THRESHOLD: u32 = INSTANCE_BUMP_AMOUNT - DAY_IN_LEDGERS; | ||
|
||
pub fn extend_instance_ttl(e: &Env) { | ||
e.storage() | ||
.instance() | ||
.extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); | ||
} | ||
|
||
pub fn set_initialized(e: &Env) { | ||
e.storage().instance().set(&DataKey::Initialized, &true); | ||
} | ||
|
||
pub fn is_initialized(e: &Env) -> bool { | ||
e.storage().instance().has(&DataKey::Initialized) | ||
} | ||
|
||
// Underlying asset | ||
pub fn set_underlying_asset(e: &Env, address: &Address) { | ||
e.storage().instance().set(&DataKey::UnderlyingAsset, &address); | ||
} | ||
|
||
pub fn get_underlying_asset(e: &Env) -> Address { | ||
e.storage().instance().get(&DataKey::UnderlyingAsset).unwrap() | ||
} | ||
|
||
// Apr | ||
pub fn set_apr(e: &Env, apr: u32) { | ||
e.storage().instance().set(&DataKey::Apr, &apr); | ||
} | ||
|
||
pub fn get_apr(e: &Env) -> u32 { | ||
e.storage().instance().get(&DataKey::Apr).unwrap() | ||
} | ||
|
||
// Last harvest time | ||
pub fn set_last_harvest_time(e: &Env, timestamp: u64, from: Address) { | ||
e.storage().instance().set(&DataKey::LastHarvestTime(from), ×tamp); | ||
} | ||
|
||
pub fn get_last_harvest_time(e: &Env, from: Address) -> u64 { | ||
e.storage().instance().get(&DataKey::LastHarvestTime(from)).unwrap_or(0) | ||
} |
Oops, something went wrong.