Compare commits

...
Author SHA1 Message Date
Mayukha Vadari ed07e19c3d docs: Fix stale facts in AGENTS.md and ARCHITECTURE.md
Describe --unittest-arg correctly, list both gtest binaries, add the payment/ and proposal/ transactor directories, drop the -Dxrpld requirement for the Rust crates, and say what to do when API-CHANGELOG.md has no Unreleased section.
2026-10-14 17:09:30 -04:00
Mayukha Vadari 2ad810ad0e Merge remote-tracking branch 'upstream/develop' into mvadari/codify-agents-md 2026-10-14 17:08:33 -04:00
Timur Yalymov 7237b3321c fix: Charge transfer fee on third-party vault withdrawals (#8267) 2026-10-14 16:35:42 +00:00
Timur Yalymov 84d980d883 fix: Reject VaultClawback with a nonexistent holder (#8470) 2026-10-14 14:29:05 +00:00
Timur YalymovandVito Tumas 791c96faad fix: Enforce pseudo-account directory contents with an invariant (#8181)
Co-authored-by: Vito Tumas <[email protected]>
2026-10-14 13:51:24 +00:00
Ayaz Salikhov 31473e7e6d build: Unify clang variables (#8581) 2026-10-14 13:51:05 +00:00
Timur YalymovandVito Tumas aa70b368b5 fix: Enforce asset authorization on LoanBrokerDelete (#8204)
Co-authored-by: Vito Tumas <[email protected]>
2026-10-14 13:51:05 +00:00
Vito TumasandJingchen d3db569b34 feat: Book FixedPrecision LoanSet through AssetsDeployed (#8260)
Co-authored-by: Jingchen <[email protected]>
2026-10-14 13:20:58 +00:00
Vito TumasandJingchen 94d6b7daf8 feat: Round LoanBroker cover on the FixedPrecision grid (#8258)
Co-authored-by: Jingchen <[email protected]>
2026-10-14 12:59:39 +00:00
Vito TumasandJingchen e089abe982 feat: Use FixedPrecision accounting in Vault transactors (#8460)
Co-authored-by: Jingchen <[email protected]>
2026-10-14 12:52:25 +00:00
Alex Kremer 33289e882b refactor: Migrate handlers to rpc-spec (C) (#8561) 2026-10-14 10:15:32 +00:00
1ed0bccb07 fix: Clamp invalid XRP amounts in Quality::ceilOut & getBookPage (#8480)
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Ed Hennis <[email protected]>
2026-10-14 07:49:27 +00:00
Gregory TsipenyukandEd Hennis 1da55e3989 fix: Waive CanTransfer on loan default and broker delete (#8231)
Co-authored-by: Ed Hennis <[email protected]>
2026-10-14 07:49:12 +00:00
Valentin Balaschenkoandxrplf-ai-reviewer[bot] 166e97e12b refactor: Decouple LedgerHistory from TaggedCache internals (#5874)
Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
2026-10-13 23:33:32 +00:00
33e5a6fa7c feat: Add TransactionProposalCancel & Cosign Ticket reservation (#8538)
Co-authored-by: Shawn Xie <[email protected]>
Co-authored-by: Kassaking7 <[email protected]>
Co-authored-by: Shawn Xie <[email protected]>
Co-authored-by: Zhiyuan Wang <[email protected]>
Co-authored-by: Claude Fable 5 <[email protected]>
2026-10-13 19:07:45 +00:00
Ayaz Salikhov b1f9dc6919 ci: Fix rpm installation problems (#8575) 2026-10-13 18:46:41 +00:00
Ayaz Salikhov 38965af055 docs: Clarify freeze on large changes 2 weeks before first RC (#8574) 2026-10-13 18:43:24 +00:00
Gregory TsipenyukandEd Hennis b010d4a563 refactor: Retire fixAMMv1_1 amendment (#8226)
Co-authored-by: Ed Hennis <[email protected]>
2026-10-13 18:03:10 +00:00
dependabot[bot] eedd6604d1 chore: [DEPENDABOT] bump pyparsing from 3.3.2 to 3.3.3 in /cmake/scripts/codegen in the codegen-dependencies group (#8572)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 17:48:57 +00:00
Mayukha Vadari 59ff04b648 fix: Allow WASM transactions in simulate only in admin mode (#8568) 2026-10-13 17:39:32 +00:00
Gregory TsipenyukandEd Hennis fcb3ee6425 fix: Detect int64 overflow in XRPAmount and MPTAmount (#8453)
Co-authored-by: Ed Hennis <[email protected]>
2026-10-13 17:35:35 +00:00
Gregory TsipenyukandEd Hennis 77eacbb52c fix: Assorted MPT/DEX offer related fixes (#8211)
Co-authored-by: Ed Hennis <[email protected]>
2026-10-13 17:18:47 +00:00
Vito TumasandJingchen 5ce85b2799 feat: Add FixedPrecision Vault grid and balance helpers (#8257)
Co-authored-by: Jingchen <[email protected]>
2026-10-13 16:19:08 +00:00
Ayaz Salikhov 4538d198e1 build: Update codegen requirements (#8570) 2026-10-13 14:29:59 +00:00
4c39b05f94 fix: Reject vault and lending amounts out of range for the asset (#8527)
Co-authored-by: Vito Tumas <[email protected]>
Co-authored-by: Ayaz Salikhov <[email protected]>
Co-authored-by: Ed Hennis <[email protected]>
2026-10-13 08:46:55 +00:00
Mayukha Vadari b4417cb105 Merge remote-tracking branch 'upstream/mvadari/codify-agents-md' into mvadari/codify-agents-md 2026-09-10 11:18:32 -04:00
Mayukha Vadari ef55ca97c8 docs: Drop redundant repo-level AGENTS.md pointers, shorten Build section
- The harness auto-loads every ancestor AGENTS.md (including root), so
  a nested file telling an agent to "see the repo-level AGENTS.md" is
  pure overhead — it can trigger a re-load/duplication-check/discard
  cycle, and some models just retain the duplicate content in context.
  Drop that pointer from every nested AGENTS.md; keep pointers to
  things that are NOT auto-loaded (README.md, docs/consensus.md,
  non-ancestor AGENTS.md like tx/ -> rpc/).
- Root AGENTS.md's Build section is now a short index pointer to
  BUILD.md and docs/build/nix.md instead of restating Nix setup steps,
  per review feedback that AGENTS.md should mostly index other docs
  rather than duplicate them.

Addresses PR review comments:
https://github.com/XRPLF/rippled/pull/8198#discussion_r3979325914
https://github.com/XRPLF/rippled/pull/8198#discussion_r3979424992
2026-09-10 11:18:15 -04:00
Mayukha Vadari de1a5f882c Merge branch 'develop' into mvadari/codify-agents-md 2026-09-09 14:00:02 -04:00
Mayukha Vadari 6dc958c136 docs: Fix std::unexpected wording, split UNREACHABLE out of amendment section
- ledger/helpers/AGENTS.md: std::expected<..., TER>/std::unexpected(tec*)
  is already the established idiom here (confirmed in AMMHelpers.cpp,
  CredentialHelpers.cpp), not something to avoid; reword so the rule
  doesn't read as discouraging it.
- transactors/AGENTS.md: the UNREACHABLE/LCOV_EXCL coverage rule isn't
  amendment-specific, so give it its own heading instead of nesting it
  under "Gating amendment-dependent code".
- Root AGENTS.md: comments should explain why, not what/how, since the
  code already shows that.
- Drop the pseudo-account exemption rule from transactors/AGENTS.md —
  needs refining before it's codified.

Addresses PR review comments:
https://github.com/XRPLF/rippled/pull/8198#discussion_r3962352164
https://github.com/XRPLF/rippled/pull/8198#discussion_r3967566278
2026-09-09 13:58:12 -04:00
Mayukha Vadari 6b9db2460c docs: Loosen Nix mandate, extract ARCHITECTURE.md, add docs-upkeep note
- Build section now recommends the Nix devshell rather than requiring
  it; contributors can use their own toolchain/build flow.
- Move the directory-by-directory architecture map out of AGENTS.md
  into a new top-level ARCHITECTURE.md, with a pointer left behind.
- Add a short note asking contributors to keep AGENTS.md/README/
  ARCHITECTURE.md files current when they change what those docs cover.
- ledger/helpers/AGENTS.md also needs the amendment-gating shape,
  per-amendment flag naming, and UNREACHABLE/LCOV_EXCL pairing notes
  (previously only in transactors/AGENTS.md) — the vault/lending
  helper files these were evidenced on live in ledger/helpers/, not
  tx/transactors/.
2026-09-09 13:46:51 -04:00
Mayukha Vadari 40988e1d77 Merge remote-tracking branch 'upstream/mvadari/codify-agents-md' into mvadari/codify-agents-md 2026-09-09 13:39:16 -04:00
Mayukha Vadari 7c642cdf9e docs: Scope AGENTS.md content to the smallest applicable folder
Team decided AGENTS.md content should live in the narrowest folder it
applies to, rather than the broadest, to keep context small for agents
working elsewhere in the repo. Restructure round 1/2's additions
accordingly, with no change in the rules themselves:

- Move the repo-wide API-CHANGELOG.md rule out of root AGENTS.md and
  into rpc/AGENTS.md (its natural home), with a one-line pointer left
  in tx/AGENTS.md for the signing-behavior case.
- Move the amendment-gating *code style* rules (single enabled/disabled
  block, per-amendment flag naming, UNREACHABLE/LCOV_EXCL pairing) from
  tx/AGENTS.md down into transactors/AGENTS.md, since all cited PR
  evidence was in transactor code, not tx/-level files.
- Move the "shared test setup belongs in jtx/" rule out of root
  AGENTS.md into a new src/test/AGENTS.md (+ CLAUDE.md symlink).
- Trim the now-duplicated amendment sentence out of root's Architecture
  bullet in favor of a pointer to tx/AGENTS.md.
2026-09-09 13:38:42 -04:00
Mayukha Vadari 6d2c69986d Merge branch 'develop' into mvadari/codify-agents-md 2026-09-08 17:11:49 -04:00
Mayukha Vadari 0a1708dbfe docs: Add subsystem AGENTS.md files for transactors, ledger helpers, and READMEs
Round 2 of PR-review mining: found two subsystem-specific, unrecorded
conventions (pseudo-account auth/freeze exemption in vault/lending
transactors; SLE-type assertion in ledger helpers) that warrant their
own nested AGENTS.md rather than more root-level bullets.

Also added thin AGENTS.md pointer files (with CLAUDE.md symlinks,
matching the existing convention) for subsystems that already have a
substantial README.md but no AGENTS.md — agent tooling auto-loads
AGENTS.md when working in a directory but not README.md, so those
conventions were otherwise invisible to an agent that didn't think to
look.
2026-09-08 17:05:08 -04:00
Mayukha Vadari 5b6cd13a59 docs: Codify recurring PR review feedback into AGENTS.md
Mined 6 months of merged PR review comments (human reviewers only) for
conventions that recur across multiple PRs/reviewers but aren't already
documented in CONTRIBUTING.md/CodingStyle.md, and folded them in:

- Doxygen comments expected on new public functions/methods.
- API-CHANGELOG.md requirement is repo-wide, not RPC-directory-scoped.
- Shared test setup belongs in jtx/, not copy-pasted across test files.
- Amendment requirement test and a design-considerations checklist,
  sourced from the internal "Considerations for Future Amendments" doc.
- Amendment gating shape (single enabled/disabled block), per-amendment
  flag naming, and UNREACHABLE/LCOV_EXCL pairing, all recurring asks in
  tx/vault/invariant PR reviews.
2026-09-04 23:07:38 -04:00
191 changed files with 18840 additions and 3255 deletions

No files matched your search

+15
View File
@@ -36,3 +36,18 @@ updates:
rust-dependencies:
patterns:
- "*"
- package-ecosystem: uv
directory: /cmake/scripts/codegen
schedule:
interval: weekly
day: monday
time: "04:00"
timezone: Etc/GMT
commit-message:
prefix: "chore: [DEPENDABOT] "
target-branch: develop
groups:
codegen-dependencies:
patterns:
- "*"
@@ -95,11 +95,15 @@ jobs:
apt-get update -qq
apt-get install -y "./${PACKAGE}"
# Rocky Linux and AlmaLinux pick a mirror from a mirrorlist, and now and
# then get one that serves the repository metadata at a few hundred kB/s,
# which alone outlasts the job's timeout. dnf only abandons a mirror below
# 1000 B/s for 30 s, so raise that floor to fail over to the next one fast.
- name: Install the RPM
if: ${{ inputs.package_type == 'rpm' }}
env:
PACKAGE: ${{ steps.find.outputs.package }}
run: dnf install -y "./${PACKAGE}"
run: dnf install -y --setopt=minrate=1M --setopt=timeout=10 "./${PACKAGE}"
- name: Run xrpld
run: xrpld --version
+11 -17
View File
@@ -4,24 +4,18 @@ This file provides guidance to AI coding agents (Claude Code, and other AGENTS.m
## Build
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`).
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.
Rust crate tests (independent of the CMake build): `cargo test --manifest-path crates/Cargo.toml --workspace` (CI uses `cargo nextest`).
## Testing
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:
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:
- 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` does nothing — don't use it.
- `--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.
- 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/`. `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/` (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`.
## Lint/Format
@@ -29,14 +23,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 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).
## Architecture
Paths below reflect the current layout; update this section if modularization moves a subsystem to a different directory.
See [ARCHITECTURE.md](./ARCHITECTURE.md) for the directory-by-directory map of the codebase.
- `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).
## 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.
+1
View File
@@ -35,6 +35,7 @@ Version 3.5.0 is not yet released.
- `channel_authorize`: The `channel_id` field now returns an `invalidParams` error if the value is not a string. [#7582](https://github.com/XRPLF/rippled/pull/7582)
- `channel_verify`: The `channel_id` and `signature` fields now return an `invalidParams` error if the value is not a string. [#7582](https://github.com/XRPLF/rippled/pull/7582)
- `account_currencies`, `account_nfts`, `account_offers`: A request with more than one problem may now report a different one of them. An invalid `ledger_hash` or `ledger_index` is reported before an invalid or missing `account`. An invalid `limit` or `marker` is reported before `lgrNotFound` or `actNotFound`, and for `account_offers` also before `actMalformed`. A request with a single problem returns the same error as before. [#8561](https://github.com/XRPLF/rippled/pull/8561)
### Bugfixes in 3.5.0
+9
View File
@@ -0,0 +1,9 @@
# 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).
+1 -1
View File
@@ -42,7 +42,7 @@ if(is_gcc)
# GCC-specific fixes
add_compile_options(-Wno-unknown-pragmas -Wno-subobject-linkage)
# -Wno-subobject-linkage can be removed when we upgrade GCC version to at least 13.3
elseif(is_clang)
elseif(is_any_clang)
# Clang-specific fixes
add_compile_options(-Wno-unknown-warning-option) # Ignore unknown warning options
elseif(is_msvc)
+16 -10
View File
@@ -34,7 +34,7 @@ so tools can filter them and protection rules can target `release/*` and `stagin
- **Branches only move forward.**
`develop`, `staging/X.Y.x`, and `release/X.Y.x` are never rewritten.
- **Cherry-picking only goes from `develop` to a staging branch**,
for fixes that must get into a release after the code freeze
for fixes that must get into a release after the [code freeze](#release-candidates)
(see [Emergency Fixes From `develop`](#emergency-fixes-from-develop)).
- **Security fixes are prepared privately and published with the release that contains them**,
so vulnerabilities are not disclosed prematurely.
@@ -49,6 +49,7 @@ develop staging/X.Y.x & release/X.Y.x
│
├── Tag: X.Y.0-b1
├── Tag: X.Y.0-bN
│ (2 weeks before rc1: freeze on large changes starts)
├── Tag: X.Y.0-rc1 ───────────────┐ (branches created)
│ │
│ (development continues) ├── Fixes
@@ -75,16 +76,20 @@ develop staging/X.Y.x & release/X.Y.x
3. **Branches:** `staging/X.Y.x` and `release/X.Y.x` are created from `develop`
at the commit tagged `X.Y.0-rc1`.
The first RC also kicks off the QE process.
4. **Code freeze:** No new features or unrelated changes are pulled from `develop`
into `staging/X.Y.x` or `release/X.Y.x`.
Only critical stabilization fixes go into the line.
5. **No large changes on `develop`:** Until `X.Y.0` is [merged back](#merging-back-into-develop),
large changes (e.g., big refactors, moving or renaming many files, mass reformatting)
are not merged into `develop`,
so that fixes on the line and the merge back don't run into conflicts.
### Freeze on Large Changes
From two weeks before `X.Y.0-rc1` until `X.Y.0` is [merged back](#merging-back-into-develop),
large changes (e.g., big refactors, moving or renaming many files, mass reformatting)
are not merged into `develop`.
### Release Candidates
After branching, the line is in code freeze:
no new features or unrelated changes are pulled from `develop`
into `staging/X.Y.x` or `release/X.Y.x`.
Only critical stabilization fixes go into the line.
1. Fixes are developed against `staging/X.Y.x`.
2. When ready, a new RC is created on `staging/X.Y.x`,
and `release/X.Y.x` is fast-forwarded to it.
@@ -106,7 +111,8 @@ one right after the other.
2. `release/X.Y.x` is fast-forwarded to `staging/X.Y.x`,
and the release is tagged on it.
3. The release is immediately [merged back into `develop`](#merging-back-into-develop).
For a major-minor release, this lifts the freeze on large changes in `develop`.
For a major-minor release, this lifts the [freeze on large changes](#freeze-on-large-changes)
in `develop`.
### Merging Back Into `develop`
@@ -127,7 +133,7 @@ After the merge, `git log develop..<tag>` must be empty.
### Emergency Fixes From `develop`
If a commit was merged to `develop`
and needs to be included in a release after the code freeze:
and needs to be included in a release after the [code freeze](#release-candidates):
1. Create a PR that cherry-picks the commit onto `staging/X.Y.x`.
2. Leave `develop` as it is, with no reverts.
+11 -3
View File
@@ -9,13 +9,21 @@ include_guard(GLOBAL)
# --------------------------------------------------------------------
# Compiler detection (C++)
# --------------------------------------------------------------------
set(is_clang FALSE)
# is_any_clang is true for both LLVM Clang and AppleClang. Their version numbers
# and feature sets differ, so use is_llvm_clang or is_apple_clang when it matters.
set(is_any_clang FALSE)
set(is_llvm_clang FALSE)
set(is_apple_clang FALSE)
set(is_gcc FALSE)
set(is_msvc FALSE)
set(is_xcode FALSE)
if(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang") # Clang or AppleClang
set(is_clang TRUE)
if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
set(is_any_clang TRUE)
set(is_llvm_clang TRUE)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
set(is_any_clang TRUE)
set(is_apple_clang TRUE)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
set(is_gcc TRUE)
elseif(MSVC)
+6 -3
View File
@@ -130,7 +130,7 @@ else()
INTERFACE
-Wall
-Wdeprecated
$<$<BOOL:${is_clang}>:-Wno-deprecated-declarations>
$<$<BOOL:${is_any_clang}>:-Wno-deprecated-declarations>
$<$<BOOL:${wextra}>:-Wextra
-Wno-unused-parameter>
$<$<BOOL:${werr}>:-Werror>
@@ -177,7 +177,7 @@ else()
if(
is_nix_compiler
AND is_linux
AND is_clang
AND is_any_clang
AND CMAKE_CXX_FLAGS MATCHES "stdlib=libstdc"
)
string(
@@ -204,7 +204,10 @@ if(voidstar)
"Antithesis instrumentation requires Linux, aborting..."
)
elseif(
NOT (is_clang AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 16.0)
NOT (
is_llvm_clang
AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 16.0
)
)
message(
FATAL_ERROR
+7 -1
View File
@@ -296,7 +296,13 @@ if(xrpld)
OUT_VAR rpcspec_instantiations
VALUE_TYPE "::json::Value"
VIEW_HEADER "xrpld/rpc/detail/JsonObjectView.hpp"
HANDLERS book_changes ledger transaction_entry
HANDLERS
account_currencies
account_nfts
account_offers
book_changes
ledger
transaction_entry
)
target_sources(xrpld PRIVATE ${rpcspec_instantiations})
+1 -1
View File
@@ -58,7 +58,7 @@ target_link_options(common INTERFACE ${sanitizers_linker_flags})
# This module appends -fsanitize-ignorelist=<path> for Clang builds.
# The ignorelist path contains CMAKE_SOURCE_DIR, so it must be set here, rather than in the Conan profile.
# GCC does not support -fsanitize-ignorelist.
if(is_clang)
if(is_any_clang)
set(ignorelist_path
"${CMAKE_SOURCE_DIR}/sanitizers/suppressions/sanitizer-ignorelist.txt"
)
+2 -5
View File
@@ -23,11 +23,8 @@ if(NOT is_multiconfig)
endif()
endif()
if(is_clang) # both Clang and AppleClang
if(
"${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang"
AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 16.0
)
if(is_llvm_clang)
if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 16.0)
message(FATAL_ERROR "This project requires clang 16 or later")
endif()
elseif(is_gcc)
+3 -3
View File
@@ -62,11 +62,11 @@ if(unity)
set(CMAKE_UNITY_BUILD ON CACHE BOOL "Do a unity build")
endif()
if(is_clang AND is_linux)
if(is_llvm_clang AND is_linux)
option(voidstar "Enable Antithesis instrumentation." OFF)
endif()
if(is_gcc OR is_clang)
if(is_gcc OR is_any_clang)
include(ProcessorCount)
ProcessorCount(PROCESSOR_COUNT)
@@ -125,7 +125,7 @@ else()
set(use_mold OFF CACHE BOOL "mold linker, linux only" FORCE)
endif()
if(is_clang)
if(is_any_clang)
option(use_lld "enables detection of lld linker" ON)
else()
set(use_lld OFF CACHE BOOL "try lld linker, clang only" FORCE)
+2 -2
View File
@@ -7,7 +7,7 @@
pcpp>=1.30
# Parser combinator library - used to parse the macro DSL
pyparsing>=3.0.0
pyparsing>=3.3.3
# Template engine - used to generate C++ code from templates
Mako>=1.2.2
Mako>=1.4.2
+6 -6
View File
@@ -1,8 +1,8 @@
# This file was autogenerated by uv via the following command:
# uv pip compile requirements.in --generate-hashes --output-file requirements.txt
mako==1.3.12 \
--hash=sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9 \
--hash=sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a
mako==1.4.3 \
--hash=sha256:723296007c870bfd6b3f0c3230dba7198096e5269297ebf5e4eff9e7ffa39d4f \
--hash=sha256:cd6537fe88d5fec315c55c2f8529bc4ce7a9a352ad7db3eeaa6a66e2dd4ec37a
# via -r requirements.in
markupsafe==3.0.3 \
--hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \
@@ -99,7 +99,7 @@ pcpp==1.30 \
--hash=sha256:05fe08292b6da57f385001c891a87f40d6aa7f46787b03e8ba326d20a3297c6e \
--hash=sha256:5af9fbce55f136d7931ae915fae03c34030a3b36c496e72d9636cedc8e2543a1
# via -r requirements.in
pyparsing==3.3.2 \
--hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \
--hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc
pyparsing==3.3.3 \
--hash=sha256:928ae7e20211f3b6f3915a72f06a0cfd29ab9d24279dd6346b6b1a7146397d36 \
--hash=sha256:ece8c00a69cf01b45d0b1dedabb469c90d8caf996d4fda40f147627a122849a4
# via -r requirements.in
+1 -1
View File
@@ -3,7 +3,7 @@
"requires": [
"zlib/1.3.2#1cb806da49011867778ffb6ac7190fcb%1782392402.122708",
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1782392402.420688",
"xrpl-rpc-spec/0.1.21#d536f87a2ae7d313452746cfa3ac4404%1790869005.122975",
"xrpl-rpc-spec/0.1.23#56e8946f5b3fb949a3732e1a51135ca2%1790977482.197086",
"sqlite3/3.53.0#324ada52333108388a9a6108bfa96734%1782392403.185447",
"soci/4.0.3#e726491a03468795453f7c83fc924a96%1782392402.679521",
"snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1782307151.633168",
+1 -1
View File
@@ -40,7 +40,7 @@ class Xrpl(ConanFile):
"nudb/2.0.9",
"openssl/3.6.3",
"soci/4.0.3",
"xrpl-rpc-spec/0.1.21",
"xrpl-rpc-spec/0.1.23",
"zlib/1.3.2",
]
+72
View File
@@ -39,6 +39,31 @@ checkedSub(std::int64_t a, std::int64_t b) noexcept
return a - b;
}
/**
* Multiply two signed 64-bit integers, returning std::nullopt when the exact
* mathematical product is not representable in std::int64_t.
*/
[[nodiscard]] constexpr std::optional<std::int64_t>
checkedMul(std::int64_t a, std::int64_t b) noexcept
{
using L = std::numeric_limits<std::int64_t>;
if (a == 0 || b == 0)
return 0;
if (a > 0)
{
if (b > 0 ? a > L::max() / b : b < L::min() / a)
return std::nullopt;
}
else if (b > 0 ? a < L::min() / b : b < L::max() / a)
{
return std::nullopt;
}
return a * b;
}
static_assert(checkedAdd(0, 0) == 0);
static_assert(checkedAdd(1, -1) == 0);
static_assert(checkedAdd(-5, 2) == -3);
@@ -81,6 +106,53 @@ static_assert(
!checkedSub(std::numeric_limits<std::int64_t>::min(), std::numeric_limits<std::int64_t>::max())
.has_value());
static_assert(checkedMul(0, std::numeric_limits<std::int64_t>::min()) == 0);
static_assert(checkedMul(std::numeric_limits<std::int64_t>::min(), 0) == 0);
static_assert(checkedMul(-1, -1) == 1);
// a > 0, b > 0
static_assert(
checkedMul(std::numeric_limits<std::int64_t>::max(), 1) ==
std::numeric_limits<std::int64_t>::max());
static_assert(
checkedMul(std::numeric_limits<std::int64_t>::max() / 2, 2) ==
std::numeric_limits<std::int64_t>::max() - 1);
static_assert(!checkedMul((std::numeric_limits<std::int64_t>::max() / 2) + 1, 2).has_value());
static_assert(!checkedMul(2, (std::numeric_limits<std::int64_t>::max() / 2) + 1).has_value());
// a > 0, b < 0
static_assert(
checkedMul(std::numeric_limits<std::int64_t>::max(), -1) ==
-std::numeric_limits<std::int64_t>::max());
static_assert(
checkedMul((std::numeric_limits<std::int64_t>::max() / 2) + 1, -2) ==
std::numeric_limits<std::int64_t>::min());
static_assert(!checkedMul((std::numeric_limits<std::int64_t>::max() / 2) + 2, -2).has_value());
static_assert(
checkedMul(1, std::numeric_limits<std::int64_t>::min()) ==
std::numeric_limits<std::int64_t>::min());
static_assert(
checkedMul(2, std::numeric_limits<std::int64_t>::min() / 2) ==
std::numeric_limits<std::int64_t>::min());
static_assert(!checkedMul(2, (std::numeric_limits<std::int64_t>::min() / 2) - 1).has_value());
// a < 0, b > 0
static_assert(
checkedMul(std::numeric_limits<std::int64_t>::min(), 1) ==
std::numeric_limits<std::int64_t>::min());
static_assert(
checkedMul(std::numeric_limits<std::int64_t>::min() / 2, 2) ==
std::numeric_limits<std::int64_t>::min());
static_assert(!checkedMul((std::numeric_limits<std::int64_t>::min() / 2) - 1, 2).has_value());
static_assert(!checkedMul(std::numeric_limits<std::int64_t>::min(), 2).has_value());
// a < 0, b < 0
static_assert(
checkedMul(-(std::numeric_limits<std::int64_t>::max() / 2), -2) ==
std::numeric_limits<std::int64_t>::max() - 1);
static_assert(!checkedMul(std::numeric_limits<std::int64_t>::min() / 2, -2).has_value());
static_assert(
checkedMul(-1, -std::numeric_limits<std::int64_t>::max()) ==
std::numeric_limits<std::int64_t>::max());
static_assert(!checkedMul(std::numeric_limits<std::int64_t>::min(), -1).has_value());
static_assert(!checkedMul(-1, std::numeric_limits<std::int64_t>::min()).has_value());
/**
* Calculate one number divided by another number in percentage.
* The result is rounded up to the next integer, and capped in the range [0,100]
+34 -3
View File
@@ -153,6 +153,24 @@ private:
Policy policy,
Callback&& replaceCallback = nullptr);
/**
* canonicalizeImpl variant for callers that already hold `mutex_`.
*
* The unnamed `scoped_lock<MutexType> const&` parameter is proof of
* ownership; this overload does not acquire the lock. Use it from other
* methods (e.g. fetchAndModify) that need to compose canonicalize with
* additional work under a single critical section, without relying on
* recursive-mutex re-entry.
*/
template <class Policy, class Callback = std::nullptr_t>
bool
canonicalizeImpl(
std::scoped_lock<MutexType> const&,
key_type const& key,
CanonicalizeClientPointerType<Policy> data,
Policy policy,
Callback&& replaceCallback = nullptr);
public:
/**
* Replace aliased objects with originals.
@@ -239,9 +257,6 @@ public:
bool
retrieve(key_type const& key, T& data);
MutexType&
peekMutex();
std::vector<key_type>
getKeys() const;
@@ -263,6 +278,22 @@ public:
fetch(key_type const& digest, Handler const& h);
// End CachedSLEs functions.
/**
* Fetch or create an entry and execute a callback while holding the lock.
*
* The entry for the given key is fetched from the cache or created if it
* doesn't exist. The callback is then invoked with a reference to the
* entry while the cache mutex is still held, allowing safe modification
* of the cached object.
*
* @param key The key to fetch or create
* @param callback Function to call with the entry under lock.
* Signature: void(T&)
*/
template <class Callback>
void
fetchAndModify(key_type const& key, Callback&& callback);
private:
SharedPointerType
initialFetch(key_type const& key, std::scoped_lock<MutexType> const& l);
+67 -18
View File
@@ -4,6 +4,7 @@
#include <xrpl/basics/Log.h> // IWYU pragma: keep
#include <xrpl/basics/TaggedCache.h>
#include <xrpl/basics/scope.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <algorithm>
@@ -335,6 +336,29 @@ template <class Policy, class Callback>
inline bool
TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash, KeyEqual, Mutex>::
canonicalizeImpl(
key_type const& key,
CanonicalizeClientPointerType<Policy> data,
Policy policy,
Callback&& replaceCallback)
{
std::scoped_lock<MutexType> const lock(mutex_);
return canonicalizeImpl(lock, key, data, policy, std::forward<Callback>(replaceCallback));
}
template <
class Key,
class T,
bool IsKeyCache,
class SharedWeakUnionPointer,
class SharedPointerType,
class Hash,
class KeyEqual,
class Mutex>
template <class Policy, class Callback>
inline bool
TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash, KeyEqual, Mutex>::
canonicalizeImpl(
std::scoped_lock<MutexType> const&,
key_type const& key,
CanonicalizeClientPointerType<Policy> data,
[[maybe_unused]] Policy policy,
@@ -354,8 +378,6 @@ TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash,
// passing a const argument is a compile error.
constexpr bool replaceCached = std::is_same_v<Policy, detail::ReplaceCached>;
std::scoped_lock const lock(mutex_);
auto const cit = cache_.find(key);
if (cit == cache_.end())
@@ -572,22 +594,6 @@ TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash,
return true;
}
template <
class Key,
class T,
bool IsKeyCache,
class SharedWeakUnionPointer,
class SharedPointerType,
class Hash,
class KeyEqual,
class Mutex>
inline auto
TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash, KeyEqual, Mutex>::
peekMutex() -> MutexType&
{
return mutex_;
}
template <
class Key,
class T,
@@ -700,6 +706,49 @@ TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash,
}
// End CachedSLEs functions.
template <
class Key,
class T,
bool IsKeyCache,
class SharedWeakUnionPointer,
class SharedPointerType,
class Hash,
class KeyEqual,
class Mutex>
template <class Callback>
inline void
TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash, KeyEqual, Mutex>::
fetchAndModify(key_type const& key, Callback&& callback)
{
static_assert(
!IsKeyCache, "fetchAndModify is only supported for value caches, not key-only caches");
static_assert(
std::is_same_v<std::shared_ptr<T>, SharedPointerType> ||
std::is_same_v<intr_ptr::SharedPtr<T>, SharedPointerType>,
"fetchAndModify requires std::shared_ptr<T> or intr_ptr::SharedPtr<T>");
std::scoped_lock<MutexType> const lock(mutex_);
// Same pointer-type dispatch as insert(): the placeholder must have exactly
// SharedPointerType so canonicalizeImpl can write the cached pointer back.
SharedPointerType entry = [] {
if constexpr (std::is_same_v<std::shared_ptr<T>, SharedPointerType>)
{
return std::make_shared<T>();
}
else
{
return intr_ptr::makeShared<T>();
}
}();
canonicalizeImpl(lock, key, entry, detail::ReplaceClient{});
ALWAYS(
entry != nullptr, "xrpl::TaggedCache::fetchAndModify : entry present after canonicalize");
callback(*entry);
}
template <
class Key,
class T,
+3
View File
@@ -0,0 +1,3 @@
# 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
@@ -0,0 +1 @@
AGENTS.md
+9 -1
View File
@@ -265,6 +265,13 @@ 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,
@@ -272,7 +279,8 @@ doWithdraw(
AccountID const& dstAcct,
AccountID const& sourceAcct,
XRPAmount priorBalance,
STAmount const& amount,
STAmount const& destinationAmount,
STAmount const& sourceAmount,
beast::Journal j);
/**
+81 -160
View File
@@ -2,7 +2,6 @@
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/ApplyView.h>
@@ -29,7 +28,6 @@
#include <expected>
#include <functional>
#include <optional>
#include <stdexcept>
#include <tuple>
#include <utility>
@@ -313,11 +311,11 @@ getAMMOfferStartWithTakerPays(
* is equal to LOB quality (in this case AMM offer quality is
* better than LOB quality) or AMM offer is equal to LOB quality
* (in this case SPQ is better than LOB quality).
* Pre-amendment code calculates takerPays first. If takerGets is the
* economically coarser integral side, it is rounded down, which results in
* worse offer quality than LOB quality, and the offer might fail to generate.
* Post-amendment code calculates the economically coarser integral offer side
* first. The result is rounded down, which makes the offer quality better.
* The economically coarser integral offer side is calculated first. The
* result is rounded down, which makes the offer quality better. Calculating
* takerPays first instead would round down takerGets when that is the coarser
* side, giving worse quality than LOB quality, and the offer might fail to
* generate.
* It might not be possible to match either SPQ or AMM offer to LOB
* quality. This generally happens at higher fees.
* @param pool AMM pool balances
@@ -331,69 +329,8 @@ changeSpotPriceQuality(
TAmounts<TIn, TOut> const& pool,
Quality const& quality,
std::uint16_t tfee,
Rules const& rules,
beast::Journal j)
{
if (!rules.enabled(fixAMMv1_1))
{
// Finds takerPays (i) and takerGets (o) such that given pool
// composition poolGets(I) and poolPays(O): (O - o) / (I + i) = quality.
// Where takerGets is calculated as the swapAssetIn (see below).
// The above equation produces the quadratic equation:
// i^2*(1-fee) + i*I*(2-fee) + I^2 - I*O/quality,
// which is solved for i, and o is found with swapAssetIn().
auto const f = feeMult(tfee); // 1 - fee
auto const& a = f;
auto const b = pool.in * (1 + f);
Number const c = pool.in * pool.in - pool.in * pool.out * quality.rate();
auto const res = b * b - 4 * a * c;
if (res < 0)
{
return std::nullopt; // LCOV_EXCL_LINE
}
if (auto const nTakerPaysPropose = (-b + root2(res)) / (2 * a); nTakerPaysPropose > 0)
{
auto const nTakerPays = [&] {
// The fee might make the AMM offer quality less than CLOB
// quality. Therefore, AMM offer has to satisfy this constraint:
// o / i >= q. Substituting o with swapAssetIn() gives: i <= O /
// q - I / (1 - fee).
auto const nTakerPaysConstraint = pool.out * quality.rate() - pool.in / f;
if (nTakerPaysPropose > nTakerPaysConstraint)
return nTakerPaysConstraint;
return nTakerPaysPropose;
}();
if (nTakerPays <= 0)
{
JLOG(j.trace()) << "changeSpotPriceQuality calc failed: " << to_string(pool.in)
<< " " << to_string(pool.out) << " " << quality << " " << tfee;
return std::nullopt;
}
auto const takerPays =
toAmount<TIn>(getAsset(pool.in), nTakerPays, Number::RoundingMode::Upward);
// should not fail
auto amounts = TAmounts<TIn, TOut>{takerPays, swapAssetIn(pool, takerPays, tfee)};
if (Quality{amounts} < quality &&
!withinRelativeDistance(Quality{amounts}, quality, Number(1, -7)))
{
JLOG(j.error()) << "changeSpotPriceQuality failed: " << to_string(pool.in) << " "
<< to_string(pool.out) << " "
<< " " << quality << " " << tfee << " " << to_string(amounts.in)
<< " " << to_string(amounts.out);
Throw<std::runtime_error>("changeSpotPriceQuality failed");
}
JLOG(j.trace()) << "changeSpotPriceQuality succeeded: " << to_string(pool.in) << " "
<< to_string(pool.out) << " "
<< " " << quality << " " << tfee << " " << to_string(amounts.in) << " "
<< to_string(amounts.out);
return amounts;
}
JLOG(j.trace()) << "changeSpotPriceQuality calc failed: " << to_string(pool.in) << " "
<< to_string(pool.out) << " " << quality << " " << tfee;
return std::nullopt;
}
auto amounts = [&] {
bool const inIntegral = getAsset(pool.in).integral();
bool const outIntegral = getAsset(pool.out).integral();
@@ -459,57 +396,49 @@ template <typename TIn, typename TOut>
TOut
swapAssetIn(TAmounts<TIn, TOut> const& pool, TIn const& assetIn, std::uint16_t tfee)
{
if (auto const& rules = getCurrentTransactionRules(); rules && rules->enabled(fixAMMv1_1))
{
// set rounding to always favor the amm. Clip to zero.
// calculate:
// pool.out -
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
// and explicitly set the rounding modes
// Favoring the amm means we should:
// minimize:
// pool.out -
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
// maximize:
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
// (pool.in * pool.out)
// minimize:
// (pool.in + assetIn * feeMult(tfee)),
// minimize:
// assetIn * feeMult(tfee)
// feeMult is: (1-fee), fee is tfee/100000
// minimize:
// 1-fee
// maximize:
// fee
SaveNumberRoundMode const _{Number::getround()};
// set rounding to always favor the amm. Clip to zero.
// calculate:
// pool.out -
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
// and explicitly set the rounding modes
// Favoring the amm means we should:
// minimize:
// pool.out -
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
// maximize:
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
// (pool.in * pool.out)
// minimize:
// (pool.in + assetIn * feeMult(tfee)),
// minimize:
// assetIn * feeMult(tfee)
// feeMult is: (1-fee), fee is tfee/100000
// minimize:
// 1-fee
// maximize:
// fee
SaveNumberRoundMode const _{Number::getround()};
Number::setround(Number::RoundingMode::Upward);
auto const numerator = pool.in * pool.out;
auto const fee = getFee(tfee);
Number::setround(Number::RoundingMode::Upward);
auto const numerator = pool.in * pool.out;
auto const fee = getFee(tfee);
Number::setround(Number::RoundingMode::Downward);
auto const denom = pool.in + assetIn * (1 - fee);
Number::setround(Number::RoundingMode::Downward);
auto const denom = pool.in + assetIn * (1 - fee);
if (denom.signum() <= 0)
return toAmount<TOut>(getAsset(pool.out), 0);
if (denom.signum() <= 0)
return toAmount<TOut>(getAsset(pool.out), 0);
Number::setround(Number::RoundingMode::Upward);
auto const ratio = numerator / denom;
Number::setround(Number::RoundingMode::Upward);
auto const ratio = numerator / denom;
Number::setround(Number::RoundingMode::Downward);
auto const swapOut = pool.out - ratio;
Number::setround(Number::RoundingMode::Downward);
auto const swapOut = pool.out - ratio;
if (swapOut.signum() < 0)
return toAmount<TOut>(getAsset(pool.out), 0);
if (swapOut.signum() < 0)
return toAmount<TOut>(getAsset(pool.out), 0);
return toAmount<TOut>(getAsset(pool.out), swapOut, Number::RoundingMode::Downward);
}
return toAmount<TOut>(
getAsset(pool.out),
pool.out - (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
Number::RoundingMode::Downward);
return toAmount<TOut>(getAsset(pool.out), swapOut, Number::RoundingMode::Downward);
}
/**
@@ -526,57 +455,49 @@ template <typename TIn, typename TOut>
TIn
swapAssetOut(TAmounts<TIn, TOut> const& pool, TOut const& assetOut, std::uint16_t tfee)
{
if (auto const& rules = getCurrentTransactionRules(); rules && rules->enabled(fixAMMv1_1))
// set rounding to always favor the amm. Clip to zero.
// calculate:
// ((pool.in * pool.out) / (pool.out - assetOut) - pool.in) /
// (1-tfee/100000)
// maximize:
// ((pool.in * pool.out) / (pool.out - assetOut) - pool.in)
// maximize:
// (pool.in * pool.out) / (pool.out - assetOut)
// maximize:
// (pool.in * pool.out)
// minimize
// (pool.out - assetOut)
// minimize:
// (1-tfee/100000)
// maximize:
// tfee/100000
SaveNumberRoundMode const _{Number::getround()};
Number::setround(Number::RoundingMode::Upward);
auto const numerator = pool.in * pool.out;
Number::setround(Number::RoundingMode::Downward);
auto const denom = pool.out - assetOut;
if (denom.signum() <= 0)
{
// set rounding to always favor the amm. Clip to zero.
// calculate:
// ((pool.in * pool.out) / (pool.out - assetOut) - pool.in) /
// (1-tfee/100000)
// maximize:
// ((pool.in * pool.out) / (pool.out - assetOut) - pool.in)
// maximize:
// (pool.in * pool.out) / (pool.out - assetOut)
// maximize:
// (pool.in * pool.out)
// minimize
// (pool.out - assetOut)
// minimize:
// (1-tfee/100000)
// maximize:
// tfee/100000
SaveNumberRoundMode const _{Number::getround()};
Number::setround(Number::RoundingMode::Upward);
auto const numerator = pool.in * pool.out;
Number::setround(Number::RoundingMode::Downward);
auto const denom = pool.out - assetOut;
if (denom.signum() <= 0)
{
return toMaxAmount<TIn>(getAsset(pool.in));
}
Number::setround(Number::RoundingMode::Upward);
auto const ratio = numerator / denom;
auto const numerator2 = ratio - pool.in;
auto const fee = getFee(tfee);
Number::setround(Number::RoundingMode::Downward);
auto const feeMult = 1 - fee;
Number::setround(Number::RoundingMode::Upward);
auto const swapIn = numerator2 / feeMult;
if (swapIn.signum() < 0)
return toAmount<TIn>(getAsset(pool.in), 0);
return toAmount<TIn>(getAsset(pool.in), swapIn, Number::RoundingMode::Upward);
return toMaxAmount<TIn>(getAsset(pool.in));
}
return toAmount<TIn>(
getAsset(pool.in),
((pool.in * pool.out) / (pool.out - assetOut) - pool.in) / feeMult(tfee),
Number::RoundingMode::Upward);
Number::setround(Number::RoundingMode::Upward);
auto const ratio = numerator / denom;
auto const numerator2 = ratio - pool.in;
auto const fee = getFee(tfee);
Number::setround(Number::RoundingMode::Downward);
auto const feeMult = 1 - fee;
Number::setround(Number::RoundingMode::Upward);
auto const swapIn = numerator2 / feeMult;
if (swapIn.signum() < 0)
return toAmount<TIn>(getAsset(pool.in), 0);
return toAmount<TIn>(getAsset(pool.in), swapIn, Number::RoundingMode::Upward);
}
/**
+125 -31
View File
@@ -7,10 +7,10 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/LedgerFormats.h> // IWYU pragma: keep
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
@@ -31,9 +31,9 @@ namespace xrpl {
/**
* Broker cover preclaim precision guard (fixCleanup3_2_0).
*
* Prevents a "silent sub-ULP no-op" where a deposit, withdrawal, or clawback
* amount is so small that it rounds to zero at `sfCoverAvailable`'s scale.
* Without this guard, both the pseudo trust-line and `sfCoverAvailable` would
* Prevents a silent sub-ULP no-op where a deposit, withdrawal, or clawback
* amount is so small that it rounds to zero at sfCoverAvailable's scale.
* Without this guard, both the pseudo trust-line and sfCoverAvailable would
* identically absorb the rounded zero, resulting in a successful transaction
* (tesSUCCESS) where no funds actually moved.
*
@@ -44,8 +44,8 @@ namespace xrpl {
* @param j Journal for logging.
* @param logPrefix Transactor name for log diagnostics.
*
* @return `tecPRECISION_LOSS` if the request rounds to zero at cover scale.
* `tesSUCCESS` if the amendment is disabled or the request is safely supra-ULP.
* @return tecPRECISION_LOSS if the request rounds to zero at cover scale.
* tesSUCCESS if the amendment is disabled or the request is safely supra-ULP.
*/
[[nodiscard]] TER
canApplyToBrokerCover(
@@ -56,6 +56,82 @@ canApplyToBrokerCover(
beast::Journal j,
std::string_view logPrefix);
namespace detail {
/**
* Return a LoanBroker's posterior live cover exponent after applying an
* unrounded delta.
*/
[[nodiscard]] int
getPosteriorBrokerCoverScale(SLE::ConstRef vault, SLE::ConstRef broker, Number const& delta);
/**
* Round a cover outflow delta (cover withdraw, cover clawback, the
* broker-side decrease of a default's cover credit) at the LoanBroker's
* posterior live exponent. Safe to round the delta directly for an outflow:
* amount on the posterior grid of CoverAvailable always leaves CoverAvailable
* minus amount representable, since the posterior grid is exactly the grid
* CoverAvailable itself will canonicalize to after the subtraction.
*/
[[nodiscard]] STAmount
roundToPosteriorBrokerCoverScale(
SLE::ConstRef vault,
SLE::ConstRef broker,
STAmount const& delta,
Number::RoundingMode roundingMode);
} // namespace detail
/**
* Round a cover debit (cover withdraw, cover clawback, the broker-side
* decrease of a default's cover credit): amount is the non-negative
* magnitude leaving CoverAvailable. Negates so the posterior is computed for
* CoverAvailable minus amount, then negates the result back to a
* non-negative magnitude, per roundToPosteriorBrokerCoverScale's outflow
* rationale above. Magnitude in, magnitude out -- unlike the vault-side
* outflow dispatcher (clampVaultOutflow), which takes and returns a negative
* delta; callers on each side already hold the value in that side's native
* form.
*/
[[nodiscard]] STAmount
debitToPosteriorBrokerCoverScale(
SLE::ConstRef vault,
SLE::ConstRef broker,
STAmount const& amount,
Number::RoundingMode roundingMode);
/**
* Round a cover inflow (cover deposit, LoanPay's fee redirect into cover)
* into CoverAvailable by flooring the sum rather than the delta:
* credit = floor16(CoverAvailable + raw) - CoverAvailable, never finer
* than the Vault's base exponent (-Scale). See
* creditToPosteriorAvailableScale's doc for why the sum, not the delta,
* must be floored; the same reasoning applies to CoverAvailable.
*/
[[nodiscard]] STAmount
creditToPosteriorBrokerCoverScale(
SLE::ConstRef vault,
SLE::ConstRef broker,
Number const& raw,
Number::RoundingMode roundingMode);
/**
* Check whether amount is an admissible optional cover inflow.
*
* Legacy and CashBasis Vaults always succeed. A LoanBroker attached to a
* FixedPrecision Vault must remain at the Vault's base scale after applying
* the rounded amount, and its posterior CoverAvailable must stay within the
* Open zone.
*
* LoanPay's fee redirect into cover is a mandatory inflow and does not call
* this check, so it can push CoverAvailable past the Open zone limit this
* check enforces. Once that happens, every subsequent LoanBrokerCoverDeposit
* returns tecLIMIT_EXCEEDED until the owner withdraws cover back under the
* limit; LoanBrokerCoverWithdraw and LoanBrokerCoverClawback are unaffected.
*/
[[nodiscard]] TER
checkOptionalBrokerCoverInflow(SLE::ConstRef vault, SLE::ConstRef broker, STAmount const& amount);
// Lending protocol has dependencies, so capture them here.
bool
checkLendingProtocolDependencies(Rules const& rules, STTx const& tx);
@@ -64,13 +140,13 @@ checkLendingProtocolDependencies(Rules const& rules, STTx const& tx);
* The accounts and asset that LoanManage::defaultLoan's fixCleanup3_4_0
* freeze/lock exemption applies to.
*
* `defaultLoan` moves funds from the LoanBroker pseudo-account to the Vault
* pseudo-account via `accountSend`. Since neither is the vault asset's
* defaultLoan moves funds from the LoanBroker pseudo-account to the Vault
* pseudo-account via accountSend. Since neither is the vault asset's
* issuer, this is a third-party transfer that transits through the issuer in
* two hops (broker -> issuer, issuer -> vault; see
* `directSendNoLimitIOU`/`directSendNoLimitMPT`), so the exemption must cover
* directSendNoLimitIOU/directSendNoLimitMPT), so the exemption must cover
* both the issuer/broker and issuer/vault pairs, not a direct broker/vault
* pair. `asset` scopes it further to the vault's own currency/MPT issuance,
* pair. asset scopes it further to the vault's own currency/MPT issuance,
* so an unrelated one the same accounts happen to hold is still protected.
*/
struct LoanDefaultFreezeExemptAccounts
@@ -88,10 +164,10 @@ struct LoanDefaultFreezeExemptAccounts
* @param view Ledger view used to resolve the Loan -> LoanBroker -> Vault
* chain.
* @param tx The transaction under invariant review.
* @return The exempt accounts and asset if `tx` is a `ttLOAN_MANAGE`
* transaction with the `tfLoanDefault` flag set, `fixCleanup3_4_0` is
* @return The exempt accounts and asset if tx is a ttLOAN_MANAGE
* transaction with the tfLoanDefault flag set, fixCleanup3_4_0 is
* enabled, and the loan/broker/vault objects it references can all be
* resolved; `std::nullopt` otherwise.
* resolved; std::nullopt otherwise.
*/
[[nodiscard]] std::optional<LoanDefaultFreezeExemptAccounts>
getLoanDefaultFreezeExemptAccounts(ReadView const& view, STTx const& tx);
@@ -262,20 +338,38 @@ getAssetsTotalScale(SLE::ConstRef vaultSle)
return scale(vaultSle->at(sfAssetsTotal), vaultSle->at(sfAsset));
}
// Compute the minimum required broker cover, rounded consistently.
// DebtTotal is a broker-level aggregate maintained at vault scale, so the
// rounding must also use vault scale — never an individual loan's scale.
inline Number
minimumBrokerCover(Number const& debtTotal, TenthBips32 coverRateMinimum, SLE::ConstRef vaultSle)
{
XRPL_ASSERT(
vaultSle && vaultSle->getType() == ltVAULT, "xrpl::minimumBrokerCover : valid Vault sle");
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
return roundToAsset(
vaultSle->at(sfAsset),
tenthBipsOfValue(debtTotal, coverRateMinimum),
getAssetsTotalScale(vaultSle));
}
/**
* Apply a signed delta to a LoanBroker's DebtTotal: on FixedPrecision Vaults,
* add the exact delta (DebtTotal moves by exactly the same amount as the
* Vault's AssetsDeployed, never re-rounded); on Legacy/CashBasis Vaults,
* round through adjustImpreciseNumber at vaultScale. Shared by LoanSet
* (origination) and LoanPay (payment, where delta is negative).
*
* @param brokerSle The LoanBroker whose DebtTotal is adjusted.
* @param vaultSle The LoanBroker's Vault.
* @param delta The signed change to DebtTotal.
* @param vaultScale The Vault scale the caller captured before changing
* AssetsTotal. Legacy/CashBasis round DebtTotal at that scale, not at
* the scale after this transaction's AssetsTotal change. Unused on
* FixedPrecision Vaults.
*/
void
adjustBrokerDebtTotal(
SLE::Ref brokerSle,
SLE::ConstRef vaultSle,
Number const& delta,
int vaultScale);
/**
* Minimum required broker cover, rounded up.
*
* DebtTotal is a broker-level aggregate, never rounded at an individual
* loan's scale. Legacy and CashBasis Vaults round at the live AssetsTotal
* exponent. FixedPrecision Vaults round at the Vault's base exponent
* (-Scale, or 0 for integral assets).
*/
Number
minimumBrokerCover(Number const& debtTotal, TenthBips32 coverRateMinimum, SLE::ConstRef vaultSle);
TER
checkLoanGuards(
@@ -379,10 +473,10 @@ loanPaymentDeltas(LoanPaymentParts const& parts);
} // namespace cash_basis
// Public dispatchers: pick cash_basis:: if featureLendingProtocolV1_1 is
// enabled AND the Vault's LEVersion (VaultHelpers::getVaultVersion) is
// VaultVersion::CashBasis, else instant_recognition::. These are the only entry points
// transactors call.
// Public dispatchers: pick cash_basis:: if the Vault's LEVersion
// (VaultHelpers::getVaultVersion) is CashBasis or later (CashBasis or
// FixedPrecision), else instant_recognition::. These are the only entry
// points transactors call.
AccountingDeltas
loanOriginationDeltas(
SLE::ConstRef vaultSle,
+25 -4
View File
@@ -312,12 +312,22 @@ checkCreateMPT(
//
//------------------------------------------------------------------------------
// MaximumAmount doesn't exceed 2**63-1
/**
* MaximumAmount doesn't exceed 2**63-1
*/
std::int64_t
maxMPTAmount(SLE const& sleIssuance);
// OutstandingAmount may overflow and available amount might be negative.
// But available amount is always <= |MaximumAmount - OutstandingAmount|.
/**
* std::nullopt if the issuance doesn't exist.
*/
std::optional<std::int64_t>
maxMPTAmount(ReadView const& view, MPTID const& mptID);
/**
* OutstandingAmount may overflow and available amount might be negative.
* But available amount is always <= |MaximumAmount - OutstandingAmount|.
*/
std::int64_t
availableMPTAmount(SLE const& sleIssuance);
@@ -331,7 +341,12 @@ availableMPTAmount(ReadView const& view, MPTID const& mptID);
* directSendNoFee transactions that bypass the payment engine.
* 2. **accountSend & Payment Engine (Overflow: Yes):** A temporary overflow
* check when `OutstandingAmount > UINT64_MAX`. This higher threshold is used
* for `accountSend` and payments processed via the payment engine.
* for `accountSend` and payments processed via the payment engine, whose
* reverse pass may issue before it redeems.
*
* In both cases a single `sendAmount > MaximumAmount` is rejected. Such a send
* can never settle: redemptions within the transaction are bounded by the
* outstanding amount, so the final OutstandingAmount would exceed the cap.
*/
bool
isMPTOverflow(
@@ -353,6 +368,12 @@ issuerFundsToSelfIssue(ReadView const& view, MPTIssue const& issue);
/**
* Facilitate tracking of MPT sold by an issuer owning MPT sell offer.
* See ApplyView::issuerSelfDebitHookMPT().
*
* `amount` may be 0. BookStep removes an offer whose fee-adjusted funded
* output is 0, but other clips (for example MaximumAmount via limitStepIn)
* can still consume a zero slice of a funded remainder. The hook must
* accept 0: first call still snapshots origBalance; a zero debit adds
* nothing to selfDebit.
*/
void
issuerSelfDebitHookMPT(ApplyView& view, MPTIssue const& issue, std::uint64_t amount);
+108 -21
View File
@@ -1,10 +1,17 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <cstdint>
#include <optional>
namespace xrpl::proposal {
@@ -30,31 +37,22 @@ proposalOwnerCount(STObject const& proposedTx)
: kProposalOwnerCount;
}
/**
* Whether the proposed transaction is itself a proposal transaction, which
* would nest one proposal inside another.
*
* TODO: cover ttTRANSACTION_PROPOSAL_SIGN and ttTRANSACTION_PROPOSAL_CANCEL
* once those transactions exist.
*/
inline bool
isProposalTx(STObject const& proposedTx)
{
return proposedTx.getFieldU16(sfTransactionType) == ttTRANSACTION_PROPOSAL_CREATE;
}
/**
* Whether the proposed transaction is independently submittable through the
* ordinary multi-sign path: not a nested proposal, not a pseudo-transaction,
* not itself flagged as someone else's inner batch transaction, and — if it
* is a Batch — none of its own inner transactions is a nested proposal or a
* pseudo-transaction either. A Batch inner transaction cannot itself be
* pseudo (preflight0 rejects the pseudo/tfInnerBatchTxn combination
* generically), but that guard lives outside this feature, so it is checked
* again here rather than relied upon.
* ordinary multi-sign path: not a proposal transaction itself, not a
* pseudo-transaction, not itself flagged as someone else's inner batch
* transaction, and — if it is a Batch — every one of its own inner
* transactions carries a TransactionType and is neither a proposal, a
* pseudo-transaction, nor a nested Batch. A Batch inner transaction cannot
* lack a TransactionType, be pseudo, or be a nested Batch (STTx construction
* rejects a typeless inner and an inner Batch, and preflight0 rejects the
* pseudo/tfInnerBatchTxn combination), but those guards live outside this
* feature, so they are checked again here rather than relied upon.
*
* TODO: cover ttTRANSACTION_PROPOSAL_SIGN once that transaction exists.
*/
bool
isValidProposal(STObject const& proposedTx);
isValidProposalTxnType(STObject const& proposedTx);
/**
* Whether the proposed transaction carries any signature field.
@@ -84,4 +82,93 @@ hasEmptySigningPubKey(STObject const& proposedTx)
proposedTx.getFieldVL(sfSigningPubKey).empty();
}
/**
* Whether the proposal is terminal (On-Chain Cosigner spec §4.5). A terminal proposal
* stops accepting signatures and anyone may delete it. An already signed
* payload can still execute after Expiration, because the Ticket reservation
* compares payloads only; a passed LastLedgerSequence does stop it.
*
* A proposal is terminal when either:
* - its Expiration has passed (the parent ledger closed at or after it), or
* - the proposed transaction carries a LastLedgerSequence and the current
* ledger sequence is strictly greater than it. This mirrors the ordinary
* transactor's tefMAX_LEDGER rule: at view.seq() == LastLedgerSequence the
* payload can still be submitted in the current ledger, so the proposal is
* still live.
*
* @param view The ledger the deciding transaction is being applied to.
* @param expiration The proposal's Expiration field.
* @param proposedTx The proposal's ProposedTransaction field.
*/
bool
isTerminal(
ReadView const& view,
std::optional<std::uint32_t> expiration,
STObject const& proposedTx);
/**
* Delete a TransactionProposal ledger entry.
*
* Removes the entry from its Owner's directory, releases the reserve the
* proposal holds against the Owner, and erases the entry. Shared by every
* deletion path the spec defines (On-Chain Cosigner spec §4.5): automatic cleanup when the
* proposed transaction's TicketSequence is consumed, and the
* TransactionProposalCancel / TransactionProposalSign cleanup paths once
* those transactions exist.
*
* The release goes through decreaseOwnerCountForObject, so it follows the
* sfSponsor an entry created with reserve sponsorship carries: the reserve is
* returned to whoever was holding it, the Owner or that sponsor.
*
* @param view The apply view for making changes
* @param sleProposal The TransactionProposal ledger entry to delete
* @param j Journal for logging
* @return tesSUCCESS, tecINTERNAL for an invalid entry, or tefBAD_LEDGER if
* the ledger contradicts the entry
*/
TER
deleteProposal(ApplyView& view, SLE::pointer const& sleProposal, beast::Journal j);
/**
* Whether tx carries the same payload as a proposal's stored
* ProposedTransaction (On-Chain Cosigner spec §4.2.1).
*
* The payload is the signing payload: the fields the collected signatures
* commit to. Compared is every field except those flagged kNotSigning
* (TxnSignature, Signers, BatchSigners, CounterpartySignature,
* SponsorSignature, Signature, MasterSignature) and SigningPubKey, which is
* stored empty and filled only if the target signs with its own key. So the
* completed transaction matches no matter which mix of collected or
* off-ledger signatures it carries, and any change to a non-signature field
* results in a false-return value.
*
* Signature (PaymentChannelClaim and the XChain attestation transactions)
* is deliberately not compared: it is outside the account's own TxnSignature
* even for an ordinary submission, so a proposal cannot bind it more tightly
* than the signatures it collects do, and those transactors still validate a
* substituted value exactly as they would for an ordinary submission.
* MasterSignature appears in no transaction format.
*
* @param proposedTx The proposal's ProposedTransaction field.
* @param tx The transaction to compare, typically an STTx.
*/
bool
payloadMatches(STObject const& proposedTx, STObject const& tx);
/**
* Whether tx may consume its Ticket.
*
* While a proposal keyed to the Ticket exists, the target's Ticket is
* reserved: only the proposal's own proposed transaction may spend it
* (On-Chain Cosigner spec §4.2.1), so that unrelated target-account activity cannot
* invalidate the proposal while signatures are being collected.
* TransactionProposalCancel is not allowed through: it deletes the
* reservation without consuming the ticket.
*
* @param view The ledger containing the Ticket and any reservation.
* @param tx The transaction attempting to consume that ticket.
*/
bool
canConsumeTicket(ReadView const& view, STTx const& tx);
} // namespace xrpl::proposal
@@ -316,6 +316,20 @@ 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)
+352 -29
View File
@@ -1,10 +1,12 @@
#pragma once
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
@@ -12,11 +14,237 @@
#include <cstdint>
#include <expected>
#include <optional>
#include <string_view>
namespace xrpl {
class STTx;
/**
* @brief Return the Vault's current assets total.
*
* FixedPrecision Vaults return AssetsAvailable plus AssetsDeployed, with no
* rounding to the asset's 16-digit precision: the sum is exact at Number's
* active 19-digit mantissa width (the large mantissa range, enabled by
* featureSingleAssetVault or featureLendingProtocol), rounds Downward regardless of the
* caller's ambient mode beyond that, and is not rounded to match the cached
* sfAssetsTotal field. Legacy and CashBasis Vaults return the stored
* AssetsTotal.
*
* @param vault The vault SLE.
* @return The current assets total.
*/
[[nodiscard]] Number
getAssetsTotal(SLE::ConstRef vault);
/**
* @brief A change to a FixedPrecision Vault's balances.
*/
struct VaultBalanceChange
{
/**
* Signed change to AssetsAvailable. Must be the exact amount moved on the
* Vault pseudo-account in the same transaction. Absent means no cash
* moved; adjustVaultBalances then uses zero of the Vault's own asset.
*/
std::optional<STAmount> cash = std::nullopt;
/**
* Signed change to AssetsDeployed: open Loan principal, exact.
*/
Number deployed = 0;
/**
* Signed change to scheduled, unpaid interest (YieldUnrealized), exact.
*/
Number yield = 0;
/**
* Signed change to impaired principal (LossUnrealized), exact.
*/
Number loss = 0;
};
/**
* @brief Apply a balance change to a FixedPrecision Vault. The only writer of
* AssetsAvailable, AssetsDeployed, AssetsTotal, YieldUnrealized and
* LossUnrealized on these Vaults after creation.
*
* @param vault The vault SLE. Must be FixedPrecision.
* @param change The balance change to apply.
* @param j Journal for logging the fatal (tefBAD_LEDGER) case.
*
* @return tesSUCCESS; tecLIMIT_EXCEEDED if LossUnrealized would exceed
* AssetsDeployed; tefBAD_LEDGER if AssetsAvailable, AssetsDeployed,
* LossUnrealized or YieldUnrealized would become negative.
*/
[[nodiscard]] TER
adjustVaultBalances(SLE::Ref vault, VaultBalanceChange const& change, beast::Journal j);
/**
* @brief Apply a signed cash movement to a Vault of any version. FixedPrecision
* Vaults go through adjustVaultBalances; Legacy and CashBasis Vaults add the
* amount to both AssetsTotal and AssetsAvailable.
*
* @param vault The vault SLE.
* @param delta Signed change to AssetsAvailable; the exact amount moved on the
* Vault pseudo-account.
* @param j Journal passed through to adjustVaultBalances.
*
* @return tesSUCCESS, or the adjustVaultBalances error for FixedPrecision Vaults.
*/
[[nodiscard]] TER
adjustVaultCash(SLE::Ref vault, STAmount const& delta, beast::Journal j);
/**
* Return the Vault's current live exponent.
*
* Legacy and CashBasis Vaults use the exponent of AssetsTotal. FixedPrecision
* Vaults use max(base exponent, exponent of AssetsTotal): the grid is never
* finer than -Scale and coarsens once AssetsTotal outgrows 16 digits there.
*/
[[nodiscard]] int
getVaultScale(SLE::ConstRef vault);
/**
* Return the Vault's base exponent.
*
* Legacy and CashBasis Vaults use their current live exponent. FixedPrecision
* Vaults use -Scale, or 0 for integral assets.
*/
[[nodiscard]] int
getVaultBaseScale(SLE::ConstRef vault);
/**
* Round an amount at the Vault's posterior live exponent.
*/
[[nodiscard]] STAmount
roundToPosteriorVaultScale(
SLE::ConstRef vault,
STAmount const& amount,
Number::RoundingMode roundingMode);
namespace detail {
/**
* Return the Vault's posterior live exponent after applying an unrounded delta.
*/
[[nodiscard]] int
getPosteriorVaultScale(SLE::ConstRef vault, STAmount const& delta);
/**
* Shared grid-floor core of getVaultScale / getPosteriorVaultScale /
* getPosteriorBrokerCoverScale: baseScale when reference is zero, otherwise
* max(baseScale, scale(reference, asset)), rounded Downward regardless of
* the caller's ambient rounding mode.
*/
[[nodiscard]] int
liveScale(Number const& reference, Asset const& asset, int baseScale);
/**
* Shared core of getPosteriorVaultScale and getPosteriorBrokerCoverScale:
* the exponent reference + delta would have under version's live-scale rule
* (floored at baseScale for FixedPrecision, unbounded for Legacy/CashBasis).
*
* @param version The Vault's LEVersion.
* @param asset The Vault's underlying asset.
* @param baseScale The Vault's base exponent; unused outside the
* FixedPrecision case.
* @param reference The balance the delta is applied to (AssetsAvailable,
* AssetsTotal, or a LoanBroker's CoverAvailable).
* @param delta The unrounded change to reference.
*/
[[nodiscard]] int
posteriorAssetScale(
VaultVersion version,
Asset const& asset,
int baseScale,
Number const& reference,
Number const& delta);
/**
* Shared core of creditToPosteriorAvailableScale and
* creditToPosteriorBrokerCoverScale: floors reference + raw at scale and
* returns the difference from reference, so the delta applied is exactly
* what moves reference onto the floored sum. See
* creditToPosteriorAvailableScale's doc for why the sum, not raw alone,
* must be floored. raw is the exact amount; it is not rounded on its own
* before being added to reference.
*/
[[nodiscard]] STAmount
creditToPosteriorScale(
Asset const& asset,
Number const& reference,
int atScale,
Number const& raw,
Number::RoundingMode roundingMode);
} // namespace detail
/**
* Round a FixedPrecision cash inflow (LoanPay's credit, LoanManage's
* default cover credit) into AssetsAvailable, never AssetsTotal, which is
* a derived cache, by flooring the sum rather than the delta:
* credit equals floor16(AssetsAvailable + raw) minus AssetsAvailable, never
* finer than the Vault's base exponent (-Scale).
*
* Flooring only the delta at its own posterior grid is not enough: the sum
* can still need a 17th digit when AssetsAvailable is off the coarser grid
* right after crossing a power of ten. Example at Scale 6: AssetsAvailable
* equals 9999999999.999999 (16 digits already) and a credit of 0.000011
* floors to 0.00001 on its own (finer) grid, but 9999999999.999999 plus
* 0.00001 equals 10000000000.000009, which still needs 17 digits. Flooring
* the sum instead, floor16(9999999999.999999 + 0.000011) minus
* 9999999999.999999, is exact by construction, since STAmount's own
* 16-digit canonical form of the sum is what gets subtracted from.
*
* raw is taken as an exact Number: rounding it to 16 digits before adding
* it to AssetsAvailable could drop a tail that moves the floored sum.
*/
[[nodiscard]] STAmount
creditToPosteriorAvailableScale(
SLE::ConstRef vault,
Number const& raw,
Number::RoundingMode roundingMode);
/**
* Open-zone capacity ceiling: 9 * 10^(15 + baseScale).
*
* Defined only for FixedPrecision Vaults, where this is 9 * 10^(15 - P).
*/
[[nodiscard]] Number
getVaultOpenLimit(SLE::ConstRef vault);
/**
* Check whether amount is an admissible optional inflow.
*
* Legacy and CashBasis Vaults always succeed. FixedPrecision Vaults must
* remain at their base scale after applying the rounded amount, and the
* posterior capacity (AssetsTotal + YieldUnrealized + rounded amount) must
* stay within the Open zone.
*
* The amount is rounded toward zero at the posterior live exponent.
*/
[[nodiscard]] TER
checkOptionalVaultInflow(SLE::ConstRef vault, STAmount const& amount);
/**
* Open-zone capacity after adding roundedAmount: AssetsTotal + YieldUnrealized
* + roundedAmount, computed TowardsZero regardless of the caller's ambient
* rounding mode. Defined only for FixedPrecision Vaults.
*
* Shared by checkOptionalVaultInflow and LoanSet's loan-origination capacity
* check, which pair this formula with different scale checks (posterior vs.
* current), so only the capacity formula itself -- not the whole check -- is
* shared here.
*
* @param vault The vault SLE. Must be FixedPrecision.
* @param roundedAmount The amount to add, already rounded to the relevant
* posterior or base scale by the caller.
*/
[[nodiscard]] Number
vaultOpenZoneCapacity(SLE::ConstRef vault, Number const& roundedAmount);
/**
* From the perspective of a vault, return the number of shares to give
* depositor when they offer a fixed amount of assets. Note, since shares are
@@ -46,31 +274,93 @@ assetsToSharesDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount cons
sharesToAssetsDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount const& shares);
/**
* Adjusts a requested asset change (`delta`) to match the decimal scale of the
* updated total vault assets. This ensures `sfAssetsTotal`, `sfAssetsAvailable`,
* Adjusts a requested asset change (delta) to match the decimal scale of the
* updated total vault assets. This ensures sfAssetsTotal, sfAssetsAvailable,
* and the actual asset transfer change by the exact same representable amount.
*
* Rounding strategy:
* - Debits (withdrawals): Rounds down `|delta|` on the new scale to prevent
* paying out more than requested.
* - Credits (deposits): Floors the resulting total asset balance and returns the
* difference from the current total. This prevents crediting the vault with
* more assets than the user deposited.
* - Legacy/CashBasis debits (withdrawals): rounds down the magnitude of delta
* on the new scale to prevent paying out more than requested.
* - Legacy/CashBasis credits: floors the resulting total asset balance and
* returns the difference from the current total.
* - FixedPrecision credits only (VaultDeposit): rounds the delta toward zero
* at the scale getAssetsTotal (AssetsAvailable + AssetsDeployed, derived,
* not the stored AssetsTotal cache) would have after applying delta.
* FixedPrecision outflows (VaultWithdraw, VaultClawback) never call this;
* they use clampVaultOutflow instead.
*
* Key rules:
* - The returned magnitude never exceeds `|delta|`.
* - Returns `tecPRECISION_LOSS` if the change is smaller than 1 ULP of the target scale
* - The returned magnitude never exceeds the magnitude of delta.
* - Returns tecPRECISION_LOSS if the change is smaller than 1 ULP of the target scale
* (prevents share operations when totals cannot change).
* - For integer assets (XRP, MPT), rounding is a no-op.
*
* @param vault The vault ledger entry.
* @param delta The requested signed change to sfAssetsTotal.
* @return The rounded, positive magnitude, or `tecPRECISION_LOSS` if the
* @return The rounded, positive magnitude, or tecPRECISION_LOSS if the
* change is below representable precision.
*/
[[nodiscard]] std::expected<STAmount, TER>
clampToAssetsTotalScale(SLE::ConstRef vault, STAmount const& delta);
/**
* Outflow clamp dispatcher for VaultWithdraw and VaultClawback: rounds a
* negative delta to the scale appropriate for the Vault's version -- for
* FixedPrecision, at AssetsAvailable's own posterior scale (the balance the
* cash actually leaves); for Legacy/CashBasis, clampToAssetsTotalScale --
* so both call sites share one dispatch point. Same tecPRECISION_LOSS and
* integral-asset and magnitude-never-exceeds-delta rules as
* clampToAssetsTotalScale.
*
* @param vault The vault ledger entry.
* @param delta The requested signed outflow (negative).
* @return The rounded, positive magnitude, or tecPRECISION_LOSS if the
* change is below representable precision.
*/
[[nodiscard]] std::expected<STAmount, TER>
clampVaultOutflow(SLE::ConstRef vault, STAmount const& delta);
/**
* Returns the Vault's base exponent for asset at scale, before a Vault
* object exists to read it from: 0 for an integral asset, -scale otherwise.
* Used by VaultCreate::preclaim, which only has the proposed Scale field,
* not yet a Vault SLE; getVaultBaseScale is the post-creation equivalent.
*
* @param asset The (prospective) Vault's underlying asset.
* @param scale The (prospective) Vault's sfScale value, ignored for
* integral assets.
*/
[[nodiscard]] int
vaultBaseScale(Asset const& asset, std::uint8_t scale);
/**
* Returns true iff value is exactly representable both as an STAmount of
* asset and on the base grid at baseScale (no precision lost rounding
* TowardsZero at baseScale). Returns false, rather than throwing, for a value
* out of range for the asset. Shared representability check for
* VaultCreate::preclaim, VaultSet::preclaim and LoanBrokerSet::preclaim's
* AssetsMaximum / DebtMaximum / cover-rate field checks.
*
* @param asset The vault's underlying asset.
* @param value The value to check.
* @param baseScale The vault's base exponent (see vaultBaseScale /
* getVaultBaseScale).
*/
[[nodiscard]] bool
isOnVaultBaseGrid(Asset const& asset, Number const& value, int baseScale);
/**
* Checks that a requested AssetsMaximum is exactly representable on the
* Vault's base grid, otherwise associateAsset would silently round the cap
* the owner asked for. Used by VaultSet::preclaim on an existing Vault.
*
* @param vault The vault ledger entry.
* @param amount The requested AssetsMaximum.
* @return tesSUCCESS, or tecPRECISION_LOSS if amount is not representable.
*/
[[nodiscard]] TER
checkAssetsMaximum(SLE::ConstRef vault, Number const& amount);
/**
* Controls whether to truncate shares instead of rounding.
*/
@@ -98,21 +388,24 @@ enum class WaiveUnrealizedLoss : bool { No = false, Yes = true };
assetsTotalForWithdrawal(SLE::ConstRef vault, WaiveUnrealizedLoss waive);
/**
* Returns true if debiting `amount` from `total` (the current value of a
* vault's sfAssetsTotal or sfAssetsAvailable) would canonicalize to the
* same STAmount value. This happens when `amount` is non-zero but too small
* to change the stored total at STAmount's precision. Shares would still
* move, so the ValidVault invariant would fail after apply; callers use
* this to reject the transaction upfront instead.
* Reject a cash outflow that is dust (see debitIsNonZeroDust) or whose check
* overflows Number, which is easy to hit with a large Scale.
*
* @param asset The vault's underlying asset, used to canonicalize both
* sides the same way the ledger will when the field is stored.
* @param total The field's current value.
* @param amount The amount to debit. Zero always returns false; that case
* is rejected separately.
* @param vault The vault SLE.
* @param amount The amount to debit.
* @param j Journal for the debug log.
* @param name Transaction name used as the log prefix.
*
* @return tesSUCCESS; tecPRECISION_LOSS if amount is dust; tecPATH_DRY if the
* check overflows. Overflow means the transaction cannot apply, but
* ledger state is still consistent.
*/
[[nodiscard]] bool
debitIsNonZeroDust(Asset const& asset, Number const& total, Number const& amount);
[[nodiscard]] TER
checkDebitNotDust(
SLE::ConstRef vault,
Number const& amount,
beast::Journal j,
std::string_view name);
/**
* From the perspective of a vault, return the number of shares to demand from
@@ -158,7 +451,7 @@ sharesToAssetsWithdraw(
WaiveUnrealizedLoss waive = WaiveUnrealizedLoss::No);
/**
* Returns true iff `account` holds all of the vault's outstanding shares —
* Returns true iff account holds all of the vault's outstanding shares,
* i.e. is the sole remaining shareholder. Returns false if the account
* holds no shares or fewer than the total outstanding.
*
@@ -171,11 +464,13 @@ sharesToAssetsWithdraw(
isSoleShareholder(ReadView const& view, AccountID const& account, SLE::ConstRef issuance);
/**
* Resolves a Vault's LEVersion, the single point every accounting touch
* point should call to determine which recognition model (instant interest
* recognition vs. cash-basis) a Vault uses. Vaults created before featureLendingProtocolV1_1
* activated never have sfLEVersion set, which resolves here to
* VaultVersion::Legacy.
* Resolves a Vault's LEVersion.
*
* LEVersion is the single point every accounting and rounding helper
* should call to decide which protocol a Vault follows. It is written
* at VaultCreate and is not updated afterwards, so a Vault created
* under an older amendment keeps that behaviour after later amendments
* activate. Absent sfLEVersion resolves to VaultVersion::Legacy.
*
* @param vault The vault SLE.
*
@@ -185,6 +480,34 @@ isSoleShareholder(ReadView const& view, AccountID const& account, SLE::ConstRef
[[nodiscard]] VaultVersion
getVaultVersion(SLE::ConstRef vault);
/**
* Decodes an already-extracted sfLEVersion value with the same range check as
* getVaultVersion. Usable from contexts, such as an invariant's ledger-entry
* snapshot, that keep the field value but not the owning SLE.
*
* @param leVersion The value of sfLEVersion, or nullopt if absent.
*
* @return The decoded LEVersion, or VaultVersion::Legacy if absent.
*/
[[nodiscard]] VaultVersion
decodeVaultVersion(std::optional<std::uint8_t> leVersion);
/**
* Resolves which LEVersion a newly-created Vault should get under the
* currently active amendments. This is the only place that decides Vault
* version policy; VaultCreate calls it instead of checking amendments
* directly.
*
* @param rules The active ledger rules.
*
* @return VaultVersion::FixedPrecision once featureLendingProtocolV1_1,
* featureLendingProtocolV1_2 and fixCleanup3_4_0 are all enabled;
* VaultVersion::CashBasis
* once featureLendingProtocolV1_1 is enabled; VaultVersion::Legacy otherwise.
*/
[[nodiscard]] VaultVersion
getVaultDefaultVersion(Rules const& rules);
/**
* Resolves the VaultKind of a vault SLE. Returns VaultKind::ClosedEnded when
* sfVaultKind is present and equal to that value; anything else (including an
+3
View File
@@ -0,0 +1,3 @@
# AGENTS.md — nodestore
See [README.md](./README.md) for backend and benchmark design.
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+32 -10
View File
@@ -311,24 +311,46 @@ constexpr std::uint8_t kVaultStrategyFirstComeFirstServe = 1;
* Default IOU scale factor for a Vault
*/
constexpr std::uint8_t kVaultDefaultIouScale = 6;
/**
* Maximum scale factor for a Vault. The number is chosen to ensure that
* 1 IOU can be always converted to shares.
* 10^19 > maxMPTokenAmount (2^64-1) > 10^18
*/
constexpr std::uint8_t kVaultMaximumIouScale = 18;
/**
* Vault ledger-entry schema versions. Assigned to newly created
* Vaults once featureLendingProtocolV1_1 is enabled. Vaults created before
* activation are left without LEVersion (implicit legacy version 0,
* instant interest recognition).
* Maximum Scale for a Vault created before featureLendingProtocolV1_2.
* Chosen so 1 IOU can always convert to shares:
* 10^19 > maxMPTokenAmount (2^64-1) > 10^18.
*/
constexpr std::uint8_t kLegacyVaultMaximumIouScale = 18;
/**
* Maximum Scale for a Vault created under featureLendingProtocolV1_2.
*/
constexpr std::uint8_t kFixedPrecisionVaultMaximumIouScale = 10;
/**
* Vault ledger-entry schema versions, persisted as sfLEVersion.
*
* LEVersion records which protocol a Vault was created under so later
* amendments can change the rules for new Vaults without rewriting
* existing ones. VaultCreate writes it from the then-active lending
* amendments; later transactions do not update it. A Vault created
* under an older amendment keeps that amendment's behaviour for its
* lifetime, even after a newer lending amendment activates.
*
* Absent sfLEVersion is implicit Legacy (version 0): instant interest
* recognition and a dynamic AssetsTotal scale. CashBasis (V1.1) uses
* cash-basis recognition on the same dynamic scale. FixedPrecision
* (V1.2) keeps cash-basis recognition and adds the lifetime base grid.
*/
enum class VaultVersion : uint8_t {
Legacy = 0,
CashBasis,
FixedPrecision,
};
// Code compares VaultVersion values with < and >= (e.g. "CashBasis or later"),
// so each later version must stay numerically larger than the one before.
static_assert(
VaultVersion::Legacy < VaultVersion::CashBasis &&
VaultVersion::CashBasis < VaultVersion::FixedPrecision);
/**
* Vault kind. Distinguishes closed-ended vaults from the default open-ended
* kind. Persisted as sfVaultKind (UINT8); absent means OpenEnded.
+25
View File
@@ -696,6 +696,16 @@ 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
@@ -766,6 +776,21 @@ roundToAsset(
return roundToScale(ret, scale);
}
/**
* Check whether an arbitrary precision Number can be converted to an STAmount
* of a given Asset without exceeding the asset's range.
*
* Use this before `roundToAsset` or `STAmount{asset, value}` on unchecked
* input, since those throw for out-of-range values, e.g. XRP above
* `STAmount::kMaxNativeN`.
*
* @param asset The relevant asset
* @param value The value to be checked
* @return true if the value is in range for the asset; false otherwise.
*/
[[nodiscard]] bool
isRepresentable(Asset const& asset, Number const& value);
//------------------------------------------------------------------------------
inline bool
+2
View File
@@ -231,6 +231,8 @@ enum TERcodes : TERUnderlyingType {
terNO_DELEGATE_PERMISSION, // Delegate does not have permission
terLOCKED, // MPT is locked
terNO_PERMISSION, // No permission but retry
terTICKET_RESERVED, // Ticket is reserved and cannot be consumed
// until the reservation is released
};
//------------------------------------------------------------------------------
+13 -20
View File
@@ -1,5 +1,6 @@
#pragma once
#include <xrpl/basics/MathUtilities.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/Zero.h>
@@ -69,7 +70,13 @@ public:
constexpr XRPAmount
operator*(value_type const& rhs) const
{
return XRPAmount{drops_ * rhs};
if (auto const result = checkedMul(drops_, rhs))
return XRPAmount{*result};
// Not reached in constant evaluation: an overflowing constant
// expression fails to compile instead.
XRPAmount product{*this};
product *= rhs;
return product;
}
friend constexpr XRPAmount
@@ -82,37 +89,23 @@ public:
XRPAmount&
operator+=(XRPAmount const& other)
{
drops_ += other.drops();
return *this;
return *this += other.drops();
}
XRPAmount&
operator-=(XRPAmount const& other)
{
drops_ -= other.drops();
return *this;
return *this -= other.drops();
}
XRPAmount&
operator+=(value_type const& rhs)
{
drops_ += rhs;
return *this;
}
operator+=(value_type const& rhs);
XRPAmount&
operator-=(value_type const& rhs)
{
drops_ -= rhs;
return *this;
}
operator-=(value_type const& rhs);
XRPAmount&
operator*=(value_type const& rhs)
{
drops_ *= rhs;
return *this;
}
operator*=(value_type const& rhs);
XRPAmount
operator-() const
+2 -1
View File
@@ -17,6 +17,7 @@
XRPL_FEATURE(Cosign, Supported::No, VoteBehavior::DefaultNo)
XRPL_FEATURE(SmartEscrow, Supported::No, VoteBehavior::DefaultNo)
// Requires LendingProtocolV1_1. New vaults take FixedPrecision plus cash-basis.
XRPL_FEATURE(LendingProtocolV1_2, Supported::No, VoteBehavior::DefaultNo)
XRPL_FIX (Cleanup3_5_0, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(ConfidentialMPTKeyRotation, Supported::No, VoteBehavior::DefaultNo)
@@ -60,7 +61,6 @@ XRPL_FIX (InnerObjTemplate2, Supported::Yes, VoteBehavior::DefaultNo
XRPL_FIX (EnforceNFTokenTrustline, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FIX (ReducedOffersV2, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(NFTokenMintOffer, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FIX (AMMv1_1, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FIX (PreviousTxnID, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FIX (XChainRewardRounding, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FIX (EmptyDID, Supported::Yes, VoteBehavior::DefaultNo)
@@ -106,6 +106,7 @@ XRPL_RETIRE_FIX(1623)
XRPL_RETIRE_FIX(1781)
XRPL_RETIRE_FIX(AmendmentMajorityCalc)
XRPL_RETIRE_FIX(AMMOverflowOffer)
XRPL_RETIRE_FIX(AMMv1_1)
XRPL_RETIRE_FIX(CheckThreading)
XRPL_RETIRE_FIX(DisallowIncomingV1)
XRPL_RETIRE_FIX(InnerObjTemplate)
@@ -514,6 +514,8 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({
{sfAssetsAvailable, SoeDefault},
{sfAssetsMaximum, SoeDefault},
{sfLossUnrealized, SoeDefault},
{sfYieldUnrealized, SoeDefault},
{sfAssetsDeployed, SoeDefault},
{sfShareMPTID, SoeRequired},
{sfWithdrawalPolicy, SoeRequired},
{sfScale, SoeDefault},
@@ -240,6 +240,8 @@ TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14)
TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15, SField::kSmdNeedsAsset | SField::kSmdDefault)
TYPED_SFIELD(sfPeriodicPayment, NUMBER, 16)
TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17, SField::kSmdNeedsAsset | SField::kSmdDefault)
TYPED_SFIELD(sfYieldUnrealized, NUMBER, 18, SField::kSmdNeedsAsset | SField::kSmdDefault)
TYPED_SFIELD(sfAssetsDeployed, NUMBER, 19, SField::kSmdNeedsAsset | SField::kSmdDefault)
// 32-bit signed (common)
TYPED_SFIELD(sfLoanScale, INT32, 1)
@@ -1175,6 +1175,20 @@ TRANSACTION(ttTRANSACTION_PROPOSAL_CREATE, 95, TransactionProposalCreate,
{sfExpiration, SoeRequired},
}))
// ttTRANSACTION_PROPOSAL_SIGN arrives in a later PR and takes the next unassigned type.
/** This transaction deletes a TransactionProposal, releasing its owner's
reserve. While the proposal is live only its Owner or its target account
may cancel; once it is terminal anyone may (On-Chain Cosigner spec §7). */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/proposal/TransactionProposalCancel.h>
#endif
TRANSACTION(ttTRANSACTION_PROPOSAL_CANCEL, 94, TransactionProposalCancel,
({.amendment = featureCosign}),
({
{sfProposalID, SoeRequired},
}))
/** This system-generated transaction type is used to update the status of the various amendments.
For details, see: https://xrpl.org/amendments.html
@@ -242,6 +242,54 @@ public:
return this->sle_->isFieldPresent(sfLossUnrealized);
}
/**
* @brief Get sfYieldUnrealized (SoeDefault)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_NUMBER::type::value_type>
getYieldUnrealized() const
{
if (hasYieldUnrealized())
return this->sle_->at(sfYieldUnrealized);
return std::nullopt;
}
/**
* @brief Check if sfYieldUnrealized is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasYieldUnrealized() const
{
return this->sle_->isFieldPresent(sfYieldUnrealized);
}
/**
* @brief Get sfAssetsDeployed (SoeDefault)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_NUMBER::type::value_type>
getAssetsDeployed() const
{
if (hasAssetsDeployed())
return this->sle_->at(sfAssetsDeployed);
return std::nullopt;
}
/**
* @brief Check if sfAssetsDeployed is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasAssetsDeployed() const
{
return this->sle_->isFieldPresent(sfAssetsDeployed);
}
/**
* @brief Get sfShareMPTID (SoeRequired)
* @return The field value.
@@ -571,6 +619,28 @@ public:
return *this;
}
/**
* @brief Set sfYieldUnrealized (SoeDefault)
* @return Reference to this builder for method chaining.
*/
VaultBuilder&
setYieldUnrealized(std::decay_t<typename SF_NUMBER::type::value_type> const& value)
{
object_[sfYieldUnrealized] = value;
return *this;
}
/**
* @brief Set sfAssetsDeployed (SoeDefault)
* @return Reference to this builder for method chaining.
*/
VaultBuilder&
setAssetsDeployed(std::decay_t<typename SF_NUMBER::type::value_type> const& value)
{
object_[sfAssetsDeployed] = value;
return *this;
}
/**
* @brief Set sfShareMPTID (SoeRequired)
* @return Reference to this builder for method chaining.
@@ -0,0 +1,131 @@
// This file is auto-generated. Do not edit.
#pragma once
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol_autogen/TransactionBase.h>
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
#include <xrpl/json/json_value.h>
#include <stdexcept>
#include <optional>
namespace xrpl::transactions {
class TransactionProposalCancelBuilder;
/**
* @brief Transaction: TransactionProposalCancel
*
* Type: ttTRANSACTION_PROPOSAL_CANCEL (94)
* Delegable: Delegation::NotDelegable
* Amendment: featureCosign
* Privileges: Privilege::NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.
* Use TransactionProposalCancelBuilder to construct new transactions.
*/
class TransactionProposalCancel : public TransactionBase
{
public:
static constexpr xrpl::TxType txType = ttTRANSACTION_PROPOSAL_CANCEL;
/**
* @brief Construct a TransactionProposalCancel transaction wrapper from an existing STTx object.
* @throws std::runtime_error if the transaction type doesn't match.
*/
explicit TransactionProposalCancel(std::shared_ptr<STTx const> tx)
: TransactionBase(std::move(tx))
{
// Verify transaction type
if (tx_->getTxnType() != txType)
{
throw std::runtime_error("Invalid transaction type for TransactionProposalCancel");
}
}
// Transaction-specific field getters
/**
* @brief Get sfProposalID (SoeRequired)
* @return The field value.
*/
[[nodiscard]]
SF_UINT256::type::value_type
getProposalID() const
{
return this->tx_->at(sfProposalID);
}
};
/**
* @brief Builder for TransactionProposalCancel transactions.
*
* Provides a fluent interface for constructing transactions with method chaining.
* Uses STObject internally for flexible transaction construction.
* Inherits common field setters from TransactionBuilderBase.
*/
class TransactionProposalCancelBuilder : public TransactionBuilderBase<TransactionProposalCancelBuilder>
{
public:
/**
* @brief Construct a new TransactionProposalCancelBuilder with required fields.
* @param account The account initiating the transaction.
* @param proposalID The sfProposalID field value.
* @param sequence Optional sequence number for the transaction.
* @param fee Optional fee for the transaction.
*/
TransactionProposalCancelBuilder(SF_ACCOUNT::type::value_type account,
std::decay_t<typename SF_UINT256::type::value_type> const& proposalID, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
)
: TransactionBuilderBase<TransactionProposalCancelBuilder>(ttTRANSACTION_PROPOSAL_CANCEL, account, sequence, fee)
{
setProposalID(proposalID);
}
/**
* @brief Construct a TransactionProposalCancelBuilder from an existing STTx object.
* @param tx The existing transaction to copy from.
* @throws std::runtime_error if the transaction type doesn't match.
*/
TransactionProposalCancelBuilder(std::shared_ptr<STTx const> tx)
{
if (tx->getTxnType() != ttTRANSACTION_PROPOSAL_CANCEL)
{
throw std::runtime_error("Invalid transaction type for TransactionProposalCancelBuilder");
}
object_ = *tx;
}
/**
* @brief Transaction-specific field setters
*/
/**
* @brief Set sfProposalID (SoeRequired)
* @return Reference to this builder for method chaining.
*/
TransactionProposalCancelBuilder&
setProposalID(std::decay_t<typename SF_UINT256::type::value_type> const& value)
{
object_[sfProposalID] = value;
return *this;
}
/**
* @brief Build and return the TransactionProposalCancel wrapper.
* @param publicKey The public key for signing.
* @param secretKey The secret key for signing.
* @return The constructed transaction wrapper.
*/
TransactionProposalCancel
build(PublicKey const& publicKey, SecretKey const& secretKey)
{
sign(publicKey, secretKey);
return TransactionProposalCancel{std::make_shared<STTx>(std::move(object_))};
}
};
} // namespace xrpl::transactions
+3
View File
@@ -0,0 +1,3 @@
# AGENTS.md — shamap
See [README.md](./README.md) for the SHAMap design.
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+16
View File
@@ -375,6 +375,22 @@ public:
/////////////////////////////////////////////////////
// Interface used by AccountDelete
/**
* This method removes one Ticket ledger-object from the ledger. AccountDelete uses it to
* clean up Ticket objects en masse.
*
* This method reduces the TicketCount and the OwnerCount of the Owner AccountRoot object. If
* a TransactionProposal reserved this Ticket, that proposal is deleted as well: once the
* Ticket is gone, its proposed transaction can never be applied (tefNO_TICKET).
*
* @param view the apply view for making changes.
* @param account the account which owns the Ticket object.
* @param ticketIndex ticketIndex is used to construct the ledger-entry ID of Ticket object.
* @param j the journal reference used to log messages.
*
* @return the final TER after all invariant checks. `tefBAD_LEDGER` is returned if Ticket,
* Owner AccountRoot is not found in the apply-view ledger.
*/
static TER
ticketDelete(
ApplyView& view,
@@ -1,7 +1,9 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
@@ -21,6 +23,7 @@
#include <boost/multiprecision/cpp_int.hpp>
#include <cstdint>
#include <map>
#include <set>
#include <string>
#include <tuple>
@@ -363,11 +366,27 @@ public:
* unique to pseudo-accounts. Check that all pseudo-accounts are following the
* rules, and that only pseudo-accounts look like pseudo-accounts.
*
* Post-fixCleanup3_5_0, a pseudo-account also owns only entries that belong
* to its own AMM, Vault or LoanBroker:
* - AMM: its AMM entry, its holdings of the two pool assets, and the trust
* lines of its LP token,
* - Vault: its share MPTokenIssuance, its holding of the vault asset, and the
* LoanBrokers operating on this vault,
* - LoanBroker: its Loans and its holding of the vault asset (the cover).
*
* A pseudo-account cannot sign transactions, so it could never accept or
* remove any other object linked into its owner directory. Such an object
* would pin the directory and block deletion of the owning object.
*/
class ValidPseudoAccounts
{
std::vector<std::string> errors_;
// Entries newly linked into owner directories, keyed by the directory
// owner. Whether an owner is a pseudo-account is decided in finalize,
// where the view is available.
std::map<AccountID, std::vector<uint256>> ownerDirAdditions_;
public:
void
visitEntry(bool, SLE::ConstRef, SLE::ConstRef);
+46 -1
View File
@@ -8,6 +8,7 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
@@ -29,7 +30,7 @@ namespace xrpl {
* - vault must have MPTokenIssuance for shares
* - vault without shares outstanding must have no shares
* - loss unrealized does not exceed the difference between assets total and
* assets available
* assets available (Legacy/CashBasis), or AssetsDeployed (FixedPrecision)
* - assets available do not exceed assets total
* - vault deposit increases assets and share issuance, and adds to:
* total assets, assets available, shares outstanding
@@ -46,6 +47,14 @@ namespace xrpl {
* - vault withdrawal may not succeed when the vault phase is Investment
* - closed-ended loan origination (ttLOAN_SET) may only succeed when the
* vault phase is Investment
* - FixedPrecision only: AssetsDeployed is non-negative and exactly
* representable at the asset's precision; AssetsDeployed changes only for
* ttLOAN_SET, ttLOAN_PAY and ttLOAN_MANAGE; AssetsDeployed is zero right
* after ttVAULT_CREATE and whenever shares outstanding return to zero; the
* stored AssetsTotal cache equals AssetsAvailable + AssetsDeployed, rounded
* Downward to the asset's precision
* - FixedPrecision only: YieldUnrealized is non-negative; YieldUnrealized
* changes only for ttLOAN_SET, ttLOAN_PAY and ttLOAN_MANAGE
*
* Immutability of VaultKind, SubscriptionDate and RedemptionDate is enforced
* by NoModifiedUnmodifiableFields (see InvariantCheck.cpp). From
@@ -68,6 +77,9 @@ class ValidVault
Number assetsAvailable = 0;
Number assetsMaximum = 0;
Number lossUnrealized = 0;
Number assetsDeployed = 0;
Number yieldUnrealized = 0;
VaultVersion version = VaultVersion::Legacy;
std::optional<std::uint8_t> vaultKind;
std::optional<std::uint32_t> subscriptionDate;
std::optional<std::uint32_t> redemptionDate;
@@ -217,6 +229,39 @@ private:
[[nodiscard]] bool
finalizeLoanSet(ReadView const& view, beast::Journal const& j) const;
/**
* @brief Check that a vault's AssetsTotal and AssetsAvailable deltas add
* up against the real transfer, for deposit/withdraw/clawback.
*
* Shared body of the ttVAULT_DEPOSIT, ttVAULT_WITHDRAW and
* ttVAULT_CLAWBACK "assets outstanding"/"assets available" add-up checks,
* which differ only in the rounded/exact deltas plugged in and the verb
* used in the log message.
*
* @param afterVault Snapshot of the vault after the transaction.
* @param beforeVault Snapshot of the vault before the transaction.
* @param vaultAsset The vault's underlying asset.
* @param fix340Enabled Whether fixCleanup3_4_0 is enabled.
* @param minScale The posterior scale computed by computeVaultMinScale.
* @param roundedVaultDelta The real transfer's delta, rounded to minScale.
* @param exactVaultDelta The real transfer's exact (unrounded) delta.
* @param verb The transaction name used in the log message ("deposit",
* "withdrawal" or "clawback").
* @param j Journal for logging.
* @return false if either add-up check fails.
*/
[[nodiscard]] static bool
checkTotalsAddUp(
Vault const& afterVault,
Vault const& beforeVault,
Asset const& vaultAsset,
bool fix340Enabled,
std::int32_t minScale,
Number const& roundedVaultDelta,
Number const& exactVaultDelta,
char const* verb,
beast::Journal const& j);
public:
// Compute the coarsest scale required to represent all numbers
[[nodiscard]] static std::int32_t
@@ -0,0 +1,44 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>
namespace xrpl {
class TransactionProposalCancel : public Transactor
{
public:
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;
explicit TransactionProposalCancel(ApplyContext& ctx) : Transactor(ctx)
{
}
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
void
visitInvariantEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after) override;
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
TER result,
XRPAmount fee,
ReadView const& view,
beast::Journal const& j) override;
};
} // namespace xrpl
-2
View File
@@ -435,8 +435,6 @@ PaymentSandbox::issuerSelfDebitHookMPT(
std::uint64_t amount,
std::int64_t origBalance)
{
XRPL_ASSERT(amount > 0, "PaymentSandbox::issuerSelfDebitHookMPT: amount must be > 0");
tab_.issuerSelfDebitMPT(issue, amount, origBalance);
}
+29 -7
View File
@@ -511,9 +511,14 @@ doWithdraw(
AccountID const& dstAcct,
AccountID const& sourceAcct,
XRPAmount priorBalance,
STAmount const& amount,
STAmount const& destinationAmount,
STAmount const& sourceAmount,
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
@@ -523,9 +528,10 @@ doWithdraw(
// create+delete MPTokens in the same transaction.
if (dstAcct == senderAcct)
{
if (amount > beast::kZero || !ctx.view.rules().enabled(fixCleanup3_4_0))
if (destinationAmount > beast::kZero || !ctx.view.rules().enabled(fixCleanup3_4_0))
{
if (auto const ter = addEmptyHolding(ctx, senderAcct, priorBalance, amount.asset(), j);
if (auto const ter =
addEmptyHolding(ctx, senderAcct, priorBalance, destinationAmount.asset(), j);
!isTesSuccess(ter) && ter != tecDUPLICATE)
return ter;
}
@@ -540,17 +546,33 @@ doWithdraw(
if (accountHolds(
ctx.view,
sourceAcct,
amount.asset(),
destinationAmount.asset(),
FreezeHandling::IgnoreFreeze,
AuthHandling::IgnoreAuth,
j) < amount)
j) < sourceAmount)
{
// LCOV_EXCL_START
JLOG(j.error()) << "doWithdraw: negative balance of broker cover assets.";
JLOG(j.error()) << "doWithdraw: source account holds fewer assets than the withdrawal.";
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
@@ -562,7 +584,7 @@ doWithdraw(
// Move the funds directly from the broker's pseudo-account to the
// dstAcct
return accountSend(
ctx.view, sourceAcct, dstAcct, amount, j, *sponsorSle, WaiveTransferFee::Yes);
ctx.view, sourceAcct, dstAcct, destinationAmount, j, *sponsorSle, WaiveTransferFee::Yes);
}
TER
+7
View File
@@ -0,0 +1,7 @@
# 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`.
+4 -34
View File
@@ -213,49 +213,19 @@ adjustAmountsByLPTokens(
if (lpTokensActual < lpTokens)
{
bool const ammRoundingEnabled = [&] {
if (auto const& rules = getCurrentTransactionRules();
rules && rules->enabled(fixAMMv1_1))
return true;
return false;
}();
// Equal trade
if (amount2)
{
Number const fr = lpTokensActual / lpTokens;
auto const amountActual = toSTAmount(amount.asset(), fr * amount);
auto const amount2Actual = toSTAmount(amount2->asset(), fr * *amount2);
if (!ammRoundingEnabled)
{
return std::make_tuple(
amountActual < amount ? amountActual : amount,
amount2Actual < amount2 ? amount2Actual : amount2,
lpTokensActual);
}
return std::make_tuple(amountActual, amount2Actual, lpTokensActual);
}
// Single trade
auto const amountActual = [&] {
if (isDeposit == IsDeposit::Yes)
{
return ammAssetIn(amountBalance, lptAMMBalance, lpTokensActual, tfee);
}
if (!ammRoundingEnabled)
{
return ammAssetOut(amountBalance, lptAMMBalance, lpTokens, tfee);
}
return ammAssetOut(amountBalance, lptAMMBalance, lpTokensActual, tfee);
}();
if (!ammRoundingEnabled)
{
return amountActual < amount
? std::make_tuple(amountActual, std::nullopt, lpTokensActual)
: std::make_tuple(amount, std::nullopt, lpTokensActual);
}
auto const amountActual = isDeposit == IsDeposit::Yes
? ammAssetIn(amountBalance, lptAMMBalance, lpTokensActual, tfee)
: ammAssetOut(amountBalance, lptAMMBalance, lpTokensActual, tfee);
return std::make_tuple(amountActual, std::nullopt, lpTokensActual);
}
@@ -980,7 +950,7 @@ verifyAndAdjustLPTokenBalance(
}
else
{
return std::unexpected<TER>(tecAMM_INVALID_TOKENS);
return std::unexpected<TER>(tecAMM_INVALID_TOKENS); // LCOV_EXCL_LINE
}
}
return true;
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+159 -5
View File
@@ -66,6 +66,161 @@ canApplyToBrokerCover(
return tesSUCCESS;
}
namespace detail {
[[nodiscard]] int
getPosteriorBrokerCoverScale(SLE::ConstRef vault, SLE::ConstRef broker, Number const& delta)
{
XRPL_ASSERT(
vault && vault->getType() == ltVAULT,
"xrpl::detail::getPosteriorBrokerCoverScale : valid Vault sle");
XRPL_ASSERT(
broker && broker->getType() == ltLOAN_BROKER,
"xrpl::detail::getPosteriorBrokerCoverScale : valid LoanBroker sle");
return posteriorAssetScale(
getVaultVersion(vault),
vault->at(sfAsset),
getVaultBaseScale(vault),
broker->at(sfCoverAvailable),
delta);
}
[[nodiscard]] STAmount
roundToPosteriorBrokerCoverScale(
SLE::ConstRef vault,
SLE::ConstRef broker,
STAmount const& delta,
Number::RoundingMode roundingMode)
{
XRPL_ASSERT(
vault && vault->getType() == ltVAULT,
"xrpl::detail::roundToPosteriorBrokerCoverScale : valid Vault sle");
XRPL_ASSERT(
broker && broker->getType() == ltLOAN_BROKER,
"xrpl::detail::roundToPosteriorBrokerCoverScale : valid LoanBroker sle");
XRPL_ASSERT(
delta.asset() == vault->at(sfAsset),
"xrpl::detail::roundToPosteriorBrokerCoverScale : delta and Vault asset match");
if (delta.integral())
return delta;
return roundToScale(delta, getPosteriorBrokerCoverScale(vault, broker, delta), roundingMode);
}
} // namespace detail
[[nodiscard]] STAmount
creditToPosteriorBrokerCoverScale(
SLE::ConstRef vault,
SLE::ConstRef broker,
Number const& raw,
Number::RoundingMode roundingMode)
{
XRPL_ASSERT(
vault && vault->getType() == ltVAULT,
"xrpl::creditToPosteriorBrokerCoverScale : valid Vault sle");
XRPL_ASSERT(
broker && broker->getType() == ltLOAN_BROKER,
"xrpl::creditToPosteriorBrokerCoverScale : valid LoanBroker sle");
Asset const asset = vault->at(sfAsset);
if (asset.integral())
{
NumberRoundModeGuard const rg(roundingMode);
return STAmount{asset, raw};
}
Number const reference = broker->at(sfCoverAvailable);
int const scale = detail::getPosteriorBrokerCoverScale(vault, broker, raw);
return detail::creditToPosteriorScale(asset, reference, scale, raw, roundingMode);
}
[[nodiscard]] STAmount
debitToPosteriorBrokerCoverScale(
SLE::ConstRef vault,
SLE::ConstRef broker,
STAmount const& amount,
Number::RoundingMode roundingMode)
{
XRPL_ASSERT(
vault && vault->getType() == ltVAULT,
"xrpl::debitToPosteriorBrokerCoverScale : valid Vault sle");
XRPL_ASSERT(
broker && broker->getType() == ltLOAN_BROKER,
"xrpl::debitToPosteriorBrokerCoverScale : valid LoanBroker sle");
XRPL_ASSERT(
amount.asset() == vault->at(sfAsset),
"xrpl::debitToPosteriorBrokerCoverScale : amount and Vault asset match");
XRPL_ASSERT(!amount.negative(), "xrpl::debitToPosteriorBrokerCoverScale : non-negative amount");
return -detail::roundToPosteriorBrokerCoverScale(vault, broker, -amount, roundingMode);
}
[[nodiscard]] TER
checkOptionalBrokerCoverInflow(SLE::ConstRef vault, SLE::ConstRef broker, STAmount const& amount)
{
XRPL_ASSERT(
vault && vault->getType() == ltVAULT,
"xrpl::checkOptionalBrokerCoverInflow : valid Vault sle");
XRPL_ASSERT(
broker && broker->getType() == ltLOAN_BROKER,
"xrpl::checkOptionalBrokerCoverInflow : valid LoanBroker sle");
XRPL_ASSERT(
amount.asset() == vault->at(sfAsset),
"xrpl::checkOptionalBrokerCoverInflow : amount and Vault asset match");
XRPL_ASSERT(!amount.negative(), "xrpl::checkOptionalBrokerCoverInflow : non-negative amount");
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
return tesSUCCESS;
// amount is the effective credit (already floored on the posterior
// CoverAvailable grid by creditToPosteriorBrokerCoverScale); rounding it
// again as a standalone delta could hide a crossing.
STAmount const& rounded = amount;
int const baseScale = getVaultBaseScale(vault);
// Keep this explicit even though the Open-limit capacity check below rejects
// every coarsening transition too. The protocol defines both conditions
// independently.
if (detail::getPosteriorBrokerCoverScale(vault, broker, rounded) != baseScale)
return tecLIMIT_EXCEEDED;
Number const posterior = [&] {
NumberRoundModeGuard const rg(Number::RoundingMode::TowardsZero);
return broker->at(sfCoverAvailable) + rounded;
}();
if (posterior > getVaultOpenLimit(vault))
return tecLIMIT_EXCEEDED;
return tesSUCCESS;
}
void
adjustBrokerDebtTotal(
SLE::Ref brokerSle,
SLE::ConstRef vaultSle,
Number const& delta,
int vaultScale)
{
// On FixedPrecision Vaults, LoanSet adds principal to DebtTotal exactly, LoanPay and default
// subtract exact amounts. Thus DebtTotal must not be rounded coarser than the base scale.
if (getVaultVersion(vaultSle) == VaultVersion::FixedPrecision)
{
brokerSle->at(sfDebtTotal) += delta;
}
else
{
adjustImpreciseNumber(brokerSle->at(sfDebtTotal), delta, vaultSle->at(sfAsset), vaultScale);
}
}
Number
minimumBrokerCover(Number const& debtTotal, TenthBips32 coverRateMinimum, SLE::ConstRef vaultSle)
{
XRPL_ASSERT(
vaultSle && vaultSle->getType() == ltVAULT, "xrpl::minimumBrokerCover : valid Vault sle");
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
return roundToAsset(
vaultSle->at(sfAsset),
tenthBipsOfValue(debtTotal, coverRateMinimum),
getVaultBaseScale(vaultSle));
}
bool
checkLendingProtocolDependencies(Rules const& rules, STTx const& tx)
{
@@ -250,14 +405,13 @@ loanPaymentDeltas(LoanPaymentParts const& parts)
namespace {
// Cash-basis accounting applies only when featureLendingProtocolV1_1 is
// enabled AND the specific Vault was created under it (LEVersion ==
// VaultVersion::CashBasis). Vaults created before activation keep instant
// interest recognition forever, even after the amendment later turns on.
// Cash-basis accounting applies to Vaults created under
// featureLendingProtocolV1_1 or a later version. Vaults created before
// activation keep instant interest recognition forever.
bool
cashBasisEnabled(SLE::ConstRef vaultSle)
{
return getVaultVersion(vaultSle) == VaultVersion::CashBasis;
return getVaultVersion(vaultSle) >= VaultVersion::CashBasis;
}
} // namespace
@@ -1047,6 +1047,14 @@ maxMPTAmount(SLE const& sleIssuance)
return sleIssuance[~sfMaximumAmount].value_or(kMaxMpTokenAmount);
}
std::optional<std::int64_t>
maxMPTAmount(ReadView const& view, MPTID const& mptID)
{
if (auto const sle = view.read(keylet::mptokenIssuance(mptID)))
return maxMPTAmount(*sle);
return std::nullopt;
}
std::int64_t
availableMPTAmount(SLE const& sleIssuance)
{
+157 -5
View File
@@ -1,18 +1,51 @@
#include <xrpl/ledger/helpers/ProposalHelpers.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <algorithm>
#include <array>
#include <cstdint>
#include <optional>
namespace xrpl::proposal {
namespace {
// Proposals do not nest, so a proposed transaction may carry none of these
// types, neither as its own payload nor as a Batch inner transaction.
constexpr auto kProposalTxTypes = std::to_array<TxType>({
ttTRANSACTION_PROPOSAL_CREATE,
ttTRANSACTION_PROPOSAL_CANCEL,
});
} // namespace
bool
isValidProposal(STObject const& proposedTx)
isValidProposalTxnType(STObject const& proposedTx)
{
if (isProposalTx(proposedTx))
auto const type = proposedTx.getFieldU16(sfTransactionType);
if (std::ranges::find(kProposalTxTypes, type) != kProposalTxTypes.end())
return false;
if (isPseudoTx(proposedTx))
@@ -22,13 +55,24 @@ isValidProposal(STObject const& proposedTx)
(proposedTx.getFieldU32(sfFlags) & tfInnerBatchTxn) != 0u)
return false;
if (proposedTx.getFieldU16(sfTransactionType) == ttBATCH &&
proposedTx.isFieldPresent(sfRawTransactions))
if (type == ttBATCH && proposedTx.isFieldPresent(sfRawTransactions))
{
STArray const& innerTxns = proposedTx.getFieldArray(sfRawTransactions);
for (STObject const& inner : innerTxns)
{
if (isProposalTx(inner) || isPseudoTx(inner))
// The only production caller reaches this walk through STTx
// construction, which rejects a typeless inner first, so this is
// re-checked rather than relied upon.
if (!inner.isFieldPresent(sfTransactionType))
return false;
auto const innerType = inner.getFieldU16(sfTransactionType);
// A nested Batch is rejected alongside nested proposals and
// pseudo-transactions: STTx construction forbids inner Batches, so
// a proposal must not be able to store one either.
if (std::ranges::find(kProposalTxTypes, innerType) != kProposalTxTypes.end() ||
innerType == ttBATCH || isPseudoTx(inner))
return false;
}
}
@@ -36,4 +80,112 @@ isValidProposal(STObject const& proposedTx)
return true;
}
bool
isTerminal(
ReadView const& view,
std::optional<std::uint32_t> expiration,
STObject const& proposedTx)
{
if (hasExpired(view, expiration))
return true;
// Mirror the ordinary transactor's tefMAX_LEDGER rule: view.seq() > bound.
// At view.seq() == bound the payload can still be submitted, so the
// proposal is still live (On-Chain Cosigner spec §4.5).
return proposedTx.isFieldPresent(sfLastLedgerSequence) &&
view.seq() > proposedTx.getFieldU32(sfLastLedgerSequence);
}
TER
deleteProposal(ApplyView& view, SLE::pointer const& sleProposal, beast::Journal j)
{
if (!sleProposal || sleProposal->getType() != ltTRANSACTION_PROPOSAL ||
!view.exists(Keylet{ltTRANSACTION_PROPOSAL, sleProposal->key()}))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Invalid TransactionProposal deletion.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
AccountID const owner = sleProposal->getAccountID(sfOwner);
std::uint64_t const page{(*sleProposal)[sfOwnerNode]};
if (!view.dirRemove(keylet::ownerDir(owner), page, sleProposal->key(), true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Unable to delete TransactionProposal from owner.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
auto const sleOwner = view.peek(keylet::account(owner));
if (!sleOwner)
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Could not find TransactionProposal owner account root.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
// Release the reserve against the Owner or, if the entry carries a
// reserve sponsor, against that sponsor.
decreaseOwnerCountForObject(
view,
sleOwner,
sleProposal,
proposalOwnerCount(sleProposal->getFieldObject(sfProposedTransaction)),
j);
view.erase(sleProposal);
return tesSUCCESS;
}
bool
payloadMatches(STObject const& proposedTx, STObject const& tx)
{
XRPL_ASSERT(
proposedTx.isFieldPresent(sfTransactionType) && tx.isFieldPresent(sfTransactionType),
"xrpl::proposal::payloadMatches : transaction-shaped inputs");
// The signing-payload serialization omits every kNotSigning field: the
// signature containers a proposal lets evolve (TxnSignature, Signers,
// BatchSigners, CounterpartySignature, SponsorSignature) plus Signature
// and MasterSignature, which the account's own signature does not cover
// either (see the header). What remains is the field set fixed at
// creation — except SigningPubKey, which signing payloads include but
// which is also mutable here (stored empty, filled if the target signs
// with its own key). Neutralize it on both sides before comparing.
auto const payloadBytes = [](STObject const& obj) {
STObject copy{obj};
copy.setFieldVL(sfSigningPubKey, Slice{});
Serializer s;
copy.addWithoutSigningFields(s);
return s.getData();
};
return payloadBytes(proposedTx) == payloadBytes(tx);
}
bool
canConsumeTicket(ReadView const& view, STTx const& tx)
{
auto const seqProx = tx.getSeqProxy();
if (!seqProx.isTicket())
{
// The caller only reaches here for a Ticket-based transaction.
// LCOV_EXCL_START
UNREACHABLE("xrpl::proposal::canConsumeTicket : tx spends a Sequence");
return false;
// LCOV_EXCL_STOP
}
auto const sleProposal =
view.read(keylet::txProposal(tx.getAccountID(sfAccount), seqProx.value()));
if (!sleProposal)
return true;
return payloadMatches(sleProposal->getFieldObject(sfProposedTransaction), tx);
}
} // namespace xrpl::proposal
+24 -17
View File
@@ -2,6 +2,7 @@
#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>
@@ -551,6 +552,22 @@ 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
@@ -973,19 +990,11 @@ accountSendIOU(
SLE::Ref sponsorSle,
WaiveTransferFee waiveFee)
{
if (view.rules().enabled(fixAMMv1_1))
{
if (saAmount < beast::kZero || saAmount.holds<MPTIssue>())
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
}
else
if (saAmount < beast::kZero || saAmount.holds<MPTIssue>())
{
// LCOV_EXCL_START
XRPL_ASSERT(
saAmount >= beast::kZero && !saAmount.holds<MPTIssue>(),
"xrpl::accountSendIOU : minimum amount and not MPT");
JLOG(j.fatal()) << "accountSendIOU: negative amount or invalid asset";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
@@ -1355,9 +1364,8 @@ directSendNoLimitMPT(
{
// Number math loses precision on large MPT amounts, which can
// overcharge the sender. MPTs are integral, so compute the cost
// 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());
// exactly, matching the payment engine.
saActual = addTransferFee(saAmount, rate);
}
else
{
@@ -1482,9 +1490,8 @@ directSendNoLimitMultiMPT(
{
// Number math loses precision on large MPT amounts, which can
// overcharge the sender. MPTs are integral, so compute the
// 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());
// cost exactly, matching the payment engine.
actualSend = addTransferFee(amount, rate);
}
else
{
+557 -51
View File
@@ -1,12 +1,16 @@
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/CredentialHelpers.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/Protocol.h>
@@ -17,13 +21,468 @@
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <algorithm>
#include <cstdint>
#include <expected>
#include <optional>
#include <stdexcept>
#include <string_view>
#include <utility>
namespace xrpl {
namespace {
[[nodiscard]] int
fixedBaseScale(SLE::ConstRef vault)
{
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::fixedBaseScale : valid Vault sle");
Asset const asset = vault->at(sfAsset);
// sfScale is never written for an integral asset (VaultCreate only sets
// it when scale != 0, and integral assets always get scale 0), so it
// must not be read here; vaultBaseScale ignores the scale argument for
// an integral asset anyway.
if (asset.integral())
return 0;
return vaultBaseScale(asset, vault->at(sfScale));
}
// Thin, vault-specific wrapper over posteriorAssetScale: used by
// getPosteriorVaultScale and creditToPosteriorAvailableScale.
[[nodiscard]] int
posteriorScale(SLE::ConstRef vault, Number const& reference, Number const& delta)
{
return detail::posteriorAssetScale(
getVaultVersion(vault), vault->at(sfAsset), fixedBaseScale(vault), reference, delta);
}
// FixedPrecision-only counterpart to clampToAssetsTotalScale, for cash
// outflows (VaultWithdraw, VaultClawback): rounds the magnitude of delta
// toward zero at AssetsAvailable's own posterior scale, the balance the cash
// actually leaves, instead of the derived AssetsTotal cache. Same
// tecPRECISION_LOSS and integral-asset and magnitude-never-exceeds-delta
// rules as clampToAssetsTotalScale. Not part of the public interface; reached
// only through clampVaultOutflow.
//
// For an outflow it is safe to round the delta directly, unlike the credit
// case: amount on the posterior grid of AssetsAvailable always leaves
// AssetsAvailable minus amount representable, since the posterior grid is
// exactly the grid AssetsAvailable itself will canonicalize to after the
// subtraction.
[[nodiscard]] std::expected<STAmount, TER>
clampToAvailableScale(SLE::ConstRef vault, STAmount const& delta)
{
XRPL_ASSERT(
delta.asset() == vault->at(sfAsset),
"xrpl::clampToAvailableScale : delta and vault asset match");
XRPL_ASSERT(
getVaultVersion(vault) == VaultVersion::FixedPrecision,
"xrpl::clampToAvailableScale : FixedPrecision Vault");
XRPL_ASSERT(delta.negative(), "xrpl::clampToAvailableScale : outflow delta is negative");
Asset const asset = vault->at(sfAsset);
STAmount magnitude = delta.negative() ? -delta : delta;
if (asset.integral())
return magnitude;
STAmount const rounded = roundToScale(
delta,
posteriorScale(vault, vault->at(sfAssetsAvailable), delta),
Number::RoundingMode::TowardsZero);
STAmount actualDelta = rounded.negative() ? -rounded : rounded;
XRPL_ASSERT(
abs(actualDelta) <= abs(delta),
"xrpl::clampToAvailableScale : actual delta smaller or equal to calculated delta");
// Reject changes below scale precision (1 ULP) to prevent share balance changes
// without corresponding asset movements.
if (actualDelta <= beast::kZero)
return std::unexpected(tecPRECISION_LOSS);
return actualDelta;
}
[[nodiscard]] VaultKind
decodeVaultKind(std::optional<std::uint8_t> vaultKind)
{
if (vaultKind && *vaultKind == std::to_underlying(VaultKind::ClosedEnded))
return VaultKind::ClosedEnded;
return VaultKind::OpenEnded;
}
// Write cached AssetsTotal from getAssetsTotal, as the exact total rounded
// Downward to the Vault asset's 16-digit STAmount representation. Below
// coarsening this equals the exact total; above it, the cache is a
// deliberate floor of the exact value. Internal to adjustVaultBalances,
// the only writer of AssetsAvailable, AssetsDeployed, AssetsTotal,
// YieldUnrealized and LossUnrealized on a FixedPrecision Vault after
// creation; not part of the public interface.
void
syncAssetsTotal(SLE::Ref vault)
{
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::syncAssetsTotal : valid Vault sle");
Asset const asset = vault->at(sfAsset);
// adjustVaultBalances writes AssetsAvailable as an already-canonical
// STAmount before calling this, so the associateAsset pass a transactor
// runs afterwards is always a no-op. If this fires, AssetsAvailable was
// written some other way.
XRPL_ASSERT(
(STAmount{asset, vault->at(sfAssetsAvailable)} == vault->at(sfAssetsAvailable)),
"xrpl::syncAssetsTotal : AssetsAvailable is a 16-digit STAmount value");
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
vault->at(sfAssetsTotal) = STAmount{asset, getAssetsTotal(vault)};
}
[[nodiscard]] bool
debitIsNonZeroDust(SLE::ConstRef vault, Number const& amount)
{
if (amount == 0)
return false;
auto const total = getVaultVersion(vault) == VaultVersion::FixedPrecision
? vault->at(sfAssetsAvailable)
: getAssetsTotal(vault);
auto const asset = vault->at(sfAsset);
return STAmount{asset, total - amount} == STAmount{asset, total};
}
} // namespace
namespace detail {
[[nodiscard]] int
liveScale(Number const& reference, Asset const& asset, int baseScale)
{
if (reference == beast::kZero)
return baseScale;
// Round Downward, not the ambient mode: the live exponent must match the
// exponent syncAssetsTotal actually stores (also Downward), regardless of
// what rounding mode the caller's own arithmetic is using.
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
return std::max(baseScale, scale(reference, asset));
}
[[nodiscard]] int
posteriorAssetScale(
VaultVersion version,
Asset const& asset,
int baseScale,
Number const& reference,
Number const& delta)
{
// ToNearest for the sum and for scale()'s canonicalization of it, as the
// Legacy/CashBasis clamp always did; liveScale sets its own mode.
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
Number const posterior = reference + delta;
switch (version)
{
case VaultVersion::Legacy:
case VaultVersion::CashBasis:
return scale(posterior, asset);
case VaultVersion::FixedPrecision:
return liveScale(posterior, asset, baseScale);
}
// LCOV_EXCL_START
UNREACHABLE("xrpl::detail::posteriorAssetScale : valid VaultVersion");
return Number::kMinExponent - 1;
// LCOV_EXCL_STOP
}
[[nodiscard]] STAmount
creditToPosteriorScale(
Asset const& asset,
Number const& reference,
int atScale,
Number const& raw,
Number::RoundingMode roundingMode)
{
// Floor the SUM (reference + raw), not just raw, at atScale. See
// creditToPosteriorAvailableScale's doc: a delta floored on its own grid
// can still leave a 17-digit sum once the reference has crossed a power
// of ten.
//
// Both reference + raw and flooredSum - reference can carry more digits
// than Number holds (the sum is on the posterior grid, the reference on
// the finer one). Compute both under roundingMode, not the caller's
// ambient mode, so the credit this returns never exceeds raw.
NumberRoundModeGuard const rg(roundingMode);
Number const flooredSum = roundToAsset(asset, reference + raw, atScale, roundingMode);
return STAmount{asset, flooredSum - reference};
}
[[nodiscard]] int
getPosteriorVaultScale(SLE::ConstRef vault, STAmount const& delta)
{
XRPL_ASSERT(
vault && vault->getType() == ltVAULT,
"xrpl::detail::getPosteriorVaultScale : valid Vault sle");
XRPL_ASSERT(
delta.asset() == vault->at(sfAsset),
"xrpl::detail::getPosteriorVaultScale : delta and Vault asset match");
return posteriorScale(vault, getAssetsTotal(vault), delta);
}
} // namespace detail
[[nodiscard]] Number
getAssetsTotal(SLE::ConstRef vault)
{
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getAssetsTotal : valid Vault sle");
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
return vault->at(sfAssetsTotal);
// AssetsAvailable is at -Scale (or coarser) and AssetsDeployed is always at
// -Scale, so the sum is exact while it fits 19 digits. Fix the mode so a
// sum that does not fit rounds the same way for every caller.
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
return vault->at(sfAssetsAvailable) + vault->at(sfAssetsDeployed);
}
[[nodiscard]] TER
adjustVaultBalances(SLE::Ref vault, VaultBalanceChange const& change, beast::Journal j)
{
XRPL_ASSERT(
vault && vault->getType() == ltVAULT, "xrpl::adjustVaultBalances : valid Vault sle");
XRPL_ASSERT(
getVaultVersion(vault) == VaultVersion::FixedPrecision,
"xrpl::adjustVaultBalances : FixedPrecision Vault");
Asset const asset = vault->at(sfAsset);
STAmount const cash = change.cash.value_or(STAmount{asset, 0});
XRPL_ASSERT(cash.asset() == asset, "xrpl::adjustVaultBalances : cash and Vault asset match");
// This is the mode the ledger's own transfers (trust line / MPT balance
// updates) normalize in; computing AA' with any other ambient mode would
// not reproduce what the real transfer just did to the backing balance.
XRPL_ASSERT(
Number::getround() == Number::RoundingMode::ToNearest,
"xrpl::adjustVaultBalances : ambient rounding mode is ToNearest");
// AA' is the exact trust-line/MPT arithmetic the ledger uses for the
// transfer itself: a Number sum re-canonicalized to the asset's 16-digit
// STAmount precision. MPT and XRP sums are already integral, so this is
// a no-op rounding for them.
//
// AssetsDeployed, LossUnrealized and YieldUnrealized move no cash, so they stay
// exact Number sums; the asserts below check each fits 16 digits, rather
// than letting an STAmount conversion round it silently.
STAmount const availableAfter{asset, Number(vault->at(sfAssetsAvailable)) + Number(cash)};
Number const assetsDeployedAfter = Number(vault->at(sfAssetsDeployed)) + change.deployed;
Number const lossUnrealizedAfter = Number(vault->at(sfLossUnrealized)) + change.loss;
// LoanSet, LoanPay and default each move YieldUnrealized by the change in
// the Loan's own TotalValueOutstanding - PrincipalOutstanding -
// ManagementFeeOutstanding, so it always equals that sum over the
// Vault's Loans and cannot go negative.
Number const yieldUnrealizedAfter = Number(vault->at(sfYieldUnrealized)) + change.yield;
if (availableAfter < beast::kZero || assetsDeployedAfter < beast::kZero ||
lossUnrealizedAfter < beast::kZero || yieldUnrealizedAfter < beast::kZero)
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "adjustVaultBalances: a balance would become negative."
<< " AssetsAvailable: " << Number(availableAfter)
<< ", AssetsDeployed: " << assetsDeployedAfter
<< ", LossUnrealized: " << lossUnrealizedAfter
<< ", YieldUnrealized: " << yieldUnrealizedAfter;
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
// The impairment rule: a Loan can only be impaired for as much as it
// still owes. Reachable only through LoanManage's impair.
if (lossUnrealizedAfter > assetsDeployedAfter)
return tecLIMIT_EXCEEDED;
// Every caller derives principal from a Loan's own PrincipalOutstanding so the new total must
// stay representable at the asset's own 16-digit precision too.
XRPL_ASSERT(
(STAmount{asset, assetsDeployedAfter} == assetsDeployedAfter),
"xrpl::adjustVaultBalances : AssetsDeployed is a 16-digit STAmount value");
XRPL_ASSERT(
(STAmount{asset, lossUnrealizedAfter} == lossUnrealizedAfter),
"xrpl::adjustVaultBalances : LossUnrealized is a 16-digit STAmount value");
XRPL_ASSERT(
(STAmount{asset, yieldUnrealizedAfter} == yieldUnrealizedAfter),
"xrpl::adjustVaultBalances : YieldUnrealized is a 16-digit STAmount value");
vault->at(sfAssetsAvailable) = availableAfter;
vault->at(sfAssetsDeployed) = assetsDeployedAfter;
vault->at(sfLossUnrealized) = lossUnrealizedAfter;
vault->at(sfYieldUnrealized) = yieldUnrealizedAfter;
syncAssetsTotal(vault);
return tesSUCCESS;
}
[[nodiscard]] TER
adjustVaultCash(SLE::Ref vault, STAmount const& delta, beast::Journal j)
{
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
return adjustVaultBalances(vault, {.cash = delta}, j);
vault->at(sfAssetsTotal) += delta;
vault->at(sfAssetsAvailable) += delta;
return tesSUCCESS;
}
[[nodiscard]] int
getVaultScale(SLE::ConstRef vault)
{
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultScale : valid Vault sle");
Number const assetsTotal = getAssetsTotal(vault);
switch (getVaultVersion(vault))
{
case VaultVersion::Legacy:
case VaultVersion::CashBasis:
return scale(assetsTotal, vault->at(sfAsset));
case VaultVersion::FixedPrecision:
return detail::liveScale(assetsTotal, vault->at(sfAsset), fixedBaseScale(vault));
}
// LCOV_EXCL_START
UNREACHABLE("xrpl::getVaultScale : valid VaultVersion");
return Number::kMinExponent - 1;
// LCOV_EXCL_STOP
}
[[nodiscard]] int
getVaultBaseScale(SLE::ConstRef vault)
{
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultBaseScale : valid Vault sle");
switch (getVaultVersion(vault))
{
case VaultVersion::Legacy:
case VaultVersion::CashBasis:
return getVaultScale(vault);
case VaultVersion::FixedPrecision:
return fixedBaseScale(vault);
}
// LCOV_EXCL_START
UNREACHABLE("xrpl::getVaultBaseScale : valid VaultVersion");
return Number::kMinExponent - 1;
// LCOV_EXCL_STOP
}
[[nodiscard]] STAmount
roundToPosteriorVaultScale(
SLE::ConstRef vault,
STAmount const& amount,
Number::RoundingMode roundingMode)
{
XRPL_ASSERT(
vault && vault->getType() == ltVAULT, "xrpl::roundToPosteriorVaultScale : valid Vault sle");
XRPL_ASSERT(
amount.asset() == vault->at(sfAsset),
"xrpl::roundToPosteriorVaultScale : amount and Vault asset match");
if (amount.integral())
return amount;
return roundToScale(amount, detail::getPosteriorVaultScale(vault, amount), roundingMode);
}
[[nodiscard]] STAmount
creditToPosteriorAvailableScale(
SLE::ConstRef vault,
Number const& raw,
Number::RoundingMode roundingMode)
{
XRPL_ASSERT(
vault && vault->getType() == ltVAULT,
"xrpl::creditToPosteriorAvailableScale : valid Vault sle");
Asset const asset = vault->at(sfAsset);
if (asset.integral())
{
NumberRoundModeGuard const rg(roundingMode);
return STAmount{asset, raw};
}
Number const reference = vault->at(sfAssetsAvailable);
int const scale = posteriorScale(vault, reference, raw);
return detail::creditToPosteriorScale(asset, reference, scale, raw, roundingMode);
}
[[nodiscard]] Number
getVaultOpenLimit(SLE::ConstRef vault)
{
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultOpenLimit : valid Vault sle");
XRPL_ASSERT(
getVaultVersion(vault) == VaultVersion::FixedPrecision,
"xrpl::getVaultOpenLimit : FixedPrecision Vault");
return Number{9, 15 + getVaultBaseScale(vault)};
}
[[nodiscard]] TER
checkOptionalVaultInflow(SLE::ConstRef vault, STAmount const& amount)
{
XRPL_ASSERT(
vault && vault->getType() == ltVAULT, "xrpl::checkOptionalVaultInflow : valid Vault sle");
XRPL_ASSERT(
amount.asset() == vault->at(sfAsset),
"xrpl::checkOptionalVaultInflow : amount and Vault asset match");
XRPL_ASSERT(!amount.negative(), "xrpl::checkOptionalVaultInflow : non-negative amount");
if (amount.negative())
return tefINTERNAL; // LCOV_EXCL_LINE
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
return tesSUCCESS;
STAmount const rounded =
roundToPosteriorVaultScale(vault, amount, Number::RoundingMode::TowardsZero);
int const baseScale = getVaultBaseScale(vault);
// Keep this explicit even though a non-negative YieldUnrealized makes the
// Open-zone capacity ceiling reject every coarsening transition too. The
// protocol defines both conditions independently.
if (detail::getPosteriorVaultScale(vault, rounded) != baseScale)
return tecLIMIT_EXCEEDED;
if (vaultOpenZoneCapacity(vault, rounded) > getVaultOpenLimit(vault))
return tecLIMIT_EXCEEDED;
return tesSUCCESS;
}
[[nodiscard]] Number
vaultOpenZoneCapacity(SLE::ConstRef vault, Number const& roundedAmount)
{
XRPL_ASSERT(
vault && vault->getType() == ltVAULT, "xrpl::vaultOpenZoneCapacity : valid Vault sle");
XRPL_ASSERT(
getVaultVersion(vault) == VaultVersion::FixedPrecision,
"xrpl::vaultOpenZoneCapacity : FixedPrecision Vault");
NumberRoundModeGuard const rg(Number::RoundingMode::TowardsZero);
return getAssetsTotal(vault) + vault->at(sfYieldUnrealized) + roundedAmount;
}
[[nodiscard]] TER
checkDebitNotDust(
SLE::ConstRef vault,
Number const& amount,
beast::Journal j,
std::string_view name)
{
try
{
if (debitIsNonZeroDust(vault, amount))
{
JLOG(j.debug()) << name << ": amount too small to change stored vault balance";
return tecPRECISION_LOSS;
}
}
// LCOV_EXCL_START
catch (std::overflow_error const&)
{
// It's easy to hit this exception from Number with large enough Scale
// so we avoid spamming the log and only use debug here.
JLOG(j.debug()) //
<< name << ": overflow error with"
<< " scale=" << (int)vault->at(sfScale) //
<< ", assetsTotal=" << vault->at(sfAssetsTotal) << ", amount=" << amount;
return tecPATH_DRY;
}
// LCOV_EXCL_STOP
return tesSUCCESS;
}
[[nodiscard]] std::optional<STAmount>
assetsToSharesDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount const& assets)
{
@@ -34,7 +493,7 @@ assetsToSharesDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount cons
if (assets.negative() || assets.asset() != vault->at(sfAsset))
return std::nullopt; // LCOV_EXCL_LINE
Number const assetTotal = vault->at(sfAssetsTotal);
Number const assetTotal = getAssetsTotal(vault);
STAmount shares{vault->at(sfShareMPTID)};
if (assetTotal == 0)
{
@@ -58,7 +517,7 @@ sharesToAssetsDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount cons
if (shares.negative() || shares.asset() != vault->at(sfShareMPTID))
return std::nullopt; // LCOV_EXCL_LINE
Number const assetTotal = vault->at(sfAssetsTotal);
Number const assetTotal = getAssetsTotal(vault);
STAmount assets{vault->at(sfAsset)};
if (assetTotal == 0)
{
@@ -85,37 +544,52 @@ clampToAssetsTotalScale(SLE::ConstRef vault, STAmount const& delta)
{
return magnitude;
}
Number const assetsTotal = vault->at(sfAssetsTotal);
// Calculate the scale after applying the delta using ToNearest rounding.
// This aligns the delta with scale checks used by vault invariants.
int const postScale = [&] {
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
return scale(assetsTotal + delta, asset);
}();
STAmount actualDelta;
if (delta.negative())
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
{
// For withdrawals (debits), floor the magnitude to the target scale
// to ensure exact grid alignment without paying out extra assets.
actualDelta = roundToScale(magnitude, postScale, Number::RoundingMode::Downward);
// FixedPrecision only reaches this branch for credits (VaultDeposit);
// outflows go through clampVaultOutflow -> clampToAvailableScale,
// which rounds at AssetsAvailable's own posterior scale instead.
XRPL_ASSERT(
!delta.negative(), "xrpl::clampToAssetsTotalScale : FixedPrecision credit only");
STAmount const rounded =
roundToPosteriorVaultScale(vault, delta, Number::RoundingMode::TowardsZero);
actualDelta = rounded.negative() ? -rounded : rounded;
}
else
{
// For deposits (credits), derive actualDelta from the floored posterior total.
// This prevents grid alignment issues from crediting the vault more than deposited.
//
// Sum using Downward rounding so intermediate precision doesn't round up
// and exceed the original requested amount.
Number const posterior = [&] {
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
return assetsTotal + magnitude;
Number const assetsTotal = getAssetsTotal(vault);
// Calculate the scale after applying the delta using ToNearest rounding.
// This aligns the delta with scale checks used by vault invariants.
int const postScale = [&] {
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
return scale(assetsTotal + delta, asset);
}();
Number const roundedPosterior =
roundToAsset(asset, posterior, postScale, Number::RoundingMode::Downward);
actualDelta = STAmount{asset, roundedPosterior - assetsTotal};
if (delta.negative())
{
// For withdrawals (debits), floor the magnitude to the target scale
// to ensure exact grid alignment without paying out extra assets.
actualDelta = roundToScale(magnitude, postScale, Number::RoundingMode::Downward);
}
else
{
// For deposits (credits), derive actualDelta from the floored posterior total.
// This prevents grid alignment issues from crediting the vault more than deposited.
//
// Sum using Downward rounding so intermediate precision doesn't round up
// and exceed the original requested amount.
Number const posterior = [&] {
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
return assetsTotal + magnitude;
}();
Number const roundedPosterior =
roundToAsset(asset, posterior, postScale, Number::RoundingMode::Downward);
actualDelta = STAmount{asset, roundedPosterior - assetsTotal};
}
}
XRPL_ASSERT(
@@ -130,21 +604,47 @@ clampToAssetsTotalScale(SLE::ConstRef vault, STAmount const& delta)
return actualDelta;
}
[[nodiscard]] Number
assetsTotalForWithdrawal(SLE::ConstRef vault, WaiveUnrealizedLoss waive)
[[nodiscard]] std::expected<STAmount, TER>
clampVaultOutflow(SLE::ConstRef vault, STAmount const& delta)
{
Number assetTotal = vault->at(sfAssetsTotal);
if (waive == WaiveUnrealizedLoss::No)
assetTotal -= vault->at(sfLossUnrealized);
return assetTotal;
XRPL_ASSERT(delta.negative(), "xrpl::clampVaultOutflow : outflow delta is negative");
return getVaultVersion(vault) == VaultVersion::FixedPrecision
? clampToAvailableScale(vault, delta)
: clampToAssetsTotalScale(vault, delta);
}
[[nodiscard]] int
vaultBaseScale(Asset const& asset, std::uint8_t scale)
{
if (asset.integral())
return 0;
return -static_cast<int>(scale);
}
[[nodiscard]] bool
debitIsNonZeroDust(Asset const& asset, Number const& total, Number const& amount)
isOnVaultBaseGrid(Asset const& asset, Number const& value, int baseScale)
{
if (amount == 0)
return false;
return STAmount{asset, total - amount} == STAmount{asset, total};
// isRepresentable first: building an STAmount from an out-of-range value
// throws, and this checks user-supplied values in preclaim.
return isRepresentable(asset, value) && STAmount{asset, value} == value &&
roundToAsset(asset, value, baseScale, Number::RoundingMode::TowardsZero) == value;
}
[[nodiscard]] TER
checkAssetsMaximum(SLE::ConstRef vault, Number const& amount)
{
return isOnVaultBaseGrid(vault->at(sfAsset), amount, getVaultBaseScale(vault))
? TER{tesSUCCESS}
: TER{tecPRECISION_LOSS};
}
[[nodiscard]] Number
assetsTotalForWithdrawal(SLE::ConstRef vault, WaiveUnrealizedLoss waive)
{
Number assetTotal = getAssetsTotal(vault);
if (waive == WaiveUnrealizedLoss::No)
assetTotal -= vault->at(sfLossUnrealized);
return assetTotal;
}
[[nodiscard]] std::optional<STAmount>
@@ -218,34 +718,40 @@ isSoleShareholder(ReadView const& view, AccountID const& account, SLE::ConstRef
}
[[nodiscard]] VaultVersion
getVaultVersion(SLE::ConstRef vault)
decodeVaultVersion(std::optional<std::uint8_t> leVersion)
{
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultVersion : valid Vault sle");
if (!vault->isFieldPresent(sfLEVersion))
if (!leVersion)
return VaultVersion::Legacy;
auto const version = vault->at(sfLEVersion);
if (version > std::to_underlying(VaultVersion::CashBasis))
if (*leVersion > std::to_underlying(VaultVersion::FixedPrecision))
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::getVaultVersion : invalid vault version");
UNREACHABLE("xrpl::decodeVaultVersion : invalid vault version");
return VaultVersion::Legacy;
// LCOV_EXCL_STOP
}
return static_cast<VaultVersion>(version);
return static_cast<VaultVersion>(*leVersion);
}
namespace {
[[nodiscard]] VaultKind
decodeVaultKind(std::optional<std::uint8_t> vaultKind)
[[nodiscard]] VaultVersion
getVaultVersion(SLE::ConstRef vault)
{
if (vaultKind && *vaultKind == std::to_underlying(VaultKind::ClosedEnded))
return VaultKind::ClosedEnded;
return VaultKind::OpenEnded;
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultVersion : valid Vault sle");
return decodeVaultVersion(vault->at(~sfLEVersion));
}
} // namespace
[[nodiscard]] VaultVersion
getVaultDefaultVersion(Rules const& rules)
{
// FixedPrecision requires featureLendingProtocolV1_1 (closed-ended Vaults and
// cash-basis accounting) and fixCleanup3_4_0, so a FixedPrecision Vault always
// sees both enabled. fixCleanup3_2_0 is already enabled on the network.
if (rules.enabled(featureLendingProtocolV1_1) && rules.enabled(featureLendingProtocolV1_2) &&
rules.enabled(fixCleanup3_4_0))
return VaultVersion::FixedPrecision;
if (rules.enabled(featureLendingProtocolV1_1))
return VaultVersion::CashBasis;
return VaultVersion::Legacy;
}
[[nodiscard]] VaultKind
getVaultKind(SLE::ConstRef vault)
+21
View File
@@ -1,10 +1,24 @@
#include <xrpl/protocol/MPTAmount.h>
#include <xrpl/basics/MathUtilities.h>
#include <xrpl/basics/contract.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Rules.h>
#include <stdexcept>
namespace xrpl {
MPTAmount&
MPTAmount::operator+=(MPTAmount const& other)
{
if (auto const result = checkedAdd(value_, other.value()))
{
value_ = *result;
return *this;
}
if (isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true))
Throw<std::overflow_error>("MPTAmount::operator+= overflow");
value_ += other.value();
return *this;
}
@@ -12,6 +26,13 @@ MPTAmount::operator+=(MPTAmount const& other)
MPTAmount&
MPTAmount::operator-=(MPTAmount const& other)
{
if (auto const result = checkedSub(value_, other.value()))
{
value_ = *result;
return *this;
}
if (isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true))
Throw<std::overflow_error>("MPTAmount::operator-= overflow");
value_ -= other.value();
return *this;
}
+38 -1
View File
@@ -3,10 +3,14 @@
#include <xrpl/beast/utility/Zero.h> // IWYU pragma: keep
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/STAmount.h>
#include <cstdint>
#include <limits> // IWYU pragma: keep
#include <optional>
#include <stdexcept>
namespace xrpl {
@@ -81,13 +85,46 @@ Quality::ceilInStrict(Amounts const& amount, STAmount const& limit, bool roundUp
return ceilInImpl<divRoundStrict>(amount, limit, roundUp, *this);
}
// Returns std::nullopt if a native result can't be represented, instead of
// throwing. Pathfinding runs without transaction
// rules, so default to the fixed behavior there to match transactions once the
// amendment is enabled.
template <STAmount (*MulRoundFunc)(STAmount const&, STAmount const&, Asset const&, bool)>
static std::optional<STAmount>
tryMulRound(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundUp)
{
try
{
return MulRoundFunc(v1, v2, asset, roundUp);
}
catch (std::overflow_error const&)
{
// std::overflow_error is a std::runtime_error, so it must be caught
// first to keep the handler below from swallowing it. It means the
// computation itself overflowed, not that the result is out of range.
// Callers handle it, e.g. BookStep removes the offer.
throw;
}
catch (std::runtime_error const&)
{
if (!asset.native() || !isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true))
throw;
return std::nullopt;
}
}
template <STAmount (*MulRoundFunc)(STAmount const&, STAmount const&, Asset const&, bool)>
static Amounts
ceilOutImpl(Amounts const& amount, STAmount const& limit, bool roundUp, Quality const& quality)
{
if (amount.out > limit)
{
Amounts result(MulRoundFunc(limit, quality.rate(), amount.in.asset(), roundUp), limit);
// An unrepresentable XRP product is larger than amount.in, so the
// clamp below would return amount.in anyway.
Amounts result(
tryMulRound<MulRoundFunc>(limit, quality.rate(), amount.in.asset(), roundUp)
.value_or(amount.in),
limit);
// Clamp in
if (result.in > amount.in)
result.in = amount.in;
+101 -55
View File
@@ -1,6 +1,7 @@
#include <xrpl/protocol/STAmount.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/MathUtilities.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/contract.h>
#include <xrpl/basics/safe_cast.h>
@@ -384,10 +385,11 @@ operator+(STAmount const& v1, STAmount const& v2)
return {v1.getFName(), v1.asset(), v2.mantissa(), v2.exponent(), v2.negative()};
}
// XRPAmount and MPTAmount check for int64 overflow under fixCleanup3_5_0.
if (v1.native())
return {v1.getFName(), getSNValue(v1) + getSNValue(v2)};
return {v1.getFName(), (v1.xrp() + v2.xrp()).drops()};
if (v1.holds<MPTIssue>())
return {v1.asset_, v1.mpt().value() + v2.mpt().value()};
return {v1.asset_, (v1.mpt() + v2.mpt()).value()};
auto x = v1;
x = v1.iou() + v2.iou();
@@ -443,6 +445,22 @@ getRate(STAmount const& offerOut, STAmount const& offerIn)
}
}
bool
isRepresentable(Asset const& asset, Number const& value)
{
try
{
// Out-of-range values throw std::overflow_error from Number or
// IOUAmount, or std::runtime_error from STAmount::canonicalize.
[[maybe_unused]] STAmount const amount{asset, value};
return true;
}
catch (std::runtime_error const&)
{
return false;
}
}
/**
* @brief Safely checks if two STAmount values can be added without overflow,
* underflow, or precision loss.
@@ -474,16 +492,7 @@ canAdd(STAmount const& a, STAmount const& b)
// XRP case (overflow & underflow check)
if (isXRP(a) && isXRP(b))
{
XRPAmount const aVal = a.xrp();
XRPAmount const bVal = b.xrp();
return !(
(bVal > XRPAmount{0} &&
aVal > XRPAmount{std::numeric_limits<XRPAmount::value_type>::max()} - bVal) ||
(bVal < XRPAmount{0} &&
aVal < XRPAmount{std::numeric_limits<XRPAmount::value_type>::min()} - bVal));
}
return checkedAdd(a.xrp().drops(), b.xrp().drops()).has_value();
// IOU case (precision check)
auto const ret = std::visit(
@@ -500,15 +509,7 @@ canAdd(STAmount const& a, STAmount const& b)
// MPT (overflow & underflow check)
if constexpr (kIsMptissueV<TIss1> && kIsMptissueV<TIss2>)
{
MPTAmount const aVal = a.mpt();
MPTAmount const bVal = b.mpt();
return !(
(bVal > MPTAmount{0} &&
aVal > MPTAmount{std::numeric_limits<MPTAmount::value_type>::max()} - bVal) ||
(bVal < MPTAmount{0} &&
aVal < MPTAmount{std::numeric_limits<MPTAmount::value_type>::min()} - bVal));
}
return checkedAdd(a.mpt().value(), b.mpt().value()).has_value();
return std::nullopt;
},
a.asset().value(),
@@ -559,11 +560,7 @@ canSubtract(STAmount const& a, STAmount const& b)
return false;
// Check for overflow
if (bVal < XRPAmount{0} &&
aVal > XRPAmount{std::numeric_limits<XRPAmount::value_type>::max()} + bVal)
return false;
return true;
return checkedSub(aVal.drops(), bVal.drops()).has_value();
}
// IOU case (no underflow)
@@ -586,10 +583,7 @@ canSubtract(STAmount const& a, STAmount const& b)
return false;
// Overflow check
if (bVal < MPTAmount{0} &&
aVal > MPTAmount{std::numeric_limits<MPTAmount::value_type>::max()} + bVal)
return false;
return true;
return checkedSub(aVal.value(), bVal.value()).has_value();
}
return std::nullopt;
},
@@ -1231,6 +1225,19 @@ divide(STAmount const& num, STAmount const& den, Asset const& asset)
if (num == beast::kZero)
return {asset};
// The legacy path below assumes 16-digit mantissas. An MPT mantissa is
// the raw 63-bit value and is never scaled down, so an MPT numerator can
// overflow the quotient and an MPT denominator shortens it, moving the +5
// nudge into a significant digit. Use Number arithmetic under
// MPTokensV2 whenever an MPT is involved, as mulRound/divRound do. Pin
// the rounding mode so the result does not depend on the caller's.
if (isFeatureEnabled(featureMPTokensV2, /*resultIfNoRules*/ true) &&
(asset.holds<MPTIssue>() || num.holds<MPTIssue>() || den.holds<MPTIssue>()))
{
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
return STAmount{asset, Number{num} / Number{den}};
}
std::uint64_t numVal = num.mantissa();
std::uint64_t denVal = den.mantissa();
int numOffset = num.exponent();
@@ -1260,11 +1267,21 @@ divide(STAmount const& num, STAmount const& den, Asset const& asset)
// numerator by 10^17 (the product is in the range of
// 10^32 to 10^33) followed by a division, so the result
// is in the range of 10^16 to 10^15.
return STAmount(
asset,
muldiv(numVal, kTenTO17, denVal) + 5,
numOffset - denOffset - 17,
num.negative() != den.negative());
std::uint64_t quotient = muldiv(numVal, kTenTO17, denVal);
int offset = numOffset - denOffset - 17;
// An integral (XRP) numerator is not scaled down to 16 digits, so the
// quotient can land in [2^63, 2^64). The STAmount constructor reads
// that as a negative IOU mantissa, and the +5 can wrap. Drop a digit
// first; the result is canonicalized to 16 digits anyway.
if (quotient > static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max()) - 5 &&
isFeatureEnabled(featureMPTokensV2, /*resultIfNoRules*/ true))
{
quotient /= 10;
++offset;
}
return STAmount(asset, quotient + 5, offset, num.negative() != den.negative());
}
STAmount
@@ -1470,28 +1487,28 @@ roundNumberResult(
// and for materializing the final integral amount.
NumberRoundModeGuard const finalRound(roundMode(resultNegative, roundUp));
auto result = STAmount{asset, number};
// An integral result can't underflow: when roundUp is set, roundMode()
// above selects Upward, and materializing a Number into an integral
// STAmount honors that mode (Number::operator rep()), so any positive
// value rounds up to at least 1.
[[maybe_unused]] bool const nonzeroPositiveRoundUp =
roundUp && !resultNegative && number != beast::kZero;
ALWAYS(
!nonzeroPositiveRoundUp || result != beast::kZero,
"xrpl::roundNumberResult : positive rounded-up MPT result is representable");
!nonzeroPositiveRoundUp || !asset.integral() || result != beast::kZero,
"xrpl::roundNumberResult : positive rounded-up integral result is representable");
if (roundUp && !resultNegative && !result)
{
// Intended to preserve existing mulRound/divRound behavior for a
// positive result too small to represent in the target asset.
//
// Unreachable in practice: when roundUp is set, roundMode() above
// selects Upward, and materializing a Number into an STAmount honors
// that mode (Number::operator rep()), so any positive value rounds up
// to at least the smallest representable unit. Hence, a positive result
// is never !result here; the only zero case is a zero operand, which
// the mulRound/divRound callers handle before reaching this function.
// LCOV_EXCL_START
// Preserve existing mulRound/divRound behavior for a positive result
// too small to represent in the target asset. An IOU result with an
// MPT operand (e.g. 1 MPT / 1e95) below the smallest IOU is flushed to
// zero by IOUAmount normalization regardless of the rounding mode, so
// snap it up to the smallest positive IOU. The only zero case for an
// integral result is a zero operand, which the mulRound/divRound
// callers handle before reaching this function.
if (asset.integral())
return STAmount{asset, 1};
return STAmount{asset, 1}; // LCOV_EXCL_LINE
return STAmount{asset, STAmount::kMinValue, STAmount::kMinOffset, false};
// LCOV_EXCL_STOP
}
return result;
@@ -1540,11 +1557,19 @@ mulRoundImpl(STAmount const& v1, STAmount const& v2, Asset const& asset, bool ro
bool const resultNegative = v1.negative() != v2.negative();
if (asset.holds<MPTIssue>() && isFeatureEnabled(featureMPTokensV2, false))
// The legacy path below assumes 16-digit mantissas. An MPT mantissa is the
// raw 63-bit balance, so any MPT operand (not just an MPT result) can
// overflow it: a large MPT amount times an IOU-shaped rate into an XRP or
// IOU result is the offer-crossing remainder and partial-fill case. Use
// Number arithmetic under MPTokensV2 whenever an MPT is involved.
//
// Outside a transaction (no current rules, e.g. pathfinding) default to
// the Number path: an MPT operand can only reach here through MPT
// offers or AMMs, which require MPTokensV2, so this keeps RPC quotes on
// the same arithmetic as the transaction they describe.
if (isFeatureEnabled(featureMPTokensV2, /*resultIfNoRules*/ true) &&
(asset.holds<MPTIssue>() || v1.holds<MPTIssue>() || v2.holds<MPTIssue>()))
{
// MPT DEX can combine 63-bit MPT amounts with IOU-shaped transfer
// rates. Use Number arithmetic under MPTokensV2 so the rounded
// operation is not limited by the legacy uint64_t scaled mantissa.
Number result;
{
NumberRoundModeGuard const operationRound(roundMode(resultNegative, roundUp));
@@ -1642,9 +1667,12 @@ divRoundImpl(STAmount const& num, STAmount const& den, Asset const& asset, bool
bool const resultNegative = (num.negative() != den.negative());
// See mulRoundImpl: any MPT operand, not just an MPT result, and the
// Number path when there are no current rules.
// fixCleanup3_5_0: the legacy path below overflows on large MPT amounts.
if (asset.holds<MPTIssue>() &&
(isFeatureEnabled(featureMPTokensV2, true) || isFeatureEnabled(fixCleanup3_5_0, true)))
auto const enabled = isFeatureEnabled(featureMPTokensV2, /*resultIfNoRules*/ true) ||
isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true);
if ((asset.holds<MPTIssue>() || num.holds<MPTIssue>() || den.holds<MPTIssue>()) && enabled)
{
// Match the multiply path above: Number performs the rounded
// operation, then STAmount materializes the final MPT amount using the
@@ -1735,6 +1763,24 @@ 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
{
+1
View File
@@ -228,6 +228,7 @@ transResults()
MAKE_ERROR(terNO_DELEGATE_PERMISSION, "Delegated account lacks permission to perform this transaction."),
MAKE_ERROR(terLOCKED, "Fund is locked."),
MAKE_ERROR(terNO_PERMISSION, "No permission to perform requested operation."),
MAKE_ERROR(terTICKET_RESERVED, "Ticket is reserved."),
MAKE_ERROR(tesSUCCESS, "The transaction was applied. Only final in a validated ledger."),
};
+54
View File
@@ -0,0 +1,54 @@
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/basics/MathUtilities.h>
#include <xrpl/basics/contract.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Rules.h>
#include <stdexcept>
namespace xrpl {
XRPAmount&
XRPAmount::operator+=(value_type const& rhs)
{
if (auto const result = checkedAdd(drops_, rhs))
{
drops_ = *result;
return *this;
}
if (isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true))
Throw<std::overflow_error>("XRPAmount::operator+= overflow");
drops_ += rhs;
return *this;
}
XRPAmount&
XRPAmount::operator-=(value_type const& rhs)
{
if (auto const result = checkedSub(drops_, rhs))
{
drops_ = *result;
return *this;
}
if (isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true))
Throw<std::overflow_error>("XRPAmount::operator-= overflow");
drops_ -= rhs;
return *this;
}
XRPAmount&
XRPAmount::operator*=(value_type const& rhs)
{
if (auto const result = checkedMul(drops_, rhs))
{
drops_ = *result;
return *this;
}
if (isFeatureEnabled(fixCleanup3_5_0, /*resultIfNoRules*/ true))
Throw<std::overflow_error>("XRPAmount::operator*= overflow");
drops_ *= rhs;
return *this;
}
} // namespace xrpl
+12 -2
View File
@@ -1,5 +1,15 @@
# AGENTS.md — tx
See the repo-level [AGENTS.md](../../../AGENTS.md) for general build/test/style guidance.
## When an amendment is required
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.
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.
+35 -2
View File
@@ -16,6 +16,7 @@
#include <xrpl/ledger/helpers/DelegateHelpers.h>
#include <xrpl/ledger/helpers/NFTokenHelpers.h>
#include <xrpl/ledger/helpers/OfferHelpers.h>
#include <xrpl/ledger/helpers/ProposalHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/protocol/AccountID.h>
@@ -327,10 +328,9 @@ Transactor::preflight2(PreflightContext const& ctx)
auto const sigValid = checkValidity(ctx.registry.get().getHashRouter(), ctx.tx, ctx.rules);
if (sigValid.first == Validity::SigBad)
{ // LCOV_EXCL_START
{
JLOG(ctx.j.debug()) << "preflight2: bad signature. " << sigValid.second;
return temINVALID;
// LCOV_EXCL_STOP
}
// Do not add any checks after this point that are relevant for
@@ -755,6 +755,19 @@ Transactor::checkSeqProxy(ReadView const& view, STTx const& tx, beast::Journal j
<< "a_seq=" << aSeq << " t_seq=" << tSeqProx;
return tefNO_TICKET;
}
// While a TransactionProposal keyed to this Ticket exists, the
// Ticket is reserved: only the proposal's own proposed transaction
// may consume it, so unrelated activity cannot invalidate the
// proposal while signatures are being collected (On-Chain Cosigner spec §4.2.1).
// Cancelling the proposal frees the Ticket, so this is a retry, not
// a final failure. No proposal can exist before the Cosign amendment,
// so the gate only makes that dependency explicit.
if (view.rules().enabled(featureCosign) && !proposal::canConsumeTicket(view, tx))
{
JLOG(j.trace()) << "applyTransaction: ticket " << tSeqProx << " is reserved";
return terTICKET_RESERVED;
}
}
return tesSUCCESS;
@@ -866,8 +879,28 @@ Transactor::ticketDelete(
// Update the Ticket owner's reserve.
decreaseOwnerCountForObject(view, sleAccount, sleTicket, 1, j);
std::uint32_t const ticketSeq{(*sleTicket)[sfTicketSequence]};
// Remove Ticket from ledger.
view.erase(sleTicket);
// Once the Ticket is gone, a TransactionProposal keyed to it can never
// execute: its proposed transaction would fail with tefNO_TICKET. Clean
// up the stale proposal and release its Owner's reserve (On-Chain Cosigner spec §4.5).
// The reservation check in checkSeqProxy means this is reached only by
// the proposal's own proposed transaction executing (or failing with a
// claimed-fee tec), or by AccountDelete sweeping the target account's
// Tickets. No proposal can exist before the Cosign amendment, so the gate
// only makes that dependency explicit.
if (view.rules().enabled(featureCosign))
{
if (auto const sleProposal = view.peek(keylet::txProposal(account, ticketSeq)))
{
if (TER const ter = proposal::deleteProposal(view, sleProposal, j); !isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
}
}
return tesSUCCESS;
}
@@ -9,6 +9,7 @@
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
@@ -22,6 +23,7 @@
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STVector256.h>
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
@@ -1010,6 +1012,78 @@ ValidClawback::finalize(
//------------------------------------------------------------------------------
// Whether `entry` is the trust line or MPToken through which `account` holds
// `asset`. XRP has no such entry.
static bool
isHolding(SLE const& entry, Asset const& asset, AccountID const& account)
{
return asset.visit(
[&](Issue const& issue) {
return !isXRP(issue) && keylet::trustLine(account, issue).check(entry);
},
[&](MPTIssue const& issue) {
return keylet::mptoken(issue.getMptID(), account).check(entry);
});
}
// Whether `entry`, linked into the owner directory of `pseudo`, belongs to the
// AMM, Vault or LoanBroker that the pseudo-account was created for.
static bool
isOwnedByPseudoAccount(ReadView const& view, SLE const& pseudo, SLE const& entry)
{
auto const pseudoAcctID = pseudo[sfAccount];
if (pseudo.isFieldPresent(sfAMMID))
{
if (entry.getType() == ltAMM)
return entry.key() == pseudo[sfAMMID];
auto const amm = view.read(keylet::amm(pseudo[sfAMMID]));
if (!amm)
return false; // LCOV_EXCL_LINE
if (isHolding(entry, (*amm)[sfAsset], pseudoAcctID) ||
isHolding(entry, (*amm)[sfAsset2], pseudoAcctID))
return true;
// The AMM issues LP tokens, so each liquidity provider's trust line
// links into its directory.
if (entry.getType() != ltRIPPLE_STATE)
return false;
auto const low = entry[sfLowLimit].getIssuer();
auto const high = entry[sfHighLimit].getIssuer();
auto const lpCurrency = (*amm)[sfLPTokenBalance].get<Issue>().currency;
return (low == pseudoAcctID || high == pseudoAcctID) &&
entry.key() == keylet::trustLine(low, high, lpCurrency).key;
}
if (pseudo.isFieldPresent(sfVaultID))
{
if (entry.getType() == ltLOAN_BROKER)
return entry[sfVaultID] == pseudo[sfVaultID];
auto const vault = view.read(keylet::vault(pseudo[sfVaultID]));
if (!vault)
return false; // LCOV_EXCL_LINE
return keylet::mptokenIssuance((*vault)[sfShareMPTID]).check(entry) ||
isHolding(entry, (*vault)[sfAsset], pseudoAcctID);
}
if (pseudo.isFieldPresent(sfLoanBrokerID))
{
if (entry.getType() == ltLOAN)
return entry[sfLoanBrokerID] == pseudo[sfLoanBrokerID];
auto const broker = view.read(keylet::loanBroker(pseudo[sfLoanBrokerID]));
if (!broker)
return false; // LCOV_EXCL_LINE
auto const vault = view.read(keylet::vault((*broker)[sfVaultID]));
return vault && isHolding(entry, (*vault)[sfAsset], pseudoAcctID);
}
// A new pseudo-account kind must say what it owns.
return false; // LCOV_EXCL_LINE
}
void
ValidPseudoAccounts::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
{
@@ -1019,6 +1093,33 @@ ValidPseudoAccounts::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstR
return;
}
// Only owner directory pages carry sfOwner; book directories don't.
if (after && after->getType() == ltDIR_NODE && after->isFieldPresent(sfOwner))
{
auto const& afterIndexes = after->getFieldV256(sfIndexes);
std::vector<uint256> added;
if (!before)
{
added.assign(afterIndexes.begin(), afterIndexes.end());
}
else
{
auto const& beforeIndexes = before->getFieldV256(sfIndexes);
for (auto const& index : afterIndexes)
{
if (!std::ranges::contains(beforeIndexes, index))
added.push_back(index);
}
}
if (!added.empty())
{
auto& owned = ownerDirAdditions_[after->getAccountID(sfOwner)];
owned.insert(owned.end(), added.begin(), added.end());
}
return;
}
if (after && after->getType() == ltACCOUNT_ROOT)
{
bool const isPseudo = [&] {
@@ -1100,6 +1201,39 @@ ValidPseudoAccounts::finalize(
if (enforce)
return false;
}
// Pre-fixCleanup3_5_0: what a pseudo-account owns is not checked.
// Post-fixCleanup3_5_0: it may own only what belongs to its AMM, Vault or
// LoanBroker.
if (!view.rules().enabled(fixCleanup3_5_0))
return true;
for (auto const& [owner, indexes] : ownerDirAdditions_)
{
auto const root = view.read(keylet::account(owner));
if (!root || !isPseudoAccount(root))
continue;
for (auto const& index : indexes)
{
auto const sle = view.read(keylet::unchecked(index));
if (!sle)
{
JLOG(j.fatal()) << "Invariant failed: pseudo-account " << toBase58(owner)
<< " owner directory links a missing object " << to_string(index);
return false;
}
if (!isOwnedByPseudoAccount(view, *root, *sle))
{
JLOG(j.fatal()) << "Invariant failed: pseudo-account " << toBase58(owner)
<< " may not own " << ledgerEntryTypeName(*sle) << " "
<< to_string(index);
return false;
}
}
}
return true;
}
@@ -5,8 +5,8 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/LendingHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -160,7 +160,7 @@ ValidLoanBroker::finalize(
roundToAsset(
Asset{vault->at(sfAsset)},
debtTotal,
getAssetsTotalScale(vault),
getVaultScale(vault),
Number::RoundingMode::TowardsZero) != beast::kZero)
{
JLOG(j.fatal())
+19 -13
View File
@@ -339,6 +339,7 @@ ValidMPTIssuance::finalize(
// Cap on MPToken creates and deletes while featureLendingProtocol is enabled.
// - LoanSet: at most two creates and no deletes.
// - VaultWithdraw: at most one create and one delete.
// - LoanBrokerDelete (post-fixCleanup3_5_0): at most one create and one delete.
// - Other MayAuthorizeMpt types: created + deleted <= 1.
// - MustAuthorizeMpt still requires exactly one create or delete below.
auto const mptokensExceedAuthorizeCap = [&] {
@@ -351,6 +352,10 @@ ValidMPTIssuance::finalize(
if (txnType == ttVAULT_WITHDRAW)
return mptokensCreated_ > 1 || mptokensDeleted_ > 1;
}
// The cover payout may create the owner's MPToken while the
// pseudo-account's MPToken is deleted.
if (rules.enabled(fixCleanup3_5_0) && txnType == ttLOAN_BROKER_DELETE)
return mptokensCreated_ > 1 || mptokensDeleted_ > 1;
return (mptokensCreated_ + mptokensDeleted_) > 1;
};
if (mptokensExceedAuthorizeCap())
@@ -1018,24 +1023,25 @@ ValidMPTTransfer::finalize(
continue;
}
// These transactions are recovery/settlement paths. They may move an
// existing MPT position even after the issuer clears CanTransfer, so
// holders are not trapped in AMM, vault, or loan protocol accounts.
auto const waivesCanTransfer = txnType == ttAMM_WITHDRAW ||
(view.rules().enabled(fixCleanup3_2_0) &&
(txnType == ttVAULT_WITHDRAW || txnType == ttLOAN_BROKER_COVER_WITHDRAW ||
txnType == ttLOAN_PAY));
auto const canTransfer = sleIssuance->isFlag(lsfMPTCanTransfer) || waivesCanTransfer;
auto const canTrade = sleIssuance->isFlag(lsfMPTCanTrade);
auto const reqAuth = sleIssuance->isFlag(lsfMPTRequireAuth);
// This issuance is the LoanManage default's own vault asset, so the
// broker/vault freeze exemption applies to it -- an unrelated MPT
// issuance the same accounts happen to hold is still caught.
// broker/vault freeze and CanTransfer exemptions apply to it -- an
// unrelated MPT the same accounts happen to hold is still caught.
bool const isLoanDefaultAsset = loanDefaultAccounts &&
loanDefaultAccounts->asset.holds<MPTIssue>() &&
loanDefaultAccounts->asset.get<MPTIssue>().getMptID() == mptID;
// These transactions settle an existing MPT position out of a protocol
// account. They do not require CanTransfer.
auto const waivesCanTransfer = txnType == ttAMM_WITHDRAW ||
(view.rules().enabled(fixCleanup3_2_0) &&
(txnType == ttVAULT_WITHDRAW || txnType == ttLOAN_BROKER_COVER_WITHDRAW ||
txnType == ttLOAN_PAY)) ||
(view.rules().enabled(fixCleanup3_5_0) &&
(isLoanDefaultAsset || txnType == ttLOAN_BROKER_DELETE));
auto const canTransfer = sleIssuance->isFlag(lsfMPTCanTransfer) || waivesCanTransfer;
auto const canTrade = sleIssuance->isFlag(lsfMPTCanTrade);
auto const reqAuth = sleIssuance->isFlag(lsfMPTRequireAuth);
for (auto const& [account, value] : values)
{
// Classify each account as a sender or receiver based on whether their MPTAmount
+226 -81
View File
@@ -7,12 +7,14 @@
#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>
@@ -63,6 +65,9 @@ ValidVault::Vault::make(SLE const& from)
self.assetsAvailable = from.at(sfAssetsAvailable);
self.assetsMaximum = from.at(sfAssetsMaximum);
self.lossUnrealized = from.at(sfLossUnrealized);
self.assetsDeployed = from[~sfAssetsDeployed].value_or(Number{0});
self.yieldUnrealized = from[~sfYieldUnrealized].value_or(Number{0});
self.version = decodeVaultVersion(from[~sfLEVersion]);
self.vaultKind = from[~sfVaultKind];
self.subscriptionDate = from[~sfSubscriptionDate];
self.redemptionDate = from[~sfRedemptionDate];
@@ -408,6 +413,70 @@ ValidVault::computeVaultMinScale(DeltaInfo const& vaultDelta, Rules const& rules
return computeCoarsestScale({vaultDelta, totalDelta, availableDelta});
}
bool
ValidVault::checkTotalsAddUp(
Vault const& afterVault,
Vault const& beforeVault,
Asset const& vaultAsset,
bool fix340Enabled,
std::int32_t minScale,
Number const& roundedVaultDelta,
Number const& exactVaultDelta,
char const* verb,
beast::Journal const& j)
{
bool result = true;
// FixedPrecision: the stored AssetsTotal is a Downward-floored cache
// (AA + AD), not an exact running total, so comparing its delta against
// the real transfer can be off by more than one unit purely from
// independent floor-rounding of the two snapshots -- not an accounting
// bug. The stored-cache-equals-derived-total check elsewhere in finalize,
// plus the AssetsDeployed-only-changed-by-lending-transactors check,
// already cover AssetsTotal; only compare it here on Legacy/CashBasis,
// where AssetsTotal is written exactly alongside AA.
if (afterVault.version != VaultVersion::FixedPrecision)
{
auto const assetTotalDelta =
roundToAsset(vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
bool const totalAddsUp = fix340Enabled
? agreesWithinOneUnit(assetTotalDelta, roundedVaultDelta, vaultAsset, minScale)
: assetTotalDelta == roundedVaultDelta;
if (!totalAddsUp)
{
JLOG(j.fatal()) << "Invariant failed: " << verb
<< " and assets outstanding must add up";
result = false;
}
}
auto const assetAvailableDelta = roundToAsset(
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
// FixedPrecision: AssetsAvailable is the exact, single writer
// (adjustVaultBalances)-managed balance, so its delta must match the real
// transfer exactly, with no rounding tolerance -- compare the exact
// deltas, not the values rounded to the posterior AssetsTotal scale.
bool const availableAddsUp = [&] {
if (afterVault.version == VaultVersion::FixedPrecision)
{
return (afterVault.assetsAvailable - beforeVault.assetsAvailable) == exactVaultDelta;
}
if (fix340Enabled)
{
return agreesWithinOneUnit(
assetAvailableDelta, roundedVaultDelta, vaultAsset, minScale);
}
return assetAvailableDelta == roundedVaultDelta;
}();
if (!availableAddsUp)
{
JLOG(j.fatal()) << "Invariant failed: " << verb << " and assets available must add up";
result = false;
}
return result;
}
bool
ValidVault::finalize(
STTx const& tx,
@@ -418,6 +487,7 @@ 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
@@ -585,6 +655,13 @@ ValidVault::finalize(
"vault must have no assets available";
result = false;
}
if (afterVault.version == VaultVersion::FixedPrecision &&
afterVault.assetsDeployed != kZero)
{
JLOG(j.fatal()) << "Invariant failed: updated zero sized "
"vault must have no AssetsDeployed";
result = false;
}
}
else if (updatedShares->sharesTotal > updatedShares->sharesMaximum)
{
@@ -609,6 +686,14 @@ ValidVault::finalize(
else
{
bool const gapExceeded = [&] {
if (afterVault.version == VaultVersion::FixedPrecision)
{
// FixedPrecision: both LossUnrealized and AssetsDeployed are exact
// (never rounded), so compare them directly. AssetsTotal is
// just a rounded cache of AssetsAvailable + AssetsDeployed and
// is not the reference this check needs.
return afterVault.lossUnrealized > afterVault.assetsDeployed;
}
if (!fix340Enabled)
{
return afterVault.lossUnrealized >
@@ -631,6 +716,43 @@ ValidVault::finalize(
}
}
if (afterVault.version == VaultVersion::FixedPrecision)
{
if (afterVault.assetsDeployed < kZero)
{
JLOG(j.fatal()) << "Invariant failed: AssetsDeployed must not be negative";
result = false;
}
else if (
Number(STAmount{afterVault.asset, afterVault.assetsDeployed}) !=
afterVault.assetsDeployed)
{
JLOG(j.fatal()) //
<< "Invariant failed: AssetsDeployed must be exactly representable "
"at the vault asset's precision";
result = false;
}
Number const expectedAssetsTotal = [&] {
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
return Number(
STAmount{afterVault.asset, afterVault.assetsAvailable + afterVault.assetsDeployed});
}();
if (afterVault.assetsTotal != expectedAssetsTotal)
{
JLOG(j.fatal()) //
<< "Invariant failed: stored AssetsTotal must equal AssetsAvailable "
"plus AssetsDeployed, rounded Downward";
result = false;
}
if (afterVault.yieldUnrealized < kZero)
{
JLOG(j.fatal()) << "Invariant failed: YieldUnrealized must not be negative";
result = false;
}
}
if (fix340Enabled && afterVault.lossUnrealized < kZero)
{
JLOG(j.fatal()) << "Invariant failed: loss unrealized must not be negative";
@@ -659,6 +781,18 @@ ValidVault::finalize(
return !enforce; // That's all we can do here
}
// ttLOAN_SET, ttLOAN_PAY and ttLOAN_MANAGE are the only transactors allowed to
// write AssetsDeployed and YieldUnrealized; any new transactor that writes
// either field must be added to both allow-lists below.
if (afterVault.version == VaultVersion::FixedPrecision && !beforeVault_.empty() &&
afterVault.assetsDeployed != beforeVault_[0].assetsDeployed && txnType != ttLOAN_SET &&
txnType != ttLOAN_PAY && txnType != ttLOAN_MANAGE)
{
JLOG(j.fatal()) << //
"Invariant failed: vault transaction must not change AssetsDeployed";
result = false;
}
if (!beforeVault_.empty() && afterVault.lossUnrealized != beforeVault_[0].lossUnrealized &&
txnType != ttLOAN_MANAGE && txnType != ttLOAN_PAY)
{
@@ -668,6 +802,16 @@ ValidVault::finalize(
result = false;
}
if (afterVault.version == VaultVersion::FixedPrecision && !beforeVault_.empty() &&
afterVault.yieldUnrealized != beforeVault_[0].yieldUnrealized && txnType != ttLOAN_SET &&
txnType != ttLOAN_PAY && txnType != ttLOAN_MANAGE)
{
JLOG(j.fatal()) << //
"Invariant failed: vault transaction must not change yield "
"unrealized";
result = false;
}
// Immutability of VaultKind, SubscriptionDate and RedemptionDate is enforced by
// NoModifiedUnmodifiableFields in InvariantCheck.cpp.
@@ -716,7 +860,9 @@ ValidVault::finalize(
}
if (afterVault.assetsAvailable != kZero || afterVault.assetsTotal != kZero ||
afterVault.lossUnrealized != kZero || updatedShares->sharesTotal != 0)
afterVault.lossUnrealized != kZero || updatedShares->sharesTotal != 0 ||
(afterVault.version == VaultVersion::FixedPrecision &&
(afterVault.assetsDeployed != kZero || afterVault.yieldUnrealized != kZero)))
{
JLOG(j.fatal()) //
<< "Invariant failed: created vault must be empty";
@@ -880,7 +1026,17 @@ ValidVault::finalize(
result = false;
}
if (vaultDeltaAssets <= kZero)
// FixedPrecision: AssetsAvailable is exact and the pseudo-account
// delta is exact; rounding either one down to the posterior
// AssetsTotal scale before the sign check can turn a legitimate
// sub-unit deposit into a false zero. Compare the exact deltas
// instead; Legacy/CashBasis keep the rounded comparison.
bool const isFixedPrecision = afterVault.version == VaultVersion::FixedPrecision;
Number const exactVaultDeltaAssets = maybeVaultDeltaAssets->delta;
Number const signCheckDeltaAssets =
isFixedPrecision ? exactVaultDeltaAssets : vaultDeltaAssets;
if (signCheckDeltaAssets <= kZero)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must increase vault balance";
@@ -976,29 +1132,17 @@ ValidVault::finalize(
result = false;
}
auto const assetTotalDelta = roundToAsset(
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
bool const totalAddsUp = fix340Enabled
? agreesWithinOneUnit(assetTotalDelta, vaultDeltaAssets, vaultAsset, minScale)
: assetTotalDelta == vaultDeltaAssets;
if (!totalAddsUp)
{
JLOG(j.fatal())
<< "Invariant failed: deposit and assets outstanding must add up";
if (!checkTotalsAddUp(
afterVault,
beforeVault,
vaultAsset,
fix340Enabled,
minScale,
vaultDeltaAssets,
exactVaultDeltaAssets,
"deposit",
j))
result = false;
}
auto const assetAvailableDelta = roundToAsset(
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
bool const availableAddsUp = fix340Enabled
? agreesWithinOneUnit(
assetAvailableDelta, vaultDeltaAssets, vaultAsset, minScale)
: assetAvailableDelta == vaultDeltaAssets;
if (!availableAddsUp)
{
JLOG(j.fatal()) << "Invariant failed: deposit and assets available must add up";
result = false;
}
return result;
}
@@ -1054,7 +1198,17 @@ ValidVault::finalize(
auto const vaultPseudoDeltaAssets =
roundToAsset(vaultAsset, vaultDeltaAssets.delta, minScale);
if (!zeroDeltaIsLegitimate && vaultPseudoDeltaAssets >= kZero)
// FixedPrecision: AssetsAvailable is exact and the pseudo-account
// delta is exact; rounding either one down to the posterior
// AssetsTotal scale before the sign check can turn a legitimate
// sub-unit withdrawal into a false zero and reject it. Compare
// the exact deltas instead; Legacy/CashBasis keep the rounded
// comparison.
bool const isFixedPrecision = afterVault.version == VaultVersion::FixedPrecision;
Number const signCheckDeltaAssets =
isFixedPrecision ? vaultDeltaAssets.delta : vaultPseudoDeltaAssets;
if (!zeroDeltaIsLegitimate && signCheckDeltaAssets >= kZero)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal must decrease vault balance";
result = false;
@@ -1144,6 +1298,18 @@ 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
@@ -1152,7 +1318,8 @@ 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 = tolerateZeroDelta &&
auto const destroyedIsSubUlp = !feeAdjustedWithdrawal &&
tolerateZeroDelta &&
roundToAsset(
vaultAsset,
vaultDeltaAssets.delta * -1 - destinationDelta.delta,
@@ -1160,11 +1327,11 @@ ValidVault::finalize(
Number::RoundingMode::Downward) == kZero;
bool const withdrawAddsUp = fix340Enabled
? agreesWithinOneUnit(
localPseudoDeltaAssets * -1,
expectedDestinationDelta,
roundedDestinationDelta,
vaultAsset,
localMinScale)
: localPseudoDeltaAssets * -1 == roundedDestinationDelta;
: expectedDestinationDelta == roundedDestinationDelta;
if (!destroyedIsSubUlp && !withdrawAddsUp)
{
JLOG(j.fatal()) << "Invariant failed: " << //
@@ -1205,33 +1372,17 @@ ValidVault::finalize(
result = false;
}
auto const assetTotalDelta = roundToAsset(
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
// Note, vaultBalance is negative (see check above)
bool const totalAddsUp = fix340Enabled
? agreesWithinOneUnit(
assetTotalDelta, vaultPseudoDeltaAssets, vaultAsset, minScale)
: assetTotalDelta == vaultPseudoDeltaAssets;
if (!totalAddsUp)
{
JLOG(j.fatal())
<< "Invariant failed: withdrawal and assets outstanding must add up";
if (!checkTotalsAddUp(
afterVault,
beforeVault,
vaultAsset,
fix340Enabled,
minScale,
vaultPseudoDeltaAssets,
vaultDeltaAssets.delta,
"withdrawal",
j))
result = false;
}
auto const assetAvailableDelta = roundToAsset(
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
bool const availableAddsUp = fix340Enabled
? agreesWithinOneUnit(
assetAvailableDelta, vaultPseudoDeltaAssets, vaultAsset, minScale)
: assetAvailableDelta == vaultPseudoDeltaAssets;
if (!availableAddsUp)
{
JLOG(j.fatal())
<< "Invariant failed: withdrawal and assets available must add up";
result = false;
}
return result;
}
@@ -1263,39 +1414,33 @@ ValidVault::finalize(
computeVaultMinScale(*maybeVaultDeltaAssets, view.rules());
auto const vaultDeltaAssets =
roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
if (vaultDeltaAssets >= kZero)
// FixedPrecision: AssetsAvailable is exact and the
// pseudo-account delta is exact; rounding either one down
// to the posterior AssetsTotal scale before the sign
// check can turn a legitimate sub-unit clawback into a
// false zero and reject it. Compare the exact deltas
// instead; Legacy/CashBasis keep the rounded comparison.
bool const isFixedPrecision =
afterVault.version == VaultVersion::FixedPrecision;
Number const signCheckDeltaAssets =
isFixedPrecision ? maybeVaultDeltaAssets->delta : vaultDeltaAssets;
if (signCheckDeltaAssets >= kZero)
{
JLOG(j.fatal()) << "Invariant failed: clawback must decrease vault balance";
result = false;
}
auto const assetsTotalDelta = roundToAsset(
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
bool const totalAddsUp = fix340Enabled
? agreesWithinOneUnit(
assetsTotalDelta, vaultDeltaAssets, vaultAsset, minScale)
: assetsTotalDelta == vaultDeltaAssets;
if (!totalAddsUp)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback and assets outstanding must add up";
if (!checkTotalsAddUp(
afterVault,
beforeVault,
vaultAsset,
fix340Enabled,
minScale,
vaultDeltaAssets,
maybeVaultDeltaAssets->delta,
"clawback",
j))
result = false;
}
auto const assetAvailableDelta = roundToAsset(
vaultAsset,
afterVault.assetsAvailable - beforeVault.assetsAvailable,
minScale);
bool const availableAddsUp = fix340Enabled
? agreesWithinOneUnit(
assetAvailableDelta, vaultDeltaAssets, vaultAsset, minScale)
: assetAvailableDelta == vaultDeltaAssets;
if (!availableAddsUp)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback and assets available must add up";
result = false;
}
}
else if (!isVaultEmpty(beforeVault))
{
+1 -1
View File
@@ -177,7 +177,7 @@ AMMLiquidity<TIn, TOut>::getOffer(ReadView const& view, std::optional<Quality> c
return maxOffer(balances);
}
if (auto const amounts =
changeSpotPriceQuality(balances, *clobQuality, tradingFee_, view.rules(), j_))
changeSpotPriceQuality(balances, *clobQuality, tradingFee_, j_))
{
return AMMOffer<TIn, TOut>(*this, *amounts, balances, Quality{*amounts});
}
+164 -70
View File
@@ -24,7 +24,6 @@
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/TER.h>
@@ -83,6 +82,11 @@ protected:
std::optional<AMMLiquidity<TIn, TOut>> ammLiquidity_;
beast::Journal const j_;
Asset const strandDeliver_;
// MaximumAmount of book_.in if it is an MPT. Taking an MPT offer has the
// issuer send TakerPays to the owner, and a send above the cap is rejected
// (isMPTOverflow). forEachOffer limits the offer's input to the cap so it
// fills partially instead of failing the strand.
std::optional<TIn> maxIn_;
struct Cache
{
@@ -106,6 +110,12 @@ private:
, j_(ctx.j)
, strandDeliver_(ctx.strandDeliver)
{
if constexpr (std::is_same_v<TIn, MPTAmount>)
{
if (auto const max = maxMPTAmount(ctx.view, in.get<MPTIssue>().getMptID()))
maxIn_.emplace(*max);
}
if (auto const ammSle = ctx.view.read(keylet::amm(in, out));
ammSle && ammSle->getFieldAmount(sfLPTokenBalance) != beast::kZero)
{
@@ -344,8 +354,7 @@ public:
Quality const& ofrQ,
DebtDirection prevStepDir,
WaiveTransferFee waiveFee,
OfferType,
Rules const&) const
OfferType) const
{
// Charge the offer owner, not the sender
// Charge a fee even if the owner is the same as the issuer
@@ -516,8 +525,7 @@ public:
Quality const& ofrQ,
DebtDirection prevStepDir,
WaiveTransferFee waiveFee,
OfferType offerType,
Rules const& rules) const
OfferType offerType) const
{
// Offer x-ing does not charge a transfer fee when the offer's owner
// is the same as the strand dst. It is important that
@@ -529,10 +537,6 @@ public:
// Single path AMM offer has to factor in the transfer in rate
// when calculating the upper bound quality and the quality function
// because single path AMM's offer quality is not constant.
if (!rules.enabled(fixAMMv1_1))
{
return ofrQ;
}
if (offerType == OfferType::Clob ||
(this->ammLiquidity_ && this->ammLiquidity_->multiPath()))
{
@@ -584,7 +588,7 @@ BookStep<TIn, TOut, TDerived>::qualityUpperBound(ReadView const& v, DebtDirectio
: WaiveTransferFee::No;
Quality const q = static_cast<TDerived const*>(this)->adjustQualityWithFees(
v, std::get<Quality>(*res), prevStepDir, waiveFee, std::get<OfferType>(*res), v.rules());
v, std::get<Quality>(*res), prevStepDir, waiveFee, std::get<OfferType>(*res));
return {q, dir};
}
@@ -603,7 +607,7 @@ BookStep<TIn, TOut, TDerived>::getQualityFunc(ReadView const& v, DebtDirection p
{
auto static const kQOne = Quality{STAmount::kURateOne};
auto const q = static_cast<TDerived const*>(this)->adjustQualityWithFees(
v, kQOne, prevStepDir, WaiveTransferFee::Yes, OfferType::Amm, v.rules());
v, kQOne, prevStepDir, WaiveTransferFee::Yes, OfferType::Amm);
if (q == kQOne)
return {res, dir};
QualityFunction qf{q, QualityFunction::CLOBLikeTag{}};
@@ -618,8 +622,7 @@ BookStep<TIn, TOut, TDerived>::getQualityFunc(ReadView const& v, DebtDirection p
// always has quality set
prevStepDir,
WaiveTransferFee::No,
OfferType::Clob,
v.rules());
OfferType::Clob);
return {QualityFunction{q, QualityFunction::CLOBLikeTag{}}, dir};
}
@@ -630,6 +633,38 @@ BookStep<TIn, TOut, TDerived>::offersUsed() const
return offersUsed_;
}
// Limit the offer to the given input (net of the taker's transfer fee), and
// the step output and owner's charge with it. The caller sets stpAmt.in.
template <class TIn, class TOut, class Offer>
static void
limitOfferIn(
Offer const& offer,
TAmounts<TIn, TOut>& ofrAmt,
TAmounts<TIn, TOut>& stpAmt,
TOut& ownerGives,
std::uint32_t transferRateOut,
TIn const& inLmt)
{
// It turns out we can prevent order book blocking by (strictly)
// rounding down the ceil_in() result. By rounding down we guarantee
// that the quality of an offer left in the ledger is as good or
// better than the quality of the containing order book page.
//
// This adjustment changes transaction outcomes, so it must be made
// under an amendment.
ofrAmt = offer.limitIn(ofrAmt, inLmt, /* roundUp */ false);
stpAmt.out = ofrAmt.out;
// Round up for MPT output so the offer owner pays the full
// ceil(amount × rate) fee, matching direct Payment semantics. IOU uses
// floating-point arithmetic so the floor/ceil distinction is sub-epsilon
// there; preserve the historical false to avoid changing IOU behavior.
ownerGives = mulRatio(
ofrAmt.out,
transferRateOut,
QUALITY_ONE,
/*roundUp*/ std::is_same_v<TOut, MPTAmount>);
}
// Adjust the offer amount and step amount subject to the given input limit
template <class TIn, class TOut, class Offer>
static void
@@ -646,24 +681,7 @@ limitStepIn(
{
stpAmt.in = limit;
auto const inLmt = mulRatio(stpAmt.in, QUALITY_ONE, transferRateIn, /*roundUp*/ false);
// It turns out we can prevent order book blocking by (strictly)
// rounding down the ceil_in() result. By rounding down we guarantee
// that the quality of an offer left in the ledger is as good or
// better than the quality of the containing order book page.
//
// This adjustment changes transaction outcomes, so it must be made
// under an amendment.
ofrAmt = offer.limitIn(ofrAmt, inLmt, /* roundUp */ false);
stpAmt.out = ofrAmt.out;
// Round up for MPT output so the offer owner pays the full
// ceil(amount × rate) fee, matching direct Payment semantics. IOU uses
// floating-point arithmetic so the floor/ceil distinction is sub-epsilon
// there; preserve the historical false to avoid changing IOU behavior.
ownerGives = mulRatio(
ofrAmt.out,
transferRateOut,
QUALITY_ONE,
/*roundUp*/ std::is_same_v<TOut, MPTAmount>);
limitOfferIn(offer, ofrAmt, stpAmt, ownerGives, transferRateOut, inLmt);
}
}
@@ -785,29 +803,45 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
auto ofrAmt = offer.amount();
TAmounts stpAmt{ofrAmt.in, ofrAmt.out};
auto ownerGives = ofrAmt.out;
// A bid above MaximumAmount is limited to it below (see maxIn_). Gross
// up no more than the cap: the whole bid may not fit once grossed up
// even though a fill at the cap does.
bool const capInMPT = prevStep_ && maxIn_ && offer.owner() != assetIn.getIssuer();
auto const amtIn = [&](TIn const& in) { return capInMPT && *maxIn_ < in ? *maxIn_ : in; };
try
{
// All arithmetic in this block runs before the offer is consumed.
// A crafted MPTokensV2 offer can overflow while transfer rates or
// crossing limits are applied; remove that unusable offer instead
// of letting it persist as a tecINTERNAL source.
stpAmt.in = mulRatio(ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true);
stpAmt.in = mulRatio(amtIn(ofrAmt.in), ofrInRate, QUALITY_ONE, /*roundUp*/ true);
// owner pays the transfer fee.
ownerGives = mulRatio(
ofrAmt.out,
ofrOutRate,
QUALITY_ONE,
/*roundUp*/ std::is_same_v<TOut, MPTAmount>);
// owner pays the transfer fee. An MPT gross-up that doesn't fit in
// an MPT amount exceeds any owner's funds (an MPT issuer's funds
// are what it can still issue), so a large, partially funded offer
// is limited to its funds below instead of removed as an overflow.
std::optional<TOut> grossOut;
if constexpr (std::is_same_v<TOut, MPTAmount>)
{
grossOut = tryMulRatio(ofrAmt.out, ofrOutRate, QUALITY_ONE, /*roundUp*/ true);
}
else
{
grossOut = mulRatio(ofrAmt.out, ofrOutRate, QUALITY_ONE, /*roundUp*/ false);
}
if (grossOut)
ownerGives = *grossOut;
auto const funds = offer.isFunded()
? ownerGives // Offer owner is issuer; they have unlimited funds
? ownerGives // Offer owner is IOU issuer; they have unlimited funds
: offers.ownerFunds();
// Only if CLOB offer
if (funds < ownerGives)
// Only a CLOB offer: an AMM offer is funded and its output rate is
// parity, so its gross-up always fits.
if (!grossOut || funds < ownerGives)
{
// We already know offer.owner()!=offer.issueOut().account
// The owner isn't an IOU issuer (those are funded). An MPT
// issuer can be here: its funds are what it can still issue.
ownerGives = funds;
stpAmt.out = mulRatio(ownerGives, QUALITY_ONE, ofrOutRate, /*roundUp*/ false);
@@ -816,32 +850,85 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
// transaction outcomes, so it must be made under an amendment.
ofrAmt = offer.limitOut(ofrAmt, stpAmt.out, /*roundUp*/ false);
stpAmt.in = mulRatio(ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true);
}
stpAmt.in = mulRatio(amtIn(ofrAmt.in), ofrInRate, QUALITY_ONE, /*roundUp*/ true);
// Limit offer's input if MPT, BookStep is the first step (an issuer
// is making a cross-currency payment), and this offer is not owned
// by the issuer. Otherwise, OutstandingAmount may overflow.
auto const& issuer = assetIn.getIssuer();
if (isAssetInMPT && !prevStep_ && offer.owner() != issuer)
{
// Funds available to issue
auto const available = toAmount<TIn>(accountFunds(
sb,
issuer,
assetIn, // STAmount{0}, but the default is not used
FreezeHandling::IgnoreFreeze,
AuthHandling::IgnoreAuth,
j_));
if (stpAmt.in > available)
// Fee clip can floor in or out to 0. OfferStream is
// fee-blind, so it misses both: out==0 blocks the book;
// in==0 / out>0 consumes ownerGives for free. MPT at
// ordinary sizes; IOU only at the smallest out — V2
// because that IOU outcome changes.
if (sb.rules().enabled(featureMPTokensV2) &&
(stpAmt.out <= beast::kZero || ofrAmt.in <= beast::kZero))
{
limitStepIn(
offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate, available);
removeOffer(
"Removing offer whose fee-clipped amount rounds to "
"zero");
return true;
}
// out = floor(funds / rate), so out × rate <= funds and
// ceil(out × rate) <= funds since funds is an integer. Charge
// that instead of funds; charging all of funds would burn the
// fraction that can't be delivered. IOU keeps funds; the
// difference is sub-epsilon there.
if constexpr (std::is_same_v<TOut, MPTAmount>)
{
ownerGives = mulRatio(stpAmt.out, ofrOutRate, QUALITY_ONE, /*roundUp*/ true);
}
}
// Limit offer's input if MPT and the offer is not owned by the
// issuer (an issuer's send to itself is a no-op).
bool inLimited = false;
std::optional<TIn> limitIn;
auto const& issuer = assetIn.getIssuer();
if (isAssetInMPT && offer.owner() != issuer)
{
if (!prevStep_)
{
// BookStep is the first step (an issuer is making a
// cross-currency payment). Limit to the funds available to
// issue, otherwise OutstandingAmount may overflow. This
// limits the taker, not the offer: its remainder is still
// funded by the owner, so the callback keeps it. With
// nothing left to issue there is nothing to consume, so
// stop before touching the offer.
auto const available = toAmount<TIn>(accountFunds(
sb,
issuer,
assetIn, // STAmount{0}, but the default is not used
FreezeHandling::IgnoreFreeze,
AuthHandling::IgnoreAuth,
j_));
if (available <= beast::kZero)
return false;
if (stpAmt.in > available)
{
inLimited = true;
limitIn = available;
}
}
else if (maxIn_ && ofrAmt.in > *maxIn_)
{
// Limit to MaximumAmount (see maxIn_). The remainder is
// still funded, so the callback keeps the offer instead of
// consuming it. stpAmt.in is already the grossed-up cap.
inLimited = true;
limitOfferIn(offer, ofrAmt, stpAmt, ownerGives, ofrOutRate, *maxIn_);
}
}
if (limitIn)
limitStepIn(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate, *limitIn);
// amtIn() grossed up no more than the cap, so the offer must have
// been limited to it above or the taker pays for less than is
// consumed.
XRPL_ASSERT(
!capInMPT || ofrAmt.in <= *maxIn_,
"xrpl::BookStep::forEachOffer : capped offer input is limited");
offerAttempted = true;
return callback(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate);
return callback(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate, inLimited);
}
catch (std::overflow_error const&)
{
@@ -876,7 +963,7 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
// If offer crossing then use either LOB quality or nullopt
// to prevent AMM being blocked by a lower quality LOB.
auto const qualityThreshold = [&] -> std::optional<Quality> {
if (sb.rules().enabled(fixAMMv1_1) && lobQuality)
if (lobQuality)
return static_cast<TDerived const*>(this)->qualityThreshold(*lobQuality);
return lobQuality;
}();
@@ -1006,7 +1093,7 @@ BookStep<TIn, TOut, TDerived>::tip(ReadView const& view) const
// as the result a LOB offer is partially crossed, and it might take a few
// iterations to fully cross the offer.
auto const qualityThreshold = [&] -> std::optional<Quality> {
if (view.rules().enabled(fixAMMv1_1) && lobQuality)
if (lobQuality)
return static_cast<TDerived const*>(this)->qualityThreshold(*lobQuality);
return std::nullopt;
}();
@@ -1094,7 +1181,8 @@ BookStep<TIn, TOut, TDerived>::revImp(
TAmounts<TIn, TOut> const& stpAmt,
TOut const& ownerGives,
std::uint32_t transferRateIn,
std::uint32_t transferRateOut) mutable -> bool {
std::uint32_t transferRateOut,
bool inLimited) mutable -> bool {
if (remainingOut <= beast::kZero)
return false;
@@ -1106,8 +1194,11 @@ BookStep<TIn, TOut, TDerived>::revImp(
remainingOut = out - result.out;
this->consumeOffer(sb, offer, ofrAmt, stpAmt, ownerGives);
// return true b/c even if the payment is satisfied,
// we need to consume the offer
return true;
// we need to consume the offer. An offer limited by what the
// issuer can issue is not consumed, though: its remainder is still
// funded, and no more of that MPT can flow through this step
// anyway.
return !inLimited;
}
auto ofrAdjAmt = ofrAmt;
@@ -1210,7 +1301,8 @@ BookStep<TIn, TOut, TDerived>::fwdImp(
TAmounts<TIn, TOut> const& stpAmt,
TOut const& ownerGives,
std::uint32_t transferRateIn,
std::uint32_t transferRateOut) mutable -> bool {
std::uint32_t transferRateOut,
bool inLimited) mutable -> bool {
XRPL_ASSERT(cache_, "xrpl::BookStep::fwdImp::eachOffer : cache is set");
if (remainingIn <= beast::kZero)
@@ -1240,8 +1332,10 @@ BookStep<TIn, TOut, TDerived>::fwdImp(
savedInsAdj.insert(stpAmt.in);
lastOut = savedOutsAdj.insert(stpAmt.out);
resultAdj = TAmounts<TIn, TOut>(sum(savedInsAdj), sum(savedOutsAdj));
// consume the offer even if stepAmt.in == remainingIn
processMore = true;
// consume the offer even if stepAmt.in == remainingIn, unless it
// was limited by what the issuer can issue: its remainder is
// still funded and must stay on the book.
processMore = !inLimited;
}
else
{
+4 -4
View File
@@ -356,10 +356,10 @@ TOfferStreamBase<TIn, TOut>::step()
offer_ = TOffer<TIn, TOut>{};
continue;
}
// The strict reduction only overflows for a crafted MPT offer, and
// MPT offers require featureMPTokensV2 (enforced at OfferCreate
// preflight). So the amendment is always enabled here and this
// legacy re-throw is unreachable in practice.
// The strict reduction only throws std::overflow_error for a
// crafted MPT offer, and MPT offers require featureMPTokensV2
// (enforced at OfferCreate preflight). So the amendment is always
// enabled here and this legacy re-throw is unreachable in practice.
// LCOV_EXCL_START
XRPL_ASSERT(
view_.rules().enabled(featureMPTokensV2),
+13
View File
@@ -0,0 +1,13 @@
# 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
@@ -0,0 +1 @@
AGENTS.md
@@ -354,11 +354,8 @@ AMMWithdraw::applyGuts(Sandbox& sb)
// Due to rounding, the LPTokenBalance of the last LP
// might not match the LP's trustline balance
if (sb.rules().enabled(fixAMMv1_1))
{
if (auto const res = verifyAndAdjustLPTokenBalance(sb, lpTokens, ammSle, accountID_); !res)
return {res.error(), false};
}
if (auto const res = verifyAndAdjustLPTokenBalance(sb, lpTokens, ammSle, accountID_); !res)
return {res.error(), false}; // LCOV_EXCL_LINE
auto const tfee = getTradingFee(ctx_.view(), *ammSle, accountID_);
@@ -591,7 +588,7 @@ AMMWithdraw::withdraw(
// Should not happen since the only LP on last withdraw
// has the balance set to the lp token trustline balance.
if (view.rules().enabled(fixAMMv1_1) && lpTokensWithdrawActual > lpTokensAMMBalance)
if (lpTokensWithdrawActual > lpTokensAMMBalance)
{
// LCOV_EXCL_START
JLOG(journal.debug()) << "AMM Withdraw: failed to withdraw, unexpected LP tokens: "
+57 -10
View File
@@ -24,6 +24,7 @@
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTAmount.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Quality.h>
@@ -50,6 +51,7 @@
#include <functional>
#include <memory>
#include <optional>
#include <stdexcept>
#include <tuple>
#include <utility>
@@ -413,19 +415,47 @@ OfferCreate::flowCross(
// offer taker. Set sendMax to allow for the gateway's cut.
Rate gatewayXferRate{QUALITY_ONE};
STAmount sendMax = takerAmount.in;
// Payment flow code compares quality after the transfer rate is
// included, so the threshold is computed from the grossed-up input.
STAmount thresholdIn = sendMax;
if (!sendMax.native() && (accountID_ != sendMax.getIssuer()))
{
gatewayXferRate = transferRate(psb, sendMax);
if (gatewayXferRate.value != QUALITY_ONE)
{
sendMax =
multiplyRound(takerAmount.in, gatewayXferRate, takerAmount.in.asset(), true);
try
{
sendMax = multiplyRound(
takerAmount.in, gatewayXferRate, takerAmount.in.asset(), true);
thresholdIn = sendMax;
}
catch (std::overflow_error const&)
{
// Only an MPT amount can overflow here (IOU rescales and
// XRP has no transfer rate), and MPT offers require
// featureMPTokensV2 at preflight, so this legacy re-throw
// is unreachable in practice.
// LCOV_EXCL_START
if (!takerAmount.in.holds<MPTIssue>() ||
!psb.rules().enabled(featureMPTokensV2))
throw;
// LCOV_EXCL_STOP
// The grossed-up MPT amount exceeds 2^63-1. The taker can't
// send more than its balance anyway. The threshold only
// needs the ratio, so compute it as an IOU, which can't
// overflow.
sendMax = inStartBalance;
thresholdIn = multiplyRound(
STAmount{noIssue(), takerAmount.in.mantissa()},
gatewayXferRate,
noIssue(),
true);
}
}
}
// Payment flow code compares quality after the transfer rate is
// included. Since transfer rate is incorporated compute threshold.
Quality threshold{takerAmount.out, sendMax};
Quality threshold{takerAmount.out, thresholdIn};
// If we're creating a passive offer adjust the threshold so we only
// cross offers that have a better quality than this one.
@@ -870,15 +900,32 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel)
return {tesSUCCESS, true};
}
// A taker redeeming an MPT into this offer pays the transfer fee, and
// BookStep grosses up TakerPays by the rate before anything else. If that
// doesn't fit in an MPT amount, every fee-paying taker overflows and
// BookStep removes the offer on the first attempt (the issuer pays no fee,
// but its send is bounded by what it can still issue). Conservative for a
// bid above MaximumAmount, which BookStep grosses up only to the cap.
// Checked on the remainder: it may fit even if the original amount didn't.
bool cannotRest = false;
if (mptV2 && saTakerPays.holds<MPTIssue>())
{
auto const rate = transferRate(sb, saTakerPays);
cannotRest = rate != kParityRate &&
!tryMulRatio(saTakerPays.mpt(), rate.value, QUALITY_ONE, /*roundUp*/ true);
}
// The remainder rests at uRate, the original pre-crossing rate. A zero
// rate (quality not representable) puts it in the directory whose index
// equals getBookBase(book), and BookTip scans keys strictly greater, so it
// could never be crossed while holding the owner's reserve. Don't place
// it; anything that crossed is kept, and a fully crossed offer has already
// returned above. Gated to preserve pre-amendment behavior.
if (mptV2 && uRate == 0)
// could never be crossed while holding the owner's reserve. Likewise, a
// bid whose fee-grossed TakerPays doesn't fit would be removed by BookStep
// on the first take. Don't place it; anything that crossed is kept, and a
// fully crossed offer has already returned above. Gated to preserve
// pre-amendment behavior.
if ((mptV2 && uRate == 0) || cannotRest)
{
JLOG(j_.debug()) << "Unrepresentable quality: remainder not placed";
JLOG(j_.debug()) << "Unrepresentable quality or fee: remainder not placed";
if (!crossed)
return {tecKILLED, false};
return {tesSUCCESS, true};
@@ -8,6 +8,7 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/LendingHelpers.h>
#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/Concepts.h>
@@ -158,7 +159,7 @@ determineAsset(
std::expected<STAmount, TER>
determineClawAmount(
SLE const& sleBroker,
SLE::ConstRef sleBroker,
Asset const& vaultAsset,
std::optional<STAmount> const& amount,
SLE::ConstRef vaultSle,
@@ -169,17 +170,19 @@ determineClawAmount(
if (rules.enabled(fixCleanup3_2_0))
{
return minimumBrokerCover(
sleBroker[sfDebtTotal], TenthBips32(sleBroker[sfCoverRateMinimum]), vaultSle);
sleBroker->at(sfDebtTotal),
TenthBips32(sleBroker->at(sfCoverRateMinimum)),
vaultSle);
}
// Always round the minimum required up
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
return tenthBipsOfValue(
sleBroker[sfDebtTotal], TenthBips32(sleBroker[sfCoverRateMinimum]));
sleBroker->at(sfDebtTotal), TenthBips32(sleBroker->at(sfCoverRateMinimum)));
}();
// The subtraction probably won't round, but round down if it does.
NumberRoundModeGuard const mg(Number::RoundingMode::Downward);
return sleBroker[sfCoverAvailable] - minRequiredCover;
return sleBroker->at(sfCoverAvailable) - minRequiredCover;
}();
if (maxClawAmount <= beast::kZero)
return std::unexpected(tecINSUFFICIENT_FUNDS);
@@ -187,12 +190,23 @@ determineClawAmount(
// Use the vaultAsset here, because it will be the right type in all
// circumstances. The amount may be an IOU indicating the pseudo-account's
// asset, which is correct, but not what is needed here.
if (!amount || *amount == beast::kZero)
return STAmount{vaultAsset, maxClawAmount};
Number const magnitude{*amount};
if (magnitude > maxClawAmount)
return STAmount{vaultAsset, maxClawAmount};
return STAmount{vaultAsset, magnitude};
STAmount requested = [&] {
if (!amount || *amount == beast::kZero)
return STAmount{vaultAsset, maxClawAmount};
Number const magnitude{*amount};
if (magnitude > maxClawAmount)
return STAmount{vaultAsset, maxClawAmount};
return STAmount{vaultAsset, magnitude};
}();
if (getVaultVersion(vaultSle) != VaultVersion::FixedPrecision)
return requested;
STAmount rounded = debitToPosteriorBrokerCoverScale(
vaultSle, sleBroker, requested, Number::RoundingMode::TowardsZero);
if (rounded == beast::kZero)
return std::unexpected(tecPRECISION_LOSS);
return rounded;
}
template <ValidIssueType T>
@@ -294,17 +308,25 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx)
}
auto const findClawAmount =
determineClawAmount(*sleBroker, vaultAsset, amount, vault, ctx.view.rules());
determineClawAmount(sleBroker, vaultAsset, amount, vault, ctx.view.rules());
if (!findClawAmount)
{
JLOG(ctx.j.warn()) << "LoanBroker cover is already at minimum.";
JLOG(ctx.j.warn()) << "LoanBroker cover is already at minimum, or the claw amount "
"rounds to zero: "
<< transToken(findClawAmount.error());
return findClawAmount.error();
}
STAmount const& clawAmount = *findClawAmount;
if (auto const ret = canApplyToBrokerCover(
ctx.view, sleBroker, vaultAsset, clawAmount, ctx.j, "LoanBrokerCoverClawback"))
return ret;
// FixedPrecision outflows already rounded at the posterior exponent; the
// live CoverAvailable scale used by canApplyToBrokerCover would reject a
// re-fining clawback as sub-ULP.
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
{
if (auto const ret = canApplyToBrokerCover(
ctx.view, sleBroker, vaultAsset, clawAmount, ctx.j, "LoanBrokerCoverClawback"))
return ret;
}
// Explicitly check the balance of the trust line / MPT to make sure the
// balance is actually there. It should always match `sfCoverAvailable`, so
@@ -357,7 +379,7 @@ LoanBrokerCoverClawback::doApply()
auto const vaultAsset = vault->at(sfAsset);
auto const findClawAmount =
determineClawAmount(*sleBroker, vaultAsset, amount, vault, view().rules());
determineClawAmount(sleBroker, vaultAsset, amount, vault, view().rules());
if (!findClawAmount)
return tecINTERNAL; // LCOV_EXCL_LINE
STAmount const& clawAmount = *findClawAmount;
@@ -5,8 +5,10 @@
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/helpers/LendingHelpers.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/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
@@ -104,22 +106,35 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx)
// here in preclaim lets us reject sub-cover-scale dust early with tecPRECISION_LOSS instead of
// failing only in doApply.
auto const roundedAmount = [&] -> STAmount {
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
{
// Cover deposit is an inflow to CoverAvailable, so it must floor
// the sum, not just the delta.
return creditToPosteriorBrokerCoverScale(
vault, sleBroker, amount, Number::RoundingMode::Downward);
}
if (!fix320Enabled)
return tx[sfAmount];
return STAmount{amount};
return roundToScale(
tx[sfAmount],
amount,
scale(sleBroker->at(sfCoverAvailable), vaultAsset),
Number::RoundingMode::Downward);
}();
if (fix320Enabled && roundedAmount == beast::kZero)
// fixCleanup3_2_0 is enabled on the network, so this also covers the
// FixedPrecision zero-credit case.
if (fix320Enabled && roundedAmount <= beast::kZero)
{
JLOG(ctx.j.warn()) << "LoanBrokerCoverDeposit: deposit amount: " << tx[sfAmount]
<< " is zero at loan broker scale";
JLOG(ctx.j.warn()) << "LoanBrokerCoverDeposit: deposit amount: " << amount
<< " is zero or negative at loan broker scale";
return tecPRECISION_LOSS;
}
if (auto const ter = checkOptionalBrokerCoverInflow(vault, sleBroker, roundedAmount);
!isTesSuccess(ter))
return ter;
if (accountHolds(
ctx.view,
account,
@@ -155,6 +170,13 @@ LoanBrokerCoverDeposit::doApply()
// see the rationale comment in preclaim.
bool const fix320Enabled = view().rules().enabled(fixCleanup3_2_0);
auto const amount = [&] -> STAmount {
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
{
// Cover deposit is an inflow to CoverAvailable, so it must floor
// the sum, not just the delta.
return creditToPosteriorBrokerCoverScale(
vault, broker, tx[sfAmount], Number::RoundingMode::Downward);
}
if (!fix320Enabled)
return tx[sfAmount];
@@ -164,12 +186,12 @@ LoanBrokerCoverDeposit::doApply()
Number::RoundingMode::Downward);
}();
// We validated zero-amount in preclaim, if we ended up with zero now, fail hard.
if (amount == beast::kZero)
// We validated zero-or-negative amount in preclaim, if we ended up with one now, fail hard.
if (amount <= beast::kZero)
{
// LCOV_EXCL_START
JLOG(j_.error()) << "LoanBrokerCoverDeposit: deposit amount: " << tx[sfAmount]
<< " is zero";
<< " is zero or negative";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
@@ -8,6 +8,7 @@
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/LendingHelpers.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/Protocol.h>
@@ -103,10 +104,28 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
if (amount.asset() != vaultAsset)
return tecWRONG_ASSET;
// Helper handles both IOU and MPT correctly without explicit branching.
if (auto const ret = canApplyToBrokerCover(
ctx.view, sleBroker, vaultAsset, amount, ctx.j, "LoanBrokerCoverWithdraw"))
return ret;
STAmount roundedAmount{amount};
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
{
// FixedPrecision outflows are rounded at the posterior exponent here
// instead of going through canApplyToBrokerCover: the live
// CoverAvailable scale it uses would reject a re-fining withdrawal as
// sub-ULP.
roundedAmount = debitToPosteriorBrokerCoverScale(
vault, sleBroker, amount, Number::RoundingMode::TowardsZero);
if (roundedAmount == beast::kZero)
{
JLOG(ctx.j.warn()) << "LoanBrokerCoverWithdraw: withdraw amount: " << amount
<< " is zero at loan broker scale";
return tecPRECISION_LOSS;
}
}
else
{
if (auto const ret = canApplyToBrokerCover(
ctx.view, sleBroker, vaultAsset, roundedAmount, ctx.j, "LoanBrokerCoverWithdraw"))
return ret;
}
// The broker's pseudo-account is the source of funds.
auto const pseudoAccountID = sleBroker->at(sfAccount);
@@ -129,7 +148,15 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
AuthType authType = AuthType::WeakAuth;
if (account != dstAcct)
{
if (auto const ret = canWithdraw(ctx.view, tx))
// Check the amount actually sent: on FixedPrecision it is the rounded
// amount, not sfAmount.
if (auto const ret = canWithdraw(
ctx.view,
account,
dstAcct,
roundedAmount,
tx.isFieldPresent(sfDestinationTag),
tx[~sfCredentialIDs]))
return ret;
// The destination account must have consented to receive the asset by
@@ -184,9 +211,9 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
tenthBipsOfValue(currentDebtTotal, TenthBips32(sleBroker->at(sfCoverRateMinimum))),
scale(currentDebtTotal, vaultAsset));
}();
if (coverAvail < amount)
if (coverAvail < roundedAmount)
return tecINSUFFICIENT_FUNDS;
if ((coverAvail - amount) < minimumCover)
if ((coverAvail - roundedAmount) < minimumCover)
return tecINSUFFICIENT_FUNDS;
auto const freezeHandling = fix330Enabled && dstAcct == vaultAsset.getIssuer()
@@ -199,7 +226,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
vaultAsset,
freezeHandling,
AuthHandling::ZeroIfUnauthorized,
ctx.j) < amount)
ctx.j) < roundedAmount)
return tecINSUFFICIENT_FUNDS;
return tesSUCCESS;
@@ -211,7 +238,7 @@ LoanBrokerCoverWithdraw::doApply()
auto const& tx = ctx_.tx;
auto const brokerID = tx[sfLoanBrokerID];
auto const amount = tx[sfAmount];
auto const requestedAmount = tx[sfAmount];
auto const dstAcct = tx[~sfDestination].value_or(accountID_);
auto const broker = view().peek(keylet::loanBroker(brokerID));
@@ -223,6 +250,10 @@ LoanBrokerCoverWithdraw::doApply()
return tecINTERNAL; // LCOV_EXCL_LINE
auto const vaultAsset = vault->at(sfAsset);
auto const amount = getVaultVersion(vault) == VaultVersion::FixedPrecision
? debitToPosteriorBrokerCoverScale(
vault, broker, requestedAmount, Number::RoundingMode::TowardsZero)
: requestedAmount;
auto const brokerPseudoID = *broker->at(sfAccount);
@@ -239,6 +270,7 @@ LoanBrokerCoverWithdraw::doApply()
brokerPseudoID,
preFeeBalance_,
amount,
amount,
j_);
}
@@ -6,6 +6,7 @@
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/LendingHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -77,7 +78,7 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
{
// Any remaining debt should have been wiped out by the last Loan
// Delete. This check is purely defensive.
auto const scale = getAssetsTotalScale(vault);
auto const scale = getVaultScale(vault);
auto const rounded =
roundToAsset(asset, debtTotal, scale, Number::RoundingMode::TowardsZero);
@@ -94,21 +95,38 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
auto const coverAvailable = STAmount{asset, sleBroker->at(sfCoverAvailable)};
// If there are assets in the cover, broker will receive them on deletion.
// So we need to check if the broker owner is deep frozen for that asset.
if (coverAvailable > beast::kZero)
{
auto const brokerPseudo = sleBroker->at(sfAccount);
// Pre-fixCleanup3_5_0: only freeze checks apply to the cover payout.
// Post-fixCleanup3_5_0: apply the cover-withdraw transfer and authorization checks too.
if (ctx.view.rules().enabled(fixCleanup3_5_0))
{
// Like a cover withdrawal, the payout skips the lsfMPTCanTransfer check so that an
// issuer cannot trap the broker's cover.
if (auto const ret = canTransfer(
ctx.view, asset, brokerPseudo, brokerOwner, WaiveMPTCanTransfer::Yes))
return ret;
if (auto const ret = requireAuth(ctx.view, asset, brokerOwner, AuthType::WeakAuth))
return ret;
if (!holdingExists(ctx.view, brokerOwner, asset))
{
if (auto const ret = canAddHolding(ctx.view, asset); !isTesSuccess(ret))
return ret;
}
}
if (auto const ret = checkDeepFrozen(ctx.view, brokerOwner, asset))
{
JLOG(ctx.j.warn()) << "Broker owner account is frozen.";
return ret;
}
}
if (ctx.view.rules().enabled(fixCleanup3_2_0))
{
if (coverAvailable > beast::kZero)
if (ctx.view.rules().enabled(fixCleanup3_2_0))
{
auto const brokerPseudo = sleBroker->at(sfAccount);
if (auto const ret = checkFrozen(ctx.view, brokerPseudo, asset))
{
JLOG(ctx.j.warn()) << "Broker pseudo-account is frozen/locked.";
@@ -151,8 +169,36 @@ LoanBrokerDelete::doApply()
return tefBAD_LEDGER; // LCOV_EXCL_LINE
}
auto const owner = view().peek(keylet::account(accountID_));
if (!owner)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
// Pre-fixCleanup3_5_0: the owner count drops after the pseudo-account is erased.
// Post-fixCleanup3_5_0: it drops first, so the owner's holding created below can use the
// reserve that the broker frees.
bool const fix350Enabled = view().rules().enabled(fixCleanup3_5_0);
if (fix350Enabled)
{
// Decreases the owner count by two: one for the LoanBroker object, and one
// for the pseudo-account.
decreaseOwnerCountForObject(view(), owner, broker, 2, j_);
}
{
auto const coverAvailable = STAmount{vaultAsset, broker->at(sfCoverAvailable)};
// Pre-fixCleanup3_5_0: the cover goes straight to accountSend, so an MPT payout to an
// owner with no MPToken fails with tecNO_AUTH.
// Post-fixCleanup3_5_0: a nonzero cover first gets the owner a holding if it has none, as a
// withdrawal to self does.
if (fix350Enabled && coverAvailable > beast::kZero &&
!holdingExists(view(), accountID_, vaultAsset))
{
if (auto const ter = addEmptyHolding(
ctx_.getApplyViewContext(), accountID_, preFeeBalance_, vaultAsset, j_))
return ter;
}
if (auto const ter = accountSend(
view(), brokerPseudoID, accountID_, coverAvailable, j_, {}, WaiveTransferFee::Yes))
return ter;
@@ -185,15 +231,8 @@ LoanBrokerDelete::doApply()
view().erase(brokerPseudoSLE);
{
auto const owner = view().peek(keylet::account(accountID_));
if (!owner)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
// Decreases the owner count by two: one for the LoanBroker object, and
// one for the pseudo-account.
if (!fix350Enabled)
decreaseOwnerCountForObject(view(), owner, broker, 2, j_);
}
view().erase(broker);
@@ -16,6 +16,7 @@
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/STTakesAsset.h>
@@ -223,10 +224,17 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx)
}
// Check that relevant values can be represented as the vault asset
// type. This is mostly only relevant for integral (non-IOU) types
// type. This is mostly only relevant for integral (non-IOU) types.
// fixCleanup3_5_0 also rejects out-of-range values, e.g. XRP above
// STAmount::kMaxNativeN, which would otherwise throw.
bool const fix350Enabled = ctx.view.rules().enabled(fixCleanup3_5_0);
for (auto const& field : getValueFields())
{
if (auto const value = tx[field]; value && STAmount{asset, *value} != *value)
if (auto const value = tx[field]; value &&
((fix350Enabled && !isRepresentable(asset, *value)) ||
STAmount{asset, *value} != *value ||
(getVaultVersion(sleVault) == VaultVersion::FixedPrecision &&
!isOnVaultBaseGrid(asset, *value, getVaultBaseScale(sleVault)))))
{
JLOG(ctx.j.warn()) << field.f->getName() << " (" << *value
<< ") can not be represented as a(n) " << to_string(asset) << ".";
@@ -6,6 +6,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/LendingHelpers.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h> // IWYU pragma: keep
@@ -120,7 +121,7 @@ LoanDelete::doApply()
roundToAsset(
vaultSle->at(sfAsset),
debtTotalProxy,
getAssetsTotalScale(vaultSle),
getVaultScale(vaultSle),
Number::RoundingMode::TowardsZero) == beast::kZero,
"xrpl::LoanDelete::doApply",
"last loan, remaining debt rounds to zero");
@@ -8,6 +8,7 @@
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/LendingHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -128,6 +129,13 @@ LoanManage::preclaim(PreclaimContext const& ctx)
return tecNO_PERMISSION;
}
if (auto const vault = ctx.view.read(keylet::vault(loanBrokerSle->at(sfVaultID)));
vault && getVaultVersion(vault) == VaultVersion::FixedPrecision)
{
// FixedPrecision Vaults are not yet supported by LoanManage.
return tecNO_PERMISSION;
}
return tesSUCCESS;
}
@@ -9,6 +9,7 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/LendingHelpers.h>
#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>
@@ -282,6 +283,11 @@ LoanPay::preclaim(PreclaimContext const& ctx)
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
if (getVaultVersion(vaultSle) == VaultVersion::FixedPrecision)
{
// FixedPrecision Vaults are not yet supported by LoanPay.
return tecNO_PERMISSION;
}
auto const asset = vaultSle->at(sfAsset);
auto const vaultPseudoAccount = vaultSle->at(sfAccount);
+59 -10
View File
@@ -20,6 +20,7 @@
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/STObject.h>
@@ -321,6 +322,8 @@ LoanSet::preclaim(PreclaimContext const& ctx)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
}
auto const vaultVersion = getVaultVersion(vault);
if (ctx.view.rules().enabled(featureLendingProtocolV1_1))
{
auto const phase = getVaultPhase(ctx.view, vault);
@@ -352,13 +355,24 @@ LoanSet::preclaim(PreclaimContext const& ctx)
// already at AssetsMaximum cannot take another loan. Cash-basis origination
// does not change AssetsTotal (see cash_basis::loanOriginationDeltas), so
// this leftover instant-recognition gate must not apply there.
if (getVaultVersion(vault) != VaultVersion::CashBasis && vault->at(sfAssetsMaximum) != 0 &&
if (vaultVersion < VaultVersion::CashBasis && vault->at(sfAssetsMaximum) != 0 &&
vault->at(sfAssetsTotal) >= vault->at(sfAssetsMaximum))
{
JLOG(ctx.j.warn()) << "Vault at maximum assets limit. Can't add another loan.";
return tecLIMIT_EXCEEDED;
}
if (vaultVersion == VaultVersion::FixedPrecision)
{
// Reject origination if the Vault is already coarsened.
if (getVaultScale(vault) != getVaultBaseScale(vault))
{
JLOG(ctx.j.warn()) << "FixedPrecision Vault is already coarsened; no further loans can "
"be originated until it returns to its base scale.";
return tecLIMIT_EXCEEDED;
}
}
Asset const asset = vault->at(sfAsset);
auto const vaultPseudo = vault->at(sfAccount);
@@ -366,10 +380,14 @@ LoanSet::preclaim(PreclaimContext const& ctx)
// Check that relevant values can be represented as the vault asset type.
// This check is almost duplicated in doApply, but that check is done after
// the overall loan scale is known. This is mostly only relevant for
// integral (non-IOU) types
// integral (non-IOU) types. fixCleanup3_5_0 also rejects out-of-range
// values, e.g. XRP above STAmount::kMaxNativeN, which would otherwise throw.
bool const fix350Enabled = ctx.view.rules().enabled(fixCleanup3_5_0);
for (auto const& field : getValueFields())
{
if (auto const value = tx[field]; value && STAmount{asset, *value} != *value)
if (auto const value = tx[field]; value &&
((fix350Enabled && !isRepresentable(asset, *value)) ||
STAmount{asset, *value} != *value))
{
JLOG(ctx.j.warn()) << field.f->getName() << " (" << *value
<< ") can not be represented as a(n) " << to_string(asset) << ".";
@@ -388,7 +406,7 @@ LoanSet::preclaim(PreclaimContext const& ctx)
// tecDUPLICATE, which doApply ignores, so run the check only when the
// borrower lacks a holding, or the origination fee is nonzero and the
// broker owner lacks one.
auto const originationFee = tx[~sfLoanOriginationFee].value_or(Number{});
auto const originationFee = tx[~sfLoanOriginationFee].value_or(0);
if (!ctx.view.rules().enabled(fixCleanup3_4_0) || !holdingExists(ctx.view, borrower, asset) ||
(originationFee != beast::kZero && !holdingExists(ctx.view, brokerOwner, asset)))
{
@@ -453,6 +471,7 @@ LoanSet::doApply()
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultPseudo = vaultSle->at(sfAccount);
Asset const vaultAsset = vaultSle->at(sfAsset);
auto const vaultVersion = getVaultVersion(vaultSle);
auto const counterparty = tx[~sfCounterparty].value_or(brokerOwner);
auto const borrower = counterparty == brokerOwner ? accountID_ : counterparty;
@@ -492,7 +511,8 @@ LoanSet::doApply()
auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable);
auto vaultTotalProxy = vaultSle->at(sfAssetsTotal);
auto const vaultScale = getAssetsTotalScale(vaultSle);
// For Legacy/CashBasis, getVaultBaseScale falls through to getVaultScale.
auto const vaultScale = getVaultBaseScale(vaultSle);
if (vaultAvailableProxy < principalRequested)
{
JLOG(j_.warn()) << "Insufficient assets available in the Vault to fund the loan.";
@@ -520,8 +540,7 @@ LoanSet::doApply()
properties.loanState.managementFeeDue);
XRPL_ASSERT_PARTS(
*vaultSle->at(sfAssetsMaximum) == 0 ||
getVaultVersion(vaultSle) == VaultVersion::CashBasis ||
*vaultSle->at(sfAssetsMaximum) == 0 || vaultVersion >= VaultVersion::CashBasis ||
*vaultSle->at(sfAssetsMaximum) > *vaultTotalProxy,
"xrpl::LoanSet::doApply",
"instant-recognition vault is below maximum limit");
@@ -568,6 +587,20 @@ LoanSet::doApply()
// LCOV_EXCL_STOP
}
if (vaultVersion == VaultVersion::FixedPrecision)
{
// Reject origination if this loan's interest would grow the Vault past its Open-zone
// capacity.
if (vaultOpenZoneCapacity(vaultSle, state.interestDue) > getVaultOpenLimit(vaultSle))
{
JLOG(j_.warn()) << "Loan interest would exceed the FixedPrecision Vault's Open zone.";
return tecLIMIT_EXCEEDED;
}
XRPL_ASSERT(
properties.loanScale == getVaultBaseScale(vaultSle),
"xrpl::LoanSet::doApply : FixedPrecision loan uses Vault base scale");
}
auto const originationFee = tx[~sfLoanOriginationFee].value_or(Number{});
auto const loanAssetsToBorrower = principalRequested - originationFee;
@@ -717,8 +750,24 @@ LoanSet::doApply()
view.insert(loan);
// Update the balances in the vault
vaultAvailableProxy -= principalRequested;
vaultTotalProxy += assetsTotalDelta;
if (vaultVersion == VaultVersion::FixedPrecision)
{
if (auto const ter = adjustVaultBalances(
vaultSle,
{
.cash = -STAmount{vaultAsset, principalRequested},
.deployed = debtTotalDelta,
.yield = state.interestDue,
},
j_);
!isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
}
else
{
vaultAvailableProxy -= principalRequested;
vaultTotalProxy += assetsTotalDelta;
}
XRPL_ASSERT_PARTS(
*vaultAvailableProxy <= *vaultTotalProxy,
"xrpl::LoanSet::doApply",
@@ -726,7 +775,7 @@ LoanSet::doApply()
view.update(vaultSle);
// Update the balances in the loan broker
adjustImpreciseNumber(brokerSle->at(sfDebtTotal), debtTotalDelta, vaultAsset, vaultScale);
adjustBrokerDebtTotal(brokerSle, vaultSle, debtTotalDelta, vaultScale);
adjustLoanBrokerOwnerCount(view, brokerSle, 1, j_);
loanSequenceProxy += 1;
// The sequence should be extremely unlikely to roll over, but fail if it
@@ -0,0 +1,119 @@
#include <xrpl/tx/transactors/proposal/TransactionProposalCancel.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/helpers/ProposalHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
namespace xrpl {
NotTEC
TransactionProposalCancel::preflight(PreflightContext const& ctx)
{
if (ctx.tx[sfProposalID] == beast::kZero)
{
JLOG(ctx.j.debug()) << "TransactionProposalCancel: zero ProposalID.";
return temMALFORMED;
}
return tesSUCCESS;
}
TER
TransactionProposalCancel::preclaim(PreclaimContext const& ctx)
{
uint256 const proposalID = ctx.tx[sfProposalID];
// Never read the zero keylet. Preflight already rejected a zero
// ProposalID, and no real proposal can have one (every ProposalID is a
// hash), so reaching here with zero is an internal error. (and a mathematical rarity. )
if (proposalID == beast::kZero)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const sleProposal = ctx.view.read(keylet::txProposal(proposalID));
if (!sleProposal)
{
JLOG(ctx.j.debug()) << "TransactionProposalCancel: proposal does not exist.";
return tecNO_ENTRY;
}
STObject const proposedTx = sleProposal->getFieldObject(sfProposedTransaction);
// A terminal proposal is closed to new signatures, so anyone may delete
// it and release the Owner's reserve (On-Chain Cosigner spec §7.2). Being
// terminal does not stop an already signed payload from executing; see
// proposal::isTerminal.
bool const terminal = proposal::isTerminal(ctx.view, (*sleProposal)[~sfExpiration], proposedTx);
// A live proposal may only be cancelled by its Owner (the proposer) or
// by its target. The target is the account the proposed transaction
// would execute against (its Account field) or the Delegate named in it.
// Targets may always refuse a proposal, because anyone can create one
// against any account and doing so ties up one of that account's
// tickets (On-Chain Cosigner spec §7.2).
//
// The Delegate is read from the stored payload as-is; whether that
// delegation exists on-ledger is not checked. The proposal asks that
// account to sign, so that account may refuse — and refusing only
// deletes the proposer's own entry and returns the proposer's own
// reserve.
AccountID const account = ctx.tx[sfAccount];
bool const ownerOrTarget = account == sleProposal->getAccountID(sfOwner) ||
account == proposedTx.getAccountID(sfAccount) || account == proposedTx[~sfDelegate];
if (!terminal && !ownerOrTarget)
{
JLOG(ctx.j.debug()) << "TransactionProposalCancel: proposal is live and "
"canceller is neither its Owner nor its target.";
return tecNO_PERMISSION;
}
return tesSUCCESS;
}
TER
TransactionProposalCancel::doApply()
{
uint256 const proposalID = ctx_.tx[sfProposalID];
// Never read the zero keylet; see the matching guard in preclaim.
if (proposalID == beast::kZero)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const sleProposal = view().peek(keylet::txProposal(proposalID));
if (!sleProposal)
return tefINTERNAL; // LCOV_EXCL_LINE
return proposal::deleteProposal(view(), sleProposal, ctx_.registry.get().getJournal("View"));
}
void
TransactionProposalCancel::visitInvariantEntry(bool, SLE::ConstRef, SLE::ConstRef)
{
// No transaction-specific invariants yet (future work).
}
bool
TransactionProposalCancel::finalizeInvariants(
STTx const&,
TER,
XRPAmount,
ReadView const&,
beast::Journal const&)
{
// No transaction-specific invariants yet (future work).
return true;
}
} // namespace xrpl
@@ -84,7 +84,7 @@ TransactionProposalCreate::preflight(PreflightContext const& ctx)
// no batch inner transactions — and, if it is a Batch, none of its own
// inner transactions may be a nested proposal or a pseudo-transaction
// either.
if (!proposal::isValidProposal(proposedTx))
if (!proposal::isValidProposalTxnType(proposedTx))
{
JLOG(ctx.j.debug()) << "TransactionProposalCreate: proposed txn is not "
"independently submittable.";
@@ -16,6 +16,7 @@
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
@@ -96,6 +97,14 @@ VaultClawback::preclaim(PreclaimContext const& ctx)
// LCOV_EXCL_STOP
}
// Pre-fixCleanup3_5_0: a missing holder fails later in doApply, and the fee is still charged.
// Post-fixCleanup3_5_0: refused here, as the other clawback transactions do.
if (ctx.view.rules().enabled(fixCleanup3_5_0) && !ctx.view.exists(keylet::account(holder)))
{
JLOG(ctx.j.debug()) << "VaultClawback: holder account does not exist.";
return terNO_ACCOUNT;
}
// A pseudo-account holds no vault shares, so a clawback naming one is a no-op: the vault's own
// pseudo-account issues the shares, and no flow hands them to another one.
// Pre-fixCleanup3_4_0: an implicit amount ends in tecPRECISION_LOSS, an explicit one debits the
@@ -269,11 +278,11 @@ VaultClawback::assetsToClawback(
STAmount sharesDestroyed;
STAmount assetsRecovered;
// Number arithmetic can throw overflow_error when Scale and totals are large. Caught below.
// Number arithmetic can throw overflow_error when Scale and totals are large.
try
{
// Do not discount a sole holder's shares: clawing back AssetsAvailable
// at the discounted rate can burn every share while loan assets remain.
// Do not discount a sole holder's shares: clawing back AssetsAvailable at the discounted
// rate can burn every share while loan assets remain.
auto const waiveUnrealizedLoss =
fix340Enabled && isSoleShareholder(view(), holder, sleShareIssuance)
? WaiveUnrealizedLoss::Yes
@@ -281,12 +290,11 @@ VaultClawback::assetsToClawback(
if (clawbackAmount == beast::kZero)
{
// Zero amount means clawback all shares the holder has; derive the corresponding asset
// amount from the share balance.
// isSoleShareholder already established that the holder owns the
// entire outstanding share supply whenever the waiver applies, so
// sfOutstandingAmount gives sharesDestroyed directly, avoiding a
// redundant MPToken read via accountHolds.
// Zero amount clawbacks all shares of the holder; derive the corresponding asset
// amount from the share balance. isSoleShareholder already established that the holder
// owns the entire outstanding share supply whenever the waiver applies, so
// sfOutstandingAmount gives sharesDestroyed directly, avoiding a redundant MPToken read
// via accountHolds.
sharesDestroyed = waiveUnrealizedLoss == WaiveUnrealizedLoss::Yes
? STAmount{share, sleShareIssuance->at(sfOutstandingAmount)}
: accountHolds(
@@ -305,11 +313,10 @@ VaultClawback::assetsToClawback(
}
else
{
// Pre-fixCleanup3_4_0: shares were rounded to nearest, so the
// round-trip back to assets could exceed clawbackAmount.
// Post-amendment: truncate shares so assetsRecovered <=
// clawbackAmount by construction (matches the clamp branch
// below).
// Pre-fixCleanup3_4_0: shares were rounded to nearest, so the round-trip back to assets
// could exceed clawbackAmount.
// Post-fixCleanup3_4_0: truncate shares so assetsRecovered <= clawbackAmount by
// construction.
auto const truncate = fix340Enabled ? TruncateShares::Yes : TruncateShares::No;
auto const maybeShares = assetsToSharesWithdraw(
vault, sleShareIssuance, clawbackAmount, truncate, waiveUnrealizedLoss);
@@ -323,8 +330,7 @@ VaultClawback::assetsToClawback(
return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE
assetsRecovered = *maybeAssets;
}
// Clamp assetsRecovered to sfAssetsAvailable, then re-derive shares and assets so the pair
// stays consistent.
if (assetsRecovered > *assetsAvailable)
{
assetsRecovered = *assetsAvailable;
@@ -356,13 +362,28 @@ VaultClawback::assetsToClawback(
}
}
// Post-fixCleanup3_4_0: round the recovery down at the posterior sfAssetsTotal scale so all
// rails change by the same representable delta. sharesDestroyed is intentionally NOT
// re-derived here: the holder's shares are burned for their pre-clamp value, so any
// sub-ULP trimmed off stays in the vault for the remaining shareholders.
if (ctx_.view().rules().enabled(fixCleanup3_4_0) && assetsRecovered > beast::kZero)
// On a coarsened Vault, an AssetsDeployed below half a unit of AssetsAvailable's grid
// rounds away: the conversion equals AssetsAvailable exactly, the clamp above never runs,
// and every share would be burned while AssetsDeployed is still outstanding.
if (getVaultVersion(vault) == VaultVersion::FixedPrecision &&
sharesDestroyed == STAmount{share, sleShareIssuance->at(sfOutstandingAmount)} &&
Number(vault->at(sfAssetsDeployed)) != beast::kZero)
{
auto const maybeClamped = clampToAssetsTotalScale(vault, -assetsRecovered);
JLOG(j_.debug())
<< "VaultClawback: cannot burn all shares while AssetsDeployed is non-zero";
return std::unexpected(tecHAS_OBLIGATIONS);
}
// Post-fixCleanup3_4_0: round the recovery down at the posterior scale of the balance
// - AssetsAvailable on FixedPrecision Vault
// - AssetsTotal on Legacy/CashBasis Vault
// All rails change by the same representable delta.
// sharesDestroyed is intentionally NOT re-derived here: the holder's shares are burned for
// their pre-clamp value, so any sub-ULP trimmed off stays in the vault for the remaining
// shareholders.
if (fix340Enabled && assetsRecovered > beast::kZero)
{
auto const maybeClamped = clampVaultOutflow(vault, -assetsRecovered);
if (!maybeClamped)
return std::unexpected(maybeClamped.error());
assetsRecovered = *maybeClamped;
@@ -408,9 +429,6 @@ VaultClawback::doApply()
Asset const vaultAsset = vault->at(sfAsset);
STAmount const amount = clawbackAmount(vault, tx[~sfAmount], accountID_);
auto assetsAvailable = vault->at(sfAssetsAvailable);
auto assetsTotal = vault->at(sfAssetsTotal);
AccountID const holder = tx[sfHolder];
STAmount sharesDestroyed = {share};
STAmount assetsRecovered = {vault->at(sfAsset)};
@@ -441,40 +459,15 @@ VaultClawback::doApply()
// Number arithmetic can throw overflow_error when Scale and totals are large.
if (view().rules().enabled(fixCleanup3_4_0))
{
try
{
// A non-zero recovery can be too small to change the stored sfAssetsTotal at
// STAmount's precision. Shares would still be burned, reject it instead.
if (debitIsNonZeroDust(vaultAsset, assetsTotal, assetsRecovered))
{
// LCOV_EXCL_START
JLOG(j_.debug())
<< "VaultClawback: clawback amount too small to change stored vault"
" balance";
return tecPRECISION_LOSS;
// LCOV_EXCL_STOP
}
}
// LCOV_EXCL_START
catch (std::overflow_error const&)
{
// It's easy to hit this exception from Number with large enough Scale
// so we avoid spamming the log and only use debug here.
JLOG(j_.debug()) //
<< "VaultClawback: overflow error with"
<< " scale=" << (int)vault->at(sfScale).value() //
<< ", assetsTotal=" << vault->at(sfAssetsTotal).value()
<< ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount)
<< ", amount=" << amount.value();
// Overflow means this transaction cannot apply, but ledger state is still
// consistent. Return tecPATH_DRY rather than a hard internal error.
return tecPATH_DRY;
}
// LCOV_EXCL_STOP
// A non-zero recovery can be too small to change the stored balance at
// STAmount's precision. Shares would still be burned, reject it instead.
if (auto const ter = checkDebitNotDust(vault, assetsRecovered, j_, "VaultClawback");
!isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
}
assetsTotal -= assetsRecovered;
assetsAvailable -= assetsRecovered;
if (auto const ter = adjustVaultCash(vault, -assetsRecovered, j_); !isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
view().update(vault);
auto const& vaultAccount = vault->at(sfAccount);
@@ -1,5 +1,6 @@
#include <xrpl/tx/transactors/vault/VaultCreate.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Zero.h>
@@ -18,6 +19,7 @@
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.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>
@@ -87,6 +89,11 @@ VaultCreate::preflight(PreflightContext const& ctx)
{
if (*assetMax < beast::kZero)
return temMALFORMED;
// An AssetsMaximum out of range for the asset, e.g. XRP above
// STAmount::kMaxNativeN, would throw when rounded to the asset.
if (ctx.rules.enabled(fixCleanup3_5_0) && !isRepresentable(ctx.tx[sfAsset], *assetMax))
return temMALFORMED;
}
if (auto const metadata = ctx.tx[~sfMPTokenMetadata])
@@ -101,7 +108,10 @@ VaultCreate::preflight(PreflightContext const& ctx)
if (vaultAsset.holds<MPTIssue>() || vaultAsset.native())
return temMALFORMED;
if (scale > kVaultMaximumIouScale)
auto const maximumScale = getVaultDefaultVersion(ctx.rules) == VaultVersion::FixedPrecision
? kFixedPrecisionVaultMaximumIouScale
: kLegacyVaultMaximumIouScale;
if (scale > maximumScale)
return temMALFORMED;
}
@@ -168,6 +178,22 @@ VaultCreate::preclaim(PreclaimContext const& ctx)
hasExpired(ctx.view, ctx.tx[~sfRedemptionDate]))
return tecEXPIRED;
// FixedPrecision: AssetsMaximum must be exactly representable on the Vault's base grid,
// otherwise associateAsset would silently round the cap the owner asked for. Legacy and
// CashBasis Vaults keep their existing behavior.
if (auto const assetMax = ctx.tx[~sfAssetsMaximum];
assetMax && getVaultDefaultVersion(ctx.view.rules()) == VaultVersion::FixedPrecision)
{
int const baseScale =
vaultBaseScale(vaultAsset, ctx.tx[~sfScale].value_or(kVaultDefaultIouScale));
if (!isOnVaultBaseGrid(vaultAsset, *assetMax, baseScale))
{
JLOG(ctx.j.debug()) << "VaultCreate: AssetsMaximum " << *assetMax
<< " is not representable at the Vault scale.";
return tecPRECISION_LOSS;
}
}
return tesSUCCESS;
}
@@ -273,10 +299,20 @@ VaultCreate::doApply()
}
if (scale != 0u)
vault->at(sfScale) = scale;
if (view().rules().enabled(featureLendingProtocolV1_1))
VaultVersion const version = getVaultDefaultVersion(view().rules());
if (version == VaultVersion::FixedPrecision)
{
vault->at(sfLEVersion) = std::to_underlying(VaultVersion::FixedPrecision);
vault->at(sfYieldUnrealized) = Number(0);
vault->at(sfAssetsDeployed) = Number(0);
}
else if (version == VaultVersion::CashBasis)
{
vault->at(sfLEVersion) = std::to_underlying(VaultVersion::CashBasis);
}
if (version != VaultVersion::Legacy)
{
auto const kind = getVaultKind(tx);
vault->at(sfVaultKind) = std::to_underlying(kind);
if (kind == VaultKind::ClosedEnded)
@@ -6,6 +6,7 @@
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -59,7 +60,7 @@ VaultDelete::preclaim(PreclaimContext const& ctx)
return tecHAS_OBLIGATIONS;
}
if (vault->at(sfAssetsTotal) != 0)
if (getAssetsTotal(vault) != 0)
{
JLOG(ctx.j.debug()) << "VaultDelete: nonzero assets total.";
return tecHAS_OBLIGATIONS;
@@ -31,24 +31,6 @@
namespace xrpl {
[[nodiscard]]
static STAmount
roundToVaultScale(STAmount const& amount, SLE::ConstRef vault)
{
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::roundToVaultScale : valid vault sle");
XRPL_ASSERT(
amount.asset() == vault->at(sfAsset), "xrpl::roundToVaultScale : valid vault asset");
if (amount.integral())
return amount;
int const postScale = [&] {
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
return scale(vault->at(sfAssetsTotal) + amount, vault->at(sfAsset));
}();
return roundToScale(amount, postScale, Number::RoundingMode::Downward);
}
// True if debiting `assets` would leave the depositor's balance where it started, so the deposit
// would mint shares against a transfer that never happened. Asking the balance directly whether it
// notices the debit avoids having to infer the rounding step: it has to be the stored balance that
@@ -187,7 +169,9 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
if (auto const ter = requireAuth(ctx.view, vaultAsset, account); !isTesSuccess(ter))
return ter;
auto const roundedAmount = fix320Enabled ? roundToVaultScale(amount, vault) : amount;
auto const roundedAmount = fix320Enabled
? roundToPosteriorVaultScale(vault, amount, Number::RoundingMode::TowardsZero)
: amount;
if (fix320Enabled && roundedAmount == beast::kZero)
{
@@ -237,10 +221,11 @@ VaultDeposit::doApply()
return tefINTERNAL; // LCOV_EXCL_LINE
auto const vaultAsset = vault->at(sfAsset);
// Post-amendment IOU only: round Downward to the AssetsTotal precision so
// Post-amendment IOU only: round toward zero to the AssetsTotal precision so
// a sub-ULP tail can't be silently absorbed by one rail and not the other.
auto const amount =
fix320Enabled ? roundToVaultScale(ctx_.tx[sfAmount], vault) : ctx_.tx[sfAmount];
auto const amount = fix320Enabled
? roundToPosteriorVaultScale(vault, ctx_.tx[sfAmount], Number::RoundingMode::TowardsZero)
: ctx_.tx[sfAmount];
// We validated zero-amount in preclaim, if we ended up with zero now, fail hard.
if (amount == beast::kZero)
@@ -374,17 +359,20 @@ VaultDeposit::doApply()
return tecPATH_DRY;
}
if (auto const ter = checkOptionalVaultInflow(vault, assetsDeposited); !isTesSuccess(ter))
return ter;
XRPL_ASSERT(
sharesCreated.asset() != assetsDeposited.asset(),
"xrpl::VaultDeposit::doApply : assets are not shares");
vault->at(sfAssetsTotal) += assetsDeposited;
vault->at(sfAssetsAvailable) += assetsDeposited;
if (auto const ter = adjustVaultCash(vault, assetsDeposited, j_); !isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
view().update(vault);
// A deposit must not push the vault over its limit.
auto const maximum = *vault->at(sfAssetsMaximum);
if (maximum != 0 && *vault->at(sfAssetsTotal) > maximum)
if (maximum != 0 && getAssetsTotal(vault) > maximum)
return tecLIMIT_EXCEEDED;
// Transfer assets from depositor to vault.
+29 -1
View File
@@ -1,12 +1,15 @@
#include <xrpl/tx/transactors/vault/VaultSet.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.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>
@@ -74,6 +77,16 @@ VaultSet::preclaim(PreclaimContext const& ctx)
return tecNO_PERMISSION;
}
// An AssetsMaximum out of range for the vault asset, e.g. XRP above
// STAmount::kMaxNativeN, would throw when rounded to the asset.
if (auto const assetMax = ctx.tx[~sfAssetsMaximum]; assetMax &&
ctx.view.rules().enabled(fixCleanup3_5_0) &&
!isRepresentable(vault->at(sfAsset), *assetMax))
{
JLOG(ctx.j.debug()) << "VaultSet: max assets out of range for the asset.";
return tecPRECISION_LOSS;
}
auto const mptIssuanceID = (*vault)[sfShareMPTID];
auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID));
if (!sleIssuance)
@@ -110,6 +123,21 @@ VaultSet::preclaim(PreclaimContext const& ctx)
}
}
// With featureLendingProtocolV1_2, AssetsMaximum must be exactly
// representable at the Vault's scale (the base grid on FixedPrecision
// Vaults, the live scale on existing Legacy/CashBasis Vaults), otherwise
// associateAsset would silently round the cap the owner asked for.
if (auto const assetMax = ctx.tx[~sfAssetsMaximum];
assetMax && ctx.view.rules().enabled(featureLendingProtocolV1_2))
{
if (auto const ter = checkAssetsMaximum(vault, *assetMax); !isTesSuccess(ter))
{
JLOG(ctx.j.debug()) << "VaultSet: AssetsMaximum " << *assetMax
<< " is not representable at the Vault scale.";
return ter;
}
}
return tesSUCCESS;
}
@@ -144,7 +172,7 @@ VaultSet::doApply()
vault->at(sfData) = tx[sfData];
if (tx.isFieldPresent(sfAssetsMaximum))
{
if (tx[sfAssetsMaximum] != 0 && tx[sfAssetsMaximum] < *vault->at(sfAssetsTotal))
if (tx[sfAssetsMaximum] != 0 && tx[sfAssetsMaximum] < getAssetsTotal(vault))
return tecLIMIT_EXCEEDED;
vault->at(sfAssetsMaximum) = tx[sfAssetsMaximum];
}
@@ -12,12 +12,15 @@
#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>
@@ -49,6 +52,26 @@ 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)
{
@@ -170,11 +193,13 @@ 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,
*maybeAssets,
amountToReceive,
ctx.tx.isFieldPresent(sfDestinationTag),
ctx.tx[~sfCredentialIDs]))
return ret;
@@ -302,6 +327,8 @@ 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};
@@ -330,7 +357,11 @@ VaultWithdraw::doApply()
view().rules().enabled(fixCleanup3_4_0) ? TruncateShares::Yes : TruncateShares::No;
{
auto const maybeShares = assetsToSharesWithdraw(
vault, sleIssuance, amount, truncate, waiveUnrealizedLoss);
vault,
sleIssuance,
addTransferFee(amount, rate),
truncate,
waiveUnrealizedLoss);
if (!maybeShares)
return tecINTERNAL; // LCOV_EXCL_LINE
sharesRedeemed = *maybeShares;
@@ -384,10 +415,20 @@ VaultWithdraw::doApply()
bool const isFinalWithdrawal =
sharesRedeemed == STAmount{share, sleIssuance->at(sfOutstandingAmount)};
auto assetsAvailable = vault->at(sfAssetsAvailable);
auto assetsTotal = vault->at(sfAssetsTotal);
auto const lossUnrealized = vault->at(sfLossUnrealized);
// FixedPrecision: a final withdrawal must not leave AssetsDeployed behind
// with no shares to back it. A final withdrawal pays out AssetsAvailable
// + AssetsDeployed, rounded to 16 digits; on a coarsened vault, an
// AssetsDeployed below half a unit of AssetsAvailable's grid rounds
// away, so the payout equals AssetsAvailable exactly and the vault would
// be left with no shares and no cash but a non-zero AssetsDeployed.
if (isFinalWithdrawal && getVaultVersion(vault) == VaultVersion::FixedPrecision &&
Number(vault->at(sfAssetsDeployed)) != beast::kZero)
{
JLOG(j_.debug()) << "VaultWithdraw: cannot empty vault while AssetsDeployed is non-zero";
return tecHAS_OBLIGATIONS;
}
auto const assetsAvailable = vault->at(sfAssetsAvailable);
if (fix340Enabled && !isFinalWithdrawal)
{
// Fixed-shares path: a small share count can round to zero assets even though the vault has
@@ -400,34 +441,11 @@ VaultWithdraw::doApply()
return tecPRECISION_LOSS;
}
// Number arithmetic can throw overflow_error when Scale and totals are large.
try
{
// A non-zero payout can be too small to change the stored sfAssetsTotal at
// STAmount's precision. Shares would still be burned, reject it instead.
if (debitIsNonZeroDust(vaultAsset, assetsTotal, assetsWithdrawn))
{
JLOG(j_.debug()) << "VaultWithdraw: withdrawal amount too small to change stored"
" vault balance";
return tecPRECISION_LOSS;
}
}
// LCOV_EXCL_START
catch (std::overflow_error const&)
{
// It's easy to hit this exception from Number with large enough Scale
// so we avoid spamming the log and only use debug here.
JLOG(j_.debug()) //
<< "VaultWithdraw: overflow error with"
<< " scale=" << (int)vault->at(sfScale).value() //
<< ", assetsTotal=" << vault->at(sfAssetsTotal).value()
<< ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount)
<< ", amount=" << amount.value();
// Overflow means this transaction cannot apply, but ledger state is still consistent.
// Return tecPATH_DRY rather than a hard internal error.
return tecPATH_DRY;
}
// LCOV_EXCL_STOP
// A non-zero payout can be too small to change the stored balance at
// STAmount's precision. Shares would still be burned, reject it instead.
if (auto const ter = checkDebitNotDust(vault, assetsWithdrawn, j_, "VaultWithdraw");
!isTesSuccess(ter))
return ter;
}
// Post-fixCleanup3_3_0: preclaim already validated all freeze conditions
@@ -466,10 +484,12 @@ VaultWithdraw::doApply()
// Number arithmetic can throw overflow_error when Scale and totals are large.
try
{
// Round down at the posterior sfAssetsTotal scale so the payout never exceeds the
// value represented by the redeemed shares. sharesRedeemed is intentionally not
// Round down at the posterior scale of the balance the payout actually
// leaves -- AssetsAvailable on FixedPrecision, the AssetsTotal cache on
// Legacy/CashBasis -- so the payout never exceeds the value represented
// by the redeemed shares. sharesRedeemed is intentionally not
// re-derived: any trimmed residue stays with remaining shareholders.
auto const maybeClamped = clampToAssetsTotalScale(vault, -assetsWithdrawn);
auto const maybeClamped = clampVaultOutflow(vault, -assetsWithdrawn);
if (!maybeClamped)
return maybeClamped.error(); // LCOV_EXCL_LINE
assetsWithdrawn = *maybeClamped;
@@ -503,14 +523,15 @@ VaultWithdraw::doApply()
// unrealized loss. Otherwise the resulting (shares == 0, assetsTotal > 0) state would violate
// the zero-sized-vault invariant.
//
// The payout is set to the remaining sfAssetsAvailable. The helper result should already
// The payout is set to the remaining AssetsAvailable. The helper result should already
// equal that value in a clean vault; any mismatch is a rounding artifact and is logged.
if (view().rules().enabled(fixCleanup3_2_0) && isFinalWithdrawal)
bool const finalPayout = view().rules().enabled(fixCleanup3_2_0) && isFinalWithdrawal;
if (finalPayout)
{
// Unreachable: a final withdrawal with lossUnrealized > 0 has
// assetsWithdrawn == assetsTotal > assetsAvailable, which the
// insufficient-funds guard above already rejected.
if (*lossUnrealized != beast::kZero)
if (*vault->at(sfLossUnrealized) != beast::kZero)
{
// LCOV_EXCL_START
UNREACHABLE(
@@ -531,17 +552,24 @@ VaultWithdraw::doApply()
<< " assetsAvailable=" << allAvailable.getText();
}
assetsWithdrawn = allAvailable;
// Do not let dust accumulate in the Vault.
assetsTotal = 0;
assetsAvailable = 0;
}
else
auto const assetsDelivered = subtractTransferFee(assetsWithdrawn, rate);
if (assetsWithdrawn > beast::kZero && assetsDelivered == beast::kZero)
{
// Debit both rails by the same delta so sfAssetsTotal and sfAssetsAvailable stay in step,
// as required by the ValidVault invariant.
assetsTotal -= assetsWithdrawn;
assetsAvailable -= assetsWithdrawn;
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.
vault->at(sfAssetsTotal) = 0;
vault->at(sfAssetsAvailable) = 0;
}
else if (auto const ter = adjustVaultCash(vault, -assetsWithdrawn, j_); !isTesSuccess(ter))
{
return ter; // LCOV_EXCL_LINE
}
view().update(vault);
@@ -583,9 +611,15 @@ VaultWithdraw::doApply()
associateAsset(*vault, vaultAsset);
auto const dstAcct = ctx_.tx[~sfDestination].value_or(accountID_);
return doWithdraw(
applyViewContext, accountID_, dstAcct, vaultAccount, preFeeBalance_, assetsWithdrawn, j_);
applyViewContext,
accountID_,
dstAcct,
vaultAccount,
preFeeBalance_,
assetsDelivered,
assetsWithdrawn,
j_);
}
void
+5
View File
@@ -0,0 +1,5 @@
# 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
@@ -0,0 +1 @@
AGENTS.md
-1
View File
@@ -425,7 +425,6 @@ class AMMCalc_test : public beast::unit_test::Suite
pool->first,
Quality{offer->first},
fee,
env.current()->rules(),
beast::Journal(beast::Journal::getNullSink()));
ammOffer)
{
+5 -3
View File
@@ -2336,13 +2336,15 @@ private:
// on 24e12GBP
// 1,200e12 - 24e12*1.25 =~ 1,170e12GBP
env.require(Balance(alice_, gbp(1'170'000'000'000'000)));
// 24e12GBP is swapped in for 28.125e12BTC
// 24e12GBP is swapped in for 28.125e12BTC, less one unit: the
// limit quality 120/90 is rounded to nearest, so a trade at
// exactly 4/3 is just over the limit
BEAST_EXPECT(amm.expectBalances(
gbp(1'024'000'000'000'000), btc(1'171'875'000'000'000), amm.tokens()));
gbp(1'024'000'000'000'000), btc(1'171'875'000'000'001), amm.tokens()));
// 25% on 22.5e12BTC is paid in tr fee
// 22.5*1.25 = 28.125e12BTC
env.require(Balance(carol_, btc(1'222'500'000'000'000)));
env.require(Balance(carol_, btc(1'222'499'999'999'999)));
}
{
// Payment via offer and AMM with limit quality, deliver less
+121 -306
View File
@@ -118,16 +118,8 @@ private:
Sendmax(BTC(1'000)),
Txflags(tfPartialPayment));
if (!features[fixAMMv1_1])
{
BEAST_EXPECT(ammCarol.expectBalances(
STAmount{BTC, UINT64_C(1'001'000000374812), -12}, USD(100'000), ammCarol.tokens()));
}
else
{
BEAST_EXPECT(ammCarol.expectBalances(
STAmount{BTC, UINT64_C(1'001'000000374815), -12}, USD(100'000), ammCarol.tokens()));
}
BEAST_EXPECT(ammCarol.expectBalances(
STAmount{BTC, UINT64_C(1'001'000000374815), -12}, USD(100'000), ammCarol.tokens()));
env.require(Balance(bob_, USD(200'100)));
BEAST_EXPECT(isOffer(env, carol_, BTC(49), XRP(49)));
@@ -693,18 +685,9 @@ private:
auto const jrr = env.rpc("json", "submit", to_string(payment));
BEAST_EXPECT(jrr[jss::result][jss::status] == "success");
BEAST_EXPECT(jrr[jss::result][jss::engine_result] == "tesSUCCESS");
if (!features[fixAMMv1_1])
{
BEAST_EXPECT(ammAlice.expectBalances(
STAmount(xts, UINT64_C(101'010101010101), -12), xxx(99), ammAlice.tokens()));
BEAST_EXPECT(expectHolding(env, bob_, STAmount{xts, UINT64_C(98'989898989899), -12}));
}
else
{
BEAST_EXPECT(ammAlice.expectBalances(
STAmount(xts, UINT64_C(101'0101010101011), -13), xxx(99), ammAlice.tokens()));
BEAST_EXPECT(expectHolding(env, bob_, STAmount{xts, UINT64_C(98'9898989898989), -13}));
}
BEAST_EXPECT(ammAlice.expectBalances(
STAmount(xts, UINT64_C(101'0101010101011), -13), xxx(99), ammAlice.tokens()));
BEAST_EXPECT(expectHolding(env, bob_, STAmount{xts, UINT64_C(98'9898989898989), -13}));
BEAST_EXPECT(expectHolding(env, bob_, xxx(101)));
}
@@ -1207,34 +1190,17 @@ private:
env(offer(cam, bBux(30), aBux(30)));
// AMM is consumed up to the first cam Offer quality
if (!features[fixAMMv1_1])
{
BEAST_EXPECT(ammCarol.expectBalances(
STAmount{aBux, UINT64_C(309'3541659651605), -13},
STAmount{bBux, UINT64_C(320'0215509984417), -13},
ammCarol.tokens()));
BEAST_EXPECT(expectOffers(
env,
cam,
1,
{{Amounts{
STAmount{bBux, UINT64_C(20'0215509984417), -13},
STAmount{aBux, UINT64_C(20'0215509984417), -13}}}}));
}
else
{
BEAST_EXPECT(ammCarol.expectBalances(
STAmount{aBux, UINT64_C(309'3541659651604), -13},
STAmount{bBux, UINT64_C(320'0215509984419), -13},
ammCarol.tokens()));
BEAST_EXPECT(expectOffers(
env,
cam,
1,
{{Amounts{
STAmount{bBux, UINT64_C(20'0215509984419), -13},
STAmount{aBux, UINT64_C(20'0215509984419), -13}}}}));
}
BEAST_EXPECT(ammCarol.expectBalances(
STAmount{aBux, UINT64_C(309'3541659651604), -13},
STAmount{bBux, UINT64_C(320'0215509984419), -13},
ammCarol.tokens()));
BEAST_EXPECT(expectOffers(
env,
cam,
1,
{{Amounts{
STAmount{bBux, UINT64_C(20'0215509984419), -13},
STAmount{aBux, UINT64_C(20'0215509984419), -13}}}}));
}
void
@@ -1429,7 +1395,7 @@ private:
using namespace jtx;
testRmFundedOffer(all_);
testRmFundedOffer(all_ - fixAMMv1_1 - fixAMMv1_3);
testRmFundedOffer(all_ - fixAMMv1_3);
testEnforceNoRipple(all_);
testFillModes(all_);
testOfferCrossWithXRP(all_);
@@ -1443,7 +1409,7 @@ private:
testOfferCreateThenCross(all_);
testSellFlagExceedLimit(all_);
testGatewayCrossCurrency(all_);
testGatewayCrossCurrency(all_ - fixAMMv1_1 - fixAMMv1_3);
testGatewayCrossCurrency(all_ - fixAMMv1_3);
testBridgedCross(all_);
testSellWithFillOrKill(all_);
testTransferRateOffer(all_);
@@ -1451,7 +1417,7 @@ private:
testBadPathAssert(all_);
testSellFlagBasic(all_);
testDirectToDirectPath(all_);
testDirectToDirectPath(all_ - fixAMMv1_1 - fixAMMv1_3);
testDirectToDirectPath(all_ - fixAMMv1_3);
testRequireAuth(all_);
testPseudoAccountRequireAuth(all_);
testPseudoAccountRequireAuth(all_ - fixCleanup3_4_0);
@@ -2164,17 +2130,8 @@ private:
// alice_ buys 107.1428USD with 120GBP and pays 25% tr fee on 120GBP
// 1,000 - 120*1.25 = 850GBP
BEAST_EXPECT(expectHolding(env, alice_, GBP(850)));
if (!features[fixAMMv1_1])
{
// 120GBP is swapped in for 107.1428USD
BEAST_EXPECT(amm.expectBalances(
GBP(1'120), STAmount{USD, UINT64_C(892'8571428571428), -13}, amm.tokens()));
}
else
{
BEAST_EXPECT(amm.expectBalances(
GBP(1'120), STAmount{USD, UINT64_C(892'8571428571429), -13}, amm.tokens()));
}
BEAST_EXPECT(amm.expectBalances(
GBP(1'120), STAmount{USD, UINT64_C(892'8571428571429), -13}, amm.tokens()));
// 25% of 85.7142USD is paid in tr fee
// 85.7142*1.25 = 107.1428USD
BEAST_EXPECT(
@@ -2246,34 +2203,17 @@ private:
env.close();
BEAST_EXPECT(expectHolding(env, alice_, GBP(850)));
if (!features[fixAMMv1_1])
{
// alice_ buys 107.1428EUR with 120GBP and pays 25% tr fee on
// 120GBP 1,000 - 120*1.25 = 850GBP 120GBP is swapped in for
// 107.1428EUR
BEAST_EXPECT(amm1.expectBalances(
GBP(1'120), STAmount{EUR, UINT64_C(892'8571428571428), -13}, amm1.tokens()));
// 25% on 85.7142EUR is paid in tr fee 85.7142*1.25 =
// 107.1428EUR 85.7142EUR is swapped in for 78.9473USD
BEAST_EXPECT(amm2.expectBalances(
STAmount(EUR, UINT64_C(1'085'714285714286), -12),
STAmount{USD, UINT64_C(921'0526315789471), -13},
amm2.tokens()));
}
else
{
// alice_ buys 107.1428EUR with 120GBP and pays 25% tr fee on
// 120GBP 1,000 - 120*1.25 = 850GBP 120GBP is swapped in for
// 107.1428EUR
BEAST_EXPECT(amm1.expectBalances(
GBP(1'120), STAmount{EUR, UINT64_C(892'8571428571429), -13}, amm1.tokens()));
// 25% on 85.7142EUR is paid in tr fee 85.7142*1.25 =
// 107.1428EUR 85.7142EUR is swapped in for 78.9473USD
BEAST_EXPECT(amm2.expectBalances(
STAmount(EUR, UINT64_C(1'085'714285714286), -12),
STAmount{USD, UINT64_C(921'052631578948), -12},
amm2.tokens()));
}
// alice_ buys 107.1428EUR with 120GBP and pays 25% tr fee on
// 120GBP 1,000 - 120*1.25 = 850GBP 120GBP is swapped in for
// 107.1428EUR
BEAST_EXPECT(amm1.expectBalances(
GBP(1'120), STAmount{EUR, UINT64_C(892'8571428571429), -13}, amm1.tokens()));
// 25% on 85.7142EUR is paid in tr fee 85.7142*1.25 =
// 107.1428EUR 85.7142EUR is swapped in for 78.9473USD
BEAST_EXPECT(amm2.expectBalances(
STAmount(EUR, UINT64_C(1'085'714285714286), -12),
STAmount{USD, UINT64_C(921'052631578948), -12},
amm2.tokens()));
// 25% on 63.1578USD is paid in tr fee 63.1578*1.25 = 78.9473USD
BEAST_EXPECT(
expectHolding(env, carol_, STAmount(USD, UINT64_C(1'063'157894736842), -12)));
@@ -2346,28 +2286,14 @@ private:
Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
env.close();
if (!features[fixAMMv1_1])
{
// alice_ buys 28.125USD with 24GBP and pays 25% tr fee
// on 24GBP
// 1,200 - 24*1.25 = 1,170GBP
BEAST_EXPECT(expectHolding(env, alice_, GBP(1'170)));
// 24GBP is swapped in for 28.125USD
BEAST_EXPECT(amm.expectBalances(GBP(1'024), USD(1'171.875), amm.tokens()));
}
else
{
// alice_ buys 28.125USD with 24GBP and pays 25% tr fee
// on 24GBP
// 1,200 - 24*1.25 =~ 1,170GBP
BEAST_EXPECT(
expectHolding(env, alice_, STAmount{GBP, UINT64_C(1'169'999999999999), -12}));
// 24GBP is swapped in for 28.125USD
BEAST_EXPECT(amm.expectBalances(
STAmount{GBP, UINT64_C(1'024'000000000001), -12},
USD(1'171.875),
amm.tokens()));
}
// alice_ buys 28.125USD with 24GBP and pays 25% tr fee
// on 24GBP
// 1,200 - 24*1.25 =~ 1,170GBP
BEAST_EXPECT(
expectHolding(env, alice_, STAmount{GBP, UINT64_C(1'169'999999999999), -12}));
// 24GBP is swapped in for 28.125USD
BEAST_EXPECT(amm.expectBalances(
STAmount{GBP, UINT64_C(1'024'000000000001), -12}, USD(1'171.875), amm.tokens()));
// 25% on 22.5USD is paid in tr fee
// 22.5*1.25 = 28.125USD
BEAST_EXPECT(expectHolding(env, carol_, USD(1'222.5)));
@@ -2400,62 +2326,31 @@ private:
Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
env.close();
if (!features[fixAMMv1_1])
{
// alice_ buys 70.4210EUR with 70.4210GBP via the offer
// and pays 25% tr fee on 70.4210GBP
// 1,400 - 70.4210*1.25 = 1400 - 88.0262 = 1311.9736GBP
BEAST_EXPECT(
expectHolding(env, alice_, STAmount{GBP, UINT64_C(1'311'973684210527), -12}));
// ed doesn't pay tr fee, the balances reflect consumed offer
// 70.4210GBP/70.4210EUR
BEAST_EXPECT(expectHolding(
env,
ed,
STAmount{EUR, UINT64_C(1'329'578947368421), -12},
STAmount{GBP, UINT64_C(1'470'421052631579), -12}));
BEAST_EXPECT(expectOffers(
env,
ed,
1,
{Amounts{
STAmount{GBP, UINT64_C(929'5789473684212), -13},
STAmount{EUR, UINT64_C(929'5789473684212), -13}}}));
// 25% on 56.3368EUR is paid in tr fee 56.3368*1.25 = 70.4210EUR
// 56.3368EUR is swapped in for 74.6651USD
BEAST_EXPECT(amm.expectBalances(
STAmount{EUR, UINT64_C(1'056'336842105263), -12},
STAmount{USD, UINT64_C(1'325'334821428571), -12},
amm.tokens()));
}
else
{
// alice_ buys 70.4210EUR with 70.4210GBP via the offer
// and pays 25% tr fee on 70.4210GBP
// 1,400 - 70.4210*1.25 = 1400 - 88.0262 = 1311.9736GBP
BEAST_EXPECT(
expectHolding(env, alice_, STAmount{GBP, UINT64_C(1'311'973684210525), -12}));
// ed doesn't pay tr fee, the balances reflect consumed offer
// 70.4210GBP/70.4210EUR
BEAST_EXPECT(expectHolding(
env,
ed,
STAmount{EUR, UINT64_C(1'329'57894736842), -11},
STAmount{GBP, UINT64_C(1'470'42105263158), -11}));
BEAST_EXPECT(expectOffers(
env,
ed,
1,
{Amounts{
STAmount{GBP, UINT64_C(929'57894736842), -11},
STAmount{EUR, UINT64_C(929'57894736842), -11}}}));
// 25% on 56.3368EUR is paid in tr fee 56.3368*1.25 = 70.4210EUR
// 56.3368EUR is swapped in for 74.6651USD
BEAST_EXPECT(amm.expectBalances(
STAmount{EUR, UINT64_C(1'056'336842105264), -12},
STAmount{USD, UINT64_C(1'325'334821428571), -12},
amm.tokens()));
}
// alice_ buys 70.4210EUR with 70.4210GBP via the offer
// and pays 25% tr fee on 70.4210GBP
// 1,400 - 70.4210*1.25 = 1400 - 88.0262 = 1311.9736GBP
BEAST_EXPECT(
expectHolding(env, alice_, STAmount{GBP, UINT64_C(1'311'973684210525), -12}));
// ed doesn't pay tr fee, the balances reflect consumed offer
// 70.4210GBP/70.4210EUR
BEAST_EXPECT(expectHolding(
env,
ed,
STAmount{EUR, UINT64_C(1'329'57894736842), -11},
STAmount{GBP, UINT64_C(1'470'42105263158), -11}));
BEAST_EXPECT(expectOffers(
env,
ed,
1,
{Amounts{
STAmount{GBP, UINT64_C(929'57894736842), -11},
STAmount{EUR, UINT64_C(929'57894736842), -11}}}));
// 25% on 56.3368EUR is paid in tr fee 56.3368*1.25 = 70.4210EUR
// 56.3368EUR is swapped in for 74.6651USD
BEAST_EXPECT(amm.expectBalances(
STAmount{EUR, UINT64_C(1'056'336842105264), -12},
STAmount{USD, UINT64_C(1'325'334821428571), -12},
amm.tokens()));
// 25% on 59.7321USD is paid in tr fee 59.7321*1.25 = 74.6651USD
BEAST_EXPECT(
expectHolding(env, carol_, STAmount(USD, UINT64_C(1'459'732142857143), -12)));
@@ -2488,40 +2383,20 @@ private:
Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
env.close();
if (!features[fixAMMv1_1])
{
// alice_ buys 53.3322EUR with 56.3368GBP via the amm
// and pays 25% tr fee on 56.3368GBP
// 1,400 - 56.3368*1.25 = 1400 - 70.4210 = 1329.5789GBP
BEAST_EXPECT(
expectHolding(env, alice_, STAmount{GBP, UINT64_C(1'329'578947368421), -12}));
/**
* / 25% on 56.3368EUR is paid in tr fee 56.3368*1.25
* = 70.4210EUR
*/
// 56.3368GBP is swapped in for 53.3322EUR
BEAST_EXPECT(amm.expectBalances(
STAmount{GBP, UINT64_C(1'056'336842105263), -12},
STAmount{EUR, UINT64_C(946'6677295918366), -13},
amm.tokens()));
}
else
{
// alice_ buys 53.3322EUR with 56.3368GBP via the amm
// and pays 25% tr fee on 56.3368GBP
// 1,400 - 56.3368*1.25 = 1400 - 70.4210 = 1329.5789GBP
BEAST_EXPECT(
expectHolding(env, alice_, STAmount{GBP, UINT64_C(1'329'57894736842), -11}));
/**
* / 25% on 56.3368EUR is paid in tr fee 56.3368*1.25
* = 70.4210EUR
*/
// 56.3368GBP is swapped in for 53.3322EUR
BEAST_EXPECT(amm.expectBalances(
STAmount{GBP, UINT64_C(1'056'336842105264), -12},
STAmount{EUR, UINT64_C(946'6677295918366), -13},
amm.tokens()));
}
// alice_ buys 53.3322EUR with 56.3368GBP via the amm
// and pays 25% tr fee on 56.3368GBP
// 1,400 - 56.3368*1.25 = 1400 - 70.4210 = 1329.5789GBP
BEAST_EXPECT(
expectHolding(env, alice_, STAmount{GBP, UINT64_C(1'329'57894736842), -11}));
/**
* / 25% on 56.3368EUR is paid in tr fee 56.3368*1.25
* = 70.4210EUR
*/
// 56.3368GBP is swapped in for 53.3322EUR
BEAST_EXPECT(amm.expectBalances(
STAmount{GBP, UINT64_C(1'056'336842105264), -12},
STAmount{EUR, UINT64_C(946'6677295918366), -13},
amm.tokens()));
// 25% on 42.6658EUR is paid in tr fee 42.6658*1.25 = 53.3322EUR
// 42.6658EUR/59.7321USD
BEAST_EXPECT(expectHolding(
@@ -2566,44 +2441,22 @@ private:
Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
env.close();
if (!features[fixAMMv1_1])
{
// alice_ buys 53.3322EUR with 107.5308GBP
// 25% on 86.0246GBP is paid in tr fee
// 1,400 - 86.0246*1.25 = 1400 - 107.5308 = 1229.4691GBP
BEAST_EXPECT(
expectHolding(env, alice_, STAmount{GBP, UINT64_C(1'292'469135802469), -12}));
// 86.0246GBP is swapped in for 79.2106EUR
BEAST_EXPECT(amm1.expectBalances(
STAmount{GBP, UINT64_C(1'086'024691358025), -12},
STAmount{EUR, UINT64_C(920'78937795562), -11},
amm1.tokens()));
// 25% on 63.3684EUR is paid in tr fee 63.3684*1.25 = 79.2106EUR
// 63.3684EUR is swapped in for 83.4291USD
BEAST_EXPECT(amm2.expectBalances(
STAmount{EUR, UINT64_C(1'063'368497635504), -12},
STAmount{USD, UINT64_C(1'316'570881226053), -12},
amm2.tokens()));
}
else
{
// alice_ buys 53.3322EUR with 107.5308GBP
// 25% on 86.0246GBP is paid in tr fee
// 1,400 - 86.0246*1.25 = 1400 - 107.5308 = 1229.4691GBP
BEAST_EXPECT(
expectHolding(env, alice_, STAmount{GBP, UINT64_C(1'292'469135802466), -12}));
// 86.0246GBP is swapped in for 79.2106EUR
BEAST_EXPECT(amm1.expectBalances(
STAmount{GBP, UINT64_C(1'086'024691358027), -12},
STAmount{EUR, UINT64_C(920'7893779556188), -13},
amm1.tokens()));
// 25% on 63.3684EUR is paid in tr fee 63.3684*1.25 = 79.2106EUR
// 63.3684EUR is swapped in for 83.4291USD
BEAST_EXPECT(amm2.expectBalances(
STAmount{EUR, UINT64_C(1'063'368497635505), -12},
STAmount{USD, UINT64_C(1'316'570881226053), -12},
amm2.tokens()));
}
// alice_ buys 53.3322EUR with 107.5308GBP
// 25% on 86.0246GBP is paid in tr fee
// 1,400 - 86.0246*1.25 = 1400 - 107.5308 = 1229.4691GBP
BEAST_EXPECT(
expectHolding(env, alice_, STAmount{GBP, UINT64_C(1'292'469135802466), -12}));
// 86.0246GBP is swapped in for 79.2106EUR
BEAST_EXPECT(amm1.expectBalances(
STAmount{GBP, UINT64_C(1'086'024691358027), -12},
STAmount{EUR, UINT64_C(920'7893779556188), -13},
amm1.tokens()));
// 25% on 63.3684EUR is paid in tr fee 63.3684*1.25 = 79.2106EUR
// 63.3684EUR is swapped in for 83.4291USD
BEAST_EXPECT(amm2.expectBalances(
STAmount{EUR, UINT64_C(1'063'368497635505), -12},
STAmount{USD, UINT64_C(1'316'570881226053), -12},
amm2.tokens()));
// 25% on 66.7432USD is paid in tr fee 66.7432*1.25 = 83.4291USD
BEAST_EXPECT(
expectHolding(env, carol_, STAmount(USD, UINT64_C(1'466'743295019157), -12)));
@@ -2629,34 +2482,17 @@ private:
Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
env.close();
if (!features[fixAMMv1_1])
{
// 108.1481GBP is swapped in for 97.5935EUR
BEAST_EXPECT(amm1.expectBalances(
STAmount{GBP, UINT64_C(1'108'148148148149), -12},
STAmount{EUR, UINT64_C(902'4064171122988), -13},
amm1.tokens()));
// 25% on 78.0748EUR is paid in tr fee 78.0748*1.25 = 97.5935EUR
// 78.0748EUR is swapped in for 101.3888USD
BEAST_EXPECT(amm2.expectBalances(
STAmount{EUR, UINT64_C(1'078'074866310161), -12},
STAmount{USD, UINT64_C(1'298'611111111111), -12},
amm2.tokens()));
}
else
{
// 108.1481GBP is swapped in for 97.5935EUR
BEAST_EXPECT(amm1.expectBalances(
STAmount{GBP, UINT64_C(1'108'148148148151), -12},
STAmount{EUR, UINT64_C(902'4064171122975), -13},
amm1.tokens()));
// 25% on 78.0748EUR is paid in tr fee 78.0748*1.25 = 97.5935EUR
// 78.0748EUR is swapped in for 101.3888USD
BEAST_EXPECT(amm2.expectBalances(
STAmount{EUR, UINT64_C(1'078'074866310162), -12},
STAmount{USD, UINT64_C(1'298'611111111111), -12},
amm2.tokens()));
}
// 108.1481GBP is swapped in for 97.5935EUR
BEAST_EXPECT(amm1.expectBalances(
STAmount{GBP, UINT64_C(1'108'148148148151), -12},
STAmount{EUR, UINT64_C(902'4064171122975), -13},
amm1.tokens()));
// 25% on 78.0748EUR is paid in tr fee 78.0748*1.25 = 97.5935EUR
// 78.0748EUR is swapped in for 101.3888USD
BEAST_EXPECT(amm2.expectBalances(
STAmount{EUR, UINT64_C(1'078'074866310162), -12},
STAmount{USD, UINT64_C(1'298'611111111111), -12},
amm2.tokens()));
// 25% on 81.1111USD is paid in tr fee 81.1111*1.25 = 101.3888USD
BEAST_EXPECT(
expectHolding(env, carol_, STAmount{USD, UINT64_C(1'481'111111111111), -12}));
@@ -2777,14 +2613,7 @@ private:
// Alice offers to buy 1000 XRP for 1000 USD. She takes Bob's first
// offer, removes 999 more as unfunded, then hits the step limit.
env(offer(alice_, USD(1'000), XRP(1'000)));
if (!features[fixAMMv1_1])
{
env.require(Balance(alice_, STAmount{USD, UINT64_C(2'050126257867561), -15}));
}
else
{
env.require(Balance(alice_, STAmount{USD, UINT64_C(2'050125257867587), -15}));
}
env.require(Balance(alice_, STAmount{USD, UINT64_C(2'050125257867587), -15}));
env.require(Owners(alice_, 2));
env.require(Balance(bob_, USD(0)));
env.require(Owners(bob_, 1'001));
@@ -2889,31 +2718,17 @@ private:
env(offer(bob_, XRP(100), USD(100)));
env(offer(bob_, XRP(1'000), USD(100)));
AMM const ammDan(env, dan, XRP(1'000), USD(1'100));
if (!features[fixAMMv1_1])
{
env(pay(alice_, carol_, USD(10'000)),
Paths(XRP),
DeliverMin(USD(200)),
Txflags(tfPartialPayment),
Sendmax(XRP(200)));
env.require(Balance(bob_, USD(0)));
env.require(Balance(carol_, USD(200)));
BEAST_EXPECT(ammDan.expectBalances(XRP(1'100), USD(1'000), ammDan.tokens()));
}
else
{
env(pay(alice_, carol_, USD(10'000)),
Paths(XRP),
DeliverMin(USD(200)),
Txflags(tfPartialPayment),
Sendmax(XRPAmount(200'000'001)));
env.require(Balance(bob_, USD(0)));
env.require(Balance(carol_, STAmount{USD, UINT64_C(200'00000090909), -11}));
BEAST_EXPECT(ammDan.expectBalances(
XRPAmount{1'100'000'001},
STAmount{USD, UINT64_C(999'99999909091), -11},
ammDan.tokens()));
}
env(pay(alice_, carol_, USD(10'000)),
Paths(XRP),
DeliverMin(USD(200)),
Txflags(tfPartialPayment),
Sendmax(XRPAmount(200'000'001)));
env.require(Balance(bob_, USD(0)));
env.require(Balance(carol_, STAmount{USD, UINT64_C(200'00000090909), -11}));
BEAST_EXPECT(ammDan.expectBalances(
XRPAmount{1'100'000'001},
STAmount{USD, UINT64_C(999'99999909091), -11},
ammDan.tokens()));
}
}
@@ -3600,7 +3415,7 @@ private:
testFalseDry(all_);
testBookStep(all_);
testTransferRateNoOwnerFee(all_);
testTransferRateNoOwnerFee(all_ - fixAMMv1_1 - fixAMMv1_3);
testTransferRateNoOwnerFee(all_ - fixAMMv1_3);
testLimitQuality();
testXRPPathLoop();
}
@@ -3610,7 +3425,7 @@ private:
{
using namespace jtx;
testStepLimit(all_);
testStepLimit(all_ - fixAMMv1_1 - fixAMMv1_3);
testStepLimit(all_ - fixAMMv1_3);
}
void
@@ -3618,7 +3433,7 @@ private:
{
using namespace jtx;
testConvertAllOfAnAsset(all_);
testConvertAllOfAnAsset(all_ - fixAMMv1_1 - fixAMMv1_3);
testConvertAllOfAnAsset(all_ - fixAMMv1_3);
}
void
+17 -57
View File
@@ -3104,7 +3104,7 @@ private:
ammAlice.deposit(carol_, 1'000'000'000'000);
ammAlice.withdraw(
carol_, MPT(ammAlice[1])(100'000000), std::nullopt, IOUAmount{520, 0});
if (!env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3))
if (!env.enabled(fixAMMv1_3))
{
BEAST_EXPECT(
ammAlice.expectBalances(
@@ -3113,16 +3113,7 @@ private:
IOUAmount{10'153'846'15384616, -2}) &&
ammAlice.expectLPTokens(carol_, IOUAmount{153'846'15384616, -2}));
}
else if (env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3))
{
BEAST_EXPECT(
ammAlice.expectBalances(
XRP(11'000'000000),
MPT(ammAlice[1])(9372781065),
IOUAmount{10'153'846'15384616, -2}) &&
ammAlice.expectLPTokens(carol_, IOUAmount{153'846'15384616, -2}));
}
else if (env.enabled(fixAMMv1_3))
else
{
BEAST_EXPECT(
ammAlice.expectBalances(
@@ -3137,14 +3128,14 @@ private:
{{XRP(10'000'000'000), gAmmmpt(10'000'000'000)}},
0,
std::nullopt,
{all, all - fixAMMv1_3, all - fixAMMv1_1 - fixAMMv1_3});
{all, all - fixAMMv1_3});
// Withdraw with EPrice limit. AssetOut is 0.
testAMM(
[&](AMM& ammAlice, Env& env) {
ammAlice.deposit(carol_, 1'000'000'000'000);
ammAlice.withdraw(carol_, MPT(ammAlice[1])(0), std::nullopt, IOUAmount{520, 0});
if (!env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3))
if (!env.enabled(fixAMMv1_3))
{
BEAST_EXPECT(
ammAlice.expectBalances(
@@ -3153,16 +3144,7 @@ private:
IOUAmount{10'153'846'15384616, -2}) &&
ammAlice.expectLPTokens(carol_, IOUAmount{153'846'15384616, -2}));
}
else if (env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3))
{
BEAST_EXPECT(
ammAlice.expectBalances(
XRP(11'000'000000),
MPT(ammAlice[1])(9372781065),
IOUAmount{10'153'846'15384616, -2}) &&
ammAlice.expectLPTokens(carol_, IOUAmount{153'846'15384616, -2}));
}
else if (env.enabled(fixAMMv1_3))
else
{
BEAST_EXPECT(
ammAlice.expectBalances(
@@ -3177,7 +3159,7 @@ private:
{{XRP(10'000'000'000), gAmmmpt(10'000'000'000)}},
0,
std::nullopt,
{all, all - fixAMMv1_3, all - fixAMMv1_1 - fixAMMv1_3});
{all, all - fixAMMv1_3});
// IOU/MPT combination + transfer fee
{
@@ -5190,8 +5172,7 @@ private:
.flags = kMptDexFlags,
});
AMM const ammAlice(env, alice_, btc(1'000'000'000'000'000), eth(1'100'000'000'000'000));
// This offer succeeds to cross pre- and post-amendment
// because the strand's out amount is small enough to match
// This offer crosses because the strand's out amount is small enough to match
// limitQuality value and limitOut() function in StrandFlow
// doesn't require an adjustment to out value.
env(offer(carol_, eth(100'000'000'000'000), btc(100'000'000'000'000)));
@@ -6920,8 +6901,7 @@ private:
if (rates.first == kLowRate)
{
// Ed offer is partially crossed.
// The updated rounding makes limitQuality
// work if both amendments are enabled
// Rounding in favor of the AMM makes limitQuality work
BEAST_EXPECT(expectOffers(
env,
ed,
@@ -7145,20 +7125,10 @@ private:
env(offer(carol_, USD(0.49), btc(1)));
env.close();
if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
{
BEAST_EXPECT(amm.expectBalances(btc(200'000), USD(100'000), amm.tokens()));
BEAST_EXPECT(expectOffers(env, alice_, 1, {{Amounts{btc(1), USD(0.01)}}}));
BEAST_EXPECT(expectOffers(env, carol_, 1, {{Amounts{USD(0.49), btc(1)}}}));
}
if (features[fixAMMv1_1] && features[fixAMMv1_3])
{
BEAST_EXPECT(amm.expectBalances(btc(200'001), USD(99'999.51), amm.tokens()));
BEAST_EXPECT(expectOffers(env, alice_, 1, {{Amounts{btc(1), USD(0.01)}}}));
// Carol's offer crosses AMM
BEAST_EXPECT(expectOffers(env, carol_, 0));
}
BEAST_EXPECT(amm.expectBalances(btc(200'001), USD(99'999.51), amm.tokens()));
BEAST_EXPECT(expectOffers(env, alice_, 1, {{Amounts{btc(1), USD(0.01)}}}));
// Carol's offer crosses AMM
BEAST_EXPECT(expectOffers(env, carol_, 0));
}
// XRP/MPT crosses AMM despite of low quality LOB
@@ -7180,20 +7150,10 @@ private:
env(offer(carol_, XRP(0.49), btc(1)));
env.close();
if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
{
BEAST_EXPECT(amm.expectBalances(btc(200'000), XRP(100'000), amm.tokens()));
BEAST_EXPECT(expectOffers(env, alice_, 1, {{Amounts{btc(1), XRP(0.01)}}}));
BEAST_EXPECT(expectOffers(env, carol_, 1, {{Amounts{XRP(0.49), btc(1)}}}));
}
if (features[fixAMMv1_1] && features[fixAMMv1_3])
{
BEAST_EXPECT(amm.expectBalances(btc(200'001), XRP(99'999.51), amm.tokens()));
BEAST_EXPECT(expectOffers(env, alice_, 1, {{Amounts{btc(1), XRP(0.01)}}}));
// Carol's offer crosses AMM
BEAST_EXPECT(expectOffers(env, carol_, 0));
}
BEAST_EXPECT(amm.expectBalances(btc(200'001), XRP(99'999.51), amm.tokens()));
BEAST_EXPECT(expectOffers(env, alice_, 1, {{Amounts{btc(1), XRP(0.01)}}}));
// Carol's offer crosses AMM
BEAST_EXPECT(expectOffers(env, carol_, 0));
}
}
@@ -7743,7 +7703,7 @@ private:
testAMMID();
testSelection(all);
testMalformed();
testFixAMMOfferBlockedByLOB(all - fixAMMv1_1 - fixAMMv1_3);
testFixAMMOfferBlockedByLOB(all - fixAMMv1_3);
testFixAMMOfferBlockedByLOB(all);
testLPTokenBalance(all);
testLPTokenBalance(all - fixAMMv1_3);
+449 -1069
View File
File diff suppressed because it is too large. Load diff
+2
View File
@@ -3309,6 +3309,8 @@ class Batch_test : public beast::unit_test::Suite
{
testcase("loan");
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
features = features - featureLendingProtocolV1_2;
bool const lendingBatchEnabled = features[featureLendingProtocolV1_2];
using namespace test::jtx;
Loaded 100 of 191 files, more files were not shown because too many files have changed in this diff. Show more