-
Notifications
You must be signed in to change notification settings - Fork 85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: enable full foreign collect flow via extrinsic #1556
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice work, like always 💯
A couple of small comments but no blockers for me.
...on-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/foreign_investments.rs
Outdated
Show resolved
Hide resolved
...on-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/foreign_investments.rs
Outdated
Show resolved
Hide resolved
...on-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/foreign_investments.rs
Show resolved
Hide resolved
...on-tests/src/liquidity_pools/pallet/development/tests/liquidity_pools/foreign_investments.rs
Show resolved
Hide resolved
Co-authored-by: Nuno Alexandre <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👌 nice
@@ -185,7 +185,7 @@ pub struct Swap< | |||
pub currency_in: Currency, | |||
/// The outgoing currency, i.e. the one which should be replaced. | |||
pub currency_out: Currency, | |||
/// The amount of outgoing currency which shall be exchanged. | |||
/// The amount of incoming currency which shall be bought. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See this comment for explanation.
Checking something from the description:
Wouldn't that also mean, that a user could overwrite this storage with another currency mid-flight and for example, change the actual currency that is signaled via |
You are right, shoot. Then, I don't see another way of limiting an |
Thinking about this more I actually think that's not really an issue since collecting an investment is synchronous. So it could only occur if within the same block
I don't see any issues with that as this signal's the investor's intention. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Unsure whether overwriting the invest currency is not potentially dangerous.
- Quite sure that the check for
payout_currency
is currently a bug
) -> DispatchResult { | ||
// Simply overwrite any existing payment currency to enable the investor to have | ||
// full control over the payment currency | ||
InvestmentPaymentCurrency::<T>::insert(who, investment_id, foreign_payout_currency); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't that criticial if something is happening currently? E.g. swapping due to an DecreaseInvestOrder
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or a IncreaseInvestOrder
with payment_currency_a
followed by an CollectInvest
with payment_currency_b
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.ok_or(Error::<T>::InvestmentPaymentCurrencyNotFound)?; | ||
let pool_currency = T::PoolInspect::currency_for(investment_id.of_pool()) | ||
.ok_or(Error::<T>::PoolNotFound)?; | ||
let collected = CollectedInvestment::<T>::take(who, investment_id); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Due to being a ValueQuery
this will always hit. Are we sure the solidity side wants to be informed if nothing was acutally transferred?
Due to the need of bridging this seems like a big overhead for no information gain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I am not. I am just saying that if we trigger a return message, we should be sure Solidity actually needs it. If not we should collect and not trigger a return message. But lets just keep it as is. Its just optimisation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have been favouring blocking a return message if the amount is zero. I will implement this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there is a possible scenario in which someone submitted a redemption order, and then the price ended up going to 0, and the redemption is fully executed but with 0 currency payout. In that case, the value of sending ExecutedCollectRedeem
to the other domains is that they will update the local remaining_redeem_order
state to 0.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there is a possible scenario in which someone submitted a redemption order, and then the price ended up going to 0, and the redemption is fully executed but with 0 currency payout. In that case, the value of sending
ExecutedCollectRedeem
to the other domains is that they will update the localremaining_redeem_order
state to 0.
IIUC, the payment amount should then not be zero. I added a short circuit if the both the accumulated payment amount as well as collected amount are zero: 0b6924d
investment_id: T::InvestmentId, | ||
) -> DispatchResult { | ||
let foreign_payout_currency = InvestmentPaymentCurrency::<T>::get(who, investment_id) | ||
.ok_or(Error::<T>::InvestmentPaymentCurrencyNotFound)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI: All these errors can be directly merged into the storage type now.
) -> DispatchResult { | ||
let invest_id: T::TrancheCurrency = Self::derive_invest_id(pool_id, tranche_id)?; | ||
let currency_index_u128 = currency_index.index; | ||
let payout_currency = Self::try_get_payout_currency(invest_id.clone(), currency_index)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CR
Looking at the code of try_get_payout_currency
I think this is not working.
The check that is being used T::ForeignInvestment::accepted_payment_currency(invest_id,currency)
will check the wrong trading pair. I.e. the wrong trading pair direction. As trading pairs are not ensured to work in both ways, this might return ok also it will fail when trying to swap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great catch! Will fix fn try_get_payout_currency
and improve integration tests to catch this. The issue is that right now we are registering a bidirectional trading pair per default because I assumed this to feasible in reality as well. Can you think of a case in which there won't be bidirectional trading for LP transferable foreign currencies?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can not, but it might still happen by accident as no logic in the order book ensures bidirectionality/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On that note: If bidirectionality is not given, an investor cannot decrease their foreign investment if it creates an orderbook swap from pool
to foreign
because then TradingPair::<T>::get(pool_currency, foreign_currency)
does not exist. Moreover, we block redemptions for that foreign
currency as well.
AFAICT, we should check in fn allow_investment_currency
both if the currency is accepted as payment as well as payout, not only the former. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAICT, we should check in
fn allow_investment_currency
both if the currency is accepted as payment as well as payout, not only the former. WDYT?
cc @hieronx
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes 100%. I think in practice governance should ensure that always both directions of an LP token trading pair are added.
But I think given the current code complexity, this is quite an assumption for the future. I mean it being syncrhonous all the time. And IIRC we wanted that for the Solidity side and if IIRC this does not handle it well Example:
Message execution on our side due to bridge or mem-pool is:
this would mean the wrong |
When would it not be synchronous? Collecting an investment does not require a swap. It just performs Regarding the example, your message id declaration is off. I suppose you wanted the first message to have index 1.
If the user invests from multiple Liquidity Pools, each investment has their separate If we had that message flow for the same TLDR: We need to restrict an investor to the same payment currency until the foreign investment state is cleared for now on Centrifuge chain IF we want to enable the full EDIT: Again, after thinking about this further: Right now (FI code on |
@hieronx need to answer if this is sufficient. I would for now maybe check on-chain that if an |
Seems totally fine to me 👍 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All looks good. I got a minor logical worry regarding the need to have a valid InvestmentPaymentCurrency
when collecting. If this is unjustified it is good to go.
) -> Result<ExecutedForeignCollectInvest<T::Balance>, DispatchError> { | ||
foreign_payment_currency: T::CurrencyId, | ||
) -> DispatchResult { | ||
let payment_currency = InvestmentPaymentCurrency::<T>::get(who, investment_id)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just wandering regarding the states of this. Wouldn't it be possible for the initial IncreaseInvestOrder
to be fully "done" before a CollectInvest
is actually triggered?
Meaning, the InvestmentPaymentCurrency
state would be None
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An InvestmentState
is only cleared if
- either the entire unprocessed amount was decreased and that swap is fulfilled (
SwapIntoForeignDone
-->NoState
) - The state only holds an unprocessed investment amount which was fully processed (
InvestmentOngoing
-->NoState
)
The first one is irrelevant here because it requires the InvestmentState
to be re-initialized by increasing the investment which sets the payment currency.
The second one is only known if the investment is collected. Therefore, we trigger the foreign collect flow before clearing the InvestmentState
in apply_invest_state_transition
:
centrifuge-chain/pallets/foreign-investments/src/impls/mod.rs
Lines 1076 to 1083 in 0b6924d
// Need to send notification before potentially killing the `InvestmentState` if | |
// all was collected and no swap is remaining | |
Self::notify_executed_collect_invest(who, investment_id)?; | |
Self::apply_invest_state_transition(who, investment_id, post_state, true).map_err(|e| { | |
log::debug!("InvestState transition error: {:?}", e); | |
Error::<T>::from(InvestError::CollectTransition) | |
})?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the details!
@@ -209,14 +238,10 @@ impl<T: Config> ForeignInvestment<T::AccountId> for Pallet<T> { | |||
foreign_payout_currency: T::CurrencyId, | |||
pool_currency: T::CurrencyId, | |||
) -> Result<(), DispatchError> { | |||
let payout_currency = RedemptionPayoutCurrency::<T>::get(who, investment_id)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if the same thinking applies to redemptions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A RedemptionState
is only cleared if
- either the entire unprocessed amount is decreased without an active/done swap (
Redeeming
-->NoState
) or - the entire swap is fulfilled without unprocessed redeeming amount (
SwapIntoForeignDone
-->NoState
)
The first one is irrelevant here because it requires the RedemptionState
to be re-initialized by increasing the redemption which sets the payout currency.
The second one triggers the redemption collection transition before/when clearing:
centrifuge-chain/pallets/foreign-investments/src/impls/mod.rs
Lines 503 to 509 in 0b6924d
// Only states left include `SwapIntoForeignDone` without | |
// `ActiveSwapIntoForeignCurrency` such that we can notify collect | |
swap_done_state => { | |
let maybe_new_state = | |
Self::apply_collect_redeem_transition(who, investment_id, swap_done_state)?; | |
Ok((maybe_new_state, None)) | |
} |
centrifuge-chain/pallets/foreign-investments/src/impls/mod.rs
Lines 706 to 714 in 0b6924d
// Need to check if `SwapReturnDone` is part of state without | |
// `ActiveSwapIntoForeignCurrency` as this implies the successful termination of | |
// a collect (with swap into foreign currency). If this is the case, the | |
// returned redeem state needs to be updated or killed as well. | |
let returning_redeem_state = Self::apply_collect_redeem_transition( | |
who, | |
investment_id, | |
maybe_redeem_state.unwrap_or_default(), | |
)? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the patience! looks good to go.
) -> Result<ExecutedForeignCollectInvest<T::Balance>, DispatchError> { | ||
foreign_payment_currency: T::CurrencyId, | ||
) -> DispatchResult { | ||
let payment_currency = InvestmentPaymentCurrency::<T>::get(who, investment_id)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the details!
Description
Collect
InvestmentPaymentCurrency
storage for theforeign payment currency
upon receiving the first IncreaseInvestOrder message (similar to redemptions)InvestmentPaymentCurrency
will be cleared if theInvestState
is cleared, i.e. if neither an unprocessed swap nor investment is happening in case of reachingNoState
orSwapIntoForeignDone
InvestmentPaymentCurrency
will be overwritten upon receivingIncreaseInvestOrder
orCollectInvestOrder
to be less restrictive to investorspallet_investments::collect_invest_for
such that the dispatch ofExecutedCollectInvest
happens automaticallycollect_foreign_investment_for
extrinsic.pool
currency) and fully fulfilling the swap order (toforeign
currency).ExecutedCollectRedeem
happens automaticallypallet_investments::collect_{invest, redeem}_for
.Misc
allow_pool_currency
extrinsic toallow_investment_currency
foreign_investments
integration test boilerplateChecklist: