diff --git a/apps/contracts/Cargo.lock b/apps/contracts/Cargo.lock index c4c169b0..64415e34 100644 --- a/apps/contracts/Cargo.lock +++ b/apps/contracts/Cargo.lock @@ -92,6 +92,14 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "blend_strategy" +version = "1.0.0" +dependencies = [ + "defindex-strategy-core", + "soroban-sdk", +] + [[package]] name = "block-buffer" version = "0.10.4" diff --git a/apps/contracts/strategies/blend/Cargo.toml b/apps/contracts/strategies/blend/Cargo.toml new file mode 100644 index 00000000..cf1a7b29 --- /dev/null +++ b/apps/contracts/strategies/blend/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "blend_strategy" +version = { workspace = true } +authors = ["coderipper "] +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"] } diff --git a/apps/contracts/strategies/blend/Makefile b/apps/contracts/strategies/blend/Makefile new file mode 100644 index 00000000..4dd2581a --- /dev/null +++ b/apps/contracts/strategies/blend/Makefile @@ -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/blend_strategy.wasm + @rm ../../target/wasm32-unknown-unknown/release/blend_strategy.wasm + +fmt: + cargo fmt --all --check + +clean: + cargo clean \ No newline at end of file diff --git a/apps/contracts/strategies/blend/src/blend_pool.rs b/apps/contracts/strategies/blend/src/blend_pool.rs new file mode 100644 index 00000000..69e25274 --- /dev/null +++ b/apps/contracts/strategies/blend/src/blend_pool.rs @@ -0,0 +1,63 @@ +use soroban_sdk::{vec, Address, Env, Vec}; + +use crate::storage::{get_blend_pool, get_underlying_asset}; + +soroban_sdk::contractimport!( + file = "../external_wasms/blend/blend_pool.wasm" +); +pub type BlendPoolClient<'a> = Client<'a>; + +// Define the RequestType enum with explicit u32 values +#[derive(Clone, PartialEq)] +#[repr(u32)] +pub enum RequestType { + // Supply = 0, + // Withdraw = 1, + SupplyCollateral = 2, + WithdrawCollateral = 3, + // Borrow = 4, + // Repay = 5, + // FillUserLiquidationAuction = 6, + // FillBadDebtAuction = 7, + // FillInterestAuction = 8, + // DeleteLiquidationAuction = 9, +} + +// Implement a method to convert RequestType to u32 +impl RequestType { + fn to_u32(self) -> u32 { + self as u32 + } +} + +pub fn submit(e: &Env, from: &Address, amount: i128, request_type: RequestType) -> Positions { + // Setting up Blend Pool client + let blend_pool_address = get_blend_pool(e); + let blend_pool_client = BlendPoolClient::new(e, &blend_pool_address); + + let underlying_asset = get_underlying_asset(&e); + + let requests: Vec = vec![&e, Request { + address: underlying_asset, + amount: amount, + request_type: request_type.to_u32(), + }]; + + blend_pool_client.submit(from, from, from, &requests) +} + +pub fn claim(e: &Env, from: &Address) -> i128 { + // Setting up Blend Pool client + let blend_pool_address = get_blend_pool(e); + let blend_pool_client = BlendPoolClient::new(e, &blend_pool_address); + + blend_pool_client.claim(from, &vec![&e, 3u32], from) +} + +pub fn get_positions(e: &Env, from: &Address) -> Positions { + // Setting up Blend Pool client + let blend_pool_address = get_blend_pool(e); + let blend_pool_client = BlendPoolClient::new(e, &blend_pool_address); + + blend_pool_client.get_positions(from) +} \ No newline at end of file diff --git a/apps/contracts/strategies/blend/src/lib.rs b/apps/contracts/strategies/blend/src/lib.rs new file mode 100644 index 00000000..1cf33d72 --- /dev/null +++ b/apps/contracts/strategies/blend/src/lib.rs @@ -0,0 +1,125 @@ +#![no_std] +use blend_pool::RequestType; +use soroban_sdk::{ + contract, contractimpl, Address, Env, IntoVal, String, Val, Vec}; + +mod blend_pool; +mod storage; + +use storage::{ + extend_instance_ttl, get_underlying_asset, is_initialized, set_blend_pool, set_initialized, set_underlying_asset +}; + +pub use defindex_strategy_core::{ + DeFindexStrategyTrait, + StrategyError, + event}; + +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 STARETEGY_NAME: &str = "BlendStrategy"; + +#[contract] +struct BlendStrategy; + +#[contractimpl] +impl DeFindexStrategyTrait for BlendStrategy { + fn initialize( + e: Env, + asset: Address, + init_args: Vec, + ) -> Result<(), StrategyError> { + if is_initialized(&e) { + return Err(StrategyError::AlreadyInitialized); + } + + let blend_pool_address = init_args.get(0).ok_or(StrategyError::InvalidArgument)?.into_val(&e); + + set_initialized(&e); + set_blend_pool(&e, blend_pool_address); + set_underlying_asset(&e, &asset); + + event::emit_initialize(&e, String::from_str(&e, STARETEGY_NAME), asset); + extend_instance_ttl(&e); + Ok(()) + } + + fn asset(e: Env) -> Result { + 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(); + + blend_pool::submit(&e, &from, amount, RequestType::SupplyCollateral); + + event::emit_deposit(&e, String::from_str(&e, STARETEGY_NAME), amount, from); + Ok(()) + } + + fn harvest(e: Env, from: Address) -> Result<(), StrategyError> { + check_initialized(&e)?; + extend_instance_ttl(&e); + + blend_pool::claim(&e, &from); + + event::emit_harvest(&e, String::from_str(&e, STARETEGY_NAME), 0i128, from); + Ok(()) + } + + fn withdraw( + e: Env, + amount: i128, + from: Address, + ) -> Result { + from.require_auth(); + check_initialized(&e)?; + check_nonnegative_amount(amount)?; + extend_instance_ttl(&e); + + blend_pool::submit(&e, &from, amount, RequestType::WithdrawCollateral); + + event::emit_withdraw(&e, String::from_str(&e, STARETEGY_NAME), amount, from); + + Ok(amount) + } + + fn balance( + e: Env, + from: Address, + ) -> Result { + check_initialized(&e)?; + extend_instance_ttl(&e); + + let positions = blend_pool::get_positions(&e, &from); + + let collateral = positions.collateral.get(1u32).unwrap_or(0i128); + Ok(collateral) + } +} + +mod test; \ No newline at end of file diff --git a/apps/contracts/strategies/blend/src/storage.rs b/apps/contracts/strategies/blend/src/storage.rs new file mode 100644 index 00000000..dbdc3239 --- /dev/null +++ b/apps/contracts/strategies/blend/src/storage.rs @@ -0,0 +1,47 @@ +use soroban_sdk::{contracttype, Address, Env}; + +#[derive(Clone)] +#[contracttype] + +pub enum DataKey { + Initialized, + UnderlyingAsset, + BlendPool, + Balance(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() +} + +// Blend Pool Address +pub fn set_blend_pool(e: &Env, address: Address) { + e.storage().instance().set(&DataKey::BlendPool, &address); +} + +pub fn get_blend_pool(e: &Env) -> Address { + e.storage().instance().get(&DataKey::BlendPool).unwrap() +} \ No newline at end of file diff --git a/apps/contracts/strategies/blend/src/test.rs b/apps/contracts/strategies/blend/src/test.rs new file mode 100644 index 00000000..ba5282eb --- /dev/null +++ b/apps/contracts/strategies/blend/src/test.rs @@ -0,0 +1,79 @@ +#![cfg(test)] +use crate::{BlendStrategy, BlendStrategyClient, StrategyError}; + +use soroban_sdk::token::{TokenClient, StellarAssetClient}; + +use soroban_sdk::{ + Env, + Address, + testutils::Address as _, +}; + +// mod blend_pool_module { +// soroban_sdk::contractimport!(file = "../external_wasms/blend/blend_pool.wasm"); +// pub type BlendPoolContractClient<'a> = Client<'a>; +// } + +// use blend_pool_module::BlendPoolContractClient; + +// // fn initialize(admin: address, name: string, oracle: address, bstop_rate: u32, max_postions: u32, backstop_id: address, blnd_id: address) + +// fn create_blend_pool_contract<'a>(e: &Env, asset: &Address, init_args: &Vec) -> BlendPoolContractClient<'a> { +// let address = &e.register_contract_wasm(None, hodl_strategy::WASM); +// let strategy = BlendPoolContractClient::new(e, address); +// strategy.initialize(asset, init_args); +// strategy +// } + +// Blend Strategy Contract +fn create_blend_strategy<'a>(e: &Env) -> BlendStrategyClient<'a> { + BlendStrategyClient::new(e, &e.register_contract(None, BlendStrategy {})) +} + +// Create Test Token +pub(crate) fn create_token_contract<'a>(e: &Env, admin: &Address) -> TokenClient<'a> { + TokenClient::new(e, &e.register_stellar_asset_contract_v2(admin.clone()).address()) +} + +pub struct HodlStrategyTest<'a> { + env: Env, + strategy: BlendStrategyClient<'a>, + token: TokenClient<'a>, + user: Address, +} + +impl<'a> HodlStrategyTest<'a> { + fn setup() -> Self { + + let env = Env::default(); + env.mock_all_auths(); + + let strategy = create_blend_strategy(&env); + let admin = Address::generate(&env); + let token = create_token_contract(&env, &admin); + let user = Address::generate(&env); + + // Mint 1,000,000,000 to user + StellarAssetClient::new(&env, &token.address).mint(&user, &1_000_000_000); + + HodlStrategyTest { + env, + strategy, + token, + user + } + } + + // pub(crate) fn generate_random_users(e: &Env, users_count: u32) -> vec::Vec
{ + // let mut users = vec![]; + // for _c in 0..users_count { + // users.push(Address::generate(e)); + // } + // users + // } +} + +mod initialize; +mod deposit; +mod events; +mod withdraw; \ No newline at end of file diff --git a/apps/contracts/strategies/blend/src/test/deposit.rs b/apps/contracts/strategies/blend/src/test/deposit.rs new file mode 100644 index 00000000..3267f974 --- /dev/null +++ b/apps/contracts/strategies/blend/src/test/deposit.rs @@ -0,0 +1,79 @@ +use crate::test::HodlStrategyTest; +use crate::test::StrategyError; +use soroban_sdk::{IntoVal, Vec, Val}; + +// test deposit with negative amount +#[test] +fn deposit_with_negative_amount() { + let test = HodlStrategyTest::setup(); + let init_fn_args: Vec = (0,).into_val(&test.env); + test.strategy.initialize(&test.token.address, &init_fn_args); + + let amount = -123456; + + let result = test.strategy.try_deposit(&amount, &test.user); + assert_eq!(result, Err(Ok(StrategyError::NegativeNotAllowed))); +} + +// check auth +#[test] +fn deposit_mock_auths() { + todo!() +} + +#[test] +fn deposit_and_withdrawal_flow() { + let test = HodlStrategyTest::setup(); + // let users = HodlStrategyTest::generate_random_users(&test.env, 1); + + // try deposit should return NotInitialized error before being initialize + + let result = test.strategy.try_deposit(&10_000_000, &test.user); + assert_eq!(result, Err(Ok(StrategyError::NotInitialized))); + + // initialize + let init_fn_args: Vec = (0,).into_val(&test.env); + test.strategy.initialize(&test.token.address, &init_fn_args); + + // Initial user token balance + let balance = test.token.balance(&test.user); + + let amount = 123456; + + // Deposit amount of token from the user to the strategy + test.strategy.deposit(&amount, &test.user); + + let balance_after_deposit = test.token.balance(&test.user); + assert_eq!(balance_after_deposit, balance - amount); + + // Reading strategy balance + let strategy_balance_after_deposit = test.token.balance(&test.strategy.address); + assert_eq!(strategy_balance_after_deposit, amount); + + // Reading user balance on strategy contract + let user_balance_on_strategy = test.strategy.balance(&test.user); + assert_eq!(user_balance_on_strategy, amount); + + + let amount_to_withdraw = 100_000; + // Withdrawing token from the strategy to user + test.strategy.withdraw(&amount_to_withdraw, &test.user); + + // Reading user balance in token + let balance = test.token.balance(&test.user); + assert_eq!(balance, balance_after_deposit + amount_to_withdraw); + + // Reading strategy balance in token + let balance = test.token.balance(&test.strategy.address); + assert_eq!(balance, amount - amount_to_withdraw); + + // Reading user balance on strategy contract + let user_balance = test.strategy.balance(&test.user); + assert_eq!(user_balance, amount - amount_to_withdraw); + + // now we will want to withdraw more of the remaining balance + let amount_to_withdraw = 200_000; + let result = test.strategy.try_withdraw(&amount_to_withdraw, &test.user); + assert_eq!(result, Err(Ok(StrategyError::InsufficientBalance))); + +} \ No newline at end of file diff --git a/apps/contracts/strategies/blend/src/test/events.rs b/apps/contracts/strategies/blend/src/test/events.rs new file mode 100644 index 00000000..239a9bd1 --- /dev/null +++ b/apps/contracts/strategies/blend/src/test/events.rs @@ -0,0 +1,6 @@ +// TODO: Write tests for events + +#[test] +fn test_events() { + todo!() +} \ No newline at end of file diff --git a/apps/contracts/strategies/blend/src/test/initialize.rs b/apps/contracts/strategies/blend/src/test/initialize.rs new file mode 100644 index 00000000..41037473 --- /dev/null +++ b/apps/contracts/strategies/blend/src/test/initialize.rs @@ -0,0 +1,21 @@ +// Cannot Initialize twice +extern crate std; +use soroban_sdk::{IntoVal, Vec, Val}; +use crate::test::HodlStrategyTest; +use crate::test::StrategyError; + +#[test] +fn cannot_initialize_twice() { + let test = HodlStrategyTest::setup(); + + let init_fn_args: Vec = (0,).into_val(&test.env); + + test.strategy.initialize(&test.token.address, &init_fn_args); + let result = test.strategy.try_initialize(&test.token.address , &init_fn_args); + assert_eq!(result, Err(Ok(StrategyError::AlreadyInitialized))); + + // get asset should return underlying asset + + let underlying_asset = test.strategy.asset(); + assert_eq!(underlying_asset, test.token.address); +} \ No newline at end of file diff --git a/apps/contracts/strategies/blend/src/test/withdraw.rs b/apps/contracts/strategies/blend/src/test/withdraw.rs new file mode 100644 index 00000000..dd8aa9d5 --- /dev/null +++ b/apps/contracts/strategies/blend/src/test/withdraw.rs @@ -0,0 +1,5 @@ + +#[test] +fn withdraw() { + todo!() +} \ No newline at end of file diff --git a/apps/contracts/strategies/external_wasms/blend/blend_pool.wasm b/apps/contracts/strategies/external_wasms/blend/blend_pool.wasm new file mode 100644 index 00000000..522dcccb Binary files /dev/null and b/apps/contracts/strategies/external_wasms/blend/blend_pool.wasm differ