From d54bb7e29bf6b18978ef2c5a79d2828621a22682 Mon Sep 17 00:00:00 2001 From: coderipper Date: Tue, 26 Nov 2024 18:53:26 -0300 Subject: [PATCH 1/8] update on blend strategy and blend strategy tests --- apps/contracts/package.json | 3 +- apps/contracts/src/strategies/deploy_blend.ts | 3 +- apps/contracts/src/tests/blend/test.ts | 232 ------------------ .../src/tests/blend/test_strategy.ts | 125 ++++++++++ apps/contracts/src/tests/blend/test_vault.ts | 209 ++++++++++++++++ .../strategies/blend/src/blend_pool.rs | 64 ++++- apps/contracts/strategies/blend/src/lib.rs | 24 +- .../contracts/strategies/blend/src/storage.rs | 11 +- apps/contracts/strategies/blend/src/test.rs | 8 +- .../strategies/blend/src/test/deposit.rs | 79 ------ .../strategies/blend/src/test/events.rs | 6 - .../strategies/blend/src/test/initialize.rs | 21 -- .../strategies/blend/src/test/withdraw.rs | 5 - 13 files changed, 419 insertions(+), 371 deletions(-) delete mode 100644 apps/contracts/src/tests/blend/test.ts create mode 100644 apps/contracts/src/tests/blend/test_strategy.ts create mode 100644 apps/contracts/src/tests/blend/test_vault.ts delete mode 100644 apps/contracts/strategies/blend/src/test/deposit.rs delete mode 100644 apps/contracts/strategies/blend/src/test/events.rs delete mode 100644 apps/contracts/strategies/blend/src/test/initialize.rs delete mode 100644 apps/contracts/strategies/blend/src/test/withdraw.rs diff --git a/apps/contracts/package.json b/apps/contracts/package.json index 7f349d64..3b52c8b4 100644 --- a/apps/contracts/package.json +++ b/apps/contracts/package.json @@ -11,7 +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": "tsc && node dist/tests/blend/test.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..ba2473ff 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,7 @@ 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 ]); const args: xdr.ScVal[] = [ diff --git a/apps/contracts/src/tests/blend/test.ts b/apps/contracts/src/tests/blend/test.ts deleted file mode 100644 index d9a36d77..00000000 --- a/apps/contracts/src/tests/blend/test.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { Address, Keypair, nativeToScVal, scValToNative, xdr } from "@stellar/stellar-sdk"; -import { airdropAccount, invokeCustomContract } from "../../utils/contract.js"; -import { getDfTokenBalance } from "../vault.js"; -import { randomBytes } from "crypto"; -import { TxResponse } from '@soroban-react/contracts'; -const blendStrategyAddress = "CCNFSOPH4XFQ5TNWGTJB4ZVKUUARSNQ67SETXVIQLUIW3B3F7KHA3NKJ" -const factoryAddress = "CB6RQM6ECU775ZC26NMZ6RJNKQLKQGLIJWWN2VZO6AGSE4V4DBQDL23O" -const XLMAddress = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC" -const BlendUSDCAddress = "" -const network = process.argv[2]; -const purple = '\x1b[35m%s\x1b[0m'; -const green = '\x1b[32m%s\x1b[0m'; - - -const newVault = { - address: '', - emergencyManager: 'GCH6YKNJ3KPESGSAIGBNHRNCIYXXXSRVU7OC552RDGQFHZ4SYRI26DQE', - feeReceiver: 'GCH6YKNJ3KPESGSAIGBNHRNCIYXXXSRVU7OC552RDGQFHZ4SYRI26DQE', - manager: 'GCH6YKNJ3KPESGSAIGBNHRNCIYXXXSRVU7OC552RDGQFHZ4SYRI26DQE', - name: 'Test', - symbol: 'Test1', - vaultShare: 10, - assets: [ - { - address: 'CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC', - strategies: [ - { - address: 'CCNFSOPH4XFQ5TNWGTJB4ZVKUUARSNQ67SETXVIQLUIW3B3F7KHA3NKJ', - name: 'Blend', - paused: false - } - ], - symbol: 'XLM', - amount: 1000 - } - ], - TVL: 0 -} -export async function createVault(user?: Keypair) { - // Create and fund a new user account if not provided - console.log(purple, '--------------------------------------------------------------------') - console.log(purple, '----------------------- Creating new account -----------------------') - console.log(purple, '--------------------------------------------------------------------') - const newUser = Keypair.random(); - console.log('🚀 ~ depositToVault ~ newUser.publicKey():', newUser.publicKey()); - console.log('🚀 ~ depositToVault ~ newUser.secret():', newUser.secret()); - - 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("New user publicKey:", newUser.publicKey()); - - - const indexName = "test"; - const indexSymbol = "TEST"; - const indexShare = 10; - const managerString = newUser.publicKey(); - const vaultName = nativeToScVal(indexName, { type: "string" }) - const vaultSymbol = nativeToScVal(indexSymbol, { type: "string" }) - const vaultShare = nativeToScVal(indexShare, { type: "u32" }) - const emergencyManager = new Address(managerString) - const feeReceiver = new Address(managerString) - const manager = new Address(managerString) - const salt = randomBytes(32) - - const strategyParamsScVal = xdr.ScVal.scvMap([ - new xdr.ScMapEntry({ - key: xdr.ScVal.scvSymbol('address'), - val: new Address(blendStrategyAddress).toScVal(), - }), - new xdr.ScMapEntry({ - key: xdr.ScVal.scvSymbol('name'), - val: nativeToScVal('Blend', { type: "string" }), - }), - new xdr.ScMapEntry({ - key: xdr.ScVal.scvSymbol('paused'), - val: nativeToScVal(false, { type: "bool" }), - }), - ]); - const strategyParamsScValVec = xdr.ScVal.scvVec([strategyParamsScVal]); - const assetsParams = xdr.ScVal.scvMap([ - new xdr.ScMapEntry({ - key: xdr.ScVal.scvSymbol('address'), - val: new Address(newVault.assets[0].address).toScVal(), - }), - new xdr.ScMapEntry({ - key: xdr.ScVal.scvSymbol('strategies'), - val: strategyParamsScValVec, - }), - ]); - const assetParamsScValVec = xdr.ScVal.scvVec([assetsParams]); - const createDefindexParams: xdr.ScVal[] = [ - emergencyManager.toScVal(), - feeReceiver.toScVal(), - vaultShare, - vaultName, - vaultSymbol, - manager.toScVal(), - assetParamsScValVec, - nativeToScVal(salt), - ]; - let result: any; - let blendVaultAddress: string; - try { - console.log(purple, '--------------------------------------------------------------') - console.log(purple, '----------------------- Creating vault -----------------------') - console.log(purple, '--------------------------------------------------------------') - result = await invokeCustomContract( - factoryAddress, - 'create_defindex_vault', - createDefindexParams, - newUser, - false - ); - blendVaultAddress = scValToNative(result.returnValue); - console.log(green, '----------------------- Vault created -------------------------') - console.log(green, 'result', blendVaultAddress) - console.log(green, '---------------------------------------------------------------') - - - // Deposit assets to the vault - - console.log(purple, '---------------------------------------------------------------------------') - console.log(purple, '----------------------- Depositing XLM to the vault -----------------------') - console.log(purple, '---------------------------------------------------------------------------') - const depositParams: xdr.ScVal[] = [ - xdr.ScVal.scvVec([nativeToScVal(987654321, { type: "i128" })]), - xdr.ScVal.scvVec([nativeToScVal(Math.ceil(987654321 * 0.9), { type: "i128" })]), - new Address(newUser.publicKey()).toScVal(), - ] - const depositResult = await invokeCustomContract( - blendVaultAddress, - 'deposit', - depositParams, - newUser, - false - ); - const depositResultValue = scValToNative(depositResult.returnValue); - - console.log(green, '------------ XLM deposited to the vault ------------') - console.log(green, 'depositResult', depositResultValue) - console.log(green, '----------------------------------------------------') - - // 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, '--------------------------------------------------------------') - - // Invest in strategy - - console.log(purple, '---------------------------------------------------------------------------') - console.log(purple, '-------------------------- Investing in strategy --------------------------') - console.log(purple, '---------------------------------------------------------------------------') - - const investment: any = [{ - "asset": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC", - "strategy_investments": [ - { - "amount": 24, - "strategy": "CCWUMJGE6LKWRDJ2IYEJBLCWJSMSUC3QCYZNI2MHTOEYPZRWZN56MIVA" - } - ], - }] - - const investmentParams = investment.map((entry:any) => - xdr.ScVal.scvMap([ - new xdr.ScMapEntry({ - key: xdr.ScVal.scvSymbol("asset"), - val: new Address(entry.asset).toScVal()// Convert asset address to ScVal - }), - new xdr.ScMapEntry({ - key: xdr.ScVal.scvSymbol("strategy_investments"), - val: xdr.ScVal.scvVec( - entry.strategy_investments.map((strategy_investment: any) => { - return xdr.ScVal.scvMap([ - new xdr.ScMapEntry({ - key: xdr.ScVal.scvSymbol("amount"), - val: nativeToScVal(BigInt((strategy_investment.amount ?? 0) * 10 ** 7), { type: "i128" }), // Ensure i128 conversion - }), - new xdr.ScMapEntry({ - key: xdr.ScVal.scvSymbol("strategy"), - val: new Address(strategy_investment.strategy).toScVal() // Convert strategy address - }), - ]) - }) - ), - }), - ]) - ) - const investmentParamsScValVec = xdr.ScVal.scvVec(investmentParams); - - const investResult = await invokeCustomContract( - blendVaultAddress, - 'invest', - [investmentParamsScValVec], - newUser, - false - ); - const investResultValue = scValToNative(investResult.returnValue); - console.log(green, '---------------------- Invested in strategy ----------------------') - console.log(green, 'Invested: ', investResultValue, ' in the strategy') - console.log(green, '------------------------------------------------------------------') - - }catch(e){ - console.log('error', e) - } -} -await createVault(); \ No newline at end of file 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..167a121a --- /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); + + 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: ', scValToNative(investResult.returnValue), ' 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/strategies/blend/src/blend_pool.rs b/apps/contracts/strategies/blend/src/blend_pool.rs index 69e25274..b1ad4191 100644 --- a/apps/contracts/strategies/blend/src/blend_pool.rs +++ b/apps/contracts/strategies/blend/src/blend_pool.rs @@ -1,6 +1,6 @@ -use soroban_sdk::{vec, Address, Env, Vec}; +use soroban_sdk::{auth::{ContractContext, InvokerContractAuthEntry, SubContractInvocation}, vec, Address, Env, IntoVal, Symbol, Vec}; -use crate::storage::{get_blend_pool, get_underlying_asset}; +use crate::storage::get_blend_pool; soroban_sdk::contractimport!( file = "../external_wasms/blend/blend_pool.wasm" @@ -11,10 +11,10 @@ pub type BlendPoolClient<'a> = Client<'a>; #[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,20 +30,57 @@ impl RequestType { } } -pub fn submit(e: &Env, from: &Address, amount: i128, request_type: RequestType) -> Positions { - // Setting up Blend Pool client +pub fn supply(e: &Env, from: &Address, underlying_asset: Address, amount: i128) -> Positions { let blend_pool_address = get_blend_pool(e); let blend_pool_client = BlendPoolClient::new(e, &blend_pool_address); - let underlying_asset = get_underlying_asset(&e); + let requests: Vec = vec![&e, Request { + address: underlying_asset.clone(), + amount, + request_type: RequestType::Supply.to_u32(), + }]; + + e.authorize_as_current_contract(vec![ + &e, + InvokerContractAuthEntry::Contract(SubContractInvocation { + context: ContractContext { + contract: underlying_asset.clone(), + fn_name: Symbol::new(&e, "transfer"), + args: ( + e.current_contract_address(), + blend_pool_address.clone(), + amount.clone()).into_val(e), + }, + sub_invocations: vec![&e], + }), + ]); + + blend_pool_client.submit( + &from, + &e.current_contract_address(), + &from, + &requests + ) +} + +pub fn withdraw(e: &Env, from: &Address, underlying_asset: Address, amount: i128) -> Positions { + let blend_pool_address = get_blend_pool(e); + let blend_pool_client = BlendPoolClient::new(e, &blend_pool_address); let requests: Vec = vec![&e, Request { - address: underlying_asset, - amount: amount, - request_type: request_type.to_u32(), + address: underlying_asset.clone(), + amount, + request_type: RequestType::Withdraw.to_u32(), }]; - blend_pool_client.submit(from, from, from, &requests) + let new_positions = blend_pool_client.submit( + &from, + &from, + &from, + &requests + ); + + new_positions } pub fn claim(e: &Env, from: &Address) -> i128 { @@ -51,6 +88,7 @@ pub fn claim(e: &Env, from: &Address) -> i128 { let blend_pool_address = get_blend_pool(e); let blend_pool_client = BlendPoolClient::new(e, &blend_pool_address); + // TODO: Check reserve_token_ids and how to get the correct one blend_pool_client.claim(from, &vec![&e, 3u32], from) } diff --git a/apps/contracts/strategies/blend/src/lib.rs b/apps/contracts/strategies/blend/src/lib.rs index 1cf33d72..da4c66b6 100644 --- a/apps/contracts/strategies/blend/src/lib.rs +++ b/apps/contracts/strategies/blend/src/lib.rs @@ -1,13 +1,12 @@ #![no_std] -use blend_pool::RequestType; 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 storage; use storage::{ - extend_instance_ttl, get_underlying_asset, is_initialized, set_blend_pool, set_initialized, set_underlying_asset + extend_instance_ttl, get_reserve_id, get_underlying_asset, is_initialized, set_blend_pool, set_initialized, set_reserve_id, set_underlying_asset }; pub use defindex_strategy_core::{ @@ -48,9 +47,11 @@ impl DeFindexStrategyTrait for BlendStrategy { } let blend_pool_address = init_args.get(0).ok_or(StrategyError::InvalidArgument)?.into_val(&e); + let reserve_id = init_args.get(1).ok_or(StrategyError::InvalidArgument)?.into_val(&e); set_initialized(&e); set_blend_pool(&e, blend_pool_address); + set_reserve_id(&e, reserve_id); set_underlying_asset(&e, &asset); event::emit_initialize(&e, String::from_str(&e, STARETEGY_NAME), asset); @@ -75,7 +76,11 @@ impl DeFindexStrategyTrait for BlendStrategy { extend_instance_ttl(&e); from.require_auth(); - blend_pool::submit(&e, &from, amount, RequestType::SupplyCollateral); + // transfer tokens from the vault to the contract + let underlying_asset = get_underlying_asset(&e); + TokenClient::new(&e, &underlying_asset).transfer(&from, &e.current_contract_address(), &amount); + + blend_pool::supply(&e, &from, underlying_asset, amount); event::emit_deposit(&e, String::from_str(&e, STARETEGY_NAME), amount, from); Ok(()) @@ -84,6 +89,7 @@ impl DeFindexStrategyTrait for BlendStrategy { fn harvest(e: Env, from: Address) -> Result<(), StrategyError> { check_initialized(&e)?; extend_instance_ttl(&e); + from.require_auth(); blend_pool::claim(&e, &from); @@ -96,12 +102,13 @@ impl DeFindexStrategyTrait for BlendStrategy { amount: i128, from: Address, ) -> Result { - from.require_auth(); check_initialized(&e)?; check_nonnegative_amount(amount)?; extend_instance_ttl(&e); + from.require_auth(); - blend_pool::submit(&e, &from, amount, RequestType::WithdrawCollateral); + let underlying_asset = get_underlying_asset(&e); + blend_pool::withdraw(&e, &from, underlying_asset, amount); event::emit_withdraw(&e, String::from_str(&e, STARETEGY_NAME), amount, from); @@ -116,9 +123,10 @@ impl DeFindexStrategyTrait for BlendStrategy { extend_instance_ttl(&e); let positions = blend_pool::get_positions(&e, &from); + let reserve_id = get_reserve_id(&e); - let collateral = positions.collateral.get(1u32).unwrap_or(0i128); - Ok(collateral) + let supply = positions.supply.get(reserve_id).unwrap_or(0i128); + Ok(supply) } } diff --git a/apps/contracts/strategies/blend/src/storage.rs b/apps/contracts/strategies/blend/src/storage.rs index dbdc3239..fa6f51b4 100644 --- a/apps/contracts/strategies/blend/src/storage.rs +++ b/apps/contracts/strategies/blend/src/storage.rs @@ -7,7 +7,8 @@ pub enum DataKey { Initialized, UnderlyingAsset, BlendPool, - Balance(Address) + Balance(Address), + ReserveId } const DAY_IN_LEDGERS: u32 = 17280; @@ -44,4 +45,12 @@ pub fn set_blend_pool(e: &Env, address: Address) { pub fn get_blend_pool(e: &Env) -> Address { e.storage().instance().get(&DataKey::BlendPool).unwrap() +} + +pub fn set_reserve_id(e: &Env, id: u32) { + e.storage().instance().set(&DataKey::ReserveId, &id); +} + +pub fn get_reserve_id(e: &Env) -> u32 { + e.storage().instance().get(&DataKey::ReserveId).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 ba5282eb..4e4ba291 100644 --- a/apps/contracts/strategies/blend/src/test.rs +++ b/apps/contracts/strategies/blend/src/test.rs @@ -73,7 +73,7 @@ impl<'a> HodlStrategyTest<'a> { // } } -mod initialize; -mod deposit; -mod events; -mod withdraw; \ No newline at end of file +// mod initialize; +// mod deposit; +// mod events; +// mod withdraw; \ No newline at end of file diff --git a/apps/contracts/strategies/blend/src/test/deposit.rs b/apps/contracts/strategies/blend/src/test/deposit.rs deleted file mode 100644 index 3267f974..00000000 --- a/apps/contracts/strategies/blend/src/test/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/events.rs b/apps/contracts/strategies/blend/src/test/events.rs deleted file mode 100644 index 239a9bd1..00000000 --- a/apps/contracts/strategies/blend/src/test/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/initialize.rs b/apps/contracts/strategies/blend/src/test/initialize.rs deleted file mode 100644 index 41037473..00000000 --- a/apps/contracts/strategies/blend/src/test/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/withdraw.rs b/apps/contracts/strategies/blend/src/test/withdraw.rs deleted file mode 100644 index dd8aa9d5..00000000 --- a/apps/contracts/strategies/blend/src/test/withdraw.rs +++ /dev/null @@ -1,5 +0,0 @@ - -#[test] -fn withdraw() { - todo!() -} \ No newline at end of file From 655ce8536eb56dd253f357a563cc0a59cb27883f Mon Sep 17 00:00:00 2001 From: coderipper Date: Thu, 28 Nov 2024 20:25:20 -0300 Subject: [PATCH 2/8] blend strategy tracking positions --- apps/contracts/Cargo.lock | 10 ++ apps/contracts/strategies/blend/Cargo.toml | 1 + .../strategies/blend/src/blend_pool.rs | 88 ++++++++++------- .../strategies/blend/src/constants.rs | 6 ++ apps/contracts/strategies/blend/src/lib.rs | 98 ++++++++++++++----- .../strategies/blend/src/positions.rs | 23 +++++ .../strategies/blend/src/reserves.rs | 39 ++++++++ .../contracts/strategies/blend/src/storage.rs | 66 +++++++++---- 8 files changed, 252 insertions(+), 79 deletions(-) create mode 100644 apps/contracts/strategies/blend/src/constants.rs create mode 100644 apps/contracts/strategies/blend/src/positions.rs create mode 100644 apps/contracts/strategies/blend/src/reserves.rs diff --git a/apps/contracts/Cargo.lock b/apps/contracts/Cargo.lock index 234d602b..69666ccb 100644 --- a/apps/contracts/Cargo.lock +++ b/apps/contracts/Cargo.lock @@ -97,6 +97,7 @@ name = "blend_strategy" version = "0.1.0" dependencies = [ "defindex-strategy-core", + "soroban-fixed-point-math", "soroban-sdk", ] @@ -1093,6 +1094,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/strategies/blend/Cargo.toml b/apps/contracts/strategies/blend/Cargo.toml index cf1a7b29..15acfa03 100644 --- a/apps/contracts/strategies/blend/Cargo.toml +++ b/apps/contracts/strategies/blend/Cargo.toml @@ -13,6 +13,7 @@ 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"] } diff --git a/apps/contracts/strategies/blend/src/blend_pool.rs b/apps/contracts/strategies/blend/src/blend_pool.rs index b1ad4191..90b527e1 100644 --- a/apps/contracts/strategies/blend/src/blend_pool.rs +++ b/apps/contracts/strategies/blend/src/blend_pool.rs @@ -1,10 +1,11 @@ -use soroban_sdk::{auth::{ContractContext, InvokerContractAuthEntry, SubContractInvocation}, vec, Address, Env, IntoVal, Symbol, 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; +use crate::storage::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 @@ -30,13 +31,19 @@ impl RequestType { } } -pub fn supply(e: &Env, from: &Address, underlying_asset: Address, amount: i128) -> Positions { - 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); + + // 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.clone(), - amount, + address: config.asset.clone(), + amount: amount.clone(), request_type: RequestType::Supply.to_u32(), }]; @@ -44,58 +51,67 @@ pub fn supply(e: &Env, from: &Address, underlying_asset: Address, amount: i128) &e, InvokerContractAuthEntry::Contract(SubContractInvocation { context: ContractContext { - contract: underlying_asset.clone(), + contract: config.asset.clone(), fn_name: Symbol::new(&e, "transfer"), args: ( e.current_contract_address(), - blend_pool_address.clone(), + config.pool.clone(), amount.clone()).into_val(e), }, sub_invocations: vec![&e], }), ]); - blend_pool_client.submit( - &from, + 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 withdraw(e: &Env, from: &Address, underlying_asset: Address, amount: i128) -> Positions { - 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: underlying_asset.clone(), - amount, + address: config.asset.clone(), + amount: amount.clone(), request_type: RequestType::Withdraw.to_u32(), }]; - let new_positions = blend_pool_client.submit( - &from, - &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 ); - new_positions + // 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 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 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 - blend_pool_client.claim(from, &vec![&e, 3u32], from) -} - -pub fn get_positions(e: &Env, from: &Address) -> Positions { - // Setting up Blend Pool client - let blend_pool_address = get_blend_pool(e); - let blend_pool_client = BlendPoolClient::new(e, &blend_pool_address); - - blend_pool_client.get_positions(from) + pool_client.claim(from, &vec![&e, 3u32], from) } \ 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..903615cf --- /dev/null +++ b/apps/contracts/strategies/blend/src/constants.rs @@ -0,0 +1,6 @@ +/// 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; diff --git a/apps/contracts/strategies/blend/src/lib.rs b/apps/contracts/strategies/blend/src/lib.rs index da4c66b6..881e51a1 100644 --- a/apps/contracts/strategies/blend/src/lib.rs +++ b/apps/contracts/strategies/blend/src/lib.rs @@ -1,13 +1,15 @@ #![no_std] +use constants::MIN_DUST; use soroban_sdk::{ - contract, contractimpl, token::TokenClient, Address, Env, IntoVal, String, Val, Vec}; + contract, contractimpl, panic_with_error, token::TokenClient, Address, Env, IntoVal, String, Val, Vec}; mod blend_pool; +mod constants; +mod positions; +mod reserves; mod storage; -use storage::{ - extend_instance_ttl, get_reserve_id, get_underlying_asset, is_initialized, set_blend_pool, set_initialized, set_reserve_id, set_underlying_asset -}; +use storage::{extend_instance_ttl, is_initialized, set_initialized, Config}; pub use defindex_strategy_core::{ DeFindexStrategyTrait, @@ -46,14 +48,19 @@ impl DeFindexStrategyTrait for BlendStrategy { return Err(StrategyError::AlreadyInitialized); } - let blend_pool_address = init_args.get(0).ok_or(StrategyError::InvalidArgument)?.into_val(&e); - let reserve_id = init_args.get(1).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); set_initialized(&e); - set_blend_pool(&e, blend_pool_address); - set_reserve_id(&e, reserve_id); - set_underlying_asset(&e, &asset); + let config = Config { + asset: asset.clone(), + pool: blend_pool_address.clone(), + reserve_id: reserve_id.clone(), + }; + + storage::set_config(&e, config); + event::emit_initialize(&e, String::from_str(&e, STARETEGY_NAME), asset); extend_instance_ttl(&e); Ok(()) @@ -63,7 +70,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( @@ -76,11 +83,29 @@ impl DeFindexStrategyTrait for BlendStrategy { extend_instance_ttl(&e); from.require_auth(); - // transfer tokens from the vault to the contract - let underlying_asset = get_underlying_asset(&e); - TokenClient::new(&e, &underlying_asset).transfer(&from, &e.current_contract_address(), &amount); + // 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 mut reserves = storage::get_strategy_reserves(&e); + + let config = storage::get_config(&e); + // transfer tokens from the vault to the strategy contract + TokenClient::new(&e, &config.asset).transfer(&from, &e.current_contract_address(), &amount); - blend_pool::supply(&e, &from, underlying_asset, 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.add(amount, b_tokens_minted); + + // Keeping track of the total amount deposited by the user and the total amount of bTokens owned by the user + let mut vault_position = storage::get_vault_position(&e, &from); + vault_position.add(amount, b_tokens_minted); + + storage::set_strategy_reserves(&e, reserves); + storage::set_vault_position(&e, &from, vault_position); event::emit_deposit(&e, String::from_str(&e, STARETEGY_NAME), amount, from); Ok(()) @@ -89,9 +114,10 @@ impl DeFindexStrategyTrait for BlendStrategy { fn harvest(e: Env, from: Address) -> Result<(), StrategyError> { check_initialized(&e)?; extend_instance_ttl(&e); - from.require_auth(); + from.require_auth(); - blend_pool::claim(&e, &from); + let config = storage::get_config(&e); + blend_pool::claim(&e, &from, &config); event::emit_harvest(&e, String::from_str(&e, STARETEGY_NAME), 0i128, from); Ok(()) @@ -106,13 +132,39 @@ impl DeFindexStrategyTrait for BlendStrategy { check_nonnegative_amount(amount)?; extend_instance_ttl(&e); 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 mut 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); + + + if tokens_withdrawn <= 0 { + panic_with_error!(e, StrategyError::InvalidArgument); + } + if b_tokens_burnt <= 0 { + panic_with_error!(e, StrategyError::InvalidArgument); + } + + reserves.remove(tokens_withdrawn, b_tokens_burnt); + + let mut vault_position = storage::get_vault_position(&e, &from); + vault_position.remove(amount, b_tokens_burnt); - let underlying_asset = get_underlying_asset(&e); - blend_pool::withdraw(&e, &from, underlying_asset, amount); + storage::set_strategy_reserves(&e, reserves); + storage::set_vault_position(&e, &from, vault_position); event::emit_withdraw(&e, String::from_str(&e, STARETEGY_NAME), amount, from); - Ok(amount) + Ok(tokens_withdrawn) } fn balance( @@ -122,12 +174,10 @@ impl DeFindexStrategyTrait for BlendStrategy { check_initialized(&e)?; extend_instance_ttl(&e); - let positions = blend_pool::get_positions(&e, &from); - let reserve_id = get_reserve_id(&e); + let vault_position = storage::get_vault_position(&e, &from); - let supply = positions.supply.get(reserve_id).unwrap_or(0i128); - Ok(supply) + Ok(vault_position.b_tokens) } } -mod test; \ No newline at end of file +// mod test; \ No newline at end of file diff --git a/apps/contracts/strategies/blend/src/positions.rs b/apps/contracts/strategies/blend/src/positions.rs new file mode 100644 index 00000000..efe7470c --- /dev/null +++ b/apps/contracts/strategies/blend/src/positions.rs @@ -0,0 +1,23 @@ +use soroban_sdk::contracttype; + +#[contracttype] +pub struct VaultPosition { + /// Total amount deposited by the user + pub deposited: i128, + /// Total amount withdrawn by the user + pub withdrawn: i128, + /// Total amount of bTokens owned by the user + pub b_tokens: i128, +} + +impl VaultPosition { + pub fn add(&mut self, amount: i128, b_tokens: i128) { + self.deposited += amount; + self.b_tokens += b_tokens; + } + + pub fn remove(&mut self, amount: i128, b_tokens: i128) { + self.withdrawn += amount; + self.b_tokens -= b_tokens; + } +} \ No newline at end of file diff --git a/apps/contracts/strategies/blend/src/reserves.rs b/apps/contracts/strategies/blend/src/reserves.rs new file mode 100644 index 00000000..b4598271 --- /dev/null +++ b/apps/contracts/strategies/blend/src/reserves.rs @@ -0,0 +1,39 @@ +use soroban_fixed_point_math::{i128, FixedPoint}; +use soroban_sdk::contracttype; + +use crate::constants::SCALAR_9; + +#[contracttype] +pub struct StrategyReserves { + /// The total deposited amount of the underlying asset + pub total_deposited: 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 { + pub fn add(&mut self, amount: i128, b_tokens: i128) { + // Calculate the new bRate - 9 decimal places of precision + // Update the reserve's bRate + self.b_rate = new_rate(amount, b_tokens); + + self.total_b_tokens += b_tokens; + self.total_deposited += amount; + } + + pub fn remove(&mut self, amount: i128, b_tokens: i128) { + // Calculate the new bRate - 9 decimal places of precision + // Update the reserve's bRate + self.b_rate = new_rate(amount, b_tokens); + + self.total_b_tokens -= b_tokens; + } +} + +fn new_rate(amount: i128, b_tokens: i128) -> i128 { + amount + .fixed_div_floor(b_tokens, SCALAR_9) + .unwrap() +} \ 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 fa6f51b4..5485879f 100644 --- a/apps/contracts/strategies/blend/src/storage.rs +++ b/apps/contracts/strategies/blend/src/storage.rs @@ -1,19 +1,29 @@ use soroban_sdk::{contracttype, Address, Env}; +use crate::{positions::VaultPosition, reserves::StrategyReserves}; + +#[contracttype] +pub struct Config { + pub asset: Address, + pub pool: Address, + pub reserve_id: u32, +} + #[derive(Clone)] #[contracttype] pub enum DataKey { Initialized, - UnderlyingAsset, - BlendPool, - Balance(Address), - ReserveId + Config, + Reserves, + VaultPos(Address) // Vaults Positions } 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() @@ -29,28 +39,46 @@ 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_underlying_asset(e: &Env) -> Address { - e.storage().instance().get(&DataKey::UnderlyingAsset).unwrap() +pub fn get_config(e: &Env) -> Config { + e.storage().instance().get(&DataKey::Config).unwrap() +} + +// Vault Position +pub fn set_vault_position(e: &Env, address: &Address, vault_position: VaultPosition) { + let key = DataKey::VaultPos(address.clone()); + e.storage().persistent().set(&key, &vault_position); + e.storage() + .persistent() + .extend_ttl(&key, LEDGER_THRESHOLD, LEDGER_BUMP); } -// Blend Pool Address -pub fn set_blend_pool(e: &Env, address: Address) { - e.storage().instance().set(&DataKey::BlendPool, &address); +pub fn get_vault_position(e: &Env, address: &Address) -> VaultPosition { + e.storage().persistent().get(&DataKey::VaultPos(address.clone())).unwrap_or( + VaultPosition { + deposited: 0, + withdrawn: 0, + b_tokens: 0, + } + ) } -pub fn get_blend_pool(e: &Env) -> Address { - e.storage().instance().get(&DataKey::BlendPool).unwrap() +// Strategy Reserves +pub fn set_strategy_reserves(e: &Env, new_reserves: StrategyReserves) { + e.storage().instance().set(&DataKey::Reserves, &new_reserves); } -pub fn set_reserve_id(e: &Env, id: u32) { - e.storage().instance().set(&DataKey::ReserveId, &id); +pub fn get_strategy_reserves(e: &Env) -> StrategyReserves { + e.storage().instance().get(&DataKey::Reserves).unwrap_or( + StrategyReserves { + total_deposited: 0, + total_b_tokens: 0, + b_rate: 0, + } + ) } -pub fn get_reserve_id(e: &Env) -> u32 { - e.storage().instance().get(&DataKey::ReserveId).unwrap() -} \ No newline at end of file From 0e66d5e2b37a050ee995af1235a3c2c028a794c6 Mon Sep 17 00:00:00 2001 From: coderipper Date: Fri, 29 Nov 2024 11:38:31 -0300 Subject: [PATCH 3/8] Keeping track of shares --- .../strategies/blend/src/blend_pool.rs | 2 +- apps/contracts/strategies/blend/src/lib.rs | 48 +++---- .../strategies/blend/src/positions.rs | 23 ---- .../strategies/blend/src/reserves.rs | 123 +++++++++++++++--- .../contracts/strategies/blend/src/storage.rs | 27 ++-- 5 files changed, 140 insertions(+), 83 deletions(-) delete mode 100644 apps/contracts/strategies/blend/src/positions.rs diff --git a/apps/contracts/strategies/blend/src/blend_pool.rs b/apps/contracts/strategies/blend/src/blend_pool.rs index 90b527e1..1f8c324f 100644 --- a/apps/contracts/strategies/blend/src/blend_pool.rs +++ b/apps/contracts/strategies/blend/src/blend_pool.rs @@ -113,5 +113,5 @@ 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, 3u32], from) + pool_client.claim(from, &vec![&e, config.reserve_id], from) } \ No newline at end of file diff --git a/apps/contracts/strategies/blend/src/lib.rs b/apps/contracts/strategies/blend/src/lib.rs index 881e51a1..fd113551 100644 --- a/apps/contracts/strategies/blend/src/lib.rs +++ b/apps/contracts/strategies/blend/src/lib.rs @@ -5,7 +5,6 @@ use soroban_sdk::{ mod blend_pool; mod constants; -mod positions; mod reserves; mod storage; @@ -89,7 +88,13 @@ impl DeFindexStrategyTrait for BlendStrategy { return Err(StrategyError::InvalidArgument); //TODO: create a new error type for this } - let mut reserves = storage::get_strategy_reserves(&e); + // Harvest if rewards exceed threshold + // let rewards = blend_pool::claim_rewards(&e); + // if rewards > REWARD_THRESHOLD { + // blend_pool::reinvest_rewards(&e, rewards); + // } + + let reserves = storage::get_strategy_reserves(&e); let config = storage::get_config(&e); // transfer tokens from the vault to the strategy contract @@ -98,14 +103,7 @@ impl DeFindexStrategyTrait for BlendStrategy { 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.add(amount, b_tokens_minted); - - // Keeping track of the total amount deposited by the user and the total amount of bTokens owned by the user - let mut vault_position = storage::get_vault_position(&e, &from); - vault_position.add(amount, b_tokens_minted); - - storage::set_strategy_reserves(&e, reserves); - storage::set_vault_position(&e, &from, vault_position); + reserves::deposit(&e, reserves, &from, amount, b_tokens_minted); event::emit_deposit(&e, String::from_str(&e, STARETEGY_NAME), amount, from); Ok(()) @@ -117,7 +115,12 @@ impl DeFindexStrategyTrait for BlendStrategy { from.require_auth(); let config = storage::get_config(&e); - blend_pool::claim(&e, &from, &config); + let _harvested_blend = blend_pool::claim(&e, &from, &config); + + // should swap to usdc + // should supply to the pool + + // etcetc event::emit_harvest(&e, String::from_str(&e, STARETEGY_NAME), 0i128, from); Ok(()) @@ -139,28 +142,13 @@ impl DeFindexStrategyTrait for BlendStrategy { return Err(StrategyError::InvalidArgument) //TODO: create a new error type for this } - let mut reserves = storage::get_strategy_reserves(&e); + 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); - - if tokens_withdrawn <= 0 { - panic_with_error!(e, StrategyError::InvalidArgument); - } - if b_tokens_burnt <= 0 { - panic_with_error!(e, StrategyError::InvalidArgument); - } - - reserves.remove(tokens_withdrawn, b_tokens_burnt); - - let mut vault_position = storage::get_vault_position(&e, &from); - vault_position.remove(amount, b_tokens_burnt); - - storage::set_strategy_reserves(&e, reserves); - storage::set_vault_position(&e, &from, vault_position); + 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); @@ -174,9 +162,9 @@ impl DeFindexStrategyTrait for BlendStrategy { check_initialized(&e)?; extend_instance_ttl(&e); - let vault_position = storage::get_vault_position(&e, &from); + let vault_shares = storage::get_vault_shares(&e, &from); - Ok(vault_position.b_tokens) + Ok(vault_shares) } } diff --git a/apps/contracts/strategies/blend/src/positions.rs b/apps/contracts/strategies/blend/src/positions.rs deleted file mode 100644 index efe7470c..00000000 --- a/apps/contracts/strategies/blend/src/positions.rs +++ /dev/null @@ -1,23 +0,0 @@ -use soroban_sdk::contracttype; - -#[contracttype] -pub struct VaultPosition { - /// Total amount deposited by the user - pub deposited: i128, - /// Total amount withdrawn by the user - pub withdrawn: i128, - /// Total amount of bTokens owned by the user - pub b_tokens: i128, -} - -impl VaultPosition { - pub fn add(&mut self, amount: i128, b_tokens: i128) { - self.deposited += amount; - self.b_tokens += b_tokens; - } - - pub fn remove(&mut self, amount: i128, b_tokens: i128) { - self.withdrawn += amount; - self.b_tokens -= b_tokens; - } -} \ No newline at end of file diff --git a/apps/contracts/strategies/blend/src/reserves.rs b/apps/contracts/strategies/blend/src/reserves.rs index b4598271..de2dcdeb 100644 --- a/apps/contracts/strategies/blend/src/reserves.rs +++ b/apps/contracts/strategies/blend/src/reserves.rs @@ -1,12 +1,13 @@ +use defindex_strategy_core::StrategyError; use soroban_fixed_point_math::{i128, FixedPoint}; -use soroban_sdk::contracttype; +use soroban_sdk::{contracttype, panic_with_error, Address, Env}; -use crate::constants::SCALAR_9; +use crate::{constants::SCALAR_9, storage}; #[contracttype] pub struct StrategyReserves { /// The total deposited amount of the underlying asset - pub total_deposited: i128, + pub total_shares: i128, /// The total bToken deposits owned by the strategy depositors. pub total_b_tokens: i128, /// The reserve's last bRate @@ -14,26 +15,112 @@ pub struct StrategyReserves { } impl StrategyReserves { - pub fn add(&mut self, amount: i128, b_tokens: i128) { - // Calculate the new bRate - 9 decimal places of precision - // Update the reserve's bRate - self.b_rate = new_rate(amount, b_tokens); - - self.total_b_tokens += b_tokens; - self.total_deposited += amount; + /// 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() } - pub fn remove(&mut self, amount: i128, b_tokens: i128) { + /// 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 - self.b_rate = new_rate(amount, b_tokens); - - self.total_b_tokens -= b_tokens; + let new_rate = amount + .fixed_div_floor(b_tokens, SCALAR_9) + .unwrap(); + + self.b_rate = new_rate; } + } -fn new_rate(amount: i128, b_tokens: i128) -> i128 { - amount - .fixed_div_floor(b_tokens, SCALAR_9) - .unwrap() +/// 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 } \ 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 5485879f..42fd9635 100644 --- a/apps/contracts/strategies/blend/src/storage.rs +++ b/apps/contracts/strategies/blend/src/storage.rs @@ -1,6 +1,6 @@ use soroban_sdk::{contracttype, Address, Env}; -use crate::{positions::VaultPosition, reserves::StrategyReserves}; +use crate::reserves::StrategyReserves; #[contracttype] pub struct Config { @@ -49,22 +49,27 @@ pub fn get_config(e: &Env) -> Config { } // Vault Position -pub fn set_vault_position(e: &Env, address: &Address, vault_position: VaultPosition) { +/// 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, &vault_position); + e.storage().persistent().set::(&key, &shares); e.storage() .persistent() .extend_ttl(&key, LEDGER_THRESHOLD, LEDGER_BUMP); } -pub fn get_vault_position(e: &Env, address: &Address) -> VaultPosition { - e.storage().persistent().get(&DataKey::VaultPos(address.clone())).unwrap_or( - VaultPosition { - deposited: 0, - withdrawn: 0, - b_tokens: 0, +/// 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, + } } // Strategy Reserves @@ -75,7 +80,7 @@ pub fn set_strategy_reserves(e: &Env, new_reserves: StrategyReserves) { pub fn get_strategy_reserves(e: &Env) -> StrategyReserves { e.storage().instance().get(&DataKey::Reserves).unwrap_or( StrategyReserves { - total_deposited: 0, + total_shares: 0, total_b_tokens: 0, b_rate: 0, } From 30f39b94e212b820ced3ebe7ad6ef8ba5ac19ef6 Mon Sep 17 00:00:00 2001 From: Matias Poblete <86752543+MattPoblete@users.noreply.github.com> Date: Mon, 2 Dec 2024 10:19:16 -0300 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=A9=B9=20Fix=20deposit=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/contracts/src/tests/blend/test_vault.ts | 2 +- apps/contracts/src/tests/vault.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/contracts/src/tests/blend/test_vault.ts b/apps/contracts/src/tests/blend/test_vault.ts index 167a121a..4be22505 100644 --- a/apps/contracts/src/tests/blend/test_vault.ts +++ b/apps/contracts/src/tests/blend/test_vault.ts @@ -173,7 +173,7 @@ export async function testBlendVault(user?: Keypair) { console.log('🚀 « investResult:', investResult); console.log(green, '---------------------- Invested in strategy ----------------------') - console.log(green, 'Invested: ', scValToNative(investResult.returnValue), ' in the strategy') + console.log(green, 'Invested: ', investResult, ' in the strategy') console.log(green, '------------------------------------------------------------------') } catch (error) { console.log('❌ Error Investing the Vault:', error); diff --git a/apps/contracts/src/tests/vault.ts b/apps/contracts/src/tests/vault.ts index 4260782b..3b99bada 100644 --- a/apps/contracts/src/tests/vault.ts +++ b/apps/contracts/src/tests/vault.ts @@ -21,9 +21,10 @@ import { airdropAccount, invokeCustomContract } from "../utils/contract.js"; const network = process.argv[2]; -export async function depositToVault(deployedVault: string, amount: number[], user?: Keypair, ) { +export async function depositToVault(deployedVault: string, amount: number[], user?: Keypair, invest?: boolean) { // Create and fund a new user account if not provided const newUser = user ? user : Keypair.random(); + const investDeposit = invest ? invest : false; console.log('🚀 ~ depositToVault ~ newUser.publicKey():', newUser.publicKey()); console.log('🚀 ~ depositToVault ~ newUser.secret():', newUser.secret()); @@ -41,7 +42,8 @@ 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(investDeposit) ]; try { From 7a65c7f37c02ebe1a2ca6b82e4e3ff401296161d Mon Sep 17 00:00:00 2001 From: coderipper Date: Tue, 3 Dec 2024 15:08:21 -0300 Subject: [PATCH 5/8] blend strategy, rust tests --- apps/contracts/Cargo.lock | 20 + apps/contracts/strategies/blend/Cargo.toml | 2 + .../strategies/blend/src/blend_pool.rs | 42 +- .../strategies/blend/src/constants.rs | 2 + apps/contracts/strategies/blend/src/lib.rs | 59 ++- .../strategies/blend/src/reserves.rs | 21 + .../strategies/blend/src/soroswap.rs | 27 ++ .../contracts/strategies/blend/src/storage.rs | 4 +- apps/contracts/strategies/blend/src/test.rs | 386 +++++++++++++++--- .../strategies/blend/src/test/success.rs | 228 +++++++++++ .../external_wasms/blend/backstop.wasm | Bin 0 -> 27518 bytes .../external_wasms/blend/comet.wasm | Bin 0 -> 29046 bytes .../external_wasms/blend/emitter.wasm | Bin 0 -> 10448 bytes .../external_wasms/blend/pool_factory.wasm | Bin 0 -> 2898 bytes 14 files changed, 712 insertions(+), 79 deletions(-) create mode 100644 apps/contracts/strategies/blend/src/soroswap.rs create mode 100644 apps/contracts/strategies/blend/src/test/success.rs create mode 100644 apps/contracts/strategies/external_wasms/blend/backstop.wasm create mode 100644 apps/contracts/strategies/external_wasms/blend/comet.wasm create mode 100644 apps/contracts/strategies/external_wasms/blend/emitter.wasm create mode 100644 apps/contracts/strategies/external_wasms/blend/pool_factory.wasm diff --git a/apps/contracts/Cargo.lock b/apps/contracts/Cargo.lock index 4226274a..39fb2feb 100644 --- a/apps/contracts/Cargo.lock +++ b/apps/contracts/Cargo.lock @@ -97,6 +97,8 @@ name = "blend_strategy" version = "0.1.0" dependencies = [ "defindex-strategy-core", + "sep-40-oracle", + "sep-41-token", "soroban-fixed-point-math", "soroban-sdk", ] @@ -928,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" diff --git a/apps/contracts/strategies/blend/Cargo.toml b/apps/contracts/strategies/blend/Cargo.toml index 15acfa03..61a40a63 100644 --- a/apps/contracts/strategies/blend/Cargo.toml +++ b/apps/contracts/strategies/blend/Cargo.toml @@ -17,3 +17,5 @@ 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 1f8c324f..f99f4a6f 100644 --- a/apps/contracts/strategies/blend/src/blend_pool.rs +++ b/apps/contracts/strategies/blend/src/blend_pool.rs @@ -1,7 +1,7 @@ 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::Config; +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" @@ -114,4 +114,44 @@ pub fn claim(e: &Env, from: &Address, config: &Config) -> i128 { // 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); + + 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 index 903615cf..b2417fec 100644 --- a/apps/contracts/strategies/blend/src/constants.rs +++ b/apps/contracts/strategies/blend/src/constants.rs @@ -4,3 +4,5 @@ pub const SCALAR_7: i128 = 1_0000000; 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 fd113551..3edb42fe 100644 --- a/apps/contracts/strategies/blend/src/lib.rs +++ b/apps/contracts/strategies/blend/src/lib.rs @@ -1,13 +1,16 @@ #![no_std] -use constants::MIN_DUST; +use blend_pool::perform_reinvest; +use constants::{MIN_DUST, SCALAR_9}; use soroban_sdk::{ - contract, contractimpl, panic_with_error, token::TokenClient, Address, Env, IntoVal, String, Val, Vec}; + contract, contractimpl, token::TokenClient, vec, Address, Env, IntoVal, String, Val, Vec}; mod blend_pool; mod constants; mod reserves; +mod soroswap; mod storage; +use soroswap::internal_swap_exact_tokens_for_tokens; use storage::{extend_instance_ttl, is_initialized, set_initialized, Config}; pub use defindex_strategy_core::{ @@ -49,13 +52,17 @@ impl DeFindexStrategyTrait for BlendStrategy { 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); let config = Config { asset: asset.clone(), - pool: blend_pool_address.clone(), - reserve_id: reserve_id.clone(), + pool: blend_pool_address, + reserve_id, + blend_token, + router: soroswap_router, }; storage::set_config(&e, config); @@ -88,15 +95,11 @@ impl DeFindexStrategyTrait for BlendStrategy { return Err(StrategyError::InvalidArgument); //TODO: create a new error type for this } - // Harvest if rewards exceed threshold - // let rewards = blend_pool::claim_rewards(&e); - // if rewards > REWARD_THRESHOLD { - // blend_pool::reinvest_rewards(&e, rewards); - // } + let config = storage::get_config(&e); + perform_reinvest(&e, &config)?; let reserves = storage::get_strategy_reserves(&e); - let config = storage::get_config(&e); // transfer tokens from the vault to the strategy contract TokenClient::new(&e, &config.asset).transfer(&from, &e.current_contract_address(), &amount); @@ -112,17 +115,13 @@ impl DeFindexStrategyTrait for BlendStrategy { fn harvest(e: Env, from: Address) -> Result<(), StrategyError> { check_initialized(&e)?; extend_instance_ttl(&e); - from.require_auth(); let config = storage::get_config(&e); - let _harvested_blend = blend_pool::claim(&e, &from, &config); + let harvested_blend = blend_pool::claim(&e, &e.current_contract_address(), &config); - // should swap to usdc - // should supply to the pool + perform_reinvest(&e, &config)?; - // etcetc - - 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(()) } @@ -161,11 +160,29 @@ impl DeFindexStrategyTrait for BlendStrategy { ) -> Result { check_initialized(&e)?; extend_instance_ttl(&e); - + + // Get the vault's shares let vault_shares = storage::get_vault_shares(&e, &from); - - Ok(vault_shares) + + // 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) } } -// mod test; \ No newline at end of file +#[cfg(any(test, feature = "testutils"))] +mod test; \ No newline at end of file diff --git a/apps/contracts/strategies/blend/src/reserves.rs b/apps/contracts/strategies/blend/src/reserves.rs index de2dcdeb..3369377e 100644 --- a/apps/contracts/strategies/blend/src/reserves.rs +++ b/apps/contracts/strategies/blend/src/reserves.rs @@ -123,4 +123,25 @@ pub fn withdraw( 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 42fd9635..2cfe07d3 100644 --- a/apps/contracts/strategies/blend/src/storage.rs +++ b/apps/contracts/strategies/blend/src/storage.rs @@ -7,6 +7,8 @@ pub struct Config { pub asset: Address, pub pool: Address, pub reserve_id: u32, + pub blend_token: Address, + pub router: Address, } #[derive(Clone)] @@ -19,7 +21,7 @@ pub enum DataKey { 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; diff --git a/apps/contracts/strategies/blend/src/test.rs b/apps/contracts/strategies/blend/src/test.rs index 4e4ba291..041fedaf 100644 --- a/apps/contracts/strategies/blend/src/test.rs +++ b/apps/contracts/strategies/blend/src/test.rs @@ -1,79 +1,353 @@ #![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}, constants::SCALAR_7, storage::DAY_IN_LEDGERS, BlendStrategy, BlendStrategyClient +}; +use sep_41_token::testutils::MockTokenClient; +use soroban_fixed_point_math::FixedPoint; 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"); +} + +mod blend_emitter { + soroban_sdk::contractimport!(file = "../external_wasms/blend/emitter.wasm"); +} + +mod blend_backstop { + soroban_sdk::contractimport!(file = "../external_wasms/blend/backstop.wasm"); +} + +mod blend_comet { + soroban_sdk::contractimport!(file = "../external_wasms/blend/comet.wasm"); +} + +pub(crate) fn register_blend_strategy(e: &Env) -> Address { + e.register_contract(None, BlendStrategy {}) +} -// use blend_pool_module::BlendPoolContractClient; +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>, +} -// // fn initialize(admin: address, name: string, oracle: address, bstop_rate: u32, max_postions: u32, backstop_id: address, blnd_id: 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); -// 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 -// } + // 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); -// Blend Strategy Contract -fn create_blend_strategy<'a>(e: &Env) -> BlendStrategyClient<'a> { - BlendStrategyClient::new(e, &e.register_contract(None, BlendStrategy {})) + // 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; } -// 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()) +/// 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 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), + ]; + + client.initialize(&underlying_asset, &init_args); + address } -pub struct HodlStrategyTest<'a> { - env: Env, - strategy: BlendStrategyClient<'a>, - token: TokenClient<'a>, - user: Address, +pub trait EnvTestUtils { + /// Jump the env by the given amount of ledgers. Assumes 5 seconds per ledger. + fn jump(&self, ledgers: u32); + + /// 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<'a> HodlStrategyTest<'a> { - fn setup() -> 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, + }); + } + + 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 env = Env::default(); - env.mock_all_auths(); + 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 strategy = create_blend_strategy(&env); - let admin = Address::generate(&env); - let token = create_token_contract(&env, &admin); - let user = Address::generate(&env); + let pool_hash = env.deployer().upload_contract_wasm(blend_pool::WASM); - // Mint 1,000,000,000 to user - StellarAssetClient::new(&env, &token.address).mint(&user, &1_000_000_000); + 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(); - HodlStrategyTest { - env, - strategy, - token, - user + BlendFixture { + backstop: backstop_client, + emitter: emitter_client, + backstop_token: comet_client, + pool_factory: pool_factory_client, } } - - // pub(crate) fn generate_random_users(e: &Env, users_count: u32) -> vec::Vec
{ - // let mut users = vec![]; - // for _c in 0..users_count { - // users.push(Address::generate(e)); - // } - // users - // } } -// mod initialize; -// mod deposit; -// mod events; -// mod withdraw; \ No newline at end of file +mod success; \ No newline at end of file diff --git a/apps/contracts/strategies/blend/src/test/success.rs b/apps/contracts/strategies/blend/src/test/success.rs new file mode 100644 index 00000000..46511ff2 --- /dev/null +++ b/apps/contracts/strategies/blend/src/test/success.rs @@ -0,0 +1,228 @@ +#![cfg(test)] +use crate::blend_pool::{BlendPoolClient, Request}; +use crate::constants::{MIN_DUST, SCALAR_7}; +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_fixed_point_math::FixedPoint; +use soroban_sdk::testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation}; +use soroban_sdk::{vec, Address, Env, Error, 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 user_5 = 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_3 and user_4 + * -> deposit 200 into pool for user_5 + * -> 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_3, &starting_balance); + usdc_client.mint(&user_4, &starting_balance); + + let user_3_balance = usdc_client.balance(&user_3); + assert_eq!(user_3_balance, starting_balance); + + + strategy_client.deposit(&starting_balance, &user_3); + // -> verify deposit auth + + assert_eq!( + e.auths()[0], + ( + user_3.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + strategy.clone(), + Symbol::new(&e, "deposit"), + vec![ + &e, + starting_balance.into_val(&e), + user_3.to_val(), + ] + )), + sub_invocations: std::vec![AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + usdc.address().clone(), + Symbol::new(&e, "transfer"), + vec![ + &e, + user_3.to_val(), + strategy.to_val(), + starting_balance.into_val(&e) + ] + )), + sub_invocations: std::vec![] + }] + } + ) + ); + + strategy_client.deposit(&starting_balance, &user_4); + + // verify deposit (pool b_rate still 1 as no time has passed) + assert_eq!(usdc_client.balance(&user_3), 0); + assert_eq!(usdc_client.balance(&user_4), 0); + assert_eq!(strategy_client.balance(&user_3), starting_balance); + assert_eq!(strategy_client.balance(&user_4), 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_5 deposit directly into pool + let merry_starting_balance = 200_0000000; + usdc_client.mint(&user_5, &merry_starting_balance); + pool_client.submit( + &user_5, + &user_5, + &user_5, + &vec![ + &e, + Request { + request_type: 0, + address: usdc.address().clone(), + amount: merry_starting_balance, + }, + ], + ); + + // admin borrow back to 50% util rate + let borrow_amount = (merry_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_5 + * -> withdraw (excluding dust) from blend strategy for user_3 and user_4 + * -> 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_5 + pool_client.submit( + &user_5, + &user_5, + &user_5, + &vec![ + &e, + Request { + request_type: 1, + address: usdc.address().clone(), + amount: merry_starting_balance * 2, + }, + ], + ); + let user_5_final_balance = usdc_client.balance(&user_5); + let user_5_profit = user_5_final_balance - merry_starting_balance; + + // withdraw from blend strategy for user_3 and user_4 + // they are expected to receive half of the profit of user_5 + let expected_user_4_profit = user_5_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_4); + assert_eq!(result, Err(Ok(StrategyError::InvalidArgument))); // TODO: Check which is the one failing + + strategy_client.withdraw(&withdraw_amount, &user_3); + // -> verify withdraw auth + assert_eq!( + e.auths()[0], + ( + user_3.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + strategy.clone(), + Symbol::new(&e, "withdraw"), + vec![ + &e, + withdraw_amount.into_val(&e), + user_3.to_val(), + ] + )), + sub_invocations: std::vec![] + } + ) + ); + + strategy_client.withdraw(&withdraw_amount, &user_4); + + // -> verify withdraw + assert_eq!(usdc_client.balance(&user_3), withdraw_amount); + assert_eq!(usdc_client.balance(&user_4), withdraw_amount); + assert_eq!(strategy_client.balance(&user_3), 0); + assert_eq!(strategy_client.balance(&user_4), 0); + + // -> verify withdraw from empty vault fails + let result = strategy_client.try_withdraw(&MIN_DUST, &user_4); + 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/external_wasms/blend/backstop.wasm b/apps/contracts/strategies/external_wasms/blend/backstop.wasm new file mode 100644 index 0000000000000000000000000000000000000000..acf4b9e0478cdfd22339382b908c8cd3c7ef6a6b GIT binary patch literal 27518 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 Whjt&E8W|cJ9cng@4GkYV=KX*Cr_BNY literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..de1b1fa4293e35ee5f6e10affe2755d38f7de2df GIT binary patch literal 29046 zcmcJY36Nb^THo*X-s|q$%WdtJyxjNf8B5G2ukv`txZm?KHnwqFlGiwqTIy%3wRFqf zEnAwjqz1J;3NS`thQyVEph^ntR3YLdBup>?O$b#cGZd-{HB4n7C_qgaC{n6qfFUGI ze*f>B``&%+mh8dg*}C`KvwY_}-+q>RJD8X|6b3;M{%Lsq?&!pc@Wk#=mwu!?Z2+jKbihAygWW-}yDA{Ai<2i# z0!rQtQOkp<}9$S)rn;R+P`(!|)lvwF04Ab~ddXzd*I1z3Q!tllL7Osb=ACXq+q3~9&FIvu|!Pyf| zS^idF9`X=Wa}RA*^LegQ;T^!40V;NDwu7qQ{g^?GiBFfmh)MzN5k95IjZ{c>L@Ul#ri0icf~k#l`bMdNOF9eWi4-{hOpw zJ=8ulGkYuwlBwyb`QZ8F`6CAp2UGp~XQrli9iEvv7#!$tADx4R(LUYa<(t9^80 z@BFTbLo-LF=XXs_2Zz?aJA3BHd@z$wwh!-`pE)p1W!2pEe0zS^L6d2f|v>=c%x7ewOObF{Gt%s7N!gnM1+LVY27Q>~!!- z*sr_G{9M>K+1@(^j&s53Fqxh>)DC_qtj--fv(K{JkGMA2bKDTLwx0kQ=_-S@)9&U;I z^01lhzadChF~Amj^GkmeG#B6g>md7{)W-agJj|nWt^Ac%vXsX@p)vn{XUrD^8+3Wd zd9XPOG|t4vnOZN+UltyWEf458yG{LX0B;KO{moxfVWp>V^I)vPt^1q*`Q3$gFft0q z4m5w$7rGHWpi%x?W%Xyb)FaS`EBm*^!M6A*J<#>Gc$`$!yr^2#XkG~RH>2!!hf<%w zUJOK(kCxb)z*a;cPt-K~Se}S@aTadO6Gy>&D9?e`y8Cl4w}R~?Br5#Ij<>$!f58*8 z$q7};)>Wv`_ev{WO1A+hXn~*O0)?MwhWkxYq6gFMVRSM&Nrmr(r&>dg(aI%WxGx!_ z!!Y}BgT~t#Y0$d+^y`eCMBnEc6zLXPNgnP<>v8iWUB{c!wO|TCME`)kB$9NrKu1V$ zexMOCRKN+ZsP*d4{gXeff?Iq7vSjxsCz?S$jK}ow88C<#d>-h$N(61h%w*Zb5i0Y}5ocnjpd{mn4Dxyxy? zOzm#%G>9R3oI@9+DyJO;p?hdoY81Oho1sG>PEBG= zcsJC^kvvG2#q6A$gnC7&5q>*#kfiD1w>xSt9dQ&%RZ^xt*^V98}>^Tlye8Q#tujvsjxZPpz?th|MLFU z;XdsV4fyYI zH!g&}!mujvNs{bfYG|Ji$=u*URkp;L^eiU%N>fkyn@Q80)E%dnmX;)7vRlB#W)*N{zC2vB zWF5Gel@8pn1UKx!1(bseeX<`3>p}CEUdGb?EEjqRc5EUOTbgI}LJiu>2qrePJ|CHQ zek!{w``&0Qaztu`ALvqB20EHAI`wY|8FQe=CaO6NQ;{5;F{{S)&oQl9t5= zQFe9q55s6IuY4>R7Hccb_4^yuEkS@GscZ>G>IqqDTrF#WQdH;@8ey8dB{>ExWG~=Cv%!?y2_!Uxo>~ z4WbWbbV+QeV|JD`!{fZ(AYgl86HYHN|4w8ipULe7^+We}(U3YX)&p5s;up)S$SpFO z{uW_K_9+)nV82_7|M9m!VIx2_mvxtb-2m7TC!nhhoO&y)S8?in<9W4p%gz_*co2`# z*VFC`bV#QROSBeM7%*eW;w;8fB-vf=A0xi25oQUb>jPZ4(5Q}|Tc{K2DtwV)Igv%6 zj~{`gXg=sOfPw5YQrBom_Su>?0VUAbU^(g6lQ2$aBRwiCdACi?NS+vfqVXaz;L)aRKF5gY7UiY@S{^ zub5bs2>`y2#OD2ZjMQWg>9%=XyeE; z9n`SoBnY;krUf)^IB@^=D5Kff0q5>g2mxKMZH14eA5p8shdFr>)^sKzooPvS)yDJ% zi`4T#lMrNCAz&kF{#sz>y_w$6_??#~I`@u&=om~C00ym7XRx70+0dpH*&2i=%+}T= z@z6f*I+L^&5V4Dy*)rau1M7C*p~3rWTb{#7(FVoHM$ zFhi*(W7)%vYMwllC}ylMBv_K2hvnh z3VE;N96g$rsmBcr$AV#LKqG>r0pel=(&N{;Hj z87d=O@tP8 zYw`~@xpoGu&M+rQc60XkKqKK_rTGn^G6X94SY%E%shU^l$91g!Q99p0v!y$_-TqB* zpqCB~BrnQZjWDZp*gL1zL7JJ}6^fv`_r$lPH@cA1Ynao)ArsHGAL>z z-JPCgE!r*k;h{15&Ho~5^g)dpN=3K}HL%oX*2M~7I1*IdgV-SRo>z(X z#nx)UhN{?5^=tq%l~7OEKvFIfsIHwOK(~6jyU>Qhcw|p^%mtX4O#)MAq7)+KiP@wI zHYq7=5}DaE7>GTRdhU;vtkWPAG0P<>{xAjssbKx0Qx^~D+n}kInoJ*d)ZXfSizuT7 zHM6%#>(&49$3Ond|L(v2FCI78Y%cpjqB}DQCSIDY(uenyaO_xI<(Cw>{UKdRcw3@)cJEr(Yy^jZTZ{p2Uv|jz@ zFZ^GWErXA^M90*`J&38XbSlA3nXVR0EyZ4{VCyW7Lgr!mG)CuJV332bJZMYb>Stjr zleIe=`miG!gCWlRYVlF2uLRp?v8O(Xw=6PenfO$ii>%uvSXyF-#Sl}gyfdK@aGf$w zhxJj!zP2&jCo3-d=7M0w<%^5?&SDA_GI7yrY$jXvdb6L2gPWqe0x*kQQBeg-6-iiY ztuzIW)92{>{6=50P zZ;GhkJ<|H}8$X@w%&Q%WocASbT7M8ck!Na|jd$9~>#N;x^Br%jvcYT!3QH&mY|4?6%R1wbo| zYpLYd9Pw88)!UD^KXFi;-Pl?}3(ow|VxJ|D$Kg5wm&^STpL;&YhPTCM$d|P{ zU6fzK$SsDgS6}<7Z~p3EetB#2^egxe)`z5%fWDm`i86!iNwUs{__|i)qvb_0IZ<@Q z4BE{2K)xy;$yY5jRy}b}nON|&uw}!*sOD?(EZYqvT#SNp*5>(uSr?L1(q3*+UA$EucVbN4Ip7JWdyZM>jUy#KJ2`N zVv`Qi^?@T;&o=?Hub@dV(*Rx(bLpl+*= z7l{s>7YGZrpVHE*cR&VgPr*5`H%1 zLQ{3c+(hP?j_)Al)~`RN$IVbu`)b5Mii^nEpy(A+*Yv2!732$BLu|x@>rHhDmNcUa z3YrQ9N7C$Zz@jrHaX5oG7*T&$#H)&Uge3Swdcb+O`7b<;{ls6hnR+a)bSz}C03b6N z7MlI?^)YU4ZUKNq)?EN#oz~6!88#(b@wr7SEgSe&1P9b=DCDRUF{&Pu zLk~SK>VbpRU`bjv`k_2QB@2qS+5u7ExVEE6fmh9?V1iqZmptQ-RSNYXsZv-1twwxr z6v)0XjUijxjy}Z-j!)TAfR(a_4G1 zUiUX}z_``8Fm%JJxv#zoXCkkf{T*4ORa!0>mbD%(S!Oyz%!Z&{PTMCtb za463f8k;c1WMYbkWQupFBZ=-N+=xYacZdSA9vYA3Y=$%zC|;FsP)tmq;i8lg?vhuKQ zDs@@wlqYk_gD^{Z*6D`wtk>Owb?*D>XnuK50Bc~%GmI4&#rxlh$OWvGEB$G;9BUX! z6}_&kJXpv=dB7P*Dfhl-kf7Ce=_<^5DU8(^mMW!B>SD`Q zrXIuE$VHuw17es-S)nUJ09_HvURSWBGV$xMT#{>IEa}SnvaYa9)T=8)SJaia-}*?E#htqgF>~2 z%}+*r#q}PvCV&O#O3|^Lmeua}wXb2JN!sUv2Isffpw~nbMGU+4(i9OlJJKr@2ATsr~7}ZA!^H#wHrb60Ya5PYc;yjP+kfQeUt{twW`6ZXUisYLv zxtin~F1d!JCzKYF-i|EhyUq_T#f(L+vxriWPZ^i#jOFyAvrQy@XDDdV**cQGGd8h` z&R7XE1`Uz*4ioQ`(-70jdnaUPj=^k+FNOKQGZcT@=HLw?#-|l5cD3h>w8sedbq4`e zUX8UHlpdINu==+p{(9KBHor!6z33pJR!>T!x{xjwka}%JgUIv(M>dF5<%RCV@eBH< zd?V>P?k4p44NWic5u@GX8CN?$=gv-y9?!2Xo7k&C<~%qHU+L^8h3EOs$ln%|FBr1r zR@ZA05lu|;Mt(ISeOJaN-?sScaEba~hfmiNli$lUMSOb`_;tY{-?*5y)XRkkW2X^( zT2F6=YPlqvz$Rx2Y z&@ha&UJUDMthv3QYvq@S;!UxIdF>mzV~}6do#0)}QLDq-;$10k+ZU#mF{%%vC6I!arJCQ#+;r-E?RZy{9^-B-|_nd_tVU*Z%!iI1Ip9DP{nl z$7=XZ6H?68>YBllaI+*aiy4*tr)E3mTiN^j<}hoD*IBq0uk4DXS>6?0Siu!B5XCC2 z4Pk>KVtc2H-W92Zxgx4LR|GGuA{&UNwB8jN=y$s!mWHhMwLPvV8gtGlzN|Bf&NjZggTvA%(sWw3JbZmYJBW5EV0d~nRxz0L%X?bTdvA(lwNczs?jC^PE_`Zh* z$v0#s)elCfz@m{kpY%qAPhue0OX5PmtQNNbk;!{Rq#hd+J$tnfV9jfU61Nwpn>fC&aK^}{*HkR7H2=hpC~Ge{-9no-s zKvDMv;qXp)9}CQb`7q1WLfeG`Zti?3tlvnS$lhs<6D2hqy6fb}*kzZLsy=Z+gHfQjrbgo6apX0M2^5-wHaHnmCYGCI z%*v$;!rZsXwA}Y!)A$c3Cl`?o|ciMI?a3`>|jlnD?GGW@tU00$oEeK)^ zG@D9n`aakUvU_n1lr!HT-|U>DY@&I0*6M=%vi_(?pZMLc{Y=P&P>1bBa)XT1Fvu90 z0#?q%CoI;LE5*99EFF!k;j^UKQ_CiRg-uq{@MehPI`j(tSKw%RwH8F)kwxXdzN~5b6{_^CP-WMXtJvkW%j&77 zrUsn&RtRd+&ruwL71Yb@l1xpwRDNebCML=U75B01!KJH!7CM>*(y9Z4b|&m8Ghs%Z z-D(D&PKEo_zNEl~1uH8sP7D>cl_M!|ZMgz#M;L@xQee4`k^-~YX==9mZ%u)*!QZ_C z(})R5KXIO{+dv|8c|5N!Q%-hxaJXfk8m6f0h;yCVA9o!p!@Ewy&P2j# zqtx|+ONtO)G37chx?J`@3)d;lWl-{>W|KhD+az=0Z@3~;*R$$EdMtHyQNL5yGb^d9 z)+<@J&F+-CUP8o))Jh6@Wpy3aLUOr!p{|7VkQ#UQZIHpkqF)_#z00dBrIJxu7Osc^ zS9r;c&#HBMP|H5|=1E{5pqs#0`-k5F__>+0rtIY&0cP5DglEWFr1L_@1>c_4ZPgo4@jw^+1)Yv!vF+7wfAg*X`WtWkWnfNDW&q+j72RzfRc^PW zH>_NnYsGUC*m@0>K5vW9hs~uQJg-wRMg#9i*~cG3HHX48&G57+cNbypZUahvg{Ylff3*EtVrYo*mG*;2sTT zouqR>-@Dzfd@!vAH`7A}Du~2;Xst=T>A2Tg6!tQaFj{L1286?p=Y8)|X%u*LxdJO= zxdN}OpBJ1GD&Y0HEDgs`No&`TL>b+{8tAZB=SJ!r$7@<~w{f4vB}lxTPOlLB7UF5n z*wp`w>w^GXo4d3{uuc8ZEp?80(I)WP*7B&B1qG-5%sr||xmf}>i>0$=#cnXY#W<7? ziYVO2@42X$1CBa2*>U8J5q#~iOhAUORcy?0Cj7IR17{3IwCqC&XLi#3{+ld1!dO99g5xFWGqOtC*0`@jvTh4^wTU+G{Zb_FENDv zC5F&mVhEej{iPOsW`Q%2)Hr8n7HNEoy%phB2?ACLv;cwNl2C9ExT!v)80 zik@URd@n z2MwKt!*4Av9_Hxzk`BO{QNtl^1uKrb^6d%ts)yck(b?x9eO2SKkL*A&3W zrC8rU%aw;E51K(;pDe-J@XoJ2jOjt=Vf8*)j^PJ_hNu-r-reC zHom%h1*2Ory{EGeD`Yy{Gt;<_RtSq;1PskE85E};-Wr+NTPTLarVhKg5jYlhp_f%U zwhQ6x=#}1zBWQH`27_#aSG$lU24Na_+eN-YZ|#UyN=^E$UPn;AQs4DVI; zp@cMND>}3k6Gcni3-&%IO1=zP(JY-SscK`YgFY(h(2vbP6zFd+u|AOCbzVi#TRtdB zP|m1i{~$EQ;@ufEQHrFEYVO85`#4nk4ZRa*b_a1P4xpRDnbEr30d!BQuLkb;xqlJD zj-SUmehvw`C(Vt?nSW>H*X`7#1fk(h3;eplz^>WxhT_0DwD-rq!55`ud+ED6{%wl- z;0L1@gYd*kAO2vFoNN@p*1!Y*=4We3_B6^?rN61b!wjW=rIf7_w!fd{rg&GH#|{in zzr%#$i=pWlThGth8$J<6kXL!wfAawc#1AwTXH({ZM$9KY*r9oP*6(#gTSWwV4b`@< z`EZXtv703bg^<%WXEH)nGx|bwk-e>PHPnYeJTy#P0gb&a8ubwu*$zmgsa?dzG}}UG zvEF?}eHz$SHynw)du#Droi7PWMytqs2b}idT?l}FD8jL`4ScgeN4F6OxZ^)aAPKP@ zbGFI@?@|5RqEYeM=nZKUr2ne(3TWp2H>PA`;nZx1(PIf;+>#s;51BYdV=?~6SIc%n ztT5Yz`nzwz;K;fVlBMkGy3X;qPt@cwc;Rn99mw=^G~eFgV4TQ*Ks`QpW`RN9HY#B6 z`OsrCs5Ha$PM#x1MUeexm8f#UzAJ%-4oK^b_c5+N88;q8YOy0OuSBSt9x;(at0}mB(q}sas71$o0UP5Mg#ptxoOz2h}4B)K8Is&?# zqehr_a=MgESIw^BXyZ{N}HiMOZTVp*BrU94A*955%tjUVjCXuIuE>6 zVd7$1QAbepeKW>~jCQEU+J>knvI#FC#j+FTILE6kBUvO`L3&3ry~1s%S|d^3hO3Wq zFcRKf7!Yflcjxi3*Bgiv*TDlQ3)9v?URh6jxXd9ve3T^4xXjB`9c;_I?!^|imatvU z;EK>>sP6~cDAfvJ#E9v(U%JbTm?@?9wvc)R(5yQdOyc{%)ok~4SS1jI)J^~zL;*#T z*Xy-L62sj_-w9rVRvGF36AK7z{>nmwciyB=HBH|oyeGt#owB(Sq|-|a3sBU)=@*|^ zSfJPRXXC;T7UmbAd4#ot&P(I7$w?Fw`^zCvGPH5dmryTMiqjbZi$Y-#ZxT-Xeq?Hd;*?d^H>L{(P}n+eqJ zv(1*Y@@Bq6HWxlKC)GzRqMfr2))+pc(=#(C4W`qvOh@_IW{D1>xd#W3S7u58a{;XO z4#0^WM$PpZr0#@(5wPY7m`9!>=*cGSMZXMr^U7>`dn6{BPyn z4VC358Z4vCI7ZgXC3l*^BTE!zrx}8VwGsOqzfGtpLoquTS4yWE^eHTT$tG@|56m#f zZk0Qhv27&SuO)N%+I$E9iGqUXYxZw4$e>Uso4Cbsrh|5pJ$zY zj!(7*%{Nu?3rVTw+>t$epLH)^t1Z{OXXa3QJ|E{(yZO!;zAAgqc=O@CeD<|H%NKCl z`f{%S%x=#k-?(+pQ`7FQkLk99ET{%SaDe*=_s??Axc?B>KJGupy`TH?dPRAWU$iUg z>pQ?*Ji6__1-#)>KSlki>B;udIR-H?>py`T1YeD^phn#r!Yrt8-@;wEe!}1HhcJAI z_@2%edUx=p;rrX|=D~wIMTaNadr59@@12>Qyq8bYTIP;@6SM7|`d+W4=EjfgIXJb~ zT24*dM|khI5B$>7eMPtBKGpmEGkmXi2Os@yHzyBGP3vR1bBFoXEg#YSE$~Ue>+9Tw zuOol^=;5i^iTSCS>0Jlglh0G{lE?ecxsOuMST>pOe)O6d0F4sYJFJD-`( zC-U8N+1{PMIC1buJ7`{5LN=Z)iN@sAi&K;Be9y7`v+da#!0Gqy@*cq{mrqPi_DYre z#$Y<(KeXDp`R@i7-w7`6_G#eav%vl{!~Rhp2zDO_{tM5-zveCyMX^};&o>X-=aJzb zG~D~*R>R$0+()GYW4Lk#N*_|z^J-oARZWKHl#KtFOOao5;-dn~PgR3opC1MVz4@JQ zDhGr{y+8M5>mUeyb~x`6MPEhj(x`7w6tNb~hu5w9!s zdnF!R38&s~yRY;O`7TbEmB}(*%k=3=^7{Vqg2=rex3^vjT3rsWE6La@ld)x<7rGZR zcqQDHf9ziTD(qhE(8TnFJQ%-1&z071g>T|pe$h6@e<`DQwn8rN{%*c2QN03R|D8C&*%54Sm0GL-E}Qhe>onP`B1+#c)u`~f7D-%WwZ2KNjFxK zf#rDh{#^hEYk8f^{;>e6f6)F*e{R4!FW@Ei=LEX)QY)e*YOO@y&R;5^ywHL7f}swp z&eJH_k(<$X$6i)mz_kZ1H6Ih`x!_?mHEG2w;n@AV2{;_91+L4+8O^&`tdO~t@bgWB zWq*6YvI`uS(@TXPq%Zep9lHEp4(m#Imi`ok;aH6Q{or^dxZS@hVPK61#rSQ!_LnBQ zWtx~2zv8b?6wl(H$_qX!zh7bfeJ{4BecYx=gnm^wS+aD&rz^!RSF*X^jow;^T>8I{ zKb2C#tDxt1i`VkLd&Yhre}d(G_Hm{0UCss-V;Q`+eRyWB+tvS%u?#)Q?00f@;-%7f z3OQMcW><=ZKgd|CZZ2sjj zk10K%i~rU9thad9eV{u9E@uZdKOrt>rlr|&Ie(~Us$4Ds{!UG&ZNA)7ke24r<${6k zq`I6P^xf~L*rnX6n{Hc9dHjQZ{MI$4(%nM7`#Y0!2Z;RI1(I1Oze~KLP0i2a)k1lN zeb;lvCihCMP-b)f-ZJ+@J6j6qmKFIc;Zcljx!qlELkoNbYq&F>Cu|Wwc?F!6?CO>L zULl8qQMqq+cE+N)Ao#E{_zjX*uxFCz0*{pZ?{7cPLI!_Dh+olvKlzVLO|#sh+^#>g z{E4HT{G#7XaAxLvB(<=|t0{YMnm}=Ck~JIFbRKTogt(Yzu4*2fZBI-ddyv03HRb+L zlJ%*@7X1d)ey+u$moO)mvMV~}`s@^FAxl4v^^u9ACR%em7y4!SQ`1knzroavCw%=* z{-8k#f+4|~nl4EedA@7|Wlppooj!Q1Fkt!%O7PX!@%?r1&;p!(8c$hu3%_UmO&oo2 zdb`$@TwN_B>F3Jk_ciqgpN_h`Rqkf+9-cZhHUFTIwG+dOK9>KeTbo$p>kn&O6GzGI zcC^-)iI1&`NozK4f0`-?*7-8w&{IJE`Vvet7ab}F!3JL@S{V_Vws862TuI=r@^#iz z2l;6pncp`vt3Q?1I@Zy?jU_k_PR|{A?zySGQ+WN3`cS^f*BJrMz3qt(7-e7W%hpj= zZeM2XHNK8$)0~`qv{B-$=RFt@}DF zk`KEDNmaSQ*Hb?Q*&m&Krn98@Az!Ys8IL?LWy^LHc0$iXR^gHM{KO>AOF3P;Q1fep z$}4Pw`%7XjZc~0arptnBM~g+m_VkM^cu#cJ37wqIbQTeN3M;d7@J8SOc+{;U81 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c6f00d0e3465362f801dca34a43f38aa1f3f6cf6 GIT binary patch literal 10448 zcmbVSYiu0Xb-wq`?1P+LYGknyP07*Sv0PKJCGUHeN>X!H4_mRVhi$2C`h!|>$I>pz zU2=Cw*@Z&VmHbG976#%U{Slx6ib8+tHU$C{Xwy^y(*9{$I6we5Kic$>AW6}p4*En9 z!2Qm-Gs~sK22M!K-aGf6d+xdCeCM2dXGSy!D^ds{*Cwy3tu48ARc^}7tJW6R)h68t zYS`qe+S(ML5KN06an(i#5fSMY{i1(MfPtw|&lc{P48u054(6CieNG;fZ8U|xK%33WGd-ovQ}EA_qk4bB5BKvy?@M3rLtp*lszGB%eHO# zKvEn~P}!JB+X=ZU)9HjI#s1`j)|T|Qx9nxq=hmL6WvFGLxwte4d#j>;U~N5E59S9q znyWjlvpkG@THGa0PA)dPi^1|egdflD9{35n)d$y4LUU)w=jU6^usI)eTb6X@=dTAm z#UQZMSXK&OI;pwSrye@<&{N~zJ6Sv;v}k+~{fS?)=Wv^Ty^*}V&(Xpa&KV`Q0C7eSvi)`Em z+4h(IJ7Fqcd^72OWZJ1nxvAzbQn`oDWI>iwe4{+s_(0xj{FS(r6#~*|X*eq4=zt?; znQX{QQ61y0X-5gCE!Eb;!grTEb(G{57th#p{@=ZNSNJLS2&D7BMfGv_h?5hB&^ii1 z4S!1u@YhOPzH)OkqiS%A<+~k_sbkc{b?5h`0#I0bf9#no1j=D}&bH>XWzVTCUuyff z5K!57#M!Kv#<=_vDbXLlz#9CTFt^j@_8+3#2cO9%K>P6}QmOJQOQcfepICCCCq61vXiqx8pCwWx3 zW}U}8^=Zt?!hFV491ix~YHYvrkM<@eR1N)Rbc5>aH$6xrNC=&{@(%ajq^STD_hUK< zAxUGFPEw)V$It=8xsQ`zMkHD>2nhuH9N~OY!G$8BfJ(LL9`MBOSc91KCX}2!o0ZcP z3J_gl6g?^91<}~K29)l8)I%6?4`EG`;*P7_DMr_wGg*lV8DjDH2Zb2|dmjX;HEnL) z2N72Ml>bBE#C_ViLndU`0f=mv@Q=uZwo4`?tXggrC3NkguwfIx7RF5Y?pXwaIbY-w z=9VI-4QuE`2PM#!Aa#*K63|>^aMmuN6cNbHa1>O}BfQuQvwcn?X3=0(?u-=9LDAqS z`^EpYOy#Z5*{(fJi%I+MnZ@iAT=UcJBT&|12luf%@cqBhVlDDaES9yZh5tQ_r3HHy zt9DaUiO4}w!=IvMNx)94j**ExGp7uEc>&)L&u0M<7K(kB$anj0Vwx;7g&9oTrvYft zo;}(C#?(4vY?{7fh+H^OETa1$f8Cca@wfE@p;q;1L>8szDY>$%XUVih*jK8FQ-p9F;Uq0~4P z!iiu>=-S1yXyJ&{7wyf4NH%Mj#rVlIgwvl-UPQv-NjewUN4R|96{YwNcz!I{h+_7ouK$ej@g)CzOMwDf! zRB^^q7qZ9*NHjq1I8tXW-N2Sm%~=?Sbf12n5(27NjM<^N4zNs7g=a#lP#jZT6H^iG z_K2x)K|dI3i+qfpl~eTUPQs%EYGKOT5g|Md zXtxGe=VOz96k=8t5m_e%4B_jlI!r^VY6i$bq0QZ-DKgj$8y8TR%^RoCp!`e+(s=?q zG>zu82YGxe#JL8g#3>1XI5**-gIzx;bHbWKkIkbJWW*UvLo3uc7tN@nM%t_m?NFnI z31B&U%({j; zU12NfWo8u422VkWsF5~uNyo>*f~HMYm5aizstg#gCUzL^;$}zOl0$B&F*lpIQ6M2U zQz*u&Y7!ckMqOkvZ%=(Ioh5x|V|_c&KT=eR^zCT2seJ)rvl&UFI?c@3qWG~#v`D*n zU&2a&Ck#tRFXum!$}w9E;4*~``lC4oMbU82dIwZngWp4hAOxO2Kx@tauIPF=sD=Iy5ED922=Q@2=uk`8 zy?<8XS2=0Nr zcmk9-glK~p+Hr9Di4+Gv5dK4GN~a$nJGqn-vD-ozk9bd5 zLJErHIR!#NgwX5;J$8W*R0Fa>2zjs&4hK**BzF=1XaPBSkDQWbx<^j(5@G=wYou>lBx;c}^XvECRt zAIIOcC;GJQ(TMM)wiJc&F%#Tql>k#tT0|gobj*eJZK6aJIGe@ZOpC{bK*$v5$-wjk z4G1JvvF45o=|8zf3i6LohxF&41D)k3L!In1rHUyPC=o4_VV~(&&w&u?@iS;3mKGCQ z{sSRU|Grq$;<&JeWBQ)O&Ttm_gOZQqNhD3NM0PSg2))1uogmry+&ec+AMlPXH7ZZTW31#4(Sd5gI9l?T7x#Z z?D(&4+snSYfs6;_rlHT_S~z_p&Kv1*-i@V)G18=9I-GHYwj&yQhlX}iX&(xYh_H>@ zW2Fdq6hw?3RR(*a6lq#Q#VOy0M?#wl7d_IZDq$EsK&5S-H?dn8oKX-Mf)1H@^kxKS z6j*JN&O$2oEQXt;gC?ahHXA5nKA4f%f*R<<6EILiV3HXrhE#bNVPP*#5;&E1D5fBM zBjrPS5rxCC<3fWQ_G=Q1R8G*!2t$g)Jrx}Ic^000EDHRR+|-33QB3kPVn~N)3PyZ{ zyV0-@Z5m)A$Wjl*c^fYx5-iG=MFPU<=s{|{NNlY}c~J8b#5%`}Xif^)h}N8j3`fWL zc7k7h;2dBc=@45yMDP-{cS&z}7M%!SIUIno%c&}CF*tN#V{?Flv+ZxIbF|-s|9F=F z-Pf_7f)85$SGY#ub)595{7>M~6*I#@1J4$jEq00EnZX1$*RX%|G2>wr+ppb47v34r zs4|H~GYJ1*&~CQXlic_=&BKG~#R5G&qH_w>T-54ZOQVKIcBBIQ5Kla;X=JdQX0gl~ zcf^WC_`u`Qn@E(g~U)aOiv0A@!@9KVb%kHHDQUE^<#ToZ7hqy`qZ{D~o z{Cx-mqu9^K5BA*XeUg@VRua8Q67icPdb~?o^G~U`bImvu6r9n|E1;L4Rnw857rmLeseM0O>}N>4sW3wP4i~C@nUZ&=)Qp0 z%8h5wpMG(v+YN3EUoel>=R+b~?=JTim*&IvN`Sdm@y0iU@+Qg$Q2r*p0YzM8M)>-pJ2zECI>3#CH2 zP$^UkwL-lxTg(>=#bU8kEEg-qYOz+V7iUZPQlV5Vl}hDOrBp4|O7+rgIbSZ6i{(qe6>(5R!h}#wNkBCYt?#nwwA9IYQ1V-u7Q<%Iw9s6pH}EU%Zs_-6 zoYTFPX1jZ?+X`N8^wzsAXnzA=0JP3^SJ%Vm+Jiy6d+kbdc|ADuGBmPtvls-ep!IBf zr5#2i{Kc@{1E(5d8~x_$%(1Jw*VWCgzKVrk)iCdR!1I7L>x$;S*NoofG#5xK)S;M%#_ka!=gv>~`UyoJ2JG zeG1*jbB!-7cUyb#N-soF1hg^#iMVYKdg4icqo2N*7NaM!ef)Rd7cuA{+|i#ioTE*l zgXg9`vTgjg`}-NwN;I*Y)UxMCHE1WC)8EgkV>4Qa`##T!Iv|Jj13>a!+{bo}$L#$9 zPu#mx-t$GzPRB^ieC4yFH+vZ&26O*2u73p`t&{W->zy$A+YCp&*lPs?{Zg;5!|Q=Y zI78a$qt(Qe6Z%wdWd)!QC<+a9vmZnw!t2dYuQbEO>w10FBpzb0u-;w{kGH$yF^7Q} zs{6reuTNp`ggz5ZLhx_D)Lx9455i_Y)L>gE;WG#<)4? zFZY9H>lI3X<}<8TJbZ}t{g=B<1i)S&A#|tL_%CsrI@cYnzXb8x2%B*hrFaXHXCoO> zjb@878*c6AqP8sg@A+PMiN51v)C&82)VH7dBH1_%47kI-5cThi#$M*^%cJd!QQsu> zooaTwy|59W#K&JmU|&LijAJ65i1c+D-+Db0yuzcYKi19oPEzK+*jM8I)K04AF$cs) z_s7Nzx*ID&*yNZ+gD06oZy3;abtd`)1U{w^$-;?R?L^Vs;5XeL3|dR3`-x)yMCC;O p;n|?LSjrcR3rOyjg<`SXtTpR}a!@EOG%JN#wGadgh4R9J_&=XdQRV;u literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cc662d3dc8e54ccebbd00aedd32cf753679895ca GIT binary patch literal 2898 zcmZ`*&2QYs6@N42F1gEHX>6ylEZ5GE77*(YWhGgb-Cjhda-bSP9RUG~9=hW0($;>< zB%|6zp%q}k2oRv?Ej<-QFF}E-e}j8#ffnd7x4JzP=%oR2?8W^(en?J(&Vqbz-q*ZO z&Qh3+1rZ6~*%L?NXipyT+dRSoG;$zkGi(TtIHEnGJvjqp(b-;=MOj#WDlUtW*d&Sf z9TH+J?veq#jdv(oIAkPNK@Y_m@QxT}??;gDK%OaUz`KwkaR{vn-nSwD1U4(sU5A}s zX6;F4XDmX4*W|Bb>tW8$`zZT$N1IBHOH0>o!>xL(qll6=Np+QbN%j7upZ3F1|ECdsyV{EmM$@O{TK%MbFrAL*2Ao)`x7)oi4cpPA zX9%a=-jBkAb|*|CL)O1^=B|+@wGfElP8a9C3G)N0ysztojV2PaH3kmm+_clNPn~TSNbqcElr8`1&X7&ZjMm7#MQyb{lJ zC8$0>aVy&J#SkZ&z8JVBWE^np=fT{`yrbzf(9Z+&M2iAZZ#Zuv2j3uHaxPPzi(mi8 z$dO-sYAUhloC;*c;H>B@E1U{-jiX&Q=m7!{{=xvLi@=c(#*nUM!*&gIH7lxLRo4Tl z-r8=8;2F~1QraQMf6m#p)pZy&HVwsg&r_Vc@X0Y!Uig|(T1vu_v3PD*WP2Q~QHwx2 zUkNE^lHF|$J-I|&1%KiFEYS-CZ482}f`)GoT!XVExA><8=QQ{;eaI1=pDq#H*B!Ws z0xla1Uas&L+HImKmJRKhnFS_Dl*t}2^``O|*q?8+1Iva&y(mDOr~xc%Gso0Giwx5h zq@ZDP6ze=Va~HkEc`|q7Oq?Tt;>RXPF`&` zhZuQRQP*@8S$nXJL(D*b^11)&cmFUyzNS*=IehRe1IN~G)6|W?%%UxN#cSj(WGNf%qNS>p zDwxGHUR^n^R1M^>iSjkIfyX=-#4b+_PBKrhYvOTK(h=U{Xm-;|#pV~XmSB2xGy z$?W4MFd;xM!=qm^8}sL(ogM_zC11D}N4JeGMaG86jBu$zn8<86U;IWW&0m})3 zeRky*+M0WdCy3Nl1hnAEK&ZFOBOfCMtb(o)k3Hej`@Tpq^WxrB?BqR+AR%04oK^yI z=HZV-7yMXIeyx18lkDK~cz}E2gD4HpFOvKgd2c#NPpXI45N_ebeF{YkyFkmRa5 zjt-`A+MhgH(H!R4NeLY5KkRqQ6OuHHQw`gz>#f!boGq4Ot$Vq9nfss{1+Td?NuziY zj!FjTqTpTjM{yMPo^pQk%ei+ZRq9{bnH=KY?(q>U=f{r{s8`-7Y%qBJcISO*e~$Ck z=8Cn)C>^t`zv9&>jd4Ml`r0c095AU`&?{kY+@Iu|O9joGgyRUfD)&h^N)h(IvDXEy zIz?xUSc<6ByIA%~I}L|XI}X#F&$Z{(9EVTvrzAl}rW1}7i+LPVX@hBJ|6K|m(;8X4 zVXqfQ39GU^>+vJ;lDvn8avl2&u3V!|IK4rGrNotf~C(^ zF&C^`(rTH%*^n@~$ldh|Wq(GKL#|sk8!V95e0nxGUt?C{d^424@=+1y;gpVDI4 zagy|gIs4tU*2>08>&D$^t-Ib@TkG6f-`MD^t=$SY!#k_DqSf_IxUsr Date: Tue, 3 Dec 2024 17:13:24 -0300 Subject: [PATCH 6/8] fixed typescript scripts for blend strategy --- apps/contracts/src/strategies/deploy_blend.ts | 4 +- apps/contracts/src/tests/blend/test_vault.ts | 2 +- apps/contracts/src/tests/vault.ts | 2 +- .../strategies/blend/src/constants.rs | 2 +- apps/contracts/strategies/blend/src/lib.rs | 5 +- apps/contracts/strategies/blend/src/test.rs | 77 +++++++++---------- .../strategies/blend/src/test/success.rs | 74 +++++++++--------- 7 files changed, 82 insertions(+), 84 deletions(-) diff --git a/apps/contracts/src/strategies/deploy_blend.ts b/apps/contracts/src/strategies/deploy_blend.ts index ba2473ff..73ab8b37 100644 --- a/apps/contracts/src/strategies/deploy_blend.ts +++ b/apps/contracts/src/strategies/deploy_blend.ts @@ -47,7 +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 + 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_vault.ts b/apps/contracts/src/tests/blend/test_vault.ts index 4be22505..22038029 100644 --- a/apps/contracts/src/tests/blend/test_vault.ts +++ b/apps/contracts/src/tests/blend/test_vault.ts @@ -139,7 +139,7 @@ export async function testBlendVault(user?: Keypair) { console.log(purple, '----------------------- Depositing XLM to the vault -----------------------') console.log(purple, '---------------------------------------------------------------------------') const { user, balanceBefore: depositBalanceBefore, result: depositResult, balanceAfter: depositBalanceAfter } - = await depositToVault(blendVaultAddress, [initialAmount], newUser); + = await depositToVault(blendVaultAddress, [initialAmount], newUser, false); console.log(green, '------------ XLM deposited to the vault ------------') console.log(green, 'Deposit balance before: ', depositBalanceBefore) diff --git a/apps/contracts/src/tests/vault.ts b/apps/contracts/src/tests/vault.ts index 3b99bada..9a536f92 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(investDeposit) ]; diff --git a/apps/contracts/strategies/blend/src/constants.rs b/apps/contracts/strategies/blend/src/constants.rs index b2417fec..1a61405b 100644 --- a/apps/contracts/strategies/blend/src/constants.rs +++ b/apps/contracts/strategies/blend/src/constants.rs @@ -1,5 +1,5 @@ /// 1 with 7 decimal places -pub const SCALAR_7: i128 = 1_0000000; +// 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 diff --git a/apps/contracts/strategies/blend/src/lib.rs b/apps/contracts/strategies/blend/src/lib.rs index 3edb42fe..38c44058 100644 --- a/apps/contracts/strategies/blend/src/lib.rs +++ b/apps/contracts/strategies/blend/src/lib.rs @@ -2,7 +2,7 @@ use blend_pool::perform_reinvest; use constants::{MIN_DUST, SCALAR_9}; use soroban_sdk::{ - contract, contractimpl, token::TokenClient, vec, Address, Env, IntoVal, String, Val, Vec}; + contract, contractimpl, token::TokenClient, Address, Env, IntoVal, String, Val, Vec}; mod blend_pool; mod constants; @@ -10,7 +10,6 @@ mod reserves; mod soroswap; mod storage; -use soroswap::internal_swap_exact_tokens_for_tokens; use storage::{extend_instance_ttl, is_initialized, set_initialized, Config}; pub use defindex_strategy_core::{ @@ -96,6 +95,7 @@ impl DeFindexStrategyTrait for BlendStrategy { } 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); @@ -184,5 +184,4 @@ impl DeFindexStrategyTrait for BlendStrategy { } } -#[cfg(any(test, feature = "testutils"))] mod test; \ 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 041fedaf..9d268443 100644 --- a/apps/contracts/strategies/blend/src/test.rs +++ b/apps/contracts/strategies/blend/src/test.rs @@ -2,10 +2,9 @@ extern crate std; use crate::{ - blend_pool::{self, BlendPoolClient, Request, ReserveConfig, ReserveEmissionMetadata}, constants::SCALAR_7, storage::DAY_IN_LEDGERS, BlendStrategy, BlendStrategyClient + blend_pool::{self, BlendPoolClient, Request, ReserveConfig, ReserveEmissionMetadata}, storage::DAY_IN_LEDGERS, BlendStrategy, BlendStrategyClient }; use sep_41_token::testutils::MockTokenClient; -use soroban_fixed_point_math::FixedPoint; use soroban_sdk::{ testutils::{Address as _, BytesN as _, Ledger as _, LedgerInfo}, token::StellarAssetClient, vec, Address, BytesN, Env, IntoVal, String, Symbol, Val, Vec }; @@ -33,7 +32,7 @@ pub(crate) fn register_blend_strategy(e: &Env) -> 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 _backstop_token: blend_comet::Client<'a>, pub pool_factory: blend_factory_pool::Client<'a>, } @@ -177,7 +176,7 @@ pub trait EnvTestUtils { fn jump(&self, ledgers: u32); /// Jump the env by the given amount of seconds. Incremends the sequence by 1. - fn jump_time(&self, seconds: u64); + // fn jump_time(&self, seconds: u64); /// Set the ledger to the default LedgerInfo /// @@ -200,18 +199,18 @@ impl EnvTestUtils for Env { }); } - 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 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 { @@ -227,32 +226,32 @@ impl EnvTestUtils for Env { } } -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 - ); -} +// 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 - ); -} +// 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}; @@ -344,7 +343,7 @@ impl<'a> BlendFixture<'a> { BlendFixture { backstop: backstop_client, emitter: emitter_client, - backstop_token: comet_client, + _backstop_token: comet_client, pool_factory: pool_factory_client, } } diff --git a/apps/contracts/strategies/blend/src/test/success.rs b/apps/contracts/strategies/blend/src/test/success.rs index 46511ff2..43f7e858 100644 --- a/apps/contracts/strategies/blend/src/test/success.rs +++ b/apps/contracts/strategies/blend/src/test/success.rs @@ -1,14 +1,13 @@ #![cfg(test)] use crate::blend_pool::{BlendPoolClient, Request}; -use crate::constants::{MIN_DUST, SCALAR_7}; +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_fixed_point_math::FixedPoint; use soroban_sdk::testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation}; -use soroban_sdk::{vec, Address, Env, Error, IntoVal, Symbol}; +use soroban_sdk::{vec, Address, Env, IntoVal, Symbol}; use crate::test::std; #[test] @@ -22,12 +21,11 @@ fn success() { let user_2 = Address::generate(&e); let user_3 = Address::generate(&e); let user_4 = Address::generate(&e); - let user_5 = 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 _blnd_client = MockTokenClient::new(&e, &blnd.address()); let usdc_client = MockTokenClient::new(&e, &usdc.address()); let xlm_client = MockTokenClient::new(&e, &xlm.address()); @@ -43,27 +41,27 @@ fn success() { /* * Deposit into pool - * -> deposit 100 into blend strategy for each user_3 and user_4 - * -> deposit 200 into pool for user_5 + * -> 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); - usdc_client.mint(&user_4, &starting_balance); - let user_3_balance = usdc_client.balance(&user_3); + let user_3_balance = usdc_client.balance(&user_2); assert_eq!(user_3_balance, starting_balance); - strategy_client.deposit(&starting_balance, &user_3); + strategy_client.deposit(&starting_balance, &user_2); // -> verify deposit auth assert_eq!( e.auths()[0], ( - user_3.clone(), + user_2.clone(), AuthorizedInvocation { function: AuthorizedFunction::Contract(( strategy.clone(), @@ -71,7 +69,7 @@ fn success() { vec![ &e, starting_balance.into_val(&e), - user_3.to_val(), + user_2.to_val(), ] )), sub_invocations: std::vec![AuthorizedInvocation { @@ -80,7 +78,7 @@ fn success() { Symbol::new(&e, "transfer"), vec![ &e, - user_3.to_val(), + user_2.to_val(), strategy.to_val(), starting_balance.into_val(&e) ] @@ -91,13 +89,13 @@ fn success() { ) ); - strategy_client.deposit(&starting_balance, &user_4); + 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!(usdc_client.balance(&user_4), 0); + assert_eq!(strategy_client.balance(&user_2), starting_balance); assert_eq!(strategy_client.balance(&user_3), starting_balance); - assert_eq!(strategy_client.balance(&user_4), starting_balance); assert_eq!( usdc_client.balance(&pool), pool_usdc_balace_start + starting_balance * 2 @@ -105,13 +103,13 @@ fn success() { let vault_positions = pool_client.get_positions(&strategy); assert_eq!(vault_positions.supply.get(0).unwrap(), starting_balance * 2); - // user_5 deposit directly into pool + // user_4 deposit directly into pool let merry_starting_balance = 200_0000000; - usdc_client.mint(&user_5, &merry_starting_balance); + usdc_client.mint(&user_4, &merry_starting_balance); pool_client.submit( - &user_5, - &user_5, - &user_5, + &user_4, + &user_4, + &user_4, &vec![ &e, Request { @@ -145,18 +143,18 @@ fn success() { /* * Withdraw from pool - * -> withdraw all funds from pool for user_5 - * -> withdraw (excluding dust) from blend strategy for user_3 and user_4 + * -> 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_5 + // withdraw all funds from pool for user_4 pool_client.submit( - &user_5, - &user_5, - &user_5, + &user_4, + &user_4, + &user_4, &vec![ &e, Request { @@ -166,25 +164,25 @@ fn success() { }, ], ); - let user_5_final_balance = usdc_client.balance(&user_5); + let user_5_final_balance = usdc_client.balance(&user_4); let user_5_profit = user_5_final_balance - merry_starting_balance; - // withdraw from blend strategy for user_3 and user_4 - // they are expected to receive half of the profit of user_5 + // 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_5_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_4); + 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_3); + strategy_client.withdraw(&withdraw_amount, &user_2); // -> verify withdraw auth assert_eq!( e.auths()[0], ( - user_3.clone(), + user_2.clone(), AuthorizedInvocation { function: AuthorizedFunction::Contract(( strategy.clone(), @@ -192,7 +190,7 @@ fn success() { vec![ &e, withdraw_amount.into_val(&e), - user_3.to_val(), + user_2.to_val(), ] )), sub_invocations: std::vec![] @@ -200,16 +198,16 @@ fn success() { ) ); - strategy_client.withdraw(&withdraw_amount, &user_4); + 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!(usdc_client.balance(&user_4), withdraw_amount); + assert_eq!(strategy_client.balance(&user_2), 0); assert_eq!(strategy_client.balance(&user_3), 0); - assert_eq!(strategy_client.balance(&user_4), 0); // -> verify withdraw from empty vault fails - let result = strategy_client.try_withdraw(&MIN_DUST, &user_4); + 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 From de5a99e1a71cf22f4b111b070ab4f51136d577cc Mon Sep 17 00:00:00 2001 From: coderipper Date: Wed, 4 Dec 2024 16:49:21 -0300 Subject: [PATCH 7/8] fixed comments --- .../strategies/blend/src/test/success.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/contracts/strategies/blend/src/test/success.rs b/apps/contracts/strategies/blend/src/test/success.rs index 43f7e858..9831bd7f 100644 --- a/apps/contracts/strategies/blend/src/test/success.rs +++ b/apps/contracts/strategies/blend/src/test/success.rs @@ -104,8 +104,8 @@ fn success() { assert_eq!(vault_positions.supply.get(0).unwrap(), starting_balance * 2); // user_4 deposit directly into pool - let merry_starting_balance = 200_0000000; - usdc_client.mint(&user_4, &merry_starting_balance); + let user_4_starting_balance = 200_0000000; + usdc_client.mint(&user_4, &user_4_starting_balance); pool_client.submit( &user_4, &user_4, @@ -115,13 +115,13 @@ fn success() { Request { request_type: 0, address: usdc.address().clone(), - amount: merry_starting_balance, + amount: user_4_starting_balance, }, ], ); // admin borrow back to 50% util rate - let borrow_amount = (merry_starting_balance + starting_balance * 2) / 2; + let borrow_amount = (user_4_starting_balance + starting_balance * 2) / 2; pool_client.submit( &admin, &admin, @@ -160,16 +160,16 @@ fn success() { Request { request_type: 1, address: usdc.address().clone(), - amount: merry_starting_balance * 2, + amount: user_4_starting_balance * 2, }, ], ); - let user_5_final_balance = usdc_client.balance(&user_4); - let user_5_profit = user_5_final_balance - merry_starting_balance; + 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_5_profit / 2; + let expected_user_4_profit = user_4_profit / 2; let withdraw_amount = starting_balance + expected_user_4_profit; // withdraw_amount = 100_0958904 From fa7b8d63fc6b4950d509b213e78082a2cdd8b2f0 Mon Sep 17 00:00:00 2001 From: coderipper Date: Wed, 4 Dec 2024 17:12:49 -0300 Subject: [PATCH 8/8] using test/blend structure --- apps/contracts/strategies/blend/src/test.rs | 2 +- apps/contracts/strategies/blend/src/test/blend/mod.rs | 1 + apps/contracts/strategies/blend/src/test/{ => blend}/success.rs | 0 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 apps/contracts/strategies/blend/src/test/blend/mod.rs rename apps/contracts/strategies/blend/src/test/{ => blend}/success.rs (100%) diff --git a/apps/contracts/strategies/blend/src/test.rs b/apps/contracts/strategies/blend/src/test.rs index a05dcf4f..927c5c91 100644 --- a/apps/contracts/strategies/blend/src/test.rs +++ b/apps/contracts/strategies/blend/src/test.rs @@ -349,4 +349,4 @@ impl<'a> BlendFixture<'a> { } } -mod success; +mod blend; diff --git a/apps/contracts/strategies/blend/src/test/blend/mod.rs b/apps/contracts/strategies/blend/src/test/blend/mod.rs new file mode 100644 index 00000000..916f9128 --- /dev/null +++ b/apps/contracts/strategies/blend/src/test/blend/mod.rs @@ -0,0 +1 @@ +mod success; \ No newline at end of file diff --git a/apps/contracts/strategies/blend/src/test/success.rs b/apps/contracts/strategies/blend/src/test/blend/success.rs similarity index 100% rename from apps/contracts/strategies/blend/src/test/success.rs rename to apps/contracts/strategies/blend/src/test/blend/success.rs