Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/paltalabs/defindex into fea…
Browse files Browse the repository at this point in the history
…t/refactorModels
  • Loading branch information
esteblock committed Nov 11, 2024
2 parents 9fcc8f5 + 7eefd81 commit 9b47823
Show file tree
Hide file tree
Showing 23 changed files with 857 additions and 51 deletions.
8 changes: 8 additions & 0 deletions apps/contracts/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/contracts/factory/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ mod defindex_vault_contract {

// Create Test Token
pub(crate) fn create_token_contract<'a>(e: &Env, admin: &Address) -> SorobanTokenClient<'a> {
SorobanTokenClient::new(e, &e.register_stellar_asset_contract(admin.clone()))
SorobanTokenClient::new(e, &e.register_stellar_asset_contract_v2(admin.clone()).address())
}

pub(crate) fn get_token_admin_client<'a>(
Expand Down
2 changes: 1 addition & 1 deletion apps/contracts/factory/src/test/all_flow.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use soroban_sdk::{vec, BytesN, Map, String, Vec};
use soroban_sdk::{vec, BytesN, String};

use crate::test::{create_asset_params, defindex_vault_contract::{self, Investment}, DeFindexFactoryTest};

Expand Down
2 changes: 1 addition & 1 deletion apps/contracts/factory/src/test/initialize.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use soroban_sdk::{vec, Address, BytesN, String, Vec};
use soroban_sdk::{BytesN, String};

use crate::error::FactoryError;
use crate::test::{create_asset_params, DeFindexFactoryTest};
Expand Down
18 changes: 18 additions & 0 deletions apps/contracts/strategies/fixed_apr/Cargo.toml
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"] }
17 changes: 17 additions & 0 deletions apps/contracts/strategies/fixed_apr/Makefile
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
43 changes: 43 additions & 0 deletions apps/contracts/strategies/fixed_apr/src/balance.rs
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(())
}
2 changes: 2 additions & 0 deletions apps/contracts/strategies/fixed_apr/src/constants.rs
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;
181 changes: 181 additions & 0 deletions apps/contracts/strategies/fixed_apr/src/lib.rs
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;
57 changes: 57 additions & 0 deletions apps/contracts/strategies/fixed_apr/src/storage.rs
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), &timestamp);
}

pub fn get_last_harvest_time(e: &Env, from: Address) -> u64 {
e.storage().instance().get(&DataKey::LastHarvestTime(from)).unwrap_or(0)
}
Loading

0 comments on commit 9b47823

Please sign in to comment.