Skip to content

Conversation

gregtatcam
Copy link
Collaborator

High Level Overview of Change

  • Add AMM bid/create/deposit/swap/withdraw/vote invariants:
    • Deposit, Withdrawal invariants: sqrt(asset1Balance * asset2Balance) >= LPTokens.
    • Bid: sqrt(asset1Balance * asset2Balance) > LPTokens and the pool balances don't change.
    • Create: sqrt(asset1Balance * assetBalance2) == LPTokens.
    • Swap: asset1BalanceAfter * asset2BalanceAfter >= asset1BalanceBefore * asset2BalanceBefore
      and LPTokens don't change.
    • Vote: LPTokens and pool balances don't change.
    • All AMM and swap transactions: amounts and tokens are greater than zero, except on withdrawal if all tokens
      are withdrawn.
  • Add AMM deposit and withdraw rounding to ensure AMM invariant:
    • On deposit, tokens out are rounded downward and deposit amount is rounded upward.
    • On withdrawal, tokens in are rounded upward and withdrawal amount is rounded downward.
  • Add Order Book Offer invariant to verify consumed amounts. Consumed amounts are less than the offer.
  • Fix Bid validation. AuthAccount can't have duplicate accounts or the submitter account.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (non-breaking change that only restructures code)
  • Performance (increase or change in throughput and/or latency)
  • Tests (you added tests for code that already exists, or your new feature included in this PR)
  • Documentation update
  • Chore (no impact to binary, e.g. .gitignore, formatting, dropping support for older tooling)
  • Release

Test Plan

AMM, AMMExtended, AMMClawback, AMMInfo unit-tests are updated

* 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
@vvysokikh1
Copy link
Collaborator

vvysokikh1 commented Feb 6, 2025

@gregtatcam could you please merge latest develop so it would be easier to review? Also build seems to be broken

Copy link
Collaborator

@vvysokikh1 vvysokikh1 left a 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_)
{
Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

@gregtatcam
Copy link
Collaborator Author

@gregtatcam could you please merge latest develop so it would be easier to review? Also build seems to be broken

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)
Copy link
Collaborator

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

Copy link
Collaborator Author

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

Copy link
Collaborator

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.

Copy link
Collaborator

@vvysokikh1 vvysokikh1 Mar 24, 2025

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.

Copy link
Collaborator Author

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.

Comment on lines 130 to 132
bool
isFeatureEnabled(uint256 const& feature);

Copy link
Collaborator

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();
Copy link
Collaborator

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?

Comment on lines 616 to 617
/** Check if feature is enabled
*/
Copy link
Collaborator

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

Comment on lines 1548 to 1581
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);
});
}
Copy link
Collaborator

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

Comment on lines 1607 to 1608
if (result == tesSUCCESS)
{
Copy link
Collaborator

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_)
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

Comment on lines +698 to +713
/** 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);
Copy link
Collaborator

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

Copy link
Collaborator Author

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.

Comment on lines 1597 to 1603
bool
ValidAMM::finalize(
STTx const& tx,
TER const result,
XRPAmount const fee,
ReadView const& view,
beast::Journal const& j)
Copy link
Collaborator

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

Copy link
Collaborator Author

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.

@Bronek
Copy link
Collaborator

Bronek commented Mar 24, 2025

@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 fixAMMv1_3 feature.

@gregtatcam
Copy link
Collaborator Author

@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 fixAMMv1_3 feature.

What is the relation between that and fixAMMv1_3?

@Bronek
Copy link
Collaborator

Bronek commented Mar 25, 2025

@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 fixAMMv1_3 feature.

What is the relation between that and fixAMMv1_3?

pseudo-accounts are used both by Vault and AMM, and this changes how they are created and their invariant (seq = 0)

Copy link
Collaborator

@vvysokikh1 vvysokikh1 left a 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;
Copy link
Collaborator

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

@gregtatcam gregtatcam requested a review from a team as a code owner May 22, 2025 18:23
@Bronek Bronek self-requested a review May 23, 2025 20:35
Copy link

codecov bot commented May 30, 2025

Codecov Report

Attention: Patch coverage is 97.37609% with 9 lines in your changes missing coverage. Please review.

Project coverage is 78.9%. Comparing base (0a34b5c) to head (5310064).
Report is 1 commits behind head on develop.

Files with missing lines Patch % Lines
src/xrpld/app/tx/detail/InvariantCheck.cpp 95.6% 5 Missing ⚠️
src/xrpld/app/tx/detail/AMMBid.cpp 84.6% 2 Missing ⚠️
src/xrpld/app/tx/detail/AMMDeposit.cpp 98.1% 1 Missing ⚠️
src/xrpld/app/tx/detail/AMMWithdraw.cpp 98.1% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            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     
Files with missing lines Coverage Δ
include/xrpl/protocol/IOUAmount.h 100.0% <ø> (ø)
include/xrpl/protocol/Rules.h 100.0% <ø> (ø)
src/libxrpl/protocol/Rules.cpp 98.2% <100.0%> (+0.1%) ⬆️
src/xrpld/app/misc/AMMHelpers.h 85.9% <100.0%> (+0.8%) ⬆️
src/xrpld/app/misc/detail/AMMHelpers.cpp 98.8% <100.0%> (+1.1%) ⬆️
src/xrpld/app/tx/detail/AMMWithdraw.h 100.0% <ø> (ø)
src/xrpld/app/tx/detail/InvariantCheck.h 100.0% <100.0%> (ø)
src/xrpld/app/tx/detail/Offer.h 97.8% <100.0%> (+0.1%) ⬆️
src/xrpld/app/tx/detail/AMMDeposit.cpp 96.4% <98.1%> (+1.2%) ⬆️
src/xrpld/app/tx/detail/AMMWithdraw.cpp 95.3% <98.1%> (-0.9%) ⬇️
... and 2 more

... and 2 files with indirect coverage changes

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Collaborator

@Bronek Bronek left a 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.

@gregtatcam gregtatcam added the Ready to merge *PR author* thinks it's ready to merge. Has passed code review. Perf sign-off may still be required. label Jun 2, 2025
@bthomee
Copy link
Collaborator

bthomee commented Jun 2, 2025

@XRPLF/rpc-reviewers Any objections to merging this?

@bthomee
Copy link
Collaborator

bthomee commented Jun 2, 2025

@XRPLF/rpc-reviewers Any objections to merging this?

On closer inspection, no RPC-specific code is affected. I'll go ahead and merge.

@bthomee bthomee merged commit 621df42 into XRPLF:develop Jun 2, 2025
26 checks passed
This was referenced Jun 12, 2025
@legleux legleux mentioned this pull request Jun 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Amendment Bug Ready to merge *PR author* thinks it's ready to merge. Has passed code review. Perf sign-off may still be required.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AuthAccounts with AMMBid should be unique and !=sfAccount [Version: rippled-2.2.2]

5 participants