-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
base: develop
Are you sure you want to change the base?
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 |
checkInvariant(TAmounts<TIn, TOut> const& consumed, beast::Journal j) const | ||
{ | ||
if (!isFeatureEnabled(fixAMMv1_3)) | ||
return true; | ||
|
||
if (consumed.in > m_amounts.in || consumed.out > m_amounts.out) | ||
{ | ||
// LCOV_EXCL_START | ||
JLOG(j.error()) | ||
<< "AMMOffer::checkInvariant failed: consumed " | ||
<< to_string(consumed.in) << " " << to_string(consumed.out) | ||
<< " amounts " << to_string(m_amounts.in) << " " | ||
<< to_string(m_amounts.out); | ||
|
||
return false; | ||
// LCOV_EXCL_STOP | ||
} | ||
|
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.
Is this a separate invariant from usual ones? Why?
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.
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.
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)?
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?
/** 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?
/** 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
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
@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 |
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