-
Couldn't load subscription status.
- Fork 1.6k
Fix invariant error in fee-sized VaultWithdraw
#5876
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
base: develop
Are you sure you want to change the base?
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #5876 +/- ##
=======================================
Coverage 78.5% 78.5%
=======================================
Files 817 817
Lines 69017 69015 -2
Branches 8276 8273 -3
=======================================
+ Hits 54197 54203 +6
+ Misses 14820 14812 -8
🚀 New features to boost your workflow:
|
|
|
||
| uint256 const key = (before ? before->key() : after->key()); | ||
| if (sign && balance != zero) | ||
| if (sign != 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.
Why is the balance check removed?
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.
Because zero balance change, as long as it came from an object actually being updated, contains valid and useful information which the Vault invariants rely on.
The bug here is that the withdrawal amount is exactly equal to the fee, so the transaction does not change its TX submitter XRP balance. If we keep balance != zero condition, we lose the information that the balance was updated but the new XRP amount happened to be the same as the old amount. That information is in turn required by the invariant, and if not provided we have invariant violation withdrawal must change one destination 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.
The bug here is that the withdrawal amount is exactly equal to the fee, so the transaction does not change its TX submitter XRP balance.
What happens if the fee is larger than the withdraw amount? The tx submitter's balance would decrease. That's still valid.
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 was looking again at accountDeltaAssets (in a couple of places), and noticed that the lambda adds in the amount of the fee paid for tx[sfAccount]. That is going to give the wrong result for a delegated transaction, since the delegate pays the fee.
Fortunately, you only need to check for the presence of sfDelegate to determine who paid the fee. You may also want to factor those two lambdas into one that can be called from the two locations so you don't have to worry about drift.
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.
Moved to single location in f36dd22 however I am not sure whether to keep this check
// Delegated transaction; no need to compensate for fees
if (auto const delegate = tx[~sfDelegate];
delegate.has_value() && *delegate != tx[sfAccount])
return ret;... since this is currently dead code, with no way to test.
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.
... actually I was wrong, this can be tested - see c3b0562 . Although I am not sure how faithfully such a test represents an actual delegated transaction.
| testcase(prefix + " deposit/withdrawal same as fee"); | ||
| auto const amount = env.current()->fees().base; | ||
|
|
||
| auto tx = vault.deposit( |
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.
Shouldn't per PR description all tx fail if the tx amount is the same as the fee?
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, it should not - we want this transaction to succeed because there's nothing inherently invalid about withdrawing an amount which matches exactly the fee.
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.
Got it! I misread the description.
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.
LGTM 👍
|
|
||
| uint256 const key = (before ? before->key() : after->key()); | ||
| if (sign && balance != zero) | ||
| if (sign != 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 was looking again at accountDeltaAssets (in a couple of places), and noticed that the lambda adds in the amount of the fee paid for tx[sfAccount]. That is going to give the wrong result for a delegated transaction, since the delegate pays the fee.
Fortunately, you only need to check for the presence of sfDelegate to determine who paid the fee. You may also want to factor those two lambdas into one that can be called from the two locations so you don't have to worry about drift.
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 a couple of small suggestions
src/test/app/Vault_test.cpp
Outdated
| // Special "error" value to recognize in tests if things fail. | ||
| // Accidentally it is also 2^31 - 1 and a Mersenne prime. | ||
| static constexpr long fail = 2'147'483'647; |
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'm not usually a fan of "magic numbers", but since this is test code, I'm ok with it. If you want to rewrite the lambda to return a std::optional<long>, I wouldn't complain about it, but I know you're eager to get this merged.
| // Number balanceDelta will capture the difference (delta) between "before" | ||
| // state (zero if created) and "after" state (zero if destroyed), so the | ||
| // invariants can validate that the change in account balances matches the | ||
| // change in vault balances, stored to deltas_ at the end of this function. | ||
| Number balance{}; | ||
| // This is done even if balanceDelta is zero, but an object was updated. | ||
| Number balanceDelta{}; | ||
|
|
||
| // By default do not add anything to deltas | ||
| // Append to deltas if sign is non-zero, i.e. an object of an interesting | ||
| // type has been updated - even if the update did not change the balance. | ||
| std::int8_t sign = 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.
These comments are contradictory, and the code behavior is to only append if the sign is not zero, so the first one is wrong. If the update did not change the balance, the sign will be zero, and this the value will not be appended.
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.
If the update did not change the balance, the sign will be zero
I think that's incorrect. SAV transactors (as all others) will call view().update(sle) unconditionally, whether or not the balance has changed, and we actually rely on this behaviour. The specifics in this particular PR: withdraw increases the submitter balance by the exact same amount of XRP as the transaction fee, hence submitters sfBalance will not change but the account root object will be still updated. We also handle even more extreme case, when the submitter withdraws less XRP than the transaction fee, hence their balance will decrease (I guess I need to add test for this one).
In the context of this block of code, sign will be set to -1 in case ltACCOUNT_ROOT: but since both before->getFieldAmount(sfBalance) and after->getFieldAmount(sfBalance) yield the same number, balanceDelta will be zero.
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 guess I need to change the comment to make it clear that updating an object happens whether or not its balance has changed, and that includes account root objects.
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.
hope the comment in 26b57ef makes this clear.
High Level Overview of Change
This fixes an invariant error where the amount withdrawn is equal to the transaction fee.
Context of Change
The invariants for
VaultWithdrawenforce that the account receiving the funds must have their balance changed (increased). This won't happen when the amount withdrawn is exactly same as fee. The submitter account balance won't change, because the deduced transaction fee is exactly refilled with the withdrawn amount.Type of Change