diff --git a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/foreign_investments.rs b/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/foreign_investments.rs index c784aefb8a..e0b27cdfa5 100644 --- a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/foreign_investments.rs +++ b/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/foreign_investments.rs @@ -3182,10 +3182,396 @@ mod mismatching_currencies { }); } + /// 1. increase initial redeem + /// 2. process partial redemption + /// 3. collect + /// 4. process redemption + /// 5. fulfill swap order should implicitly collect + #[tokio::test] + async fn fulfill_redeem_swap_order_requires_collect() { + let mut env = { + let mut genesis = Storage::default(); + genesis::default_balances::(&mut genesis); + env::test_env_with_centrifuge_storage(Handle::current(), genesis) + }; + + setup_test_env(&mut env); + + env.with_mut_state(Chain::Para(PARA_ID), || { + let pool_id = DEFAULT_POOL_ID; + let investor: AccountId = + AccountConverter::::convert(( + DOMAIN_MOONBEAM, + Keyring::Bob.into(), + )); + let trader: AccountId = Keyring::Alice.into(); + let pool_currency: CurrencyId = AUSD_CURRENCY_ID; + let foreign_currency: CurrencyId = USDT_CURRENCY_ID; + let pool_currency_decimals = currency_decimals::AUSD; + let invest_amount_pool_denominated: u128 = 10 * dollar(18); + let swap_order_id = 1; + create_currency_pool(pool_id, pool_currency, pool_currency_decimals.into()); + let invest_amount_foreign_denominated: u128 = enable_usdt_trading( + pool_currency, + invest_amount_pool_denominated, + true, + true, + true, + || {}, + ); + // invest in pool currency to reach `InvestmentOngoing` quickly + do_initial_increase_investment( + pool_id, + invest_amount_pool_denominated, + investor.clone(), + pool_currency, + true, + ); + // Manually set payment currency since we removed it in the above shortcut setup + InvestmentPaymentCurrency::::insert( + &investor, + default_investment_id(), + foreign_currency, + ); + assert_ok!(Tokens::mint_into( + foreign_currency, + &trader, + invest_amount_foreign_denominated * 2 + )); + + // Decrease invest setup to have invest order swapping into foreign currency + let msg = LiquidityPoolMessage::DecreaseInvestOrder { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(foreign_currency), + amount: invest_amount_foreign_denominated, + }; + assert_ok!(LiquidityPools::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg.clone() + )); + + // Redeem setup: Increase and process + assert_ok!(Tokens::mint_into( + default_investment_id().into(), + &Domain::convert(DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain()), + invest_amount_pool_denominated + )); + let msg = LiquidityPoolMessage::IncreaseRedeemOrder { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(foreign_currency), + amount: invest_amount_pool_denominated, + }; + assert_ok!(LiquidityPools::submit( + DEFAULT_DOMAIN_ADDRESS_MOONBEAM, + msg.clone() + )); + let pool_account = + pallet_pool_system::pool_types::PoolLocator { pool_id }.into_account_truncating(); + assert_ok!(Tokens::mint_into( + pool_currency, + &pool_account, + invest_amount_pool_denominated + )); + assert_ok!(Investments::process_redeem_orders(default_investment_id())); + // Process 50% of redemption at 25% rate, i.e. 1 pool currency = 4 tranche + // tokens + assert_ok!(Investments::redeem_fulfillment( + default_investment_id(), + FulfillmentWithPrice { + of_amount: Perquintill::from_percent(50), + price: Ratio::checked_from_rational(1, 4).unwrap(), + } + )); + assert_eq!( + ForeignInvestments::foreign_investment_info(swap_order_id) + .unwrap() + .last_swap_reason + .unwrap(), + TokenSwapReason::Investment + ); + assert_ok!(Investments::collect_redemptions_for( + RuntimeOrigin::signed(Keyring::Charlie.into()), + investor.clone(), + default_investment_id() + )); + assert_eq!( + ForeignInvestments::foreign_investment_info(swap_order_id) + .unwrap() + .last_swap_reason + .unwrap(), + TokenSwapReason::InvestmentAndRedemption + ); + assert_eq!( + InvestmentState::::get(&investor, default_investment_id()), + InvestState::ActiveSwapIntoForeignCurrency { + swap: Swap { + amount: invest_amount_foreign_denominated, + currency_in: foreign_currency, + currency_out: pool_currency + } + } + ); + assert_eq!( + RedemptionState::::get(&investor, default_investment_id()), + RedeemState::RedeemingAndActiveSwapIntoForeignCurrency { + redeem_amount: invest_amount_pool_denominated / 2, + swap: Swap { + amount: invest_amount_foreign_denominated / 8, + currency_in: foreign_currency, + currency_out: pool_currency + } + } + ); + assert_eq!( + RedemptionPayoutCurrency::::get( + &investor, + default_investment_id() + ) + .unwrap(), + foreign_currency + ); + let swap_amount = + invest_amount_foreign_denominated + invest_amount_foreign_denominated / 8; + assert!(System::events().iter().any(|e| { + e.event + == pallet_order_book::Event::::OrderUpdated { + order_id: swap_order_id, + account: investor.clone(), + buy_amount: swap_amount, + sell_rate_limit: Ratio::one(), + min_fulfillment_amount: min_fulfillment_amount(foreign_currency), + } + .into() + })); + ensure_executed_collect_redeem_not_dispatched(); + + // Process remaining redemption at 25% rate, i.e. 1 pool currency = + // 4 tranche tokens + assert_ok!(Investments::process_redeem_orders(default_investment_id())); + assert_ok!(Investments::redeem_fulfillment( + default_investment_id(), + FulfillmentWithPrice { + of_amount: Perquintill::from_percent(100), + price: Ratio::checked_from_rational(1, 4).unwrap(), + } + )); + assert_ok!(Investments::collect_redemptions_for( + RuntimeOrigin::signed(Keyring::Charlie.into()), + investor.clone(), + default_investment_id() + )); + assert_eq!( + InvestmentState::::get(&investor, default_investment_id()), + InvestState::ActiveSwapIntoForeignCurrency { + swap: Swap { + amount: invest_amount_foreign_denominated, + currency_in: foreign_currency, + currency_out: pool_currency + } + } + ); + assert_eq!( + RedemptionState::::get(&investor, default_investment_id()), + RedeemState::ActiveSwapIntoForeignCurrency { + swap: Swap { + amount: invest_amount_foreign_denominated / 4, + currency_in: foreign_currency, + currency_out: pool_currency + } + } + ); + let swap_amount = + invest_amount_foreign_denominated + invest_amount_foreign_denominated / 4; + assert!(System::events().iter().any(|e| { + e.event + == pallet_order_book::Event::::OrderUpdated { + order_id: swap_order_id, + account: investor.clone(), + buy_amount: swap_amount, + sell_rate_limit: Ratio::one(), + min_fulfillment_amount: min_fulfillment_amount(foreign_currency), + } + .into() + })); + + // Partially fulfilling the swap order below the invest swapping amount should + // still have both states swapping into foreign + assert_ok!(OrderBook::fill_order_partial( + RuntimeOrigin::signed(trader.clone()), + swap_order_id, + invest_amount_foreign_denominated / 2 + )); + assert!(System::events().iter().any(|e| { + e.event + == pallet_order_book::Event::::OrderFulfillment { + order_id: swap_order_id, + placing_account: investor.clone(), + fulfilling_account: trader.clone(), + partial_fulfillment: true, + fulfillment_amount: invest_amount_foreign_denominated / 2, + currency_in: foreign_currency, + currency_out: pool_currency, + sell_rate_limit: Ratio::one(), + } + .into() + })); + assert_eq!( + InvestmentState::::get(&investor, default_investment_id()), + InvestState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { + swap: Swap { + amount: invest_amount_foreign_denominated / 2, + currency_in: foreign_currency, + currency_out: pool_currency + }, + done_amount: invest_amount_foreign_denominated / 2 + } + ); + assert_eq!( + RedemptionState::::get(&investor, default_investment_id()), + RedeemState::ActiveSwapIntoForeignCurrency { + swap: Swap { + amount: invest_amount_foreign_denominated / 4, + currency_in: foreign_currency, + currency_out: pool_currency + }, + } + ); + assert!( + RedemptionPayoutCurrency::::contains_key( + &investor, + default_investment_id() + ) + ); + assert!(ForeignInvestments::foreign_investment_info(swap_order_id).is_some()); + assert!( + ForeignInvestments::token_swap_order_ids(&investor, default_investment_id()) + .is_some() + ); + ensure_executed_collect_redeem_not_dispatched(); + + // Partially fulfilling the swap order for the remaining invest swap amount + // should still clear the investment state + assert_ok!(OrderBook::fill_order_partial( + RuntimeOrigin::signed(trader.clone()), + swap_order_id, + invest_amount_foreign_denominated / 2 + )); + assert!(!InvestmentState::::contains_key( + &investor, + default_investment_id() + ),); + assert_eq!( + RedemptionState::::get(&investor, default_investment_id()), + RedeemState::ActiveSwapIntoForeignCurrency { + swap: Swap { + amount: invest_amount_foreign_denominated / 4, + currency_in: foreign_currency, + currency_out: pool_currency + }, + } + ); + assert!( + RedemptionPayoutCurrency::::contains_key( + &investor, + default_investment_id() + ) + ); + assert!(ForeignInvestments::foreign_investment_info(swap_order_id).is_some()); + assert!( + ForeignInvestments::token_swap_order_ids(&investor, default_investment_id()) + .is_some() + ); + ensure_executed_collect_redeem_not_dispatched(); + + // Partially fulfilling the swap order below the redeem swap amount should still + // clear the investment state + assert_ok!(OrderBook::fill_order_partial( + RuntimeOrigin::signed(trader.clone()), + swap_order_id, + invest_amount_foreign_denominated / 8 + )); + assert!(!InvestmentState::::contains_key( + &investor, + default_investment_id() + ),); + assert_eq!( + RedemptionState::::get(&investor, default_investment_id()), + RedeemState::ActiveSwapIntoForeignCurrencyAndSwapIntoForeignDone { + swap: Swap { + amount: invest_amount_foreign_denominated / 8, + currency_in: foreign_currency, + currency_out: pool_currency + }, + done_amount: invest_amount_foreign_denominated / 8 + } + ); + assert!( + RedemptionPayoutCurrency::::contains_key( + &investor, + default_investment_id() + ) + ); + assert!(ForeignInvestments::foreign_investment_info(swap_order_id).is_some()); + assert!( + ForeignInvestments::token_swap_order_ids(&investor, default_investment_id()) + .is_some() + ); + ensure_executed_collect_redeem_not_dispatched(); + + // Partially fulfilling the swap order below the redeem swap amount should still + // clear the investment state + assert_ok!(OrderBook::fill_order_partial( + RuntimeOrigin::signed(trader.clone()), + swap_order_id, + invest_amount_foreign_denominated / 8 + )); + assert!(!InvestmentState::::contains_key( + &investor, + default_investment_id() + ),); + assert!(!RedemptionState::::contains_key( + &investor, + default_investment_id() + ),); + assert!( + !RedemptionPayoutCurrency::::contains_key( + &investor, + default_investment_id() + ) + ); + assert!(ForeignInvestments::foreign_investment_info(swap_order_id).is_none()); + assert!( + ForeignInvestments::token_swap_order_ids(&investor, default_investment_id()) + .is_none() + ); + assert!(System::events().iter().any(|e| { + e.event + == pallet_liquidity_pools_gateway::Event::OutboundMessageSubmitted { + sender: TreasuryAccount::get(), + domain: DEFAULT_DOMAIN_ADDRESS_MOONBEAM.domain(), + message: pallet_liquidity_pools::Message::ExecutedCollectRedeem { + pool_id, + tranche_id: default_tranche_id(pool_id), + investor: investor.clone().into(), + currency: general_currency_index(foreign_currency), + currency_payout: invest_amount_foreign_denominated / 4, + tranche_tokens_payout: invest_amount_pool_denominated, + remaining_redeem_amount: 0, + }, + } + .into() + })); + }); + } + /// Similar to [concurrent_swap_orders_same_direction] but with partial /// fulfillment #[tokio::test] - async fn fulfill_redeem_swap_order_requires_collect() { + async fn partial_fulfillment_concurrent_swap_orders_same_direction() { let mut env = { let mut genesis = Storage::default(); genesis::default_balances::(&mut genesis); @@ -3195,6 +3581,7 @@ mod mismatching_currencies { setup_test_env(&mut env); env.with_mut_state(Chain::Para(PARA_ID), || { + // Increase invest setup let pool_id = DEFAULT_POOL_ID; let investor: AccountId = AccountConverter::::convert(( diff --git a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/transfers.rs b/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/transfers.rs index 17c81ded9b..d95e21ce2b 100644 --- a/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/transfers.rs +++ b/runtime/integration-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/transfers.rs @@ -200,7 +200,6 @@ async fn transfer_non_tranche_tokens_to_local() { amount, }; - // assert_eq!(OrmlTokens::total_issuance(currency_id), AUSD_ED * 2); assert_eq!(OrmlTokens::total_issuance(currency_id), 0); // Finally, verify that we can now transfer the tranche to the destination @@ -208,15 +207,7 @@ async fn transfer_non_tranche_tokens_to_local() { assert_ok!(LiquidityPools::submit(DEFAULT_DOMAIN_ADDRESS_MOONBEAM, msg)); // Verify that the correct amount was minted - // assert_eq!( - // OrmlTokens::total_issuance(currency_id), - // amount + AUSD_ED * 2 - // ); assert_eq!(OrmlTokens::total_issuance(currency_id), amount); - // assert_eq!( - // OrmlTokens::free_balance(currency_id, &receiver), - // amount + AUSD_ED - // ); assert_eq!(OrmlTokens::free_balance(currency_id, &receiver), amount); // Verify empty transfers throw