diff --git a/README.md b/README.md index ee498517..90464d45 100644 --- a/README.md +++ b/README.md @@ -169,4 +169,23 @@ yarn build # Publish to npm npm publish --access public +``` + +## 📄 Whitepaper + +To generate a pdf version of the whitepaper, you need to install mdbook: +```sh +cargo install mdbook +# Install mdbook-pdf +cargo install mdbook-pdf + +# Install mdbook-katex +cargo install mdbook-katex +``` + + Then, run the following command: +```sh +cd apps/docs/10-whitepaper +mdbook build + ``` \ No newline at end of file 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 diff --git a/apps/contracts/vault/src/test/vault/withdraw.rs b/apps/contracts/vault/src/test/vault/withdraw.rs index 366289f1..ec8d71ab 100644 --- a/apps/contracts/vault/src/test/vault/withdraw.rs +++ b/apps/contracts/vault/src/test/vault/withdraw.rs @@ -14,7 +14,6 @@ use crate::test::{ DeFindexVaultTest, }; - #[test] fn not_yet_initialized() { let test = DeFindexVaultTest::setup(); @@ -194,7 +193,7 @@ fn from_idle_one_asset_one_strategy_success() { &sorobanvec![&test.env, amount_to_deposit], &sorobanvec![&test.env, amount_to_deposit], &users[0], - &false + &false, ); // Check Balances after deposit @@ -215,7 +214,7 @@ fn from_idle_one_asset_one_strategy_success() { // Df balance of user should be equal to deposited amount - 1000 let df_balance = test.defindex_contract.balance(&users[0]); - assert_eq!(df_balance, amount_to_deposit - 1000 ); // 1000 gets locked in the vault forever + assert_eq!(df_balance, amount_to_deposit - 1000); // 1000 gets locked in the vault forever // check total manage funds let mut total_managed_funds_expected = Map::new(&test.env); @@ -259,7 +258,7 @@ fn from_idle_one_asset_one_strategy_success() { let strategy_balance = test.token0.balance(&test.strategy_client_token0.address); assert_eq!(strategy_balance, 0); - // Df balance of user should be equal to deposited amount - amount_to_withdraw - 1000 + // Df balance of user should be equal to deposited amount - amount_to_withdraw - 1000 let df_balance = test.defindex_contract.balance(&users[0]); assert_eq!(df_balance, amount_to_deposit - amount_to_withdraw - 1000); @@ -288,16 +287,18 @@ fn from_idle_one_asset_one_strategy_success() { let result = test .defindex_contract .try_withdraw(&amount_to_withdraw_more, &users[0]); - - assert_eq!(result, - Err(Ok(ContractError::AmountOverTotalSupply))); + assert_eq!(result, Err(Ok(ContractError::AmountOverTotalSupply))); // // withdraw remaining balance - let result= test.defindex_contract + let result = test + .defindex_contract .withdraw(&(amount_to_deposit - amount_to_withdraw - 1000), &users[0]); - assert_eq!(result, sorobanvec![&test.env, amount_to_deposit - amount_to_withdraw - 1000]); + assert_eq!( + result, + sorobanvec![&test.env, amount_to_deposit - amount_to_withdraw - 1000] + ); let df_balance = test.defindex_contract.balance(&users[0]); assert_eq!(df_balance, 0i128); @@ -563,7 +564,7 @@ fn from_strategy_one_asset_one_strategy_success() { &sorobanvec![&test.env, amount], &sorobanvec![&test.env, amount], &users[0], - &false + &false, ); let df_balance = test.defindex_contract.balance(&users[0]); @@ -583,11 +584,10 @@ fn from_strategy_one_asset_one_strategy_success() { }), ]; - test.defindex_contract.invest(&investments); let vault_balance = test.token0.balance(&test.defindex_contract.address); - assert_eq!(vault_balance, 0); + assert_eq!(vault_balance, 0); test.defindex_contract.withdraw(&df_balance, &users[0]); @@ -597,7 +597,6 @@ fn from_strategy_one_asset_one_strategy_success() { let user_balance = test.token0.balance(&users[0]); assert_eq!(user_balance, amount - 1000); } - #[test] fn from_strategies_one_asset_two_strategies_success() { @@ -933,4 +932,4 @@ fn from_strategies_two_asset_each_one_strategy_success() { #[test] fn from_strategy_success_no_mock_all_auths() { todo!(); -} \ No newline at end of file +} diff --git a/apps/docs/.gitignore b/apps/docs/.gitignore index 683584b6..2ebb8ad5 100644 --- a/apps/docs/.gitignore +++ b/apps/docs/.gitignore @@ -1,2 +1,3 @@ # we want to be able to compile the book locally and not push that to the repo -_book/ \ No newline at end of file +_book/ +book/ \ No newline at end of file diff --git a/apps/docs/01-introduction/README.md b/apps/docs/01-introduction/README.md index 02deee53..c8bcf089 100644 --- a/apps/docs/01-introduction/README.md +++ b/apps/docs/01-introduction/README.md @@ -1,2 +1,5 @@ # Introduction -DeFindex is a protocol where users can define how investment are distributed among multiple DeFi protocols and strategies. \ No newline at end of file +DeFindex is a protocol where users can define how investment are distributed among multiple DeFi protocols and strategies. + +You can find the whitepaper [here](../10-whitepaper/README.md). +and a PDF version [here](https://drive.proton.me/urls/ZGFGQ7JAKC#IzQdqXo8r331). \ No newline at end of file diff --git a/apps/docs/10-whitepaper/02-state-of-the-art/README.md b/apps/docs/10-whitepaper/02-state-of-the-art/README.md deleted file mode 100644 index 7241acaf..00000000 --- a/apps/docs/10-whitepaper/02-state-of-the-art/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# State of the Art -From all types of DeFi protocols that achieve similar goals currently available on different ecosystem, we have selected 3 to define the basic concepts for DeFidex. These are **Yearn.Finance**, **SetsProtocol** and **YieldYak**. \ No newline at end of file diff --git a/apps/docs/10-whitepaper/03-the-defindex-approach/01-design-decisions.md b/apps/docs/10-whitepaper/03-the-defindex-approach/01-design-decisions.md index 112c1e33..3dbc566a 100644 --- a/apps/docs/10-whitepaper/03-the-defindex-approach/01-design-decisions.md +++ b/apps/docs/10-whitepaper/03-the-defindex-approach/01-design-decisions.md @@ -1,25 +1,27 @@ # Design Decisions We have decided to do: -## Multi Assets Index. -We think is important to offer diversified Indexes to our users, not only in the platforms or strategies they will be interacting, but also in the assets they will be exposed to. +## Multi Assets Vaults. +We think is important to offer diversified Vaults to our users, not only in the platforms or strategies they will be interacting, but also in the assets they will be exposed to. ## AMM Liquidity Pool Support When supporting a AMM Liquity Pool, the underlying asset will be considered as the **AMM LP token**, for example, for a Soroswap USDC-XLM liquidity pool, the underlying asset will be the Soroswap-USDC-XLM-LP token and not the USDC or XLM tokens. ## User should provide the exact underlying assets -Even if we would provide the best user experience, every Index will only accept the corresponding assets it will be using for its strategies. We can help the user to get these assets before investing in the Index(See Zapper contract). However it is a decision that the Vault will only accept the desired assets in the correct ratio. +Even if we would provide the best user experience, every Vault only accepts the corresponding assets it will be using for its strategies. We can help the user to get these assets before investing in the Vault(See Zapper contract). However it is a decision that the Vault will only accept the desired assets in the correct ratio. To understand better why we decide this please check the [Why we can`t swap on deposit](../10-apendix/01-why-we-cant-swap-on-deposit-or-withdraw.md) section. ## IDLE funds. -- Security: Enables emergency withdrawal -- Performance: Separate high CPU instructions limits tx. -- Transaction Cost: Enable small investment that wont be affected by costly txs. +IDLE funds are funds that are not being used for any strategy. But they are protected by being held inside the DeFindex Smart Contracts. +- Security: Enables emergency withdrawal. This means that if a DeFi protocol gets too risky, the users won't lose their funds because they can be withdrawn from the DeFi protocol to the DeFindex Smart Contracts. +- Performance: Enable multi transaction movements. +- Transaction Cost: Enable small transactions that wont be affected by costly txs. ## Emergency Withdrawal -- Secutiry. We have roles. +- It allows the Emergency Manager to rescue funds in case of an emergency. These are held in the DeFindex Smart Contracts. Thus, the users won't lose their funds and they will be able to withdraw them anytime. ## Roles -- Manager -- Emergency \ No newline at end of file +- Manager: Can change the Emergency Manager and the Fee Receiver. Rebalance between strategies to optimize the performance and minimize the risk. +- Emergency Manager: Can rescue funds in case of an emergency. +- Fee Receiver: Receives the fees that the protocol pays to incentivize good management. \ No newline at end of file diff --git a/apps/docs/10-whitepaper/03-the-defindex-approach/README.md b/apps/docs/10-whitepaper/03-the-defindex-approach/README.md index f002ebe4..0ac38f62 100644 --- a/apps/docs/10-whitepaper/03-the-defindex-approach/README.md +++ b/apps/docs/10-whitepaper/03-the-defindex-approach/README.md @@ -1 +1,3 @@ -# The DeFindex Approach \ No newline at end of file +# The DeFindex Approach + +In this section we will describe the approach we took to build DeFindex. Why we chose the design decisions we made and how we tried to improve the current state of the art, leveraging the Stellar ecosystem. \ No newline at end of file diff --git a/apps/docs/10-whitepaper/02-state-of-the-art/01-yearn-finance.md b/apps/docs/10-whitepaper/04-state-of-the-art/01-yearn-finance.md similarity index 100% rename from apps/docs/10-whitepaper/02-state-of-the-art/01-yearn-finance.md rename to apps/docs/10-whitepaper/04-state-of-the-art/01-yearn-finance.md diff --git a/apps/docs/10-whitepaper/02-state-of-the-art/02-tokensets.md b/apps/docs/10-whitepaper/04-state-of-the-art/02-set-protocol.md similarity index 100% rename from apps/docs/10-whitepaper/02-state-of-the-art/02-tokensets.md rename to apps/docs/10-whitepaper/04-state-of-the-art/02-set-protocol.md diff --git a/apps/docs/10-whitepaper/04-state-of-the-art/README.md b/apps/docs/10-whitepaper/04-state-of-the-art/README.md new file mode 100644 index 00000000..05cb593c --- /dev/null +++ b/apps/docs/10-whitepaper/04-state-of-the-art/README.md @@ -0,0 +1,5 @@ +# State of the Art +From all types of DeFi protocols that achieve similar goals currently available on different ecosystem, we have selected 2 to define the basic concepts for DeFidex. These are **Yearn.Finance** and **SetProtocol**. + +- [Yearn.Finance](01-yearn-finance.md) +- [SetProtocol](02-set-protocol.md) diff --git a/apps/docs/10-whitepaper/10-appendix/01-why-we-cant-swap-on-deposit-or-withdraw.md b/apps/docs/10-whitepaper/05-appendix/01-why-we-cant-swap-on-deposit-or-withdraw.md similarity index 100% rename from apps/docs/10-whitepaper/10-appendix/01-why-we-cant-swap-on-deposit-or-withdraw.md rename to apps/docs/10-whitepaper/05-appendix/01-why-we-cant-swap-on-deposit-or-withdraw.md diff --git a/apps/docs/10-whitepaper/05-appendix/README.md b/apps/docs/10-whitepaper/05-appendix/README.md new file mode 100644 index 00000000..169b0bed --- /dev/null +++ b/apps/docs/10-whitepaper/05-appendix/README.md @@ -0,0 +1,3 @@ +# Appendix + +- [Why we can't swap on deposit or withdraw](01-why-we-cant-swap-on-deposit-or-withdraw.md) \ No newline at end of file diff --git a/apps/docs/10-whitepaper/10-appendix/README.md b/apps/docs/10-whitepaper/10-appendix/README.md index 606e30e6..044ae324 100644 --- a/apps/docs/10-whitepaper/10-appendix/README.md +++ b/apps/docs/10-whitepaper/10-appendix/README.md @@ -1 +1,3 @@ -# Apendix \ No newline at end of file +# Apendix + +[Why we can't swap on deposit or withdraw](01-why-we-cant-swap-on-deposit-or-withdraw.md) \ No newline at end of file diff --git a/apps/docs/10-whitepaper/README.md b/apps/docs/10-whitepaper/README.md index a0634abe..cd30f668 100644 --- a/apps/docs/10-whitepaper/README.md +++ b/apps/docs/10-whitepaper/README.md @@ -1,6 +1,48 @@ # DeFindex Whitepaper +This protocol by Palta Labs. Francisco Catrileo | Joaquin Soza | Esteban Iglesias -## Abstract -DeFindex is a set of Smart Contracts to Interact better with different Descentralized Finance protocols on the Stellar / Soroban Blockchain +### Introduction +- [Introduction](./01-introduction/README.md) +- [Core Concepts](./01-introduction/02-core-concepts.md) + + +### The DeFindex Approach +- [Overview](./03-the-defindex-approach/README.md) +- [Design Decisions](./03-the-defindex-approach/01-design-decisions.md) + +### Contracts +- [Vault Contract](./03-the-defindex-approach/02-contracts/01-vault-contract.md) +- [Strategy Contract](./03-the-defindex-approach/02-contracts/02-strategy-contract.md) +- [Zapper Contract](./03-the-defindex-approach/02-contracts/02-zapper-contract.md) + +### State of the Art +- [State of the Art](./04-state-of-the-art/README.md) + +### Appendix +- [Appendix](./05-appendix/README.md) + +### Abstract + +DeFindex is a suite of smart contracts designed to facilitate interaction with various Decentralized Finance (DeFi) protocols on the Stellar/Soroban Blockchain. It enables users to create custom strategies, allowing investments to be distributed across multiple DeFi protocols in a streamlined manner. The protocol serves two primary audiences: + +1. **Wallet Users (including Web2 users):** DeFindex provides a simplified interface that wallet developers can integrate into their platforms, enabling users to access DeFi investment services effortlessly. +2. **Expert Users:** For experienced investors, DeFindex offers an efficient way to diversify investments without the complexity of building and managing their own strategies. + +Inspired by projects such as Yearn, Set Protocol, Compound, and YieldYak, DeFindex adapts their core principles to the Stellar ecosystem. + +The protocol comprises three main components: + +1. **Factory:** A smart contract responsible for creating new Vaults. +2. **Vaults:** The primary contracts through which users interact, enabling deposits, withdrawals, and position adjustments. +3. **Strategies:** Contracts that allocate Vault assets across various DeFi protocols. + +To ensure robust functionality and security, DeFindex implements a role-based management system: + +- **Manager:** Oversees strategies and the assets within Vaults. +- **Emergency Manager:** Handles emergency withdrawals. +- **Fee Receiver:** Collects and manages strategy-related fees. + +By combining simplicity for newcomers with advanced features for seasoned users, DeFindex aims to make DeFi more accessible and efficient on the Stellar Blockchain. + diff --git a/apps/docs/10-whitepaper/SUMMARY.md b/apps/docs/10-whitepaper/SUMMARY.md new file mode 100644 index 00000000..12804705 --- /dev/null +++ b/apps/docs/10-whitepaper/SUMMARY.md @@ -0,0 +1,22 @@ +# Summary + +[DeFindex Whitepaper](./README.md) + +# Introduction +- [Introduction](./01-introduction/README.md) +- [Core Concepts](./01-introduction/02-core-concepts.md) + +# The DeFindex Approach +- [Overview](./03-the-defindex-approach/README.md) +- [Design Decisions](./03-the-defindex-approach/01-design-decisions.md) + +# Contracts +- [Vault Contract](./03-the-defindex-approach/02-contracts/01-vault-contract.md) +- [Strategy Contract](./03-the-defindex-approach/02-contracts/02-strategy-contract.md) +- [Zapper Contract](./03-the-defindex-approach/02-contracts/02-zapper-contract.md) + +# State of the Art +- [State of the Art](./04-state-of-the-art/README.md) + +# Appendix +- [Appendix](./05-appendix/README.md) diff --git a/apps/docs/10-whitepaper/book.toml b/apps/docs/10-whitepaper/book.toml new file mode 100644 index 00000000..ac8e3c24 --- /dev/null +++ b/apps/docs/10-whitepaper/book.toml @@ -0,0 +1,20 @@ +[book] +authors = ["Francisco Catrileo", "Joaquin Soza", "Esteban Iglesias"] +language = "en" +multilingual = false +src = "." +title = "DeFindex Whitepaper" + +[output.html] +default-theme = "light" +preferred-dark-theme = "navy" +git-repository-url = "https://github.com/yourusername/yourrepo" +mathjax-support = true + +[preprocessor.katex] +after = ["links"] + +[output.pdf] +optional = true +pdf.links = false +no-links = true \ No newline at end of file diff --git a/apps/docs/11-developer-guides/03-how-to-create-a-strategy.md b/apps/docs/11-developer-guides/03-how-to-create-a-strategy.md new file mode 100644 index 00000000..3edd16d4 --- /dev/null +++ b/apps/docs/11-developer-guides/03-how-to-create-a-strategy.md @@ -0,0 +1,239 @@ +# Implementing a Blend Strategy for DeFindex: A Step-by-Step Guide + +DeFindex is designed for flexibility, enabling developers to create custom strategies that integrate seamlessly with its Vault architecture. In this guide, we’ll explore how to implement a Blend Strategy, which interacts with a Blend Pool to manage assets. The strategy includes key features such as deposits, withdrawals and claiming rewards. + +For more details on the role of strategies in DeFindex, refer to the whitepaper’s strategy section. + +## Overview of the Blend Strategy + +The Blend Strategy handles: + +- Initialization: Sets up the strategy with the Blend Pool address and the underlying asset. +- Deposits: Transfers user assets to the Blend Pool as collateral. +- Withdrawals: Retrieves user assets from the Blend Pool. +- Harvesting: Claims rewards from the Blend Pool for reinvestment or user distribution. +- Balance Tracking: Reports the current holdings of the strategy. + +## Prerequisites + +- Install the defindex-strategy-core crate + +This library provides a standardized interface for DeFindex strategies. + +```bash +cargo add defindex-strategy-core +``` + +- Familiarity with Soroban SDK + +Soroban SDK is used to build and interact with Stellar smart contracts. + +## Understanding Blend Pool Contracts + +Use the Stellar CLI to fetch the Blend Pool WASM: + +```bash +stellar contract fetch --network mainnet --id +``` + +## Implementing the Core Strategy + +Start by defining the BlendStrategy structure and implementing the DeFindexStrategyTrait in lib.rs. + +```rust +use defindex_strategy_core::{DeFindexStrategyTrait, StrategyError}; +use soroban_sdk::{contractimpl, Address, Env, Val, Vec}; + +pub struct BlendStrategy; + +#[contractimpl] +impl DeFindexStrategyTrait for BlendStrategy { + + fn initialize(env: Env, asset: Address, init_args: Vec) -> Result<(), StrategyError> { + // Initialization logic here_ + Ok(()) + } + + fn asset(env: Env) -> Result { + // Return the asset managed by this strategy_ + Ok(Address::random(&env)) + } + + fn deposit(env: Env, amount: i128, from: Address) -> Result<(), StrategyError> { + // Logic to deposit assets into the strategy_ + Ok(()) + } + + fn harvest(env: Env, from: Address) -> Result<(), StrategyError> { + // Reinvest rewards to optimize yields_ + Ok(()) + } + + fn balance(env: Env, from: Address) -> Result { + // Return the current balance_ + Ok(1000) + } + + fn withdraw(env: Env, amount: i128, from: Address) -> Result { + // Withdraw assets from the strategy_ + Ok(amount) + } +} +``` + +This is the basic skeleton. Let’s move on to integrate Blend Pool functionality. + +## Integrating Blend Pool + +Create blend_pool.rs to manage interactions with the Blend Pool smart contract. + +```rust +use soroban_sdk::{vec, Address, Env, Vec}; + +use crate::storage::{get_blend_pool, get_underlying_asset}; + +// Import the Blend Pool WASM_ + +soroban_sdk::contractimport!( + file = "../external_wasms/blend/blend_pool.wasm" +); + +pub type BlendPoolClient<'a> = Client<'a>; + +pub enum RequestType { + SupplyCollateral = 2, + WithdrawCollateral = 3, +} + + +pub fn submit(e: &Env, from: &Address, amount: i128, request_type: RequestType) { + 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![&e, Request { + address: underlying_asset, + amount, + request_type: request_type as u32, + }]; + + blend_pool_client.submit(from, from, from, &requests); +} + +pub fn claim(e: &Env, from: &Address) { + 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 { + let blend_pool_address = get_blend_pool(e); + let blend_pool_client = BlendPoolClient::new(e, &blend_pool_address); + + blend_pool_client.get_positions(from) +} +``` + +## Managing Strategy Storage + +In storage.rs, maintain initialization flags and asset addresses for the strategy. + +```rust +use soroban_sdk::{contracttype, Address, Env}; + +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + Initialized, + UnderlyingAsset, + BlendPool, +} + +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) +} + + +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() +} + + +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() +} +``` + +## Updating Core Methods + +Finally, update lib.rs to call the blend_pool methods for deposits, withdrawals, and harvesting. + +```rust +fn initialize( + e: Env, + asset: Address, + init_args: Vec, +) -> Result<(), StrategyError> { + if storage::is_initialized(&e) { + return Err(StrategyError::AlreadyInitialized); + } + + // We get the pool address from init_args that we are passing when initializing the contract + let blend_pool_address = init_args + .get(0) + .ok_or(StrategyError::InvalidArgument)? + .into_val(&e); + + storage::set_initialized(&e); + // Set the blend pool address in the storage + storage::set_blend_pool(&e, blend_pool_address); + // Sets the underlying asset in the strategy, refer to the Whitepaper if confused + storage::set_underlying_asset(&e, &asset); + + // Events come from the strategy core package, so we can have an standarized events for all the defindex strategies + event::emit_initialize(&e, String::from_str(&e, STRATEGY_NAME), asset); + storage::extend_instance_ttl(&e); + Ok(()) +} + +fn deposit(e: Env, amount: i128, from: Address) -> Result<(), StrategyError> { + storage::check_initialized(&e)?; + + blend_pool::submit(&e, &from, amount, RequestType::SupplyCollateral); + Ok(()) +} + + +fn withdraw(e: Env, amount: i128, from: Address) -> Result { + storage::check_initialized(&e)?; + blend_pool::submit(&e, &from, amount, RequestType::WithdrawCollateral); + Ok(amount) +} + + +fn harvest(e: Env, from: Address) -> Result<(), StrategyError> { + storage::check_initialized(&e)?; + blend_pool::claim(&e, &from); + Ok(()) +} +``` + +## Conclusion + +By following this guide, you can create a Blend Strategy for DeFindex that integrates smoothly with Vaults and Blend Pools. The extensible architecture ensures that your strategy remains secure, modular, and aligned with DeFindex’s design principles. Happy coding! 🎉 \ No newline at end of file