-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Add fixAMMv1_3 amendment #5203
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 fixAMMv1_3 amendment #5203
Conversation
* Add AMM deposit and withdraw rounding to ensure AMM invariant * Add AMM bid/create/deposit/swap/withdraw/vote invariants * Add Offer invariant to verify consumed amounts * Fix Bid validation
5f8bd94
to
c789b5f
Compare
@gregtatcam could you please merge latest develop so it would be easier to review? Also build seems to be broken |
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.
Initial review of the invariant implementation
if ((txType == ttAMM_DEPOSIT || txType == ttAMM_WITHDRAW || | ||
txType == ttAMM_CLAWBACK || txType == ttAMM_BID) && | ||
ammAccount_) | ||
{ |
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 would actually split these if blocks into 4 separate functions, each checking their own condition. When named properly that would improve readability.
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.
IMO it's a lot of code repetition. I added comments and broke the if
expression, which checks the invariant. It should be a lot better now.
done |
XRPL_FEATURE(DynamicNFT, Supported::yes, VoteBehavior::DefaultNo) | ||
XRPL_FEATURE(Credentials, Supported::yes, VoteBehavior::DefaultNo) | ||
XRPL_FEATURE(AMMClawback, Supported::yes, VoteBehavior::DefaultNo) | ||
XRPL_FIX (AMMv1_2, Supported::yes, VoteBehavior::DefaultNo) |
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.
Maybe should add //clang-format off for this file and revert this change? I personally don't really see this as a problem though
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.
Makes sense to add // clang-format off
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 file does not have an extension expected by clang-format
, so it will not be automatically formatted by it. No need for //clang-format off
here.
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.
My VS code setup has application of clang-format on save. So when I add new feature to this file, it reformats it on save regardless of it's extension.
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 need to update my local tooling, which runs clang-format on every changed file. Just need to add an exception for *.macro
.
include/xrpl/protocol/Rules.h
Outdated
bool | ||
isFeatureEnabled(uint256 const& feature); | ||
|
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.
Maybe worth to put on the top of the file (same as other free functions)?
src/libxrpl/protocol/Rules.cpp
Outdated
bool | ||
isFeatureEnabled(uint256 const& feature) | ||
{ | ||
auto const rules = getCurrentTransactionRules(); |
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.
maybe worth using auto const&
to avoid copying the rules?
src/test/jtx/Env.h
Outdated
/** Check if feature is enabled | ||
*/ |
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: this comment doesn't bring any value
void | ||
ValidAMM::visitEntry( | ||
bool isDelete, | ||
std::shared_ptr<SLE const> const& before, | ||
std::shared_ptr<SLE const> const& after) | ||
{ | ||
if (isDelete) | ||
return; | ||
|
||
auto set = [&](std::shared_ptr<SLE const> const& sle, auto&& cb) { | ||
auto const type = sle->getType(); | ||
if (type == ltAMM) | ||
{ | ||
cb(); | ||
} | ||
else if ( | ||
sle->isFieldPresent(sfBalance) && | ||
((type == ltRIPPLE_STATE && sle->getFlags() & lsfAMMNode) || | ||
(type == ltACCOUNT_ROOT && sle->isFieldPresent(sfAMMID)))) | ||
{ | ||
ammPoolChanged_ = true; | ||
} | ||
}; | ||
|
||
if (after) | ||
set(after, [&] { | ||
ammAccount_ = after->getAccountID(sfAccount); | ||
lptAMMBalanceAfter_ = after->getFieldAmount(sfLPTokenBalance); | ||
}); | ||
if (before) | ||
set(before, [&] { | ||
lptAMMBalanceBefore_ = before->getFieldAmount(sfLPTokenBalance); | ||
}); | ||
} |
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 appreciate deduplication, but I find this code too difficult to read. Lambda + callback is too much for my taste.
I'd suggest something like this:
void
ValidAMM::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (isDelete)
return;
// Check if pool changed
auto ammPoolChanged = [&](std::shared_ptr<SLE const> const& sle) {
auto const type = sle->getType();
return sle->isFieldPresent(sfBalance) &&
((type == ltRIPPLE_STATE && sle->getFlags() & lsfAMMNode) ||
(type == ltACCOUNT_ROOT && sle->isFieldPresent(sfAMMID)));
};
// Check if entry is AMM
auto isAMM = [](std::shared_ptr<SLE const> const& sle) {
auto const type = after->getType();
return type == ltAMM;
};
if (after)
{
if (isAMM(after))
{
ammAccount_ = after->getAccountID(sfAccount);
lptAMMBalanceAfter_ = after->getFieldAmount(sfLPTokenBalance);
}
else if (ammPoolChanged(after))
{
ammPoolChanged_ = true;
}
}
if (before)
{
if (isAMM(before))
{
lptAMMBalanceBefore_ = before->getFieldAmount(sfLPTokenBalance);
}
else if (ammPoolChanged(before))
{
ammPoolChanged_ = true;
}
}
}
I also wonder if ammPoolChanged should be checked for before
. To me it seems like both lsfAMMNode
and sfAMMID
flags can be only set and not removed. And if this is a deletion, we shortcut early. Also I've not found a single occurence of isFieldPresent(sfBalance)
, whole codebase assumes it's always present.
So I assume it's sufficient to check these flags only for after
and not check sfBalance
flag existence (assuming it's always present for both trust line and amm node), thus we could simplify the code further
if (result == tesSUCCESS) | ||
{ |
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'd suggest a shortcut here.
if (result != tesSUCCESS)
return true;
auto const txType = tx.getTxnType(); | ||
if ((txType == ttAMM_DEPOSIT || txType == ttAMM_WITHDRAW || | ||
txType == ttAMM_CLAWBACK || txType == ttAMM_BID) && | ||
ammAccount_) |
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.
ammAccount_ is checked on every if/else statement, maybe take it out? Is that even possible to have a situation where ammAccount_ would not be present? Should it be an assert of some sort?
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 not checked on ttAMM_VOTE
and if it's present in ttPAYMENT
then it's an error. I think in other cases should return false
. I.e., consider it as variant failure.
/** Round AMM single deposit/withdrawal amount. | ||
* The lambda's are used to delay evaluation until the function | ||
* is executed so that the calculation is not done twice. noRoundCb() is | ||
* called if AMMv1_3 is disabled. Otherwise, the rounding is set and | ||
* the amount is: | ||
* isDeposit is Yes - the balance multiplied by productCb() | ||
* isDeposit is No - the result of productCb(). The rounding is | ||
* the same for all calculations in productCb() | ||
*/ | ||
STAmount | ||
getRoundedAsset( | ||
Rules const& rules, | ||
std::function<Number()>&& noRoundCb, | ||
STAmount const& balance, | ||
std::function<Number()>&& productCb, | ||
IsDeposit isDeposit); |
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 convinced that these callbacks are required here. Callback is not even a correct term here.
All those callbacks are either identical or calculable from one another:
auto amtNoRoundCb = [&] { return tokensAdj / ePrice; };
auto amtProdCb = [&] { return tokensAdj / ePrice; };
or
auto tokNoRoundCb = [&] {
return lptAMMBalance * **(lptAMMBalance + ae * (f - 2)) /
(lptAMMBalance * f - ae);**
};
auto tokProdCb = [&] {
return **(lptAMMBalance + ae * (f - 2)) / (lptAMMBalance * f - ae)**;
};
where tokNoRoundCb() == tokProdCb() * lptAMMBalance;
So if we don't want to do calculation twice, we can do that without lambdas relatively easy.
Overall I would suggest revisiting all such callback taking functions. They're probably not needed and could be simplified/refactored into more easy readable code
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 not that straightforward. First, deposit/withdraw logic is different. And I'd rather keep this logic in respective deposit/withdraw function than have a complicated logic in one function. Second, an important thing is expression evaluation order. Let's take tokNoRoundCb
and tokProdCb
. Conceptually tokNoRoundCb = A * B / C
and tokProdCb = B / C
. We could represent tokNoRoundCb = A * tokProdCb
. But A * B / C
is evaluated as (A * B) / C
and we need to keep this evaluation order because it's pre-amendment code. If we change it to A * (B / C) then the result might be different due to the rounding. We must have the same result when this amendment is disabled.
bool | ||
ValidAMM::finalize( | ||
STTx const& tx, | ||
TER const result, | ||
XRPAmount const fee, | ||
ReadView const& view, | ||
beast::Journal const& j) |
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 would like to see unit tests checking this invariant. I understand it's probably going to take a lot of effort thought.
While the current version of the code is definitely much more readable that previous, I would still suggest splitting finalize into a couple of different functions each covering certain scenario (or group of scenarios). That would make writing and reading tests much easier for this invariant
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 is pretty hard to unit-test this invariant. First, the rounding change in this PR has mitigated the invariant failure. There are some unit-tests, which would have failed have it not been for this fix. They don't fail because there is no an invariant check for deposit/withdraw pre-amendment but the error messages are still logged. Second, there is a fudge factor - an allowance for a small error. This makes it really difficult to make this invariant fail. One option is to remove this fudge factor but then there will be deposit/withdraw failures because of some pretty tiny differences and it might not even be an edge case so I'm reluctant to do this.
@gregtatcam I want to bring this commit 1e565e8 in a different PR #5224 to your attention. You might (or might not - up to you) want to copy & alter it to check the |
What is the relation between that and |
pseudo-accounts are used both by Vault and AMM, and this changes how they are created and their invariant (seq = 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.
LGTM
{ | ||
auto authAccount = obj[sfAccount]; | ||
if (authAccount == account || unique.contains(authAccount)) | ||
return temMALFORMED; |
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 codes I suggested are already existing codes so it does not have to be added.
Leaving up to your judgement which one is more suitable
Co-authored-by: Bronek Kozicki <[email protected]>
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #5203 +/- ##
=========================================
+ Coverage 78.8% 78.9% +0.1%
=========================================
Files 817 817
Lines 70782 71071 +289
Branches 8235 8237 +2
=========================================
+ Hits 55791 56077 +286
- Misses 14991 14994 +3
🚀 New features to boost your workflow:
|
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.
Two code suggestions 1. in AMMBid.cpp
and 2. in InvariantCheck.h
(see comment above). I do not feel very strongly about either.
I dislike using static
state for accessing rules, but that boat has sailed a long time ago, nothing to do with this PR.
Unit tests coverage is good at >91% . I see missing lines where you return tecAMM_INVALID_TOKENS
which makes me think these are defensive checks. If I'm mistaken, then you should definitely add unit tests but if I am correct, you won't be able to do so (without white-box testing which we normally do not do), in which case it would be great if you added comments like // LCOV_EXCL_LINE
to these lines.
Some parts of this code would need reformatting, because of recent changes in develop
in .clang-format
. I've found that pip install clang-format==18.1.8
works well for ensuring you always have stable (and correct) version for this purpose.
You will need to fix structured-bindings capture in lambdas error - this should be allowed in C++20 but we still insist on supporting older non-compliant clang versions (until #5369 I guess), so a fix workaround is needed.
More importantly, you need to fix reference-fee-test
, perhaps @vvysokikh1 will have some hints for you.
Overall, no show-stoppers here assuming you fix the obvious issues shown in CI.
Co-authored-by: Bronek Kozicki <[email protected]>
Suppress error logs in some unit-tests.
@XRPLF/rpc-reviewers Any objections to merging this? |
On closer inspection, no RPC-specific code is affected. I'll go ahead and merge. |
High Level Overview of Change
sqrt(asset1Balance * asset2Balance) >= LPTokens
.sqrt(asset1Balance * asset2Balance) > LPTokens
and the pool balances don't change.sqrt(asset1Balance * assetBalance2) == LPTokens
.asset1BalanceAfter * asset2BalanceAfter >= asset1BalanceBefore * asset2BalanceBefore
and
LPTokens
don't change.LPTokens
and pool balances don't change.are withdrawn.
AuthAccount
can't have duplicate accounts or the submitter account.Type of Change
.gitignore
, formatting, dropping support for older tooling)Test Plan
AMM, AMMExtended, AMMClawback, AMMInfo unit-tests are updated