Skip to content
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

Add improved homa lite redeem match #1626

Merged
merged 25 commits into from
Nov 24, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b9e9e57
Added a new mock that costs no fees. This can be used to test economi…
Nov 16, 2021
6b5746c
Fixed a bug where total_staking is not changed when redeeming from av…
Nov 17, 2021
3ea7064
Added one more test
Nov 17, 2021
e02b9cf
Updated comments
Nov 17, 2021
83d7593
Added one more test
Nov 17, 2021
23c2000
Updated homalite redeem logic so if redeemer doesn't enough reserve b…
Nov 18, 2021
9ee460c
Fixed a clippy errot
Nov 18, 2021
0e59973
Added function that iterates redeem request from a starting element. …
Nov 19, 2021
e8c4b9e
Replaced redeem logic to iterate from the "Next" item
Nov 20, 2021
1c4c68d
Merge remote-tracking branch 'origin/master' into feature/homa-lite-r…
Nov 20, 2021
785658b
Improved the way iterator from next element
Nov 21, 2021
2bb50e2
Changed the storage of NextRedeemRequestToMatch to store the last red…
Nov 22, 2021
84f0042
Improved how "first element" is handled
Nov 22, 2021
47851d6
Improved coding structure for homalite redeem
Nov 23, 2021
a1613ed
Refactored the way functions are structured in HomaLite
Nov 23, 2021
e3acbff
Fixed a benchmarking test
Nov 23, 2021
f494599
refactor
xlc Nov 24, 2021
43318a9
Fixed unit tests
Nov 24, 2021
b8ca01d
Tidied up how events are asserted in HomaLite unit tests
Nov 24, 2021
4b7ee6a
Tidied up how events are asserted
Nov 24, 2021
62083c8
minor comments update
Nov 24, 2021
308d6e2
Added System::reset_events to make event assert clearer
Nov 24, 2021
37d0827
Fixed clippy
Nov 24, 2021
4cd40e4
Update modules/homa-lite/src/lib.rs
xlc Nov 24, 2021
64ca4fb
Merge remote-tracking branch 'origin/master' into feature/homa-lite-r…
xlc Nov 24, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
234 changes: 172 additions & 62 deletions modules/homa-lite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ pub mod module {
pub type XcmDestWeight<T: Config> = StorageValue<_, Weight, ValueQuery>;

/// Requests to redeem staked currencies.
/// RedeemRequests: Map: AccountId => Option<(liquid_amount: Balance, addtional_fee: Permill)>
/// RedeemRequests: Map: AccountId => Option<(liquid_amount: Balance, additional_fee: Permill)>
#[pallet::storage]
#[pallet::getter(fn redeem_requests)]
pub type RedeemRequests<T: Config> = StorageMap<_, Twox64Concat, T::AccountId, (Balance, Permill), OptionQuery>;
Expand All @@ -260,6 +260,12 @@ pub mod module {
#[pallet::getter(fn staking_interest_rate_per_update)]
pub type StakingInterestRatePerUpdate<T: Config> = StorageValue<_, Permill, ValueQuery>;

/// Next redeem request to iterate from when matching redeem requests.
/// NextRedeemRequestToMatch: Value: T::AccountId
#[pallet::storage]
#[pallet::getter(fn next_redeem_request_to_match)]
pub type NextRedeemRequestToMatch<T: Config> = StorageValue<_, T::AccountId, ValueQuery>;
syan095 marked this conversation as resolved.
Show resolved Hide resolved

#[pallet::pallet]
pub struct Pallet<T>(_);

Expand Down Expand Up @@ -306,7 +312,7 @@ pub mod module {
}

fn on_initialize(n: T::BlockNumber) -> Weight {
// Update the total amount of Staking balance by acrueing the interest periodically.
// Update the total amount of Staking balance by accruing the interest periodically.
let interest_rate = Self::staking_interest_rate_per_update();
if !interest_rate.is_zero()
&& n.checked_rem(&T::StakingUpdateFrequency::get())
Expand All @@ -333,7 +339,7 @@ pub mod module {
/// liquid currency.
///
/// If any amount is minted through XCM, a portion of that amount (T::MintFee and
/// T::MaxRewardPerEra) is reducted as fee.
/// T::MaxRewardPerEra) is deducted as fee.
///
/// Parameters:
/// - `amount`: The amount of Staking currency to be exchanged.
Expand Down Expand Up @@ -565,7 +571,7 @@ pub mod module {
/// Requires `T::GovernanceOrigin`
///
/// Parameters:
/// - `new_unbonds`: The new ScheduledUnbond storage to replace the currrent storage.
/// - `new_unbonds`: The new ScheduledUnbond storage to replace the current storage.
#[pallet::weight(< T as Config >::WeightInfo::replace_schedule_unbond())]
#[transactional]
pub fn replace_schedule_unbond(
Expand Down Expand Up @@ -634,7 +640,7 @@ pub mod module {
}

/// Set the interest rate for TotalStakingCurrency.
/// TotakStakingCurrency is incremented every `T::StakingUpdateFrequency` blocks
/// TotalStakingCurrency is incremented every `T::StakingUpdateFrequency` blocks
///
/// Requires `T::GovernanceOrigin`
///
Expand Down Expand Up @@ -768,10 +774,10 @@ pub mod module {
// Attempt to match redeem requests if there are any.
let total_liquid_to_mint = Self::convert_staking_to_liquid(amount)?;

// The amount of liquid currency to be redeemed for the mint reuqest.
// The amount of liquid currency to be redeemed for the mint request.
let mut liquid_remaining = total_liquid_to_mint;

// New balances after redeem requests are fullfilled.
// New balances after redeem requests are fulfilled.
let mut new_balances: Vec<(T::AccountId, Balance, Permill)> = vec![];

// Iterate through the prioritized requests first
Expand Down Expand Up @@ -801,21 +807,34 @@ pub mod module {
new_balances.clear();

let mut redeem_requests_limit_remaining = T::MaximumRedeemRequestMatchesForMint::get();
// Iterate all remaining redeem requests now.
for (redeemer, (request_amount, extra_fee)) in RedeemRequests::<T>::iter() {
// If all the currencies are minted, return.
if liquid_remaining.is_zero() || redeem_requests_limit_remaining.is_zero() {
break;
}
Self::match_mint_with_redeem_request(
minter,
&redeemer,
request_amount,
extra_fee,
&mut liquid_remaining,
&mut new_balances,
)?;
redeem_requests_limit_remaining -= 1;
if !liquid_remaining.is_zero() && !redeem_requests_limit_remaining.is_zero() {
// Define the function to be executed for each of the redeem request:
// Redeem the given requests with the current minting request.
//
// Params: redeemer: T::AccountId, request_amount: Balance, extra_fee: Permill
// Returns: Result<should_break, Error>
//
// Capturing: &mut liquid_remaining
// &mut redeem_requests_limit_remaining
// &mut new_balances
// If all the currencies are minted, return `should_break` as true.
let mut f = |redeemer, request_amount, extra_fee| -> Result<bool, DispatchError> {
Self::match_mint_with_redeem_request(
minter,
&redeemer,
request_amount,
extra_fee,
&mut liquid_remaining,
&mut new_balances,
)?;
redeem_requests_limit_remaining -= 1;

// Should break when all currencies are minted
Ok(liquid_remaining.is_zero() || redeem_requests_limit_remaining.is_zero())
};

// Call the function on redeem requests - iterating from the `NextRedeemRequestToMatch`
Self::iterate_from_next_redeem_request(&mut f)?;
}

// Update storage to the new balances. Remove Redeem requests that have been filled.
Expand Down Expand Up @@ -895,7 +914,7 @@ pub mod module {
/// - `max_num_matches`: Maximum number of redeem requests to be matched.
///
/// return:
/// Result<u32, DispatchError>: The number of redeem reqeusts actually matched.
/// Result<u32, DispatchError>: The number of redeem requests actually matched.
#[transactional]
pub fn process_redeem_requests_with_available_staking_balance(
max_num_matches: u32,
Expand All @@ -910,45 +929,59 @@ pub mod module {

let mut new_balances: Vec<(T::AccountId, Balance, Permill)> = vec![];
let mut num_matched = 0u32;
for (redeemer, (request_amount, extra_fee)) in RedeemRequests::<T>::iter() {
let actual_liquid_amount = min(
request_amount,
Self::convert_staking_to_liquid(available_staking_balance)?,
);

// Ensure the redeemer have enough liquid currency in their account.
if T::Currency::reserved_balance(T::LiquidCurrencyId::get(), &redeemer) >= actual_liquid_amount {
let actual_staking_amount = Self::convert_liquid_to_staking(actual_liquid_amount)?;

Self::update_total_staking_currency_storage(|total| {
Ok(total.saturating_sub(actual_staking_amount))
})?;

//Actual deposit amount has `T::XcmUnbondFee` deducted.
let actual_staking_amount_deposited = actual_staking_amount.saturating_sub(T::XcmUnbondFee::get());
T::Currency::deposit(T::StakingCurrencyId::get(), &redeemer, actual_staking_amount_deposited)?;

// Burn the corresponding amount of Liquid currency from the user.
// The redeemer is guaranteed to have enough fund
T::Currency::unreserve(T::LiquidCurrencyId::get(), &redeemer, actual_liquid_amount);
T::Currency::slash(T::LiquidCurrencyId::get(), &redeemer, actual_liquid_amount);

available_staking_balance = available_staking_balance.saturating_sub(actual_staking_amount);
let request_amount_remaining = request_amount.saturating_sub(actual_liquid_amount);
new_balances.push((redeemer.clone(), request_amount_remaining, extra_fee));

Self::deposit_event(Event::<T>::Redeemed(
redeemer,
actual_staking_amount_deposited,
actual_liquid_amount,
));
num_matched += 1u32;
}
{
// Define the function to be executed for each of the redeem request:
// Redeem the given requests using available_staking_balance.
//
// Params: redeemer: T::AccountId, request_amount: Balance, extra_fee: Permill
// Returns: Result< should_break, Error>
//
// Capturing: &mut available_staking_balance
// &mut num_matched
// &mut new_balances
let mut f = |redeemer, request_amount, extra_fee| -> Result<bool, DispatchError> {
let actual_liquid_amount = min(
request_amount,
Self::convert_staking_to_liquid(available_staking_balance)?,
);

// If all the currencies are minted, return.
if available_staking_balance < T::MinimumMintThreshold::get() || num_matched >= max_num_matches {
break;
}
// Ensure the redeemer have enough liquid currency in their account.
if T::Currency::reserved_balance(T::LiquidCurrencyId::get(), &redeemer) >= actual_liquid_amount {
let actual_staking_amount = Self::convert_liquid_to_staking(actual_liquid_amount)?;

Self::update_total_staking_currency_storage(|total| {
Ok(total.saturating_sub(actual_staking_amount))
})?;

//Actual deposit amount has `T::XcmUnbondFee` deducted.
let actual_staking_amount_deposited =
actual_staking_amount.saturating_sub(T::XcmUnbondFee::get());
T::Currency::deposit(T::StakingCurrencyId::get(), &redeemer, actual_staking_amount_deposited)?;

// Burn the corresponding amount of Liquid currency from the user.
// The redeemer is guaranteed to have enough fund
T::Currency::unreserve(T::LiquidCurrencyId::get(), &redeemer, actual_liquid_amount);
T::Currency::slash(T::LiquidCurrencyId::get(), &redeemer, actual_liquid_amount);

available_staking_balance = available_staking_balance.saturating_sub(actual_staking_amount);
let request_amount_remaining = request_amount.saturating_sub(actual_liquid_amount);
new_balances.push((redeemer.clone(), request_amount_remaining, extra_fee));

Self::deposit_event(Event::<T>::Redeemed(
redeemer,
actual_staking_amount_deposited,
actual_liquid_amount,
));
num_matched += 1u32;
}

// If all the currencies are minted, return `should_break` as true
Ok(available_staking_balance <= T::MinimumMintThreshold::get() || num_matched >= max_num_matches)
};

// Redeem requests from `NextRedeemRequestToMatch` using available_staking_balance.
Self::iterate_from_next_redeem_request(&mut f)?;
}

// Update storage to the new balances. Remove Redeem requests that have been filled.
Expand Down Expand Up @@ -976,12 +1009,22 @@ pub mod module {
}
}

// Helper function that checks if the `liquid_amount` is above the minimum redeem threshold, and
// is enough to pay for the XCM unbond fee.
fn liquid_amount_is_above_minimum_threshold(liquid_amount: Balance) -> bool {
liquid_amount > T::MinimumRedeemThreshold::get()
&& Self::convert_liquid_to_staking(liquid_amount).unwrap_or_default() > T::XcmUnbondFee::get()
}

/// Construct a XCM message
/// Helper function that construct an XCM message that:
/// 1. `withdraw_unbonded` from HomaLite sub-account.
/// 2. Transfer the withdrew fund into Sovereign account.
///
/// Param:
/// - `parachain_account` : sovereign account's AccountId
/// - `amount` : amount to withdraw from unbonded.
/// Return:
/// Xcm<()>: the Xcm message constructed.
pub fn construct_xcm_unreserve_message(parachain_account: T::AccountId, amount: Balance) -> Xcm<()> {
let xcm_message = T::RelayChainCallBuilder::utility_as_derivative_call(
T::RelayChainCallBuilder::utility_batch_call(vec![
Expand All @@ -997,7 +1040,8 @@ pub mod module {
)
}

/// Helper function that update the storage of total_staking_currency and emit event.
/// Helper function that update the storage of total_staking_currency.
/// Ensures that the total staking amount would not become zero, and emit an event.
fn update_total_staking_currency_storage(
f: impl FnOnce(Balance) -> Result<Balance, DispatchError>,
) -> DispatchResult {
Expand All @@ -1008,6 +1052,72 @@ pub mod module {
Ok(())
})
}

/// Helper function that iterates `RedeemRequests` storage from `NextRedeemRequestToMatch`,
/// and call the MutFn f() on that request.
///
/// If `NextRedeemRequestToMatch` is not found in storage, iterate from the start.
///
/// Param for FnMut f:
/// - `redeemer`: AccountId of the redeemer
/// - `amount`: The amount to be redeemed
/// - `extra_fee`: The extra fee to be paid to the minter from the redeemer
/// Return for FnMut f:
/// Result<should_break, error>: If Ok, return if the iteration should end. Otherwise return
/// the Error
pub fn iterate_from_next_redeem_request(
f: &mut impl FnMut(T::AccountId, Balance, Permill) -> Result<bool, DispatchError>,
) -> DispatchResult {
let first_element = match RedeemRequests::<T>::iter().next() {
// Current storage is empty. Do nothing and return
None => return Ok(()),
Some(element) => element,
};

// If "next" exists in storage, use it as the "starting_item". Otherwise use the first item.
let starting_redeemer = Self::next_redeem_request_to_match();
let starting_element = match Self::redeem_requests(starting_redeemer.clone()) {
syan095 marked this conversation as resolved.
Show resolved Hide resolved
Some(request) => (starting_redeemer, request),
None => first_element,
syan095 marked this conversation as resolved.
Show resolved Hide resolved
};
let starting_key = RedeemRequests::<T>::hashed_key_for(starting_element.0.clone());
let mut iterator = RedeemRequests::<T>::iter_from(starting_key);

// Call the function for the first item
let (redeemer, (request_amount, extra_fee)) = starting_element.clone();
if f(redeemer, request_amount, extra_fee)? {
// Store the `next` element as `NextRedeemRequestToMatch`
NextRedeemRequestToMatch::<T>::put(iterator.next().unwrap_or_default().0);
return Ok(());
}

// Iterate until the end of the storage, calling f() for each element
#[allow(clippy::while_let_on_iterator)]
while let Some((redeemer, (request_amount, extra_fee))) = iterator.next() {
if f(redeemer, request_amount, extra_fee)? {
// Store the `next` element as `NextRedeemRequestToMatch`
NextRedeemRequestToMatch::<T>::put(iterator.next().unwrap_or_default().0);
return Ok(());
}
}

// Reset the iterator and start from the beginning of the storage until `starting_redeemer`, calling
// f() for each element.
iterator = RedeemRequests::<T>::iter();

#[allow(clippy::while_let_on_iterator)]
while let Some((redeemer, (request_amount, extra_fee))) = iterator.next() {
if redeemer == starting_element.0 {
// We have wrapped to the beginning. Return.
return Ok(());
}
if f(redeemer, request_amount, extra_fee)? {
NextRedeemRequestToMatch::<T>::put(iterator.next().unwrap_or_default().0);
return Ok(());
}
}
Ok(())
}
}

impl<T: Config> ExchangeRateProvider for Pallet<T> {
Expand Down
4 changes: 2 additions & 2 deletions modules/homa-lite/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ pub const MOCK_XCM_DESTINATION: MultiLocation = X1(Junction::AccountId32 {
id: [1u8; 32],
})
.into();
pub const MOCK_XCM_ACCOUNTID: AccountId = AccountId32::new([255u8; 32]);
pub const MOCK_XCM_ACCOUNT_ID: AccountId = AccountId32::new([255u8; 32]);
pub const PARACHAIN_ID: u32 = 2000;

/// For testing only. Does not check for overflow.
Expand Down Expand Up @@ -258,7 +258,7 @@ parameter_types! {
pub MinimumMintThreshold: Balance = millicent(1000);
pub MinimumRedeemThreshold: Balance = millicent(1000);
pub const MockXcmDestination: MultiLocation = MOCK_XCM_DESTINATION;
pub const MockXcmAccountId: AccountId = MOCK_XCM_ACCOUNTID;
pub const MockXcmAccountId: AccountId = MOCK_XCM_ACCOUNT_ID;
pub DefaultExchangeRate: ExchangeRate = ExchangeRate::saturating_from_rational(1, 10);
pub const MaxRewardPerEra: Permill = Permill::from_percent(1);
pub MintFee: Balance = millicent(1000);
Expand Down
2 changes: 1 addition & 1 deletion modules/homa-lite/src/mock_no_fees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ parameter_types! {
pub MinimumMintThreshold: Balance = 0;
pub MinimumRedeemThreshold: Balance = 0;
pub const MockXcmDestination: MultiLocation = MOCK_XCM_DESTINATION;
pub const MockXcmAccountId: AccountId = MOCK_XCM_ACCOUNTID;
pub const MockXcmAccountId: AccountId = MOCK_XCM_ACCOUNT_ID;
pub DefaultExchangeRate: ExchangeRate = ExchangeRate::saturating_from_rational(1, 10);
pub const MaxRewardPerEra: Permill = Permill::zero();
pub MintFee: Balance = 0;
Expand Down
Loading