-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Asset Pallet: Support repeated destroys to safely destroy large assets #12310
Changes from 59 commits
6a9d9d1
7db1fd5
cbe14b2
001eaef
1beae07
c9ce171
3067e38
7c4f610
aca497f
634345a
93d886d
2d3b650
5a612d1
b961255
91e3c18
60f954a
ce00b5b
5492b6d
bc20a6c
6ceb592
2edac52
aa2aecf
10fdd90
139d2e9
597f532
a09e762
70930ac
dc4a243
05d778d
62d4f13
e9e108b
5df4a62
544ac52
9206acf
29c7745
2986f29
6ac84a1
1f2930f
791aa7f
450a698
3daef79
ea34cf5
84cbb47
082a10c
e84d5ae
78cde15
c6470fb
85e50b9
c97e850
df27e0b
ca9fa2e
9609f13
26d27cc
72ec8f8
ee30cf8
87a7877
773212e
a4be57f
111da0e
9c06bb8
4eef069
486814f
c185cb0
91095af
9ef3c2b
6165576
7811b62
cd3e28d
aee596e
30814df
9b653a5
57fe747
7ba1ef5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -205,6 +205,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> { | |
keep_alive: bool, | ||
) -> Result<T::Balance, DispatchError> { | ||
let details = Asset::<T, I>::get(id).ok_or(Error::<T, I>::Unknown)?; | ||
ensure!(details.status == AssetStatus::Live, Error::<T, I>::AssetNotLive); | ||
ensure!(!details.is_frozen, Error::<T, I>::Frozen); | ||
|
||
let account = Account::<T, I>::get(id, who).ok_or(Error::<T, I>::NoAccount)?; | ||
|
@@ -300,6 +301,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> { | |
ensure!(!Account::<T, I>::contains_key(id, &who), Error::<T, I>::AlreadyExists); | ||
let deposit = T::AssetAccountDeposit::get(); | ||
let mut details = Asset::<T, I>::get(&id).ok_or(Error::<T, I>::Unknown)?; | ||
ensure!(details.status == AssetStatus::Live, Error::<T, I>::AssetNotLive); | ||
let reason = Self::new_account(&who, &mut details, Some(deposit))?; | ||
T::Currency::reserve(&who, deposit)?; | ||
Asset::<T, I>::insert(&id, details); | ||
|
@@ -390,7 +392,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> { | |
Self::can_increase(id, beneficiary, amount, true).into_result()?; | ||
Asset::<T, I>::try_mutate(id, |maybe_details| -> DispatchResult { | ||
let details = maybe_details.as_mut().ok_or(Error::<T, I>::Unknown)?; | ||
|
||
ensure!(details.status == AssetStatus::Live, Error::<T, I>::AssetNotLive); | ||
muharem marked this conversation as resolved.
Show resolved
Hide resolved
|
||
check(details)?; | ||
|
||
Account::<T, I>::try_mutate(id, beneficiary, |maybe_account| -> DispatchResult { | ||
|
@@ -430,6 +432,9 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> { | |
maybe_check_admin: Option<T::AccountId>, | ||
f: DebitFlags, | ||
) -> Result<T::Balance, DispatchError> { | ||
let details = Asset::<T, I>::get(id).ok_or(Error::<T, I>::Unknown)?; | ||
ensure!(details.status == AssetStatus::Live, Error::<T, I>::AssetNotLive); | ||
|
||
let actual = Self::decrease_balance(id, target, amount, f, |actual, details| { | ||
// Check admin rights. | ||
if let Some(check_admin) = maybe_check_admin { | ||
|
@@ -467,12 +472,14 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> { | |
return Ok(amount) | ||
} | ||
|
||
let details = Asset::<T, I>::get(id).ok_or(Error::<T, I>::Unknown)?; | ||
ensure!(details.status == AssetStatus::Live, Error::<T, I>::AssetNotLive); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this could have it's own There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right, but this logic is in a lot of places, and I don't want to add more things to this PR. I would rather as a different task |
||
|
||
let actual = Self::prep_debit(id, target, amount, f)?; | ||
let mut target_died: Option<DeadConsequence> = None; | ||
|
||
Asset::<T, I>::try_mutate(id, |maybe_details| -> DispatchResult { | ||
let details = maybe_details.as_mut().ok_or(Error::<T, I>::Unknown)?; | ||
|
||
check(actual, details)?; | ||
|
||
Account::<T, I>::try_mutate(id, target, |maybe_account| -> DispatchResult { | ||
|
@@ -540,6 +547,8 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> { | |
if amount.is_zero() { | ||
return Ok((amount, None)) | ||
} | ||
let details = Asset::<T, I>::get(id).ok_or(Error::<T, I>::Unknown)?; | ||
ensure!(details.status == AssetStatus::Live, Error::<T, I>::AssetNotLive); | ||
|
||
// Figure out the debit and credit, together with side-effects. | ||
let debit = Self::prep_debit(id, source, amount, f.into())?; | ||
|
@@ -652,71 +661,127 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> { | |
sufficients: 0, | ||
approvals: 0, | ||
is_frozen: false, | ||
status: AssetStatus::Live, | ||
}, | ||
); | ||
Self::deposit_event(Event::ForceCreated { asset_id: id, owner }); | ||
Ok(()) | ||
} | ||
|
||
/// Destroy an existing asset. | ||
/// | ||
/// * `id`: The asset you want to destroy. | ||
muharem marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// * `witness`: Witness data needed about the current state of the asset, used to confirm | ||
/// complexity of the operation. | ||
/// * `maybe_check_owner`: An optional check before destroying the asset, if the provided | ||
/// account is the owner of that asset. Can be used for authorization checks. | ||
pub(super) fn do_destroy( | ||
/// Start the process of destroying an asset, by setting the asset status to `Destroying`, and | ||
/// emitting the `DestructionStarted` event. | ||
pub(super) fn do_start_destroy( | ||
id: T::AssetId, | ||
witness: DestroyWitness, | ||
maybe_check_owner: Option<T::AccountId>, | ||
) -> Result<DestroyWitness, DispatchError> { | ||
let mut dead_accounts: Vec<T::AccountId> = vec![]; | ||
) -> DispatchResult { | ||
Asset::<T, I>::try_mutate_exists(id, |maybe_details| -> Result<(), DispatchError> { | ||
let mut details = maybe_details.as_mut().ok_or(Error::<T, I>::Unknown)?; | ||
if let Some(check_owner) = maybe_check_owner { | ||
ensure!(details.owner == check_owner, Error::<T, I>::NoPermission); | ||
} | ||
ensure!(details.is_frozen, Error::<T, I>::NotFrozen); | ||
details.status = AssetStatus::Destroying; | ||
|
||
let result_witness: DestroyWitness = Asset::<T, I>::try_mutate_exists( | ||
id, | ||
|maybe_details| -> Result<DestroyWitness, DispatchError> { | ||
let mut details = maybe_details.take().ok_or(Error::<T, I>::Unknown)?; | ||
if let Some(check_owner) = maybe_check_owner { | ||
ensure!(details.owner == check_owner, Error::<T, I>::NoPermission); | ||
} | ||
ensure!(details.accounts <= witness.accounts, Error::<T, I>::BadWitness); | ||
ensure!(details.sufficients <= witness.sufficients, Error::<T, I>::BadWitness); | ||
ensure!(details.approvals <= witness.approvals, Error::<T, I>::BadWitness); | ||
Self::deposit_event(Event::DestructionStarted { asset_id: id }); | ||
Ok(()) | ||
}) | ||
} | ||
|
||
/// Destroy accounts associated with a given asset up to the max (T::RemoveItemsLimit). | ||
/// | ||
/// Each call emits the `Event::DestroyedAccounts` event. | ||
/// Returns the number of destroyed accounts. | ||
pub(super) fn do_destroy_accounts( | ||
id: T::AssetId, | ||
max_items: u32, | ||
) -> Result<u32, DispatchError> { | ||
let mut dead_accounts: Vec<T::AccountId> = vec![]; | ||
let mut remaining_accounts = 0; | ||
let _ = | ||
ggwpez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Asset::<T, I>::try_mutate_exists(id, |maybe_details| -> Result<(), DispatchError> { | ||
let mut details = maybe_details.as_mut().ok_or(Error::<T, I>::Unknown)?; | ||
ensure!(details.is_frozen, Error::<T, I>::NotFrozen); | ||
// Should only destroy accounts while the asset is being destroyed | ||
ensure!(details.status == AssetStatus::Destroying, Error::<T, I>::LiveAsset); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. error is not so accurate now There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, but it might still work though. Or i could introduce Error::::NotDestroying? But There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It could be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, that makes sense. changing |
||
|
||
for (who, v) in Account::<T, I>::drain_prefix(id) { | ||
// We have to force this as it's destroying the entire asset class. | ||
// This could mean that some accounts now have irreversibly reserved | ||
// funds. | ||
let _ = Self::dead_account(&who, &mut details, &v.reason, true); | ||
dead_accounts.push(who); | ||
if dead_accounts.len() >= (max_items as usize) { | ||
break | ||
} | ||
} | ||
debug_assert_eq!(details.accounts, 0); | ||
debug_assert_eq!(details.sufficients, 0); | ||
remaining_accounts = details.accounts; | ||
Ok(()) | ||
})?; | ||
|
||
let metadata = Metadata::<T, I>::take(&id); | ||
T::Currency::unreserve( | ||
&details.owner, | ||
details.deposit.saturating_add(metadata.deposit), | ||
); | ||
for who in &dead_accounts { | ||
T::Freezer::died(id, &who); | ||
} | ||
|
||
for ((owner, _), approval) in Approvals::<T, I>::drain_prefix((&id,)) { | ||
Self::deposit_event(Event::AccountsDestroyed { | ||
asset_id: id, | ||
accounts_destroyed: dead_accounts.len() as u32, | ||
accounts_remaining: remaining_accounts as u32, | ||
}); | ||
Ok(dead_accounts.len() as u32) | ||
} | ||
|
||
/// Destroy approvals associated with a given asset up to the max (T::RemoveItemsLimit). | ||
/// | ||
/// Each call emits the `Event::DestroyedApprovals` event | ||
tonyalaribe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// Returns the number of destroyed approvals. | ||
pub(super) fn do_destroy_approvals( | ||
id: T::AssetId, | ||
max_items: u32, | ||
) -> Result<u32, DispatchError> { | ||
let mut removed_approvals = 0; | ||
let _ = | ||
Asset::<T, I>::try_mutate_exists(id, |maybe_details| -> Result<(), DispatchError> { | ||
let mut details = maybe_details.as_mut().ok_or(Error::<T, I>::Unknown)?; | ||
|
||
ensure!(details.is_frozen, Error::<T, I>::NotFrozen); | ||
// Should only destroy accounts while the asset is being destroyed | ||
ensure!(details.status == AssetStatus::Destroying, Error::<T, I>::LiveAsset); | ||
|
||
for ((owner, _), approval) in Approvals::<T, I>::drain_prefix((id,)) { | ||
T::Currency::unreserve(&owner, approval.deposit); | ||
removed_approvals = removed_approvals.saturating_add(1); | ||
details.approvals = details.approvals.saturating_sub(1); | ||
if removed_approvals >= max_items { | ||
break | ||
} | ||
} | ||
Self::deposit_event(Event::Destroyed { asset_id: id }); | ||
Self::deposit_event(Event::ApprovalsDestroyed { | ||
asset_id: id, | ||
approvals_destroyed: removed_approvals as u32, | ||
approvals_remaining: details.approvals as u32, | ||
}); | ||
Ok(()) | ||
})?; | ||
Ok(removed_approvals) | ||
} | ||
|
||
Ok(DestroyWitness { | ||
accounts: details.accounts, | ||
sufficients: details.sufficients, | ||
approvals: details.approvals, | ||
}) | ||
}, | ||
)?; | ||
/// Complete destroying an asset and unreserve the deposit. | ||
/// | ||
/// On success, the `Event::Destroyed` event is emitted. | ||
pub(super) fn do_finish_destroy(id: T::AssetId) -> DispatchResult { | ||
Asset::<T, I>::try_mutate_exists(id, |maybe_details| -> Result<(), DispatchError> { | ||
let details = maybe_details.take().ok_or(Error::<T, I>::Unknown)?; | ||
ensure!(details.is_frozen, Error::<T, I>::NotFrozen); | ||
ensure!(details.status == AssetStatus::Destroying, Error::<T, I>::LiveAsset); | ||
ensure!(details.accounts == 0, Error::<T, I>::InUse); | ||
ensure!(details.approvals == 0, Error::<T, I>::InUse); | ||
|
||
let metadata = Metadata::<T, I>::take(&id); | ||
T::Currency::unreserve( | ||
&details.owner, | ||
details.deposit.saturating_add(metadata.deposit), | ||
); | ||
Self::deposit_event(Event::Destroyed { asset_id: id }); | ||
|
||
// Execute hooks outside of `mutate`. | ||
for who in dead_accounts { | ||
T::Freezer::died(id, &who); | ||
} | ||
Ok(result_witness) | ||
Ok(()) | ||
}) | ||
} | ||
|
||
/// Creates an approval from `owner` to spend `amount` of asset `id` tokens by 'delegate' | ||
|
@@ -730,6 +795,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> { | |
amount: T::Balance, | ||
) -> DispatchResult { | ||
let mut d = Asset::<T, I>::get(id).ok_or(Error::<T, I>::Unknown)?; | ||
ensure!(d.status == AssetStatus::Live, Error::<T, I>::AssetNotLive); | ||
ensure!(!d.is_frozen, Error::<T, I>::Frozen); | ||
Approvals::<T, I>::try_mutate( | ||
(id, &owner, &delegate), | ||
|
@@ -826,6 +892,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> { | |
symbol.clone().try_into().map_err(|_| Error::<T, I>::BadMetadata)?; | ||
|
||
let d = Asset::<T, I>::get(id).ok_or(Error::<T, I>::Unknown)?; | ||
ensure!(d.status == AssetStatus::Live, Error::<T, I>::AssetNotLive); | ||
ensure!(from == &d.owner, Error::<T, I>::NoPermission); | ||
|
||
Metadata::<T, I>::try_mutate_exists(id, |metadata| { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -179,22 +179,6 @@ impl<T: Config<I>, I: 'static> fungibles::Create<T::AccountId> for Pallet<T, I> | |
} | ||
} | ||
|
||
impl<T: Config<I>, I: 'static> fungibles::Destroy<T::AccountId> for Pallet<T, I> { | ||
type DestroyWitness = DestroyWitness; | ||
|
||
fn get_destroy_witness(asset: &T::AssetId) -> Option<Self::DestroyWitness> { | ||
Asset::<T, I>::get(asset).map(|asset_details| asset_details.destroy_witness()) | ||
} | ||
|
||
fn destroy( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removal of that method would be a breaking change for the existing integrations, for those who use our pallet and for other similar assets pallets like orml. What could we do about this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, that's true. Though my understanding from @joepetrowski is that this particular functionality isn't really used in the wild yet. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we could keep that destroy method for compatibility, but change the implementation and allow to call it if there are less than T::RemoveItemsLimit items to remove? In that case, this method would consist of the items amount check and will simply call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, that makes sense. Though it's always to be a bit annoying maintaining 2 ways of doing the same thing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. agree There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I weighed it in with @joepetrowski and since it seems no one is using the destroy extrinsic yet, it should be fine to break it. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. @joepetrowski @tonyalaribe What if we simply put |
||
id: T::AssetId, | ||
witness: Self::DestroyWitness, | ||
maybe_check_owner: Option<T::AccountId>, | ||
) -> Result<Self::DestroyWitness, DispatchError> { | ||
Self::do_destroy(id, witness, maybe_check_owner) | ||
} | ||
} | ||
|
||
impl<T: Config<I>, I: 'static> fungibles::metadata::Inspect<<T as SystemConfig>::AccountId> | ||
for Pallet<T, I> | ||
{ | ||
|
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.
It the
is_frozen
depends on the asset status, then this is superfluous.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 don't think they depend on each other, since they have different meanings. But in a future PR, we might also make is_frozen an asset status.
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.
it's probably simpler to do it with current changes, the migration already in place.
If not, let's have an issue.