Skip to content

Conversation

Bronek
Copy link
Collaborator

@Bronek Bronek commented Aug 6, 2025

High Level Overview of Change

This change adds Scale to SAV when asset is an IOU, per XRPLF/XRPL-Standards#301

It also removes empty MPToken for vault shares when VaultClawback or VaultWithdraw zeroes the holders shares balance.

Context of Change

SAV shares are always expressed as an integer, since they are MPTs. We need to ensure that this loss of precision does not cause problems when asset has fractional part, like IOUs do.

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

@Bronek Bronek added the DraftRunCI Normally CI does not run on draft PRs. This opts in. label Aug 6, 2025
Copy link

codecov bot commented Aug 7, 2025

Codecov Report

❌ Patch coverage is 97.92531% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.7%. Comparing base (c38f2a3) to head (011c3c6).
⚠️ Report is 1 commits behind head on develop.

Files with missing lines Patch % Lines
src/xrpld/app/tx/detail/VaultCreate.cpp 89.7% 3 Missing ⚠️
src/xrpld/app/tx/detail/VaultClawback.cpp 98.5% 1 Missing ⚠️
src/xrpld/app/tx/detail/VaultDeposit.cpp 97.2% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##           develop   #5652     +/-   ##
=========================================
+ Coverage     78.7%   78.7%   +0.1%     
=========================================
  Files          816     816             
  Lines        71748   71924    +176     
  Branches      8477    8466     -11     
=========================================
+ Hits         56440   56627    +187     
+ Misses       15308   15297     -11     
Files with missing lines Coverage Δ
include/xrpl/basics/Number.h 100.0% <100.0%> (ø)
include/xrpl/protocol/detail/ledger_entries.macro 100.0% <ø> (ø)
include/xrpl/protocol/detail/transactions.macro 100.0% <ø> (ø)
src/xrpld/app/tx/detail/InvariantCheck.cpp 89.2% <100.0%> (+0.1%) ⬆️
src/xrpld/app/tx/detail/VaultDelete.cpp 92.4% <100.0%> (+0.8%) ⬆️
src/xrpld/app/tx/detail/VaultSet.cpp 100.0% <100.0%> (ø)
src/xrpld/app/tx/detail/VaultWithdraw.cpp 96.8% <100.0%> (+2.7%) ⬆️
src/xrpld/ledger/View.h 100.0% <ø> (ø)
src/xrpld/ledger/detail/View.cpp 91.7% <100.0%> (+0.9%) ⬆️
src/xrpld/app/tx/detail/VaultClawback.cpp 95.5% <98.5%> (+3.5%) ⬆️
... and 2 more

... and 4 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.

@Bronek Bronek changed the title Add AssetScale to SingleAssetVault Add ScaleFactor to SingleAssetVault Aug 13, 2025
@Bronek Bronek force-pushed the Bronek/Vault_add_AssetScale branch from 44422a5 to 44b60fe Compare August 15, 2025 15:11
@Bronek Bronek force-pushed the Bronek/Vault_add_AssetScale branch 2 times, most recently from ecfbf80 to 5f54dae Compare August 15, 2025 20:11
@Bronek Bronek force-pushed the Bronek/Vault_add_AssetScale branch from 5f54dae to 97e44c7 Compare August 15, 2025 20:23
@Bronek Bronek force-pushed the Bronek/Vault_add_AssetScale branch from 50a393d to 2047661 Compare August 18, 2025 14:31
@Bronek Bronek changed the title Add ScaleFactor to SingleAssetVault Add Scale to SingleAssetVault Aug 18, 2025
Comment on lines 298 to 304
if (auto const ter = accountSend(
view(),
vaultAccount,
dstAcct,
assetsWithdrawn,
j_,
WaiveTransferFee::Yes))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some other places in this PR uses isTesSuccess to check the result. I think we'd prefer to use this for explicitness and consistency

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, added explicit isTesSuccess(ter) . I also prefer it, only missed in few places, thanks for spotting it !

Comment on lines 252 to 258
if (auto const ter = accountSend(
view(),
holder,
vaultAccount,
sharesDestroyed,
j_,
WaiveTransferFee::Yes))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment on lines 267 to 273
if (auto const ter = accountSend(
view(),
account_,
vaultAccount,
sharesRedeemed,
j_,
WaiveTransferFee::Yes))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

@Bronek Bronek force-pushed the Bronek/Vault_add_AssetScale branch from 7f6e13a to f28ce76 Compare August 28, 2025 11:53
@Bronek Bronek force-pushed the Bronek/Vault_add_AssetScale branch from f28ce76 to d24632d Compare August 28, 2025 11:58
@Bronek Bronek requested a review from ximinez August 28, 2025 13:45
Copy link
Collaborator

@ximinez ximinez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have one suggestion about a log message, but it's so minor that I'm going to approve this whether you add it or not.

return (mantissa_ < 0) ? -1 : (mantissa_ ? 1 : 0);
}

Number
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can simplify or maybe just use combination of towards_zero rounding and static_cast since truncate() usage is limited.

inline Number
Number::truncate() const noexcept
{
    if (exponent_ >= 0 || mantissa_ == 0)
        return *this;

    NumberRoundModeGuard mg(Number::towards_zero);
    return static_cast<rep>(*this);
}

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 am not sure if that would be an improvement, given the complexity of operator rep(). On the other hand, normalize() is also quite complex. However with the current implementation we can guarantee noexcept and I am not sure how this would look like if we used operator rep()

I will add a comment about noexcept and leave it like this for now.

