diff --git a/apps/contracts/Cargo.lock b/apps/contracts/Cargo.lock index e656b7bc..39fb2feb 100644 --- a/apps/contracts/Cargo.lock +++ b/apps/contracts/Cargo.lock @@ -97,6 +97,9 @@ name = "blend_strategy" version = "0.1.0" dependencies = [ "defindex-strategy-core", + "sep-40-oracle", + "sep-41-token", + "soroban-fixed-point-math", "soroban-sdk", ] @@ -927,6 +930,24 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +[[package]] +name = "sep-40-oracle" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "019c355be5fa5dac942350fff686cfd97fb6cd5302cefb69fae3ac7ec15ac72d" +dependencies = [ + "soroban-sdk", +] + +[[package]] +name = "sep-41-token" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c181783c38f2ffd99cd97c66b5e2a8f7f2e8ebfb15441d58f74485d1e1cfa20" +dependencies = [ + "soroban-sdk", +] + [[package]] name = "serde" version = "1.0.192" @@ -1114,6 +1135,15 @@ dependencies = [ "syn", ] +[[package]] +name = "soroban-fixed-point-math" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d386a1ca0a148121b21331f9da68f33bf3dfb6de69646f719935d2dec3d49c" +dependencies = [ + "soroban-sdk", +] + [[package]] name = "soroban-ledger-snapshot" version = "21.7.6" diff --git a/apps/contracts/package.json b/apps/contracts/package.json index 4ef8a7bc..3b52c8b4 100644 --- a/apps/contracts/package.json +++ b/apps/contracts/package.json @@ -11,6 +11,8 @@ "publish-addresses": "tsc && node dist/publish_addresses.js", "test": "tsc && node dist/test.js", "test-vault": "tsc && node dist/tests/testOnlyVault.js", + "test-blend-strategy": "tsc && node dist/tests/blend/test_strategy.js", + "test-blend-vault": "tsc && node dist/tests/blend/test_vault.js", "test-dev": "tsc && node dist/tests/dev.js", "test-two-strat-vault": "tsc && node dist/tests/testTwoStrategiesVault.js" }, diff --git a/apps/contracts/src/strategies/deploy_blend.ts b/apps/contracts/src/strategies/deploy_blend.ts index 7b6db3b5..73ab8b37 100644 --- a/apps/contracts/src/strategies/deploy_blend.ts +++ b/apps/contracts/src/strategies/deploy_blend.ts @@ -1,4 +1,4 @@ -import { Address, Asset, Networks, xdr } from "@stellar/stellar-sdk"; +import { Address, Asset, nativeToScVal, Networks, xdr } from "@stellar/stellar-sdk"; import { AddressBook } from "../utils/address_book.js"; import { airdropAccount, @@ -47,6 +47,9 @@ export async function deployBlendStrategy(addressBook: AddressBook) { const initArgs = xdr.ScVal.scvVec([ new Address("CCEVW3EEW4GRUZTZRTAMJAXD6XIF5IG7YQJMEEMKMVVGFPESTRXY2ZAV").toScVal(), //Blend pool on testnet! + nativeToScVal(0, { type: "u32" }), // ReserveId 0 is XLM + new Address("CB22KRA3YZVCNCQI64JQ5WE7UY2VAV7WFLK6A2JN3HEX56T2EDAFO7QF").toScVal(), // BLND Token + new Address("CAG5LRYQ5JVEUI5TEID72EYOVX44TTUJT5BQR2J6J77FH65PCCFAJDDH").toScVal(), // Soroswap router ]); const args: xdr.ScVal[] = [ diff --git a/apps/contracts/src/tests/blend/test_strategy.ts b/apps/contracts/src/tests/blend/test_strategy.ts new file mode 100644 index 00000000..443ea4a1 --- /dev/null +++ b/apps/contracts/src/tests/blend/test_strategy.ts @@ -0,0 +1,125 @@ +import { Address, Keypair, nativeToScVal, scValToNative, xdr } from "@stellar/stellar-sdk"; +import { AddressBook } from "../../utils/address_book.js"; +import { airdropAccount, invokeContract } from "../../utils/contract.js"; + +const network = process.argv[2]; +const addressBook = AddressBook.loadFromFile(network); + +const purple = '\x1b[35m%s\x1b[0m'; +const green = '\x1b[32m%s\x1b[0m'; + +export async function testBlendStrategy(user?: Keypair) { + // Create and fund a new user account if not provided + const newUser = Keypair.random(); + console.log(green, '----------------------- New account created -------------------------') + console.log(green, 'Public key: ',newUser.publicKey()) + console.log(green, '---------------------------------------------------------------------') + + if (network !== "mainnet") { + console.log(purple, '-------------------------------------------------------------------') + console.log(purple, '----------------------- Funding new account -----------------------') + console.log(purple, '-------------------------------------------------------------------') + await airdropAccount(newUser); + } + + try { + // Deposit XLM into Blend Strategy + console.log(purple, '---------------------------------------------------------------------------') + console.log(purple, '----------------------- Depositing XLM to the Strategy -----------------------') + console.log(purple, '---------------------------------------------------------------------------') + const depositParams: xdr.ScVal[] = [ + nativeToScVal(1000_0_000_000, { type: "i128" }), + new Address(newUser.publicKey()).toScVal(), + ] + const depositResult = await invokeContract( + 'blend_strategy', + addressBook, + 'deposit', + depositParams, + newUser, + false + ); + console.log('🚀 « depositResult:', depositResult); + const depositResultValue = scValToNative(depositResult.returnValue); + + console.log(green, '------------ XLM deposited to the Strategy ------------') + console.log(green, 'depositResult', depositResultValue) + console.log(green, '----------------------------------------------------') + }catch(e){ + console.log('error', e) + } + + // Wait for 1 minute + console.log(purple, '---------------------------------------------------------------------------') + console.log(purple, '----------------------- Waiting for 1 minute -----------------------') + console.log(purple, '---------------------------------------------------------------------------') + await new Promise(resolve => setTimeout(resolve, 100)); + + try { + // Withdrawing XLM from Blend Strategy + console.log(purple, '---------------------------------------------------------------------------') + console.log(purple, '----------------------- Withdrawing XLM from the Strategy -----------------------') + console.log(purple, '---------------------------------------------------------------------------') + + const balanceScVal = await invokeContract( + 'blend_strategy', + addressBook, + 'balance', + [new Address(newUser.publicKey()).toScVal()], + newUser, + true + ); + console.log('🚀 « balanceScVal:', balanceScVal); + + const balance = scValToNative(balanceScVal.result.retval); + console.log('🚀 « balance:', balance); + + const withdrawParams: xdr.ScVal[] = [ + nativeToScVal(1000_0_000_000, { type: "i128" }), + new Address(newUser.publicKey()).toScVal(), + ] + const withdrawResult = await invokeContract( + 'blend_strategy', + addressBook, + 'withdraw', + withdrawParams, + newUser, + false + ); + const withdrawResultValue = scValToNative(withdrawResult.returnValue); + + console.log(green, '------------ XLM withdrawed from the Strategy ------------') + console.log(green, 'withdrawResult', withdrawResultValue) + console.log(green, '----------------------------------------------------') + }catch(e){ + console.log('error', e) + } + + try { + // Harvest rewards from Blend Strategy + console.log(purple, '---------------------------------------------------------------------------') + console.log(purple, '----------------------- Harvesting from the Strategy -----------------------') + console.log(purple, '---------------------------------------------------------------------------') + + const harvestParams: xdr.ScVal[] = [ + new Address(newUser.publicKey()).toScVal(), + ] + const harvestResult = await invokeContract( + 'blend_strategy', + addressBook, + 'harvest', + harvestParams, + newUser, + false + ); + const harvestResultValue = scValToNative(harvestResult.returnValue); + + console.log(green, '------------ BLND Harvested from the vault ------------') + console.log(green, 'harvestResult', harvestResultValue) + console.log(green, '----------------------------------------------------') + }catch(e){ + console.log('error', e) + } +} + +await testBlendStrategy(); \ No newline at end of file diff --git a/apps/contracts/src/tests/blend/test_vault.ts b/apps/contracts/src/tests/blend/test_vault.ts new file mode 100644 index 00000000..22038029 --- /dev/null +++ b/apps/contracts/src/tests/blend/test_vault.ts @@ -0,0 +1,209 @@ +import { Address, Asset, Keypair, nativeToScVal, Networks, scValToNative, xdr } from "@stellar/stellar-sdk"; +import { randomBytes } from "crypto"; +import { exit } from "process"; +import { AddressBook } from "../../utils/address_book.js"; +import { airdropAccount, invokeContract } from "../../utils/contract.js"; +import { config } from "../../utils/env_config.js"; +import { AssetInvestmentAllocation, depositToVault, investVault } from "../vault.js"; + +const network = process.argv[2]; +const loadedConfig = config(network); +const addressBook = AddressBook.loadFromFile(network); + +const purple = '\x1b[35m%s\x1b[0m'; +const green = '\x1b[32m%s\x1b[0m'; + + + +export async function testBlendVault(user?: Keypair) { + const newUser = Keypair.random(); + console.log(green, '----------------------- New account created -------------------------') + console.log(green, 'Public key: ',newUser.publicKey()) + console.log(green, '---------------------------------------------------------------------') + + if (network !== "mainnet") { + console.log(purple, '-------------------------------------------------------------------') + console.log(purple, '----------------------- Funding new account -----------------------') + console.log(purple, '-------------------------------------------------------------------') + await airdropAccount(newUser); + } + + console.log("Setting Emergengy Manager, Fee Receiver and Manager accounts"); + const emergencyManager = loadedConfig.getUser("DEFINDEX_EMERGENCY_MANAGER_SECRET_KEY"); + if (network !== "mainnet") await airdropAccount(emergencyManager); + + const feeReceiver = loadedConfig.getUser("DEFINDEX_FEE_RECEIVER_SECRET_KEY"); + if (network !== "mainnet") await airdropAccount(feeReceiver); + + const manager = loadedConfig.getUser("DEFINDEX_MANAGER_SECRET_KEY"); + if (network !== "mainnet") await airdropAccount(manager); + + const blendStrategyAddress = addressBook.getContractId("blend_strategy"); + + const xlm = Asset.native(); + let xlmContractId: string; + switch (network) { + case "testnet": + xlmContractId = xlm.contractId(Networks.TESTNET); + break; + case "mainnet": + xlmContractId = xlm.contractId(Networks.PUBLIC); + break; + default: + console.log("Invalid network:", network, "It should be either testnet or mainnet"); + return; + } + + const assets = [ + { + address: new Address(xlmContractId), + strategies: [ + { + name: "Blend Strategy", + address: blendStrategyAddress, + paused: false + }, + ] + } + ]; + + const assetAllocations = assets.map((asset) => { + return xdr.ScVal.scvMap([ + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol("address"), + val: asset.address.toScVal(), + }), + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol("strategies"), + val: xdr.ScVal.scvVec( + asset.strategies.map((strategy) => + xdr.ScVal.scvMap([ + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol("address"), + val: new Address(strategy.address).toScVal(), + }), + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol("name"), + val: nativeToScVal(strategy.name, { type: "string" }), + }), + new xdr.ScMapEntry({ + key: xdr.ScVal.scvSymbol("paused"), + val: nativeToScVal(false, { type: "bool" }), + }), + ]) + ) + ), + }), + ]); + }); + + const createDeFindexParams: xdr.ScVal[] = [ + new Address(emergencyManager.publicKey()).toScVal(), + new Address(feeReceiver.publicKey()).toScVal(), + nativeToScVal(100, { type: "u32" }), + nativeToScVal("BLND Vault", { type: "string" }), + nativeToScVal("BLNVLT", { type: "string" }), + new Address(manager.publicKey()).toScVal(), + xdr.ScVal.scvVec(assetAllocations), + nativeToScVal(randomBytes(32)), + ]; + + const initialAmount = 100_0_000_000; + let blendVaultAddress: string = ""; + + try { + console.log(purple, '--------------------------------------------------------------') + console.log(purple, '----------------------- Creating vault -----------------------') + console.log(purple, '--------------------------------------------------------------') + const createResult = await invokeContract( + 'defindex_factory', + addressBook, + 'create_defindex_vault', + createDeFindexParams, + manager, + false + ); + + blendVaultAddress = scValToNative(createResult.returnValue); + console.log(green, '----------------------- Vault created -------------------------') + console.log(green, 'createResult', blendVaultAddress) + console.log(green, '---------------------------------------------------------------') + } catch(e){ + console.log('❌ Error Creating the vault', e) + exit("Error Creating"); + } + + try { + // Deposit assets to the vault + console.log(purple, '---------------------------------------------------------------------------') + console.log(purple, '----------------------- Depositing XLM to the vault -----------------------') + console.log(purple, '---------------------------------------------------------------------------') + const { user, balanceBefore: depositBalanceBefore, result: depositResult, balanceAfter: depositBalanceAfter } + = await depositToVault(blendVaultAddress, [initialAmount], newUser, false); + + console.log(green, '------------ XLM deposited to the vault ------------') + console.log(green, 'Deposit balance before: ', depositBalanceBefore) + console.log(green, 'depositResult', depositResult) + console.log(green, 'Deposit balance after: ', depositBalanceAfter) + console.log(green, '----------------------------------------------------') + } catch (error) { + console.log('❌ Error depositing into the vault:', error); + exit("Error Depositing"); + } + + try { + // Invest in strategy + console.log(purple, '---------------------------------------------------------------------------') + console.log(purple, '-------------------------- Investing in strategy --------------------------') + console.log(purple, '---------------------------------------------------------------------------') + + const investParams: AssetInvestmentAllocation[] = [ + { + asset: new Address(xlmContractId), + strategy_investments: [ + { + amount: BigInt(50_0_000_000), + strategy: new Address(blendStrategyAddress) + } + ] + } + ]; + + const investResult = await investVault(blendVaultAddress, investParams, manager) + console.log('🚀 « investResult:', investResult); + + console.log(green, '---------------------- Invested in strategy ----------------------') + console.log(green, 'Invested: ', investResult, ' in the strategy') + console.log(green, '------------------------------------------------------------------') + } catch (error) { + console.log('❌ Error Investing the Vault:', error); + exit("Error Investing"); + } + + // try { + // // Withdraw assets from the vault + // console.log(purple, '------------------------------------------------------------------------------') + // console.log(purple, '----------------------- Withdrawing XLM from the vault -----------------------') + // console.log(purple, '------------------------------------------------------------------------------') + // const withdrawAmount = Math.ceil(100); + // const withdrawParams: xdr.ScVal[] = [ + // nativeToScVal(withdrawAmount, { type: "i128" }), + // new Address(newUser.publicKey()).toScVal(), + // ] + // const withdrawResult = await invokeCustomContract( + // blendVaultAddress, + // 'withdraw', + // withdrawParams, + // newUser, + // false + // ); + // const withdrawResultValue = scValToNative(withdrawResult.returnValue); + // console.log(green, '---------------- XLM withdrawn from the vault ----------------') + // console.log(green, 'Withdrawed: ', withdrawResultValue, ' from the vault') + // console.log(green, '--------------------------------------------------------------') + // } catch (error) { + // console.log('🚀 « error:', error); + + // } +} +await testBlendVault(); \ No newline at end of file diff --git a/apps/contracts/src/tests/vault.ts b/apps/contracts/src/tests/vault.ts index 223385a8..2f09f824 100644 --- a/apps/contracts/src/tests/vault.ts +++ b/apps/contracts/src/tests/vault.ts @@ -42,7 +42,7 @@ export async function depositToVault(deployedVault: string, amount: number[], us const depositParams: xdr.ScVal[] = [ xdr.ScVal.scvVec(amountsDesired.map((amount) => nativeToScVal(amount, { type: "i128" }))), xdr.ScVal.scvVec(amountsMin.map((min) => nativeToScVal(min, { type: "i128" }))), - (new Address(newUser.publicKey())).toScVal(), + new Address(newUser.publicKey()).toScVal(), xdr.ScVal.scvBool(investBool) ]; diff --git a/apps/contracts/strategies/blend/Cargo.toml b/apps/contracts/strategies/blend/Cargo.toml index cf1a7b29..61a40a63 100644 --- a/apps/contracts/strategies/blend/Cargo.toml +++ b/apps/contracts/strategies/blend/Cargo.toml @@ -13,6 +13,9 @@ crate-type = ["cdylib"] [dependencies] soroban-sdk = { workspace = true } defindex-strategy-core = { workspace = true } +soroban-fixed-point-math = "1.2.0" [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } +sep-40-oracle = { version = "1.0.0", features = ["testutils"] } +sep-41-token = { version = " 1.0.0", features = ["testutils"] } diff --git a/apps/contracts/strategies/blend/src/blend_pool.rs b/apps/contracts/strategies/blend/src/blend_pool.rs index 69e25274..f99f4a6f 100644 --- a/apps/contracts/strategies/blend/src/blend_pool.rs +++ b/apps/contracts/strategies/blend/src/blend_pool.rs @@ -1,20 +1,21 @@ -use soroban_sdk::{vec, Address, Env, Vec}; +use defindex_strategy_core::StrategyError; +use soroban_sdk::{auth::{ContractContext, InvokerContractAuthEntry, SubContractInvocation}, panic_with_error, token::TokenClient, vec, Address, Env, IntoVal, Symbol, Vec}; -use crate::storage::{get_blend_pool, get_underlying_asset}; +use crate::{constants::REWARD_THRESHOLD, reserves, soroswap::internal_swap_exact_tokens_for_tokens, storage::{self, Config}}; soroban_sdk::contractimport!( - file = "../external_wasms/blend/blend_pool.wasm" -); + 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, + Supply = 0, + Withdraw = 1, + // SupplyCollateral = 2, + // WithdrawCollateral = 3, // Borrow = 4, // Repay = 5, // FillUserLiquidationAuction = 6, @@ -30,34 +31,127 @@ impl RequestType { } } -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); +pub fn supply(e: &Env, from: &Address, amount: &i128, config: &Config) -> i128 { + let pool_client = BlendPoolClient::new(e, &config.pool); - let underlying_asset = get_underlying_asset(&e); + // Get deposit amount pre-supply + let pre_supply = pool_client + .get_positions(&e.current_contract_address()) + .supply + .get(config.reserve_id) + .unwrap_or(0); let requests: Vec = vec![&e, Request { - address: underlying_asset, - amount: amount, - request_type: request_type.to_u32(), + address: config.asset.clone(), + amount: amount.clone(), + request_type: RequestType::Supply.to_u32(), }]; - blend_pool_client.submit(from, from, from, &requests) + e.authorize_as_current_contract(vec![ + &e, + InvokerContractAuthEntry::Contract(SubContractInvocation { + context: ContractContext { + contract: config.asset.clone(), + fn_name: Symbol::new(&e, "transfer"), + args: ( + e.current_contract_address(), + config.pool.clone(), + amount.clone()).into_val(e), + }, + sub_invocations: vec![&e], + }), + ]); + + let new_positions = pool_client.submit( + &e.current_contract_address(), + &e.current_contract_address(), + &from, + &requests + ); + + // Calculate the amount of bTokens received + let b_tokens_amount = new_positions.supply.get_unchecked(config.reserve_id) - pre_supply; + b_tokens_amount } -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); +pub fn withdraw(e: &Env, from: &Address, amount: &i128, config: &Config) -> (i128, i128) { + let pool_client = BlendPoolClient::new(e, &config.pool); + + let pre_supply = pool_client + .get_positions(&e.current_contract_address()) + .supply + .get(config.reserve_id) + .unwrap_or_else(|| panic_with_error!(e, StrategyError::InsufficientBalance)); + + // Get balance pre-withdraw, as the pool can modify the withdrawal amount + let pre_withdrawal_balance = TokenClient::new(&e, &config.asset).balance(&from); + + let requests: Vec = vec![&e, Request { + address: config.asset.clone(), + amount: amount.clone(), + request_type: RequestType::Withdraw.to_u32(), + }]; - blend_pool_client.claim(from, &vec![&e, 3u32], from) + // Execute the withdrawal - the tokens are transferred from the pool to the vault + let new_positions = pool_client.submit( + &e.current_contract_address(), + &e.current_contract_address(), + &from, + &requests + ); + + // Calculate the amount of tokens withdrawn and bTokens burnt + let post_withdrawal_balance = TokenClient::new(&e, &config.asset).balance(&from); + let real_amount = post_withdrawal_balance - pre_withdrawal_balance; + + // position entry is deleted if the position is cleared + let b_tokens_amount = pre_supply - new_positions.supply.get(config.reserve_id).unwrap_or(0); + (real_amount, b_tokens_amount) } -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); +pub fn claim(e: &Env, from: &Address, config: &Config) -> i128 { + let pool_client = BlendPoolClient::new(e, &config.pool); + + // TODO: Check reserve_token_ids and how to get the correct one + pool_client.claim(from, &vec![&e, config.reserve_id], from) +} + +pub fn perform_reinvest(e: &Env, config: &Config) -> Result{ + // Check the current BLND balance + let blnd_balance = TokenClient::new(e, &config.blend_token).balance(&e.current_contract_address()); + + // If balance does not exceed threshold, skip harvest + if blnd_balance < REWARD_THRESHOLD { + return Ok(false); + } + + // Swap BLND to the underlying asset + let mut swap_path: Vec
= vec![&e]; + swap_path.push_back(config.blend_token.clone()); + swap_path.push_back(config.asset.clone()); + + let deadline = e.ledger().timestamp() + 600; + + // Swapping BLND tokens to Underlying Asset + let swapped_amounts = internal_swap_exact_tokens_for_tokens( + e, + &blnd_balance, + &0i128, + swap_path, + &e.current_contract_address(), + &deadline, + config, + )?; + let amount_out: i128 = swapped_amounts + .get(1) + .ok_or(StrategyError::InvalidArgument)? + .into_val(e); + + // Supplying underlying asset into blend pool + let b_tokens_minted = supply(&e, &e.current_contract_address(), &amount_out, &config); + + let reserves = storage::get_strategy_reserves(&e); + reserves::harvest(&e, reserves, amount_out, b_tokens_minted); - blend_pool_client.get_positions(from) + Ok(true) } \ No newline at end of file diff --git a/apps/contracts/strategies/blend/src/constants.rs b/apps/contracts/strategies/blend/src/constants.rs new file mode 100644 index 00000000..1a61405b --- /dev/null +++ b/apps/contracts/strategies/blend/src/constants.rs @@ -0,0 +1,8 @@ +/// 1 with 7 decimal places +// pub const SCALAR_7: i128 = 1_0000000; +/// 1 with 9 decimal places +pub const SCALAR_9: i128 = 1_000_000_000; +/// The minimum amount of tokens than can be deposited or withdrawn from the vault +pub const MIN_DUST: i128 = 0_0010000; + +pub const REWARD_THRESHOLD: i128 = 500_0000000; \ No newline at end of file diff --git a/apps/contracts/strategies/blend/src/lib.rs b/apps/contracts/strategies/blend/src/lib.rs index 1cf33d72..38c44058 100644 --- a/apps/contracts/strategies/blend/src/lib.rs +++ b/apps/contracts/strategies/blend/src/lib.rs @@ -1,14 +1,16 @@ #![no_std] -use blend_pool::RequestType; +use blend_pool::perform_reinvest; +use constants::{MIN_DUST, SCALAR_9}; use soroban_sdk::{ - contract, contractimpl, Address, Env, IntoVal, String, Val, Vec}; + contract, contractimpl, token::TokenClient, Address, Env, IntoVal, String, Val, Vec}; mod blend_pool; +mod constants; +mod reserves; +mod soroswap; mod storage; -use storage::{ - extend_instance_ttl, get_underlying_asset, is_initialized, set_blend_pool, set_initialized, set_underlying_asset -}; +use storage::{extend_instance_ttl, is_initialized, set_initialized, Config}; pub use defindex_strategy_core::{ DeFindexStrategyTrait, @@ -47,12 +49,23 @@ impl DeFindexStrategyTrait for BlendStrategy { return Err(StrategyError::AlreadyInitialized); } - let blend_pool_address = init_args.get(0).ok_or(StrategyError::InvalidArgument)?.into_val(&e); + let blend_pool_address: Address = init_args.get(0).ok_or(StrategyError::InvalidArgument)?.into_val(&e); + let reserve_id: u32 = init_args.get(1).ok_or(StrategyError::InvalidArgument)?.into_val(&e); + let blend_token: Address = init_args.get(2).ok_or(StrategyError::InvalidArgument)?.into_val(&e); + let soroswap_router: Address = init_args.get(3).ok_or(StrategyError::InvalidArgument)?.into_val(&e); set_initialized(&e); - set_blend_pool(&e, blend_pool_address); - set_underlying_asset(&e, &asset); + let config = Config { + asset: asset.clone(), + pool: blend_pool_address, + reserve_id, + blend_token, + router: soroswap_router, + }; + + storage::set_config(&e, config); + event::emit_initialize(&e, String::from_str(&e, STARETEGY_NAME), asset); extend_instance_ttl(&e); Ok(()) @@ -62,7 +75,7 @@ impl DeFindexStrategyTrait for BlendStrategy { check_initialized(&e)?; extend_instance_ttl(&e); - Ok(get_underlying_asset(&e)) + Ok(storage::get_config(&e).asset) } fn deposit( @@ -75,7 +88,25 @@ impl DeFindexStrategyTrait for BlendStrategy { extend_instance_ttl(&e); from.require_auth(); - blend_pool::submit(&e, &from, amount, RequestType::SupplyCollateral); + // protect against rouding of reserve_vault::update_rate, as small amounts + // can cause incorrect b_rate calculations due to the pool rounding + if amount < MIN_DUST { + return Err(StrategyError::InvalidArgument); //TODO: create a new error type for this + } + + let config = storage::get_config(&e); + blend_pool::claim(&e, &e.current_contract_address(), &config); + perform_reinvest(&e, &config)?; + + let reserves = storage::get_strategy_reserves(&e); + + // transfer tokens from the vault to the strategy contract + TokenClient::new(&e, &config.asset).transfer(&from, &e.current_contract_address(), &amount); + + let b_tokens_minted = blend_pool::supply(&e, &from, &amount, &config); + + // Keeping track of the total deposited amount and the total bTokens owned by the strategy depositors + reserves::deposit(&e, reserves, &from, amount, b_tokens_minted); event::emit_deposit(&e, String::from_str(&e, STARETEGY_NAME), amount, from); Ok(()) @@ -85,9 +116,12 @@ impl DeFindexStrategyTrait for BlendStrategy { check_initialized(&e)?; extend_instance_ttl(&e); - blend_pool::claim(&e, &from); + let config = storage::get_config(&e); + let harvested_blend = blend_pool::claim(&e, &e.current_contract_address(), &config); + + perform_reinvest(&e, &config)?; - event::emit_harvest(&e, String::from_str(&e, STARETEGY_NAME), 0i128, from); + event::emit_harvest(&e, String::from_str(&e, STARETEGY_NAME), harvested_blend, from); Ok(()) } @@ -96,16 +130,28 @@ impl DeFindexStrategyTrait for BlendStrategy { 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); + from.require_auth(); + + // protect against rouding of reserve_vault::update_rate, as small amounts + // can cause incorrect b_rate calculations due to the pool rounding + if amount < MIN_DUST { + return Err(StrategyError::InvalidArgument) //TODO: create a new error type for this + } + + let reserves = storage::get_strategy_reserves(&e); + + let config = storage::get_config(&e); + + let (tokens_withdrawn, b_tokens_burnt) = blend_pool::withdraw(&e, &from, &amount, &config); + + let _burnt_shares = reserves::withdraw(&e, reserves, &from, tokens_withdrawn, b_tokens_burnt); event::emit_withdraw(&e, String::from_str(&e, STARETEGY_NAME), amount, from); - Ok(amount) + Ok(tokens_withdrawn) } fn balance( @@ -114,11 +160,27 @@ impl DeFindexStrategyTrait for BlendStrategy { ) -> 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) + + // Get the vault's shares + let vault_shares = storage::get_vault_shares(&e, &from); + + // Get the strategy's total shares and bTokens + let reserves = storage::get_strategy_reserves(&e); + let total_shares = reserves.total_shares; + let total_b_tokens = reserves.total_b_tokens; + + if total_shares == 0 || total_b_tokens == 0 { + // No shares or bTokens in the strategy + return Ok(0); + } + + // Calculate the bTokens corresponding to the vault's shares + let vault_b_tokens = (vault_shares * total_b_tokens) / total_shares; + + // Use the b_rate to convert bTokens to underlying assets + let underlying_balance = (vault_b_tokens * reserves.b_rate) / SCALAR_9; + + Ok(underlying_balance) } } diff --git a/apps/contracts/strategies/blend/src/reserves.rs b/apps/contracts/strategies/blend/src/reserves.rs new file mode 100644 index 00000000..3369377e --- /dev/null +++ b/apps/contracts/strategies/blend/src/reserves.rs @@ -0,0 +1,147 @@ +use defindex_strategy_core::StrategyError; +use soroban_fixed_point_math::{i128, FixedPoint}; +use soroban_sdk::{contracttype, panic_with_error, Address, Env}; + +use crate::{constants::SCALAR_9, storage}; + +#[contracttype] +pub struct StrategyReserves { + /// The total deposited amount of the underlying asset + pub total_shares: i128, + /// The total bToken deposits owned by the strategy depositors. + pub total_b_tokens: i128, + /// The reserve's last bRate + pub b_rate: i128, +} + +impl StrategyReserves { + /// Converts a b_token amount to shares rounding down + pub fn b_tokens_to_shares_down(&self, amount: i128) -> i128 { + if self.total_shares == 0 || self.total_b_tokens == 0 { + return amount; + } + amount + .fixed_mul_floor(self.total_shares, self.total_b_tokens) + .unwrap() + } + + /// Converts a b_token amount to shares rounding up + pub fn b_tokens_to_shares_up(&self, amount: i128) -> i128 { + if self.total_shares == 0 || self.total_b_tokens == 0 { + return amount; + } + amount + .fixed_mul_ceil(self.total_shares, self.total_b_tokens) + .unwrap() + } + + /// Coverts a share amount to a b_token amount rounding down + pub fn shares_to_b_tokens_down(&self, amount: i128) -> i128 { + amount + .fixed_div_floor(self.total_shares, self.total_b_tokens) + .unwrap() + } + + pub fn update_rate(&mut self, amount: i128, b_tokens: i128) { + // Calculate the new bRate - 9 decimal places of precision + // Update the reserve's bRate + let new_rate = amount + .fixed_div_floor(b_tokens, SCALAR_9) + .unwrap(); + + self.b_rate = new_rate; + } + +} + +/// Deposit into the reserve vault. This function expects the deposit to have already been made +/// into the pool, and accounts for the deposit in the reserve vault. +pub fn deposit( + e: &Env, + mut reserves: StrategyReserves, + from: &Address, + underlying_amount: i128, + b_tokens_amount: i128, +) -> i128 { + if underlying_amount <= 0 { + panic_with_error!(e, StrategyError::InvalidArgument); //TODO: create a new error type for this + } + + if b_tokens_amount <= 0 { + panic_with_error!(e, StrategyError::InvalidArgument); //TODO: create a new error type for this + } + + reserves.update_rate(underlying_amount, b_tokens_amount); + + let mut vault_shares = storage::get_vault_shares(&e, &from); + let share_amount: i128 = reserves.b_tokens_to_shares_down(b_tokens_amount); + + reserves.total_shares += share_amount; + reserves.total_b_tokens += b_tokens_amount; + + vault_shares += share_amount; + + storage::set_strategy_reserves(&e, reserves); + storage::set_vault_shares(&e, &from, vault_shares); + share_amount +} + +/// Withdraw from the reserve vault. This function expects the withdraw to have already been made +/// from the pool, and only accounts for the withdraw from the reserve vault. +pub fn withdraw( + e: &Env, + mut reserves: StrategyReserves, + from: &Address, + underlying_amount: i128, + b_tokens_amount: i128, +) -> i128 { + if underlying_amount <= 0 { + panic_with_error!(e, StrategyError::InvalidArgument); + } + if b_tokens_amount <= 0 { + panic_with_error!(e, StrategyError::InvalidArgument); + } + + reserves.update_rate(underlying_amount, b_tokens_amount); + + let mut vault_shares = storage::get_vault_shares(&e, &from); + let share_amount = reserves.b_tokens_to_shares_up(b_tokens_amount); + + if reserves.total_shares < share_amount || reserves.total_b_tokens < b_tokens_amount { + panic_with_error!(e, StrategyError::InvalidArgument); + } + + reserves.total_shares -= share_amount; + reserves.total_b_tokens -= b_tokens_amount; + + if share_amount > vault_shares { + panic_with_error!(e, StrategyError::InvalidArgument); + } + + vault_shares -= share_amount; + storage::set_strategy_reserves(&e, reserves); + storage::set_vault_shares(&e, &from, vault_shares); + + share_amount +} + +pub fn harvest( + e: &Env, + mut reserves: StrategyReserves, + underlying_amount: i128, + b_tokens_amount: i128, +) { + if underlying_amount <= 0 { + panic_with_error!(e, StrategyError::InvalidArgument); //TODO: create a new error type for this + } + + if b_tokens_amount <= 0 { + panic_with_error!(e, StrategyError::InvalidArgument); //TODO: create a new error type for this + } + + reserves.update_rate(underlying_amount, b_tokens_amount); + + reserves.total_b_tokens += b_tokens_amount; + + storage::set_strategy_reserves(&e, reserves); +} \ No newline at end of file diff --git a/apps/contracts/strategies/blend/src/soroswap.rs b/apps/contracts/strategies/blend/src/soroswap.rs new file mode 100644 index 00000000..7a9318ee --- /dev/null +++ b/apps/contracts/strategies/blend/src/soroswap.rs @@ -0,0 +1,27 @@ +use defindex_strategy_core::StrategyError; +use soroban_sdk::{vec, Address, Env, IntoVal, Symbol, Val, Vec}; + +use crate::storage::Config; + +pub fn internal_swap_exact_tokens_for_tokens( + e: &Env, + amount_in: &i128, + amount_out_min: &i128, + path: Vec
, + to: &Address, + deadline: &u64, + config: &Config, +) -> Result, StrategyError> { + let mut swap_args: Vec = vec![&e]; + swap_args.push_back(amount_in.into_val(e)); + swap_args.push_back(amount_out_min.into_val(e)); + swap_args.push_back(path.into_val(e)); + swap_args.push_back(to.to_val()); + swap_args.push_back(deadline.into_val(e)); + + e.invoke_contract( + &config.router, + &Symbol::new(&e, "swap_exact_tokens_for_tokens"), + swap_args, + ) +} \ No newline at end of file diff --git a/apps/contracts/strategies/blend/src/storage.rs b/apps/contracts/strategies/blend/src/storage.rs index dbdc3239..2cfe07d3 100644 --- a/apps/contracts/strategies/blend/src/storage.rs +++ b/apps/contracts/strategies/blend/src/storage.rs @@ -1,18 +1,31 @@ use soroban_sdk::{contracttype, Address, Env}; +use crate::reserves::StrategyReserves; + +#[contracttype] +pub struct Config { + pub asset: Address, + pub pool: Address, + pub reserve_id: u32, + pub blend_token: Address, + pub router: Address, +} + #[derive(Clone)] #[contracttype] pub enum DataKey { Initialized, - UnderlyingAsset, - BlendPool, - Balance(Address) + Config, + Reserves, + VaultPos(Address) // Vaults Positions } -const DAY_IN_LEDGERS: u32 = 17280; +pub 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; +const LEDGER_BUMP: u32 = 120 * DAY_IN_LEDGERS; +const LEDGER_THRESHOLD: u32 = LEDGER_BUMP - 20 * DAY_IN_LEDGERS; pub fn extend_instance_ttl(e: &Env) { e.storage() @@ -28,20 +41,51 @@ 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); +// Config +pub fn set_config(e: &Env, config: Config) { + e.storage().instance().set(&DataKey::Config, &config); +} + +pub fn get_config(e: &Env) -> Config { + e.storage().instance().get(&DataKey::Config).unwrap() +} + +// Vault Position +/// Set the number of shares shares a user owns. Shares are stored with 7 decimal places of precision. +pub fn set_vault_shares(e: &Env, address: &Address, shares: i128) { + let key = DataKey::VaultPos(address.clone()); + e.storage().persistent().set::(&key, &shares); + e.storage() + .persistent() + .extend_ttl(&key, LEDGER_THRESHOLD, LEDGER_BUMP); +} + +/// Get the number of strategy shares a user owns. Shares are stored with 7 decimal places of precision. +pub fn get_vault_shares(e: &Env, address: &Address) -> i128 { + let result = e.storage().persistent().get::(&DataKey::VaultPos(address.clone())); + match result { + Some(shares) => { + e.storage() + .persistent() + .extend_ttl(&DataKey::VaultPos(address.clone()), LEDGER_THRESHOLD, LEDGER_BUMP); + shares + } + None => 0, + } } -pub fn get_underlying_asset(e: &Env) -> Address { - e.storage().instance().get(&DataKey::UnderlyingAsset).unwrap() +// Strategy Reserves +pub fn set_strategy_reserves(e: &Env, new_reserves: StrategyReserves) { + e.storage().instance().set(&DataKey::Reserves, &new_reserves); } -// Blend Pool Address -pub fn set_blend_pool(e: &Env, address: Address) { - e.storage().instance().set(&DataKey::BlendPool, &address); +pub fn get_strategy_reserves(e: &Env) -> StrategyReserves { + e.storage().instance().get(&DataKey::Reserves).unwrap_or( + StrategyReserves { + total_shares: 0, + total_b_tokens: 0, + b_rate: 0, + } + ) } -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 index 09398008..927c5c91 100644 --- a/apps/contracts/strategies/blend/src/test.rs +++ b/apps/contracts/strategies/blend/src/test.rs @@ -1,76 +1,352 @@ #![cfg(test)] -use crate::{BlendStrategy, BlendStrategyClient, StrategyError}; - -use soroban_sdk::token::{TokenClient, StellarAssetClient}; +extern crate std; +use crate::{ + blend_pool::{self, BlendPoolClient, Request, ReserveConfig, ReserveEmissionMetadata}, storage::DAY_IN_LEDGERS, BlendStrategy, BlendStrategyClient +}; +use sep_41_token::testutils::MockTokenClient; use soroban_sdk::{ - Env, - Address, - testutils::Address as _, + testutils::{Address as _, BytesN as _, Ledger as _, LedgerInfo}, token::StellarAssetClient, vec, Address, BytesN, Env, IntoVal, String, Symbol, Val, Vec }; -// mod blend_pool_module { -// soroban_sdk::contractimport!(file = "../external_wasms/blend/blend_pool.wasm"); -// pub type BlendPoolContractClient<'a> = Client<'a>; -// } +mod blend_factory_pool { + soroban_sdk::contractimport!(file = "../external_wasms/blend/pool_factory.wasm"); +} -// use blend_pool_module::BlendPoolContractClient; +mod blend_emitter { + soroban_sdk::contractimport!(file = "../external_wasms/blend/emitter.wasm"); +} -// // fn initialize(admin: address, name: string, oracle: address, bstop_rate: u32, max_postions: u32, backstop_id: address, blnd_id: address) +mod blend_backstop { + soroban_sdk::contractimport!(file = "../external_wasms/blend/backstop.wasm"); +} -// 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 -// } +mod blend_comet { + soroban_sdk::contractimport!(file = "../external_wasms/blend/comet.wasm"); +} -// Blend Strategy Contract -fn create_blend_strategy<'a>(e: &Env) -> BlendStrategyClient<'a> { - BlendStrategyClient::new(e, &e.register_contract(None, BlendStrategy {})) +pub(crate) fn register_blend_strategy(e: &Env) -> Address { + 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 BlendFixture<'a> { + pub backstop: blend_backstop::Client<'a>, + pub emitter: blend_emitter::Client<'a>, + pub _backstop_token: blend_comet::Client<'a>, + pub pool_factory: blend_factory_pool::Client<'a>, } -pub struct HodlStrategyTest<'a> { - env: Env, - strategy: BlendStrategyClient<'a>, - token: TokenClient<'a>, - user: Address, +pub(crate) fn create_blend_pool( + e: &Env, + blend_fixture: &BlendFixture, + admin: &Address, + usdc: &MockTokenClient, + xlm: &MockTokenClient, +) -> Address { + // Mint usdc to admin + usdc.mint(&admin, &200_000_0000000); + // Mint xlm to admin + xlm.mint(&admin, &200_000_0000000); + + // set up oracle + let (oracle, oracle_client) = create_mock_oracle(e); + oracle_client.set_data( + &admin, + &Asset::Other(Symbol::new(&e, "USD")), + &vec![ + e, + Asset::Stellar(usdc.address.clone()), + Asset::Stellar(xlm.address.clone()), + ], + &7, + &300, + ); + oracle_client.set_price_stable(&vec![e, 1_000_0000, 100_0000]); + let salt = BytesN::<32>::random(&e); + let pool = blend_fixture.pool_factory.deploy( + &admin, + &String::from_str(e, "TEST"), + &salt, + &oracle, + &0, + &4, + ); + let pool_client = BlendPoolClient::new(e, &pool); + blend_fixture + .backstop + .deposit(&admin, &pool, &20_0000_0000000); + let reserve_config = ReserveConfig { + c_factor: 900_0000, + decimals: 7, + index: 0, + l_factor: 900_0000, + max_util: 900_0000, + reactivity: 0, + r_base: 100_0000, + r_one: 0, + r_two: 0, + r_three: 0, + util: 0, + }; + pool_client.queue_set_reserve(&usdc.address, &reserve_config); + pool_client.set_reserve(&usdc.address); + pool_client.queue_set_reserve(&xlm.address, &reserve_config); + pool_client.set_reserve(&xlm.address); + let emission_config = vec![ + e, + ReserveEmissionMetadata { + res_index: 0, + res_type: 0, + share: 250_0000, + }, + ReserveEmissionMetadata { + res_index: 0, + res_type: 1, + share: 250_0000, + }, + ReserveEmissionMetadata { + res_index: 1, + res_type: 0, + share: 250_0000, + }, + ReserveEmissionMetadata { + res_index: 1, + res_type: 1, + share: 250_0000, + }, + ]; + pool_client.set_emissions_config(&emission_config); + pool_client.set_status(&0); + blend_fixture.backstop.add_reward(&pool, &pool); + + // wait a week and start emissions + e.jump(DAY_IN_LEDGERS * 7); + blend_fixture.emitter.distribute(); + blend_fixture.backstop.gulp_emissions(); + pool_client.gulp_emissions(); + + // admin joins pool + let requests = vec![ + e, + Request { + address: usdc.address.clone(), + amount: 200_000_0000000, + request_type: 2, + }, + Request { + address: usdc.address.clone(), + amount: 100_000_0000000, + request_type: 4, + }, + Request { + address: xlm.address.clone(), + amount: 200_000_0000000, + request_type: 2, + }, + Request { + address: xlm.address.clone(), + amount: 100_000_0000000, + request_type: 4, + }, + ]; + pool_client + .mock_all_auths() + .submit(&admin, &admin, &admin, &requests); + return pool; } -impl<'a> HodlStrategyTest<'a> { - fn setup() -> Self { +/// Create a Blend Strategy +pub(crate) fn create_blend_strategy(e: &Env, underlying_asset: &Address, blend_pool: &Address, reserve_id: &u32, blend_token: &Address, soroswap_router: &Address) -> Address { + let address = register_blend_strategy(e); + let client = BlendStrategyClient::new(e, &address); - let env = Env::default(); - env.mock_all_auths(); + let init_args: Vec = vec![e, + blend_pool.into_val(e), + reserve_id.into_val(e), + blend_token.into_val(e), + soroswap_router.into_val(e), + ]; - let strategy = create_blend_strategy(&env); - let admin = Address::generate(&env); - let token = create_token_contract(&env, &admin); - let user = Address::generate(&env); + client.initialize(&underlying_asset, &init_args); + address +} - // Mint 1,000,000,000 to user - StellarAssetClient::new(&env, &token.address).mint(&user, &1_000_000_000); +pub trait EnvTestUtils { + /// Jump the env by the given amount of ledgers. Assumes 5 seconds per ledger. + fn jump(&self, ledgers: u32); - HodlStrategyTest { - env, - strategy, - token, - user - } + /// Jump the env by the given amount of seconds. Incremends the sequence by 1. + // fn jump_time(&self, seconds: u64); + + /// Set the ledger to the default LedgerInfo + /// + /// Time -> 1441065600 (Sept 1st, 2015 12:00:00 AM UTC) + /// Sequence -> 100 + fn set_default_info(&self); +} + +impl EnvTestUtils for Env { + fn jump(&self, ledgers: u32) { + self.ledger().set(LedgerInfo { + timestamp: self.ledger().timestamp().saturating_add(ledgers as u64 * 5), + protocol_version: 21, + sequence_number: self.ledger().sequence().saturating_add(ledgers), + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 30 * DAY_IN_LEDGERS, + min_persistent_entry_ttl: 30 * DAY_IN_LEDGERS, + max_entry_ttl: 365 * DAY_IN_LEDGERS, + }); } - - // 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 + + // fn jump_time(&self, seconds: u64) { + // self.ledger().set(LedgerInfo { + // timestamp: self.ledger().timestamp().saturating_add(seconds), + // protocol_version: 21, + // sequence_number: self.ledger().sequence().saturating_add(1), + // network_id: Default::default(), + // base_reserve: 10, + // min_temp_entry_ttl: 30 * DAY_IN_LEDGERS, + // min_persistent_entry_ttl: 30 * DAY_IN_LEDGERS, + // max_entry_ttl: 365 * DAY_IN_LEDGERS, + // }); // } + + fn set_default_info(&self) { + self.ledger().set(LedgerInfo { + timestamp: 1441065600, // Sept 1st, 2015 12:00:00 AM UTC + protocol_version: 21, + sequence_number: 100, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 30 * DAY_IN_LEDGERS, + min_persistent_entry_ttl: 30 * DAY_IN_LEDGERS, + max_entry_ttl: 365 * DAY_IN_LEDGERS, + }); + } +} + +// pub fn assert_approx_eq_abs(a: i128, b: i128, delta: i128) { +// assert!( +// a > b - delta && a < b + delta, +// "assertion failed: `(left != right)` \ +// (left: `{:?}`, right: `{:?}`, epsilon: `{:?}`)", +// a, +// b, +// delta +// ); +// } + +/// Asset that `b` is within `percentage` of `a` where `percentage` +/// is a percentage in decimal form as a fixed-point number with 7 decimal +/// places +// pub fn assert_approx_eq_rel(a: i128, b: i128, percentage: i128) { +// let rel_delta = b.fixed_mul_floor(percentage, SCALAR_7).unwrap(); + +// assert!( +// a > b - rel_delta && a < b + rel_delta, +// "assertion failed: `(left != right)` \ +// (left: `{:?}`, right: `{:?}`, epsilon: `{:?}`)", +// a, +// b, +// rel_delta +// ); +// } + +/// Oracle +use sep_40_oracle::testutils::{Asset, MockPriceOracleClient, MockPriceOracleWASM}; + +pub fn create_mock_oracle<'a>(e: &Env) -> (Address, MockPriceOracleClient<'a>) { + let contract_id = Address::generate(e); + e.register_contract_wasm(&contract_id, MockPriceOracleWASM); + ( + contract_id.clone(), + MockPriceOracleClient::new(e, &contract_id), + ) +} + +impl<'a> BlendFixture<'a> { + /// Deploy a new set of Blend Protocol contracts. Mints 200k backstop + /// tokens to the deployer that can be used in the future to create up to 4 + /// reward zone pools (50k tokens each). + /// + /// This function also resets the env budget via `reset_unlimited`. + /// + /// ### Arguments + /// * `env` - The environment to deploy the contracts in + /// * `deployer` - The address of the deployer + /// * `blnd` - The address of the BLND token + /// * `usdc` - The address of the USDC token + pub fn deploy( + env: &Env, + deployer: &Address, + blnd: &Address, + usdc: &Address, + ) -> BlendFixture<'a> { + env.budget().reset_unlimited(); + let backstop = env.register_contract_wasm(None, blend_backstop::WASM); + let emitter = env.register_contract_wasm(None, blend_emitter::WASM); + let comet = env.register_contract_wasm(None, blend_comet::WASM); + let pool_factory = env.register_contract_wasm(None, blend_factory_pool::WASM); + let blnd_client = StellarAssetClient::new(env, &blnd); + let usdc_client = StellarAssetClient::new(env, &usdc); + blnd_client + .mock_all_auths() + .mint(deployer, &(1_000_0000000 * 2001)); + usdc_client + .mock_all_auths() + .mint(deployer, &(25_0000000 * 2001)); + + let comet_client: blend_comet::Client<'a> = blend_comet::Client::new(env, &comet); + comet_client.mock_all_auths().init( + &deployer, + &vec![env, blnd.clone(), usdc.clone()], + &vec![env, 0_8000000, 0_2000000], + &vec![env, 1_000_0000000, 25_0000000], + &0_0030000, + ); + + comet_client.mock_all_auths().join_pool( + &199_900_0000000, // finalize mints 100 + &vec![env, 1_000_0000000 * 2000, 25_0000000 * 2000], + deployer, + ); + + blnd_client.mock_all_auths().set_admin(&emitter); + let emitter_client: blend_emitter::Client<'a> = blend_emitter::Client::new(env, &emitter); + emitter_client + .mock_all_auths() + .initialize(&blnd, &backstop, &comet); + + let backstop_client: blend_backstop::Client<'a> = blend_backstop::Client::new(env, &backstop); + backstop_client.mock_all_auths().initialize( + &comet, + &emitter, + &usdc, + &blnd, + &pool_factory, + &Vec::new(env), + ); + + let pool_hash = env.deployer().upload_contract_wasm(blend_pool::WASM); + + let pool_factory_client = blend_factory_pool::Client::new(env, &pool_factory); + pool_factory_client + .mock_all_auths() + .initialize(&blend_factory_pool::PoolInitMeta { + backstop, + blnd_id: blnd.clone(), + pool_hash, + }); + backstop_client.update_tkn_val(); + + BlendFixture { + backstop: backstop_client, + emitter: emitter_client, + _backstop_token: comet_client, + pool_factory: pool_factory_client, + } + } } -mod blend; \ No newline at end of file +mod blend; diff --git a/apps/contracts/strategies/blend/src/test/blend/deposit.rs b/apps/contracts/strategies/blend/src/test/blend/deposit.rs deleted file mode 100644 index 3267f974..00000000 --- a/apps/contracts/strategies/blend/src/test/blend/deposit.rs +++ /dev/null @@ -1,79 +0,0 @@ -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/blend/events.rs b/apps/contracts/strategies/blend/src/test/blend/events.rs deleted file mode 100644 index 239a9bd1..00000000 --- a/apps/contracts/strategies/blend/src/test/blend/events.rs +++ /dev/null @@ -1,6 +0,0 @@ -// 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/blend/initialize.rs b/apps/contracts/strategies/blend/src/test/blend/initialize.rs deleted file mode 100644 index 41037473..00000000 --- a/apps/contracts/strategies/blend/src/test/blend/initialize.rs +++ /dev/null @@ -1,21 +0,0 @@ -// 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/blend/mod.rs b/apps/contracts/strategies/blend/src/test/blend/mod.rs index 75e4b2f2..916f9128 100644 --- a/apps/contracts/strategies/blend/src/test/blend/mod.rs +++ b/apps/contracts/strategies/blend/src/test/blend/mod.rs @@ -1,4 +1 @@ -mod deposit; -mod events; -mod initialize; -mod withdraw; \ No newline at end of file +mod success; \ No newline at end of file diff --git a/apps/contracts/strategies/blend/src/test/blend/success.rs b/apps/contracts/strategies/blend/src/test/blend/success.rs new file mode 100644 index 00000000..9831bd7f --- /dev/null +++ b/apps/contracts/strategies/blend/src/test/blend/success.rs @@ -0,0 +1,226 @@ +#![cfg(test)] +use crate::blend_pool::{BlendPoolClient, Request}; +use crate::constants::MIN_DUST; +use crate::storage::DAY_IN_LEDGERS; +use crate::test::{create_blend_pool, create_blend_strategy, BlendFixture, EnvTestUtils}; +use crate::BlendStrategyClient; +use defindex_strategy_core::StrategyError; +use sep_41_token::testutils::MockTokenClient; +use soroban_sdk::testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation}; +use soroban_sdk::{vec, Address, Env, IntoVal, Symbol}; +use crate::test::std; + +#[test] +fn success() { + let e = Env::default(); + e.budget().reset_unlimited(); + e.mock_all_auths(); + e.set_default_info(); + + let admin = Address::generate(&e); + let user_2 = Address::generate(&e); + let user_3 = Address::generate(&e); + let user_4 = Address::generate(&e); + + let blnd = e.register_stellar_asset_contract_v2(admin.clone()); + let usdc = e.register_stellar_asset_contract_v2(admin.clone()); + let xlm = e.register_stellar_asset_contract_v2(admin.clone()); + let _blnd_client = MockTokenClient::new(&e, &blnd.address()); + let usdc_client = MockTokenClient::new(&e, &usdc.address()); + let xlm_client = MockTokenClient::new(&e, &xlm.address()); + + let blend_fixture = BlendFixture::deploy(&e, &admin, &blnd.address(), &usdc.address()); + + // usdc (0) and xlm (1) charge a fixed 10% borrow rate with 0% backstop take rate + // admin deposits 200m tokens and borrows 100m tokens for a 50% util rate + // emits to each reserve token evently, and starts emissions + let pool = create_blend_pool(&e, &blend_fixture, &admin, &usdc_client, &xlm_client); + let pool_client = BlendPoolClient::new(&e, &pool); + let strategy = create_blend_strategy(&e, &usdc.address(), &pool, &0u32, &blnd.address(), &Address::generate(&e)); + let strategy_client = BlendStrategyClient::new(&e, &strategy); + + /* + * Deposit into pool + * -> deposit 100 into blend strategy for each user_2 and user_3 + * -> deposit 200 into pool for user_4 + * -> admin borrow from pool to return to 50% util rate + * -> verify a deposit into an uninitialized vault fails + */ + let pool_usdc_balace_start = usdc_client.balance(&pool); + let starting_balance = 100_0000000; + usdc_client.mint(&user_2, &starting_balance); + usdc_client.mint(&user_3, &starting_balance); + + let user_3_balance = usdc_client.balance(&user_2); + assert_eq!(user_3_balance, starting_balance); + + + strategy_client.deposit(&starting_balance, &user_2); + // -> verify deposit auth + + assert_eq!( + e.auths()[0], + ( + user_2.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + strategy.clone(), + Symbol::new(&e, "deposit"), + vec![ + &e, + starting_balance.into_val(&e), + user_2.to_val(), + ] + )), + sub_invocations: std::vec![AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + usdc.address().clone(), + Symbol::new(&e, "transfer"), + vec![ + &e, + user_2.to_val(), + strategy.to_val(), + starting_balance.into_val(&e) + ] + )), + sub_invocations: std::vec![] + }] + } + ) + ); + + strategy_client.deposit(&starting_balance, &user_3); + + // verify deposit (pool b_rate still 1 as no time has passed) + assert_eq!(usdc_client.balance(&user_2), 0); + assert_eq!(usdc_client.balance(&user_3), 0); + assert_eq!(strategy_client.balance(&user_2), starting_balance); + assert_eq!(strategy_client.balance(&user_3), starting_balance); + assert_eq!( + usdc_client.balance(&pool), + pool_usdc_balace_start + starting_balance * 2 + ); + let vault_positions = pool_client.get_positions(&strategy); + assert_eq!(vault_positions.supply.get(0).unwrap(), starting_balance * 2); + + // user_4 deposit directly into pool + let user_4_starting_balance = 200_0000000; + usdc_client.mint(&user_4, &user_4_starting_balance); + pool_client.submit( + &user_4, + &user_4, + &user_4, + &vec![ + &e, + Request { + request_type: 0, + address: usdc.address().clone(), + amount: user_4_starting_balance, + }, + ], + ); + + // admin borrow back to 50% util rate + let borrow_amount = (user_4_starting_balance + starting_balance * 2) / 2; + pool_client.submit( + &admin, + &admin, + &admin, + &vec![ + &e, + Request { + request_type: 4, + address: usdc.address().clone(), + amount: borrow_amount, + }, + ], + ); + + /* + * Allow 1 week to pass + */ + e.jump(DAY_IN_LEDGERS * 7); + + /* + * Withdraw from pool + * -> withdraw all funds from pool for user_4 + * -> withdraw (excluding dust) from blend strategy for user_2 and user_3 + * -> verify a withdraw from an uninitialized vault fails + * -> verify a withdraw from an empty vault fails + * -> verify an over withdraw fails + */ + + // withdraw all funds from pool for user_4 + pool_client.submit( + &user_4, + &user_4, + &user_4, + &vec![ + &e, + Request { + request_type: 1, + address: usdc.address().clone(), + amount: user_4_starting_balance * 2, + }, + ], + ); + let user_4_final_balance = usdc_client.balance(&user_4); + let user_4_profit = user_4_final_balance - user_4_starting_balance; + + // withdraw from blend strategy for user_2 and user_3 + // they are expected to receive half of the profit of user_4 + let expected_user_4_profit = user_4_profit / 2; + let withdraw_amount = starting_balance + expected_user_4_profit; + // withdraw_amount = 100_0958904 + + // -> verify over withdraw fails + let result = strategy_client.try_withdraw(&(withdraw_amount + 100_000_000_0000000), &user_3); + assert_eq!(result, Err(Ok(StrategyError::InvalidArgument))); // TODO: Check which is the one failing + + strategy_client.withdraw(&withdraw_amount, &user_2); + // -> verify withdraw auth + assert_eq!( + e.auths()[0], + ( + user_2.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + strategy.clone(), + Symbol::new(&e, "withdraw"), + vec![ + &e, + withdraw_amount.into_val(&e), + user_2.to_val(), + ] + )), + sub_invocations: std::vec![] + } + ) + ); + + strategy_client.withdraw(&withdraw_amount, &user_3); + + // -> verify withdraw + assert_eq!(usdc_client.balance(&user_2), withdraw_amount); + assert_eq!(usdc_client.balance(&user_3), withdraw_amount); + assert_eq!(strategy_client.balance(&user_2), 0); + assert_eq!(strategy_client.balance(&user_3), 0); + + // -> verify withdraw from empty vault fails + let result = strategy_client.try_withdraw(&MIN_DUST, &user_3); + assert_eq!(result, Err(Ok(StrategyError::InsufficientBalance))); + + // TODO: Finish harvest testings, pending soroswap router setup with a blend token pair with the underlying asset + /* + * Harvest + * -> claim emissions for the strategy + * -> Swaps them into the underlying asset + * -> Re invest this claimed usdc into the blend pool + */ + + // harvest + // strategy_client.harvest(&usdc, &user_2, &expected_fees); + + // -> verify harvest + +} diff --git a/apps/contracts/strategies/blend/src/test/blend/withdraw.rs b/apps/contracts/strategies/blend/src/test/blend/withdraw.rs deleted file mode 100644 index dd8aa9d5..00000000 --- a/apps/contracts/strategies/blend/src/test/blend/withdraw.rs +++ /dev/null @@ -1,5 +0,0 @@ - -#[test] -fn withdraw() { - todo!() -} \ No newline at end of file diff --git a/apps/contracts/strategies/external_wasms/blend/backstop.wasm b/apps/contracts/strategies/external_wasms/blend/backstop.wasm new file mode 100644 index 00000000..acf4b9e0 Binary files /dev/null and b/apps/contracts/strategies/external_wasms/blend/backstop.wasm differ diff --git a/apps/contracts/strategies/external_wasms/blend/comet.wasm b/apps/contracts/strategies/external_wasms/blend/comet.wasm new file mode 100644 index 00000000..de1b1fa4 Binary files /dev/null and b/apps/contracts/strategies/external_wasms/blend/comet.wasm differ diff --git a/apps/contracts/strategies/external_wasms/blend/emitter.wasm b/apps/contracts/strategies/external_wasms/blend/emitter.wasm new file mode 100644 index 00000000..c6f00d0e Binary files /dev/null and b/apps/contracts/strategies/external_wasms/blend/emitter.wasm differ diff --git a/apps/contracts/strategies/external_wasms/blend/pool_factory.wasm b/apps/contracts/strategies/external_wasms/blend/pool_factory.wasm new file mode 100644 index 00000000..cc662d3d Binary files /dev/null and b/apps/contracts/strategies/external_wasms/blend/pool_factory.wasm differ