diff --git a/contracts/etf/Cargo.toml b/contracts/etf/Cargo.toml index a70f23a..1e5a156 100644 --- a/contracts/etf/Cargo.toml +++ b/contracts/etf/Cargo.toml @@ -45,5 +45,6 @@ cw-staking = { git = "https://github.com/Abstract-OS/apis.git", features = ["jun abstract-boot = { workspace = true, features = ["daemon"] } abstract-testing = { workspace = true } boot-cw-plus = { workspace = true } +semver = {workspace = true} anyhow = {workspace = true} speculoos = {workspace = true} diff --git a/contracts/etf/examples/schema.rs b/contracts/etf/examples/schema.rs index 4adf043..fe07915 100644 --- a/contracts/etf/examples/schema.rs +++ b/contracts/etf/examples/schema.rs @@ -1,6 +1,6 @@ +use abstract_etf::contract::EtfApp; +use abstract_etf::msg::StateResponse; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; -use etf::contract::EtfApp; -use etf::msg::StateResponse; use std::env::current_dir; use std::fs::create_dir_all; diff --git a/contracts/etf/src/handlers/execute.rs b/contracts/etf/src/handlers/execute.rs index a1b778d..311295f 100644 --- a/contracts/etf/src/handlers/execute.rs +++ b/contracts/etf/src/handlers/execute.rs @@ -26,7 +26,7 @@ pub fn execute_handler( msg: EtfExecuteMsg, ) -> EtfResult { match msg { - EtfExecuteMsg::ProvideLiquidity { asset } => { + EtfExecuteMsg::Deposit { asset } => { // Check asset let asset = asset.check(deps.api, None)?; try_provide_liquidity(deps, info, vault, asset, None) @@ -92,7 +92,7 @@ pub fn try_provide_liquidity( let account_value = vault.query_total_value()?; let total_value = account_value.total_value.amount; // Get total supply of LP tokens and calculate share - let total_share = query_supply(&deps.querier, state.liquidity_token_addr.clone())?; + let total_share = query_supply(&deps.querier, state.share_token_address.clone())?; let share = if total_share == Uint128::zero() || total_value.is_zero() { // Initial share = deposit amount @@ -108,7 +108,7 @@ pub fn try_provide_liquidity( // mint LP token to liq_manager let mint_lp = CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: state.liquidity_token_addr.to_string(), + contract_addr: state.share_token_address.to_string(), msg: to_binary(&Cw20ExecuteMsg::Mint { recipient: liq_manager.to_string(), amount: share, @@ -147,7 +147,7 @@ pub fn try_withdraw_liquidity( let mut attrs = vec![("liquidity_tokens", amount.to_string())]; // Calculate share of pool and requested pool value - let total_share: Uint128 = query_supply(&deps.querier, state.liquidity_token_addr.clone())?; + let total_share: Uint128 = query_supply(&deps.querier, state.share_token_address.clone())?; // Get manager fee in LP tokens let manager_fee = fee.compute(amount); @@ -159,7 +159,7 @@ pub fn try_withdraw_liquidity( if !manager_fee.is_zero() { // LP token fee let lp_token_manager_fee = Asset { - info: AssetInfo::Cw20(state.liquidity_token_addr.clone()), + info: AssetInfo::Cw20(state.share_token_address.clone()), amount: manager_fee, }; // Construct manager fee msg @@ -186,7 +186,7 @@ pub fn try_withdraw_liquidity( // LP burn msg let burn_msg: CosmosMsg = wasm_execute( - state.liquidity_token_addr, + state.share_token_address, // Burn exludes fee &Cw20ExecuteMsg::Burn { amount: (amount - manager_fee), diff --git a/contracts/etf/src/handlers/instantiate.rs b/contracts/etf/src/handlers/instantiate.rs index c6c9774..ae1c5d6 100644 --- a/contracts/etf/src/handlers/instantiate.rs +++ b/contracts/etf/src/handlers/instantiate.rs @@ -18,7 +18,7 @@ pub fn instantiate_handler( msg: EtfInstantiateMsg, ) -> EtfResult { let state: State = State { - liquidity_token_addr: Addr::unchecked(""), + share_token_address: Addr::unchecked(""), manager_addr: deps.api.addr_validate(msg.manager_addr.as_str())?, }; diff --git a/contracts/etf/src/handlers/query.rs b/contracts/etf/src/handlers/query.rs index e6daa0a..659c4ce 100644 --- a/contracts/etf/src/handlers/query.rs +++ b/contracts/etf/src/handlers/query.rs @@ -8,7 +8,7 @@ pub fn query_handler(deps: Deps, _env: Env, _etf: &EtfApp, msg: EtfQueryMsg) -> EtfQueryMsg::State {} => { let fee = FEE.load(deps.storage)?; to_binary(&StateResponse { - liquidity_token: STATE.load(deps.storage)?.liquidity_token_addr.to_string(), + share_token_address: STATE.load(deps.storage)?.share_token_address.to_string(), fee: fee.share(), }) } diff --git a/contracts/etf/src/handlers/receive.rs b/contracts/etf/src/handlers/receive.rs index c01cf7c..541c5e1 100644 --- a/contracts/etf/src/handlers/receive.rs +++ b/contracts/etf/src/handlers/receive.rs @@ -1,7 +1,7 @@ use crate::contract::{EtfApp, EtfResult}; use crate::error::EtfError; use crate::handlers::execute; -use crate::msg::DepositHookMsg; +use crate::msg::Cw20HookMsg; use crate::state::{State, STATE}; use cosmwasm_std::from_binary; use cosmwasm_std::DepsMut; @@ -21,9 +21,9 @@ pub fn receive_cw20( cw20_msg: Cw20ReceiveMsg, ) -> EtfResult { match from_binary(&cw20_msg.msg)? { - DepositHookMsg::WithdrawLiquidity {} => { + Cw20HookMsg::Claim {} => { let state: State = STATE.load(deps.storage)?; - if msg_info.sender != state.liquidity_token_addr { + if msg_info.sender != state.share_token_address { return Err(EtfError::NotLPToken { token: msg_info.sender.to_string(), }); @@ -31,7 +31,7 @@ pub fn receive_cw20( let sender = deps.as_ref().api.addr_validate(&cw20_msg.sender)?; execute::try_withdraw_liquidity(deps, env, dapp, sender, cw20_msg.amount) } - DepositHookMsg::ProvideLiquidity {} => { + Cw20HookMsg::Deposit {} => { // Construct deposit asset let asset = Asset { info: AssetInfo::Cw20(msg_info.sender.clone()), diff --git a/contracts/etf/src/handlers/reply.rs b/contracts/etf/src/handlers/reply.rs index 66f26d9..87ab4fa 100644 --- a/contracts/etf/src/handlers/reply.rs +++ b/contracts/etf/src/handlers/reply.rs @@ -11,17 +11,17 @@ pub fn instantiate_reply(deps: DepsMut, _env: Env, etf: EtfApp, reply: Reply) -> Message::parse_from_bytes(data.as_slice()).map_err(|_| { StdError::parse_err("MsgInstantiateContractResponse", "failed to parse data") })?; - let liquidity_token = res.get_contract_address(); + let share_token_address = res.get_contract_address(); let api = deps.api; STATE.update(deps.storage, |mut meta| -> StdResult<_> { - meta.liquidity_token_addr = api.addr_validate(liquidity_token)?; + meta.share_token_address = api.addr_validate(share_token_address)?; Ok(meta) })?; Ok(etf.custom_tag_response( Response::default(), "instantiate_reply", - vec![("liquidity_token_addr", liquidity_token)], + vec![("share_token_address", share_token_address)], )) } diff --git a/contracts/etf/src/lib.rs b/contracts/etf/src/lib.rs index a37de89..00a6d14 100644 --- a/contracts/etf/src/lib.rs +++ b/contracts/etf/src/lib.rs @@ -15,7 +15,7 @@ pub mod boot { use boot_core::ContractWrapper; use boot_core::{boot_contract, BootEnvironment, Contract}; - #[boot_contract(EtfInstantiateMsg, EtfExecuteMsg, EtfQueryMsg, MigrateMsg)] + #[boot_contract(InstantiateMsg, ExecuteMsg, QueryMsg, MigrateMsg)] pub struct ETF; impl AppDeployer for ETF {} @@ -28,7 +28,8 @@ pub mod boot { crate::contract::execute, crate::contract::instantiate, crate::contract::query, - ), + ) + .with_reply(crate::contract::reply), )); Self(contract) } diff --git a/contracts/etf/src/msg.rs b/contracts/etf/src/msg.rs index 4ac9958..3a9d777 100644 --- a/contracts/etf/src/msg.rs +++ b/contracts/etf/src/msg.rs @@ -41,8 +41,10 @@ use cosmwasm_schema::QueryResponses; use cosmwasm_std::Decimal; use cw_asset::AssetUnchecked; +pub type InstantiateMsg = app::InstantiateMsg; pub type ExecuteMsg = app::ExecuteMsg; pub type QueryMsg = app::QueryMsg; +pub type MigrateMsg = app::MigrateMsg; impl app::AppExecuteMsg for EtfExecuteMsg {} @@ -67,8 +69,9 @@ pub struct EtfInstantiateMsg { #[cfg_attr(feature = "boot", derive(boot_core::ExecuteFns))] #[cfg_attr(feature = "boot", impl_into(ExecuteMsg))] pub enum EtfExecuteMsg { - /// Provide liquidity to the attached proxy using a native token. - ProvideLiquidity { asset: AssetUnchecked }, + /// Deposit asset into the ETF + #[cfg_attr(feature = "boot", payable)] + Deposit { asset: AssetUnchecked }, /// Set the withdraw fee SetFee { fee: Decimal }, } @@ -85,13 +88,13 @@ pub enum EtfQueryMsg { } #[cosmwasm_schema::cw_serde] -pub enum DepositHookMsg { - WithdrawLiquidity {}, - ProvideLiquidity {}, +pub enum Cw20HookMsg { + Deposit {}, + Claim {}, } #[cosmwasm_schema::cw_serde] pub struct StateResponse { - pub liquidity_token: String, + pub share_token_address: String, pub fee: Decimal, } diff --git a/contracts/etf/src/state.rs b/contracts/etf/src/state.rs index 35822e9..98638e6 100644 --- a/contracts/etf/src/state.rs +++ b/contracts/etf/src/state.rs @@ -6,7 +6,7 @@ use cw_storage_plus::Item; /// BaseState is initialized in contract #[cosmwasm_schema::cw_serde] pub struct State { - pub liquidity_token_addr: Addr, + pub share_token_address: Addr, pub manager_addr: Addr, } diff --git a/contracts/etf/tests/integration.rs b/contracts/etf/tests/integration.rs index 5b3c7ad..7de6a58 100644 --- a/contracts/etf/tests/integration.rs +++ b/contracts/etf/tests/integration.rs @@ -1,40 +1,46 @@ // #[cfg(test)] // mod test_utils; -use std::f32::consts::E; - -use abstract_boot::{Abstract, AbstractBootError, AppDeployer, ManagerQueryFns}; +use abstract_boot::{ + Abstract, AbstractBootError, AppDeployer, ManagerQueryFns, ProxyExecFns, ProxyQueryFns, OS, +}; use abstract_etf::boot::ETF; -use abstract_etf::msg::EtfQueryMsgFns; +use abstract_etf::msg::{Cw20HookMsg, EtfExecuteMsgFns, EtfQueryMsgFns}; use abstract_etf::ETF_ID; -use abstract_os::api::{BaseExecuteMsgFns, BaseQueryMsgFns}; -use abstract_os::objects::{AnsAsset, AssetEntry}; + +use abstract_os::objects::price_source::UncheckedPriceSource; +use abstract_os::objects::AssetEntry; use abstract_sdk::os as abstract_os; use abstract_boot::boot_core::*; +use abstract_testing::prelude::TEST_ADMIN; use boot_cw_plus::Cw20; -use cosmwasm_std::{ - coin, coins, to_binary, Addr, Binary, Decimal, Empty, StdResult, Timestamp, Uint128, Uint64, -}; -use cw20::{BalanceResponse, Cw20ExecuteMsg, Cw20QueryMsg}; -use cw_asset::Asset; -use cw_staking::CW_STAKING; -use dex::msg::*; -use dex::EXCHANGE; -use speculoos::assert_that; -use speculoos::prelude::OrderedAssertions; +use cosmwasm_std::{coin, Addr, Decimal, Empty}; -type AResult = anyhow::Result<()>; +use cw_asset::{AssetInfo, AssetUnchecked}; + +use semver::Version; +use speculoos::assert_that; use wyndex_bundle::*; -const COMMISSION_TRADER: &str = "commission_receiver"; +type AResult = anyhow::Result<()>; + +const ETF_MANAGER: &str = "etf_manager"; const ETF_TOKEN: &str = "etf_token"; -fn create_etf(mock: Mock) -> Result, AbstractBootError> { - let version = "1.0.0".parse().unwrap(); +pub struct EtfEnv { + pub os: OS, + pub etf: ETF, + pub share_token: Cw20, + pub wyndex: WynDex, + pub abstract_os: Abstract, +} + +fn create_etf(mock: Mock) -> Result, AbstractBootError> { + let version: Version = "1.0.0".parse().unwrap(); // Deploy abstract - let abstract_ = Abstract::deploy_on(mock.clone(), version)?; + let abstract_ = Abstract::deploy_on(mock.clone(), version.clone())?; // create first OS abstract_.os_factory.create_default_os( abstract_os::objects::gov_type::GovernanceDetails::Monarchy { @@ -44,8 +50,6 @@ fn create_etf(mock: Mock) -> Result, AbstractBootError> { // Deploy mock dex let wyndex = WynDex::deploy_on(mock.clone(), Empty {})?; - let eur_asset = AssetEntry::new(EUR); - let usd_asset = AssetEntry::new(USD); let mut etf = ETF::new(ETF_ID, mock.clone()); etf.deploy(version)?; @@ -67,7 +71,7 @@ fn create_etf(mock: Mock) -> Result, AbstractBootError> { &abstract_os::app::InstantiateMsg { app: abstract_etf::msg::EtfInstantiateMsg { fee: Decimal::percent(5), - provider_addr: COMMISSION_TRADER.into(), + manager_addr: ETF_MANAGER.into(), token_code_id: etf_toke_code_id, token_name: Some("Test ETF Shares".into()), token_symbol: Some("TETF".into()), @@ -86,443 +90,72 @@ fn create_etf(mock: Mock) -> Result, AbstractBootError> { // set the etf token address let etf_config = etf.state()?; - etf_token.set_address(&Addr::unchecked(etf_config.liquidity_token)); + etf_token.set_address(&Addr::unchecked(etf_config.share_token_address)); - Ok(etf { + Ok(EtfEnv { os, etf, - etf_token, + share_token: etf_token, abstract_os: abstract_, wyndex, - dex: exchange_api, - staking: staking_api, }) } #[test] -fn proper_initialisation() { - // initialize with non existing pair - // initialize with non existing fee token - // initialize with non existing reward token - // initialize with no pool for the fee token and reward token -} - -/// This test covers: -/// - Create a etf and check its configuration setup. -/// - Deposit balanced funds into the auto-compounder and check the minted etf token. -/// - Withdraw a part from the auto-compounder and check the pending claims. -/// - Check that the pending claims are updated after another withdraw. -/// - Batch unbond and check the pending claims are removed. -/// - Withdraw and check the removal of claims. -/// - Check the balances and staked balances. -/// - Withdraw all from the auto-compounder and check the balances again. -#[test] -fn generator_without_reward_proxies_balanced_assets() -> AResult { - let owner = Addr::unchecked(test_utils::OWNER); +fn proper_initialization() -> AResult { + let owner = Addr::unchecked(TEST_ADMIN); // create testing environment let (_state, mock) = instantiate_default_mock_env(&owner)?; // create a etf - let etf = crate::create_etf(mock.clone())?; - let WynDex { - eur_token, - usd_token, - eur_usd_lp, - .. - } = etf.wyndex; - let etf_token = etf.etf_token; - let etf_addr = etf.etf.addr_str()?; - let eur_asset = AssetEntry::new("eur"); - let usd_asset = AssetEntry::new("usd"); - let asset_infos = vec![eur_token.clone(), usd_token.clone()]; - - // check config setup - let config = etf.etf.config()?; - assert_that!(config.liquidity_token).is_equal_to(eur_usd_lp.address()?); - - // give user some funds - mock.set_balances(&[( - &owner, - &[ - coin(100_000u128, eur_token.to_string()), - coin(100_000u128, usd_token.to_string()), - ], - )])?; - - // initial deposit must be > 1000 (of both assets) - // this is set by WynDex - etf.etf.deposit( - vec![ - AnsAsset::new(eur_asset, 10000u128), - AnsAsset::new(usd_asset, 10000u128), - ], - &[coin(10000u128, EUR), coin(10000u128, USD)], + let etf_env = crate::create_etf(mock.clone())?; + let WynDex { usd_token, .. } = etf_env.wyndex; + let etf = etf_env.etf; + let etf_token = etf_env.share_token; + let etf_addr = etf.addr_str()?; + let proxy = &etf_env.os.proxy; + let manager = &etf_env.os.manager; + + // Set usd as base asset + proxy.call_as(&manager.address()?).update_assets( + vec![(AssetEntry::new("usd"), UncheckedPriceSource::None)], + vec![], )?; - - // check that the etf token is minted - let etf_token_balance = etf_token.balance(&owner)?; - assert_that!(etf_token_balance).is_equal_to(10000u128); - - // and eur balance decreased and usd balance stayed the same - let balances = mock.query_all_balances(&owner)?; - - // .sort_by(|a, b| a.denom.cmp(&b.denom)); - assert_that!(balances).is_equal_to(vec![ - coin(90_000u128, eur_token.to_string()), - coin(90_000u128, usd_token.to_string()), - ]); - - // withdraw part from the auto-compounder - etf_token.send(&Cw20HookMsg::Redeem {}, 2000, etf_addr.clone())?; - // check that the etf token decreased - let etf_token_balance = etf_token.balance(&owner)?; - let pending_claims: Uint128 = etf.etf.pending_claims(owner.to_string())?; - assert_that!(etf_token_balance).is_equal_to(8000u128); - assert_that!(pending_claims.u128()).is_equal_to(2000u128); - - // check that the pending claims are updated - etf_token.send(&Cw20HookMsg::Redeem {}, 2000, etf_addr.clone())?; - let pending_claims: Uint128 = etf.etf.pending_claims(owner.to_string())?; - assert_that!(pending_claims.u128()).is_equal_to(4000u128); - - etf.etf.batch_unbond()?; - - // checks if the pending claims are now removed - let pending_claims: Uint128 = etf.etf.pending_claims(owner.to_string())?; - assert_that!(pending_claims.u128()).is_equal_to(0u128); - - mock.next_block()?; - let claims = etf.etf.claims(owner.to_string())?; - let unbonding: Expiration = claims[0].unbonding_timestamp; - if let Expiration::AtTime(time) = unbonding { - mock.app.borrow_mut().update_block(|b| { - b.time = time.plus_seconds(10); - }); - } - mock.next_block()?; - etf.etf.withdraw()?; - - // check that the claim is removed - let claims: Vec = etf.etf.claims(owner.to_string())?; - assert_that!(claims.len()).is_equal_to(0); - - let balances = mock.query_all_balances(&owner)?; - // .sort_by(|a, b| a.denom.cmp(&b.denom)); - assert_that!(balances).is_equal_to(vec![ - coin(94_000u128, eur_token.to_string()), - coin(94_000u128, usd_token.to_string()), - ]); - - let staked = etf - .wyndex - .suite - .query_all_staked(asset_infos, &etf.os.proxy.addr_str()?)?; - - let generator_staked_balance = staked.stakes.first().unwrap(); - assert_that!(generator_staked_balance.stake.u128()).is_equal_to(6000u128); - - // withdraw all from the auto-compounder - etf_token.send(&Cw20HookMsg::Redeem {}, 6000, etf_addr)?; - etf.etf.batch_unbond()?; - mock.wait_blocks(60 * 60 * 24 * 21)?; - etf.etf.withdraw()?; - - // and eur balance decreased and usd balance stayed the same - let balances = mock.query_all_balances(&owner)?; - - // .sort_by(|a, b| a.denom.cmp(&b.denom)); - assert_that!(balances).is_equal_to(vec![ - coin(100_000u128, eur_token.to_string()), - coin(100_000u128, usd_token.to_string()), - ]); - Ok(()) -} - -/// This test covers: -/// - depositing with 2 assets -/// - depositing and withdrawing with a single sided asset -/// - querying the state of the auto-compounder -/// - querying the balance of a users position in the auto-compounder -/// - querying the total lp balance of the auto-compounder -#[test] -fn generator_without_reward_proxies_single_sided() -> AResult { - let owner = Addr::unchecked(test_utils::OWNER); - - // create testing environment - let (_state, mock) = instantiate_default_mock_env(&owner)?; - - // create a etf - let etf = crate::create_etf(mock.clone())?; - let WynDex { - eur_token, - usd_token, - eur_usd_lp, - .. - } = etf.wyndex; - let etf_token = etf.etf_token; - let etf_addr = etf.etf.addr_str()?; - let eur_asset = AssetEntry::new("eur"); - let usd_asset = AssetEntry::new("usd"); - let asset_infos = vec![eur_token.clone(), usd_token.clone()]; + let base_asset = proxy.base_asset()?; + assert_that!(base_asset.base_asset).is_equal_to(AssetInfo::native("usd")); // check config setup - let config: Config = etf.etf.config()?; - let position = etf.etf.total_lp_position()?; - assert_that!(position).is_equal_to(Uint128::zero()); - - assert_that!(config.liquidity_token).is_equal_to(eur_usd_lp.address()?); + let state = etf.state()?; + assert_that!(state.share_token_address).is_equal_to(etf_token.addr_str()?); // give user some funds - mock.set_balances(&[( - &owner, - &[ - coin(100_000u128, eur_token.to_string()), - coin(100_000u128, usd_token.to_string()), - ], - )])?; - - // initial deposit must be > 1000 (of both assets) - // this is set by WynDex - etf.etf.deposit( - vec![ - AnsAsset::new(eur_asset.clone(), 10000u128), - AnsAsset::new(usd_asset.clone(), 10000u128), - ], - &[coin(10_000u128, EUR), coin(10_000u128, USD)], - )?; - - let position = etf.etf.total_lp_position()?; - assert_that!(position).is_greater_than(Uint128::zero()); + mock.set_balances(&[(&owner, &[coin(1_000u128, usd_token.to_string())])])?; - // single asset deposit - etf.etf.deposit( - vec![AnsAsset::new(eur_asset, 1000u128)], - &[coin(1000u128, EUR)], + etf.deposit( + AssetUnchecked::new(AssetInfo::native("usd".to_string()), 1000u128), + &[coin(1_000u128, USD)], )?; // check that the etf token is minted let etf_token_balance = etf_token.balance(&owner)?; - assert_that!(etf_token_balance).is_equal_to(10487u128); - let new_position = etf.etf.total_lp_position()?; - assert_that!(new_position).is_greater_than(position); + assert_that!(etf_token_balance).is_equal_to(1000u128); - etf.etf.deposit( - vec![AnsAsset::new(usd_asset, 1000u128)], - &[coin(1000u128, USD)], - )?; - - // check that the etf token is increased - let etf_token_balance = etf_token.balance(&owner)?; - assert_that!(etf_token_balance).is_equal_to(10986u128); - // check if the etf balance query functions properly: - let etf_balance_queried = etf.etf.balance(owner.to_string())?; - assert_that!(etf_balance_queried).is_equal_to(Uint128::from(etf_token_balance)); - - let position = new_position; - let new_position = etf.etf.total_lp_position()?; - assert_that!(new_position).is_greater_than(position); + // the proxy contract received the funds + let balances = mock.query_all_balances(&proxy.address()?)?; + assert_that!(balances).is_equal_to(vec![coin(1_000u128, usd_token.to_string())]); - // and eur balance decreased and usd balance stayed the same - let balances = mock.query_all_balances(&owner)?; - assert_that!(balances).is_equal_to(vec![ - coin(89_000u128, eur_token.to_string()), - coin(89_000u128, usd_token.to_string()), - ]); - - // withdraw part from the auto-compounder - etf_token.send(&Cw20HookMsg::Redeem {}, 4986, etf_addr.clone())?; + // withdraw from the etf + etf_token.send(&Cw20HookMsg::Claim {}, 500, etf_addr.clone())?; // check that the etf token decreased let etf_token_balance = etf_token.balance(&owner)?; - assert_that!(etf_token_balance).is_equal_to(6000u128); - - let pending_claim = etf.etf.pending_claims(owner.to_string())?; - assert_that!(pending_claim.u128()).is_equal_to(4986u128); - let etf_token_balance = etf_token.balance(&etf.etf.address()?)?; - assert_that!(etf_token_balance).is_equal_to(4986u128); - - let total_lp_balance = etf.etf.total_lp_position()?; - assert_that!(total_lp_balance).is_equal_to(new_position); - - // Batch unbond pending claims - etf.etf.batch_unbond()?; - - // query the claims of the auto-compounder - let claims = etf.etf.claims(owner.to_string())?; - let expected_claim = Claim { - unbonding_timestamp: Expiration::AtTime(mock.block_info()?.time.plus_seconds(1)), - amount_of_etf_tokens_to_burn: 4986u128.into(), - amount_of_lp_tokens_to_unbond: 4985u128.into(), - }; - assert_that!(claims).is_equal_to(vec![expected_claim]); - - // let the time pass and withdraw the claims - mock.wait_blocks(60 * 60 * 24 * 10)?; - - // let total_lp_balance = etf.etf.total_lp_position()?; - // assert_that!(total_lp_balance).is_equal_to(new_position); - etf.etf.withdraw()?; - - // and eur and usd balance increased - let balances = mock.query_all_balances(&owner)?; - assert_that!(balances).is_equal_to(vec![ - coin(93_988u128, eur_token.to_string()), - coin(93_988u128, usd_token.to_string()), - ]); - - let position = new_position; - let new_position = etf.etf.total_lp_position()?; - assert_that!(new_position).is_less_than(position); - - let generator_staked_balance = etf - .wyndex - .suite - .query_all_staked(asset_infos, &etf.os.proxy.addr_str()?)? - .stakes[0] - .stake; - assert_that!(generator_staked_balance.u128()).is_equal_to(6001u128); - - // withdraw all from the auto-compounder - etf_token.send(&Cw20HookMsg::Redeem {}, 6000, etf_addr)?; - - // testing general non unbonding staking contract functionality - let pending_claims = etf.etf.pending_claims(owner.to_string())?.into(); - assert_that!(pending_claims).is_equal_to(6000u128); // no unbonding period, so no pending claims - - etf.etf.batch_unbond()?; // batch unbonding not enabled - mock.wait_blocks(60 * 60 * 24 * 10)?; - etf.etf.withdraw()?; // withdraw wont have any effect, because there are no pending claims - // mock.next_block()?; + assert_that!(etf_token_balance).is_equal_to(500u128); + // check that the proxy USD balance decreased (by 500 - fee (5%) = 475))) + let balances = mock.query_all_balances(&proxy.address()?)?; + assert_that!(balances).is_equal_to(vec![coin(525u128, usd_token.to_string())]); + // and the owner USD balance increased (by 500 - fee (5%) = 475) let balances = mock.query_all_balances(&owner)?; - assert_that!(balances).is_equal_to(vec![ - coin(99_993u128, eur_token.to_string()), - coin(99_993u128, usd_token.to_string()), - ]); - - let new_position = etf.etf.total_lp_position()?; - assert_that!(new_position).is_equal_to(Uint128::zero()); - + assert_that!(balances).is_equal_to(vec![coin(475u128, usd_token.to_string())]); Ok(()) } - -/// This test covers the following scenario: -/// - create a pool with rewards -/// - deposit into the pool in-balance -/// - compound rewards -/// - checks if the fee distribution is correct -/// - checks if the rewards are distributed correctly -#[test] -fn generator_with_rewards_test_fee_and_reward_distribution() -> AResult { - let owner = Addr::unchecked(test_utils::OWNER); - let commission_addr = Addr::unchecked(COMMISSION_RECEIVER); - let wyndex_owner = Addr::unchecked(WYNDEX_OWNER); - - // create testing environment - let (_state, mock) = instantiate_default_mock_env(&owner)?; - - // create a etf - let mut etf = crate::create_etf(mock.clone())?; - let WynDex { - eur_token, - usd_token, - eur_usd_lp, - eur_usd_staking, - .. - } = etf.wyndex; - - let etf_token = etf.etf_token; - let etf_addr = etf.etf.addr_str()?; - let eur_asset = AssetEntry::new("eur"); - let usd_asset = AssetEntry::new("usd"); - - // check config setup - let config = etf.etf.config()?; - assert_that!(config.liquidity_token).is_equal_to(eur_usd_lp.address()?); - - // give user some funds - mock.set_balances(&[ - ( - &owner, - &[ - coin(100_000u128, eur_token.to_string()), - coin(100_000u128, usd_token.to_string()), - ], - ), - (&wyndex_owner, &[coin(100_000u128, WYND_TOKEN.to_string())]), - ])?; - - // initial deposit must be > 1000 (of both assets) - // this is set by WynDex - etf.etf.deposit( - vec![ - AnsAsset::new(eur_asset, 100_000u128), - AnsAsset::new(usd_asset, 100_000u128), - ], - &[coin(100_000u128, EUR), coin(100_000u128, USD)], - )?; - - // query how much lp tokens are in the etf - let etf_lp_balance = etf.etf.total_lp_position()? as Uint128; - - // check that the etf token is minted - let etf_token_balance = etf_token.balance(&owner)?; - assert_that!(etf_token_balance).is_equal_to(100_000u128); - let ownerbalance = mock.query_balance(&owner, EUR)?; - assert_that!(ownerbalance.u128()).is_equal_to(0u128); - - // process block -> the AC should have pending rewards at the staking contract - mock.next_block()?; - etf.wyndex.suite.distribute_funds( - eur_usd_staking, - wyndex_owner.as_str(), - &coins(1000, WYND_TOKEN), - )?; // distribute 1000 EUR - - // rewards are 1_000 WYND each block for the entire amount of staked lp. - // the fee received should be equal to 3% of the rewarded tokens which is then swapped using the astro/EUR pair. - // the fee is 3% of 1K = 30, rewards are then 970 - // the fee is then swapped using the astro/EUR pair - // the price of the WYND/EUR pair is 10K:10K - // which will result in a 29 EUR fee for the autocompounder due to spread + rounding. - etf.etf.compound()?; - - let commission_received: Uint128 = mock.query_balance(&commission_addr, EUR)?; - assert_that!(commission_received.u128()).is_equal_to(29u128); - - // The reward for the user is then 970 WYND which is then swapped using the WYND/EUR pair - // this will be swapped for ~880 EUR, which then is provided using single sided provide_liquidity - let new_etf_lp_balance: Uint128 = etf.etf.total_lp_position()?; - let new_lp: Uint128 = new_etf_lp_balance - etf_lp_balance; - let expected_new_value: Uint128 = Uint128::from(etf_lp_balance.u128() * 4u128 / 1000u128); // 0.4% of the previous position - assert_that!(new_lp).is_greater_than(expected_new_value); - - let owner_balance_eur = mock.query_balance(&owner, EUR)?; - let owner_balance_usd = mock.query_balance(&owner, USD)?; - - // Redeem etf tokens and create pending claim of user tokens to see if the user actually received more of EUR and USD then they deposited - etf_token.send(&Cw20HookMsg::Redeem {}, etf_token_balance, etf_addr)?; - - // Unbond tokens & clear pending claims - etf.etf.batch_unbond()?; - - mock.wait_blocks(1)?; - - // Withdraw EUR and USD tokens to user - etf.etf.withdraw()?; - - let new_owner_balance = mock.query_all_balances(&owner)?; - let eur_diff = new_owner_balance[0].amount.u128() - owner_balance_eur.u128(); - let usd_diff = new_owner_balance[1].amount.u128() - owner_balance_usd.u128(); - - // the user should have received more of EUR and USD then they deposited - assert_that!(eur_diff).is_greater_than(100_000u128); // estimated value - assert_that!(usd_diff).is_greater_than(100_000u128); - - Ok(()) -} - -fn generator_with_rewards_test_rewards_distribution_with_multiple_users() -> AResult { - // test multiple user deposits and withdrawals - todo!() -}