{sfDomainID, soeOPTIONAL},
{sfWithdrawalPolicy, soeOPTIONAL},
{sfData, soeOPTIONAL},
{sfScale, soeOPTIONAL},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be sfShareScale to stress that it applies to the shares?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sfScale is used to similar effect elsewhere already; I think it's better to stick to this name.


// Compute exchange before transferring any amounts.
auto const shares = assetsToSharesDeposit(vault, sleIssuance, assets);
STAmount sharesCreated = {vault->at(sfShareMPTID)}, assetsDeposited;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider defining assetsDeposited on a separate line. I don't think we really do this chained definition. It's also easier to read if defined on a separate line.

auto const share = MPTIssue(mptIssuanceID);
STAmount shares, assets;
if (amount.asset() == asset)
STAmount sharesRedeemed = {vault->at(sfShareMPTID)}, assetsWithdrawn;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider defining assetsWithdraw on a separate line for a better readability and style consistency.


if (sharesRedeemed == beast::zero)
return tecPRECISION_LOSS;
auto const maybeAssets =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because of the truncation/recalculation, which is done on deposit and recalculation here then is it possible that if I deposit A and get S; if I withdraw A then I'll redeem more than S and likewise, if I withdraw S then I'll get less than A? Another case would be also bad; i.e. I withdraw A and redeem less than S and I withdraw S and get more than A. The same concern applies to VaultClawback.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are given an A (number of assets) then we always (i.e. in VaultDeposit VaultClawback VaultWithdraw) do roundtrip calculation : first from assets to shares, then from such rounded shares to assets again. This guarantees that the number of assets always corresponds with the number of shares, bar rounding of assets (for MPT or XRP) which can go either way. This is similar how DEX works when one side is XRP - which also can go either way due to rounding. If the asset is an IOU then there is no rounding of assets (other than epsilon) which is also similar to how DEX works.

assets = sharesToAssetsWithdraw(vault, sleIssuance, shares);
}
else
STAmount sharesDestroyed = {vault->at(sfShareMPTID)}, assetsRecovered;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider defining assetsRecovered on a separate line.

{
if (auto const ter =
removeEmptyHolding(view(), holder, sharesDestroyed.asset(), j_);
isTesSuccess(ter))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't error be returned on failure?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. We are opportunistically trying to remove MPToken in case if its balance is zero. This is expected to fail if the balance is not zero, and we want to ignore this error.

On the other hand, reporting other errors would be useful here. I will add it.

@Bronek Bronek requested a review from gregtatcam September 3, 2025 09:47
Copy link
Collaborator

@gregtatcam gregtatcam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 LGTM

Copy link
Collaborator

@ximinez ximinez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have couple of stylistic-ish nits, but otherwise looks good.

Comment on lines 156 to 157
auto const mptIssuanceID = (*vault)[sfShareMPTID];
auto const sleIssuance = view().read(keylet::mptIssuance(mptIssuanceID));
auto const sleIssuance = view().read(keylet::mptIssuance(*mptIssuanceID));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than using the * operator every time you access mptIssuanceID, you could do it when you do the initial lookup:

Suggested change
auto const mptIssuanceID = (*vault)[sfShareMPTID];
auto const sleIssuance = view().read(keylet::mptIssuance(mptIssuanceID));
auto const sleIssuance = view().read(keylet::mptIssuance(*mptIssuanceID));
auto const mptIssuanceID = *(*vault)[sfShareMPTID];
auto const sleIssuance = view().read(keylet::mptIssuance(mptIssuanceID));

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, implemented in 6893015 (with extra parents for readability)

Comment on lines 289 to 292
if (auto const ter =
removeEmptyHolding(view(), holder, sharesDestroyed.asset(), j_);
isTesSuccess(ter))
auto const ter =
removeEmptyHolding(view(), holder, sharesDestroyed.asset(), j_);
if (isTesSuccess(ter))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even with the else if, ter is only used in the context of the if, so you can put it back.

Suggested change
if (auto const ter =
removeEmptyHolding(view(), holder, sharesDestroyed.asset(), j_);
isTesSuccess(ter))
auto const ter =
removeEmptyHolding(view(), holder, sharesDestroyed.asset(), j_);
if (isTesSuccess(ter))
if (auto const ter =
removeEmptyHolding(view(), holder, sharesDestroyed.asset(), j_);
isTesSuccess(ter))

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, implemented in 6893015

Comment on lines 180 to 181
auto const mptIssuanceID = (*vault)[sfShareMPTID];
auto const sleIssuance = view().read(keylet::mptIssuance(mptIssuanceID));
auto const sleIssuance = view().read(keylet::mptIssuance(*mptIssuanceID));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as in VaultClawback, mptIssuanceID can be declared with the *.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, implemented in 6893015 (with extra parents for readability)

Comment on lines 296 to 298
auto const ter =
removeEmptyHolding(view(), account_, sharesRedeemed.asset(), j_);
if (isTesSuccess(ter))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as in VaultClawback, ter can be put back into the if statement.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, implemented in 6893015

@Bronek Bronek requested a review from ximinez September 3, 2025 16:33
Copy link
Collaborator

@ximinez ximinez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm realizing there's a lot of duplicated code between VaultClawback and VaultWithdraw that can be refactored into helper functions, but that can be done later.

@Bronek Bronek enabled auto-merge (squash) September 3, 2025 17:16
@Bronek Bronek 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 Sep 3, 2025
@Bronek Bronek merged commit cf5f65b into develop Sep 4, 2025
67 of 70 checks passed
@Bronek Bronek deleted the Bronek/Vault_add_AssetScale branch September 4, 2025 08:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
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.

5 participants