Skip to content
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

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from

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

Comment on lines +173 to +190
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
}

Copy link
Collaborator

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)
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

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.

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?

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

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
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]
4 participants