Compare commits

..
Author SHA1 Message Date
Mayukha Vadari 91985dc320 perf: Use memcmp for BaseUInt comparisons
operator<=> used std::mismatch over unsigned char iterators, which
compares one byte at a time, and operator== went through operator<=>.
The data is stored big-endian, so memcmp gives the same order and is
much faster. These operators run on every UInt256, AccountID and
Currency key compare in maps, hash tables and caches.

The operators could never be constant-evaluated, because data() uses
reinterpret_cast, so memcmp loses nothing.
2026-10-14 12:58:27 -04:00
37 changed files with 89 additions and 1015 deletions

No files matched your search

+17 -11
View File
@@ -4,18 +4,24 @@ This file provides guidance to AI coding agents (Claude Code, and other AGENTS.m
## Build
For the build steps, CMake options, and protocol codegen commands, see [BUILD.md](./BUILD.md) (`## Steps`, `## Options`, `## Code generation`). Nix development shells are available in the repo (not required) — see [docs/build/nix.md](./docs/build/nix.md) for setup and variants.
Required on Linux/macOS: use the Nix devshell, which sets up the compiler, Conan, ccache, and (optionally) Rust automatically.
```bash
nix develop
```
For alternate devshell variants (specific compiler, no-compiler, coverage), see [docs/build/nix.md](./docs/build/nix.md). For the manual build steps, CMake options, and protocol codegen commands, see [BUILD.md](./BUILD.md) (`## Steps`, `## Options`, `## Code generation`).
Rust crate tests (independent of the CMake build): `cargo test --manifest-path crates/Cargo.toml --workspace` (CI uses `cargo nextest`).
## Testing
The legacy unit tests under `src/test/` are a custom framework (derived from Beast, not Boost.Test/GTest/Catch) built into the `xrpld` binary; see [CONTRIBUTING.md](./CONTRIBUTING.md#unit-tests) for the basic invocation. The gtest suites under `src/tests/` build into the separate `xrpl_tests` and `xrpld_tests` binaries (run with `--gtest_filter=...`). Notes not covered there:
Unit tests are a custom framework built into the `xrpld` binary itself (not Boost.Test/GTest/Catch); see [CONTRIBUTING.md](./CONTRIBUTING.md#unit-tests) for the basic invocation. Notes not covered there:
- A suite's `--unittest` name is built from the arguments to its `BEAST_DEFINE_TESTSUITE`/`BEAST_DEFINE_TESTSUITE_PRIO` macro (usually at the bottom of the test file), in reverse order and joined with `.`: `BEAST_DEFINE_TESTSUITE(Credentials, app, xrpl)` → `xrpl.app.Credentials`.
- `--unittest-arg=<string>` passes one string to every suite that runs; a suite reads it with `arg()` and interprets it itself. Only a few suites use it (e.g. `TheoreticalQuality`, the nodestore `import` test); for the rest it has no effect.
- `--unittest-arg` does nothing — don't use it.
- Tests that run offline in under a minute should be automatic `--unittest` suites; anything else is a manual/integration test.
- New tests should be written using `gtest` under `src/tests/` unless that isn't possible, in which case fall back to the legacy Beast framework under `src/test/` (see [src/test/AGENTS.md](./src/test/AGENTS.md) for conventions specific to that directory). `tests/` (top-level) holds integration tests exercised against `libxrpl`/`xrpld`.
- New tests should be written using `gtest` under `src/tests/` unless that isn't possible, in which case fall back to the legacy Beast framework under `src/test/`. `tests/` (top-level) holds integration tests exercised against `libxrpl`/`xrpld`.
## Lint/Format
@@ -23,14 +29,14 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md#pre-commit-hooks) for `pre-commit` setup
## Code Style
New file placement and header levelization: see [CONTRIBUTING.md](./CONTRIBUTING.md#before-making-a-pull-request). Braces, whitespace, member order, and other conventions: see [docs/CodingStyle.md](./docs/CodingStyle.md). `XRPL_ASSERT`/`UNREACHABLE` contracts: see [CONTRIBUTING.md](./CONTRIBUTING.md#contracts-and-instrumentation). Commit messages: see [CONTRIBUTING.md](./CONTRIBUTING.md#good-commit-messages). New public functions/methods need a Doxygen-style comment.
Comments should explain _why_, not _what_/_how_ — the code already shows that. Only describe what/how when the code itself would otherwise be confusing (a non-obvious workaround, a subtle invariant, a surprising constraint).
New file placement and header levelization: see [CONTRIBUTING.md](./CONTRIBUTING.md#before-making-a-pull-request). Braces, whitespace, member order, and other conventions: see [docs/CodingStyle.md](./docs/CodingStyle.md). `XRPL_ASSERT`/`UNREACHABLE` contracts: see [CONTRIBUTING.md](./CONTRIBUTING.md#contracts-and-instrumentation). Commit messages: see [CONTRIBUTING.md](./CONTRIBUTING.md#good-commit-messages).
## Architecture
See [ARCHITECTURE.md](./ARCHITECTURE.md) for the directory-by-directory map of the codebase.
Paths below reflect the current layout; update this section if modularization moves a subsystem to a different directory.
## Keeping docs current
When you add or change a convention, or touch a subsystem that has its own `AGENTS.md`, `README.md`, or `ARCHITECTURE.md`, update that documentation in the same change rather than leaving it stale.
- `include/xrpl/` + `src/libxrpl/` — the core protocol library: ledger, shamap, consensus, crypto, json, resource, nodestore, rdb, peerfinder, and `tx/` (transaction application: `Transactor.cpp`, `applySteps.cpp`, invariants, payment paths). `tx/transactors/` has one file per transaction type, grouped by subsystem: `escrow/`, `vault/`, `lending/`, `sponsor/`, `nft/`, `token/` (MPT), `payment_channel/`, `permissioned_domain/`, `dex/`, `oracle/`, `did/`, `credentials/`, `bridge/`, `check/`, `delegate/`, `account/`, `system/`. Any change to transaction-processing behavior must be gated behind an Amendment.
- `src/xrpld/` — the server application built on top of `libxrpl`: `app`, `core`, `overlay` (P2P networking), `peerfinder`, `perflog`, `rpc`, `shamap`. `main` builds an `ApplicationImp` implementing `Application`; most components hold a reference to it (`app_`), giving broad cross-component access — expect to trace call chains through `Application&`.
- `src/test/` — unit tests mirroring the subsystems above, plus `jtx/` (the transaction-building test DSL — e.g. `jtx/escrow.h`, `jtx/vault.h`, `jtx/sponsor.h`, `jtx/permissioned_dex.h`) and `unit_test/` (the custom test framework itself, derived from Beast).
- `src/tests/` — unit tests for `libxrpl` written in `gtest`, gradually replacing the `src/test` equivalents.
- `crates/` — a Rust workspace (only built with `-Dxrpld -Drust=ON`) bridged into C++ via `cxxbridge`/the `cxx` crate; currently just a `hello_world` interop scaffold. Requires the Rust toolchain pinned in `rust-toolchain.toml` (the Nix devshell provides it automatically).
-9
View File
@@ -1,9 +0,0 @@
# Architecture
Paths below reflect the current layout; update this doc if modularization moves a subsystem to a different directory.
- `include/xrpl/` + `src/libxrpl/` — the core protocol library: ledger, shamap, consensus, crypto, json, resource, nodestore, rdb, peerfinder, and `tx/` (transaction application: `Transactor.cpp`, `applySteps.cpp`, invariants, payment paths — see [src/libxrpl/tx/AGENTS.md](./src/libxrpl/tx/AGENTS.md) for amendment-gating conventions). `tx/transactors/` has one file per transaction type, grouped by subsystem: `payment/`, `escrow/`, `vault/`, `lending/`, `sponsor/`, `nft/`, `token/` (MPT), `payment_channel/`, `permissioned_domain/`, `dex/`, `oracle/`, `did/`, `credentials/`, `bridge/`, `check/`, `delegate/`, `proposal/`, `account/`, `system/`.
- `src/xrpld/` — the server application built on top of `libxrpl`: `app`, `core`, `overlay` (P2P networking), `peerfinder`, `perflog`, `rpc`, `shamap`. `main` builds an `ApplicationImp` implementing `Application`; most components hold a reference to it (`app_`), giving broad cross-component access — expect to trace call chains through `Application&`.
- `src/test/` — unit tests mirroring the subsystems above, plus `jtx/` (the transaction-building test DSL — e.g. `jtx/escrow.h`, `jtx/vault.h`, `jtx/sponsor.h`, `jtx/permissioned_dex.h`) and `unit_test/` (the custom test framework itself, derived from Beast).
- `src/tests/` — gtest unit tests, with `libxrpl/` and `xrpld/` subtrees, gradually replacing the `src/test` equivalents.
- `crates/` — a Rust workspace (only built with `-Drust=ON`; today only `xrpl_tests` links it, so `-Dtests=ON` is needed to exercise it) bridged into C++ via `cxxbridge`/the `cxx` crate; currently just a `hello_world` interop scaffold. Requires the Rust toolchain pinned in `rust-toolchain.toml` (the Nix devshell provides it automatically).
+4 -11
View File
@@ -596,23 +596,16 @@ operator<=>(BaseUInt<Bits, Tag> const& lhs, BaseUInt<Bits, Tag> const& rhs)
// note that the underlying data is stored in big endian, even if the
// platform is little endian. This makes the comparison correct.
//
// FIXME: use std::lexicographical_compare_three_way once support is
// added to MacOS.
auto const ret = std::mismatch(lhs.cbegin(), lhs.cend(), rhs.cbegin());
// a == b
if (ret.first == lhs.cend())
return std::strong_ordering::equivalent;
return (*ret.first > *ret.second) ? std::strong_ordering::greater : std::strong_ordering::less;
// memcmp compares unsigned bytes in order, and is much faster than a
// byte-at-a-time loop.
return std::memcmp(lhs.data(), rhs.data(), lhs.size()) <=> 0;
}
template <std::size_t Bits, typename Tag>
[[nodiscard]] constexpr bool
operator==(BaseUInt<Bits, Tag> const& lhs, BaseUInt<Bits, Tag> const& rhs)
{
return (lhs <=> rhs) == 0; // NOLINT(modernize-use-nullptr)
return std::memcmp(lhs.data(), rhs.data(), lhs.size()) == 0;
}
//------------------------------------------------------------------------------
-3
View File
@@ -1,3 +0,0 @@
# AGENTS.md — consensus
See [README.md](./README.md) for a short pointer, and [docs/consensus.md](../../../docs/consensus.md) for the full consensus design.
-1
View File
@@ -1 +0,0 @@
AGENTS.md
+1 -9
View File
@@ -265,13 +265,6 @@ canWithdraw(
[[nodiscard]] TER
canWithdraw(ReadView const& view, STTx const& tx);
/**
* Pays out a withdrawal from a vault or loan broker pseudo-account.
*
* `sourceAmount` leaves `sourceAcct` and `destinationAmount` reaches `dstAcct`.
* The two differ only when the withdrawal carries a transfer fee; the
* difference is the fee and is settled through the issuer.
*/
[[nodiscard]] TER
doWithdraw(
ApplyViewContext ctx,
@@ -279,8 +272,7 @@ doWithdraw(
AccountID const& dstAcct,
AccountID const& sourceAcct,
XRPAmount priorBalance,
STAmount const& destinationAmount,
STAmount const& sourceAmount,
STAmount const& amount,
beast::Journal j);
/**
@@ -316,20 +316,6 @@ transferRate(ReadView const& view, Asset const& asset);
[[nodiscard]] Rate
transferRate(ReadView const& view, STAmount const& amount);
/**
* Returns the amount a sender must part with for `amount` to reach the
* receiver once the issuer takes the transfer fee. Rounds up.
*/
[[nodiscard]] STAmount
addTransferFee(STAmount const& amount, Rate const& rate);
/**
* Returns the amount a receiver gets when `amount` leaves the sender and the
* issuer takes the transfer fee. Rounds down.
*/
[[nodiscard]] STAmount
subtractTransferFee(STAmount const& amount, Rate const& rate);
//------------------------------------------------------------------------------
//
// Holding operations (Asset-based dispatchers)
-3
View File
@@ -1,3 +0,0 @@
# AGENTS.md — nodestore
See [README.md](./README.md) for backend and benchmark design.
-1
View File
@@ -1 +0,0 @@
AGENTS.md
-10
View File
@@ -696,16 +696,6 @@ divRound(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundU
STAmount
divRoundStrict(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundUp);
/**
* Returns amt * num / den, rounded in the requested direction.
*
* Dispatches to the XRPAmount, IOUAmount or MPTAmount overload. XRP and MPT
* results are exact; an IOU result is rounded to the 16-digit mantissa in the
* requested direction. Only Upward and Downward are supported.
*/
[[nodiscard]] STAmount
mulRatio(STAmount const& amt, std::uint32_t num, std::uint32_t den, Number::RoundingMode rounding);
// Someone is offering X for Y, what is the rate?
// Rate: smaller is better, the taker wants the most out: in/out
// VFALCO TODO Return a Quality object
-3
View File
@@ -1,3 +0,0 @@
# AGENTS.md — shamap
See [README.md](./README.md) for the SHAMap design.
-1
View File
@@ -1 +0,0 @@
AGENTS.md
+7 -29
View File
@@ -511,14 +511,9 @@ doWithdraw(
AccountID const& dstAcct,
AccountID const& sourceAcct,
XRPAmount priorBalance,
STAmount const& destinationAmount,
STAmount const& sourceAmount,
STAmount const& amount,
beast::Journal j)
{
XRPL_ASSERT(
destinationAmount.asset() == sourceAmount.asset(),
"xrpl::doWithdraw : delivered and source amounts use the same asset");
auto const dstSle = ctx.view.read(keylet::account(dstAcct));
// Create a trust line or MPToken for a self-destination only when there
@@ -528,10 +523,9 @@ doWithdraw(
// create+delete MPTokens in the same transaction.
if (dstAcct == senderAcct)
{
if (destinationAmount > beast::kZero || !ctx.view.rules().enabled(fixCleanup3_4_0))
if (amount > beast::kZero || !ctx.view.rules().enabled(fixCleanup3_4_0))
{
if (auto const ter =
addEmptyHolding(ctx, senderAcct, priorBalance, destinationAmount.asset(), j);
if (auto const ter = addEmptyHolding(ctx, senderAcct, priorBalance, amount.asset(), j);
!isTesSuccess(ter) && ter != tecDUPLICATE)
return ter;
}
@@ -546,33 +540,17 @@ doWithdraw(
if (accountHolds(
ctx.view,
sourceAcct,
destinationAmount.asset(),
amount.asset(),
FreezeHandling::IgnoreFreeze,
AuthHandling::IgnoreAuth,
j) < sourceAmount)
j) < amount)
{
// LCOV_EXCL_START
JLOG(j.error()) << "doWithdraw: source account holds fewer assets than the withdrawal.";
JLOG(j.error()) << "doWithdraw: negative balance of broker cover assets.";
return tefINTERNAL;
// LCOV_EXCL_STOP
}
// A transfer fee applies. Redeem the gross amount with the issuer, then
// issue the net amount to the destination.
if (sourceAmount != destinationAmount)
{
auto const issuer = destinationAmount.getIssuer();
XRPL_ASSERT(
!destinationAmount.native() && sourceAcct != issuer && dstAcct != issuer,
"xrpl::doWithdraw : transfer fee applies between token holders");
if (auto const ter = directSendNoFee(ctx.view, sourceAcct, issuer, sourceAmount, false, j);
!isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
return directSendNoFee(ctx.view, issuer, dstAcct, destinationAmount, false, j);
}
// A reserve sponsor only covers tx.Account's own objects, so resolve the
// sponsor against the destination. accountSend can auto-create a holding
// for dstAcct; keying on the destination ensures a third-party destination's
@@ -584,7 +562,7 @@ doWithdraw(
// Move the funds directly from the broker's pseudo-account to the
// dstAcct
return accountSend(
ctx.view, sourceAcct, dstAcct, destinationAmount, j, *sponsorSle, WaiveTransferFee::Yes);
ctx.view, sourceAcct, dstAcct, amount, j, *sponsorSle, WaiveTransferFee::Yes);
}
TER
-7
View File
@@ -1,7 +0,0 @@
# AGENTS.md — ledger/helpers
A helper that takes an `SLE`/`std::shared_ptr<SLE const>` should `XRPL_ASSERT` that it's non-null and of the expected ledger-entry type at entry, and keep a real runtime check/error-return alongside the assert (asserts compile out in release builds) — the established idiom in this directory is `std::expected<..., TER>`, returning `std::unexpected(tec*)` on failure. Don't invent a new error-handling idiom for this.
Prefer a single amendment-enabled block and a single disabled block over scattering `rules.enabled(...)` checks through a function, even if the two blocks are similar. When a file or function checks more than one amendment, name local enablement booleans per-amendment (e.g. `fix340Enabled` for `fixCleanup3_4_0`), not a generic `fixEnabled`.
Only use `UNREACHABLE` for genuinely impossible paths, not to avoid writing a test for one that's reachable but rare. When a branch marked `UNREACHABLE` is excluded from coverage, wrap it in `LCOV_EXCL_START`/`LCOV_EXCL_STOP`.
-1
View File
@@ -1 +0,0 @@
AGENTS.md
+6 -21
View File
@@ -2,7 +2,6 @@
#include <xrpl/basics/Log.h>
#include <xrpl/basics/MathUtilities.h>
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
@@ -552,22 +551,6 @@ transferRate(ReadView const& view, STAmount const& amount)
return transferRate(view, amount.asset());
}
STAmount
addTransferFee(STAmount const& amount, Rate const& rate)
{
if (rate == kParityRate)
return amount;
return mulRatio(amount, rate.value, QUALITY_ONE, Number::RoundingMode::Upward);
}
STAmount
subtractTransferFee(STAmount const& amount, Rate const& rate)
{
if (rate == kParityRate)
return amount;
return mulRatio(amount, QUALITY_ONE, rate.value, Number::RoundingMode::Downward);
}
//------------------------------------------------------------------------------
//
// Holding operations
@@ -1364,8 +1347,9 @@ directSendNoLimitMPT(
{
// Number math loses precision on large MPT amounts, which can
// overcharge the sender. MPTs are integral, so compute the cost
// exactly, matching the payment engine.
saActual = addTransferFee(saAmount, rate);
// exactly and round it up, matching the payment engine.
auto const cost = mulRatio(saAmount.mpt(), rate.value, QUALITY_ONE, true);
saActual = STAmount(saAmount.asset(), cost.value());
}
else
{
@@ -1490,8 +1474,9 @@ directSendNoLimitMultiMPT(
{
// Number math loses precision on large MPT amounts, which can
// overcharge the sender. MPTs are integral, so compute the
// cost exactly, matching the payment engine.
actualSend = addTransferFee(amount, rate);
// cost exactly and round it up, matching the payment engine.
auto const cost = mulRatio(amount.mpt(), rate.value, QUALITY_ONE, true);
actualSend = STAmount(amount.asset(), cost.value());
}
else
{
-18
View File
@@ -1763,24 +1763,6 @@ divRoundStrict(STAmount const& num, STAmount const& den, Asset const& asset, boo
return divRoundImpl<NumberRoundModeGuard>(num, den, asset, roundUp);
}
STAmount
mulRatio(STAmount const& amt, std::uint32_t num, std::uint32_t den, Number::RoundingMode rounding)
{
XRPL_ASSERT(
rounding == Number::RoundingMode::Upward || rounding == Number::RoundingMode::Downward,
"xrpl::mulRatio(STAmount) : rounding is Upward or Downward");
bool const roundUp = rounding == Number::RoundingMode::Upward;
return amt.asset().visit(
[&](Issue const& issue) -> STAmount {
if (amt.native())
return mulRatio(amt.xrp(), num, den, roundUp);
return {mulRatio(amt.iou(), num, den, roundUp), issue};
},
[&](MPTIssue const& issue) -> STAmount {
return {mulRatio(amt.mpt(), num, den, roundUp), issue};
});
}
[[nodiscard]] bool
STAmount::isZeroAtScale(int scale) const
{
+2 -12
View File
@@ -1,15 +1,5 @@
# AGENTS.md — tx
## When an amendment is required
See the repo-level [AGENTS.md](../../../AGENTS.md) for general build/test/style guidance.
A change needs an amendment if it affects transaction processing, ledger objects, or anything else about the binary format or hash of the ledger. An amendment is optional if a change only affects what transactions get proposed for consensus (e.g. fee escalation). Otherwise, don't use one.
New amendments (and fixes, i.e. `fix*` amendments) are added to [`include/xrpl/protocol/detail/features.macro`](../../../include/xrpl/protocol/detail/features.macro), as an `XRPL_FEATURE(...)` or `XRPL_FIX(...)` entry added to the top of the list (the list is kept in reverse chronological order). Once the pre-amendment code path for a retired amendment is removed, move its entry to `XRPL_RETIRE_FEATURE(...)`/`XRPL_RETIRE_FIX(...)` instead of deleting it.
When adding a new amendment or transaction type, check its interaction with: invariants, fees, Deposit Auth, Batch transaction inclusion/exclusion, Permission Delegation inclusion/exclusion, Freeze/Deep Freeze (IOU) and Lock (MPT), Clawback, Credentials and Permissioned Domain, the case where the submitting account is the asset's issuer, and numeric over/underflow. Stick to existing paradigms rather than inventing new ones — consistency between features matters more than a locally "better" design.
New (or deleted) invariant checks must be amendment-gated: they introduce (or remove) a way for a transaction to fail, and an un-gated change risks validators disagreeing on a transaction's result, i.e. a network fork.
See [transactors/AGENTS.md](./transactors/AGENTS.md) for conventions on writing the amendment-gated code itself.
A change to transaction/signing behavior that's visible through the public API also needs an `API-CHANGELOG.md` entry — see [../../xrpld/rpc/AGENTS.md](../../xrpld/rpc/AGENTS.md) for the full rule.
Any change to transaction-processing behavior must be gated behind an amendment. New amendments (and fixes, i.e. `fix*` amendments) are added to [`include/xrpl/protocol/detail/features.macro`](../../../include/xrpl/protocol/detail/features.macro), as an `XRPL_FEATURE(...)` or `XRPL_FIX(...)` entry added to the top of the list (the list is kept in reverse chronological order). Once the pre-amendment code path for a retired amendment is removed, move its entry to `XRPL_RETIRE_FEATURE(...)`/`XRPL_RETIRE_FIX(...)` instead of deleting it.
+3 -19
View File
@@ -7,14 +7,12 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
@@ -487,7 +485,6 @@ ValidVault::finalize(
{
bool const enforce = view.rules().enabled(featureSingleAssetVault);
bool const fix340Enabled = view.rules().enabled(fixCleanup3_4_0);
bool const fix350Enabled = view.rules().enabled(fixCleanup3_5_0);
if (!isTesSuccess(ret))
return true; // Do not perform checks
@@ -1298,18 +1295,6 @@ ValidVault::finalize(
auto const localPseudoDeltaAssets =
roundToAsset(vaultAsset, vaultPseudoDeltaAssets, localMinScale);
auto const rate = transferRate(view, vaultAsset);
bool const feeAdjustedWithdrawal = fix350Enabled && distinctDestination &&
!vaultAsset.native() && tx[sfAccount] != vaultAsset.getIssuer() &&
rate != kParityRate;
auto expectedDestinationDelta = localPseudoDeltaAssets * -1;
if (feeAdjustedWithdrawal)
{
expectedDestinationDelta =
subtractTransferFee(
STAmount{vaultAsset, expectedDestinationDelta}, rate)
.value();
}
// For IOU assets near a precision boundary the destination's STAmount
// exponent can shift, making part of the sent value unrepresentable at
// the receiver's new scale — that portion is irreversibly absorbed by the
@@ -1318,8 +1303,7 @@ ValidVault::finalize(
// the destination's scale. Floor rounding is used so that values exactly
// at the step boundary are not mistakenly dismissed. Any representable
// discrepancy indicates a real accounting bug and must be caught.
auto const destroyedIsSubUlp = !feeAdjustedWithdrawal &&
tolerateZeroDelta &&
auto const destroyedIsSubUlp = tolerateZeroDelta &&
roundToAsset(
vaultAsset,
vaultDeltaAssets.delta * -1 - destinationDelta.delta,
@@ -1327,11 +1311,11 @@ ValidVault::finalize(
Number::RoundingMode::Downward) == kZero;
bool const withdrawAddsUp = fix340Enabled
? agreesWithinOneUnit(
expectedDestinationDelta,
localPseudoDeltaAssets * -1,
roundedDestinationDelta,
vaultAsset,
localMinScale)
: expectedDestinationDelta == roundedDestinationDelta;
: localPseudoDeltaAssets * -1 == roundedDestinationDelta;
if (!destroyedIsSubUlp && !withdrawAddsUp)
{
JLOG(j.fatal()) << "Invariant failed: " << //
-13
View File
@@ -1,13 +0,0 @@
# AGENTS.md — transactors
Prefer a single object-level invariant over duplicating the same delta/balance check in every transactor that touches an object — e.g. one invariant asserting a Vault's pseudo-account balance and `assetsAvailable` always move together, rather than repeating that check in `VaultDeposit`, `VaultWithdraw`, `VaultClawback`, `LoanSet`, etc.
## Gating amendment-dependent code
Prefer a single amendment-enabled block and a single disabled block over scattering `rules.enabled(...)` checks through a function, even if the two blocks are similar.
When a file or function checks more than one amendment, name local enablement booleans per-amendment (e.g. `fix340Enabled` for `fixCleanup3_4_0`), not a generic `fixEnabled` — it becomes ambiguous once a second amendment is checked in the same scope.
## `UNREACHABLE` and test coverage
Only use `UNREACHABLE` for genuinely impossible paths, not to avoid writing a test for one that's reachable but rare. When a branch marked `UNREACHABLE` is excluded from coverage, wrap it in `LCOV_EXCL_START`/`LCOV_EXCL_STOP`.
-1
View File
@@ -1 +0,0 @@
AGENTS.md
@@ -270,7 +270,6 @@ LoanBrokerCoverWithdraw::doApply()
brokerPseudoID,
preFeeBalance_,
amount,
amount,
j_);
}
@@ -12,15 +12,12 @@
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h> // IWYU pragma: keep
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
#include <xrpl/protocol/STTakesAsset.h>
@@ -52,26 +49,6 @@ shouldWaiveWithdrawal(ReadView const& view, AccountID const& account, SLE::Const
: WaiveUnrealizedLoss::No;
}
static Rate
withdrawalTransferRate(
ReadView const& view,
AccountID const& account,
AccountID const& destination,
Asset const& asset)
{
// Pre-fixCleanup3_5_0: every vault withdrawal waives the transfer fee.
// Post-fixCleanup3_5_0: the fee applies only when a holder other than the issuer withdraws to
// another such holder.
if (!view.rules().enabled(fixCleanup3_5_0))
return kParityRate;
if (asset.native() || destination == account || account == asset.getIssuer() ||
destination == asset.getIssuer())
return kParityRate;
return transferRate(view, asset);
}
NotTEC
VaultWithdraw::preflight(PreflightContext const& ctx)
{
@@ -193,13 +170,11 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
if (!maybeAssets)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const rate = withdrawalTransferRate(ctx.view, account, dstAcct, vaultAsset);
auto const amountToReceive = subtractTransferFee(*maybeAssets, rate);
if (auto const ret = canWithdraw(
ctx.view,
account,
dstAcct,
amountToReceive,
*maybeAssets,
ctx.tx.isFieldPresent(sfDestinationTag),
ctx.tx[~sfCredentialIDs]))
return ret;
@@ -327,8 +302,6 @@ VaultWithdraw::doApply()
auto const amount = ctx_.tx[sfAmount];
Asset const vaultAsset = vault->at(sfAsset);
auto const dstAcct = ctx_.tx[~sfDestination].value_or(accountID_);
auto const rate = withdrawalTransferRate(view(), accountID_, dstAcct, vaultAsset);
MPTIssue const share{mptIssuanceID};
STAmount sharesRedeemed = {share};
@@ -357,11 +330,7 @@ VaultWithdraw::doApply()
view().rules().enabled(fixCleanup3_4_0) ? TruncateShares::Yes : TruncateShares::No;
{
auto const maybeShares = assetsToSharesWithdraw(
vault,
sleIssuance,
addTransferFee(amount, rate),
truncate,
waiveUnrealizedLoss);
vault, sleIssuance, amount, truncate, waiveUnrealizedLoss);
if (!maybeShares)
return tecINTERNAL; // LCOV_EXCL_LINE
sharesRedeemed = *maybeShares;
@@ -554,13 +523,6 @@ VaultWithdraw::doApply()
assetsWithdrawn = allAvailable;
}
auto const assetsDelivered = subtractTransferFee(assetsWithdrawn, rate);
if (assetsWithdrawn > beast::kZero && assetsDelivered == beast::kZero)
{
JLOG(j_.debug()) << "VaultWithdraw: transfer fee reduces the payout to zero";
return tecPRECISION_LOSS;
}
if (finalPayout && getVaultVersion(vault) != VaultVersion::FixedPrecision)
{
// Do not let dust accumulate in the Vault.
@@ -611,15 +573,9 @@ VaultWithdraw::doApply()
associateAsset(*vault, vaultAsset);
auto const dstAcct = ctx_.tx[~sfDestination].value_or(accountID_);
return doWithdraw(
applyViewContext,
accountID_,
dstAcct,
vaultAccount,
preFeeBalance_,
assetsDelivered,
assetsWithdrawn,
j_);
applyViewContext, accountID_, dstAcct, vaultAccount, preFeeBalance_, assetsWithdrawn, j_);
}
void
-5
View File
@@ -1,5 +0,0 @@
# AGENTS.md — test
See [README.md](./README.md) for basic `--unittest` invocation.
Shared test setup/helper code used by more than one test file belongs in `jtx/`, not copy-pasted across test files.
-1
View File
@@ -1 +0,0 @@
AGENTS.md
+43 -611
View File
@@ -50,39 +50,6 @@ namespace xrpl {
class VaultLifecycle_test : public VaultTestBase
{
private:
struct FundedVault
{
Keylet keylet;
test::jtx::Account account;
test::jtx::PrettyAsset shares;
};
/**
* Creates a vault owned by `owner` and deposits `amount` into it from `depositor`.
*/
static FundedVault
createFundedVault(
test::jtx::Env& env,
test::jtx::Vault& vault,
test::jtx::Account const& owner,
test::jtx::Account const& depositor,
STAmount const& amount)
{
auto [tx, keylet] = vault.create({.owner = owner, .asset = amount.asset()});
env(tx);
env.close();
env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = amount}));
env.close();
auto const sle = env.le(keylet);
test::jtx::Account const account{"vault", sle->at(sfAccount)};
env.memoize(account);
return {
.keylet = keylet,
.account = account,
.shares = test::jtx::PrettyAsset{MPTIssue{sle->at(sfShareMPTID)}},
};
}
void
testSequences()
{
@@ -606,7 +573,6 @@ private:
bool enableClawback = true;
bool requireAuth = true;
int initialXRP = 1000;
std::uint16_t transferFee = 0;
FeatureBitset features = testableAmendments();
};
@@ -631,7 +597,6 @@ private:
MPTTester mptt{env, issuer, kMptInitNoFund};
auto const kNone = LedgerSpecificFlags(0);
mptt.create({
.transferFee = args.transferFee,
.flags = tfMPTCanTransfer | tfMPTCanLock |
(args.enableClawback ? tfMPTCanClawback : kNone) |
(args.requireAuth ? tfMPTRequireAuth : kNone),
@@ -651,324 +616,6 @@ private:
test(env, issuer, owner, depositor, asset, vault, mptt);
};
// The MPT charges a 25% transfer fee, so 100 gross delivers 80 net.
auto const feeArgs = CaseArgs{.transferFee = 25'000};
auto const testTransferFeeGate = [&](FeatureBitset const& features) {
testCase(
[this, features](
Env& env,
Account const&,
Account const& owner,
Account const& depositor,
PrettyAsset const& asset,
Vault& vault,
MPTTester& mptt) {
bool const feeCharged = features[fixCleanup3_5_0];
testcase(
feeCharged ? "MPT transfer fee on third-party withdrawal"
: "MPT transfer fee waived pre-fixCleanup3_5_0");
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
auto tx = vault.withdraw({
.depositor = depositor,
.id = funded.keylet.key,
.amount = funded.shares(100),
});
tx[sfDestination] = owner.human();
env(tx);
env.close();
auto const expected = feeCharged ? 80 : 100;
BEAST_EXPECT(mptt.checkMPTokenAmount(depositor, 900));
BEAST_EXPECT(mptt.checkMPTokenAmount(owner, expected));
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(900 + expected));
BEAST_EXPECT(
env.balance(funded.account, asset.raw().get<MPTIssue>()) == asset(0));
env(vault.del({.owner = owner, .id = funded.keylet.key}));
env.close();
},
CaseArgs{.transferFee = 25'000, .features = features});
};
testTransferFeeGate(testableAmendments() - fixCleanup3_5_0);
testTransferFeeGate(testableAmendments());
testCase(
[this](
Env& env,
Account const&,
Account const& owner,
Account const& depositor,
PrettyAsset const& asset,
Vault& vault,
MPTTester& mptt) {
testcase("MPT transfer fee on fixed-asset third-party withdrawal");
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
auto const mptIssue = asset.raw().get<MPTIssue>();
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
auto tx = vault.withdraw(
{.depositor = depositor, .id = funded.keylet.key, .amount = asset(40)});
tx[sfDestination] = owner.human();
env(tx);
env.close();
// The destination receives the requested 40; the gross 50 leaves the vault.
BEAST_EXPECT(mptt.checkMPTokenAmount(owner, 40));
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(50));
BEAST_EXPECT(env.balance(depositor, shareIssue) == funded.shares(50));
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(990));
},
feeArgs);
testCase(
[this](
Env& env,
Account const&,
Account const& owner,
Account const& depositor,
PrettyAsset const& asset,
Vault& vault,
MPTTester& mptt) {
testcase("MPT transfer fee on fixed-share third-party withdrawal");
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
auto const mptIssue = asset.raw().get<MPTIssue>();
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
auto tx = vault.withdraw(
{.depositor = depositor, .id = funded.keylet.key, .amount = funded.shares(25)});
tx[sfDestination] = owner.human();
env(tx);
env.close();
// The 25 shares redeem 25 assets; the destination receives 20 net.
BEAST_EXPECT(mptt.checkMPTokenAmount(owner, 20));
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(75));
BEAST_EXPECT(env.balance(depositor, shareIssue) == funded.shares(75));
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(995));
},
feeArgs);
testCase(
[this](
Env& env,
Account const& issuer,
Account const& owner,
Account const& depositor,
PrettyAsset const& asset,
Vault& vault,
MPTTester& mptt) {
testcase("MPT no transfer fee on withdrawal to issuer");
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
auto const mptIssue = asset.raw().get<MPTIssue>();
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
auto tx = vault.withdraw(
{.depositor = depositor, .id = funded.keylet.key, .amount = asset(10)});
tx[sfDestination] = issuer.human();
env(tx);
env.close();
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(90));
BEAST_EXPECT(env.balance(depositor, shareIssue) == funded.shares(90));
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(990));
},
feeArgs);
testCase(
[this](
Env& env,
Account const&,
Account const& owner,
Account const& depositor,
PrettyAsset const& asset,
Vault& vault,
MPTTester& mptt) {
testcase("MPT no transfer fee on self-withdrawal");
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
auto const mptIssue = asset.raw().get<MPTIssue>();
env(vault.withdraw(
{.depositor = depositor, .id = funded.keylet.key, .amount = asset(10)}));
env.close();
BEAST_EXPECT(mptt.checkMPTokenAmount(depositor, 910));
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(90));
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(1000));
},
feeArgs);
testCase(
[this](
Env& env,
Account const&,
Account const& owner,
Account const& depositor,
PrettyAsset const& asset,
Vault& vault,
MPTTester& mptt) {
testcase("MPT transfer fee must fit in the vault position");
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
auto const mptIssue = asset.raw().get<MPTIssue>();
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
// Delivering 81 costs 102 gross, more than the 100 shares held.
auto tx = vault.withdraw(
{.depositor = depositor, .id = funded.keylet.key, .amount = asset(81)});
tx[sfDestination] = owner.human();
env(tx, Ter{tecINSUFFICIENT_FUNDS});
env.close();
// Delivering 80 costs exactly the 100 shares held.
tx = vault.withdraw(
{.depositor = depositor, .id = funded.keylet.key, .amount = asset(80)});
tx[sfDestination] = owner.human();
env(tx);
env.close();
BEAST_EXPECT(mptt.checkMPTokenAmount(owner, 80));
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(0));
BEAST_EXPECT(env.balance(depositor, shareIssue) == funded.shares(0));
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(980));
env(vault.del({.owner = owner, .id = funded.keylet.key}));
env.close();
},
feeArgs);
// The payout rounds down whether or not MPTokensV2 is enabled.
auto const testTransferFeeRoundsDown = [&](FeatureBitset const& features) {
testCase(
[this, features](
Env& env,
Account const&,
Account const& owner,
Account const& depositor,
PrettyAsset const& asset,
Vault& vault,
MPTTester& mptt) {
testcase(
features[featureMPTokensV2]
? "MPT transfer fee rounds the payout down to zero"
: "MPT transfer fee rounds the payout down to zero without "
"MPTokensV2");
auto const funded = createFundedVault(env, vault, owner, depositor, asset(1));
auto const mptIssue = asset.raw().get<MPTIssue>();
auto tx = vault.withdraw({
.depositor = depositor,
.id = funded.keylet.key,
.amount = funded.shares(1),
});
tx[sfDestination] = owner.human();
env(tx, Ter{tecPRECISION_LOSS});
env.close();
// A self-withdrawal pays no fee, so the same share still redeems.
env(vault.withdraw({
.depositor = depositor,
.id = funded.keylet.key,
.amount = funded.shares(1),
}));
env.close();
BEAST_EXPECT(mptt.checkMPTokenAmount(depositor, 1000));
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(0));
env(vault.del({.owner = owner, .id = funded.keylet.key}));
env.close();
},
CaseArgs{.transferFee = 25'000, .features = features});
};
testTransferFeeRoundsDown(testableAmendments());
testTransferFeeRoundsDown(testableAmendments() - featureMPTokensV2);
testCase(
[this](
Env& env,
Account const& issuer,
Account const& owner,
Account const& depositor,
PrettyAsset const& asset,
Vault& vault,
MPTTester& mptt) {
testcase("MPT transfer fee leaves other depositors whole");
Account const charlie{"charlie"};
env.fund(XRP(1000), charlie);
env.close();
mptt.authorize({.account = charlie});
mptt.authorize({.account = issuer, .holder = charlie});
env(pay(issuer, owner, asset(100)));
env.close();
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
env(vault.deposit(
{.depositor = owner, .id = funded.keylet.key, .amount = asset(100)}));
env.close();
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
auto const vaultBefore = env.le(funded.keylet);
Number const assetsTotalBefore = vaultBefore->at(sfAssetsTotal);
Number const assetsAvailableBefore = vaultBefore->at(sfAssetsAvailable);
// Delivering 40 to charlie costs the depositor 50 gross.
auto tx = vault.withdraw(
{.depositor = depositor, .id = funded.keylet.key, .amount = asset(40)});
tx[sfDestination] = charlie.human();
env(tx);
env.close();
BEAST_EXPECT(mptt.checkMPTokenAmount(charlie, 40));
auto const vaultAfter = env.le(funded.keylet);
BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == assetsTotalBefore - 50);
BEAST_EXPECT(vaultAfter->at(sfAssetsAvailable) == assetsAvailableBefore - 50);
// The owner's 100 shares still redeem for the 100 it deposited.
BEAST_EXPECT(env.balance(owner, shareIssue) == funded.shares(100));
env(vault.withdraw(
{.depositor = owner, .id = funded.keylet.key, .amount = funded.shares(100)}));
env.close();
BEAST_EXPECT(mptt.checkMPTokenAmount(owner, 100));
},
feeArgs);
testCase(
[this](
Env& env,
Account const& issuer,
Account const& owner,
Account const& depositor,
PrettyAsset const& asset,
Vault& vault,
MPTTester& mptt) {
testcase("MPT no transfer fee on issuer withdrawal to a third party");
auto const funded = createFundedVault(env, vault, owner, issuer, asset(100));
auto const mptIssue = asset.raw().get<MPTIssue>();
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
auto tx = vault.withdraw(
{.depositor = issuer, .id = funded.keylet.key, .amount = asset(40)});
tx[sfDestination] = depositor.human();
env(tx);
env.close();
BEAST_EXPECT(mptt.checkMPTokenAmount(depositor, 1040));
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(60));
BEAST_EXPECT(env.balance(issuer, shareIssue) == funded.shares(60));
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(1100));
},
feeArgs);
testCase([this](
Env& env,
Account const& issuer,
@@ -1852,281 +1499,66 @@ private:
env.close();
});
// The issuer charges a 25% transfer fee, so 100 gross delivers 80 net.
auto const feeArgs = CaseArgs{.transferRate = 1.25};
testCase(
[&, this](
Env& env,
Account const& owner,
Account const& issuer,
Account const& charlie,
auto vaultAccount,
Vault& vault,
PrettyAsset const& asset,
auto issuanceId) {
testcase("IOU transfer fees not applied");
auto const testTransferFeeGate = [&](FeatureBitset const& features) {
testCase(
[this, features](
Env& env,
Account const& owner,
Account const& issuer,
Account const& charlie,
auto,
Vault& vault,
PrettyAsset const& asset,
auto&&...) {
bool const feeCharged = features[fixCleanup3_5_0];
testcase(
feeCharged ? "IOU transfer fee on third-party withdrawal"
: "IOU transfer fee waived pre-fixCleanup3_5_0");
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
env(tx);
env.close();
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
auto const issue = asset.raw().get<Issue>();
env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
env.close();
// Deposits are fee-free.
BEAST_EXPECT(env.balance(owner, issue) == asset(100));
BEAST_EXPECT(env.balance(funded.account, issue) == asset(100));
auto const issue = asset.raw().get<Issue>();
Asset const share = Asset(issuanceId(keylet));
env(vault.clawback({
.issuer = issuer,
.id = funded.keylet.key,
.holder = owner,
.amount = asset(50),
}));
env.close();
// transfer fees ignored on deposit
BEAST_EXPECT(env.balance(owner, issue) == asset(100));
BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(100));
// Clawbacks are fee-free.
BEAST_EXPECT(env.balance(owner, issue) == asset(100));
BEAST_EXPECT(env.balance(funded.account, issue) == asset(50));
env(vault.withdraw({
.depositor = owner,
.id = funded.keylet.key,
.amount = funded.shares(20'000'000),
}));
env.close();
// Self-withdrawals are fee-free.
BEAST_EXPECT(env.balance(owner, issue) == asset(120));
BEAST_EXPECT(env.balance(funded.account, issue) == asset(30));
auto tx = vault.withdraw({
.depositor = owner,
.id = funded.keylet.key,
.amount = funded.shares(30'000'000),
});
tx[sfDestination] = charlie.human();
{
auto const tx = vault.clawback(
{.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(50)});
env(tx);
env.close();
}
auto const expected = feeCharged ? asset(24) : asset(30);
BEAST_EXPECT(env.balance(owner, issue) == asset(120));
BEAST_EXPECT(env.balance(charlie, issue) == expected);
BEAST_EXPECT(env.balance(funded.account, issue) == asset(0));
env(vault.del({.owner = owner, .id = funded.keylet.key}));
env.close();
},
CaseArgs{.transferRate = 1.25, .features = features});
};
testTransferFeeGate(testableAmendments() - fixCleanup3_5_0);
testTransferFeeGate(testableAmendments());
testCase(
[this](
Env& env,
Account const& owner,
Account const&,
Account const& charlie,
auto,
Vault& vault,
PrettyAsset const& asset,
auto&&...) {
testcase("IOU transfer fee on fixed-asset third-party withdrawal");
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
auto const issue = asset.raw().get<Issue>();
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
auto tx = vault.withdraw(
{.depositor = owner, .id = funded.keylet.key, .amount = asset(40)});
tx[sfDestination] = charlie.human();
env(tx);
env.close();
// The destination receives the requested 40; the gross 50 leaves the vault.
BEAST_EXPECT(env.balance(charlie, issue) == asset(40));
BEAST_EXPECT(env.balance(funded.account, issue) == asset(50));
BEAST_EXPECT(env.balance(owner, shareIssue) == funded.shares(50'000'000));
// transfer fees ignored on clawback
BEAST_EXPECT(env.balance(owner, issue) == asset(100));
},
feeArgs);
testCase(
[this](
Env& env,
Account const& owner,
Account const&,
Account const& charlie,
auto,
Vault& vault,
PrettyAsset const& asset,
auto&&...) {
testcase("IOU transfer fee on fixed-share third-party withdrawal");
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
auto const issue = asset.raw().get<Issue>();
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
auto tx = vault.withdraw({
.depositor = owner,
.id = funded.keylet.key,
.amount = funded.shares(25'000'000),
});
tx[sfDestination] = charlie.human();
env(tx);
env.close();
// The shares redeem 25 assets; the destination receives 20 net.
BEAST_EXPECT(env.balance(charlie, issue) == asset(20));
BEAST_EXPECT(env.balance(funded.account, issue) == asset(75));
BEAST_EXPECT(env.balance(owner, shareIssue) == funded.shares(75'000'000));
},
feeArgs);
testCase(
[this](
Env& env,
Account const& owner,
Account const& issuer,
Account const&,
auto,
Vault& vault,
PrettyAsset const& asset,
auto&&...) {
testcase("IOU no transfer fee on withdrawal to issuer");
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
auto const issue = asset.raw().get<Issue>();
auto tx = vault.withdraw(
{.depositor = owner, .id = funded.keylet.key, .amount = asset(10)});
tx[sfDestination] = issuer.human();
env(tx);
env.close();
BEAST_EXPECT(env.balance(funded.account, issue) == asset(90));
BEAST_EXPECT(env.balance(owner, issue) == asset(100));
},
feeArgs);
testCase(
[this](
Env& env,
Account const& owner,
Account const&,
Account const&,
auto,
Vault& vault,
PrettyAsset const& asset,
auto&&...) {
testcase("IOU no transfer fee on self-withdrawal");
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
auto const issue = asset.raw().get<Issue>();
BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(50));
env(vault.withdraw(
{.depositor = owner, .id = funded.keylet.key, .amount = asset(10)}));
env.close();
{.depositor = owner, .id = keylet.key, .amount = share(20'000'000)}));
BEAST_EXPECT(env.balance(owner, issue) == asset(110));
BEAST_EXPECT(env.balance(funded.account, issue) == asset(90));
},
feeArgs);
// transfer fees ignored on withdraw
BEAST_EXPECT(env.balance(owner, issue) == asset(120));
BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(30));
testCase(
[this](
Env& env,
Account const& owner,
Account const&,
Account const& charlie,
auto,
Vault& vault,
PrettyAsset const& asset,
auto&&...) {
testcase("IOU transfer fee must fit in the vault position");
{
auto tx = vault.withdraw(
{.depositor = owner, .id = keylet.key, .amount = share(30'000'000)});
tx[sfDestination] = charlie.human();
env(tx);
}
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
auto const issue = asset.raw().get<Issue>();
// transfer fees ignored on withdraw to 3rd party
BEAST_EXPECT(env.balance(owner, issue) == asset(120));
BEAST_EXPECT(env.balance(charlie, issue) == asset(30));
BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(0));
// Delivering 81 costs 101.25 gross, more than the vault position.
auto tx = vault.withdraw(
{.depositor = owner, .id = funded.keylet.key, .amount = asset(81)});
tx[sfDestination] = charlie.human();
env(tx, Ter{tecINSUFFICIENT_FUNDS});
env.close();
// Delivering 80 costs exactly the 100 held.
tx = vault.withdraw(
{.depositor = owner, .id = funded.keylet.key, .amount = asset(80)});
tx[sfDestination] = charlie.human();
env(tx);
env.close();
BEAST_EXPECT(env.balance(charlie, issue) == asset(80));
BEAST_EXPECT(env.balance(funded.account, issue) == asset(0));
env(vault.del({.owner = owner, .id = funded.keylet.key}));
env(vault.del({.owner = owner, .id = keylet.key}));
env.close();
},
feeArgs);
testCase(
[this](
Env& env,
Account const& owner,
Account const& issuer,
Account const& charlie,
auto,
Vault& vault,
PrettyAsset const& asset,
auto&&...) {
testcase("IOU no transfer fee on issuer withdrawal to a third party");
auto const funded = createFundedVault(env, vault, owner, issuer, asset(100));
auto const issue = asset.raw().get<Issue>();
auto tx = vault.withdraw(
{.depositor = issuer, .id = funded.keylet.key, .amount = asset(40)});
tx[sfDestination] = charlie.human();
env(tx);
env.close();
BEAST_EXPECT(env.balance(charlie, issue) == asset(40));
BEAST_EXPECT(env.balance(funded.account, issue) == asset(60));
},
feeArgs);
testCase(
[this](
Env& env,
Account const& owner,
Account const&,
Account const& charlie,
auto,
Vault& vault,
PrettyAsset const& asset,
auto&&...) {
testcase("IOU receiver limit applies to the net amount");
// The limit fits the 40 delivered but not the 50 gross.
env(trust(charlie, asset(40)));
env.close();
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
auto const issue = asset.raw().get<Issue>();
auto tx = vault.withdraw(
{.depositor = owner, .id = funded.keylet.key, .amount = asset(40)});
tx[sfDestination] = charlie.human();
env(tx);
env.close();
BEAST_EXPECT(env.balance(charlie, issue) == asset(40));
BEAST_EXPECT(env.balance(funded.account, issue) == asset(50));
},
feeArgs);
CaseArgs{.transferRate = 1.25});
testCase([&, this](
Env& env,
-50
View File
@@ -1,50 +0,0 @@
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/UintTypes.h>
#include <gtest/gtest.h>
using namespace xrpl;
namespace {
// A 25% transfer fee.
Rate const kRate125{1'250'000'000};
AccountID const kIssuer{1};
} // namespace
TEST(TokenHelpers, add_transfer_fee)
{
Issue const usd{toCurrency("USD"), kIssuer};
MPTIssue const mpt{makeMptID(1, kIssuer)};
EXPECT_EQ(addTransferFee(STAmount{usd, 80}, kRate125), STAmount(usd, 100));
EXPECT_EQ(addTransferFee(STAmount{usd, 80}, kParityRate), STAmount(usd, 80));
// Integral MPT: the fee on 1 unit rounds up to a whole unit.
EXPECT_EQ(addTransferFee(STAmount{mpt, 80}, kRate125), STAmount(mpt, 100));
EXPECT_EQ(addTransferFee(STAmount{mpt, 1}, kRate125), STAmount(mpt, 2));
EXPECT_EQ(addTransferFee(STAmount{mpt, 1}, kParityRate), STAmount(mpt, 1));
}
TEST(TokenHelpers, subtract_transfer_fee)
{
Issue const usd{toCurrency("USD"), kIssuer};
MPTIssue const mpt{makeMptID(1, kIssuer)};
EXPECT_EQ(subtractTransferFee(STAmount{usd, 100}, kRate125), STAmount(usd, 80));
EXPECT_EQ(subtractTransferFee(STAmount{usd, 100}, kParityRate), STAmount(usd, 100));
// Integral MPT: the payout rounds down, to zero for a single unit.
EXPECT_EQ(subtractTransferFee(STAmount{mpt, 31}, kRate125), STAmount(mpt, 24));
EXPECT_EQ(subtractTransferFee(STAmount{mpt, 1}, kRate125), STAmount(mpt));
EXPECT_EQ(subtractTransferFee(STAmount{mpt, 1}, kParityRate), STAmount(mpt, 1));
}
-84
View File
@@ -1,84 +0,0 @@
#include <xrpl/protocol/STAmount.h>
#include <xrpl/basics/Number.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
#include <gtest/gtest.h>
#include <cstdint>
#include <stdexcept>
using namespace xrpl;
namespace {
// A 25% transfer fee.
constexpr std::uint32_t kRate125 = 1'250'000'000;
AccountID const kIssuer{1};
} // namespace
TEST(STAmount, mul_ratio_iou)
{
Issue const usd{toCurrency("USD"), kIssuer};
EXPECT_EQ(
mulRatio(STAmount{usd, 100}, QUALITY_ONE, kRate125, Number::RoundingMode::Downward),
STAmount(usd, 80));
EXPECT_EQ(
mulRatio(STAmount{usd, 80}, kRate125, QUALITY_ONE, Number::RoundingMode::Upward),
STAmount(usd, 100));
// 1 / 1.7 does not fit in a 16-digit mantissa, so the rounding direction
// decides the last digit.
STAmount const one{usd, 1};
EXPECT_LT(
mulRatio(one, QUALITY_ONE, 1'700'000'000, Number::RoundingMode::Downward),
mulRatio(one, QUALITY_ONE, 1'700'000'000, Number::RoundingMode::Upward));
STAmount const full{usd, std::uint64_t{9'999'999'999'999'999}, -5};
EXPECT_EQ(mulRatio(full, QUALITY_ONE, QUALITY_ONE, Number::RoundingMode::Downward), full);
}
TEST(STAmount, mul_ratio_mpt)
{
MPTIssue const mpt{makeMptID(1, kIssuer)};
EXPECT_EQ(
mulRatio(STAmount{mpt, 31}, QUALITY_ONE, kRate125, Number::RoundingMode::Downward),
STAmount(mpt, 24));
EXPECT_EQ(
mulRatio(STAmount{mpt, 31}, QUALITY_ONE, kRate125, Number::RoundingMode::Upward),
STAmount(mpt, 25));
EXPECT_EQ(
mulRatio(STAmount{mpt, 1}, QUALITY_ONE, kRate125, Number::RoundingMode::Downward),
STAmount(mpt));
EXPECT_EQ(
mulRatio(STAmount{mpt, 1}, QUALITY_ONE, kRate125, Number::RoundingMode::Upward),
STAmount(mpt, 1));
STAmount const max{mpt, kMaxMpTokenAmount};
EXPECT_EQ(mulRatio(max, QUALITY_ONE, QUALITY_ONE, Number::RoundingMode::Downward), max);
EXPECT_NO_THROW((void)mulRatio(max, QUALITY_ONE, kRate125, Number::RoundingMode::Downward));
EXPECT_THROW(
(void)mulRatio(max, kRate125, QUALITY_ONE, Number::RoundingMode::Upward),
std::overflow_error);
}
TEST(STAmount, mul_ratio_xrp)
{
EXPECT_EQ(
mulRatio(STAmount{XRPAmount{31}}, QUALITY_ONE, kRate125, Number::RoundingMode::Downward),
STAmount{XRPAmount{24}});
EXPECT_EQ(
mulRatio(STAmount{XRPAmount{31}}, QUALITY_ONE, kRate125, Number::RoundingMode::Upward),
STAmount{XRPAmount{25}});
}
-3
View File
@@ -1,3 +0,0 @@
# AGENTS.md — consensus
See [README.md](./README.md) for a short pointer, and [docs/consensus.md](../../../../docs/consensus.md) for the full consensus design.
-1
View File
@@ -1 +0,0 @@
AGENTS.md
-3
View File
@@ -1,3 +0,0 @@
# AGENTS.md — ledger
See [README.md](./README.md) for ledger lifecycle and fetch-pack design.
-1
View File
@@ -1 +0,0 @@
AGENTS.md
-3
View File
@@ -1,3 +0,0 @@
# AGENTS.md — overlay
See [README.md](./README.md) for the peer-protocol handshake, clustering, gossip, and monitoring design.
-1
View File
@@ -1 +0,0 @@
AGENTS.md
-3
View File
@@ -1,3 +0,0 @@
# AGENTS.md — peerfinder
See [README.md](./README.md) for the peer-discovery design.
-1
View File
@@ -1 +0,0 @@
AGENTS.md
+2 -2
View File
@@ -1,5 +1,5 @@
# AGENTS.md — rpc
See [README.md](./README.md) for the RPC subsystem design.
See the repo-level [AGENTS.md](../../../AGENTS.md) for general build/test/style guidance.
Any change to publicly-visible API behavior — RPC/WebSocket fields, parameters, or error conditions, or transaction/signing behavior surfaced through the API even from outside this directory — needs an entry in [`API-CHANGELOG.md`](../../../API-CHANGELOG.md) under a `## Unreleased` section (`### Additions`, `### Bugfixes`, `### Deprecations`, etc. as appropriate). If that section is absent, create it above the latest `## XRP Ledger server version x.y.z` heading; it is renamed at release.
Any change to a public RPC method's behavior (new/changed/removed fields, parameters, or error conditions) needs a corresponding entry in [`API-CHANGELOG.md`](../../../API-CHANGELOG.md), under the `## Unreleased` section (`### Additions`, `### Deprecations`, etc. as appropriate).