mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-21 19:18:22 +00:00
Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91985dc320 | ||
|
|
84d980d883 | ||
|
|
791c96faad | ||
|
|
31473e7e6d | ||
|
|
aa70b368b5 | ||
|
|
d3db569b34 | ||
|
|
94d6b7daf8 | ||
|
|
e089abe982 | ||
|
|
33289e882b | ||
|
|
1ed0bccb07 | ||
|
|
1da55e3989 | ||
|
|
166e97e12b | ||
|
|
33e5a6fa7c | ||
|
|
b1f9dc6919 | ||
|
|
38965af055 | ||
|
|
b010d4a563 | ||
|
|
eedd6604d1 | ||
|
|
59ff04b648 | ||
|
|
fcb3ee6425 | ||
|
|
77eacbb52c | ||
|
|
5ce85b2799 | ||
|
|
4538d198e1 | ||
|
|
4c39b05f94 | ||
|
|
c2478f509c | ||
|
|
c908ed7839 | ||
|
|
fc57311c81 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."),
|
||||
};
|
||||
|
||||
@@ -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
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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: "
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+1634
-170
File diff suppressed because it is too large.
Load diff
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
Reference in new issue
Block a user