Compare commits

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

The operators could never be constant-evaluated, because data() uses
reinterpret_cast, so memcmp loses nothing.
2026-10-14 12:58:27 -04:00
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
BartandBart c2478f509c refactor: Open the acquisition seams its tests need (#8081)
Co-authored-by: Bart <[email protected]>
2026-10-13 03:51:12 +00:00
Mayukha Vadari c908ed7839 fix: Reject malformed conditions in EscrowFinish preflight (#8523) 2026-10-12 20:48:55 +00:00
BartandBart fc57311c81 refactor: Expose a SHAMapAddNode verdict as counts (#8080)
Co-authored-by: Bart <[email protected]>
2026-10-12 20:02:36 +00:00
176 changed files with 18253 additions and 3275 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
+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
+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",
]
+1 -1
View File
@@ -565,7 +565,7 @@ template <class Adaptor>
class Consensus
{
public:
Consensus(ClockType, Adaptor &, beast::journal);
Consensus(clock_type, Adaptor &, beast::journal);
// Kick-off the next round of consensus.
void startRound(
+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,
+4 -11
View File
@@ -596,23 +596,16 @@ operator<=>(BaseUInt<Bits, Tag> const& lhs, BaseUInt<Bits, Tag> const& rhs)
// note that the underlying data is stored in big endian, even if the
// platform is little endian. This makes the comparison correct.
//
// FIXME: use std::lexicographical_compare_three_way once support is
// added to MacOS.
auto const ret = std::mismatch(lhs.cbegin(), lhs.cend(), rhs.cbegin());
// a == b
if (ret.first == lhs.cend())
return std::strong_ordering::equivalent;
return (*ret.first > *ret.second) ? std::strong_ordering::greater : std::strong_ordering::less;
// memcmp compares unsigned bytes in order, and is much faster than a
// byte-at-a-time loop.
return std::memcmp(lhs.data(), rhs.data(), lhs.size()) <=> 0;
}
template <std::size_t Bits, typename Tag>
[[nodiscard]] constexpr bool
operator==(BaseUInt<Bits, Tag> const& lhs, BaseUInt<Bits, Tag> const& rhs)
{
return (lhs <=> rhs) == 0; // NOLINT(modernize-use-nullptr)
return std::memcmp(lhs.data(), rhs.data(), lhs.size()) == 0;
}
//------------------------------------------------------------------------------
+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
+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
+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.
+15
View File
@@ -766,6 +766,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
+120 -31
View File
@@ -14,35 +14,112 @@ private:
public:
SHAMapAddNode();
/**
* Record one node that was rejected.
*/
void
incInvalid();
/**
* Record one node that produced a good result.
*/
void
incUseful();
/**
* Record one node that was not needed: the map already held it, or the map
* or its acquisition was no longer taking nodes. isGood() counts it on the
* accepted side.
*/
void
incDuplicate();
void
reset();
/**
* @return How many nodes produced a good result.
*/
[[nodiscard]] int
getGood() const;
/**
* @return How many nodes this code rejected, which is not how many the
* batch was given: a batch that stops at its first bad node counts
* one, and a batch that carries on counts each.
*/
[[nodiscard]] int
getBad() const;
/**
* @return How many nodes were not needed, whether already held or offered
* to a map no longer taking nodes, tallied apart from the good and
* bad counts.
*/
[[nodiscard]] int
getDuplicate() const;
/**
* Whether nodes that produced a good result or were not needed outnumber
* the ones rejected.
*
* @return Whether the batch was good.
*/
[[nodiscard]] bool
isGood() const;
/**
* @return Whether at least one node in the batch was rejected.
*/
[[nodiscard]] bool
isInvalid() const;
/**
* @return Whether at least one node in the batch produced a good result.
*/
[[nodiscard]] bool
isUseful() const;
/**
* @return A verdict recording one node that was not needed.
*/
static SHAMapAddNode
duplicate();
/**
* @return A verdict recording one useful node.
*/
static SHAMapAddNode
useful();
/**
* @return A verdict recording one invalid node.
*/
static SHAMapAddNode
invalid();
/**
* Clear every count back to zero.
*/
void
reset();
/**
* Render the tally as a log line.
*
* @return The tally, e.g. "good:2 bad:1 dupe:1", or "no nodes processed" if
* every count is zero.
*/
[[nodiscard]] std::string
get() const;
/**
* Add another verdict's counts into this one.
*
* @param n The verdict to add.
* @return This verdict, updated.
*/
SHAMapAddNode&
operator+=(SHAMapAddNode const& n);
static SHAMapAddNode
duplicate();
static SHAMapAddNode
useful();
static SHAMapAddNode
invalid();
private:
SHAMapAddNode(int good, int bad, int duplicate);
};
@@ -74,18 +151,30 @@ SHAMapAddNode::incDuplicate()
++duplicate_;
}
inline void
SHAMapAddNode::reset()
{
good_ = bad_ = duplicate_ = 0;
}
inline int
SHAMapAddNode::getGood() const
{
return good_;
}
inline int
SHAMapAddNode::getBad() const
{
return bad_;
}
inline int
SHAMapAddNode::getDuplicate() const
{
return duplicate_;
}
inline bool
SHAMapAddNode::isGood() const
{
return (good_ + duplicate_) > bad_;
}
inline bool
SHAMapAddNode::isInvalid() const
{
@@ -98,22 +187,6 @@ SHAMapAddNode::isUseful() const
return good_ > 0;
}
inline SHAMapAddNode&
SHAMapAddNode::operator+=(SHAMapAddNode const& n)
{
good_ += n.good_;
bad_ += n.bad_;
duplicate_ += n.duplicate_;
return *this;
}
inline bool
SHAMapAddNode::isGood() const
{
return (good_ + duplicate_) > bad_;
}
inline SHAMapAddNode
SHAMapAddNode::duplicate()
{
@@ -132,6 +205,12 @@ SHAMapAddNode::invalid()
return SHAMapAddNode(0, 1, 0);
}
inline void
SHAMapAddNode::reset()
{
good_ = bad_ = duplicate_ = 0;
}
inline std::string
SHAMapAddNode::get() const
{
@@ -160,4 +239,14 @@ SHAMapAddNode::get() const
return ret;
}
inline SHAMapAddNode&
SHAMapAddNode::operator+=(SHAMapAddNode const& n)
{
good_ += n.good_;
bad_ += n.bad_;
duplicate_ += n.duplicate_;
return *this;
}
} // namespace xrpl
+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);
}
+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;
+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
+3 -11
View File
@@ -973,19 +973,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
}
+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;
+83 -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
+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
+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
+207 -78
View File
@@ -63,6 +63,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 +411,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,
@@ -585,6 +652,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 +683,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 +713,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 +778,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 +799,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 +857,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 +1023,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 +1129,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 +1195,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;
@@ -1205,33 +1356,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 +1398,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),
@@ -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};
@@ -95,20 +95,40 @@ EscrowFinish::preflightSigValidated(PreflightContext const& ctx)
auto& router = ctx.registry.get().getHashRouter();
auto const id = ctx.tx.getTransactionID();
auto const flags = router.getFlags(id);
auto flags = router.getFlags(id);
// If we haven't checked the condition, check it
// now. Whether it passes or not isn't important
// in preflight.
// now. Other than a malformed condition, whether
// it passes or not isn't important in preflight.
if (!any(flags & (kSfCfInvalid | kSfCfValid)))
{
if (checkCondition(*fb, *cb))
{
router.setFlags(id, kSfCfValid);
flags = kSfCfValid;
}
else
{
router.setFlags(id, kSfCfInvalid);
flags = kSfCfInvalid;
}
}
// The invalid flag also covers a bad fulfillment, so
// check the condition itself. This runs whether or not
// the flag was cached, so the result is the same on
// every node.
if (any(flags & kSfCfInvalid) && ctx.rules.enabled(fixCleanup3_5_0))
{
using namespace xrpl::cryptoconditions;
std::error_code ec;
auto const condition = Condition::deserialize(*cb, ec);
if (!condition)
{
JLOG(ctx.j.debug()) << "Malformed condition during escrow finish: " << ec.message();
return temMALFORMED;
}
}
}
@@ -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);
@@ -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];
}
@@ -384,10 +384,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 +410,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 +453,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 +492,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 +521,17 @@ VaultWithdraw::doApply()
<< " assetsAvailable=" << allAvailable.getText();
}
assetsWithdrawn = allAvailable;
// Do not let dust accumulate in the Vault.
assetsTotal = 0;
assetsAvailable = 0;
}
else
if (finalPayout && getVaultVersion(vault) != VaultVersion::FixedPrecision)
{
// Debit both rails by the same delta so sfAssetsTotal and sfAssetsAvailable stay in step,
// as required by the ValidVault invariant.
assetsTotal -= assetsWithdrawn;
assetsAvailable -= assetsWithdrawn;
// 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);
-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;
+11 -3
View File
@@ -579,10 +579,17 @@ struct Credentials_test : public beast::unit_test::Suite
if (!BEAST_EXPECT(sleVault))
return;
Account const vaultPseudo{"vault", sleVault->at(sfAccount)};
auto const expectedResult =
features[fixCleanup3_3_0] ? Ter(tecPSEUDO_ACCOUNT) : Ter(tesSUCCESS);
// Without the CredentialCreate guard, the ownership invariant
// is the backstop that rejects the pin.
auto const expectedResult = [&] -> TER {
if (features[fixCleanup3_3_0])
return tecPSEUDO_ACCOUNT;
if (features[fixCleanup3_5_0])
return tecINVARIANT_FAILED;
return tesSUCCESS;
}();
env(credentials::create(vaultPseudo, issuer, credType), expectedResult);
env(credentials::create(vaultPseudo, issuer, credType), Ter(expectedResult));
env.close();
}
}
@@ -1175,6 +1182,7 @@ struct Credentials_test : public beast::unit_test::Suite
testCreateFailed(all);
testCreateFailed(all - fixDirectoryLimit);
testCreateFailed(all - fixCleanup3_3_0);
testCreateFailed(all - fixCleanup3_3_0 - fixCleanup3_5_0);
testAcceptFailed(all);
testDeleteFailed(all);
testFeatureFailed(all - featureCredentials);
+49 -9
View File
@@ -941,41 +941,43 @@ struct Escrow_test : public beast::unit_test::Suite
// Now, try to fulfill using the same sequence of
// malformed conditions.
TER const malformedCondTer =
features[fixCleanup3_5_0] ? TER{temMALFORMED} : TER{tecCRYPTOCONDITION_ERROR};
env(escrow::finish("bob", "alice", seq),
escrow::kCondition(Slice{cp, cs}),
escrow::kFulfillment(Slice{fp, fs}),
Fee(150 * baseFee),
Ter(tecCRYPTOCONDITION_ERROR));
Ter(malformedCondTer));
env(escrow::finish("bob", "alice", seq),
escrow::kCondition(Slice{cp, cs - 1}),
escrow::kFulfillment(Slice{fp, fs}),
Fee(150 * baseFee),
Ter(tecCRYPTOCONDITION_ERROR));
Ter(malformedCondTer));
env(escrow::finish("bob", "alice", seq),
escrow::kCondition(Slice{cp, cs - 2}),
escrow::kFulfillment(Slice{fp, fs}),
Fee(150 * baseFee),
Ter(tecCRYPTOCONDITION_ERROR));
Ter(malformedCondTer));
env(escrow::finish("bob", "alice", seq),
escrow::kCondition(Slice{cp + 1, cs - 1}),
escrow::kFulfillment(Slice{fp, fs}),
Fee(150 * baseFee),
Ter(tecCRYPTOCONDITION_ERROR));
Ter(malformedCondTer));
env(escrow::finish("bob", "alice", seq),
escrow::kCondition(Slice{cp + 1, cs - 3}),
escrow::kFulfillment(Slice{fp, fs}),
Fee(150 * baseFee),
Ter(tecCRYPTOCONDITION_ERROR));
Ter(malformedCondTer));
env(escrow::finish("bob", "alice", seq),
escrow::kCondition(Slice{cp + 2, cs - 2}),
escrow::kFulfillment(Slice{fp, fs}),
Fee(150 * baseFee),
Ter(tecCRYPTOCONDITION_ERROR));
Ter(malformedCondTer));
env(escrow::finish("bob", "alice", seq),
escrow::kCondition(Slice{cp + 2, cs - 3}),
escrow::kFulfillment(Slice{fp, fs}),
Fee(150 * baseFee),
Ter(tecCRYPTOCONDITION_ERROR));
Ter(malformedCondTer));
// Now, using the correct kCondition, try malformed fulfillments:
env(escrow::finish("bob", "alice", seq),
@@ -1043,11 +1045,13 @@ struct Escrow_test : public beast::unit_test::Suite
escrow::kCondition(escrow::kCb3),
escrow::kCancelTime(env.now() + 1s));
TER const emptyCondTer =
features[fixCleanup3_5_0] ? TER{temMALFORMED} : TER{tecCRYPTOCONDITION_ERROR};
env(escrow::finish("bob", "alice", seq),
escrow::kCondition(Slice{}),
escrow::kFulfillment(Slice{}),
Fee(150 * baseFee),
Ter(tecCRYPTOCONDITION_ERROR));
Ter(emptyCondTer));
env(escrow::finish("bob", "alice", seq),
escrow::kCondition(escrow::kCb3),
escrow::kFulfillment(Slice{}),
@@ -1057,7 +1061,7 @@ struct Escrow_test : public beast::unit_test::Suite
escrow::kCondition(Slice{}),
escrow::kFulfillment(escrow::kFb3),
Fee(150 * baseFee),
Ter(tecCRYPTOCONDITION_ERROR));
Ter(emptyCondTer));
// Assemble finish that is missing the Condition or the Fulfillment
// since either both must be present, or neither can:
@@ -1097,6 +1101,41 @@ struct Escrow_test : public beast::unit_test::Suite
escrow::kCancelTime(env.now() + 1s),
Ter(temMALFORMED));
}
{ // Test finish with a malformed condition
Env env(*this, features);
env.fund(XRP(5000), "alice", "bob", "carol");
env.close();
auto const seq = env.seq("alice");
env(escrow::create("alice", "carol", XRP(1000)),
escrow::kCondition(escrow::kCb1),
escrow::kCancelTime(env.now() + 100s));
env.close();
// Does not deserialize as a condition.
std::array<std::uint8_t, 3> const malformed = {{0x78, 0x78, 0x78}};
auto const baseFee = env.current()->fees().base;
auto const bobBalance = env.balance("bob");
bool const fixEnabled = features[fixCleanup3_5_0];
auto const jt = env.jt(
escrow::finish("bob", "alice", seq),
escrow::kCondition(malformed),
escrow::kFulfillment(escrow::kFb1),
Fee(150 * baseFee));
env(jt, Ter(fixEnabled ? TER{temMALFORMED} : TER{tecCRYPTOCONDITION_ERROR}));
// Run preflight on the same transaction again, now with the
// HashRouter flag cached. The result must not change.
auto const pf =
preflight(env.app(), env.current()->rules(), *jt.stx, TapNone, env.journal);
BEAST_EXPECT(pf.ter == (fixEnabled ? TER{temMALFORMED} : TER{tesSUCCESS}));
env.close();
// Before the fix, the transaction is included and charges a fee.
env.require(
Balance("bob", fixEnabled ? bobBalance : bobBalance - drops(150 * baseFee)));
BEAST_EXPECT(env.le(keylet::escrow(Account("alice").id(), SeqProxy::rawSequence(seq))));
}
}
void
@@ -1642,6 +1681,7 @@ public:
FeatureBitset const all{testableAmendments()};
testWithFeats(all);
testWithFeats(all - featureTokenEscrow);
testWithFeats(all - fixCleanup3_5_0);
testTags(all - fixIncludeKeyletFields);
}
};
+488 -35
View File
@@ -41,9 +41,11 @@
#include <xrpl/tx/paths/detail/Steps.h>
#include <cstdint>
#include <initializer_list>
#include <optional>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
namespace xrpl::test {
@@ -793,6 +795,426 @@ struct FlowMPT_test : public beast::unit_test::Suite
test(maxRepresentable + 1, kMaxMpTokenAmount, "limiting");
}
void
testBookStepInputAboveMaximumAmount(FeatureBitset features)
{
// OfferCreate doesn't compare TakerPays with the MPT's MaximumAmount,
// so a bid for more than the cap rests. Taking it has the issuer send
// TakerPays to the owner, and isMPTOverflow() rejects a single send
// above the cap. BookStep now limits the offer's input to the cap so
// the bid fills partially, like an underfunded offer, instead of
// throwing out of consumeOffer() and drying the strand (which left the
// bid on the book to dead-end every payment that crossed it).
//
// XRP -> x -> y. carol asks the whole 1,000 x supply for XRP(1,000);
// mallory bids x for 2,000 y; alice pays dave y.
testcase("BookStep input above MaximumAmount");
using namespace jtx;
Account const gwX("gwX");
Account const gwY("gwY");
Account const alice("alice");
Account const carol("carol");
Account const mallory("mallory");
Account const dave("dave");
std::int64_t constexpr cap = 1'000;
std::int64_t constexpr bidGets = 2'000;
// A bid above the cap at a non-unit price.
std::int64_t constexpr unevenBidPays = (3 * cap) + 100;
auto const test = [&](Account const& bidder,
std::int64_t bidPays,
std::int64_t deliver,
bool partial,
std::int64_t delivered,
std::optional<TER> const& ter = std::nullopt) {
Env env(*this, features);
env.fund(XRP(100'000), gwX, gwY, alice, carol, mallory, dave);
env.close();
MPT const x = MPTTester(
{.env = env, .issuer = gwX, .holders = {carol, mallory, dave}, .maxAmt = cap});
MPT const y = MPTTester({.env = env, .issuer = gwY, .holders = {mallory, dave, gwX}});
env(pay(gwX, carol, x(cap)));
env(pay(gwY, bidder, y(bidGets)));
env.close();
env(offer(carol, XRP(cap), x(cap)));
auto const bidSeq = env.seq(bidder);
env(offer(bidder, x(bidPays), y(bidGets)));
env.close();
auto const bidKeylet = keylet::offer(bidder.id(), SeqProxy::rawSequence(bidSeq));
BEAST_EXPECT(env.le(bidKeylet) != nullptr);
auto const flags = partial ? tfNoRippleDirect | tfPartialPayment : tfNoRippleDirect;
env(pay(alice, dave, y(deliver)),
Path(~x, ~y),
Sendmax(XRP(5'000)),
Txflags(flags),
ter ? Ter(*ter) : Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(env.balance(dave, y) == y(delivered));
// Each x delivered through the bid costs bidGets/bidPays y.
std::int64_t const xTaken = delivered * bidPays / bidGets;
if (bidder != gwX)
BEAST_EXPECT(env.balance(bidder, x) == x(xTaken));
BEAST_EXPECT(env.balance(carol, x) == x(cap - xTaken));
auto const sle = env.le(bidKeylet);
if (xTaken == bidPays)
{
BEAST_EXPECT(!sle);
}
else if (BEAST_EXPECT(sle))
{
BEAST_EXPECT((*sle)[sfTakerPays] == x(bidPays - xTaken));
BEAST_EXPECT((*sle)[sfTakerGets] == y(bidGets - delivered));
}
auto const issuance = env.le(keylet::mptokenIssuance(x.mpt()));
if (BEAST_EXPECT(issuance))
{
// x taken by the issuer's own bid is redeemed.
std::int64_t const outstanding = bidder == gwX ? cap - xTaken : cap;
BEAST_EXPECT(
(*issuance)[sfOutstandingAmount] == static_cast<std::uint64_t>(outstanding));
}
};
// Bid above the cap: the input is limited to the cap, so at most
// 1,000 x (= 1,000 y at this bid's price) can flow through it.
test(mallory, 2 * cap, 1'001, true, cap);
test(mallory, 2 * cap, bidGets, true, cap);
// Not partial: the strand no longer throws, but 1,001 y can't be
// delivered.
test(mallory, 2 * cap, 1'001, false, 0, tecPATH_PARTIAL);
// Controls: a request within the cap, and a bid at the cap.
test(mallory, 2 * cap, 500, true, 500);
test(mallory, cap, bidGets, true, bidGets);
// The issuer's own bid above the cap isn't limited: the issuer's send
// to itself is a no-op, so upstream supply bounds the fill as usual.
test(gwX, 2 * cap, 1'001, true, cap);
// A bid above the cap at a non-unit price, crossed repeatedly. Each
// crossing is limited to the cap and the output is rounded down, so
// the remainder's quality is no worse than the original's. The
// remainder stays on the book and is eventually consumed.
{
Env env(*this, features);
env.fund(XRP(100'000), gwX, gwY, alice, carol, mallory, dave);
env.close();
MPT const x = MPTTester(
{.env = env, .issuer = gwX, .holders = {carol, mallory, dave}, .maxAmt = cap});
MPT const y = MPTTester({.env = env, .issuer = gwY, .holders = {mallory, dave}});
env(pay(gwX, carol, x(cap)));
env(pay(gwY, mallory, y(bidGets)));
env.close();
auto const bidSeq = env.seq(mallory);
env(offer(mallory, x(unevenBidPays), y(bidGets)));
env.close();
auto const bidKeylet = keylet::offer(mallory.id(), SeqProxy::rawSequence(bidSeq));
// {x taken, y delivered} per round; the first three are capped.
// 1,000 x buys floor(1,000 * gets / pays) y at the remainder's
// price: 2,000/3,100, 1,355/2,100, 710/1,100. The last 100 x buy
// the remaining 65 y and consume the bid.
std::int64_t totalY = 0;
std::int64_t bidPays = unevenBidPays;
std::int64_t bidGetsLeft = bidGets;
for (auto const& [xTaken, yDelivered] :
std::initializer_list<std::pair<std::int64_t, std::int64_t>>{
{cap, 645},
{cap, 645},
{cap, 645},
{100, 65},
})
{
auto const askSeq = env.seq(carol);
env(offer(carol, XRP(cap), x(cap)));
env.close();
env(pay(alice, dave, y(1'001)),
Path(~x, ~y),
Sendmax(XRP(5'000)),
Txflags(tfNoRippleDirect | tfPartialPayment));
env.close();
totalY += yDelivered;
bidPays -= xTaken;
bidGetsLeft -= yDelivered;
BEAST_EXPECT(env.balance(dave, y) == y(totalY));
BEAST_EXPECT(env.balance(mallory, x) == x(xTaken));
BEAST_EXPECT(env.balance(carol, x) == x(cap - xTaken));
auto const sle = env.le(bidKeylet);
if (bidPays == 0)
{
BEAST_EXPECT(!sle);
}
else if (BEAST_EXPECT(sle))
{
BEAST_EXPECT((*sle)[sfTakerPays] == x(bidPays));
BEAST_EXPECT((*sle)[sfTakerGets] == y(bidGetsLeft));
// The remainder's quality is no worse than the original's.
BEAST_EXPECT(bidGetsLeft * unevenBidPays >= bidGets * bidPays);
}
// Return x to carol for the next round, and cancel her unfilled
// ask remainder.
env(pay(mallory, carol, x(xTaken)));
env(offerCancel(carol, askSeq));
env.close();
}
BEAST_EXPECT(totalY == bidGets);
}
// A transfer fee on x: the cap is grossed up by the rate when limiting
// the taker's input, and the owner receives exactly the cap. The
// previous step must redeem for the fee to apply, so alice pays with
// x directly (MPTEndpoint alice -> gwX, then the x/y book). alice can
// hold at most the cap, so upstream still bounds the fill: she pays
// 1,000 gross, mallory receives floor(1,000 / 1.1) = 909 x, dave gets
// floor(909 * 2,000 / 3,100) = 586 y.
{
Env env(*this, features);
env.fund(XRP(100'000), gwX, gwY, alice, mallory, dave);
env.close();
MPT const x = MPTTester({
.env = env,
.issuer = gwX,
.holders = {alice, mallory, dave},
.transferFee = 10'000,
.maxAmt = cap,
});
MPT const y = MPTTester({.env = env, .issuer = gwY, .holders = {mallory, dave}});
env(pay(gwX, alice, x(cap)));
env(pay(gwY, mallory, y(bidGets)));
env.close();
auto const bidSeq = env.seq(mallory);
env(offer(mallory, x(unevenBidPays), y(bidGets)));
env.close();
auto const bidKeylet = keylet::offer(mallory.id(), SeqProxy::rawSequence(bidSeq));
env(pay(alice, dave, y(1'001)),
Path(~y),
Sendmax(x(cap)),
Txflags(tfNoRippleDirect | tfPartialPayment));
env.close();
BEAST_EXPECT(env.balance(dave, y) == y(586));
BEAST_EXPECT(env.balance(mallory, x) == x(909));
BEAST_EXPECT(env.balance(alice, x) == x(0));
auto const sle = env.le(bidKeylet);
if (BEAST_EXPECT(sle))
{
BEAST_EXPECT((*sle)[sfTakerPays] == x(unevenBidPays - 909));
BEAST_EXPECT((*sle)[sfTakerGets] == y(bidGets - 586));
}
auto const issuance = env.le(keylet::mptokenIssuance(x.mpt()));
if (BEAST_EXPECT(issuance))
BEAST_EXPECT((*issuance)[sfOutstandingAmount] == 909);
}
// Two bids above the cap at the same quality, mallory's ahead of
// bob's. Stopping after a cap-limited bid doesn't strand bob's: the
// step's input is already at the cap, which is all carol can supply,
// so delivery equals the single-bid case. If mallory's bid is
// underfunded below the cap instead, it isn't cap-limited: it is
// consumed, removed, and bob's bid is crossed in the same pass.
auto const twoBids = [&](std::int64_t malloryY,
std::int64_t delivered,
std::int64_t malloryX,
std::int64_t malloryYSold,
std::int64_t bobX,
std::int64_t bobYSold,
std::optional<std::pair<std::int64_t, std::int64_t>> malloryBid) {
Env env(*this, features);
Account const bob("bob");
env.fund(XRP(100'000), gwX, gwY, alice, bob, carol, mallory, dave);
env.close();
MPT const x = MPTTester(
{.env = env, .issuer = gwX, .holders = {bob, carol, mallory, dave}, .maxAmt = cap});
MPT const y = MPTTester({.env = env, .issuer = gwY, .holders = {bob, mallory, dave}});
env(pay(gwX, carol, x(cap)));
env(pay(gwY, mallory, y(malloryY)));
env(pay(gwY, bob, y(bidGets)));
env.close();
env(offer(carol, XRP(cap), x(cap)));
// Close between the bids: within one ledger, transactions apply in
// canonical order, and mallory's bid must be ahead of bob's.
auto const mallorySeq = env.seq(mallory);
env(offer(mallory, x(unevenBidPays), y(bidGets)));
env.close();
auto const bobSeq = env.seq(bob);
env(offer(bob, x(unevenBidPays), y(bidGets)));
env.close();
env(pay(alice, dave, y(bidGets)),
Path(~x, ~y),
Sendmax(XRP(5'000)),
Txflags(tfNoRippleDirect | tfPartialPayment));
env.close();
BEAST_EXPECT(env.balance(dave, y) == y(delivered));
BEAST_EXPECT(env.balance(mallory, x) == x(malloryX));
BEAST_EXPECT(env.balance(mallory, y) == y(malloryY - malloryYSold));
BEAST_EXPECT(env.balance(bob, x) == x(bobX));
BEAST_EXPECT(env.balance(bob, y) == y(bidGets - bobYSold));
BEAST_EXPECT(env.balance(carol, x) == x(cap - malloryX - bobX));
auto const mallorySle =
env.le(keylet::offer(mallory.id(), SeqProxy::rawSequence(mallorySeq)));
if (!malloryBid)
{
BEAST_EXPECT(!mallorySle);
}
else if (BEAST_EXPECT(mallorySle))
{
BEAST_EXPECT((*mallorySle)[sfTakerPays] == x(malloryBid->first));
BEAST_EXPECT((*mallorySle)[sfTakerGets] == y(malloryBid->second));
}
auto const bobSle = env.le(keylet::offer(bob.id(), SeqProxy::rawSequence(bobSeq)));
if (BEAST_EXPECT(bobSle))
{
BEAST_EXPECT((*bobSle)[sfTakerPays] == x(unevenBidPays - bobX));
BEAST_EXPECT((*bobSle)[sfTakerGets] == y(bidGets - bobYSold));
}
auto const issuance = env.le(keylet::mptokenIssuance(x.mpt()));
if (BEAST_EXPECT(issuance))
BEAST_EXPECT((*issuance)[sfOutstandingAmount] == static_cast<std::uint64_t>(cap));
};
// Both funded: mallory's bid takes the cap for 645 y and rests at
// 2,100/1,355; bob's is untouched.
twoBids(bidGets, 645, cap, 645, 0, 0, std::pair{unevenBidPays - cap, bidGets - 645});
// mallory holds 500 y: funds limit her bid to 775 x, under the cap, so
// it is consumed and removed; bob's bid takes the remaining 225 x for
// 145 y in the same pass. Same 645 y total.
twoBids(500, 645, 775, 500, 225, 145, std::nullopt);
}
void
testBookStepInputAboveMaximumAmountWithFee(FeatureBitset features)
{
// BookStep limits an MPT input to MaximumAmount (see maxIn_) and
// grosses up the transfer fee on no more than the cap. Grossing up
// the bid's whole TakerPays can overflow even though a fill at the
// cap fits, and the overflow used to remove the bid. OfferCreate
// doesn't place a bid whose grossed-up TakerPays overflows, but one
// can rest at a lower fee before the issuer raises it.
//
// mallory bids for 9e18 x, above x's 8e18 cap, while x has no fee.
// gwX then raises the fee. alice pays x directly, so the step redeems
// and the fee applies.
testcase("BookStep input above MaximumAmount with transfer fee");
using namespace jtx;
Account const gwX("gwX");
Account const gwY("gwY");
Account const alice("alice");
Account const mallory("mallory");
Account const dave("dave");
std::int64_t constexpr cap = 8'000'000'000'000'000'000LL;
std::int64_t constexpr bid = 9'000'000'000'000'000'000LL;
std::int64_t constexpr deliver = 1'000;
auto const test = [&](std::uint16_t fee, bool fills) {
Env env(*this, features);
env.fund(XRP(100'000), gwX, gwY, alice, mallory, dave);
env.close();
MPTTester x(
{.env = env, .issuer = gwX, .holders = {alice, mallory, dave}, .maxAmt = cap});
MPT const y = MPTTester({.env = env, .issuer = gwY, .holders = {mallory, dave}});
std::int64_t const gross = deliver + ((deliver * fee) / 100'000);
env(pay(gwX, alice, x(gross)));
env(pay(gwY, mallory, y(bid)));
env.close();
auto const bidSeq = env.seq(mallory);
env(offer(mallory, x(bid), y(bid)));
env.close();
auto const bidKeylet = keylet::offer(mallory.id(), SeqProxy::rawSequence(bidSeq));
BEAST_EXPECT(env.le(bidKeylet) != nullptr);
x.set({.account = gwX, .transferFee = fee});
env.close();
env(pay(alice, dave, y(deliver)),
Path(~y),
Sendmax(x(gross)),
Txflags(tfNoRippleDirect),
Ter(fills ? TER(tesSUCCESS) : TER(tecPATH_PARTIAL)));
env.close();
auto const sle = env.le(bidKeylet);
if (fills)
{
BEAST_EXPECT(env.balance(dave, y) == y(deliver));
BEAST_EXPECT(env.balance(mallory, x) == x(deliver));
BEAST_EXPECT(env.balance(alice, x) == x(0));
// The remainder is still funded and stays on the book.
if (BEAST_EXPECT(sle))
{
BEAST_EXPECT((*sle)[sfTakerPays] == x(bid - deliver));
BEAST_EXPECT((*sle)[sfTakerGets] == y(bid - deliver));
}
}
else
{
// Even the cap overflows once grossed up, so BookStep removes
// the bid and nothing is delivered. The failed payment
// discards its changes, so the bid is still on the ledger.
BEAST_EXPECT(env.balance(dave, y) == y(0));
BEAST_EXPECT(env.balance(alice, x) == x(gross));
BEAST_EXPECT(sle);
}
};
// 9e18 * 1.1 overflows but 8e18 * 1.1 fits: the bid fills partially.
test(10'000, true);
// 8e18 * 1.5 overflows too, so the bid is removed as before.
test(kMaxTransferFee, false);
}
void
testBookStepMissingIssuance(FeatureBitset features)
{
// A path through a book whose MPT issuance doesn't exist. BookStep's
// constructor looks the issuance up for MaximumAmount before check()
// rejects the step, so the lookup must tolerate a missing entry.
testcase("BookStep on a missing MPT issuance");
using namespace jtx;
Env env(*this, features);
Account const gw("gw");
Account const alice("alice");
Account const bob("bob");
env.fund(XRP(10'000), gw, alice, bob);
env.close();
MPT const y = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}});
MPT const missing{"missing", makeMptID(99, gw)};
BEAST_EXPECT(!env.le(keylet::mptokenIssuance(missing.mpt())));
env(pay(alice, bob, y(10)),
Path(~missing, ~y),
Sendmax(XRP(100)),
Txflags(tfNoRippleDirect),
Ter(tecOBJECT_NOT_FOUND));
env.close();
BEAST_EXPECT(env.balance(bob, y) == y(0));
}
void
testFalseDry(FeatureBitset features)
{
@@ -1123,14 +1545,7 @@ struct FlowMPT_test : public beast::unit_test::Suite
{
auto const offer = *offerPtr;
BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
if constexpr (std::is_same_v<std::decay_t<decltype(eur)>, IOU>)
{
BEAST_EXPECT(offer[sfTakerGets] == eur(5'988));
}
else
{
BEAST_EXPECT(offer[sfTakerGets] == eur(5'989));
}
BEAST_EXPECT(offer[sfTakerGets] == eur(5'988));
BEAST_EXPECT(offer[sfTakerPays] == usd(4'990));
}
};
@@ -1169,12 +1584,10 @@ struct FlowMPT_test : public beast::unit_test::Suite
// OutstandingAmount since it doesn't know if the
// BookStep redeems or not. The BookStep then has 600EUR
// available. Consequently, the entire offer is crossed.
// Note remaining takerGets is 541 rather than 540 due to integral
// rounding. XRP has a similar result.
return TokenData<MPT, MPT>{
.gets = eur,
.pays = usd,
.remTakerGets = eur(541),
.remTakerGets = eur(540),
.remTakerPays = usd(450),
};
};
@@ -1186,12 +1599,10 @@ struct FlowMPT_test : public beast::unit_test::Suite
// OutstandingAmount since it doesn't know if the
// BookStep redeems or not. The BookStep then has 600EUR
// available. Consequently, the entire offer is crossed.
// Note remaining takerGets is 540.000001 rather than 540 due to
// integral rounding.
return TokenData<XrpT, MPT>{
.gets = XRP,
.pays = usd,
.remTakerGets = XRP(540.000001),
.remTakerGets = XRP(540),
.remTakerPays = usd(450),
};
};
@@ -2401,10 +2812,13 @@ struct FlowMPT_test : public beast::unit_test::Suite
test(t);
}
// Cross-currency payment with BookStep as the first step.
// BookStep limits the buy amount.
// Cross-currency payment with BookStep as the first step. BookStep
// limits the buy amount to what the issuer can still issue. That
// limits the issuer, not carol's offer: it keeps its funded
// remainder, and since the step stops there the issuer's own offer
// behind it at the same quality is not reached either.
{
auto const test = [&](int sendMax, std::uint16_t dstXRP, std::uint8_t expGwOffers) {
auto const test = [&](int sendMax) {
Env env(*this);
env.fund(XRP(1'000), gw, alice, carol);
@@ -2420,27 +2834,63 @@ struct FlowMPT_test : public beast::unit_test::Suite
Path(~XRP),
Txflags(tfPartialPayment | tfNoRippleDirect));
BEAST_EXPECT(env.balance(alice) == XRP(dstXRP));
BEAST_EXPECT(env.balance(alice) == XRP(1'300));
BEAST_EXPECT(env.balance(gw, usd) == usd(-300));
BEAST_EXPECT(env.balance(carol, usd) == usd(300));
BEAST_EXPECT(expectOffers(env, carol, 0));
BEAST_EXPECT(expectOffers(env, gw, expGwOffers));
BEAST_EXPECT(expectOffers(env, carol, 1, {{usd(100), XRP(100)}}));
BEAST_EXPECT(expectOffers(env, gw, 1));
};
// carol's offer is partially consumed - 300USD/300XRP
// because available amount to issue is 300USD. gw's
// offer is fully consumed because it doesn't change
// OutstandingAmount. Both offers are removed from the
// order book - carol's offer is unfunded and gw's offer
// is fully consumed.
test(500, 1'400, 0);
// carol's offer is partially consumed - 300USD/300XRP
// because available amount to issue is 300USD. gw's
// offer is partially consumed because of sendMax limit.
// carol's offer is removed from the order book because
// it's unfunded. gw's offer remains on the order book
// because it's partially consumed and gw has more
// funds.
test(350, 1'350, 1);
test(500);
test(350);
}
// Same-quality offers behind the clipped one, with the issuer's own
// offer at the back. Once the issuer has nothing left to issue the
// step stops: no offer is consumed for zero, deleted, or given an
// MPToken.
{
Account const dan{"dan"};
Account const erin{"erin"};
auto const test = [&](int outstanding) {
Env env(*this);
env.fund(XRP(1'000), gw, alice, carol, dan, erin);
env.close();
MPT const usd =
MPTTester({.env = env, .issuer = gw, .holders = {carol}, .maxAmt = 300});
if (outstanding)
env(pay(gw, carol, usd(outstanding)));
env(offer(carol, usd(400), XRP(400)));
env.close();
env(offer(dan, usd(100), XRP(100)));
env.close();
env(offer(erin, usd(400), XRP(400)));
env.close();
env(offer(gw, usd(100), XRP(100)));
env.close();
int const issued = 300 - outstanding;
env(pay(gw, alice, XRP(500)),
Sendmax(usd(500)),
Path(~XRP),
Txflags(tfPartialPayment | tfNoRippleDirect),
Ter(issued ? TER{tesSUCCESS} : TER{tecPATH_DRY}));
env.close();
BEAST_EXPECT(env.balance(alice) == XRP(1'000 + issued));
BEAST_EXPECT(env.balance(gw, usd) == usd(-300));
BEAST_EXPECT(env.balance(carol, usd) == usd(300));
BEAST_EXPECT(expectOffers(env, carol, 1, {{usd(400 - issued), XRP(400 - issued)}}));
BEAST_EXPECT(expectOffers(env, dan, 1));
BEAST_EXPECT(expectOffers(env, erin, 1));
BEAST_EXPECT(expectOffers(env, gw, 1));
BEAST_EXPECT(!env.le(keylet::mptoken(usd.mpt(), dan)));
BEAST_EXPECT(!env.le(keylet::mptoken(usd.mpt(), erin)));
};
// carol is clipped to 300 and keeps 100/100
test(0);
// nothing left to issue, the step is dry
test(300);
}
}
@@ -2555,6 +3005,9 @@ struct FlowMPT_test : public beast::unit_test::Suite
testTransferRate(features);
testMPTEndpointTransferRateOverflow(features);
testMPTEndpointRipplingInputOverflow(features);
testBookStepInputAboveMaximumAmount(features);
testBookStepInputAboveMaximumAmountWithFee(features);
testBookStepMissingIssuance(features);
testSelfPayment1(features);
testSelfPayment2(features);
testSelfFundedXRPEndpoint(false, features);
+157
View File
@@ -13,6 +13,8 @@
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/insight/NullCollector.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/Ledger.h>
#include <xrpl/ledger/OpenView.h>
@@ -79,6 +81,76 @@ public:
return res;
}
void
testHashIndexInvariant()
{
testcase("LedgerHistory hash/index invariant");
using namespace jtx;
using namespace std::chrono;
Env env{*this};
LedgerHistory lh{beast::insight::NullCollector::make(), env.app()};
// Create and insert validated ledgers
auto const genesis = makeLedger({}, env, lh, 0s);
auto const ledger1 = makeLedger(genesis, env, lh, 4s);
auto const ledger2 = makeLedger(ledger1, env, lh, 4s);
auto const ledger3 = makeLedger(ledger2, env, lh, 4s);
// Insert as validated (so they go into by_index)
lh.insert(genesis, true);
lh.insert(ledger1, true);
lh.insert(ledger2, true);
lh.insert(ledger3, true);
// Verify the hash/index invariant holds
// Can retrieve by sequence and get correct hash
BEAST_EXPECT(lh.getLedgerHash(genesis->header().seq) == genesis->header().hash);
BEAST_EXPECT(lh.getLedgerHash(ledger1->header().seq) == ledger1->header().hash);
BEAST_EXPECT(lh.getLedgerHash(ledger2->header().seq) == ledger2->header().hash);
BEAST_EXPECT(lh.getLedgerHash(ledger3->header().seq) == ledger3->header().hash);
// Can retrieve by sequence and get correct ledger
auto const fetched1 = lh.getLedgerBySeq(ledger1->header().seq);
if (BEAST_EXPECT(fetched1 != nullptr))
BEAST_EXPECT(fetched1->header().hash == ledger1->header().hash);
auto const fetched2 = lh.getLedgerBySeq(ledger2->header().seq);
if (BEAST_EXPECT(fetched2 != nullptr))
BEAST_EXPECT(fetched2->header().hash == ledger2->header().hash);
// Clear ledgers prior to ledger2's sequence
lh.clearLedgerCachePrior(ledger2->header().seq);
// Verify old entries are gone from the in-memory by_index map
// Note: getLedgerHash checks by_index directly without DB fallback
BEAST_EXPECT(lh.getLedgerHash(genesis->header().seq).isZero());
BEAST_EXPECT(lh.getLedgerHash(ledger1->header().seq).isZero());
// Verify newer entries are still present in by_index
BEAST_EXPECT(lh.getLedgerHash(ledger2->header().seq) == ledger2->header().hash);
BEAST_EXPECT(lh.getLedgerHash(ledger3->header().seq) == ledger3->header().hash);
// The by_hash cache must be pruned to the same cutoff. A cache hit
// hands back the very object that was inserted, so identity tells a
// hit from a miss. (A null check would not: ledger1 is a transaction-
// free successor of genesis and therefore has the same hash as the
// Env's own ledger 2, which getLedgerByHash can reload from SQL.)
BEAST_EXPECT(lh.getLedgerByHash(ledger2->header().hash).get() == ledger2.get());
BEAST_EXPECT(lh.getLedgerByHash(ledger3->header().hash).get() == ledger3.get());
BEAST_EXPECT(lh.getLedgerByHash(ledger1->header().hash).get() != ledger1.get());
// Verify newer entries remain retrievable and consistent
// getLedgerBySeq uses by_index first, then falls back to DB if needed
auto const fetched2After = lh.getLedgerBySeq(ledger2->header().seq);
if (BEAST_EXPECT(fetched2After != nullptr))
BEAST_EXPECT(fetched2After->header().hash == ledger2->header().hash);
auto const fetched3After = lh.getLedgerBySeq(ledger3->header().seq);
if (BEAST_EXPECT(fetched3After != nullptr))
BEAST_EXPECT(fetched3After->header().hash == ledger3->header().hash);
}
void
testHandleMismatch()
{
@@ -170,12 +242,97 @@ public:
BEAST_EXPECT(found);
}
// Reverse order: validatedLedger arrives first, then builtLedger
// detects the mismatch. Covers the mismatch branch in builtLedger.
{
bool found = false;
Env env{
*this,
envconfig(),
std::make_unique<CheckMessageLogs>("MISMATCH on close time", &found)};
LedgerHistory lh{beast::insight::NullCollector::make(), env.app()};
auto const genesis = makeLedger({}, env, lh, 0s);
auto const ledgerA = makeLedger(genesis, env, lh, 4s);
auto const ledgerB = makeLedger(genesis, env, lh, 40s);
UInt256 const dummyTxHash{1};
lh.validatedLedger(ledgerB, dummyTxHash);
lh.builtLedger(ledgerA, dummyTxHash, {});
BEAST_EXPECT(found);
}
// The consensus JSON is captured inside the cache lock and handed to
// handleMismatch after it is released. The payload must survive that
// hand-off whichever side reports first: builtLedger passes its own
// argument, validatedLedger the copy stored in the cache entry.
for (bool const builtFirst : {true, false})
{
bool found = false;
// handleMismatch logs the consensus data at debug level, so raise
// the Env's log threshold (it defaults to Error) to capture it.
Env env{
*this,
envconfig(),
std::make_unique<CheckMessageLogs>("consensus-payload-marker", &found),
beast::Severity::Debug};
LedgerHistory lh{beast::insight::NullCollector::make(), env.app()};
auto const genesis = makeLedger({}, env, lh, 0s);
auto const ledgerA = makeLedger(genesis, env, lh, 4s);
auto const ledgerB = makeLedger(genesis, env, lh, 40s);
json::Value consensus{json::ValueType::Object};
consensus["marker"] = "consensus-payload-marker";
UInt256 const dummyTxHash{1};
if (builtFirst)
{
lh.builtLedger(ledgerA, dummyTxHash, consensus);
lh.validatedLedger(ledgerB, dummyTxHash);
}
else
{
lh.validatedLedger(ledgerB, dummyTxHash);
lh.builtLedger(ledgerA, dummyTxHash, consensus);
}
BEAST_EXPECT(found);
}
}
void
testFixIndex()
{
testcase("LedgerHistory fixIndex");
using namespace jtx;
using namespace std::chrono;
Env env{*this};
LedgerHistory lh{beast::insight::NullCollector::make(), env.app()};
auto const genesis = makeLedger({}, env, lh, 0s);
auto const ledger1 = makeLedger(genesis, env, lh, 4s);
lh.insert(ledger1, true);
// Unknown index: returns true, no repair.
BEAST_EXPECT(lh.fixIndex(999, ledger1->header().hash));
// Known index with the same hash: returns true, no repair.
BEAST_EXPECT(lh.fixIndex(ledger1->header().seq, ledger1->header().hash));
// Known index with a different hash: returns false and repairs.
UInt256 const bogusHash{42};
BEAST_EXPECT(!lh.fixIndex(ledger1->header().seq, bogusHash));
BEAST_EXPECT(lh.getLedgerHash(ledger1->header().seq) == bogusHash);
}
void
run() override
{
testHashIndexInvariant();
testHandleMismatch();
testFixIndex();
}
};
File diff suppressed because it is too large. Load diff
+93
View File
@@ -445,6 +445,98 @@ public:
}
}
void
testRmSmallIncreasedQOffersClampNative(FeatureBitset features)
{
testcase("Rm small increased q offers clamp native");
// Carol's partially funded offer is reduced to her funds. With
// fixCleanup3_5_0 the reduced input is clamped to the offer's input.
using namespace jtx;
auto const alice = Account{"alice"};
auto const bob = Account{"bob"};
auto const carol = Account{"carol"};
auto const dan = Account{"dan"};
auto const gw = Account{"gw"};
auto const usd = gw["USD"];
STAmount const carolPays{XRPAmount{static_cast<std::int64_t>(STAmount::kMaxNativeN)}};
STAmount const carolGets{usd.issue(), UINT64_C(9'999'999'999'999'995), -15};
STAmount const carolFunds{usd.issue(), UINT64_C(9'999'999'999'999'994), -15};
auto const setup = [&](Env& env, bool bobOffer) {
env.fund(XRP(10'000), alice, bob, carol, dan, gw);
env.close();
env.trust(usd(1'000), alice, bob, carol, dan);
env.close();
env(pay(gw, carol, carolFunds));
env(pay(gw, bob, usd(10)));
env.close();
// bob's offer is better than carol's, so it is taken first
if (bobOffer)
env(offer(bob, XRP(10), usd(10)));
env(offer(carol, carolPays, carolGets));
env.close();
env.require(Offers(bob, bobOffer ? 1 : 0), Offers(carol, 1));
};
for (auto const feat : {features, features - fixCleanup3_5_0})
{
bool const withFix = feat[fixCleanup3_5_0];
// Test payments: the payment needs more than bob's offer, so it
// reaches carol's offer.
{
Env env{*this, feat};
setup(env, true);
env(pay(alice, dan, usd(20)),
Path(~usd),
Sendmax(XRP(5'000)),
Txflags(tfPartialPayment),
Ter(withFix ? TER{tesSUCCESS} : TER{tecINTERNAL}));
env.close();
if (withFix)
{
// bob's offer is consumed and carol's is partially taken
env.require(Offers(bob, 0), Offers(carol, 1));
BEAST_EXPECT(env.balance(dan, usd).value() > usd(10).value());
BEAST_EXPECT(env.balance(carol, usd).value() < carolFunds);
}
else
{
env.require(Offers(bob, 1), Offers(carol, 1), Balance(dan, usd(0)));
env.require(Balance(carol, carolFunds));
}
}
// Test offer crossing: alice's offer crosses carol's quality
{
Env env{*this, feat};
setup(env, false);
STAmount const aliceGets{usd.issue(), 1, -7};
env(offer(alice, aliceGets, XRP(5'000)),
Ter(withFix ? TER{tesSUCCESS} : TER{tecINTERNAL}));
env.close();
env.require(Offers(carol, 1));
if (withFix)
{
env.require(Offers(alice, 0), Balance(alice, aliceGets));
}
else
{
env.require(
Offers(alice, 0), Balance(alice, usd(0)), Balance(carol, carolFunds));
}
}
}
}
void
testRmSmallIncreasedQOffersIOU(FeatureBitset features)
{
@@ -5338,6 +5430,7 @@ public:
testTicketCancelOffer(features);
testRmSmallIncreasedQOffersXRP(features);
testRmSmallIncreasedQOffersIOU(features);
testRmSmallIncreasedQOffersClampNative(features);
testFillOrKill(features);
}
+2 -1
View File
@@ -1859,7 +1859,8 @@ public:
{
// existing owner objects that are outside the v1 SponsorshipTransfer
// object allow-list
Env env{*this, features};
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
Env env{*this, features - featureLendingProtocolV1_2};
Account const alice("alice");
Account const sponsor("sponsor");
env.fund(XRP(10000), alice, sponsor);
File diff suppressed because it is too large. Load diff
@@ -19,11 +19,13 @@
#include <test/jtx/token.h>
#include <test/jtx/trust.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/ledger/helpers/ProposalHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -36,6 +38,7 @@
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/jss.h>
#include <chrono> // IWYU pragma: keep
@@ -44,6 +47,7 @@
#include <functional>
#include <memory>
#include <string>
#include <utility>
#include <vector>
namespace xrpl::test {
@@ -163,7 +167,7 @@ struct TransactionProposalCreate_test : public beast::unit_test::Suite
// Proposals do not nest. This payload also isn't a valid instance of
// TransactionProposalCreate's own template (it lacks Expiration and
// ProposedTransaction), so it fails STTx construction before ever
// reaching our own isProposalTx check.
// reaching our own nesting check.
{
json::Value tx = payload();
tx[jss::TransactionType] = "TransactionProposalCreate";
@@ -189,6 +193,49 @@ struct TransactionProposalCreate_test : public beast::unit_test::Suite
reject(tx, temINVALID);
}
// A Batch inner lacking TransactionType cannot be constructed as an
// STTx, so Create rejects the payload as temMALFORMED before the
// nesting walk runs at all. That inner would otherwise make the walk
// report temINVALID, so this pins the ordering.
{
json::Value missingType;
missingType[jss::Account] = target.human();
missingType[jss::Flags] = tfInnerBatchTxn;
missingType[jss::Sequence] = env.seq(target);
missingType[jss::Fee] = "10";
missingType[jss::SigningPubKey] = "";
reject(
proposal::unsignedBatch(
env,
target,
targetTicketSeq,
tfAllOrNothing,
{
proposal::innerTx(pay(target, bob, XRP(1)), env.seq(target) + 1),
missingType,
}),
temMALFORMED);
}
// The walk rejects such an inner in its own right. Nothing submitted
// through Create can reach that guard — the case above shows STTx
// construction throwing first — so it is checked directly here, which
// also pins it for a caller that does not build an STTx beforehand.
{
STObject inner(sfRawTransaction);
inner.setAccountID(sfAccount, target.id());
STArray rawTxns(sfRawTransactions);
rawTxns.push_back(std::move(inner));
STObject batch(sfGeneric);
batch.setFieldU16(sfTransactionType, ttBATCH);
batch.setFieldArray(sfRawTransactions, rawTxns);
BEAST_EXPECT(!xrpl::proposal::isValidProposalTxnType(batch));
}
// The proposed transaction must be ticket-based: a missing
// TicketSequence, or a live Sequence alongside it, is rejected.
{
File diff suppressed because it is too large. Load diff
@@ -970,10 +970,11 @@ class InvariantsMPT_test : public InvariantsBase
});
}
// LoanSet / VaultWithdraw MayAuthorizeMpt caps (fixCleanup3_4_0):
// LoanSet allows at most two creates and no deletes; VaultWithdraw
// allows at most one of each. Fabricate one extra mutation so a
// too-loose cap would miss these.
// LoanSet / VaultWithdraw MayAuthorizeMpt caps (fixCleanup3_4_0) and
// the LoanBrokerDelete cap (fixCleanup3_5_0): LoanSet allows at most
// two creates and no deletes; VaultWithdraw and LoanBrokerDelete allow
// at most one of each. Fabricate one extra mutation so a too-loose cap
// would miss these.
{
auto const insertHolderTokens =
[](Account const& issuer, Account const& holder, ApplyContext& ac, int n) {
@@ -993,8 +994,8 @@ class InvariantsMPT_test : public InvariantsBase
return true;
};
std::array<std::pair<xrpl::TxType, std::uint8_t>, 2> const createOverCap{
{{ttLOAN_SET, 3}, {ttVAULT_WITHDRAW, 2}},
std::array<std::pair<xrpl::TxType, std::uint8_t>, 3> const createOverCap{
{{ttLOAN_SET, 3}, {ttVAULT_WITHDRAW, 2}, {ttLOAN_BROKER_DELETE, 2}},
};
for (auto const& [txnType, nTokens] : createOverCap)
{
@@ -1016,8 +1017,8 @@ class InvariantsMPT_test : public InvariantsBase
id = mpt.issuanceID();
return true;
};
std::array<std::pair<xrpl::TxType, std::uint8_t>, 2> const deleteOverCap{
{{ttLOAN_SET, 1}, {ttVAULT_WITHDRAW, 2}},
std::array<std::pair<xrpl::TxType, std::uint8_t>, 3> const deleteOverCap{
{{ttLOAN_SET, 1}, {ttVAULT_WITHDRAW, 2}, {ttLOAN_BROKER_DELETE, 2}},
};
for (auto const& [txnType, nTokens] : deleteOverCap)
{
@@ -1,15 +1,19 @@
#include <test/app/invariants/InvariantsBase.h>
#include <test/jtx/AMM.h>
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/amount.h>
#include <test/jtx/fee.h>
#include <test/jtx/mpt.h>
#include <test/jtx/pay.h>
#include <test/jtx/sig.h>
#include <test/jtx/trust.h>
#include <test/jtx/vault.h>
#include <test/unit_test/SuiteJournal.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h>
@@ -28,6 +32,7 @@
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
@@ -37,6 +42,7 @@
#include <xrpl/tx/applySteps.h>
#include <array>
#include <chrono>
#include <cstdint>
#include <functional>
#include <initializer_list>
@@ -825,11 +831,360 @@ class InvariantsPseudoAccount_test : public InvariantsBase
}
}
// Fabricate a credential with the given subject and link it into the
// subject's owner directory, as a transactor bug bypassing the
// CredentialCreate guard would.
static bool
pinCredential(AccountID const& subject, AccountID const& issuer, ApplyContext& ac)
{
Slice const credType{"pinned", 6};
auto const cred = std::make_shared<SLE>(keylet::credential(subject, issuer, credType));
cred->setAccountID(sfSubject, subject);
cred->setAccountID(sfIssuer, issuer);
cred->setFieldVL(sfCredentialType, credType);
auto const page =
ac.view().dirInsert(keylet::ownerDir(subject), cred->key(), describeOwnerDir(subject));
if (!page)
return false;
cred->setFieldU64(sfSubjectNode, *page);
ac.view().insert(cred);
return true;
}
// Link an existing entry into the owner directory of `owner`, as a
// transactor bug that attaches someone else's entry would.
static bool
linkEntry(AccountID const& owner, uint256 const& key, ApplyContext& ac)
{
return ac.view()
.dirInsert(keylet::ownerDir(owner), key, describeOwnerDir(owner))
.has_value();
}
// Pin an object to a pseudo-account. The invariant rejects it with
// `message` only once fixCleanup3_5_0 is enabled.
void
checkPinned(
FeatureBitset features,
std::string const& message,
Precheck const& pin,
Preclose const& setup)
{
bool const enabled = features[fixCleanup3_5_0];
TER const failed = enabled ? TER{tecINVARIANT_FAILED} : TER{tesSUCCESS};
TER const retried = enabled ? TER{tefINVARIANT_FAILED} : TER{tesSUCCESS};
doInvariantCheck(
makeEnv(features),
enabled ? std::vector<std::string>{message} : std::vector<std::string>{},
pin,
XRPAmount{},
STTx{ttACCOUNT_SET, [](STObject&) {}},
{failed, retried},
setup);
}
// The pseudo-account of the vault the broker operates on.
static std::optional<AccountID>
brokerVaultPseudo(Keylet const& brokerKeylet, ApplyContext& ac)
{
auto const sleBroker = ac.view().read(brokerKeylet);
if (!sleBroker)
return std::nullopt;
auto const sleVault = ac.view().read(keylet::vault(sleBroker->at(sfVaultID)));
if (!sleVault)
return std::nullopt;
return sleVault->at(sfAccount);
}
static char const*
cleanupLabel(FeatureBitset features)
{
return features[fixCleanup3_5_0] ? "post-Cleanup3.5" : "pre-Cleanup3.5";
}
void
testVaultOwnership(FeatureBitset features)
{
using namespace jtx;
std::optional<Keylet> vaultKeylet;
Preclose const createVault = [&](Account const& a1, Account const&, Env& env) -> bool {
Vault const vault{env};
auto [tx, k] = vault.create({.owner = a1, .asset = xrpIssue()});
env(tx);
env.close();
vaultKeylet = k;
return true;
};
auto const vaultPseudo = [&](ApplyContext& ac) -> std::optional<AccountID> {
if (!vaultKeylet)
return std::nullopt;
auto const sleVault = ac.view().read(*vaultKeylet);
if (!sleVault)
return std::nullopt;
return sleVault->at(sfAccount);
};
testcase << "vault pseudo-account pinned by a credential, " << cleanupLabel(features);
checkPinned(
features,
"may not own Credential",
[&](Account const&, Account const& a2, ApplyContext& ac) {
auto const pseudo = vaultPseudo(ac);
return pseudo && pinCredential(*pseudo, a2.id(), ac);
},
createVault);
testcase << "vault pseudo-account pinned by a check, " << cleanupLabel(features);
vaultKeylet.reset();
checkPinned(
features,
"may not own Check",
[&](Account const&, Account const& a2, ApplyContext& ac) {
auto const pseudo = vaultPseudo(ac);
if (!pseudo)
return false;
auto const sleAcct = ac.view().peek(keylet::account(a2.id()));
if (!sleAcct)
return false;
auto const check = std::make_shared<SLE>(
keylet::check(a2.id(), SeqProxy::rawSequence((*sleAcct)[sfSequence])));
check->setAccountID(sfAccount, a2.id());
check->setAccountID(sfDestination, *pseudo);
auto const page = ac.view().dirInsert(
keylet::ownerDir(*pseudo), check->key(), describeOwnerDir(*pseudo));
if (!page)
return false;
check->setFieldU64(sfDestinationNode, *page);
ac.view().insert(check);
return true;
},
createVault);
testcase << "vault pseudo-account links a missing object, " << cleanupLabel(features);
vaultKeylet.reset();
checkPinned(
features,
"owner directory links a missing object",
[&](Account const&, Account const& a2, ApplyContext& ac) {
auto const pseudo = vaultPseudo(ac);
if (!pseudo)
return false;
// Link a key with no ledger entry behind it.
auto const missing = keylet::check(a2.id(), SeqProxy::rawSequence(0));
return linkEntry(*pseudo, missing.key, ac);
},
createVault);
testcase << "vault pseudo-account linked to an unrelated trust line, "
<< cleanupLabel(features);
vaultKeylet.reset();
checkPinned(
features,
"may not own RippleState",
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const pseudo = vaultPseudo(ac);
return pseudo && linkEntry(*pseudo, keylet::trustLine(a1, a2["EUR"]).key, ac);
},
[&](Account const& a1, Account const& a2, Env& env) -> bool {
env(trust(a1, a2["EUR"](100)));
return createVault(a1, a2, env);
});
testcase << "vault pseudo-account linked to another vault's loan broker, "
<< cleanupLabel(features);
std::optional<Keylet> broker;
std::optional<Keylet> otherBroker;
checkPinned(
features,
"may not own LoanBroker",
[&](Account const&, Account const&, ApplyContext& ac) {
if (!broker || !otherBroker)
return false;
auto const pseudo = brokerVaultPseudo(*broker, ac);
return pseudo && linkEntry(*pseudo, otherBroker->key, ac);
},
[&, this](Account const& a1, Account const&, Env& env) -> bool {
PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
broker = createLoanBroker(a1, env, xrpAsset);
otherBroker = createLoanBroker(a1, env, xrpAsset);
return BEAST_EXPECT(env.le(*broker) && env.le(*otherBroker));
});
}
void
testLoanBrokerOwnership(FeatureBitset features)
{
using namespace jtx;
testcase << "loan broker pseudo-account pinned by a credential, " << cleanupLabel(features);
std::optional<Keylet> brokerKeylet;
checkPinned(
features,
"may not own Credential",
[&](Account const&, Account const& a2, ApplyContext& ac) {
if (!brokerKeylet)
return false;
auto const sleBroker = ac.view().read(*brokerKeylet);
if (!sleBroker)
return false;
AccountID const pseudo = sleBroker->at(sfAccount);
return pinCredential(pseudo, a2.id(), ac);
},
[&, this](Account const& a1, Account const&, Env& env) -> bool {
PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
brokerKeylet = createLoanBroker(a1, env, xrpAsset);
return brokerKeylet.has_value();
});
testcase << "loan broker pseudo-account linked to another broker's loan, "
<< cleanupLabel(features);
std::optional<Keylet> otherLoan;
checkPinned(
features,
"may not own Loan",
[&](Account const&, Account const&, ApplyContext& ac) {
if (!brokerKeylet || !otherLoan)
return false;
auto const sleBroker = ac.view().read(*brokerKeylet);
return sleBroker && linkEntry(sleBroker->at(sfAccount), otherLoan->key, ac);
},
[&, this](Account const& a1, Account const& a2, Env& env) -> bool {
PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
brokerKeylet = createLoanBroker(a1, env, xrpAsset);
otherLoan = createLoan(a1, a2, env, createLoanBroker(a1, env, xrpAsset));
return otherLoan.has_value();
});
}
// Fund the broker's vault and have `borrower` take a loan from it.
std::optional<Keylet>
createLoan(
jtx::Account const& lender,
jtx::Account const& borrower,
jtx::Env& env,
Keylet const& brokerKeylet)
{
using namespace jtx;
PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
auto const sleBroker = env.le(brokerKeylet);
if (!BEAST_EXPECT(sleBroker))
return std::nullopt;
Vault const vault{env};
env(vault.deposit(
{.depositor = lender, .id = sleBroker->at(sfVaultID), .amount = xrpAsset(100)}));
env.close(std::chrono::seconds{61});
auto const loanKeylet =
keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
env(loan::set(borrower, brokerKeylet.key, xrpAsset(50).value()),
loan::kCounterparty(lender),
Sig(sfCounterpartySignature, lender),
loan::kPaymentInterval(60),
loan::kPaymentTotal(1),
Fee(env.current()->fees().base * 2));
env.close();
if (!BEAST_EXPECT(env.le(loanKeylet)))
return std::nullopt;
return loanKeylet;
}
void
testAMMOwnership(FeatureBitset features)
{
using namespace jtx;
testcase << "AMM pseudo-account pinned by a credential, " << cleanupLabel(features);
std::optional<AccountID> ammAccount;
checkPinned(
features,
"may not own Credential",
[&](Account const&, Account const& a2, ApplyContext& ac) {
return ammAccount && pinCredential(*ammAccount, a2.id(), ac);
},
[&](Account const& a1, Account const&, Env& env) -> bool {
AMM const amm(env, a1, XRP(100), a1["USD"](100));
ammAccount = amm.ammAccount();
return true;
});
testcase << "AMM pseudo-account linked to an unrelated trust line, "
<< cleanupLabel(features);
ammAccount.reset();
checkPinned(
features,
"may not own RippleState",
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
return ammAccount &&
linkEntry(*ammAccount, keylet::trustLine(a2, a1["EUR"]).key, ac);
},
[&](Account const& a1, Account const& a2, Env& env) -> bool {
env(trust(a2, a1["EUR"](100)));
AMM const amm(env, a1, XRP(100), a1["USD"](100));
ammAccount = amm.ammAccount();
return true;
});
}
// Real transactions link only entries that belong to the pseudo-account,
// so the ownership check lets every one of them through.
void
testOwnEntriesAllowed()
{
using namespace jtx;
testcase << "pseudo-accounts own their own entries, post-Cleanup3.5";
Env env{*this, all_};
Account const owner{"owner"};
Account const borrower{"borrower"};
Account const issuer{"issuer"};
env.fund(XRP(100'000), owner, borrower, issuer);
env.close();
// Vault and broker holdings of an IOU and of an MPT, the broker in
// its vault's directory, and the cover the broker holds.
PrettyAsset const iou = issuer["IOU"];
env(trust(owner, iou(1'000)));
env(pay(issuer, owner, iou(1'000)));
MPTTester mptt{env, issuer, kMptInitNoFund};
mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
PrettyAsset const mpt = mptt.issuanceID();
mptt.authorize({.account = owner});
env(pay(issuer, owner, mpt(1'000)));
env.close();
for (auto const& asset : {iou, mpt})
{
auto const broker = createLoanBroker(owner, env, asset);
env(loan_broker::coverDeposit(owner, broker.key, asset(10)));
env.close();
}
// A loan in its broker's directory.
PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
BEAST_EXPECT(createLoan(owner, borrower, env, createLoanBroker(owner, env, xrpAsset)));
// The AMM entry, the pool holdings and the LP token trust lines.
AMM amm(env, issuer, XRP(100), iou(100));
amm.deposit(owner, iou(10));
}
void
run() override
{
testValidPseudoAccounts();
testValidLoanBroker();
testVaultOwnership(all_);
testVaultOwnership(all_ - fixCleanup3_5_0);
testLoanBrokerOwnership(all_);
testLoanBrokerOwnership(all_ - fixCleanup3_5_0);
testAMMOwnership(all_);
testAMMOwnership(all_ - fixCleanup3_5_0);
testOwnEntriesAllowed();
}
};
@@ -63,6 +63,9 @@ namespace xrpl::test {
class InvariantsVault_test : public InvariantsBase
{
FeatureBitset const all_{test::jtx::testableAmendments()};
// testableAmendments() leaves featureLendingProtocolV1_2 off until the
// LoanManage PR; enable it explicitly for FixedPrecision rows.
FeatureBitset const fixedPrecision_{all_ | featureLendingProtocolV1_2};
void
testVault() // NOLINT(readability-function-size)
@@ -1443,8 +1446,11 @@ class InvariantsVault_test : public InvariantsBase
// valid positive dust while moving the posterior AssetsTotal to a much
// finer scale. The dust must be bounded by the former scale rather than
// compared with one unit at the posterior scale.
// CashBasis, not FixedPrecision: AssetsTotal is set independently of
// AssetsAvailable plus the vault's AssetsDeployed, which the FixedPrecision
// sync invariant rejects.
{
Env env{*this, all_ | featureLendingProtocolV1_1};
Env env{*this, (all_ | featureLendingProtocolV1_1) - featureLendingProtocolV1_2};
Account const issuer{"issuer"};
Account const owner{"owner"};
Account const borrower{"borrower"};
@@ -1591,7 +1597,9 @@ class InvariantsVault_test : public InvariantsBase
// (here the neutral ttACCOUNT_SET) is a violation, even while the
// loan still has outstanding obligations: the transaction-type check
// fires before the not-fully-paid-off check.
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
doInvariantCheck(
makeEnv(all_ - featureLendingProtocolV1_2),
{"Loan deleted by a transaction other than LoanDelete"},
eraseLoan,
XRPAmount{},
@@ -1933,6 +1941,59 @@ class InvariantsVault_test : public InvariantsBase
return true;
});
// FixedPrecision-only: a new vault must have AssetsDeployed == 0.
doInvariantCheck(
makeEnv(fixedPrecision_),
{
"created vault must be empty",
"updated zero sized vault must have no AssetsDeployed",
"create operation must not have updated a vault",
},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
auto const sleVault = ac.view().peek(keylet);
if (!sleVault)
return false;
(*sleVault)[sfAssetsDeployed] = Number{1};
ac.view().update(sleVault);
return true;
},
XRPAmount{},
STTx{ttVAULT_CREATE, [](STObject&) {}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
[&](Account const& a1, Account const& a2, Env& env) {
Vault const vault{env};
auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
env(tx);
return true;
});
// FixedPrecision-only: a new vault must have YieldUnrealized == 0.
doInvariantCheck(
makeEnv(fixedPrecision_),
{
"created vault must be empty",
"create operation must not have updated a vault",
},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
auto const sleVault = ac.view().peek(keylet);
if (!sleVault)
return false;
(*sleVault)[sfYieldUnrealized] = Number{1};
ac.view().update(sleVault);
return true;
},
XRPAmount{},
STTx{ttVAULT_CREATE, [](STObject&) {}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
[&](Account const& a1, Account const& a2, Env& env) {
Vault const vault{env};
auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
env(tx);
return true;
});
doInvariantCheck(
{
"created vault must be empty",
@@ -2238,7 +2299,9 @@ class InvariantsVault_test : public InvariantsBase
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseXrp);
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
doInvariantCheck(
makeEnv(all_ - featureLendingProtocolV1_2),
{
"deposit must increase vault balance",
"deposit must decrease depositor balance",
@@ -2338,7 +2401,9 @@ class InvariantsVault_test : public InvariantsBase
precloseXrp,
TxAccount::A2);
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
doInvariantCheck(
makeEnv(all_ - featureLendingProtocolV1_2),
{"deposit and assets outstanding must add up"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const sleA3 = ac.view().peek(keylet::account(a3.id()));
@@ -2362,7 +2427,9 @@ class InvariantsVault_test : public InvariantsBase
precloseXrp,
TxAccount::A2);
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
doInvariantCheck(
makeEnv(all_ - featureLendingProtocolV1_2),
{
"deposit and assets outstanding must add up",
"deposit and assets available must add up",
@@ -2426,7 +2493,9 @@ class InvariantsVault_test : public InvariantsBase
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseXrp);
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
doInvariantCheck(
makeEnv(all_ - featureLendingProtocolV1_2),
{
"withdrawal must change vault and destination balance by equal amount",
"withdrawal must decrease vault balance",
@@ -2523,7 +2592,9 @@ class InvariantsVault_test : public InvariantsBase
precloseXrp,
TxAccount::A2);
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
doInvariantCheck(
makeEnv(all_ - featureLendingProtocolV1_2),
{
"withdrawal and assets outstanding must add up",
"withdrawal and assets available must add up",
@@ -2541,7 +2612,9 @@ class InvariantsVault_test : public InvariantsBase
precloseXrp,
TxAccount::A2);
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
doInvariantCheck(
makeEnv(all_ - featureLendingProtocolV1_2),
{"withdrawal and assets outstanding must add up"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const sleA3 = ac.view().peek(keylet::account(a3.id()));
@@ -2712,7 +2785,9 @@ class InvariantsVault_test : public InvariantsBase
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseMpt);
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
doInvariantCheck(
makeEnv(all_ - featureLendingProtocolV1_2),
{
"clawback must change holder and vault shares by equal amount",
"clawback and assets outstanding must add up",
@@ -3147,7 +3222,8 @@ class InvariantsVault_test : public InvariantsBase
for (auto const withFix : {false, true})
{
FeatureBitset amendments = all_;
// Temporary: FixedPrecision lending is incomplete until the LoanManage PR.
FeatureBitset amendments = all_ - featureLendingProtocolV1_2;
if (!withFix)
amendments = amendments - fixCleanup3_4_0;
@@ -3367,12 +3443,228 @@ class InvariantsVault_test : public InvariantsBase
}
}
static void
addToVaultField(std::shared_ptr<SLE> const& sle, SF_NUMBER const& field, Number const& delta)
{
(*sle)[field] = Number(sle->at(field)) + delta;
}
// Corrupts a vault SLE. The runner updates the SLE afterwards.
using VaultMutation = std::function<bool(ApplyContext&, std::shared_ptr<SLE> const&)>;
struct AssetsDeployedRow
{
char const* name = nullptr;
std::vector<std::string> logs;
VaultMutation mutate;
TxType txType = ttVAULT_SET;
// Set sfVaultID on the mock tx.
bool withVaultId = false;
TER secondTer = tecINVARIANT_FAILED;
};
void
checkXrpVaultRow(AssetsDeployedRow const& row)
{
using namespace test::jtx;
testcase(row.name);
Account const a3{"A3"};
auto const preclose = [&](Account const& a1, Account const& a2, Env& env) -> bool {
env.fund(XRP(1000), a3);
Vault const vault{env};
auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
env(tx);
for (auto const* depositor : {&a1, &a2, &a3})
{
env(vault.deposit({.depositor = *depositor, .id = keylet.key, .amount = XRP(10)}));
}
return true;
};
doInvariantCheck(
makeEnv(fixedPrecision_),
row.logs,
[&](Account const& a1, Account const&, ApplyContext& ac) {
auto const sle =
ac.view().peek(keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())));
if (!sle || !row.mutate(ac, sle))
{
return false;
}
ac.view().update(sle);
return true;
},
XRPAmount{},
STTx{row.txType, [](STObject&) {}},
{tecINVARIANT_FAILED, row.secondTer},
preclose,
TxAccount::A2);
}
void
testVaultAssetsDeployedInvariants()
{
AssetsDeployedRow const xrpRows[] = {
{
.name = "FixedPrecision vault: negative AssetsDeployed",
.logs =
{
"AssetsDeployed must not be negative",
"vault transaction must not change AssetsDeployed",
},
.mutate =
[](ApplyContext&, std::shared_ptr<SLE> const& sle) {
(*sle)[sfAssetsDeployed] = Number{-1};
return true;
},
},
// 1.5 drops is never representable on an XRP vault.
{
.name = "FixedPrecision vault: AssetsDeployed not exactly representable at asset "
"precision",
.logs =
{
"AssetsDeployed must be exactly representable at the vault asset's "
"precision",
"vault transaction must not change AssetsDeployed",
},
.mutate =
[](ApplyContext&, std::shared_ptr<SLE> const& sle) {
(*sle)[sfAssetsDeployed] = Number{15, -1};
return true;
},
},
};
for (auto const& row : xrpRows)
{
checkXrpVaultRow(row);
}
}
// FixedPrecision only: YieldUnrealized must not be negative, and may
// change only for ttLOAN_SET, ttLOAN_PAY and ttLOAN_MANAGE. Both rows use
// a plain (no loan) FixedPrecision vault, mirroring the AssetsDeployed
// vault-only rows above.
void
testVaultYieldUnrealizedInvariants()
{
AssetsDeployedRow const xrpRows[] = {
{
.name = "FixedPrecision vault: negative YieldUnrealized",
.logs =
{
"YieldUnrealized must not be negative",
"vault transaction must not change yield unrealized",
},
.mutate =
[](ApplyContext&, std::shared_ptr<SLE> const& sle) {
(*sle)[sfYieldUnrealized] = Number{-1};
return true;
},
},
{
.name =
"FixedPrecision vault: YieldUnrealized changed by a non-lending transaction",
.logs = {"vault transaction must not change yield unrealized"},
.mutate =
[](ApplyContext&, std::shared_ptr<SLE> const& sle) {
addToVaultField(sle, sfYieldUnrealized, Number{1});
return true;
},
},
};
for (auto const& row : xrpRows)
{
checkXrpVaultRow(row);
}
}
// A FixedPrecision vault's AssetsAvailable is the exact balance; its delta
// must equal the real pseudo-account transfer with no rounding tolerance.
// The stored AssetsTotal is a derived cache and is not compared per
// transaction. Move AssetsAvailable by one drop more than the real
// transfer and expect a rejection.
void
testVaultAvailableOffByOneSubUnit()
{
testcase(
"FixedPrecision vault: AssetsAvailable off by one sub-unit from the pseudo balance "
"is rejected");
using namespace test::jtx;
Keylet vaultKeylet = keylet::vault(uint256{});
auto const preclose = [&](Account const& a1, Account const&, Env& env) -> bool {
Vault const vault{env};
auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
env(tx);
env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = XRP(10)}));
vaultKeylet = keylet;
auto const sle = env.le(vaultKeylet);
return BEAST_EXPECT(sle) && BEAST_EXPECT(sle->isFieldPresent(sfLEVersion));
};
doInvariantCheck(
makeEnv(fixedPrecision_),
{"withdrawal and assets available must add up"},
[&vaultKeylet](Account const& a1, Account const&, ApplyContext& ac) -> bool {
auto const sleVault = ac.view().peek(vaultKeylet);
if (!sleVault)
{
return false;
}
// Real transfer: pseudo account loses 10 drops, depositor gains 10.
auto const slePseudo = ac.view().peek(keylet::account(sleVault->at(sfAccount)));
auto const sleA1 = ac.view().peek(keylet::account(a1.id()));
if (!slePseudo || !sleA1)
{
return false;
}
(*slePseudo)[sfBalance] = *(*slePseudo)[sfBalance] - XRPAmount{10};
(*sleA1)[sfBalance] = *(*sleA1)[sfBalance] + XRPAmount{10};
ac.view().update(slePseudo);
ac.view().update(sleA1);
// Burn 10 shares (1:1 initial price) from depositor and pool.
auto const mptIssuanceID = sleVault->at(sfShareMPTID);
auto const sleShares = ac.view().peek(keylet::mptokenIssuance(mptIssuanceID));
auto const sleA1Token = ac.view().peek(keylet::mptoken(mptIssuanceID, a1.id()));
if (!sleShares || !sleA1Token)
{
return false;
}
sleShares->setFieldU64(
sfOutstandingAmount, sleShares->getFieldU64(sfOutstandingAmount) - 10);
sleA1Token->setFieldU64(sfMPTAmount, sleA1Token->getFieldU64(sfMPTAmount) - 10);
ac.view().update(sleShares);
ac.view().update(sleA1Token);
// 11 drops: one more than the real transfer. AssetsTotal
// follows to keep the sync invariant quiet.
addToVaultField(sleVault, sfAssetsAvailable, Number{-11});
addToVaultField(sleVault, sfAssetsTotal, Number{-11});
ac.view().update(sleVault);
return true;
},
XRPAmount{},
STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
preclose,
TxAccount::A1);
}
void
run() override
{
testVault();
testVaultLossExceedsGap();
testVaultComputeCoarsestScale();
testVaultAssetsDeployedInvariants();
testVaultYieldUnrealizedInvariants();
testVaultAvailableOffByOneSubUnit();
}
};
+16 -12
View File
@@ -837,19 +837,23 @@ public:
{
// Batch rejects Vault and Lending inners before LendingProtocolV1_2.
FeatureBitset const lendingBatch{all_ | featureLendingProtocolV1_2};
for (auto const& features : {lendingBatch, lendingBatch | featureLendingProtocolV1_1})
{
testVaultLifecycle(features);
testArbitrage(features);
testArbitrageRollback(features);
testImpairAndCoverDeposit(features);
testDefaultAndCoverWithdraw(features);
testIndependentVaultChain(features);
testIndependentLoanChain(features);
}
testClosedEndedVaultLifecycle(lendingBatch | featureLendingProtocolV1_1);
testVaultLifecycle(lendingBatch);
testArbitrage(lendingBatch);
testArbitrageRollback(lendingBatch);
testImpairAndCoverDeposit(lendingBatch);
testDefaultAndCoverWithdraw(lendingBatch);
testIndependentVaultChain(lendingBatch);
testIndependentLoanChain(lendingBatch);
testLoanLifecycleOpenEndedVault(lendingBatch);
testLoanLifecycleClosedEndedVault(lendingBatch | featureLendingProtocolV1_1);
// Temporary while the FixedPrecision Vault stack lands: with
// featureLendingProtocolV1_1 these Vaults are FixedPrecision, whose
// lending transactions are enabled in later PRs. Only the Vault-only
// cases run here; the rest are restored in the LoanManage PR.
FeatureBitset const fixedPrecision{lendingBatch | featureLendingProtocolV1_1};
testVaultLifecycle(fixedPrecision);
testIndependentVaultChain(fixedPrecision);
testClosedEndedVaultLifecycle(fixedPrecision);
}
};
Loaded 100 of 176 files, more files were not shown because too many files have changed in this diff. Show more