-
Notifications
You must be signed in to change notification settings - Fork 15
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
Introducing AssetBalance type #5290
base: main
Are you sure you want to change the base?
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5290 +/- ##
======================================
- Coverage 70% 70% -0%
======================================
Files 487 488 +1
Lines 87463 87568 +105
Branches 87463 87568 +105
======================================
+ Hits 61511 61519 +8
- Misses 22672 22758 +86
- Partials 3280 3291 +11
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
- Tests to check we don't mix assets - Tests for scalar operations - Tests for overladed operators
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 are some issues with this.
I want to have a go at refactoring it, I have some ideas on how to split up the types/traits etc a bit more clearly.
Will put the PR in draft mode for now.
pub struct AssetBalance { | ||
/// The asset of the balance. For example, DOT, ETH, etc. | ||
asset: Asset, | ||
/// The amount of the balance. |
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.
nit: some of these doc comments don't add much information.
/// replacement for the AssetAmount type where it is possible and straight forward. It's intended | ||
/// that this type is not deriving Copy or Clone to force the user to think about the handling of | ||
/// the resource. | ||
#[derive(Debug, Encode, Decode, TypeInfo, Eq)] |
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.
What happened to must_use
?
/// Subtracts the given amount from the balance, saturating at 0. | ||
/// Note: This is a primitive operation and should be used with caution. | ||
/// It is the caller's responsibility to ensure **not** to mix assets. | ||
pub fn saturating_sub_amount(&mut self, amount: AssetAmount) { | ||
self.amount = self.amount.saturating_sub(amount); | ||
} | ||
|
||
/// Adds the given amount to the balance, saturating at MAX. | ||
/// Note: This is a primitive operation and should be used with caution. | ||
/// It is the caller's responsibility to ensure **not** to mix assets. | ||
pub fn saturating_add_amount(&mut self, amount: AssetAmount) { | ||
self.amount = self.amount.saturating_add(amount); | ||
} |
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.
These methods should not exist. If we allow the checks to be trivially avoided like this, then this whole change is a bit pointless.
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.
We also had methods like this in your first draft, so I was thinking this can be useful or should be possible. But yeah, I was also wondering...
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.
Nope the first draft didn't have these. The reason was that we also want to try to track ownership (this why we don't implement Clone or Copy).
fn lt(&self, other: &Self) -> bool { | ||
Self::ensure_asset_compatibility(self, other); | ||
self.amount < other.amount | ||
} | ||
|
||
fn le(&self, other: &Self) -> bool { | ||
Self::ensure_asset_compatibility(self, other); | ||
self.amount <= other.amount | ||
} | ||
|
||
fn gt(&self, other: &Self) -> bool { | ||
Self::ensure_asset_compatibility(self, other); | ||
self.amount > other.amount | ||
} | ||
|
||
fn ge(&self, other: &Self) -> bool { | ||
Self::ensure_asset_compatibility(self, other); | ||
self.amount >= other.amount | ||
} |
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.
These don't need to be derived, they are provided through partial_cmp
@@ -140,12 +137,12 @@ pub mod pallet { | |||
/// Liabilities are funds that are owed to some external party. | |||
#[pallet::storage] | |||
pub type Liabilities<T: Config> = | |||
StorageMap<_, Twox64Concat, Asset, BTreeMap<ExternalOwner, AssetAmount>, ValueQuery>; | |||
StorageMap<_, Twox64Concat, Asset, BTreeMap<ExternalOwner, AssetBalance>, OptionQuery>; |
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.
Since we don't actually own these assets, I don't think use AssetBalance is correct here - should be AssetAmount.
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 understand what you mean by "own"
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.
What I mean is that the idea is that AssetBalance represents some assets that owned by network: controlled by the validators. Liabilities is for tracking things that are owed by the network: fees we have paid and need to refund, for example.
@@ -208,24 +205,24 @@ impl<T: Config> Pallet<T> { | |||
fn reconcile( | |||
chain: ForeignChain, | |||
owner: &ExternalOwner, | |||
amount_owed: &mut AssetAmount, | |||
available: &mut AssetAmount, | |||
amount_owed: &mut AssetBalance, |
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.
Here too - the amount owed is not some amount of asset that is owned on-chain, it's amount that is owed. I think using AssetAmount here would make more sense.
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 makes sense in a way that everywhere you use AssetBalance you have more guarantees, and it's saver to use also if you change the implementation in the future. If you assume that the implementation is 100% correct, and we will never do any changes again that could introduce new bugs mixing up assets or accounting assets in a wrong way, then there is not much point in changing it at all in my opinion.
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 agree checking the Asset might make sense here, but this is a rare case where we're comparing assets (owned) to liabilities (not owned). Most of the time we are dealing exclusively with assets owned by the network.
|
||
/// Ensures we consume the other asset, checks the compatibility and reduces it from asset | ||
/// balance saturating at 0. | ||
pub fn saturating_reduce(&mut self, other: Self) { |
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.
This also doesn't make much sense: we effectively delete other
, which is what we're trying to avoid.
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.
That is exactly what we want, or? We have 2 assets, we add the other asset to self, so other doesn't exist anymore (burned/destroyed/what ever). The whole point of this is to use the type system of rust to enforce handling an
value of an AssetBalance type in a way of a physical thing that is gone after I have used it. Like a coin for ex. I am a bit confused now...
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 this function signature would make sense for adding, but not reducing. How can can you reduce an amount by consuming some other amount? You can't.
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 you can. Lets say you have two Assets. A (self) is 50 B (other) is 30. You reduce A by B. B is consumed by saturating_reduce
and A is 20. B is consumed and gone. Makes total sense 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.
Where did B go in this example?
It's like saying: I have €10 and you give me €2, I reduce my 10 by 2 and now I have €8.
/// Ensures we consume the other asset, checks the compatibility and adds it to asset balance. | ||
/// Wraps the actual checked_add method and so provides the same functionality. Doesn't modify | ||
/// the original balance. | ||
pub fn checked_add(&self, other: Self) -> Option<Self> { |
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.
This doesn't take ownership of self
, so it duplicates funds: after calling this, the caller still owns self
but now also has the result, which is self+other.
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.
Indeed, the doc comment is not correct here. I changed the implementation after writing this. The intense was to have exactly something like checked_add (which is useful) but with the check of the assets.
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 were 2 things we wanted to achieve: 1. avoid mixing assets. 2. avoid duplicating funds. This method duplicates funds.
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.
Okay, I see what you mean... It's true that checked_add doesn't make a lost of sense here
/// Ensures we consume the other asset, checks the compatibility and subtracts it from asset | ||
/// balance. Wraps the actual checked_sub method and so provides the same functionality. Doesn't | ||
/// modify the original balance. | ||
pub fn checked_sub(&self, other: Self) -> Option<Self> { |
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.
Same.
} | ||
} | ||
|
||
impl PartialOrd for AssetBalance { |
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 deriving these is fine. (same for Ord and PartialEq).
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.
In fact maybe it makes sense to not implement Eq/PartialEq (or only PartialEq?)... We might want to compare asset, or amount, but one balance is only equal to another balance if they are then same and we are ensuring that we can't duplicate balances.
In other words: if I have 1€ and you have 1€, we have the same amounts, and the same currency, but they are not the same coins. So asking "is my coin equal to yours?" is kind of meaningless.
Also Ord: it doesn't make sense to compare $1 to 1FLIP, so PartialOrd
might be sufficient (which can return an ordering of None
).
My 2 cents: So after reading your comments, I am not sure if this change really makes a lot of sense. The only real benefit we get is that we are more safe in the future with the guarantee a new type can offer (don't mix assets, don't account in a wrong way). It will not add more benefit to our current codebase (assuming it is correct) other than making the code more complex because it's more complex to handle this type and possibly adding new bugs because we have to do a lot of refracting. Most likely we will end up in a scenario where we use in some places |
You're entitled to your opinion - I think the change makes sense if we can figure out how to implement it properly and with minimal friction. We have had bugs in the past that would have been caught by a system like this.
Exactly - we have to just 'assume' that everywhere we deal with asset balances, it's always correct. Instead, if we make this change, we only have to assume that the AssetBalances type is implemented correcty. That's a much smaller assumption :) I agree it's not an urgent priority though - I'll keep working on it in the background. |
Pull Request
Closes: PRO-1466
Checklist
Please conduct a thorough self-review before opening the PR.
Summary
Please include a succinct description of the purpose and content of the PR. What problem does it solve, and how? Link issues, discussions, other PRs, and anything else that will help the reviewer.
Non-Breaking changes
If this PR includes non-breaking changes, select the
non-breaking
label. On merge, CI will automatically cherry-pick the commit to a PR against the release branch.